reka-ui
Version:
Vue port for Radix UI Primitives.
1 lines • 12 kB
Source Map (JSON)
{"version":3,"file":"ToastViewport.cjs","sources":["../../src/Toast/ToastViewport.vue"],"sourcesContent":["<script lang=\"ts\">\nimport type { ComponentPublicInstance } from 'vue'\nimport type { PrimitiveProps } from '@/Primitive'\nimport { useCollection } from '@/Collection'\nimport { getActiveElement, useForwardExpose } from '@/shared'\n\nexport interface ToastViewportProps extends PrimitiveProps {\n /**\n * The keys to use as the keyboard shortcut that will move focus to the toast viewport.\n * @defaultValue ['F8']\n */\n hotkey?: string[]\n /**\n * An author-localized label for the toast viewport to provide context for screen reader users\n * when navigating page landmarks. The available `{hotkey}` placeholder will be replaced for you.\n * Alternatively, you can pass in a custom function to generate the label.\n * @defaultValue 'Notifications ({hotkey})'\n */\n label?: string | ((hotkey: string) => string)\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, ref, toRefs, watchEffect } from 'vue'\nimport { Primitive } from '@/Primitive'\nimport { injectToastProviderContext } from './ToastProvider.vue'\nimport { onKeyStroke, unrefElement } from '@vueuse/core'\nimport FocusProxy from './FocusProxy.vue'\nimport { focusFirst, getTabbableCandidates } from '@/FocusScope/utils'\nimport { VIEWPORT_PAUSE, VIEWPORT_RESUME } from './utils'\nimport { DismissableLayerBranch } from '@/DismissableLayer'\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(defineProps<ToastViewportProps>(), {\n hotkey: () => ['F8'], // from VIEWPORT_DEFAULT_HOTKEY\n label: 'Notifications ({hotkey})',\n as: 'ol',\n})\nconst { hotkey, label } = toRefs(props)\n\nconst { forwardRef, currentElement } = useForwardExpose()\nconst { CollectionSlot, getItems } = useCollection()\nconst providerContext = injectToastProviderContext()\nconst hasToasts = computed(() => providerContext.toastCount.value > 0)\nconst headFocusProxyRef = ref<HTMLElement>()\nconst tailFocusProxyRef = ref<HTMLElement>()\n\nconst hotkeyMessage = computed(() => hotkey.value.join('+').replace(/Key/g, '').replace(/Digit/g, ''))\n\nonKeyStroke(hotkey.value, () => {\n currentElement.value.focus()\n})\n\nonMounted(() => {\n providerContext.onViewportChange(currentElement.value)\n})\n\nwatchEffect((cleanupFn) => {\n const viewport = currentElement.value\n if (hasToasts.value && viewport) {\n const handlePause = () => {\n if (!providerContext.isClosePausedRef.value) {\n const pauseEvent = new CustomEvent(VIEWPORT_PAUSE)\n viewport.dispatchEvent(pauseEvent)\n providerContext.isClosePausedRef.value = true\n }\n }\n\n const handleResume = () => {\n if (providerContext.isClosePausedRef.value) {\n const resumeEvent = new CustomEvent(VIEWPORT_RESUME)\n viewport.dispatchEvent(resumeEvent)\n providerContext.isClosePausedRef.value = false\n }\n }\n\n const handleFocusOutResume = (event: FocusEvent) => {\n const isFocusMovingOutside = !viewport.contains(event.relatedTarget as HTMLElement)\n if (isFocusMovingOutside)\n handleResume()\n }\n\n const handlePointerLeaveResume = () => {\n const isFocusInside = viewport.contains(getActiveElement())\n if (!isFocusInside)\n handleResume()\n }\n\n // We programmatically manage tabbing as we are unable to influence\n // the source order with portals, this allows us to reverse the\n // tab order so that it runs from most recent toast to least\n const handleKeyDown = (event: KeyboardEvent) => {\n const isMetaKey = event.altKey || event.ctrlKey || event.metaKey\n const isTabKey = event.key === 'Tab' && !isMetaKey\n\n if (isTabKey) {\n const focusedElement = getActiveElement()\n const isTabbingBackwards = event.shiftKey\n const targetIsViewport = event.target === viewport\n\n // If we're back tabbing after jumping to the viewport then we simply\n // proxy focus out to the preceding document\n if (targetIsViewport && isTabbingBackwards) {\n headFocusProxyRef.value?.focus()\n return\n }\n\n const tabbingDirection = isTabbingBackwards ? 'backwards' : 'forwards'\n const sortedCandidates = getSortedTabbableCandidates({ tabbingDirection })\n const index = sortedCandidates.findIndex(candidate => candidate === focusedElement)\n if (focusFirst(sortedCandidates.slice(index + 1))) {\n event.preventDefault()\n }\n else {\n // If we can't focus that means we're at the edges so we\n // proxy to the corresponding exit point and let the browser handle\n // tab/shift+tab keypress and implicitly pass focus to the next valid element in the document\n isTabbingBackwards\n ? headFocusProxyRef.value?.focus()\n : tailFocusProxyRef.value?.focus()\n }\n }\n }\n\n viewport.addEventListener('focusin', handlePause)\n viewport.addEventListener('focusout', handleFocusOutResume)\n viewport.addEventListener('pointermove', handlePause)\n viewport.addEventListener('pointerleave', handlePointerLeaveResume)\n viewport.addEventListener('keydown', handleKeyDown)\n window.addEventListener('blur', handlePause)\n window.addEventListener('focus', handleResume)\n\n cleanupFn(() => {\n viewport.removeEventListener('focusin', handlePause)\n viewport.removeEventListener('focusout', handleFocusOutResume)\n viewport.removeEventListener('pointermove', handlePause)\n viewport.removeEventListener('pointerleave', handlePointerLeaveResume)\n viewport.removeEventListener('keydown', handleKeyDown)\n window.removeEventListener('blur', handlePause)\n window.removeEventListener('focus', handleResume)\n })\n }\n})\n\nfunction getSortedTabbableCandidates({ tabbingDirection }: { tabbingDirection: 'forwards' | 'backwards' }) {\n const toastItems = getItems().map(i => i.ref)\n const tabbableCandidates = toastItems.map((toastNode) => {\n const toastTabbableCandidates = [toastNode, ...getTabbableCandidates(toastNode)]\n return tabbingDirection === 'forwards'\n ? toastTabbableCandidates\n : toastTabbableCandidates.reverse()\n })\n return (\n tabbingDirection === 'forwards' ? tabbableCandidates.reverse() : tabbableCandidates\n ).flat()\n}\n</script>\n\n<template>\n <DismissableLayerBranch\n role=\"region\"\n :aria-label=\"typeof label === 'string' ? label.replace('{hotkey}', hotkeyMessage) : label(hotkeyMessage)\"\n tabindex=\"-1\"\n :style=\"{\n // incase list has size when empty (e.g. padding), we remove pointer events so\n // it doesn't prevent interactions with page elements that it overlays\n pointerEvents: hasToasts ? undefined : 'none',\n }\"\n >\n <FocusProxy\n v-if=\"hasToasts\"\n :ref=\"(node: ComponentPublicInstance) => {\n headFocusProxyRef = unrefElement(node) as HTMLElement\n return undefined\n }\"\n @focus-from-outside-viewport=\"() => {\n const tabbableCandidates = getSortedTabbableCandidates({\n tabbingDirection: 'forwards',\n })\n focusFirst(tabbableCandidates)\n }\"\n />\n <CollectionSlot>\n <Primitive\n :ref=\"forwardRef\"\n tabindex=\"-1\"\n :as=\"as\"\n :as-child=\"asChild\"\n v-bind=\"$attrs\"\n >\n <slot />\n </Primitive>\n </CollectionSlot>\n <FocusProxy\n v-if=\"hasToasts\"\n :ref=\"(node: ComponentPublicInstance) => {\n tailFocusProxyRef = unrefElement(node) as HTMLElement\n return undefined\n }\"\n @focus-from-outside-viewport=\"() => {\n const tabbableCandidates = getSortedTabbableCandidates({\n tabbingDirection: 'backwards',\n })\n focusFirst(tabbableCandidates)\n }\"\n />\n </DismissableLayerBranch>\n</template>\n"],"names":["toRefs","useForwardExpose","useCollection","injectToastProviderContext","computed","ref","onKeyStroke","onMounted","watchEffect","VIEWPORT_PAUSE","VIEWPORT_RESUME","getActiveElement","focusFirst","getTabbableCandidates"],"mappings":"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"}