fix(genre-filter): popup uses CSS max-height for smart positioning

The collision check in positionPopup was reading the popup's measured
rect right after mount, but the popup grows after that runs (the
genre tree fills in asynchronously). The first measurement reported a
height much smaller than the final ~500px / ~700px max-heights from
CSS, so the "would I overflow the bottom?" branch evaluated false even
when the final popup didn't fit. Result: bottom of the popup got clipped
by the window edge (visible in the Discover -> Filter by genre flow on
short windows).

Use the CSS max-heights (530/630 already used for width) as the
collision target instead. If the down-placement still overflows, try
flipping above the trigger; if neither fits (very small window), clamp
to the bottom padding and let the popup's own internal scroll handle
the overflow.

cc
This commit is contained in:
vicrodh committed 2026-04-15 13:40:58 -06:00
1 parent 6049240640
commit 824b577dc2
2 files changed
+22 -8

No files matched your search

@@ -64,6 +64,7 @@
<li>Editor's Picks tab cleanup: removed redundant "Album of the Week" and "Qobuzissime" ribbons on cards inside their own eponymous sections</li>
<li>Playlist context menu in the sidebar now flips above the cursor when it would otherwise fall below the window</li>
<li>Sort submenu in the sidebar playlists menu no longer gets clipped by its parent dropdown</li>
<li>Genre filter popup uses its CSS max-height for collision detection so it flips above the trigger or clamps to the viewport instead of overflowing the bottom of the window</li>
<li>DAC Setup Wizard icon contrast fixed for light accent themes</li>
<li>Hardening to audio request signing (internal)</li>
</ul>
+21 -8
View File
@@ -92,26 +92,39 @@
if (!anchorEl || !popupEl) return;
const anchorRect = anchorEl.getBoundingClientRect();
// Use expected width based on expanded state (CSS values)
// Use expected dimensions from CSS instead of the measured rect: the
// popup grows after this runs (the genre tree fills in async) and
// measuring once on mount gives a too-small height, which left the
// collision check thinking it fit when it actually overflowed.
const popupWidth = showAllGenres ? 630 : 530;
const popupRect = popupEl.getBoundingClientRect();
const popupMaxHeight = showAllGenres ? 700 : 500;
const pad = 8;
let left: number;
let top = anchorRect.bottom + 8;
let top = anchorRect.bottom + pad;
if (align === 'right') {
left = anchorRect.left;
if (left + popupWidth > window.innerWidth - 8) {
left = window.innerWidth - popupWidth - 8;
if (left + popupWidth > window.innerWidth - pad) {
left = window.innerWidth - popupWidth - pad;
}
} else {
// Align right edge of popup with right edge of anchor (extends to the left)
left = anchorRect.right - popupWidth;
if (left < 8) left = 8;
if (left < pad) left = pad;
}
if (top + popupRect.height > window.innerHeight - 8) {
top = anchorRect.top - popupRect.height - 8;
if (top + popupMaxHeight > window.innerHeight - pad) {
// Try flipping above the anchor first.
const flipped = anchorRect.top - popupMaxHeight - pad;
if (flipped >= pad) {
top = flipped;
} else {
// Not enough room either way (small window): clamp so the popup
// sits flush with the bottom padding; its internal scroll handles
// the overflow.
top = Math.max(pad, window.innerHeight - popupMaxHeight - pad);
}
}
popupStyle = `left: ${left}px; top: ${top}px;`;