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 ( -