import { useRef } from 'react'; import { useMemo, useCallback } from 'use-memo-one'; import type { DraggableId, ContextId } from '../../types'; import type { FocusMarshal, Unregister } from './focus-marshal-types'; import { dragHandle as dragHandleAttr } from '../data-attributes'; import useLayoutEffect from '../use-isomorphic-layout-effect'; import findDragHandle from '../get-elements/find-drag-handle'; interface Entry { id: DraggableId; focus: () => void; } type EntryMap = { [id in DraggableId]: Entry; }; export default function useFocusMarshal(contextId: ContextId): FocusMarshal { const entriesRef = useRef({}); const recordRef = useRef(null); const restoreFocusFrameRef = useRef(null); const isMountedRef = useRef(false); const register = useCallback(function register( id: DraggableId, focus: () => void, ): Unregister { const entry: Entry = { id, focus }; entriesRef.current[id] = entry; return function unregister() { const entries: EntryMap = entriesRef.current; const current: Entry = entries[id]; // entry might have been overrided by another registration if (current !== entry) { delete entries[id]; } }; }, []); const tryGiveFocus = useCallback( function tryGiveFocus(tryGiveFocusTo: DraggableId) { const handle: HTMLElement | null = findDragHandle( contextId, tryGiveFocusTo, ); if (handle && handle !== document.activeElement) { handle.focus(); } }, [contextId], ); const tryShiftRecord = useCallback(function tryShiftRecord( previous: DraggableId, redirectTo: DraggableId, ) { if (recordRef.current === previous) { recordRef.current = redirectTo; } }, []); const tryRestoreFocusRecorded = useCallback( function tryRestoreFocusRecorded() { // frame already queued if (restoreFocusFrameRef.current) { return; } // cannot give focus if unmounted // this code path is generally not hit expect for some hot-reloading flows if (!isMountedRef.current) { return; } restoreFocusFrameRef.current = requestAnimationFrame(() => { restoreFocusFrameRef.current = null; const record: DraggableId | null = recordRef.current; if (record) { tryGiveFocus(record); } }); }, [tryGiveFocus], ); const tryRecordFocus = useCallback(function tryRecordFocus(id: DraggableId) { // clear any existing record recordRef.current = null; const focused: Element | null = document.activeElement; // no item focused so it cannot be our item if (!focused) { return; } // focused element is not a drag handle or does not have the right id if (focused.getAttribute(dragHandleAttr.draggableId) !== id) { return; } recordRef.current = id; }, []); useLayoutEffect(() => { isMountedRef.current = true; return function clearFrameOnUnmount() { isMountedRef.current = false; const frameId: AnimationFrameID | null = restoreFocusFrameRef.current; if (frameId) { cancelAnimationFrame(frameId); } }; }, []); const marshal: FocusMarshal = useMemo( () => ({ register, tryRecordFocus, tryRestoreFocusRecorded, tryShiftRecord, }), [register, tryRecordFocus, tryRestoreFocusRecorded, tryShiftRecord], ); return marshal; }