UNPKG

@vuesax-alpha/nightly

Version:
254 lines (251 loc) • 9.29 kB
import { defineComponent, ref, provide, watch, unref, nextTick, onMounted, onBeforeUnmount, renderSlot } from 'vue'; import { isNil } from 'lodash-unified'; import '../../../constants/index.mjs'; import '../../../hooks/index.mjs'; import '../../../utils/index.mjs'; import '../../../tokens/index.mjs'; import { focusTrapProps, focusTrapEmits } from './focus-trap.mjs'; import { useFocusReason, getEdges, createFocusOutPreventedEvent, tryFocus, focusableStack, focusFirstDescendant, obtainAllFocusableElements, isFocusCausedByUserEvent } from './utils.mjs'; import _export_sfc from '../../../_virtual/plugin-vue_export-helper.mjs'; import { useEscapeKeydown } from '../../../hooks/use-escape-keydown/index.mjs'; import { EVENT_CODE } from '../../../constants/aria.mjs'; import { focusTrapInjectionKey, onTrapFocusEvent, onReleaseFocusEvent, focusAfterTrapped, focusAfterTrappedOpts, focusAfterReleased } from '../../../tokens/focus-trap.mjs'; import { isString } from '@vue/shared'; const __default__ = defineComponent({ name: "VsFocusTrap", inheritAttrs: false }); const _sfc_main = defineComponent({ ...__default__, props: focusTrapProps, emits: focusTrapEmits, setup(__props, { emit }) { const props = __props; const forwardRef = ref(); let lastFocusBeforeTrapped = null; let lastFocusAfterTrapped = null; const { focusReason } = useFocusReason(); useEscapeKeydown((event) => { if (props.trapped && !focusLayer.paused) { emit("releaseRequested", event); } }); const focusLayer = { paused: false, pause() { this.paused = true; }, resume() { this.paused = false; } }; const onKeydown = (e) => { if (!props.loop && !props.trapped) return; if (focusLayer.paused) return; const { key, altKey, ctrlKey, metaKey, currentTarget, shiftKey } = e; const { loop } = props; const isTabbing = key === EVENT_CODE.tab && !altKey && !ctrlKey && !metaKey; const currentFocusingEl = document.activeElement; if (isTabbing && currentFocusingEl) { const container = currentTarget; const [first, last] = getEdges(container); const isTabbable = first && last; if (!isTabbable) { if (currentFocusingEl === container) { const focusoutPreventedEvent = createFocusOutPreventedEvent({ focusReason: focusReason.value }); emit("focusoutPrevented", focusoutPreventedEvent); if (!focusoutPreventedEvent.defaultPrevented) { e.preventDefault(); } } } else { if (!shiftKey && currentFocusingEl === last) { const focusoutPreventedEvent = createFocusOutPreventedEvent({ focusReason: focusReason.value }); emit("focusoutPrevented", focusoutPreventedEvent); if (!focusoutPreventedEvent.defaultPrevented) { e.preventDefault(); if (loop) tryFocus(first, true); } } else if (shiftKey && [first, container].includes(currentFocusingEl)) { const focusoutPreventedEvent = createFocusOutPreventedEvent({ focusReason: focusReason.value }); emit("focusoutPrevented", focusoutPreventedEvent); if (!focusoutPreventedEvent.defaultPrevented) { e.preventDefault(); if (loop) tryFocus(last, true); } } } } }; provide(focusTrapInjectionKey, { focusTrapRef: forwardRef, onKeydown }); watch( () => props.focusTrapEl, (focusTrapEl) => { if (focusTrapEl) { forwardRef.value = focusTrapEl; } }, { immediate: true } ); watch(forwardRef, (forwardRef2, oldForwardRef) => { if (forwardRef2) { forwardRef2.addEventListener("keydown", onKeydown); forwardRef2.addEventListener("focusin", onFocusIn); forwardRef2.addEventListener("focusout", onFocusOut); } if (oldForwardRef instanceof HTMLElement) { oldForwardRef.removeEventListener("keydown", onKeydown); oldForwardRef.removeEventListener("focusin", onFocusIn); oldForwardRef.removeEventListener("focusout", onFocusOut); } }); const trapOnFocus = (e) => { emit(onTrapFocusEvent, e); }; const releaseOnFocus = (e) => emit(onReleaseFocusEvent, e); const onFocusIn = (e) => { const trapContainer = unref(forwardRef); if (!trapContainer) return; const target = e.target; const relatedTarget = e.relatedTarget; const isFocusedInTrap = target && trapContainer.contains(target); if (!props.trapped) { const isPrevFocusedInTrap = relatedTarget && trapContainer.contains(relatedTarget); if (!isPrevFocusedInTrap) { lastFocusBeforeTrapped = relatedTarget; } } if (isFocusedInTrap) emit("focusin", e); if (focusLayer.paused) return; if (props.trapped) { if (isFocusedInTrap) { lastFocusAfterTrapped = target; } else { tryFocus(lastFocusAfterTrapped, true); } } }; const onFocusOut = (e) => { const trapContainer = unref(forwardRef); if (focusLayer.paused || !trapContainer) return; if (props.trapped) { const relatedTarget = e.relatedTarget; if (!isNil(relatedTarget) && !trapContainer.contains(relatedTarget)) { setTimeout(() => { if (!focusLayer.paused && props.trapped) { const focusoutPreventedEvent = createFocusOutPreventedEvent({ focusReason: focusReason.value }); emit("focusoutPrevented", focusoutPreventedEvent); if (!focusoutPreventedEvent.defaultPrevented) { tryFocus(lastFocusAfterTrapped, true); } } }, 0); } } else { const target = e.target; const isFocusedInTrap = target && trapContainer.contains(target); if (!isFocusedInTrap) emit("focusout", e); } }; const startTrap = async () => { await nextTick(); const trapContainer = unref(forwardRef); if (trapContainer) { focusableStack.push(focusLayer); const prevFocusedElement = trapContainer.contains(document.activeElement) ? lastFocusBeforeTrapped : document.activeElement; lastFocusBeforeTrapped = prevFocusedElement; const isPrevFocusContained = trapContainer.contains(prevFocusedElement); if (!isPrevFocusContained) { const focusEvent = new Event(focusAfterTrapped, focusAfterTrappedOpts); trapContainer.addEventListener(focusAfterTrapped, trapOnFocus); trapContainer.dispatchEvent(focusEvent); if (!focusEvent.defaultPrevented) { nextTick(() => { let focusStartEl = props.focusStartEl; if (!isString(focusStartEl)) { tryFocus(focusStartEl); if (document.activeElement !== focusStartEl) { focusStartEl = "first"; } } if (focusStartEl === "first") { focusFirstDescendant( obtainAllFocusableElements(trapContainer), true ); } if (document.activeElement === prevFocusedElement || focusStartEl === "container") { tryFocus(trapContainer); } }); } } } }; const stopTrap = () => { const trapContainer = unref(forwardRef); if (trapContainer) { trapContainer.removeEventListener(focusAfterTrapped, trapOnFocus); const releasedEvent = new CustomEvent(focusAfterReleased, { ...focusAfterTrappedOpts, detail: { focusReason: focusReason.value } }); trapContainer.addEventListener(focusAfterReleased, releaseOnFocus); trapContainer.dispatchEvent(releasedEvent); if (!releasedEvent.defaultPrevented && (focusReason.value == "keyboard" || !isFocusCausedByUserEvent())) { tryFocus(lastFocusBeforeTrapped != null ? lastFocusBeforeTrapped : document.body); } trapContainer.removeEventListener(focusAfterReleased, trapOnFocus); focusableStack.remove(focusLayer); } }; onMounted(() => { if (props.trapped) { startTrap(); } watch( () => props.trapped, (trapped) => { if (trapped) { startTrap(); } else { stopTrap(); } } ); }); onBeforeUnmount(() => { if (props.trapped) { stopTrap(); } }); return (_ctx, _cache) => { return renderSlot(_ctx.$slots, "default", { handleKeydown: onKeydown }); }; } }); var FocusTrap = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "/home/runner/work/vuesax-alpha/vuesax-alpha/packages/components/focus-trap/src/focus-trap.vue"]]); export { FocusTrap as default }; //# sourceMappingURL=focus-trap2.mjs.map