import { ref, type Ref, unref, watch } from 'vue'; import { getFocusableElements, isActiveElement, isSSR, type TemplateRef, unrefTemplateElement, wrapFocus } from '../util'; import useFocusTrapLock from './useFocusTrapLock'; import useFocusTrapReturn from './useFocusTrapReturn'; export default function (containerRef: TemplateRef, options: UseFocusTrapOptions = {}): void { if (isSSR) { return; } const {disable = ref(false), disableReturn = ref(false), attachTo = null} = options; const enabled = useFocusTrapLock(!unref(disable)); useFocusTrapReturn(containerRef, disableReturn); watch(containerRef, (_, __, onCleanup) => { const container = unrefTemplateElement(containerRef); const attach = attachTo || document; if (enabled.value && container && document.activeElement && !container.contains(document.activeElement) && !container.querySelector('[autofocus]')) { wrapFocus(container, document.activeElement, true); } function onFocusIn(evt: FocusEvent): void { if (!enabled.value || !container) { return; } const newFocusElement = (evt.target as HTMLElement | null) || document.body; if (container.contains(newFocusElement)) { return; } evt.preventDefault(); evt.stopImmediatePropagation(); wrapFocus(container, newFocusElement); } function onFocusOut(evt: FocusEvent): void { if (!enabled.value || !container) { return; } if (!evt.relatedTarget || evt.relatedTarget === document.body) { evt.preventDefault(); container.focus(); } const newFocusElement = (evt.target as HTMLElement | null) || document.body; if (container.contains(newFocusElement)) { return; } wrapFocus(container, newFocusElement); } attach.addEventListener('focusin', onFocusIn as EventListener, {capture: true}); attach.addEventListener('focusout', onFocusOut as EventListener, {capture: true}); if (container) { const autofocusElement = container.querySelector('[autofocus]'); if (autofocusElement) { autofocusElement.focus(); } else { const elements = getFocusableElements(container); const isActiveIndex = elements.findIndex(elm => isActiveElement(elm) && !elm.hasAttribute('aria-disabled')); const notDisabledIndex = elements.findIndex(elm => !elm.hasAttribute('aria-disabled')); let element = elements[0]; if (isActiveIndex > -1) { element = elements[isActiveIndex]; } else if (notDisabledIndex > -1) { element = elements[notDisabledIndex]; } if (element) { element.focus(); } else { container.focus(); } } } onCleanup(() => { attach.removeEventListener('focusin', onFocusIn as EventListener); attach.removeEventListener('focusout', onFocusOut as EventListener); }); }, {immediate: true}); watch(disable, disabled => { const container = unrefTemplateElement(containerRef); enabled.value = !disabled; if (disabled || !container) { return; } const elements = getFocusableElements(container as HTMLElement); if (elements.includes(document.activeElement as HTMLElement)) { return; } elements[0]?.focus(); }, {immediate: true}); } type UseFocusTrapOptions = { attachTo?: HTMLElement | Document; disable?: Ref; disableReturn?: Ref; };