@tamagui/v2-toast 2.0.0-1769464493958

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 (307) hide show
  1. package/dist/cjs/Toast.cjs +170 -0
  2. package/dist/cjs/Toast.js +119 -0
  3. package/dist/cjs/Toast.js.map +6 -0
  4. package/dist/cjs/Toast.native.js +174 -0
  5. package/dist/cjs/Toast.native.js.map +1 -0
  6. package/dist/cjs/ToastAnnounce.cjs +97 -0
  7. package/dist/cjs/ToastAnnounce.js +72 -0
  8. package/dist/cjs/ToastAnnounce.js.map +6 -0
  9. package/dist/cjs/ToastAnnounce.native.js +105 -0
  10. package/dist/cjs/ToastAnnounce.native.js.map +1 -0
  11. package/dist/cjs/ToastImperative.cjs +100 -0
  12. package/dist/cjs/ToastImperative.js +71 -0
  13. package/dist/cjs/ToastImperative.js.map +6 -0
  14. package/dist/cjs/ToastImperative.native.js +122 -0
  15. package/dist/cjs/ToastImperative.native.js.map +1 -0
  16. package/dist/cjs/ToastImpl.cjs +292 -0
  17. package/dist/cjs/ToastImpl.js +227 -0
  18. package/dist/cjs/ToastImpl.js.map +6 -0
  19. package/dist/cjs/ToastImpl.native.js +327 -0
  20. package/dist/cjs/ToastImpl.native.js.map +1 -0
  21. package/dist/cjs/ToastItem.cjs +466 -0
  22. package/dist/cjs/ToastItem.js +356 -0
  23. package/dist/cjs/ToastItem.js.map +6 -0
  24. package/dist/cjs/ToastItem.native.js +547 -0
  25. package/dist/cjs/ToastItem.native.js.map +1 -0
  26. package/dist/cjs/ToastPortal.cjs +44 -0
  27. package/dist/cjs/ToastPortal.js +26 -0
  28. package/dist/cjs/ToastPortal.js.map +6 -0
  29. package/dist/cjs/ToastPortal.native.js +47 -0
  30. package/dist/cjs/ToastPortal.native.js.map +1 -0
  31. package/dist/cjs/ToastProvider.cjs +146 -0
  32. package/dist/cjs/ToastProvider.js +105 -0
  33. package/dist/cjs/ToastProvider.js.map +6 -0
  34. package/dist/cjs/ToastProvider.native.js +159 -0
  35. package/dist/cjs/ToastProvider.native.js.map +1 -0
  36. package/dist/cjs/ToastState.cjs +248 -0
  37. package/dist/cjs/ToastState.js +160 -0
  38. package/dist/cjs/ToastState.js.map +6 -0
  39. package/dist/cjs/ToastState.native.js +257 -0
  40. package/dist/cjs/ToastState.native.js.map +1 -0
  41. package/dist/cjs/ToastViewport.cjs +278 -0
  42. package/dist/cjs/ToastViewport.js +263 -0
  43. package/dist/cjs/ToastViewport.js.map +6 -0
  44. package/dist/cjs/ToastViewport.native.js +316 -0
  45. package/dist/cjs/ToastViewport.native.js.map +1 -0
  46. package/dist/cjs/Toaster.cjs +219 -0
  47. package/dist/cjs/Toaster.js +177 -0
  48. package/dist/cjs/Toaster.js.map +6 -0
  49. package/dist/cjs/Toaster.native.js +279 -0
  50. package/dist/cjs/Toaster.native.js.map +1 -0
  51. package/dist/cjs/constants.cjs +28 -0
  52. package/dist/cjs/constants.js +22 -0
  53. package/dist/cjs/constants.js.map +6 -0
  54. package/dist/cjs/constants.native.js +31 -0
  55. package/dist/cjs/constants.native.js.map +1 -0
  56. package/dist/cjs/createNativeToast.cjs +51 -0
  57. package/dist/cjs/createNativeToast.js +44 -0
  58. package/dist/cjs/createNativeToast.js.map +6 -0
  59. package/dist/cjs/createNativeToast.native.js +47 -0
  60. package/dist/cjs/createNativeToast.native.js.map +1 -0
  61. package/dist/cjs/index.cjs +28 -0
  62. package/dist/cjs/index.js +22 -0
  63. package/dist/cjs/index.js.map +6 -0
  64. package/dist/cjs/index.native.js +31 -0
  65. package/dist/cjs/index.native.js.map +1 -0
  66. package/dist/cjs/types.cjs +16 -0
  67. package/dist/cjs/types.js +14 -0
  68. package/dist/cjs/types.js.map +6 -0
  69. package/dist/cjs/types.native.js +19 -0
  70. package/dist/cjs/types.native.js.map +1 -0
  71. package/dist/cjs/useDragGesture.cjs +129 -0
  72. package/dist/cjs/useDragGesture.js +100 -0
  73. package/dist/cjs/useDragGesture.js.map +6 -0
  74. package/dist/cjs/useDragGesture.native.js +146 -0
  75. package/dist/cjs/useDragGesture.native.js.map +1 -0
  76. package/dist/esm/Toast.js +107 -0
  77. package/dist/esm/Toast.js.map +6 -0
  78. package/dist/esm/Toast.mjs +131 -0
  79. package/dist/esm/Toast.mjs.map +1 -0
  80. package/dist/esm/Toast.native.js +132 -0
  81. package/dist/esm/Toast.native.js.map +1 -0
  82. package/dist/esm/ToastAnnounce.js +55 -0
  83. package/dist/esm/ToastAnnounce.js.map +6 -0
  84. package/dist/esm/ToastAnnounce.mjs +62 -0
  85. package/dist/esm/ToastAnnounce.mjs.map +1 -0
  86. package/dist/esm/ToastAnnounce.native.js +67 -0
  87. package/dist/esm/ToastAnnounce.native.js.map +1 -0
  88. package/dist/esm/ToastImperative.js +50 -0
  89. package/dist/esm/ToastImperative.js.map +6 -0
  90. package/dist/esm/ToastImperative.mjs +63 -0
  91. package/dist/esm/ToastImperative.mjs.map +1 -0
  92. package/dist/esm/ToastImperative.native.js +82 -0
  93. package/dist/esm/ToastImperative.native.js.map +1 -0
  94. package/dist/esm/ToastImpl.js +225 -0
  95. package/dist/esm/ToastImpl.js.map +6 -0
  96. package/dist/esm/ToastImpl.mjs +256 -0
  97. package/dist/esm/ToastImpl.mjs.map +1 -0
  98. package/dist/esm/ToastImpl.native.js +288 -0
  99. package/dist/esm/ToastImpl.native.js.map +1 -0
  100. package/dist/esm/ToastItem.js +339 -0
  101. package/dist/esm/ToastItem.js.map +6 -0
  102. package/dist/esm/ToastItem.mjs +432 -0
  103. package/dist/esm/ToastItem.mjs.map +1 -0
  104. package/dist/esm/ToastItem.native.js +510 -0
  105. package/dist/esm/ToastItem.native.js.map +1 -0
  106. package/dist/esm/ToastPortal.js +13 -0
  107. package/dist/esm/ToastPortal.js.map +6 -0
  108. package/dist/esm/ToastPortal.mjs +21 -0
  109. package/dist/esm/ToastPortal.mjs.map +1 -0
  110. package/dist/esm/ToastPortal.native.js +21 -0
  111. package/dist/esm/ToastPortal.native.js.map +1 -0
  112. package/dist/esm/ToastProvider.js +87 -0
  113. package/dist/esm/ToastProvider.js.map +6 -0
  114. package/dist/esm/ToastProvider.mjs +108 -0
  115. package/dist/esm/ToastProvider.mjs.map +1 -0
  116. package/dist/esm/ToastProvider.native.js +118 -0
  117. package/dist/esm/ToastProvider.native.js.map +1 -0
  118. package/dist/esm/ToastState.js +144 -0
  119. package/dist/esm/ToastState.js.map +6 -0
  120. package/dist/esm/ToastState.mjs +224 -0
  121. package/dist/esm/ToastState.mjs.map +1 -0
  122. package/dist/esm/ToastState.native.js +230 -0
  123. package/dist/esm/ToastState.native.js.map +1 -0
  124. package/dist/esm/ToastViewport.js +250 -0
  125. package/dist/esm/ToastViewport.js.map +6 -0
  126. package/dist/esm/ToastViewport.mjs +241 -0
  127. package/dist/esm/ToastViewport.mjs.map +1 -0
  128. package/dist/esm/ToastViewport.native.js +276 -0
  129. package/dist/esm/ToastViewport.native.js.map +1 -0
  130. package/dist/esm/Toaster.js +160 -0
  131. package/dist/esm/Toaster.js.map +6 -0
  132. package/dist/esm/Toaster.mjs +185 -0
  133. package/dist/esm/Toaster.mjs.map +1 -0
  134. package/dist/esm/Toaster.native.js +242 -0
  135. package/dist/esm/Toaster.native.js.map +1 -0
  136. package/dist/esm/constants.js +6 -0
  137. package/dist/esm/constants.js.map +6 -0
  138. package/dist/esm/constants.mjs +4 -0
  139. package/dist/esm/constants.mjs.map +1 -0
  140. package/dist/esm/constants.native.js +4 -0
  141. package/dist/esm/constants.native.js.map +1 -0
  142. package/dist/esm/createNativeToast.js +28 -0
  143. package/dist/esm/createNativeToast.js.map +6 -0
  144. package/dist/esm/createNativeToast.mjs +27 -0
  145. package/dist/esm/createNativeToast.mjs.map +1 -0
  146. package/dist/esm/createNativeToast.native.js +20 -0
  147. package/dist/esm/createNativeToast.native.js.map +1 -0
  148. package/dist/esm/index.js +7 -0
  149. package/dist/esm/index.js.map +6 -0
  150. package/dist/esm/index.mjs +4 -0
  151. package/dist/esm/index.mjs.map +1 -0
  152. package/dist/esm/index.native.js +4 -0
  153. package/dist/esm/index.native.js.map +1 -0
  154. package/dist/esm/types.js +1 -0
  155. package/dist/esm/types.js.map +6 -0
  156. package/dist/esm/types.mjs +2 -0
  157. package/dist/esm/types.mjs.map +1 -0
  158. package/dist/esm/types.native.js +2 -0
  159. package/dist/esm/types.native.js.map +1 -0
  160. package/dist/esm/useDragGesture.js +76 -0
  161. package/dist/esm/useDragGesture.js.map +6 -0
  162. package/dist/esm/useDragGesture.mjs +95 -0
  163. package/dist/esm/useDragGesture.mjs.map +1 -0
  164. package/dist/esm/useDragGesture.native.js +109 -0
  165. package/dist/esm/useDragGesture.native.js.map +1 -0
  166. package/dist/jsx/Toast.js +107 -0
  167. package/dist/jsx/Toast.js.map +6 -0
  168. package/dist/jsx/Toast.mjs +131 -0
  169. package/dist/jsx/Toast.mjs.map +1 -0
  170. package/dist/jsx/Toast.native.js +174 -0
  171. package/dist/jsx/Toast.native.js.map +1 -0
  172. package/dist/jsx/ToastAnnounce.js +55 -0
  173. package/dist/jsx/ToastAnnounce.js.map +6 -0
  174. package/dist/jsx/ToastAnnounce.mjs +62 -0
  175. package/dist/jsx/ToastAnnounce.mjs.map +1 -0
  176. package/dist/jsx/ToastAnnounce.native.js +105 -0
  177. package/dist/jsx/ToastAnnounce.native.js.map +1 -0
  178. package/dist/jsx/ToastImperative.js +50 -0
  179. package/dist/jsx/ToastImperative.js.map +6 -0
  180. package/dist/jsx/ToastImperative.mjs +63 -0
  181. package/dist/jsx/ToastImperative.mjs.map +1 -0
  182. package/dist/jsx/ToastImperative.native.js +122 -0
  183. package/dist/jsx/ToastImperative.native.js.map +1 -0
  184. package/dist/jsx/ToastImpl.js +225 -0
  185. package/dist/jsx/ToastImpl.js.map +6 -0
  186. package/dist/jsx/ToastImpl.mjs +256 -0
  187. package/dist/jsx/ToastImpl.mjs.map +1 -0
  188. package/dist/jsx/ToastImpl.native.js +327 -0
  189. package/dist/jsx/ToastImpl.native.js.map +1 -0
  190. package/dist/jsx/ToastItem.js +339 -0
  191. package/dist/jsx/ToastItem.js.map +6 -0
  192. package/dist/jsx/ToastItem.mjs +432 -0
  193. package/dist/jsx/ToastItem.mjs.map +1 -0
  194. package/dist/jsx/ToastItem.native.js +547 -0
  195. package/dist/jsx/ToastItem.native.js.map +1 -0
  196. package/dist/jsx/ToastPortal.js +13 -0
  197. package/dist/jsx/ToastPortal.js.map +6 -0
  198. package/dist/jsx/ToastPortal.mjs +21 -0
  199. package/dist/jsx/ToastPortal.mjs.map +1 -0
  200. package/dist/jsx/ToastPortal.native.js +47 -0
  201. package/dist/jsx/ToastPortal.native.js.map +1 -0
  202. package/dist/jsx/ToastProvider.js +87 -0
  203. package/dist/jsx/ToastProvider.js.map +6 -0
  204. package/dist/jsx/ToastProvider.mjs +108 -0
  205. package/dist/jsx/ToastProvider.mjs.map +1 -0
  206. package/dist/jsx/ToastProvider.native.js +159 -0
  207. package/dist/jsx/ToastProvider.native.js.map +1 -0
  208. package/dist/jsx/ToastState.js +144 -0
  209. package/dist/jsx/ToastState.js.map +6 -0
  210. package/dist/jsx/ToastState.mjs +224 -0
  211. package/dist/jsx/ToastState.mjs.map +1 -0
  212. package/dist/jsx/ToastState.native.js +257 -0
  213. package/dist/jsx/ToastState.native.js.map +1 -0
  214. package/dist/jsx/ToastViewport.js +250 -0
  215. package/dist/jsx/ToastViewport.js.map +6 -0
  216. package/dist/jsx/ToastViewport.mjs +241 -0
  217. package/dist/jsx/ToastViewport.mjs.map +1 -0
  218. package/dist/jsx/ToastViewport.native.js +316 -0
  219. package/dist/jsx/ToastViewport.native.js.map +1 -0
  220. package/dist/jsx/Toaster.js +160 -0
  221. package/dist/jsx/Toaster.js.map +6 -0
  222. package/dist/jsx/Toaster.mjs +185 -0
  223. package/dist/jsx/Toaster.mjs.map +1 -0
  224. package/dist/jsx/Toaster.native.js +279 -0
  225. package/dist/jsx/Toaster.native.js.map +1 -0
  226. package/dist/jsx/constants.js +6 -0
  227. package/dist/jsx/constants.js.map +6 -0
  228. package/dist/jsx/constants.mjs +4 -0
  229. package/dist/jsx/constants.mjs.map +1 -0
  230. package/dist/jsx/constants.native.js +31 -0
  231. package/dist/jsx/constants.native.js.map +1 -0
  232. package/dist/jsx/createNativeToast.js +28 -0
  233. package/dist/jsx/createNativeToast.js.map +6 -0
  234. package/dist/jsx/createNativeToast.mjs +27 -0
  235. package/dist/jsx/createNativeToast.mjs.map +1 -0
  236. package/dist/jsx/createNativeToast.native.js +47 -0
  237. package/dist/jsx/createNativeToast.native.js.map +1 -0
  238. package/dist/jsx/index.js +7 -0
  239. package/dist/jsx/index.js.map +6 -0
  240. package/dist/jsx/index.mjs +4 -0
  241. package/dist/jsx/index.mjs.map +1 -0
  242. package/dist/jsx/index.native.js +31 -0
  243. package/dist/jsx/index.native.js.map +1 -0
  244. package/dist/jsx/types.js +1 -0
  245. package/dist/jsx/types.js.map +6 -0
  246. package/dist/jsx/types.mjs +2 -0
  247. package/dist/jsx/types.mjs.map +1 -0
  248. package/dist/jsx/types.native.js +19 -0
  249. package/dist/jsx/types.native.js.map +1 -0
  250. package/dist/jsx/useDragGesture.js +76 -0
  251. package/dist/jsx/useDragGesture.js.map +6 -0
  252. package/dist/jsx/useDragGesture.mjs +95 -0
  253. package/dist/jsx/useDragGesture.mjs.map +1 -0
  254. package/dist/jsx/useDragGesture.native.js +146 -0
  255. package/dist/jsx/useDragGesture.native.js.map +1 -0
  256. package/package.json +77 -0
  257. package/src/Toast.tsx +219 -0
  258. package/src/ToastAnnounce.tsx +102 -0
  259. package/src/ToastImperative.tsx +190 -0
  260. package/src/ToastImpl.tsx +503 -0
  261. package/src/ToastItem.tsx +694 -0
  262. package/src/ToastPortal.tsx +19 -0
  263. package/src/ToastProvider.tsx +197 -0
  264. package/src/ToastState.ts +397 -0
  265. package/src/ToastViewport.tsx +430 -0
  266. package/src/Toaster.tsx +445 -0
  267. package/src/constants.ts +2 -0
  268. package/src/createNativeToast.native.tsx +22 -0
  269. package/src/createNativeToast.tsx +48 -0
  270. package/src/index.ts +17 -0
  271. package/src/types.ts +71 -0
  272. package/src/useDragGesture.native.ts +199 -0
  273. package/src/useDragGesture.ts +218 -0
  274. package/types/Toast.d.ts +84 -0
  275. package/types/Toast.d.ts.map +1 -0
  276. package/types/ToastAnnounce.d.ts +18 -0
  277. package/types/ToastAnnounce.d.ts.map +1 -0
  278. package/types/ToastImperative.d.ts +95 -0
  279. package/types/ToastImperative.d.ts.map +1 -0
  280. package/types/ToastImpl.d.ts +109 -0
  281. package/types/ToastImpl.d.ts.map +1 -0
  282. package/types/ToastItem.d.ts +34 -0
  283. package/types/ToastItem.d.ts.map +1 -0
  284. package/types/ToastPortal.d.ts +8 -0
  285. package/types/ToastPortal.d.ts.map +1 -0
  286. package/types/ToastProvider.d.ts +92 -0
  287. package/types/ToastProvider.d.ts.map +1 -0
  288. package/types/ToastState.d.ts +177 -0
  289. package/types/ToastState.d.ts.map +1 -0
  290. package/types/ToastViewport.d.ts +75 -0
  291. package/types/ToastViewport.d.ts.map +1 -0
  292. package/types/Toaster.d.ts +120 -0
  293. package/types/Toaster.d.ts.map +1 -0
  294. package/types/constants.d.ts +3 -0
  295. package/types/constants.d.ts.map +1 -0
  296. package/types/createNativeToast.d.ts +4 -0
  297. package/types/createNativeToast.d.ts.map +1 -0
  298. package/types/createNativeToast.native.d.ts +4 -0
  299. package/types/createNativeToast.native.d.ts.map +1 -0
  300. package/types/index.d.ts +7 -0
  301. package/types/index.d.ts.map +1 -0
  302. package/types/types.d.ts +61 -0
  303. package/types/types.d.ts.map +1 -0
  304. package/types/useDragGesture.d.ts +32 -0
  305. package/types/useDragGesture.d.ts.map +1 -0
  306. package/types/useDragGesture.native.d.ts +26 -0
  307. package/types/useDragGesture.native.d.ts.map +1 -0
@@ -0,0 +1,430 @@
1
+ import { AnimatePresence } from '@tamagui/animate-presence'
2
+ import { useComposedRefs } from '@tamagui/compose-refs'
3
+ import { isWeb } from '@tamagui/constants'
4
+ import type { GetProps, TamaguiElement } from '@tamagui/core'
5
+ import { styled } from '@tamagui/core'
6
+ import { PortalHost } from '@tamagui/portal'
7
+ import { YStack } from '@tamagui/stacks'
8
+ import { VisuallyHidden } from '@tamagui/visually-hidden'
9
+ import * as React from 'react'
10
+ import { TOAST_CONTEXT } from './constants'
11
+ import { ToastPortal } from './ToastPortal'
12
+ import type { ScopedProps, ToastProviderContextValue } from './ToastProvider'
13
+ import { Collection, useCollection, useToastProviderContext } from './ToastProvider'
14
+
15
+ const VIEWPORT_NAME = 'ToastViewport'
16
+ const VIEWPORT_DEFAULT_HOTKEY = ['F8']
17
+ const VIEWPORT_PAUSE = 'toast.viewportPause'
18
+ const VIEWPORT_RESUME = 'toast.viewportResume'
19
+
20
+ const ToastViewportWrapperFrame = styled(YStack, {
21
+ name: 'ViewportWrapper',
22
+
23
+ variants: {
24
+ unstyled: {
25
+ false: {
26
+ pointerEvents: 'box-none',
27
+ top: 0,
28
+ bottom: 0,
29
+ left: 0,
30
+ right: 0,
31
+ position: isWeb ? ('fixed' as any) : 'absolute',
32
+ maxWidth: '100%',
33
+ tabIndex: 0,
34
+ zIndex: 100000,
35
+ },
36
+ },
37
+ } as const,
38
+
39
+ defaultVariants: {
40
+ unstyled: process.env.TAMAGUI_HEADLESS === '1',
41
+ },
42
+ })
43
+
44
+ const ToastViewportFrame = styled(YStack, {
45
+ name: VIEWPORT_NAME,
46
+
47
+ variants: {
48
+ unstyled: {
49
+ false: {
50
+ pointerEvents: 'box-none',
51
+ position: isWeb ? ('fixed' as any) : 'absolute',
52
+ maxWidth: '100%',
53
+ },
54
+ },
55
+ } as const,
56
+
57
+ defaultVariants: {
58
+ unstyled: process.env.TAMAGUI_HEADLESS === '1',
59
+ },
60
+ })
61
+
62
+ type ToastViewportFrameProps = GetProps<typeof ToastViewportFrame>
63
+ type ToastViewportProps = ToastViewportFrameProps & {
64
+ /**
65
+ * The keys to use as the keyboard shortcut that will move focus to the toast viewport.
66
+ * @defaultValue ['F8']
67
+ */
68
+ hotkey?: string[]
69
+ /**
70
+ * An author-localized label for the toast viewport to provide context for screen reader users
71
+ * when navigating page landmarks. The available `{hotkey}` placeholder will be replaced for you.
72
+ * @defaultValue 'Notifications ({hotkey})'
73
+ */
74
+ label?: string
75
+ /**
76
+ * Used to reference the viewport if you want to have multiple viewports in the same provider.
77
+ */
78
+ name?: string
79
+ /**
80
+ * Pass this when you want to have multiple/duplicated toasts.
81
+ */
82
+ multipleToasts?: boolean
83
+ /**
84
+ * When true, uses a portal to render at the very top of the root TamaguiProvider.
85
+ */
86
+ portalToRoot?: boolean
87
+ }
88
+
89
+ const ToastViewport = React.memo(
90
+ React.forwardRef<HTMLDivElement, ToastViewportProps>(
91
+ (props: ScopedProps<ToastViewportProps>, forwardedRef) => {
92
+ const {
93
+ scope,
94
+ hotkey = VIEWPORT_DEFAULT_HOTKEY,
95
+ label = 'Notifications ({hotkey})',
96
+ name = 'default',
97
+ multipleToasts,
98
+ zIndex,
99
+ portalToRoot,
100
+ ...viewportProps
101
+ } = props
102
+ const context = useToastProviderContext(scope)
103
+ const getItems = useCollection(scope || TOAST_CONTEXT)
104
+ const headFocusProxyRef = React.useRef<FocusProxyElement>(null)
105
+ const tailFocusProxyRef = React.useRef<FocusProxyElement>(null)
106
+ const wrapperRef = React.useRef<HTMLDivElement>(null)
107
+ const ref = React.useRef<HTMLDivElement>(null)
108
+ const onViewportChange = React.useCallback(
109
+ (el: TamaguiElement) => {
110
+ if (context.viewports[name] !== el) context.onViewportChange(name, el)
111
+ },
112
+ [name, context.viewports]
113
+ )
114
+ // @ts-ignore TODO react 19 type needs fix
115
+ const composedRefs = useComposedRefs(forwardedRef, ref, onViewportChange)
116
+ const hotkeyLabel = hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, '')
117
+ const hasToasts = context.toastCount > 0
118
+
119
+ React.useEffect(() => {
120
+ if (!isWeb) return
121
+ if (context.toastCount === 0) return
122
+ const handleKeyDown = (event: KeyboardEvent) => {
123
+ // we use `event.code` as it is consistent regardless of meta keys that were pressed.
124
+ // for example, `event.key` for `Control+Alt+t` is `†` and `t !== †`
125
+ const isHotkeyPressed = hotkey.every(
126
+ (key) => (event as any)[key] || event.code === key
127
+ )
128
+ if (isHotkeyPressed) ref.current?.focus()
129
+ }
130
+ document.addEventListener('keydown', handleKeyDown)
131
+ return () => {
132
+ document.removeEventListener('keydown', handleKeyDown)
133
+ }
134
+ }, [hotkey, context.toastCount])
135
+
136
+ React.useEffect(() => {
137
+ if (!isWeb) return
138
+ if (context.toastCount === 0) return
139
+ const wrapper = wrapperRef.current
140
+ const viewport = ref.current
141
+ if (hasToasts && wrapper && viewport) {
142
+ const handlePause = () => {
143
+ if (!context.isClosePausedRef.current) {
144
+ const pauseEvent = new CustomEvent(VIEWPORT_PAUSE)
145
+ viewport.dispatchEvent(pauseEvent)
146
+ context.isClosePausedRef.current = true
147
+ }
148
+ }
149
+
150
+ const handleResume = () => {
151
+ if (context.isClosePausedRef.current) {
152
+ const resumeEvent = new CustomEvent(VIEWPORT_RESUME)
153
+ viewport.dispatchEvent(resumeEvent)
154
+ context.isClosePausedRef.current = false
155
+ }
156
+ }
157
+
158
+ const handleFocusOutResume = (event: FocusEvent) => {
159
+ const isFocusMovingOutside = !wrapper.contains(
160
+ event.relatedTarget as HTMLElement
161
+ )
162
+ if (isFocusMovingOutside) handleResume()
163
+ }
164
+
165
+ const handlePointerLeaveResume = () => {
166
+ const isFocusInside = wrapper.contains(document.activeElement)
167
+ if (!isFocusInside) handleResume()
168
+ }
169
+
170
+ // Toasts are not in the viewport React tree so we need to bind DOM events
171
+ wrapper.addEventListener('focusin', handlePause)
172
+ wrapper.addEventListener('focusout', handleFocusOutResume)
173
+ wrapper.addEventListener('pointermove', handlePause)
174
+ wrapper.addEventListener('pointerleave', handlePointerLeaveResume)
175
+ window.addEventListener('blur', handlePause)
176
+ window.addEventListener('focus', handleResume)
177
+ return () => {
178
+ wrapper.removeEventListener('focusin', handlePause)
179
+ wrapper.removeEventListener('focusout', handleFocusOutResume)
180
+ wrapper.removeEventListener('pointermove', handlePause)
181
+ wrapper.removeEventListener('pointerleave', handlePointerLeaveResume)
182
+ window.removeEventListener('blur', handlePause)
183
+ window.removeEventListener('focus', handleResume)
184
+ }
185
+ }
186
+ }, [hasToasts, context.isClosePausedRef, context.toastCount])
187
+
188
+ const getSortedTabbableCandidates = React.useCallback(
189
+ ({ tabbingDirection }: { tabbingDirection: 'forwards' | 'backwards' }) => {
190
+ const toastItems = getItems()
191
+ const tabbableCandidates = toastItems.map((toastItem) => {
192
+ const toastNode = toastItem.ref.current!
193
+ const toastTabbableCandidates = [
194
+ toastNode,
195
+ ...getTabbableCandidates(toastNode),
196
+ ]
197
+ return tabbingDirection === 'forwards'
198
+ ? toastTabbableCandidates
199
+ : toastTabbableCandidates.reverse()
200
+ })
201
+ return (
202
+ tabbingDirection === 'forwards'
203
+ ? tabbableCandidates.reverse()
204
+ : tabbableCandidates
205
+ ).flat()
206
+ },
207
+ [getItems]
208
+ )
209
+
210
+ React.useEffect(() => {
211
+ if (!isWeb) return
212
+ if (context.toastCount === 0) return
213
+
214
+ const viewport = ref.current
215
+ // We programmatically manage tabbing as we are unable to influence
216
+ // the source order with portals, this allows us to reverse the
217
+ // tab order so that it runs from most recent toast to least
218
+ if (viewport) {
219
+ const handleKeyDown = (event: KeyboardEvent) => {
220
+ const isMetaKey = event.altKey || event.ctrlKey || event.metaKey
221
+ const isTabKey = event.key === 'Tab' && !isMetaKey
222
+
223
+ if (isTabKey) {
224
+ const focusedElement = document.activeElement
225
+ const isTabbingBackwards = event.shiftKey
226
+ const targetIsViewport = event.target === viewport
227
+
228
+ // If we're back tabbing after jumping to the viewport then we simply
229
+ // proxy focus out to the preceding document
230
+ if (targetIsViewport && isTabbingBackwards) {
231
+ // @ts-ignore ali TODO type
232
+ headFocusProxyRef.current?.focus()
233
+ return
234
+ }
235
+
236
+ const tabbingDirection = isTabbingBackwards ? 'backwards' : 'forwards'
237
+ const sortedCandidates = getSortedTabbableCandidates({ tabbingDirection })
238
+ const index = sortedCandidates.findIndex(
239
+ (candidate) => candidate === focusedElement
240
+ )
241
+ if (focusFirst(sortedCandidates.slice(index + 1) as any)) {
242
+ event.preventDefault()
243
+ } else {
244
+ // If we can't focus that means we're at the edges so we
245
+ // proxy to the corresponding exit point and let the browser handle
246
+ // tab/shift+tab keypress and implicitly pass focus to the next valid element in the document
247
+ isTabbingBackwards
248
+ ? // @ts-ignore ali TODO type
249
+ headFocusProxyRef.current?.focus()
250
+ : // @ts-ignore ali TODO type
251
+ tailFocusProxyRef.current?.focus()
252
+ }
253
+ }
254
+ }
255
+
256
+ // Toasts are not in the viewport React tree so we need to bind DOM events
257
+ viewport.addEventListener('keydown', handleKeyDown)
258
+ return () => viewport.removeEventListener('keydown', handleKeyDown)
259
+ }
260
+ }, [getItems, getSortedTabbableCandidates, context.toastCount])
261
+
262
+ const contents = (
263
+ <ToastViewportWrapperFrame
264
+ ref={wrapperRef as any}
265
+ // biome-ignore lint/a11y/useSemanticElements: <explanation>
266
+ role="region"
267
+ aria-label={label.replace('{hotkey}', hotkeyLabel)}
268
+ // // Ensure virtual cursor from landmarks menus triggers focus/blur for pause/resume
269
+ tabIndex={-1}
270
+ // // incase list has size when empty (e.g. padding), we remove pointer events so
271
+ // // it doesn't prevent interactions with page elements that it overlays
272
+ // pointerEvents={hasToasts ? undefined : 'none'}
273
+ >
274
+ {hasToasts && (
275
+ <FocusProxy
276
+ context={context}
277
+ viewportName={name}
278
+ ref={headFocusProxyRef}
279
+ onFocusFromOutsideViewport={() => {
280
+ const tabbableCandidates = getSortedTabbableCandidates({
281
+ tabbingDirection: 'forwards',
282
+ })
283
+ focusFirst(tabbableCandidates as any)
284
+ }}
285
+ />
286
+ )}
287
+ {/**
288
+ * tabindex on the the list so that it can be focused when items are removed. we focus
289
+ * the list instead of the viewport so it announces number of items remaining.
290
+ */}
291
+ <Collection.Slot scope={context.toastScope}>
292
+ <ToastViewportFrame
293
+ focusable={context.toastCount > 0}
294
+ ref={composedRefs as any}
295
+ {...viewportProps}
296
+ >
297
+ <PortalHost
298
+ render={(children) => (
299
+ <AnimatePresence exitBeforeEnter={!multipleToasts}>
300
+ {children}
301
+ </AnimatePresence>
302
+ )}
303
+ name={name ?? 'default'}
304
+ />
305
+ </ToastViewportFrame>
306
+ </Collection.Slot>
307
+ {hasToasts && (
308
+ <FocusProxy
309
+ context={context}
310
+ viewportName={name}
311
+ ref={tailFocusProxyRef}
312
+ onFocusFromOutsideViewport={() => {
313
+ const tabbableCandidates = getSortedTabbableCandidates({
314
+ tabbingDirection: 'backwards',
315
+ })
316
+ focusFirst(tabbableCandidates as any)
317
+ }}
318
+ />
319
+ )}
320
+ </ToastViewportWrapperFrame>
321
+ )
322
+
323
+ if (portalToRoot) {
324
+ return (
325
+ <ToastPortal
326
+ context={context}
327
+ {...(typeof zIndex === 'number' ? { zIndex } : {})}
328
+ >
329
+ {contents}
330
+ </ToastPortal>
331
+ )
332
+ }
333
+
334
+ return contents
335
+ }
336
+ )
337
+ )
338
+
339
+ ToastViewport.displayName = VIEWPORT_NAME
340
+
341
+ /* -----------------------------------------------------------------------------------------------*/
342
+
343
+ const FOCUS_PROXY_NAME = 'ToastFocusProxy'
344
+
345
+ type FocusProxyElement = React.ElementRef<typeof VisuallyHidden>
346
+
347
+ type VisuallyHiddenProps = GetProps<typeof VisuallyHidden>
348
+
349
+ interface FocusProxyProps extends VisuallyHiddenProps {
350
+ onFocusFromOutsideViewport(): void
351
+ viewportName: string
352
+ context: ToastProviderContextValue
353
+ }
354
+
355
+ const FocusProxy = React.forwardRef<FocusProxyElement, ScopedProps<FocusProxyProps>>(
356
+ (props, forwardedRef) => {
357
+ const { onFocusFromOutsideViewport, viewportName, context, ...proxyProps } = props
358
+ const viewport = context.viewports[viewportName] as HTMLElement
359
+
360
+ return (
361
+ <VisuallyHidden
362
+ aria-hidden
363
+ tabIndex={0}
364
+ {...proxyProps}
365
+ ref={forwardedRef}
366
+ // Avoid page scrolling when focus is on the focus proxy
367
+ position={isWeb ? ('fixed' as any) : 'absolute'}
368
+ onFocus={(event) => {
369
+ if (!isWeb) return
370
+ const prevFocusedElement = event.relatedTarget as HTMLElement | null
371
+ const isFocusFromOutsideViewport = !viewport?.contains(prevFocusedElement)
372
+ if (isFocusFromOutsideViewport) onFocusFromOutsideViewport()
373
+ }}
374
+ />
375
+ )
376
+ }
377
+ )
378
+
379
+ FocusProxy.displayName = FOCUS_PROXY_NAME
380
+
381
+ /* -----------------------------------------------------------------------------------------------*/
382
+
383
+ function focusFirst(candidates: TamaguiElement[]) {
384
+ if (!isWeb) return
385
+ const previouslyFocusedElement = document.activeElement
386
+ return candidates.some((candidate) => {
387
+ // if focus is already where we want to go, we don't want to keep going through the candidates
388
+ if (candidate === previouslyFocusedElement) return true
389
+ candidate.focus()
390
+ return document.activeElement !== previouslyFocusedElement
391
+ })
392
+ }
393
+
394
+ /**
395
+ * Returns a list of potential tabbable candidates.
396
+ *
397
+ * NOTE: This is only a close approximation. For example it doesn't take into account cases like when
398
+ * elements are not visible. This cannot be worked out easily by just reading a property, but rather
399
+ * necessitate runtime knowledge (computed styles, etc). We deal with these cases separately.
400
+ *
401
+ * See: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
402
+ * Credit: https://github.com/discord/focus-layers/blob/master/src/util/wrapFocus.tsx#L1
403
+ */
404
+ function getTabbableCandidates(container: TamaguiElement) {
405
+ if (!isWeb) return []
406
+ const containerHtml = container as HTMLElement
407
+ const nodes: HTMLElement[] = []
408
+ const walker = document.createTreeWalker(containerHtml, NodeFilter.SHOW_ELEMENT, {
409
+ acceptNode: (node: any) => {
410
+ const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden'
411
+ if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP
412
+ // `.tabIndex` is not the same as the `tabindex` attribute. It works on the
413
+ // runtime's understanding of tabbability, so this automatically accounts
414
+ // for any kind of element that could be tabbed to.
415
+ return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
416
+ },
417
+ })
418
+ while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement)
419
+ // we do not take into account the order of nodes with positive `tabIndex` as it
420
+ // hinders accessibility to have tab order different from visual order.
421
+ return nodes
422
+ }
423
+
424
+ export {
425
+ ToastViewport,
426
+ VIEWPORT_DEFAULT_HOTKEY,
427
+ VIEWPORT_PAUSE,
428
+ VIEWPORT_RESUME,
429
+ type ToastViewportProps,
430
+ }