UNPKG

react-aria

Version:
1 lines 11.8 kB
{"mappings":"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","sources":["packages/react-aria/src/toast/useToastRegion.ts"],"sourcesContent":["/*\n * Copyright 2025 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {AriaLabelingProps, DOMAttributes, FocusableElement, RefObject} from '@react-types/shared';\nimport {focusWithoutScrolling} from '../utils/focusWithoutScrolling';\nimport {getEventTarget} from '../utils/shadowdom/DOMFunctions';\nimport {getInteractionModality} from '../interactions/useFocusVisible';\nimport intlMessages from '../../intl/toast/*.json';\nimport {mergeProps} from '../utils/mergeProps';\nimport {ToastState} from 'react-stately/useToastState';\nimport {useCallback, useEffect, useRef} from 'react';\n// @ts-ignore\nimport {useFocusWithin} from '../interactions/useFocusWithin';\nimport {useHover} from '../interactions/useHover';\nimport {useLandmark} from '../landmark/useLandmark';\nimport {useLayoutEffect} from '../utils/useLayoutEffect';\nimport {useLocalizedStringFormatter} from '../i18n/useLocalizedStringFormatter';\n\nexport interface AriaToastRegionProps extends AriaLabelingProps {\n /**\n * An accessibility label for the toast region.\n *\n * @default 'Notifications'\n */\n 'aria-label'?: string;\n}\n\nexport interface ToastRegionAria {\n /** Props for the landmark region element. */\n regionProps: DOMAttributes;\n}\n\n/**\n * Provides the behavior and accessibility implementation for a toast region containing one or more\n * toasts. Toasts display brief, temporary notifications of actions, errors, or other events in an\n * application.\n */\nexport function useToastRegion<T>(\n props: AriaToastRegionProps,\n state: ToastState<T>,\n ref: RefObject<HTMLElement | null>\n): ToastRegionAria {\n let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/toast');\n let {landmarkProps} = useLandmark(\n {\n role: 'region',\n 'aria-label':\n props['aria-label'] ||\n stringFormatter.format('notifications', {count: state.visibleToasts.length})\n },\n ref\n );\n\n let isHovered = useRef(false);\n let isFocused = useRef(false);\n let updateTimers = useCallback(() => {\n if (isHovered.current || isFocused.current) {\n state.pauseAll();\n } else {\n state.resumeAll();\n }\n }, [state]);\n\n let {hoverProps} = useHover({\n onHoverStart: () => {\n isHovered.current = true;\n updateTimers();\n },\n onHoverEnd: () => {\n isHovered.current = false;\n updateTimers();\n }\n });\n\n // Manage focus within the toast region.\n // If a focused containing toast is removed, move focus to the next toast, or the previous toast if there is no next toast.\n let toasts = useRef<FocusableElement[]>([]);\n let prevVisibleToasts = useRef(state.visibleToasts);\n let focusedToast = useRef<number | null>(null);\n useLayoutEffect(() => {\n // If no toast has focus, then don't do anything.\n if (focusedToast.current === -1 || state.visibleToasts.length === 0 || !ref.current) {\n toasts.current = [];\n prevVisibleToasts.current = state.visibleToasts;\n return;\n }\n toasts.current = [\n ...ref.current.querySelectorAll('[role=\"alertdialog\"]')\n ] as FocusableElement[];\n // If the visible toasts haven't changed, we don't need to do anything.\n if (\n prevVisibleToasts.current.length === state.visibleToasts.length &&\n state.visibleToasts.every((t, i) => t.key === prevVisibleToasts.current[i].key)\n ) {\n prevVisibleToasts.current = state.visibleToasts;\n return;\n }\n // Get a list of all toasts by index and add info if they are removed.\n let allToasts = prevVisibleToasts.current.map((t, i) => ({\n ...t,\n i,\n isRemoved: !state.visibleToasts.some(t2 => t.key === t2.key)\n }));\n\n let removedFocusedToastIndex = allToasts.findIndex(\n t => t.i === focusedToast.current && t.isRemoved\n );\n\n // If the focused toast was removed, focus the next or previous toast.\n if (removedFocusedToastIndex > -1) {\n // In pointer modality, move focus out of the toast region.\n // Otherwise auto-dismiss timers will appear \"stuck\".\n if (getInteractionModality() === 'pointer' && lastFocused.current?.isConnected) {\n focusWithoutScrolling(lastFocused.current);\n } else {\n let i = 0;\n let nextToast;\n let prevToast;\n while (i <= removedFocusedToastIndex) {\n if (!allToasts[i].isRemoved) {\n prevToast = Math.max(0, i - 1);\n }\n i++;\n }\n while (i < allToasts.length) {\n if (!allToasts[i].isRemoved) {\n nextToast = i - 1;\n break;\n }\n i++;\n }\n\n // in the case where it's one toast at a time, both will be undefined, but we know the index must be 0\n if (prevToast === undefined && nextToast === undefined) {\n prevToast = 0;\n }\n\n // prioritize going to newer toasts\n if (prevToast >= 0 && prevToast < toasts.current.length) {\n focusWithoutScrolling(toasts.current[prevToast]);\n } else if (nextToast >= 0 && nextToast < toasts.current.length) {\n focusWithoutScrolling(toasts.current[nextToast]);\n }\n }\n }\n\n prevVisibleToasts.current = state.visibleToasts;\n }, [state.visibleToasts, ref]);\n\n let lastFocused = useRef<FocusableElement | null>(null);\n let {focusWithinProps} = useFocusWithin({\n onFocusWithin: e => {\n isFocused.current = true;\n lastFocused.current = e.relatedTarget as FocusableElement;\n updateTimers();\n },\n onBlurWithin: () => {\n isFocused.current = false;\n lastFocused.current = null;\n updateTimers();\n }\n });\n\n // When the number of visible toasts becomes 0 or the region unmounts,\n // restore focus to the last element that had focus before the user moved focus\n // into the region. FocusScope restore focus doesn't update whenever the focus\n // moves in, it only happens once, so we correct it.\n // Because we're in a hook, we can't control if the user unmounts or not.\n useEffect(() => {\n if (state.visibleToasts.length === 0 && lastFocused.current?.isConnected) {\n if (getInteractionModality() === 'pointer') {\n focusWithoutScrolling(lastFocused.current);\n } else {\n lastFocused.current.focus();\n }\n lastFocused.current = null;\n }\n }, [ref, state.visibleToasts.length]);\n\n useEffect(() => {\n return () => {\n if (lastFocused.current?.isConnected) {\n if (getInteractionModality() === 'pointer') {\n focusWithoutScrolling(lastFocused.current);\n } else {\n lastFocused.current.focus();\n }\n lastFocused.current = null;\n }\n };\n }, [ref]);\n\n return {\n regionProps: mergeProps(landmarkProps, hoverProps, focusWithinProps, {\n tabIndex: -1,\n // Mark the toast region as a \"top layer\", so that it:\n // - is not aria-hidden when opening an overlay\n // - allows focus even outside a containing focus scope\n // - doesn’t dismiss overlays when clicking on it, even though it is outside\n // @ts-ignore\n 'data-react-aria-top-layer': true,\n // listen to focus events separate from focuswithin because that will only fire once\n // and we need to follow all focus changes\n onFocus: e => {\n let target = (getEventTarget(e) as Element).closest('[role=\"alertdialog\"]');\n focusedToast.current = toasts.current.findIndex(t => t === target);\n },\n onBlur: () => {\n focusedToast.current = -1;\n }\n })\n };\n}\n"],"names":[],"version":3,"file":"useToastRegion.cjs.map"}