UNPKG

reka-ui

Version:

Vue port for Radix UI Primitives.

1 lines 11.8 kB
{"version":3,"file":"FocusScope.cjs","sources":["../../src/FocusScope/FocusScope.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { PrimitiveProps } from '@/Primitive'\nimport { getActiveElement, useForwardExpose } from '@/shared'\n\nexport type FocusScopeEmits = {\n /**\n * Event handler called when auto-focusing on mount.\n * Can be prevented.\n */\n mountAutoFocus: [event: Event]\n\n /**\n * Event handler called when auto-focusing on unmount.\n * Can be prevented.\n */\n unmountAutoFocus: [event: Event]\n}\n\nexport interface FocusScopeProps extends PrimitiveProps {\n /**\n * When `true`, tabbing from last item will focus first tabbable\n * and shift+tab from first item will focus last tababble.\n * @defaultValue false\n */\n loop?: boolean\n\n /**\n * When `true`, focus cannot escape the focus scope via keyboard,\n * pointer, or a programmatic focus.\n * @defaultValue false\n */\n trapped?: boolean\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { nextTick, reactive, ref, watchEffect } from 'vue'\nimport { isClient } from '@vueuse/shared'\nimport {\n AUTOFOCUS_ON_MOUNT,\n AUTOFOCUS_ON_UNMOUNT,\n EVENT_OPTIONS,\n focus,\n focusFirst,\n getTabbableCandidates,\n getTabbableEdges,\n} from './utils'\nimport { createFocusScopesStack, removeLinks } from './stack'\nimport { Primitive } from '@/Primitive'\n\nconst props = withDefaults(defineProps<FocusScopeProps>(), {\n loop: false,\n trapped: false,\n})\nconst emits = defineEmits<FocusScopeEmits>()\n\nconst { currentRef, currentElement } = useForwardExpose()\nconst lastFocusedElementRef = ref<HTMLElement | null>(null)\nconst focusScopesStack = createFocusScopesStack()\n\nconst focusScope = reactive({\n paused: false,\n pause() {\n this.paused = true\n },\n resume() {\n this.paused = false\n },\n})\n\nwatchEffect((cleanupFn) => {\n if (!isClient)\n return\n const container = currentElement.value\n if (!props.trapped)\n return\n\n function handleFocusIn(event: FocusEvent) {\n if (focusScope.paused || !container)\n return\n const target = event.target as HTMLElement | null\n if (container.contains(target))\n lastFocusedElementRef.value = target\n else focus(lastFocusedElementRef.value, { select: true })\n }\n\n function handleFocusOut(event: FocusEvent) {\n if (focusScope.paused || !container)\n return\n const relatedTarget = event.relatedTarget as HTMLElement | null\n\n // A `focusout` event with a `null` `relatedTarget` will happen in at least two cases:\n //\n // 1. When the user switches app/tabs/windows/the browser itself loses focus.\n // 2. In Google Chrome, when the focused element is removed from the DOM.\n //\n // We let the browser do its thing here because:\n //\n // 1. The browser already keeps a memory of what's focused for when the page gets refocused.\n // 2. In Google Chrome, if we try to focus the deleted focused element (as per below), it\n // throws the CPU to 100%, so we avoid doing anything for this reason here too.\n if (relatedTarget === null)\n return\n\n // If the focus has moved to an actual legitimate element (`relatedTarget !== null`)\n // that is outside the container, we move focus to the last valid focused element inside.\n if (!container.contains(relatedTarget))\n focus(lastFocusedElementRef.value, { select: true })\n }\n\n // When the focused element gets removed from the DOM, browsers move focus\n // back to the document.body. In this case, we move focus to the container\n // to keep focus trapped correctly.\n // -- related: https://github.com/unovue/reka-ui/issues/518\n // Reka UI tentative solution:\n // instead of leaning on document.activeElement, we use lastFocusedElementRef.value to check\n // if the element still exist inside the container,\n // if not then we focus to the container\n function handleMutations(mutations: MutationRecord[]) {\n const isLastFocusedElementExist = container.contains(lastFocusedElementRef.value)\n if (!isLastFocusedElementExist)\n focus(container)\n }\n\n document.addEventListener('focusin', handleFocusIn)\n document.addEventListener('focusout', handleFocusOut)\n const mutationObserver = new MutationObserver(handleMutations)\n if (container)\n mutationObserver.observe(container, { childList: true, subtree: true })\n\n cleanupFn(() => {\n document.removeEventListener('focusin', handleFocusIn)\n document.removeEventListener('focusout', handleFocusOut)\n mutationObserver.disconnect()\n })\n})\n\nwatchEffect(async (cleanupFn) => {\n const container = currentElement.value\n\n await nextTick()\n if (!container)\n return\n focusScopesStack.add(focusScope)\n const previouslyFocusedElement = getActiveElement() as HTMLElement | null\n const hasFocusedCandidate = container.contains(previouslyFocusedElement)\n\n if (!hasFocusedCandidate) {\n const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS)\n container.addEventListener(AUTOFOCUS_ON_MOUNT, (ev: Event) =>\n emits('mountAutoFocus', ev))\n container.dispatchEvent(mountEvent)\n\n if (!mountEvent.defaultPrevented) {\n focusFirst(removeLinks(getTabbableCandidates(container)), {\n select: true,\n })\n if (getActiveElement() === previouslyFocusedElement)\n focus(container)\n }\n }\n\n cleanupFn(() => {\n container.removeEventListener(AUTOFOCUS_ON_MOUNT, (ev: Event) =>\n emits('mountAutoFocus', ev))\n\n const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS)\n const unmountEventHandler = (ev: Event) => {\n emits('unmountAutoFocus', ev)\n }\n container.addEventListener(AUTOFOCUS_ON_UNMOUNT, unmountEventHandler)\n container.dispatchEvent(unmountEvent)\n\n setTimeout(() => {\n if (!unmountEvent.defaultPrevented)\n focus(previouslyFocusedElement ?? document.body, { select: true })\n\n // we need to remove the listener after we `dispatchEvent`\n container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, unmountEventHandler)\n\n focusScopesStack.remove(focusScope)\n }, 0)\n })\n})\n\nfunction handleKeyDown(event: KeyboardEvent) {\n if (!props.loop && !props.trapped)\n return\n if (focusScope.paused)\n return\n\n const isTabKey\n = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey\n const focusedElement = getActiveElement() as HTMLElement | null\n\n if (isTabKey && focusedElement) {\n const container = event.currentTarget as HTMLElement\n const [first, last] = getTabbableEdges(container)\n const hasTabbableElementsInside = first && last\n\n // we can only wrap focus if we have tabbable edges\n if (!hasTabbableElementsInside) {\n if (focusedElement === container)\n event.preventDefault()\n }\n else {\n if (!event.shiftKey && focusedElement === last) {\n event.preventDefault()\n if (props.loop)\n focus(first, { select: true })\n }\n else if (event.shiftKey && focusedElement === first) {\n event.preventDefault()\n if (props.loop)\n focus(last, { select: true })\n }\n }\n }\n}\n</script>\n\n<template>\n <Primitive\n ref=\"currentRef\"\n tabindex=\"-1\"\n :as-child=\"asChild\"\n :as=\"as\"\n @keydown=\"handleKeyDown\"\n >\n <slot />\n </Primitive>\n</template>\n"],"names":["useForwardExpose","ref","createFocusScopesStack","reactive","watchEffect","isClient","focus","nextTick","getActiveElement","AUTOFOCUS_ON_MOUNT","EVENT_OPTIONS","focusFirst","removeLinks","getTabbableCandidates","AUTOFOCUS_ON_UNMOUNT","getTabbableEdges"],"mappings":"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"}