import { createContext, useCallback, useContext, useMemo, useRef, useState, type MouseEvent, type ReactNode, type RefObject, } from "react"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { useTaskManagementFlow } from "@/hooks/use-task-management-flow"; import { TaskMenuButton } from "@/components/task/task-item-menu-button"; import { TaskManagementSurface } from "@/components/task/task-management-surface"; type Entry = { taskId: string | null; open: (taskId: string, trigger: HTMLElement, point?: { x: number; y: number }) => void; }; const ActionsContext = createContext(null); function visibleTrigger(board: HTMLDivElement | null, preferred: HTMLElement | null) { const bounds = board?.getBoundingClientRect(); const visible = (element: HTMLElement) => { const rect = element.getBoundingClientRect(); return ( rect.width > 0 && rect.left >= (bounds?.left ?? 0) && rect.right <= (bounds?.right ?? window.innerWidth) ); }; if (preferred?.isConnected && visible(preferred)) return preferred; return ( Array.from( board?.querySelectorAll("[data-thread-task-menu] button") ?? [], ).find(visible) ?? null ); } export function ThreadTaskActionsProvider({ children, boardRef, }: { children: ReactNode; boardRef: RefObject; }) { const flow = useTaskManagementFlow(); const origin = useRef(null); const fallback = useRef(null); const [point, setPoint] = useState({ x: 0, y: 0 }); const focusReturnRef = useMemo( () => ({ get current() { return visibleTrigger(boardRef.current, origin.current) ?? fallback.current; }, }), [boardRef], ); const restoreFocus = useCallback(() => { const active = document.activeElement; if ( document.querySelector( '[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"], [role="menu"][data-state="open"]', ) ) return; if ( active instanceof HTMLElement && active !== document.body && active !== origin.current && !active.closest('[data-state="closed"]') && active !== fallback.current ) return; focusReturnRef.current?.focus({ preventScroll: true }); }, [focusReturnRef]); const open = (taskId: string, trigger: HTMLElement, coordinates?: { x: number; y: number }) => { origin.current = trigger; const bounds = trigger.getBoundingClientRect(); setPoint(coordinates ?? { x: bounds.left, y: bounds.bottom }); flow.open(taskId); }; return (
{children}
{flow.identity && ( )}
); } export function useThreadTaskContextMenu(taskId: string) { const actions = useContext(ActionsContext); const { isMobile, isFinePointer } = useResponsiveBreakpoint(); return (event: MouseEvent) => { if (isMobile || !isFinePointer || !actions) return; if ( (event.target as Element).closest( 'button, a, input, textarea, [contenteditable="true"], [role="combobox"]', ) ) return; event.preventDefault(); const trigger = event.currentTarget.querySelector( "[data-thread-task-menu] button", ); if (trigger) actions.open(taskId, trigger, { x: event.clientX, y: event.clientY }); }; } export function ThreadTaskMenuButton({ taskId }: { taskId: string }) { const actions = useContext(ActionsContext); const { isMobile, isFinePointer } = useResponsiveBreakpoint(); const touch = isMobile || !isFinePointer; return ( actions?.open(taskId, event.currentTarget)} /> ); }