UNPKG

@oruga-ui/oruga-next

Version:

UI components for Vue.js and CSS framework agnostic

106 lines (92 loc) 3.78 kB
/***************************** * vue v-trap-focus directive *****************************/ import type { DirectiveHook, ObjectDirective } from "vue"; function findFocusable(element: HTMLElement): NodeListOf<HTMLElement> | null { if (!element) return null; return element.querySelectorAll(`a[href]:not([tabindex="-1"]), area[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex]:not([tabindex="-1"]):not([disabled]), *[contenteditable]`); } export function useTrapFocus(): { /** vue directive - trap focus on the current element */ vTrapFocus: ObjectDirective<HTMLElement>; } { /** keydown event, which compares event target with trap element */ let onKeyDown: ((event: KeyboardEvent) => void) | null = null; function applyHandler(el: HTMLElement, value: boolean): void { if (value) { // move focus inside the root element el.focus({ preventScroll: true }); // set keydown event listener if (typeof onKeyDown === "function") el.addEventListener("keydown", onKeyDown); } else { // remove keydown event listener if (typeof onKeyDown === "function") el.removeEventListener("keydown", onKeyDown); } } const onMounted: DirectiveHook<HTMLElement> = (el, { value }) => { // create onKeyDown event listener onKeyDown = (event: KeyboardEvent): void => { const target = event.target as HTMLElement; if (!target) return; // Need to get focusable each time since it can change between key events // ex. changing month in a datepicker const focusable = findFocusable(el); if (!focusable?.length) { event.preventDefault(); return; } const firstFocusable = focusable[0]; const lastFocusable = focusable[focusable.length - 1]; if ( target === firstFocusable && event.shiftKey && event.key === "Tab" ) { // prevent moving focus outside by setting the focus to last focusable element event.preventDefault(); lastFocusable.focus(); } else if ( target === lastFocusable && !event.shiftKey && event.key === "Tab" ) { // prevent moving focus outside by setting the focus to first focusable element event.preventDefault(); firstFocusable.focus(); } }; // apply handler when binding value is already true if (value) applyHandler(el, value); }; /** cleanup on beforeUnmount */ const onBeforeUnmount: DirectiveHook<HTMLElement> = (el) => { // remove handler applyHandler(el, false); onKeyDown = null; }; const onUpdate: DirectiveHook<HTMLElement> = (el, { value, oldValue }) => { // check if binding value has changed if (value !== oldValue) // update handler based on binding value applyHandler(el, value); }; return { vTrapFocus: { mounted: onMounted, beforeUnmount: onBeforeUnmount, updated: onUpdate, }, }; }