@react-aria/toast 3.0.0-beta.12 → 3.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/dist/ar-AE.main.js +4 -1
  2. package/dist/ar-AE.main.js.map +1 -1
  3. package/dist/ar-AE.mjs +4 -1
  4. package/dist/ar-AE.module.js +4 -1
  5. package/dist/ar-AE.module.js.map +1 -1
  6. package/dist/bg-BG.main.js +4 -1
  7. package/dist/bg-BG.main.js.map +1 -1
  8. package/dist/bg-BG.mjs +4 -1
  9. package/dist/bg-BG.module.js +4 -1
  10. package/dist/bg-BG.module.js.map +1 -1
  11. package/dist/cs-CZ.main.js +4 -1
  12. package/dist/cs-CZ.main.js.map +1 -1
  13. package/dist/cs-CZ.mjs +4 -1
  14. package/dist/cs-CZ.module.js +4 -1
  15. package/dist/cs-CZ.module.js.map +1 -1
  16. package/dist/da-DK.main.js +4 -1
  17. package/dist/da-DK.main.js.map +1 -1
  18. package/dist/da-DK.mjs +4 -1
  19. package/dist/da-DK.module.js +4 -1
  20. package/dist/da-DK.module.js.map +1 -1
  21. package/dist/de-DE.main.js +4 -1
  22. package/dist/de-DE.main.js.map +1 -1
  23. package/dist/de-DE.mjs +4 -1
  24. package/dist/de-DE.module.js +4 -1
  25. package/dist/de-DE.module.js.map +1 -1
  26. package/dist/el-GR.main.js +4 -1
  27. package/dist/el-GR.main.js.map +1 -1
  28. package/dist/el-GR.mjs +4 -1
  29. package/dist/el-GR.module.js +4 -1
  30. package/dist/el-GR.module.js.map +1 -1
  31. package/dist/en-US.main.js +4 -1
  32. package/dist/en-US.main.js.map +1 -1
  33. package/dist/en-US.mjs +4 -1
  34. package/dist/en-US.module.js +4 -1
  35. package/dist/en-US.module.js.map +1 -1
  36. package/dist/es-ES.main.js +4 -1
  37. package/dist/es-ES.main.js.map +1 -1
  38. package/dist/es-ES.mjs +4 -1
  39. package/dist/es-ES.module.js +4 -1
  40. package/dist/es-ES.module.js.map +1 -1
  41. package/dist/et-EE.main.js +4 -1
  42. package/dist/et-EE.main.js.map +1 -1
  43. package/dist/et-EE.mjs +4 -1
  44. package/dist/et-EE.module.js +4 -1
  45. package/dist/et-EE.module.js.map +1 -1
  46. package/dist/fi-FI.main.js +4 -1
  47. package/dist/fi-FI.main.js.map +1 -1
  48. package/dist/fi-FI.mjs +4 -1
  49. package/dist/fi-FI.module.js +4 -1
  50. package/dist/fi-FI.module.js.map +1 -1
  51. package/dist/fr-FR.main.js +4 -1
  52. package/dist/fr-FR.main.js.map +1 -1
  53. package/dist/fr-FR.mjs +4 -1
  54. package/dist/fr-FR.module.js +4 -1
  55. package/dist/fr-FR.module.js.map +1 -1
  56. package/dist/he-IL.main.js +4 -1
  57. package/dist/he-IL.main.js.map +1 -1
  58. package/dist/he-IL.mjs +4 -1
  59. package/dist/he-IL.module.js +4 -1
  60. package/dist/he-IL.module.js.map +1 -1
  61. package/dist/hr-HR.main.js +4 -1
  62. package/dist/hr-HR.main.js.map +1 -1
  63. package/dist/hr-HR.mjs +4 -1
  64. package/dist/hr-HR.module.js +4 -1
  65. package/dist/hr-HR.module.js.map +1 -1
  66. package/dist/hu-HU.main.js +4 -1
  67. package/dist/hu-HU.main.js.map +1 -1
  68. package/dist/hu-HU.mjs +4 -1
  69. package/dist/hu-HU.module.js +4 -1
  70. package/dist/hu-HU.module.js.map +1 -1
  71. package/dist/it-IT.main.js +4 -1
  72. package/dist/it-IT.main.js.map +1 -1
  73. package/dist/it-IT.mjs +4 -1
  74. package/dist/it-IT.module.js +4 -1
  75. package/dist/it-IT.module.js.map +1 -1
  76. package/dist/ja-JP.main.js +4 -1
  77. package/dist/ja-JP.main.js.map +1 -1
  78. package/dist/ja-JP.mjs +4 -1
  79. package/dist/ja-JP.module.js +4 -1
  80. package/dist/ja-JP.module.js.map +1 -1
  81. package/dist/ko-KR.main.js +4 -1
  82. package/dist/ko-KR.main.js.map +1 -1
  83. package/dist/ko-KR.mjs +4 -1
  84. package/dist/ko-KR.module.js +4 -1
  85. package/dist/ko-KR.module.js.map +1 -1
  86. package/dist/lt-LT.main.js +4 -1
  87. package/dist/lt-LT.main.js.map +1 -1
  88. package/dist/lt-LT.mjs +4 -1
  89. package/dist/lt-LT.module.js +4 -1
  90. package/dist/lt-LT.module.js.map +1 -1
  91. package/dist/lv-LV.main.js +4 -1
  92. package/dist/lv-LV.main.js.map +1 -1
  93. package/dist/lv-LV.mjs +4 -1
  94. package/dist/lv-LV.module.js +4 -1
  95. package/dist/lv-LV.module.js.map +1 -1
  96. package/dist/nb-NO.main.js +4 -1
  97. package/dist/nb-NO.main.js.map +1 -1
  98. package/dist/nb-NO.mjs +4 -1
  99. package/dist/nb-NO.module.js +4 -1
  100. package/dist/nb-NO.module.js.map +1 -1
  101. package/dist/nl-NL.main.js +4 -1
  102. package/dist/nl-NL.main.js.map +1 -1
  103. package/dist/nl-NL.mjs +4 -1
  104. package/dist/nl-NL.module.js +4 -1
  105. package/dist/nl-NL.module.js.map +1 -1
  106. package/dist/pl-PL.main.js +6 -1
  107. package/dist/pl-PL.main.js.map +1 -1
  108. package/dist/pl-PL.mjs +6 -1
  109. package/dist/pl-PL.module.js +6 -1
  110. package/dist/pl-PL.module.js.map +1 -1
  111. package/dist/pt-BR.main.js +4 -1
  112. package/dist/pt-BR.main.js.map +1 -1
  113. package/dist/pt-BR.mjs +4 -1
  114. package/dist/pt-BR.module.js +4 -1
  115. package/dist/pt-BR.module.js.map +1 -1
  116. package/dist/pt-PT.main.js +4 -1
  117. package/dist/pt-PT.main.js.map +1 -1
  118. package/dist/pt-PT.mjs +4 -1
  119. package/dist/pt-PT.module.js +4 -1
  120. package/dist/pt-PT.module.js.map +1 -1
  121. package/dist/ro-RO.main.js +4 -1
  122. package/dist/ro-RO.main.js.map +1 -1
  123. package/dist/ro-RO.mjs +4 -1
  124. package/dist/ro-RO.module.js +4 -1
  125. package/dist/ro-RO.module.js.map +1 -1
  126. package/dist/ru-RU.main.js +4 -1
  127. package/dist/ru-RU.main.js.map +1 -1
  128. package/dist/ru-RU.mjs +4 -1
  129. package/dist/ru-RU.module.js +4 -1
  130. package/dist/ru-RU.module.js.map +1 -1
  131. package/dist/sk-SK.main.js +5 -1
  132. package/dist/sk-SK.main.js.map +1 -1
  133. package/dist/sk-SK.mjs +5 -1
  134. package/dist/sk-SK.module.js +5 -1
  135. package/dist/sk-SK.module.js.map +1 -1
  136. package/dist/sl-SI.main.js +6 -1
  137. package/dist/sl-SI.main.js.map +1 -1
  138. package/dist/sl-SI.mjs +6 -1
  139. package/dist/sl-SI.module.js +6 -1
  140. package/dist/sl-SI.module.js.map +1 -1
  141. package/dist/sr-SP.main.js +4 -1
  142. package/dist/sr-SP.main.js.map +1 -1
  143. package/dist/sr-SP.mjs +4 -1
  144. package/dist/sr-SP.module.js +4 -1
  145. package/dist/sr-SP.module.js.map +1 -1
  146. package/dist/sv-SE.main.js +4 -1
  147. package/dist/sv-SE.main.js.map +1 -1
  148. package/dist/sv-SE.mjs +4 -1
  149. package/dist/sv-SE.module.js +4 -1
  150. package/dist/sv-SE.module.js.map +1 -1
  151. package/dist/tr-TR.main.js +4 -1
  152. package/dist/tr-TR.main.js.map +1 -1
  153. package/dist/tr-TR.mjs +4 -1
  154. package/dist/tr-TR.module.js +4 -1
  155. package/dist/tr-TR.module.js.map +1 -1
  156. package/dist/types.d.ts +6 -5
  157. package/dist/types.d.ts.map +1 -1
  158. package/dist/uk-UA.main.js +4 -1
  159. package/dist/uk-UA.main.js.map +1 -1
  160. package/dist/uk-UA.mjs +4 -1
  161. package/dist/uk-UA.module.js +4 -1
  162. package/dist/uk-UA.module.js.map +1 -1
  163. package/dist/useToast.main.js +14 -22
  164. package/dist/useToast.main.js.map +1 -1
  165. package/dist/useToast.mjs +16 -24
  166. package/dist/useToast.module.js +16 -24
  167. package/dist/useToast.module.js.map +1 -1
  168. package/dist/useToastRegion.main.js +85 -6
  169. package/dist/useToastRegion.main.js.map +1 -1
  170. package/dist/useToastRegion.mjs +86 -7
  171. package/dist/useToastRegion.module.js +86 -7
  172. package/dist/useToastRegion.module.js.map +1 -1
  173. package/dist/zh-CN.main.js +4 -1
  174. package/dist/zh-CN.main.js.map +1 -1
  175. package/dist/zh-CN.mjs +4 -1
  176. package/dist/zh-CN.module.js +4 -1
  177. package/dist/zh-CN.module.js.map +1 -1
  178. package/dist/zh-TW.main.js +4 -1
  179. package/dist/zh-TW.main.js.map +1 -1
  180. package/dist/zh-TW.mjs +4 -1
  181. package/dist/zh-TW.module.js +4 -1
  182. package/dist/zh-TW.module.js.map +1 -1
  183. package/package.json +10 -10
  184. package/src/useToast.ts +24 -30
  185. package/src/useToastRegion.ts +98 -10
@@ -1,7 +1,10 @@
1
1
  var $52e8b1b00094e5a3$exports = {};
2
2
  $52e8b1b00094e5a3$exports = {
3
3
  "close": `\u{95DC}\u{9589}`,
4
- "notifications": `\u{901A}\u{77E5}`
4
+ "notifications": (args, formatter)=>`${formatter.plural(args.count, {
5
+ one: ()=>`${formatter.number(args.count)} \u{500B}\u{901A}\u{77E5}`,
6
+ other: ()=>`${formatter.number(args.count)} \u{500B}\u{901A}\u{77E5}`
7
+ })}\u{3002}`
5
8
  };
6
9
 
7
10
 
@@ -1 +1 @@
1
- {"mappings":";AAAA,4BAAiB;IAAG,SAAS,CAAC,gBAAE,CAAC;IAC/B,iBAAiB,CAAC,gBAAE,CAAC;AACvB","sources":["packages/@react-aria/toast/intl/zh-TW.json"],"sourcesContent":["{\n \"close\": \"關閉\",\n \"notifications\": \"通知\"\n}\n"],"names":[],"version":3,"file":"zh-TW.module.js.map"}
1
+ {"mappings":";AAAA,4BAAiB;IAAG,SAAS,CAAC,gBAAE,CAAC;IAC/B,iBAAiB,CAAC,MAAM,YAAc,CAAC,EAAE,UAAU,MAAM,CAAC,KAAK,KAAK,EAAE;YAAC,KAAK,IAAM,CAAC,EAAE,UAAU,MAAM,CAAC,KAAK,KAAK,EAAE,yBAAI,CAAC;YAAE,OAAO,IAAM,CAAC,EAAE,UAAU,MAAM,CAAC,KAAK,KAAK,EAAE,yBAAI,CAAC;QAAA,GAAG,QAAC,CAAC;AAClL","sources":["packages/@react-aria/toast/intl/zh-TW.json"],"sourcesContent":["{\n \"close\": \"關閉\",\n \"notifications\": \"{count, plural, one {# 個通知} other {# 個通知}}。\"\n}\n"],"names":[],"version":3,"file":"zh-TW.module.js.map"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-aria/toast",
3
- "version": "3.0.0-beta.12",
3
+ "version": "3.0.0-beta.13",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
@@ -22,20 +22,20 @@
22
22
  "url": "https://github.com/adobe/react-spectrum"
23
23
  },
24
24
  "dependencies": {
25
- "@react-aria/i18n": "^3.11.1",
26
- "@react-aria/interactions": "^3.21.3",
27
- "@react-aria/landmark": "3.0.0-beta.12",
28
- "@react-aria/utils": "^3.24.1",
29
- "@react-stately/toast": "3.0.0-beta.4",
30
- "@react-types/button": "^3.9.4",
31
- "@react-types/shared": "^3.23.1",
25
+ "@react-aria/i18n": "^3.12.0",
26
+ "@react-aria/interactions": "^3.22.0",
27
+ "@react-aria/landmark": "3.0.0-beta.13",
28
+ "@react-aria/utils": "^3.25.0",
29
+ "@react-stately/toast": "3.0.0-beta.5",
30
+ "@react-types/button": "^3.9.5",
31
+ "@react-types/shared": "^3.24.0",
32
32
  "@swc/helpers": "^0.5.0"
33
33
  },
34
34
  "peerDependencies": {
35
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
35
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
39
39
  },
40
- "gitHead": "b77d7d594dff4dcfb5359bffbcfd18142b146433"
40
+ "gitHead": "86d80e3216bc32e75108831cf3a5a720bc849206"
41
41
  }
package/src/useToast.ts CHANGED
@@ -11,12 +11,12 @@
11
11
  */
12
12
 
13
13
  import {AriaButtonProps} from '@react-types/button';
14
- import {AriaLabelingProps, DOMAttributes, FocusableElement} from '@react-types/shared';
14
+ import {AriaLabelingProps, DOMAttributes, FocusableElement, RefObject} from '@react-types/shared';
15
15
  // @ts-ignore
16
16
  import intlMessages from '../intl/*.json';
17
17
  import {QueuedToast, ToastState} from '@react-stately/toast';
18
- import {RefObject, useEffect, useRef} from 'react';
19
- import {useId, useLayoutEffect, useSlotId} from '@react-aria/utils';
18
+ import React, {useEffect} from 'react';
19
+ import {useId, useSlotId} from '@react-aria/utils';
20
20
  import {useLocalizedStringFormatter} from '@react-aria/i18n';
21
21
 
22
22
  export interface AriaToastProps<T> extends AriaLabelingProps {
@@ -25,8 +25,10 @@ export interface AriaToastProps<T> extends AriaLabelingProps {
25
25
  }
26
26
 
27
27
  export interface ToastAria {
28
- /** Props for the toast container element. */
28
+ /** Props for the toast container, non-modal dialog element. */
29
29
  toastProps: DOMAttributes,
30
+ /** Props for the toast content alert message. */
31
+ contentProps: DOMAttributes,
30
32
  /** Props for the toast title element. */
31
33
  titleProps: DOMAttributes,
32
34
  /** Props for the toast description element, if any. */
@@ -39,7 +41,8 @@ export interface ToastAria {
39
41
  * Provides the behavior and accessibility implementation for a toast component.
40
42
  * Toasts display brief, temporary notifications of actions, errors, or other events in an application.
41
43
  */
42
- export function useToast<T>(props: AriaToastProps<T>, state: ToastState<T>, ref: RefObject<FocusableElement>): ToastAria {
44
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
45
+ export function useToast<T>(props: AriaToastProps<T>, state: ToastState<T>, ref: RefObject<FocusableElement | null>): ToastAria {
43
46
  let {
44
47
  key,
45
48
  timer,
@@ -58,30 +61,12 @@ export function useToast<T>(props: AriaToastProps<T>, state: ToastState<T>, ref:
58
61
  };
59
62
  }, [timer, timeout]);
60
63
 
61
- // Restore focus to the toast container on unmount.
62
- // If there are no more toasts, the container will be unmounted
63
- // and will restore focus to wherever focus was before the user
64
- // focused the toast region.
65
- let focusOnUnmount = useRef(null);
66
- useLayoutEffect(() => {
67
- let container = ref.current.closest('[role=region]') as HTMLElement;
68
- return () => {
69
- if (container && container.contains(document.activeElement)) {
70
- // Focus must be delayed for focus ring to appear, but we can't wait
71
- // until useEffect cleanup to check if focus was inside the container.
72
- focusOnUnmount.current = container;
73
- }
74
- };
75
- }, [ref]);
76
-
77
- // eslint-disable-next-line
64
+ let [isEntered, setIsEntered] = React.useState(false);
78
65
  useEffect(() => {
79
- return () => {
80
- if (focusOnUnmount.current) {
81
- focusOnUnmount.current.focus();
82
- }
83
- };
84
- }, [ref]);
66
+ if (animation === 'entering' || animation === 'queued') {
67
+ setIsEntered(true);
68
+ }
69
+ }, [animation]);
85
70
 
86
71
  let titleId = useId();
87
72
  let descriptionId = useSlotId();
@@ -89,13 +74,22 @@ export function useToast<T>(props: AriaToastProps<T>, state: ToastState<T>, ref:
89
74
 
90
75
  return {
91
76
  toastProps: {
92
- role: 'alert',
77
+ role: 'alertdialog',
78
+ 'aria-modal': 'false',
93
79
  'aria-label': props['aria-label'],
94
80
  'aria-labelledby': props['aria-labelledby'] || titleId,
95
81
  'aria-describedby': props['aria-describedby'] || descriptionId,
96
82
  'aria-details': props['aria-details'],
97
83
  // Hide toasts that are animating out so VoiceOver doesn't announce them.
98
- 'aria-hidden': animation === 'exiting' ? 'true' : undefined
84
+ 'aria-hidden': animation === 'exiting' ? 'true' : undefined,
85
+ tabIndex: 0
86
+ },
87
+ contentProps: {
88
+ role: 'alert',
89
+ 'aria-atomic': 'true',
90
+ style: {
91
+ visibility: isEntered || animation === null ? 'visible' : 'hidden'
92
+ }
99
93
  },
100
94
  titleProps: {
101
95
  id: titleId
@@ -1,10 +1,10 @@
1
- import {AriaLabelingProps, DOMAttributes} from '@react-types/shared';
2
- import {focusWithoutScrolling, mergeProps} from '@react-aria/utils';
1
+ import {AriaLabelingProps, DOMAttributes, RefObject} from '@react-types/shared';
2
+ import {focusWithoutScrolling, mergeProps, useLayoutEffect} from '@react-aria/utils';
3
3
  import {getInteractionModality, useFocusWithin, useHover} from '@react-aria/interactions';
4
4
  // @ts-ignore
5
5
  import intlMessages from '../intl/*.json';
6
- import {RefObject, useEffect, useRef} from 'react';
7
6
  import {ToastState} from '@react-stately/toast';
7
+ import {useEffect, useRef} from 'react';
8
8
  import {useLandmark} from '@react-aria/landmark';
9
9
  import {useLocalizedStringFormatter} from '@react-aria/i18n';
10
10
 
@@ -25,11 +25,11 @@ export interface ToastRegionAria {
25
25
  * Provides the behavior and accessibility implementation for a toast region containing one or more toasts.
26
26
  * Toasts display brief, temporary notifications of actions, errors, or other events in an application.
27
27
  */
28
- export function useToastRegion<T>(props: AriaToastRegionProps, state: ToastState<T>, ref: RefObject<HTMLElement>): ToastRegionAria {
28
+ export function useToastRegion<T>(props: AriaToastRegionProps, state: ToastState<T>, ref: RefObject<HTMLElement | null>): ToastRegionAria {
29
29
  let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/toast');
30
30
  let {landmarkProps} = useLandmark({
31
31
  role: 'region',
32
- 'aria-label': props['aria-label'] || stringFormatter.format('notifications')
32
+ 'aria-label': props['aria-label'] || stringFormatter.format('notifications', {count: state.visibleToasts.length})
33
33
  }, ref);
34
34
 
35
35
  let {hoverProps} = useHover({
@@ -37,6 +37,72 @@ export function useToastRegion<T>(props: AriaToastRegionProps, state: ToastState
37
37
  onHoverEnd: state.resumeAll
38
38
  });
39
39
 
40
+ // Manage focus within the toast region.
41
+ // If a focused containing toast is removed, move focus to the next toast, or the previous toast if there is no next toast.
42
+ // We might be making an assumption with how this works if someone implements the priority queue differently, or
43
+ // if they only show one toast at a time.
44
+ let toasts = useRef([]);
45
+ let prevVisibleToasts = useRef(state.visibleToasts);
46
+ let focusedToast = useRef(null);
47
+ useLayoutEffect(() => {
48
+ // If no toast has focus, then don't do anything.
49
+ if (focusedToast.current === -1 || state.visibleToasts.length === 0) {
50
+ toasts.current = [];
51
+ prevVisibleToasts.current = state.visibleToasts;
52
+ return;
53
+ }
54
+ toasts.current = [...ref.current.querySelectorAll('[role="alertdialog"]')];
55
+ // If the visible toasts haven't changed, we don't need to do anything.
56
+ if (prevVisibleToasts.current.length === state.visibleToasts.length
57
+ && state.visibleToasts.every((t, i) => t.key === prevVisibleToasts.current[i].key)) {
58
+ prevVisibleToasts.current = state.visibleToasts;
59
+ return;
60
+ }
61
+ // Get a list of all toasts by index and add info if they are removed.
62
+ let allToasts = prevVisibleToasts.current
63
+ .map((t, i) => ({
64
+ ...t,
65
+ i,
66
+ isRemoved: !state.visibleToasts.some(t2 => t.key === t2.key)
67
+ }));
68
+
69
+ let removedFocusedToastIndex = allToasts.findIndex(t => t.i === focusedToast.current);
70
+
71
+ // If the focused toast was removed, focus the next or previous toast.
72
+ if (removedFocusedToastIndex > -1) {
73
+ let i = 0;
74
+ let nextToast;
75
+ let prevToast;
76
+ while (i <= removedFocusedToastIndex) {
77
+ if (!allToasts[i].isRemoved) {
78
+ prevToast = Math.max(0, i - 1);
79
+ }
80
+ i++;
81
+ }
82
+ while (i < allToasts.length) {
83
+ if (!allToasts[i].isRemoved) {
84
+ nextToast = i - 1;
85
+ break;
86
+ }
87
+ i++;
88
+ }
89
+
90
+ // in the case where it's one toast at a time, both will be undefined, but we know the index must be 0
91
+ if (prevToast === undefined && nextToast === undefined) {
92
+ prevToast = 0;
93
+ }
94
+
95
+ // prioritize going to newer toasts
96
+ if (prevToast >= 0 && prevToast < toasts.current.length) {
97
+ focusWithoutScrolling(toasts.current[prevToast]);
98
+ } else if (nextToast >= 0 && nextToast < toasts.current.length) {
99
+ focusWithoutScrolling(toasts.current[nextToast]);
100
+ }
101
+ }
102
+
103
+ prevVisibleToasts.current = state.visibleToasts;
104
+ }, [state.visibleToasts, ref]);
105
+
40
106
  let lastFocused = useRef(null);
41
107
  let {focusWithinProps} = useFocusWithin({
42
108
  onFocusWithin: (e) => {
@@ -49,10 +115,22 @@ export function useToastRegion<T>(props: AriaToastRegionProps, state: ToastState
49
115
  }
50
116
  });
51
117
 
52
- // When the region unmounts, restore focus to the last element that had focus
53
- // before the user moved focus into the region.
54
- // TODO: handle when the element has unmounted like FocusScope does?
55
- // eslint-disable-next-line arrow-body-style
118
+ // When the number of visible toasts becomes 0 or the region unmounts,
119
+ // restore focus to the last element that had focus before the user moved focus
120
+ // into the region. FocusScope restore focus doesn't update whenever the focus
121
+ // moves in, it only happens once, so we correct it.
122
+ // Because we're in a hook, we can't control if the user unmounts or not.
123
+ useEffect(() => {
124
+ if (state.visibleToasts.length === 0 && lastFocused.current && document.body.contains(lastFocused.current)) {
125
+ if (getInteractionModality() === 'pointer') {
126
+ focusWithoutScrolling(lastFocused.current);
127
+ } else {
128
+ lastFocused.current.focus();
129
+ }
130
+ lastFocused.current = null;
131
+ }
132
+ }, [ref, state.visibleToasts.length]);
133
+
56
134
  useEffect(() => {
57
135
  return () => {
58
136
  if (lastFocused.current && document.body.contains(lastFocused.current)) {
@@ -61,6 +139,7 @@ export function useToastRegion<T>(props: AriaToastRegionProps, state: ToastState
61
139
  } else {
62
140
  lastFocused.current.focus();
63
141
  }
142
+ lastFocused.current = null;
64
143
  }
65
144
  };
66
145
  }, [ref]);
@@ -73,7 +152,16 @@ export function useToastRegion<T>(props: AriaToastRegionProps, state: ToastState
73
152
  // - allows focus even outside a containing focus scope
74
153
  // - doesn’t dismiss overlays when clicking on it, even though it is outside
75
154
  // @ts-ignore
76
- 'data-react-aria-top-layer': true
155
+ 'data-react-aria-top-layer': true,
156
+ // listen to focus events separate from focuswithin because that will only fire once
157
+ // and we need to follow all focus changes
158
+ onFocus: (e) => {
159
+ let target = e.target.closest('[role="alertdialog"]');
160
+ focusedToast.current = toasts.current.findIndex(t => t === target);
161
+ },
162
+ onBlur: () => {
163
+ focusedToast.current = -1;
164
+ }
77
165
  })
78
166
  };
79
167
  }