diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx
index af6cf655..e8a4824d 100644
--- a/src/components/ContextMenu.tsx
+++ b/src/components/ContextMenu.tsx
@@ -1089,6 +1089,22 @@ export default function ContextMenu() {
});
}, [contextMenu.isOpen]);
+ // Outside-click closes the menu without occluding the underlying UI. The
+ // previous implementation rendered a transparent fullscreen backdrop, which
+ // also blocked right-clicks from reaching elements *under* it — so users
+ // couldn't reposition the menu by right-clicking another row.
+ useEffect(() => {
+ if (!contextMenu.isOpen) return;
+ const handler = (e: MouseEvent) => {
+ const target = e.target as Node | null;
+ if (!target) return;
+ if (menuRef.current?.contains(target)) return;
+ closeContextMenu();
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, [contextMenu.isOpen, closeContextMenu]);
+
useEffect(() => {
if (!pendingSubmenuKeyboardFocus || !playlistSubmenuOpen) return;
let cancelled = false;
@@ -1417,11 +1433,6 @@ export default function ContextMenu() {
return (
<>
- {/* Transparent backdrop — catches all outside clicks cleanly, preventing freeze */}
-
closeContextMenu()}
- />
void, ms: number): (q: string) => void {
let timer: ReturnType
;
@@ -23,7 +24,11 @@ export default function LiveSearch() {
const [loading, setLoading] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const navigate = useNavigate();
- const playTrack = usePlayerStore(state => state.playTrack);
+ const enqueue = usePlayerStore(state => state.enqueue);
+ const openContextMenu = usePlayerStore(state => state.openContextMenu);
+ const ctxIsOpen = usePlayerStore(state => state.contextMenu.isOpen);
+ const ctxItemId = usePlayerStore(state => (state.contextMenu.item as { id?: string } | null)?.id);
+ const ctxType = usePlayerStore(state => state.contextMenu.type);
const ref = useRef(null);
const dropdownRef = useRef(null);
const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion);
@@ -45,14 +50,19 @@ export default function LiveSearch() {
useEffect(() => { doSearch(query); setActiveIndex(-1); }, [query, doSearch]);
- // Close on click outside
+ // Close on click outside — but stay open while a song context menu is up.
+ // The CM renders a fullscreen transparent backdrop (z-index 998) above the
+ // dropdown, so any mousedown — including a second right-click on another
+ // row — would otherwise hit the backdrop and trip this handler, yanking the
+ // dropdown closed mid-interaction.
useEffect(() => {
const handler = (e: MouseEvent) => {
+ if (ctxIsOpen) return;
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
- }, []);
+ }, [ctxIsOpen]);
const hasResults = results && (results.artists.length || results.albums.length || results.songs.length);
@@ -61,7 +71,9 @@ export default function LiveSearch() {
...(results.artists.map(a => ({ id: a.id, action: () => { navigate(`/artist/${a.id}`); setOpen(false); setQuery(''); } }))),
...(results.albums.map(a => ({ id: a.id, action: () => { navigate(`/album/${a.id}`); setOpen(false); setQuery(''); } }))),
...(results.songs.map(s => ({ id: s.id, action: () => {
- playTrack(songToTrack(s));
+ const track = songToTrack(s);
+ enqueue([track]);
+ showToast(t('search.addedToQueueToast', { title: track.title }), 2200, 'info');
setOpen(false); setQuery('');
}}))),
] : [];
@@ -191,12 +203,21 @@ export default function LiveSearch() {
{t('search.songs')}
{results.songs.map(s => {
const i = idx++;
+ const isCtxActive = ctxIsOpen && ctxType === 'song' && ctxItemId === s.id;
return (
-