diff --git a/docs/features/playlists.md b/docs/features/playlists.md index 80415d1c..5fcd3826 100644 --- a/docs/features/playlists.md +++ b/docs/features/playlists.md @@ -23,7 +23,7 @@ The sidebar's two pinned rows — Liked tracks and Recently played — open the A **server** playlist gets one item, Open. Every other action takes a local rowid it does not have, and its playback runs through the view's own remote path rather than `player_play_tracks` — so the alternative was a menu with every item greyed out, which is worse than the inert right-click this replaced. -The grid is virtualized, so the menu state lives in [`PlaylistGrid`](../../src/components/views/library/PlaylistGrid.tsx) rather than in a card — a card can unmount under an open menu. `ContextMenu` portals to `body` and closes on scroll, so that unmount is harmless. +The grid is virtualized, so the menu state lives in [`PlaylistGrid`](../../src/components/views/library/PlaylistGrid.tsx) rather than in a card — a card can unmount under an open menu. `ContextMenu` portals to `body` and closes when the page scrolls, so that unmount is harmless. A scroll inside the menu itself — the playlist list of "Add to a playlist" — leaves it open. ## Reordering diff --git a/src/components/common/ContextMenu.tsx b/src/components/common/ContextMenu.tsx index 00c9761f..ed77cf16 100644 --- a/src/components/common/ContextMenu.tsx +++ b/src/components/common/ContextMenu.tsx @@ -178,18 +178,27 @@ export function ContextMenu({ if (target?.closest("[data-context-menu]")) return; onClose(); }; + // Close when the page scrolls — keeps the menu glued to its anchor + // without chasing the page. Not when the scroll is inside the menu + // itself: the listener captures every scroll, and the playlist + // submenu's own list closed the menu the moment it moved. + const onScroll = (e: Event) => { + const target = e.target; + if (target instanceof Element && target.closest("[data-context-menu]")) { + return; + } + onClose(); + }; // `mousedown` instead of `click` so dragging doesn't accidentally // dismiss before the user releases. window.addEventListener("keydown", onKey); window.addEventListener("mousedown", onMouseDown); - // Close on scroll — keeps the menu glued to its anchor without - // chasing the page. - window.addEventListener("scroll", onClose, true); + window.addEventListener("scroll", onScroll, true); window.addEventListener("resize", onClose); return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("mousedown", onMouseDown); - window.removeEventListener("scroll", onClose, true); + window.removeEventListener("scroll", onScroll, true); window.removeEventListener("resize", onClose); }; }, [onClose, menuItems]); diff --git a/src/components/common/TrackContextMenu.tsx b/src/components/common/TrackContextMenu.tsx index 521259cf..d550b012 100644 --- a/src/components/common/TrackContextMenu.tsx +++ b/src/components/common/TrackContextMenu.tsx @@ -139,7 +139,9 @@ export function TrackContextMenu({ icon={} label={t("trackActions.addToPlaylist")} > -
+ {/* `overscroll-contain`: reaching the end of the list must not + scroll the page behind it, which closes the menu. */} +
{playlists.length === 0 ? (
{t("trackActions.noPlaylists")}