fix(album): contain Artist Biography modal scroll within frame (#734)

* fix(album): contain Artist Biography modal scroll within frame

The modal was rendered inside the album page tree, where an ancestor
broke `position: fixed` so the overlay scrolled with the page instead
of pinning to the viewport. Long bios also pushed the modal past the
viewport entirely.

- Render `BioModal` via `createPortal(document.body)` so no ancestor's
  transform/filter/backdrop-filter can break fixed positioning.
- New `.modal-content.bio-modal` variant: flex column, `overflow:
  hidden`. Title + close button stay pinned, only `.bio-modal-body`
  scrolls. The existing `max-height: 80vh` on `.modal-content` now
  reliably caps the modal to the visible viewport.

Reported by zunoz on Discord.

* docs(changelog): note Artist Biography modal scroll fix (#734)
This commit is contained in:
Frank Stellmacher
2026-05-16 13:22:59 +02:00
committed by GitHub
parent 7f9b5bd57d
commit 2233e8fb91
3 changed files with 30 additions and 4 deletions
+7
View File
@@ -565,6 +565,13 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa
* The Album header showed an **Artist Bio** button on every album, but when the album-artist label is **"Various Artists"**, **"Various"**, **"VA"** or a language equivalent there is no single artist to fetch a bio for and the button opened an empty modal. Both the mobile icon and the desktop button are now hidden when the album-artist label matches that compilation heuristic.
### Album header — Artist Biography modal stays in viewport and scrolls internally
**By [@Psychotoxical](https://github.com/Psychotoxical), thanks to zunoz for the report on the Psysonic Discord, PR [#734](https://github.com/Psychotoxical/psysonic/pull/734)**
* The **Artist Biography** modal lived under the album page tree, where an ancestor broke **`position: fixed`** on the overlay — opening a long bio scrolled the whole page instead of staying pinned to the viewport, and the modal itself stretched past the visible area. It now mounts via **`createPortal(..., document.body)`** (same approach as **`RadioEditModal`** / **`CoverLightbox`**), so the overlay always pins to the viewport.
* A new **`.modal-content.bio-modal`** variant turns the modal into a flex column with **`overflow: hidden`** and an inner **`.bio-modal-body`** that handles the scrolling. The existing **`max-height: 80vh`** cap is now honored, and the title + close button stay pinned while the bio scrolls.
## [1.45.0] - 2026-05-04
## Added
+8 -4
View File
@@ -1,6 +1,7 @@
import { buildCoverArtUrl } from '../api/subsonicStreamUrl';
import type { EntityRatingSupportLevel, SubsonicOpenArtistRef, SubsonicSong } from '../api/subsonicTypes';
import React, { useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { Play, Heart, ExternalLink, X, ChevronLeft, Download, ListPlus, HardDriveDownload, Share2, Highlighter, Loader2, Shuffle } from 'lucide-react';
import CachedImage from './CachedImage';
@@ -35,14 +36,17 @@ function isVariousArtistsLabel(name: string | undefined | null): boolean {
function BioModal({ bio, onClose }: { bio: string; onClose: () => void }) {
const { t } = useTranslation();
return (
return createPortal(
<div className="modal-overlay" onClick={onClose} role="dialog" aria-modal="true" aria-label={t('albumDetail.bioModal')}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-content bio-modal" onClick={e => e.stopPropagation()}>
<button className="modal-close" onClick={onClose} aria-label={t('albumDetail.bioClose')}><X size={18} /></button>
<h3 className="modal-title">{t('albumDetail.bioModal')}</h3>
<div className="artist-bio" dangerouslySetInnerHTML={{ __html: sanitizeHtml(bio) }} data-selectable />
<div className="bio-modal-body">
<div className="artist-bio" dangerouslySetInnerHTML={{ __html: sanitizeHtml(bio) }} data-selectable />
</div>
</div>
</div>
</div>,
document.body
);
}
+15
View File
@@ -34,6 +34,21 @@
box-shadow: var(--shadow-lg);
}
/* Bio modal: keep title + close visible, scroll only the bio body. */
.modal-content.bio-modal {
display: flex;
flex-direction: column;
overflow: hidden;
padding-right: var(--space-6);
}
.bio-modal-body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding-right: var(--space-2);
}
/* Modal CSV Import - siempre centrado */
.modal-overlay--csv {
position: fixed !important;