import type { JSX } from 'solid-js'; import { createEffect, createSignal, createUniqueId, For, onCleanup, Show } from 'solid-js'; import { Portal } from 'solid-js/web'; import type { OpportunityMark } from '@/types/Annotations'; import { MARK_LABELS, OPPORTUNITY_MARKS } from '@/types/Annotations'; import { MARK_BG } from './styles'; interface Props { marks: OpportunityMark[]; onToggle: (mark: OpportunityMark) => void; /** Used in the button's accessible name. */ opportunityTitle: string; } const MENU_WIDTH = 200; const GAP = 4; /** * A dropdown of mark checkboxes. Each mark toggles on its own and the menu * stays open, so several can be set in one go. The menu is portalled to * with fixed positioning so the table's scroll container can't clip it. */ export function MarkMenu(props: Props): JSX.Element { const [isOpen, setIsOpen] = createSignal(false); const [pos, setPos] = createSignal({ top: 0, left: 0, isAbove: false }); const menuId = createUniqueId(); let button!: HTMLButtonElement; let menu: HTMLDivElement | undefined; const place = () => { const rect = button.getBoundingClientRect(); const height = menu?.offsetHeight ?? 220; const isAbove = rect.bottom + GAP + height > window.innerHeight && rect.top - GAP - height > 0; const left = Math.min(Math.max(8, rect.right - MENU_WIDTH), window.innerWidth - MENU_WIDTH - 8); setPos({ top: isAbove ? rect.top - GAP - height : rect.bottom + GAP, left, isAbove }); }; const close = (shouldRefocus = false) => { setIsOpen(false); if (shouldRefocus) button.focus(); }; createEffect(() => { if (!isOpen()) return; place(); requestAnimationFrame(() => { place(); // re-measure now the menu has a real height menu?.querySelector('input')?.focus(); }); const onPointer = (e: PointerEvent) => { const target = e.target as Node; if (!button.contains(target) && !menu?.contains(target)) close(); }; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') close(true); }; const onScroll = (e: Event) => { if (!menu?.contains(e.target as Node)) close(); }; document.addEventListener('pointerdown', onPointer); document.addEventListener('keydown', onKey); window.addEventListener('scroll', onScroll, true); window.addEventListener('resize', place); onCleanup(() => { document.removeEventListener('pointerdown', onPointer); document.removeEventListener('keydown', onKey); window.removeEventListener('scroll', onScroll, true); window.removeEventListener('resize', place); }); }); const summary = (): string => props.marks.length === 0 ? 'not marked' : props.marks.map((mark) => MARK_LABELS[mark]).join(', '); return ( <>
{(mark) => ( )}
); }