hongluan-ui
Version:
Hongluan Component Library for Vue 3
1 lines • 14.9 kB
Source Map (JSON)
{"version":3,"file":"focus-trap.mjs","sources":["../../../../../../packages/components/focus-trap/src/focus-trap.vue"],"sourcesContent":["<template>\n <slot :handle-keydown=\"onKeydown\"></slot>\n</template>\n<script lang=\"ts\">\nimport {\n defineComponent,\n nextTick,\n onBeforeUnmount,\n onMounted,\n provide,\n ref,\n unref,\n watch,\n} from 'vue'\nimport { isNil } from 'lodash-unified'\nimport { EVENT_CODE } from '@hongluan-ui/constants'\nimport { useEscapeKeydown } from '@hongluan-ui/hooks'\nimport { isString } from '@hongluan-ui/utils'\nimport {\n createFocusOutPreventedEvent,\n focusFirstDescendant,\n focusableStack,\n getEdges,\n isFocusCausedByUserEvent,\n obtainAllFocusableElements,\n tryFocus,\n useFocusReason,\n} from './utils'\nimport {\n FOCUS_AFTER_RELEASED,\n FOCUS_AFTER_TRAPPED,\n FOCUS_AFTER_TRAPPED_OPTS,\n FOCUS_TRAP_INJECTION_KEY,\n ON_RELEASE_FOCUS_EVT,\n ON_TRAP_FOCUS_EVT,\n} from './tokens'\n\nimport type { PropType } from 'vue'\nimport type { FocusLayer } from './utils'\n\nexport default defineComponent({\n name: 'FocusTrap',\n inheritAttrs: false,\n props: {\n loop: Boolean,\n trapped: Boolean,\n focusTrapEl: Object as PropType<HTMLElement>,\n focusStartEl: {\n type: [Object, String] as PropType<'container' | 'first' | HTMLElement>,\n default: 'first',\n },\n },\n emits: [\n ON_TRAP_FOCUS_EVT,\n ON_RELEASE_FOCUS_EVT,\n 'focusin',\n 'focusout',\n 'focusout-prevented',\n 'release-requested',\n ],\n setup(props, { emit }) {\n const forwardRef = ref<HTMLElement | undefined>()\n let lastFocusBeforeTrapped: HTMLElement | null\n let lastFocusAfterTrapped: HTMLElement | null\n\n const { focusReason } = useFocusReason()\n\n useEscapeKeydown(event => {\n if (props.trapped && !focusLayer.paused) {\n emit('release-requested', event)\n }\n })\n\n const focusLayer: FocusLayer = {\n paused: false,\n pause() {\n this.paused = true\n },\n resume() {\n this.paused = false\n },\n }\n\n const onKeydown = (e: KeyboardEvent) => {\n if (!props.loop && !props.trapped) return\n if (focusLayer.paused) return\n\n const { key, altKey, ctrlKey, metaKey, currentTarget, shiftKey } = e\n const { loop } = props\n const isTabbing =\n key === EVENT_CODE.tab && !altKey && !ctrlKey && !metaKey\n\n const currentFocusingEl = document.activeElement\n if (isTabbing && currentFocusingEl) {\n const container = currentTarget as HTMLElement\n const [first, last] = getEdges(container)\n const isTabbable = first && last\n\n if (!isTabbable) {\n if (currentFocusingEl === container) {\n const focusoutPreventedEvent = createFocusOutPreventedEvent({\n focusReason: focusReason.value,\n })\n emit('focusout-prevented', focusoutPreventedEvent)\n if (!focusoutPreventedEvent.defaultPrevented) {\n e.preventDefault()\n }\n }\n } else {\n if (!shiftKey && currentFocusingEl === last) {\n const focusoutPreventedEvent = createFocusOutPreventedEvent({\n focusReason: focusReason.value,\n })\n emit('focusout-prevented', focusoutPreventedEvent)\n if (!focusoutPreventedEvent.defaultPrevented) {\n e.preventDefault()\n if (loop) tryFocus(first, true)\n }\n } else if (\n shiftKey &&\n [first, container].includes(currentFocusingEl as HTMLElement)\n ) {\n const focusoutPreventedEvent = createFocusOutPreventedEvent({\n focusReason: focusReason.value,\n })\n emit('focusout-prevented', focusoutPreventedEvent)\n if (!focusoutPreventedEvent.defaultPrevented) {\n e.preventDefault()\n if (loop) tryFocus(last, true)\n }\n }\n }\n }\n }\n\n provide(FOCUS_TRAP_INJECTION_KEY, {\n focusTrapRef: forwardRef,\n onKeydown,\n })\n\n watch(\n () => props.focusTrapEl,\n focusTrapEl => {\n if (focusTrapEl) {\n forwardRef.value = focusTrapEl\n }\n },\n { immediate: true },\n )\n\n watch([forwardRef], ([forwardRef], [oldForwardRef]) => {\n if (forwardRef) {\n forwardRef.addEventListener('keydown', onKeydown)\n forwardRef.addEventListener('focusin', onFocusIn)\n forwardRef.addEventListener('focusout', onFocusOut)\n }\n if (oldForwardRef) {\n oldForwardRef.removeEventListener('keydown', onKeydown)\n oldForwardRef.removeEventListener('focusin', onFocusIn)\n oldForwardRef.removeEventListener('focusout', onFocusOut)\n }\n })\n\n const trapOnFocus = (e: Event) => {\n emit(ON_TRAP_FOCUS_EVT, e)\n }\n const releaseOnFocus = (e: Event) => emit(ON_RELEASE_FOCUS_EVT, e)\n\n const onFocusIn = (e: FocusEvent) => {\n const trapContainer = unref(forwardRef)\n if (!trapContainer) return\n\n const target = e.target as HTMLElement | null\n const relatedTarget = e.relatedTarget as HTMLElement | null\n const isFocusedInTrap = target && trapContainer.contains(target)\n\n if (!props.trapped) {\n const isPrevFocusedInTrap =\n relatedTarget && trapContainer.contains(relatedTarget)\n if (!isPrevFocusedInTrap) {\n lastFocusBeforeTrapped = relatedTarget\n }\n }\n\n if (isFocusedInTrap) emit('focusin', e)\n\n if (focusLayer.paused) return\n\n if (props.trapped) {\n if (isFocusedInTrap) {\n lastFocusAfterTrapped = target\n } else {\n tryFocus(lastFocusAfterTrapped, true)\n }\n }\n }\n\n const onFocusOut = (e: Event) => {\n const trapContainer = unref(forwardRef)\n if (focusLayer.paused || !trapContainer) return\n\n if (props.trapped) {\n const relatedTarget = (e as FocusEvent)\n .relatedTarget as HTMLElement | null\n if (!isNil(relatedTarget) && !trapContainer.contains(relatedTarget)) {\n // Give embedded focus layer time to pause this layer before reclaiming focus\n // And only reclaim focus if it should currently be trapping\n setTimeout(() => {\n if (!focusLayer.paused && props.trapped) {\n const focusoutPreventedEvent = createFocusOutPreventedEvent({\n focusReason: focusReason.value,\n })\n emit('focusout-prevented', focusoutPreventedEvent)\n if (!focusoutPreventedEvent.defaultPrevented) {\n tryFocus(lastFocusAfterTrapped, true)\n }\n }\n }, 0)\n }\n } else {\n const target = e.target as HTMLElement | null\n const isFocusedInTrap = target && trapContainer.contains(target)\n if (!isFocusedInTrap) emit('focusout', e)\n }\n }\n\n async function startTrap() {\n // Wait for forwardRef to resolve\n await nextTick()\n const trapContainer = unref(forwardRef)\n if (trapContainer) {\n focusableStack.push(focusLayer)\n const prevFocusedElement = trapContainer.contains(\n document.activeElement,\n )\n ? lastFocusBeforeTrapped\n : document.activeElement\n lastFocusBeforeTrapped = prevFocusedElement as HTMLElement | null\n const isPrevFocusContained = trapContainer.contains(prevFocusedElement)\n if (!isPrevFocusContained) {\n const focusEvent = new Event(\n FOCUS_AFTER_TRAPPED,\n FOCUS_AFTER_TRAPPED_OPTS,\n )\n trapContainer.addEventListener(FOCUS_AFTER_TRAPPED, trapOnFocus)\n trapContainer.dispatchEvent(focusEvent)\n if (!focusEvent.defaultPrevented) {\n nextTick(() => {\n let focusStartEl = props.focusStartEl\n if (!isString(focusStartEl)) {\n tryFocus(focusStartEl)\n if (document.activeElement !== focusStartEl) {\n focusStartEl = 'first'\n }\n }\n if (focusStartEl === 'first') {\n focusFirstDescendant(\n obtainAllFocusableElements(trapContainer),\n true,\n )\n }\n if (\n document.activeElement === prevFocusedElement ||\n focusStartEl === 'container'\n ) {\n tryFocus(trapContainer)\n }\n })\n }\n }\n }\n }\n\n function stopTrap() {\n const trapContainer = unref(forwardRef)\n\n if (trapContainer) {\n trapContainer.removeEventListener(FOCUS_AFTER_TRAPPED, trapOnFocus)\n\n const releasedEvent = new CustomEvent(FOCUS_AFTER_RELEASED, {\n ...FOCUS_AFTER_TRAPPED_OPTS,\n detail: {\n focusReason: focusReason.value,\n },\n })\n trapContainer.addEventListener(FOCUS_AFTER_RELEASED, releaseOnFocus)\n trapContainer.dispatchEvent(releasedEvent)\n\n if (\n !releasedEvent.defaultPrevented &&\n (focusReason.value == 'keyboard' ||\n !isFocusCausedByUserEvent() ||\n trapContainer.contains(document.activeElement))\n ) {\n tryFocus(lastFocusBeforeTrapped ?? document.body)\n }\n\n trapContainer.removeEventListener(FOCUS_AFTER_RELEASED, releaseOnFocus)\n focusableStack.remove(focusLayer)\n }\n }\n\n onMounted(() => {\n if (props.trapped) {\n startTrap()\n }\n\n watch(\n () => props.trapped,\n trapped => {\n if (trapped) {\n startTrap()\n } else {\n stopTrap()\n }\n },\n )\n })\n\n onBeforeUnmount(() => {\n if (props.trapped) {\n stopTrap()\n }\n\n if (forwardRef.value) {\n forwardRef.value.removeEventListener('keydown', onKeydown)\n forwardRef.value.removeEventListener('focusin', onFocusIn)\n forwardRef.value.removeEventListener('focusout', onFocusOut)\n forwardRef.value = undefined\n }\n })\n\n return {\n onKeydown,\n }\n },\n})\n</script>\n"],"names":["_renderSlot"],"mappings":"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"}