import { useEffect, useRef } from 'react'; import type { RefObject } from 'react'; import { DEFAULT_VIEWER_SHORTCUTS, getKeyboardEventTarget, matchViewerShortcut, } from '../utils/viewerKeyboardShortcuts'; import type { ViewerCommandId, ViewerShortcutChord, ViewerShortcutDefinition, } from '../utils/viewerKeyboardShortcuts'; export interface UseViewerKeyboardShortcutsOptions { readonly enabled?: boolean; readonly shortcuts?: readonly ViewerShortcutDefinition[]; readonly target?: Window | Document | HTMLElement | null; readonly scopeRef?: RefObject; readonly requireFocusWithin?: boolean; readonly ignoreEditableTargets?: boolean; readonly ignoreInteractiveTargets?: boolean; readonly capture?: boolean; readonly onCommand: ( command: ViewerCommandId, event: KeyboardEvent, definition: ViewerShortcutDefinition, chord: ViewerShortcutChord, ) => void; } export function useViewerKeyboardShortcuts({ enabled = true, shortcuts = DEFAULT_VIEWER_SHORTCUTS, target, scopeRef, requireFocusWithin = false, ignoreEditableTargets = true, ignoreInteractiveTargets = true, capture = false, onCommand, }: UseViewerKeyboardShortcutsOptions): readonly ViewerShortcutDefinition[] { const onCommandRef = useRef(onCommand); const shortcutsRef = useRef(shortcuts); useEffect(() => { onCommandRef.current = onCommand; }, [onCommand]); useEffect(() => { shortcutsRef.current = shortcuts; }, [shortcuts]); useEffect(() => { if (!enabled || typeof window === 'undefined') { return undefined; } const listenerTarget = target ?? window; const handleKeyDown = (event: KeyboardEvent) => { if (requireFocusWithin) { const scope = scopeRef?.current; if (!scope || !isEventWithinScope(event, scope)) { return; } } const match = matchViewerShortcut(event, shortcutsRef.current, { ignoreEditableTargets, ignoreInteractiveTargets, }); if (!match) { return; } if (match.definition.preventDefault !== false) { event.preventDefault(); } onCommandRef.current(match.command, event, match.definition, match.chord); }; listenerTarget.addEventListener('keydown', handleKeyDown, capture); return () => { listenerTarget.removeEventListener('keydown', handleKeyDown, capture); }; }, [ capture, enabled, ignoreEditableTargets, ignoreInteractiveTargets, requireFocusWithin, scopeRef, target, ]); return shortcuts; } function isEventWithinScope(event: KeyboardEvent, scope: HTMLElement): boolean { const path = typeof event.composedPath === 'function' ? event.composedPath() : undefined; if (path?.includes(scope)) { return true; } const target = getKeyboardEventTarget(event); if (typeof Node !== 'undefined' && target instanceof Node && scope.contains(target)) { return true; } const activeElement = scope.ownerDocument.activeElement; return Boolean( activeElement && typeof Node !== 'undefined' && activeElement instanceof Node && scope.contains(activeElement), ); }