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={