diff --git a/src/lib/components/ContextMenu.svelte b/src/lib/components/ContextMenu.svelte index 36dc812..8acaa85 100644 --- a/src/lib/components/ContextMenu.svelte +++ b/src/lib/components/ContextMenu.svelte @@ -12,14 +12,42 @@ } = $props(); let open = $state(false); - let menuEl: HTMLDivElement | undefined = $state(undefined); + let triggerEl: HTMLButtonElement | undefined = $state(undefined); + let dropdownEl: HTMLDivElement | undefined = $state(undefined); + let pos = $state({ top: 0, right: 0 }); + + // Position the dropdown relative to the trigger. It's rendered as a body + // portal (see `portal` below) so it escapes the per-card stacking context. + function reposition() { + if (!open || !triggerEl) return; + const r = triggerEl.getBoundingClientRect(); + // Anchor against the viewport content width (clientWidth excludes the + // scrollbar) so a fixed `right` offset lines the dropdown up with the + // trigger's right edge; window.innerWidth would shift it left by the + // scrollbar width. + pos = { top: r.bottom + 4, right: document.documentElement.clientWidth - r.right }; + } function toggle(e: MouseEvent) { e.stopPropagation(); e.preventDefault(); open = !open; + if (open) reposition(); } + // Keep the fixed-positioned dropdown pinned to its trigger while open, then + // drop the listeners on close so a list of closed menus does no scroll work. + // Capture-phase scroll also catches nested scroll containers, not just window. + $effect(() => { + if (!open) return; + window.addEventListener('scroll', reposition, true); + window.addEventListener('resize', reposition); + return () => { + window.removeEventListener('scroll', reposition, true); + window.removeEventListener('resize', reposition); + }; + }); + function select(action: string, e: MouseEvent) { e.stopPropagation(); e.preventDefault(); @@ -28,18 +56,28 @@ } function handleClickOutside(e: MouseEvent) { - if (open && menuEl && !menuEl.contains(e.target as Node)) { - open = false; - } + if (!open) return; + const target = e.target as Node; + if (triggerEl?.contains(target) || dropdownEl?.contains(target)) return; + open = false; + } + + function portal(node: HTMLElement) { + document.body.appendChild(node); + return { + destroy() { + node.remove(); + } + }; } -
- +
+ {#if open} -