UNPKG

arrowtab

Version:

Use arrow keys to "tab" between focusable elements

108 lines 4.01 kB
import { activateDebugMode, deactivateDebugMode, isInDebugMode, } from './debugMode.js'; import { setHistoryCandidate, startAutoDetectHistory } from './focusHistory.js'; import { getFocusable } from './getFocusable.js'; import { hasDisabledKeys } from './hasDisabledKeys.js'; import { hasTextSelection } from './hasTextSelection.js'; import { preventNativeArrowKeyPresses } from './preventNativeArrowKeyPresses.js'; import { getByGrid } from './strategies.js'; // TODO: Options // modifierKey: 'ctrlKey', // onlyWithModifierKey: false, // TODO: Better name // strategy: arrowTab.XWalkEuclidean, // selector: undefined, // elementFilter: () => boolean, // getElementPosition: (element) => { x: number, y: number }, // eventTriggerName: 'keydown' | 'keyup', // earlyReturn: (event) => boolean, // TODO: Better name // TODO: Check if tab focuses the same elements as arrowTab // TODO: select, details, summary, iframe // TODO: Investigate links that don't show focus outline sometimes export const initArrowTab = ({ debug = false, autoDetectHistory = false, } = {}) => { const onKeyDown = (event) => { if (event.key === 'Escape' && isInDebugMode()) { deactivateDebugMode(); return; } if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight' && event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { return; } // TODO: Use modifierKey if (event.shiftKey) { return; } const activeElement = document.activeElement; if (!activeElement) { if (debug) { console.log('no active element'); } return; } if (hasDisabledKeys({ event, activeElement })) { if (debug) { console.log('arrow keys are disabled'); } return; } preventNativeArrowKeyPresses({ event, activeElement }); const nothingHasFocus = activeElement === document.body; if (nothingHasFocus) { if (debug) { console.log('nothing has focus. using first focusable'); } const firstFocusable = getFocusable()?.[0]; if (firstFocusable instanceof HTMLElement) { firstFocusable.focus(); return; } } if (hasTextSelection({ activeElement, event })) { if (debug) { console.log('element has text selection'); } return; } const allFocusable = getFocusable(); const withoutActiveElement = allFocusable.filter((element) => element !== activeElement); const sorted = getByGrid({ focusableElements: withoutActiveElement, activeElement, event, }); const withinReach = sorted.filter(({ withinReach }) => withinReach); if (debug && event.ctrlKey) { if (isInDebugMode()) { deactivateDebugMode(); } activateDebugMode({ focusableElements: sorted }); return; } let nearest = withinReach.at(0); if (debug) { console.log('found nearest element to select', nearest); } if (!nearest) { return; } if (nearest.element instanceof HTMLElement) { if (debug) { console.log('focusing element', nearest); } nearest.element.focus(); setHistoryCandidate({ element: nearest.element }); } }; document.addEventListener('keydown', onKeyDown, { capture: true }); const { cleanup: cleanupAutoDetectHistory } = startAutoDetectHistory({ active: autoDetectHistory, }); return { cleanup: () => { document.removeEventListener('keydown', onKeyDown, { capture: true }); cleanupAutoDetectHistory(); }, }; }; //# sourceMappingURL=initArrowTab.js.map