Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/features/playlists.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 13 additions & 4 deletions src/components/common/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
4 changes: 3 additions & 1 deletion src/components/common/TrackContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ export function TrackContextMenu({
icon={<Plus size={14} />}
label={t("trackActions.addToPlaylist")}
>
<div className="max-h-64 overflow-y-auto">
{/* `overscroll-contain`: reaching the end of the list must not
scroll the page behind it, which closes the menu. */}
<div className="max-h-64 overflow-y-auto overscroll-contain">
{playlists.length === 0 ? (
<div className="px-3 py-2 text-xs text-zinc-400">
{t("trackActions.noPlaylists")}
Expand Down
Loading