@tamagui/toast 1.7.11 → 1.8.0

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 (131) hide show
  1. package/dist/cjs/Toast.js +95 -543
  2. package/dist/cjs/Toast.js.map +3 -3
  3. package/dist/cjs/ToastAnnounce.js +93 -0
  4. package/dist/cjs/ToastAnnounce.js.map +7 -0
  5. package/dist/cjs/ToastImpl.js +354 -0
  6. package/dist/cjs/ToastImpl.js.map +7 -0
  7. package/dist/cjs/ToastProvider.js +109 -0
  8. package/dist/cjs/ToastProvider.js.map +7 -0
  9. package/dist/cjs/ToastViewport.js +307 -0
  10. package/dist/cjs/ToastViewport.js.map +7 -0
  11. package/dist/cjs/constants.js +29 -0
  12. package/dist/cjs/constants.js.map +7 -0
  13. package/dist/cjs/createNativeToast.js +49 -0
  14. package/dist/cjs/createNativeToast.js.map +7 -0
  15. package/dist/cjs/createNativeToast.native.js +37 -0
  16. package/dist/cjs/createNativeToast.native.js.map +7 -0
  17. package/dist/cjs/createToast.js +88 -0
  18. package/dist/cjs/createToast.js.map +7 -0
  19. package/dist/cjs/types.js +17 -0
  20. package/dist/cjs/types.js.map +7 -0
  21. package/dist/esm/Toast.js +101 -531
  22. package/dist/esm/Toast.js.map +3 -3
  23. package/dist/esm/Toast.mjs +101 -531
  24. package/dist/esm/Toast.mjs.map +3 -3
  25. package/dist/esm/ToastAnnounce.js +64 -0
  26. package/dist/esm/ToastAnnounce.js.map +7 -0
  27. package/dist/esm/ToastAnnounce.mjs +64 -0
  28. package/dist/esm/ToastAnnounce.mjs.map +7 -0
  29. package/dist/esm/ToastImpl.js +330 -0
  30. package/dist/esm/ToastImpl.js.map +7 -0
  31. package/dist/esm/ToastImpl.mjs +330 -0
  32. package/dist/esm/ToastImpl.mjs.map +7 -0
  33. package/dist/esm/ToastProvider.js +70 -0
  34. package/dist/esm/ToastProvider.js.map +7 -0
  35. package/dist/esm/ToastProvider.mjs +70 -0
  36. package/dist/esm/ToastProvider.mjs.map +7 -0
  37. package/dist/esm/ToastViewport.js +274 -0
  38. package/dist/esm/ToastViewport.js.map +7 -0
  39. package/dist/esm/ToastViewport.mjs +274 -0
  40. package/dist/esm/ToastViewport.mjs.map +7 -0
  41. package/dist/esm/constants.js +5 -0
  42. package/dist/esm/constants.js.map +7 -0
  43. package/dist/esm/constants.mjs +5 -0
  44. package/dist/esm/constants.mjs.map +7 -0
  45. package/dist/esm/createNativeToast.js +25 -0
  46. package/dist/esm/createNativeToast.js.map +7 -0
  47. package/dist/esm/createNativeToast.mjs +25 -0
  48. package/dist/esm/createNativeToast.mjs.map +7 -0
  49. package/dist/esm/createNativeToast.native.js +13 -0
  50. package/dist/esm/createNativeToast.native.js.map +7 -0
  51. package/dist/esm/createNativeToast.native.mjs +13 -0
  52. package/dist/esm/createNativeToast.native.mjs.map +7 -0
  53. package/dist/esm/createToast.js +54 -0
  54. package/dist/esm/createToast.js.map +7 -0
  55. package/dist/esm/createToast.mjs +54 -0
  56. package/dist/esm/createToast.mjs.map +7 -0
  57. package/dist/esm/types.js +1 -0
  58. package/dist/esm/types.js.map +7 -0
  59. package/dist/esm/types.mjs +1 -0
  60. package/dist/esm/types.mjs.map +7 -0
  61. package/dist/jsx/Toast.js +97 -509
  62. package/dist/jsx/Toast.js.map +3 -3
  63. package/dist/jsx/Toast.mjs +97 -509
  64. package/dist/jsx/Toast.mjs.map +3 -3
  65. package/dist/jsx/ToastAnnounce.js +60 -0
  66. package/dist/jsx/ToastAnnounce.js.map +7 -0
  67. package/dist/jsx/ToastAnnounce.mjs +60 -0
  68. package/dist/jsx/ToastAnnounce.mjs.map +7 -0
  69. package/dist/jsx/ToastImpl.js +309 -0
  70. package/dist/jsx/ToastImpl.js.map +7 -0
  71. package/dist/jsx/ToastImpl.mjs +309 -0
  72. package/dist/jsx/ToastImpl.mjs.map +7 -0
  73. package/dist/jsx/ToastProvider.js +65 -0
  74. package/dist/jsx/ToastProvider.js.map +7 -0
  75. package/dist/jsx/ToastProvider.mjs +65 -0
  76. package/dist/jsx/ToastProvider.mjs.map +7 -0
  77. package/dist/jsx/ToastViewport.js +254 -0
  78. package/dist/jsx/ToastViewport.js.map +7 -0
  79. package/dist/jsx/ToastViewport.mjs +254 -0
  80. package/dist/jsx/ToastViewport.mjs.map +7 -0
  81. package/dist/jsx/constants.js +5 -0
  82. package/dist/jsx/constants.js.map +7 -0
  83. package/dist/jsx/constants.mjs +5 -0
  84. package/dist/jsx/constants.mjs.map +7 -0
  85. package/dist/jsx/createNativeToast.js +25 -0
  86. package/dist/jsx/createNativeToast.js.map +7 -0
  87. package/dist/jsx/createNativeToast.mjs +25 -0
  88. package/dist/jsx/createNativeToast.mjs.map +7 -0
  89. package/dist/jsx/createNativeToast.native.js +13 -0
  90. package/dist/jsx/createNativeToast.native.js.map +7 -0
  91. package/dist/jsx/createNativeToast.native.mjs +13 -0
  92. package/dist/jsx/createNativeToast.native.mjs.map +7 -0
  93. package/dist/jsx/createToast.js +49 -0
  94. package/dist/jsx/createToast.js.map +7 -0
  95. package/dist/jsx/createToast.mjs +49 -0
  96. package/dist/jsx/createToast.mjs.map +7 -0
  97. package/dist/jsx/types.js +1 -0
  98. package/dist/jsx/types.js.map +7 -0
  99. package/dist/jsx/types.mjs +1 -0
  100. package/dist/jsx/types.mjs.map +7 -0
  101. package/package.json +14 -13
  102. package/src/Toast.tsx +139 -908
  103. package/src/ToastAnnounce.tsx +97 -0
  104. package/src/ToastImpl.tsx +439 -0
  105. package/src/ToastProvider.tsx +134 -0
  106. package/src/ToastViewport.tsx +354 -0
  107. package/src/constants.ts +1 -0
  108. package/src/createNativeToast.native.tsx +16 -0
  109. package/src/createNativeToast.tsx +25 -0
  110. package/src/createToast.tsx +93 -0
  111. package/src/types.ts +21 -0
  112. package/types/Toast.d.ts +429 -106
  113. package/types/Toast.d.ts.map +1 -1
  114. package/types/ToastAnnounce.d.ts +35 -0
  115. package/types/ToastAnnounce.d.ts.map +1 -0
  116. package/types/ToastImpl.d.ts +267 -0
  117. package/types/ToastImpl.d.ts.map +1 -0
  118. package/types/ToastProvider.d.ts +80 -0
  119. package/types/ToastProvider.d.ts.map +1 -0
  120. package/types/ToastViewport.d.ts +115 -0
  121. package/types/ToastViewport.d.ts.map +1 -0
  122. package/types/constants.d.ts +2 -0
  123. package/types/constants.d.ts.map +1 -0
  124. package/types/createNativeToast.d.ts +3 -0
  125. package/types/createNativeToast.d.ts.map +1 -0
  126. package/types/createNativeToast.native.d.ts +3 -0
  127. package/types/createNativeToast.native.d.ts.map +1 -0
  128. package/types/createToast.d.ts +42 -0
  129. package/types/createToast.d.ts.map +1 -0
  130. package/types/types.d.ts +17 -0
  131. package/types/types.d.ts.map +1 -0
package/src/Toast.tsx CHANGED
@@ -1,763 +1,63 @@
1
- // adapted from https://raw.githubusercontent.com/radix-ui/primitives/main/packages/react/toast/src/Toast.tsx
2
- // https://swiftobc.com/repo/BastiaanJansen-Toast-Swift
3
-
4
- import { AnimatePresence } from '@tamagui/animate-presence'
5
- import { useComposedRefs } from '@tamagui/compose-refs'
6
1
  import {
7
- Slot,
2
+ GetProps,
8
3
  TamaguiElement,
9
4
  composeEventHandlers,
10
- isWeb,
5
+ styled,
6
+ themeable,
11
7
  useEvent,
12
- useIsomorphicLayoutEffect,
8
+ useId,
9
+ withStaticProperties,
13
10
  } from '@tamagui/core'
14
- import { createContextScope } from '@tamagui/create-context'
15
- import type { Scope } from '@tamagui/create-context'
16
- import * as DismissableLayer from '@tamagui/dismissable'
17
- import { Portal } from '@tamagui/portal'
18
- import { YStack } from '@tamagui/stacks'
11
+ import { ThemeableStack } from '@tamagui/stacks'
12
+ import { SizableText } from '@tamagui/text'
19
13
  import { useControllableState } from '@tamagui/use-controllable-state'
20
- import { VisuallyHidden } from '@tamagui/visually-hidden'
21
14
  import * as React from 'react'
22
- import * as ReactDOM from 'react-dom'
23
-
24
- /* -------------------------------------------------------------------------------------------------
25
- * ToastProvider
26
- * -----------------------------------------------------------------------------------------------*/
27
-
28
- const PROVIDER_NAME = 'ToastProvider'
29
-
30
- type SwipeDirection = 'up' | 'down' | 'left' | 'right'
31
- type ToastProviderContextValue = {
32
- label: string
33
- duration: number
34
- swipeDirection: SwipeDirection
35
- swipeThreshold: number
36
- toastCount: number
37
- viewport: ToastViewportElement | null
38
- onViewportChange(viewport: ToastViewportElement): void
39
- onToastAdd(): void
40
- onToastRemove(): void
41
- isFocusedToastEscapeKeyDownRef: React.MutableRefObject<boolean>
42
- isClosePausedRef: React.MutableRefObject<boolean>
43
- }
44
-
45
- type ScopedProps<P> = P & { __scopeToast?: Scope }
46
- // const [createToastContext, createToastScope] = createContextScope('Toast', [createCollectionScope])
47
- const [ToastProviderProvider, useToastProviderContext] = [] as any
48
- // createToastContext<ToastProviderContextValue>(PROVIDER_NAME)
49
-
50
- interface ToastProviderProps {
51
- children?: React.ReactNode
52
- /**
53
- * An author-localized label for each toast. Used to help screen reader users
54
- * associate the interruption with a toast.
55
- * @defaultValue 'Notification'
56
- */
57
- label?: string
58
- /**
59
- * Time in milliseconds that each toast should remain visible for.
60
- * @defaultValue 5000
61
- */
62
- duration?: number
63
- /**
64
- * Direction of pointer swipe that should close the toast.
65
- * @defaultValue 'right'
66
- */
67
- swipeDirection?: SwipeDirection
68
- /**
69
- * Distance in pixels that the swipe must pass before a close is triggered.
70
- * @defaultValue 50
71
- */
72
- swipeThreshold?: number
73
- }
74
-
75
- const ToastProvider: React.FC<ToastProviderProps> = (
76
- props: ScopedProps<ToastProviderProps>,
77
- ) => {
78
- const {
79
- __scopeToast,
80
- label = 'Notification',
81
- duration = 5000,
82
- swipeDirection = 'right',
83
- swipeThreshold = 50,
84
- children,
85
- } = props
86
- const [viewport, setViewport] = React.useState<ToastViewportElement | null>(null)
87
- const [toastCount, setToastCount] = React.useState(0)
88
- const isFocusedToastEscapeKeyDownRef = React.useRef(false)
89
- const isClosePausedRef = React.useRef(false)
90
- return (
91
- // <Collection.Provider scope={__scopeToast}>
92
- <ToastProviderProvider
93
- scope={__scopeToast}
94
- label={label}
95
- duration={duration}
96
- swipeDirection={swipeDirection}
97
- swipeThreshold={swipeThreshold}
98
- toastCount={toastCount}
99
- viewport={viewport}
100
- onViewportChange={setViewport}
101
- onToastAdd={React.useCallback(
102
- () => setToastCount((prevCount) => prevCount + 1),
103
- [],
104
- )}
105
- onToastRemove={React.useCallback(
106
- () => setToastCount((prevCount) => prevCount - 1),
107
- [],
108
- )}
109
- isFocusedToastEscapeKeyDownRef={isFocusedToastEscapeKeyDownRef}
110
- isClosePausedRef={isClosePausedRef}
111
- >
112
- {children}
113
- </ToastProviderProvider>
114
- // </Collection.Provider>
115
- )
116
- }
117
-
118
- ToastProvider.propTypes = {
119
- label(props) {
120
- if (props.label && typeof props.label === 'string' && !props.label.trim()) {
121
- const error = `Invalid prop \`label\` supplied to \`${PROVIDER_NAME}\`. Expected non-empty \`string\`.`
122
- return new Error(error)
123
- }
124
- return null
125
- },
126
- }
127
-
128
- ToastProvider.displayName = PROVIDER_NAME
129
-
130
- /* -------------------------------------------------------------------------------------------------
131
- * ToastViewport
132
- * -----------------------------------------------------------------------------------------------*/
133
-
134
- const VIEWPORT_NAME = 'ToastViewport'
135
- const VIEWPORT_DEFAULT_HOTKEY = ['F8']
136
- const VIEWPORT_PAUSE = 'toast.viewportPause'
137
- const VIEWPORT_RESUME = 'toast.viewportResume'
138
-
139
- type ToastViewportElement = TamaguiElement
140
- type PrimitiveOrderedListProps = any //Radix.ComponentPropsWithoutRef<typeof Primitive.ol>
141
- interface ToastViewportProps extends PrimitiveOrderedListProps {
142
- /**
143
- * The keys to use as the keyboard shortcut that will move focus to the toast viewport.
144
- * @defaultValue ['F8']
145
- */
146
- hotkey?: string[]
147
- /**
148
- * An author-localized label for the toast viewport to provide context for screen reader users
149
- * when navigating page landmarks. The available `{hotkey}` placeholder will be replaced for you.
150
- * @defaultValue 'Notifications ({hotkey})'
151
- */
152
- label?: string
153
- }
154
-
155
- const ToastViewport = React.forwardRef<ToastViewportElement, ToastViewportProps>(
156
- (props: ScopedProps<ToastViewportProps>, forwardedRef) => {
157
- const {
158
- __scopeToast,
159
- hotkey = VIEWPORT_DEFAULT_HOTKEY,
160
- label = 'Notifications ({hotkey})',
161
- ...viewportProps
162
- } = props
163
- const context = useToastProviderContext(VIEWPORT_NAME, __scopeToast)
164
- // const getItems = useCollection(__scopeToast)
165
- const wrapperRef = React.useRef<HTMLDivElement>(null)
166
- const headFocusProxyRef = React.useRef<FocusProxyElement>(null)
167
- const tailFocusProxyRef = React.useRef<FocusProxyElement>(null)
168
- const ref = React.useRef<ToastViewportElement>(null)
169
- const composedRefs = useComposedRefs(forwardedRef, ref, context.onViewportChange)
170
- const hotkeyLabel = hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, '')
171
- const hasToasts = context.toastCount > 0
172
-
173
- React.useEffect(() => {
174
- const handleKeyDown = (event: KeyboardEvent) => {
175
- // we use `event.code` as it is consistent regardless of meta keys that were pressed.
176
- // for example, `event.key` for `Control+Alt+t` is `†` and `t !== †`
177
- const isHotkeyPressed = hotkey.every(
178
- (key) => (event as any)[key] || event.code === key,
179
- )
180
- if (isHotkeyPressed) ref.current?.focus()
181
- }
182
- document.addEventListener('keydown', handleKeyDown)
183
- return () => document.removeEventListener('keydown', handleKeyDown)
184
- }, [hotkey])
185
-
186
- React.useEffect(() => {
187
- const wrapper = wrapperRef.current
188
- const viewport = ref.current
189
- if (wrapper && viewport) {
190
- const handlePause = () => {
191
- const pauseEvent = new CustomEvent(VIEWPORT_PAUSE)
192
- // viewport.dispatchEvent(pauseEvent)
193
- context.isClosePausedRef.current = true
194
- }
195
-
196
- const handleResume = () => {
197
- const resumeEvent = new CustomEvent(VIEWPORT_RESUME)
198
- // viewport.dispatchEvent(resumeEvent)
199
- context.isClosePausedRef.current = false
200
- }
201
-
202
- // Toasts are not in the viewport React tree so we need to bind DOM events
203
- wrapper.addEventListener('focusin', handlePause)
204
- wrapper.addEventListener('focusout', handleResume)
205
- wrapper.addEventListener('pointerenter', handlePause)
206
- wrapper.addEventListener('pointerleave', handleResume)
207
- window.addEventListener('blur', handlePause)
208
- window.addEventListener('focus', handleResume)
209
- return () => {
210
- wrapper.removeEventListener('focusin', handlePause)
211
- wrapper.removeEventListener('focusout', handleResume)
212
- wrapper.removeEventListener('pointerenter', handlePause)
213
- wrapper.removeEventListener('pointerleave', handleResume)
214
- window.removeEventListener('blur', handlePause)
215
- window.removeEventListener('focus', handleResume)
216
- }
217
- }
218
- }, [context.isClosePausedRef])
219
-
220
- // const getSortedTabbableCandidates = React.useCallback(
221
- // ({ tabbingDirection }: { tabbingDirection: 'forwards' | 'backwards' }) => {
222
- // const toastItems = getItems()
223
- // const tabbableCandidates = toastItems.map((toastItem) => {
224
- // const toastNode = toastItem.ref.current!
225
- // const toastTabbableCandidates = [toastNode, ...getTabbableCandidates(toastNode)]
226
- // return tabbingDirection === 'forwards'
227
- // ? toastTabbableCandidates
228
- // : toastTabbableCandidates.reverse()
229
- // })
230
- // return (
231
- // tabbingDirection === 'forwards' ? tabbableCandidates.reverse() : tabbableCandidates
232
- // ).flat()
233
- // },
234
- // [getItems]
235
- // )
236
-
237
- React.useEffect(
238
- () => {
239
- const viewport = ref.current
240
- // We programmatically manage tabbing as we are unable to influence
241
- // the source order with portals, this allows us to reverse the
242
- // tab order so that it runs from most recent toast to least
243
- if (viewport) {
244
- const handleKeyDown = (event: KeyboardEvent) => {
245
- const isMetaKey = event.altKey || event.ctrlKey || event.metaKey
246
- const isTabKey = event.key === 'Tab' && !isMetaKey
247
-
248
- if (isTabKey) {
249
- const focusedElement = document.activeElement
250
- const isTabbingBackwards = event.shiftKey
251
- const targetIsViewport = event.target === viewport
252
-
253
- // If we're back tabbing after jumping to the viewport then we simply
254
- // proxy focus out to the preceding document
255
- if (targetIsViewport && isTabbingBackwards) {
256
- headFocusProxyRef.current?.focus()
257
- return
258
- }
259
-
260
- const tabbingDirection = isTabbingBackwards ? 'backwards' : 'forwards'
261
- // const sortedCandidates = getSortedTabbableCandidates({ tabbingDirection })
262
- // const index = sortedCandidates.findIndex((candidate) => candidate === focusedElement)
263
- // if (focusFirst(sortedCandidates.slice(index + 1))) {
264
- // event.preventDefault()
265
- // } else {
266
- // // If we can't focus that means we're at the edges so we
267
- // // proxy to the corresponding exit point and let the browser handle
268
- // // tab/shift+tab keypress and implicitly pass focus to the next valid element in the document
269
- // isTabbingBackwards
270
- // ? headFocusProxyRef.current?.focus()
271
- // : tailFocusProxyRef.current?.focus()
272
- // }
273
- }
274
- }
275
-
276
- // Toasts are not in the viewport React tree so we need to bind DOM events
277
- // viewport.addEventListener('keydown', handleKeyDown)
278
- return () => {
279
- // viewport.removeEventListener('keydown', handleKeyDown)
280
- }
281
- }
282
- },
283
- [
284
- // getItems, getSortedTabbableCandidates
285
- ],
286
- )
287
-
288
- return null
289
- // return (
290
- // <DismissableLayer.Branch
291
- // ref={wrapperRef}
292
- // role="region"
293
- // aria-label={label.replace('{hotkey}', hotkeyLabel)}
294
- // // Ensure virtual cursor from landmarks menus triggers focus/blur for pause/resume
295
- // tabIndex={-1}
296
- // // incase list has size when empty (e.g. padding), we remove pointer events so
297
- // // it doesn't prevent interactions with page elements that it overlays
298
- // style={{ pointerEvents: hasToasts ? undefined : 'none' }}
299
- // >
300
- // {hasToasts && (
301
- // <FocusProxy
302
- // ref={headFocusProxyRef}
303
- // onFocusFromOutsideViewport={() => {
304
- // const tabbableCandidates = getSortedTabbableCandidates({
305
- // tabbingDirection: 'forwards',
306
- // })
307
- // focusFirst(tabbableCandidates)
308
- // }}
309
- // />
310
- // )}
311
- // {/**
312
- // * tabindex on the the list so that it can be focused when items are removed. we focus
313
- // * the list instead of the viewport so it announces number of items remaining.
314
- // */}
315
- // <Slot scope={__scopeToast}>
316
- // <Primitive.ol tabIndex={-1} {...viewportProps} ref={composedRefs} />
317
- // </Slot>
318
- // {hasToasts && (
319
- // <FocusProxy
320
- // ref={tailFocusProxyRef}
321
- // onFocusFromOutsideViewport={() => {
322
- // const tabbableCandidates = getSortedTabbableCandidates({
323
- // tabbingDirection: 'backwards',
324
- // })
325
- // focusFirst(tabbableCandidates)
326
- // }}
327
- // />
328
- // )}
329
- // </DismissableLayer.Branch>
330
- // )
331
- },
332
- )
333
-
334
- ToastViewport.displayName = VIEWPORT_NAME
335
-
336
- /* -----------------------------------------------------------------------------------------------*/
337
-
338
- const FOCUS_PROXY_NAME = 'ToastFocusProxy'
339
15
 
340
- type FocusProxyElement = React.ElementRef<typeof VisuallyHidden>
341
- type VisuallyHiddenProps = any // Radix.ComponentPropsWithoutRef<typeof VisuallyHidden>
342
- interface FocusProxyProps extends VisuallyHiddenProps {
343
- onFocusFromOutsideViewport(): void
344
- }
345
-
346
- const FocusProxy = React.forwardRef<FocusProxyElement, ScopedProps<FocusProxyProps>>(
347
- (props, forwardedRef) => {
348
- const { __scopeToast, onFocusFromOutsideViewport, ...proxyProps } = props
349
- const context = useToastProviderContext(FOCUS_PROXY_NAME, __scopeToast)
350
-
351
- return (
352
- <VisuallyHidden
353
- aria-hidden
354
- // @ts-ignore
355
- tabIndex={0}
356
- {...proxyProps}
357
- ref={forwardedRef}
358
- // Avoid page scrolling when focus is on the focus proxy
359
- position={isWeb ? ('fixed' as any) : 'absolute'}
360
- onFocus={(event) => {
361
- const prevFocusedElement = event.relatedTarget as HTMLElement | null
362
- const isFocusFromOutsideViewport =
363
- !context.viewport?.contains(prevFocusedElement)
364
- if (isFocusFromOutsideViewport) onFocusFromOutsideViewport()
365
- }}
366
- />
367
- )
368
- },
369
- )
370
-
371
- FocusProxy.displayName = FOCUS_PROXY_NAME
16
+ import { TOAST_NAME } from './constants'
17
+ import { createToast } from './createToast'
18
+ import { ToastAnnounceExclude } from './ToastAnnounce'
19
+ import {
20
+ ToastImpl,
21
+ ToastImplFrame,
22
+ ToastProps,
23
+ useToastInteractiveContext,
24
+ } from './ToastImpl'
25
+ import {
26
+ ScopedProps,
27
+ ToastProvider,
28
+ ToastProviderProps,
29
+ createToastScope,
30
+ } from './ToastProvider'
31
+ import { ToastViewport, ToastViewportProps } from './ToastViewport'
372
32
 
373
33
  /* -------------------------------------------------------------------------------------------------
374
- * Toast
34
+ * ToastTitle
375
35
  * -----------------------------------------------------------------------------------------------*/
376
36
 
377
- const TOAST_NAME = 'Toast'
378
- const TOAST_SWIPE_START = 'toast.swipeStart'
379
- const TOAST_SWIPE_MOVE = 'toast.swipeMove'
380
- const TOAST_SWIPE_CANCEL = 'toast.swipeCancel'
381
- const TOAST_SWIPE_END = 'toast.swipeEnd'
382
-
383
- type ToastElement = ToastImplElement
384
- interface ToastProps extends Omit<ToastImplProps, keyof ToastImplPrivateProps> {
385
- open?: boolean
386
- defaultOpen?: boolean
387
- onOpenChange?(open: boolean): void
388
- /**
389
- * Used to force mounting when more control is needed. Useful when
390
- * controlling animation with React animation libraries.
391
- */
392
- forceMount?: true
393
- }
394
-
395
- const Toast = React.forwardRef<ToastElement, ToastProps>(
396
- (props: ScopedProps<ToastProps>, forwardedRef) => {
397
- const {
398
- forceMount,
399
- open: openProp,
400
- defaultOpen,
401
- onOpenChange,
402
- ...toastProps
403
- } = props
404
- const [open, setOpen] = useControllableState({
405
- prop: openProp,
406
- defaultProp: true,
407
- onChange: onOpenChange,
408
- transition: true,
409
- })
410
- return (
411
- // <Presence present={forceMount || open}>
412
- <ToastImpl
413
- open={open}
414
- {...toastProps}
415
- ref={forwardedRef}
416
- onClose={() => setOpen(false)}
417
- // onSwipeStart={composeEventHandlers(props.onSwipeStart, (event) => {
418
- // event.currentTarget.setAttribute('data-swipe', 'start')
419
- // })}
420
- // onSwipeMove={composeEventHandlers(props.onSwipeMove, (event) => {
421
- // const { x, y } = event.detail.delta
422
- // event.currentTarget.setAttribute('data-swipe', 'move')
423
- // event.currentTarget.style.setProperty('--radix-toast-swipe-move-x', `${x}px`)
424
- // event.currentTarget.style.setProperty('--radix-toast-swipe-move-y', `${y}px`)
425
- // })}
426
- // onSwipeCancel={composeEventHandlers(props.onSwipeCancel, (event) => {
427
- // event.currentTarget.setAttribute('data-swipe', 'cancel')
428
- // event.currentTarget.style.removeProperty('--radix-toast-swipe-move-x')
429
- // event.currentTarget.style.removeProperty('--radix-toast-swipe-move-y')
430
- // event.currentTarget.style.removeProperty('--radix-toast-swipe-end-x')
431
- // event.currentTarget.style.removeProperty('--radix-toast-swipe-end-y')
432
- // })}
433
- // onSwipeEnd={composeEventHandlers(props.onSwipeEnd, (event) => {
434
- // const { x, y } = event.detail.delta
435
- // event.currentTarget.setAttribute('data-swipe', 'end')
436
- // event.currentTarget.style.removeProperty('--radix-toast-swipe-move-x')
437
- // event.currentTarget.style.removeProperty('--radix-toast-swipe-move-y')
438
- // event.currentTarget.style.setProperty('--radix-toast-swipe-end-x', `${x}px`)
439
- // event.currentTarget.style.setProperty('--radix-toast-swipe-end-y', `${y}px`)
440
- // setOpen(false)
441
- // })}
442
- />
443
- // </Presence>
444
- )
445
- },
446
- )
447
-
448
- Toast.displayName = TOAST_NAME
449
-
450
- /* -----------------------------------------------------------------------------------------------*/
451
-
452
- type SwipeEvent = { currentTarget: EventTarget & ToastElement } & Omit<
453
- CustomEvent<{
454
- originalEvent: React.PointerEvent
455
- delta: { x: number; y: number }
456
- }>,
457
- 'currentTarget'
458
- >
459
-
460
- const [ToastInteractiveProvider, useToastInteractiveContext] = {} as any
461
- // createToastContext(TOAST_NAME, {
462
- // isInteractive: false,
463
- // onClose() {},
464
- // })
465
-
466
- type ToastImplElement = TamaguiElement
467
- type DismissableLayerProps = any //Radix.ComponentPropsWithoutRef<typeof DismissableLayer.Root>
468
- type ToastImplPrivateProps = { open: boolean; onClose(): void }
469
- type PrimitiveListItemProps = any //Radix.ComponentPropsWithoutRef<typeof Primitive.li>
470
- interface ToastImplProps extends ToastImplPrivateProps, PrimitiveListItemProps {
471
- type?: 'foreground' | 'background'
472
- /**
473
- * Time in milliseconds that toast should remain visible for. Overrides value
474
- * given to `ToastProvider`.
475
- */
476
- duration?: number
477
- onEscapeKeyDown?: DismissableLayerProps['onEscapeKeyDown']
478
- onSwipeStart?(event: SwipeEvent): void
479
- onSwipeMove?(event: SwipeEvent): void
480
- onSwipeCancel?(event: SwipeEvent): void
481
- onSwipeEnd?(event: SwipeEvent): void
482
- }
483
-
484
- const ToastImpl = React.forwardRef<ToastImplElement, ToastImplProps>(
485
- (
486
- props: ScopedProps<ToastImplProps> & { children?: React.ReactNode },
487
- forwardedRef,
488
- ) => {
489
- const {
490
- __scopeToast,
491
- type = 'foreground',
492
- duration: durationProp,
493
- open,
494
- onClose,
495
- onEscapeKeyDown,
496
- onSwipeStart,
497
- onSwipeMove,
498
- onSwipeCancel,
499
- onSwipeEnd,
500
- ...toastProps
501
- } = props
502
- const context = useToastProviderContext(TOAST_NAME, __scopeToast)
503
- const ref = React.useRef<ToastImplElement>(null)
504
- const composedRefs = useComposedRefs(forwardedRef, ref)
505
- const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null)
506
- const swipeDeltaRef = React.useRef<{ x: number; y: number } | null>(null)
507
- const duration = durationProp || context.duration
508
- const closeTimerStartTimeRef = React.useRef(0)
509
- const closeTimerRemainingTimeRef = React.useRef(duration)
510
- const closeTimerRef = React.useRef(0)
511
- const { onToastAdd, onToastRemove } = context
512
- // was useCallbackRef
513
- const handleClose = useEvent(() => {
514
- // focus viewport if focus is within toast to read the remaining toast
515
- // count to SR users and ensure focus isn't lost
516
- // const isFocusInToast = ref.current?.contains(document.activeElement)
517
- // if (isFocusInToast) context.viewport?.focus()
518
- onClose()
519
- })
37
+ const TITLE_NAME = 'ToastTitle'
520
38
 
521
- const startTimer = React.useCallback(
522
- (duration: number) => {
523
- if (!duration || duration === Infinity) return
524
- window.clearTimeout(closeTimerRef.current)
525
- closeTimerStartTimeRef.current = new Date().getTime()
526
- closeTimerRef.current = window.setTimeout(handleClose, duration)
39
+ const ToastTitleFrame = styled(SizableText, {
40
+ name: TITLE_NAME,
41
+ color: '$color',
42
+ variants: {
43
+ unstyled: {
44
+ false: {
45
+ size: '$4',
527
46
  },
528
- [handleClose],
529
- )
530
-
531
- React.useEffect(() => {
532
- const viewport = context.viewport
533
- if (viewport) {
534
- const handleResume = () => {
535
- startTimer(closeTimerRemainingTimeRef.current)
536
- }
537
- const handlePause = () => {
538
- const elapsedTime = new Date().getTime() - closeTimerStartTimeRef.current
539
- closeTimerRemainingTimeRef.current =
540
- closeTimerRemainingTimeRef.current - elapsedTime
541
- window.clearTimeout(closeTimerRef.current)
542
- }
543
- viewport.addEventListener(VIEWPORT_PAUSE, handlePause)
544
- viewport.addEventListener(VIEWPORT_RESUME, handleResume)
545
- return () => {
546
- viewport.removeEventListener(VIEWPORT_PAUSE, handlePause)
547
- viewport.removeEventListener(VIEWPORT_RESUME, handleResume)
548
- }
549
- }
550
- }, [context.viewport, duration, startTimer])
551
-
552
- // start timer when toast opens or duration changes.
553
- // we include `open` in deps because closed !== unmounted when animating
554
- // so it could reopen before being completely unmounted
555
- React.useEffect(() => {
556
- if (open && !context.isClosePausedRef.current) startTimer(duration)
557
- }, [open, duration, context.isClosePausedRef, startTimer])
558
-
559
- React.useEffect(() => {
560
- onToastAdd()
561
- return () => onToastRemove()
562
- }, [onToastAdd, onToastRemove])
563
-
564
- if (!context.viewport) return null
565
-
566
- return (
567
- <>
568
- <ToastAnnounce
569
- __scopeToast={__scopeToast}
570
- // Toasts are always role=status to avoid stuttering issues with role=alert in SRs.
571
- role="status"
572
- aria-live={type === 'foreground' ? 'assertive' : 'polite'}
573
- aria-atomic
574
- >
575
- {props.children}
576
- </ToastAnnounce>
577
-
578
- <ToastInteractiveProvider
579
- scope={__scopeToast}
580
- isInteractive
581
- onClose={handleClose}
582
- >
583
- {ReactDOM.createPortal(
584
- <div />,
585
- // <Collection.ItemSlot scope={__scopeToast}>
586
- // <DismissableLayer.Root
587
- // asChild
588
- // onEscapeKeyDown={composeEventHandlers(onEscapeKeyDown, () => {
589
- // if (!context.isFocusedToastEscapeKeyDownRef.current) handleClose()
590
- // context.isFocusedToastEscapeKeyDownRef.current = false
591
- // })}
592
- // >
593
- // <Primitive.li
594
- // // Ensure toasts are announced as status list or status when focused
595
- // role="status"
596
- // aria-live="off"
597
- // aria-atomic
598
- // tabIndex={0}
599
- // data-state={open ? 'open' : 'closed'}
600
- // data-swipe-direction={context.swipeDirection}
601
- // {...toastProps}
602
- // ref={composedRefs}
603
- // style={{ userSelect: 'none', touchAction: 'none', ...props.style }}
604
- // onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
605
- // if (event.key !== 'Escape') return
606
- // onEscapeKeyDown?.(event.nativeEvent)
607
- // if (!event.nativeEvent.defaultPrevented) {
608
- // context.isFocusedToastEscapeKeyDownRef.current = true
609
- // handleClose()
610
- // }
611
- // })}
612
- // onPointerDown={composeEventHandlers(props.onPointerDown, (event) => {
613
- // if (event.button !== 0) return
614
- // pointerStartRef.current = { x: event.clientX, y: event.clientY }
615
- // })}
616
- // onPointerMove={composeEventHandlers(props.onPointerMove, (event) => {
617
- // if (!pointerStartRef.current) return
618
- // const x = event.clientX - pointerStartRef.current.x
619
- // const y = event.clientY - pointerStartRef.current.y
620
- // const hasSwipeMoveStarted = Boolean(swipeDeltaRef.current)
621
- // const isHorizontalSwipe = ['left', 'right'].includes(context.swipeDirection)
622
- // const clamp = ['left', 'up'].includes(context.swipeDirection)
623
- // ? Math.min
624
- // : Math.max
625
- // const clampedX = isHorizontalSwipe ? clamp(0, x) : 0
626
- // const clampedY = !isHorizontalSwipe ? clamp(0, y) : 0
627
- // const moveStartBuffer = event.pointerType === 'touch' ? 10 : 2
628
- // const delta = { x: clampedX, y: clampedY }
629
- // const eventDetail = { originalEvent: event, delta }
630
- // if (hasSwipeMoveStarted) {
631
- // swipeDeltaRef.current = delta
632
- // handleAndDispatchCustomEvent(TOAST_SWIPE_MOVE, onSwipeMove, eventDetail, {
633
- // discrete: false,
634
- // })
635
- // } else if (isDeltaInDirection(delta, context.swipeDirection, moveStartBuffer)) {
636
- // swipeDeltaRef.current = delta
637
- // handleAndDispatchCustomEvent(TOAST_SWIPE_START, onSwipeStart, eventDetail, {
638
- // discrete: false,
639
- // })
640
- // ;(event.target as HTMLElement).setPointerCapture(event.pointerId)
641
- // } else if (Math.abs(x) > moveStartBuffer || Math.abs(y) > moveStartBuffer) {
642
- // // User is swiping in wrong direction so we disable swipe gesture
643
- // // for the current pointer down interaction
644
- // pointerStartRef.current = null
645
- // }
646
- // })}
647
- // onPointerUp={composeEventHandlers(props.onPointerUp, (event) => {
648
- // const delta = swipeDeltaRef.current
649
- // ;(event.target as HTMLElement).releasePointerCapture(event.pointerId)
650
- // swipeDeltaRef.current = null
651
- // pointerStartRef.current = null
652
- // if (delta) {
653
- // const toast = event.currentTarget
654
- // const eventDetail = { originalEvent: event, delta }
655
- // if (isDeltaInDirection(delta, context.swipeDirection, context.swipeThreshold)) {
656
- // handleAndDispatchCustomEvent(TOAST_SWIPE_END, onSwipeEnd, eventDetail, {
657
- // discrete: true,
658
- // })
659
- // } else {
660
- // handleAndDispatchCustomEvent(TOAST_SWIPE_CANCEL, onSwipeCancel, eventDetail, {
661
- // discrete: true,
662
- // })
663
- // }
664
- // // Prevent click event from triggering on items within the toast when
665
- // // pointer up is part of a swipe gesture
666
- // toast.addEventListener('click', (event) => event.preventDefault(), {
667
- // once: true,
668
- // })
669
- // }
670
- // })}
671
- // />
672
- // </DismissableLayer.Root>,
673
- // </Collection.ItemSlot>,
674
- context.viewport,
675
- )}
676
- </ToastInteractiveProvider>
677
- </>
678
- )
47
+ },
679
48
  },
680
- )
681
-
682
- ToastImpl.propTypes = {
683
- type(props) {
684
- if (props.type && !['foreground', 'background'].includes(props.type)) {
685
- const error = `Invalid prop \`type\` supplied to \`${TOAST_NAME}\`. Expected \`foreground | background\`.`
686
- return new Error(error)
687
- }
688
- return null
49
+ defaultVariants: {
50
+ unstyled: false,
689
51
  },
690
- }
691
-
692
- /* -----------------------------------------------------------------------------------------------*/
693
-
694
- type ToastAnnounceProps = any
695
- // extends React.ComponentPropsWithoutRef<'div'>,
696
- // ScopedProps<{ children?: ToastImplProps['children'] }> {}
697
-
698
- const ToastAnnounce: React.FC<ToastAnnounceProps> = (
699
- props: ScopedProps<ToastAnnounceProps>,
700
- ) => {
701
- const { __scopeToast, children, ...announceProps } = props
702
- const context = useToastProviderContext(TOAST_NAME, __scopeToast)
703
- const [renderAnnounceText, setRenderAnnounceText] = React.useState(false)
704
- const [isAnnounced, setIsAnnounced] = React.useState(false)
705
- const [fragment, setFragment] = React.useState<DocumentFragment>()
706
- const [rootFragmentNode, setRootFragmentNode] =
707
- React.useState<HTMLDivElement | null>(null)
708
-
709
- // We portal children into a document fragment so that we can extract the bare text nodes
710
- // before rendering to the DOM. This avoids issues with duplicate `children`
711
- // and animation libraries when composing via `asChild`.
712
- const announceTextContent = React.useMemo(() => {
713
- return rootFragmentNode ? getAnnounceTextContent(rootFragmentNode) : null
714
- }, [rootFragmentNode])
715
-
716
- // setting the fragment in `useIsomorphicLayoutEffect` as `DocumentFragment` doesn't exist on the server
717
- useIsomorphicLayoutEffect(() => {
718
- setFragment(new DocumentFragment())
719
- }, [])
720
-
721
- // render text content in the next frame to ensure toast is announced in NVDA
722
- useNextFrame(() => setRenderAnnounceText(true))
723
-
724
- React.useEffect(() => {
725
- const timer = window.setTimeout(() => setIsAnnounced(true), 1000)
726
- return () => window.clearTimeout(timer)
727
- }, [])
728
-
729
- return isAnnounced ? null : (
730
- <>
731
- {fragment && (
732
- // @ts-ignore
733
- <Portal container={fragment as any} ref={setRootFragmentNode}>
734
- {context.label} {children}
735
- </Portal>
736
- )}
737
- <Portal asChild>
738
- <VisuallyHidden {...announceProps}>
739
- {renderAnnounceText && announceTextContent}
740
- </VisuallyHidden>
741
- </Portal>
742
- </>
743
- )
744
- }
745
-
746
- /* -------------------------------------------------------------------------------------------------
747
- * ToastTitle
748
- * -----------------------------------------------------------------------------------------------*/
749
-
750
- const TITLE_NAME = 'ToastTitle'
751
-
752
- type ToastTitleElement = TamaguiElement
753
- type PrimitiveDivProps = any // Radix.ComponentPropsWithoutRef<typeof YStack>
754
- interface ToastTitleProps extends PrimitiveDivProps {}
52
+ })
53
+ type PrimitiveDivProps = GetProps<typeof ToastTitleFrame>
54
+ type ToastTitleProps = PrimitiveDivProps & {}
755
55
 
756
- const ToastTitle = React.forwardRef<ToastTitleElement, ToastTitleProps>(
56
+ const ToastTitle = React.forwardRef<TamaguiElement, ToastTitleProps>(
757
57
  (props: ScopedProps<ToastTitleProps>, forwardedRef) => {
758
58
  const { __scopeToast, ...titleProps } = props
759
- return <YStack {...titleProps} ref={forwardedRef} />
760
- },
59
+ return <ToastTitleFrame {...titleProps} ref={forwardedRef} />
60
+ }
761
61
  )
762
62
 
763
63
  ToastTitle.displayName = TITLE_NAME
@@ -768,16 +68,29 @@ ToastTitle.displayName = TITLE_NAME
768
68
 
769
69
  const DESCRIPTION_NAME = 'ToastDescription'
770
70
 
771
- type ToastDescriptionElement = React.ElementRef<typeof YStack>
772
- interface ToastDescriptionProps extends PrimitiveDivProps {}
773
-
774
- const ToastDescription = React.forwardRef<
775
- ToastDescriptionElement,
776
- ToastDescriptionProps
777
- >((props: ScopedProps<ToastDescriptionProps>, forwardedRef) => {
778
- const { __scopeToast, ...descriptionProps } = props
779
- return <YStack {...descriptionProps} ref={forwardedRef} />
71
+ const ToastDescriptionFrame = styled(SizableText, {
72
+ name: DESCRIPTION_NAME,
73
+ color: '$color11',
74
+ variants: {
75
+ unstyled: {
76
+ false: {
77
+ size: '$1',
78
+ },
79
+ },
80
+ },
81
+ defaultVariants: {
82
+ unstyled: false,
83
+ },
780
84
  })
85
+ type ToastDescriptionFrameProps = GetProps<typeof ToastDescriptionFrame>
86
+ type ToastDescriptionProps = ToastDescriptionFrameProps & {}
87
+
88
+ const ToastDescription = React.forwardRef<TamaguiElement, ToastDescriptionProps>(
89
+ (props: ScopedProps<ToastDescriptionProps>, forwardedRef) => {
90
+ const { __scopeToast, ...descriptionProps } = props
91
+ return <ToastDescriptionFrame {...descriptionProps} ref={forwardedRef} />
92
+ }
93
+ )
781
94
 
782
95
  ToastDescription.displayName = DESCRIPTION_NAME
783
96
 
@@ -787,8 +100,7 @@ ToastDescription.displayName = DESCRIPTION_NAME
787
100
 
788
101
  const ACTION_NAME = 'ToastAction'
789
102
 
790
- type ToastActionElement = ToastCloseElement
791
- interface ToastActionProps extends ToastCloseProps {
103
+ type ToastActionProps = ToastCloseProps & {
792
104
  /**
793
105
  * A short description for an alternate way to carry out the action. For screen reader users
794
106
  * who will not be able to navigate to the button easily/quickly.
@@ -798,17 +110,16 @@ interface ToastActionProps extends ToastCloseProps {
798
110
  altText: string
799
111
  }
800
112
 
801
- const ToastAction = React.forwardRef<ToastActionElement, ToastActionProps>(
113
+ const ToastAction = React.forwardRef<TamaguiElement, ToastActionProps>(
802
114
  (props: ScopedProps<ToastActionProps>, forwardedRef) => {
803
115
  const { altText, ...actionProps } = props
804
- const context = useToastInteractiveContext(ACTION_NAME, props.__scopeToast)
805
116
  if (!altText) return null
806
- return context.isInteractive ? (
807
- <ToastClose {...actionProps} ref={forwardedRef} />
808
- ) : (
809
- <span>{altText}</span>
117
+ return (
118
+ <ToastAnnounceExclude altText={altText} asChild>
119
+ <ToastClose {...actionProps} ref={forwardedRef} />
120
+ </ToastAnnounceExclude>
810
121
  )
811
- },
122
+ }
812
123
  )
813
124
 
814
125
  ToastAction.propTypes = {
@@ -828,178 +139,98 @@ ToastAction.displayName = ACTION_NAME
828
139
 
829
140
  const CLOSE_NAME = 'ToastClose'
830
141
 
831
- type ToastCloseElement = TamaguiElement
832
- type PrimitiveButtonProps = any //Radix.ComponentPropsWithoutRef<typeof Primitive.button>
833
- interface ToastCloseProps extends PrimitiveButtonProps {}
142
+ const ToastCloseFrame = styled(ThemeableStack, {
143
+ name: CLOSE_NAME,
144
+ tag: 'button',
145
+ })
146
+ type ToastCloseFrameProps = GetProps<typeof ToastCloseFrame>
147
+ type ToastCloseProps = ToastCloseFrameProps & {}
834
148
 
835
- const ToastClose = React.forwardRef<ToastCloseElement, ToastCloseProps>(
149
+ const ToastClose = React.forwardRef<TamaguiElement, ToastCloseProps>(
836
150
  (props: ScopedProps<ToastCloseProps>, forwardedRef) => {
837
151
  const { __scopeToast, ...closeProps } = props
838
152
  const interactiveContext = useToastInteractiveContext(CLOSE_NAME, __scopeToast)
839
- return interactiveContext.isInteractive ? (
840
- <YStack
841
- tag="button"
842
- {...closeProps}
843
- ref={forwardedRef}
844
- onPress={composeEventHandlers(props.onPress, interactiveContext.onClose)}
845
- />
846
- ) : null
847
- },
153
+
154
+ return (
155
+ <ToastAnnounceExclude asChild>
156
+ <ToastCloseFrame
157
+ accessibilityLabel="Dialog Close"
158
+ {...closeProps}
159
+ ref={forwardedRef}
160
+ onPress={composeEventHandlers(props.onPress as any, interactiveContext.onClose)}
161
+ />
162
+ </ToastAnnounceExclude>
163
+ )
164
+ }
848
165
  )
849
166
 
850
167
  ToastClose.displayName = CLOSE_NAME
851
168
 
852
- /* ---------------------------------------------------------------------------------------------- */
169
+ /* -------------------------------------------------------------------------------------------------
170
+ * Toast
171
+ * -----------------------------------------------------------------------------------------------*/
853
172
 
854
- function handleAndDispatchCustomEvent<
855
- E extends CustomEvent,
856
- ReactEvent extends React.SyntheticEvent,
857
- >(
858
- name: string,
859
- handler: ((event: E) => void) | undefined,
860
- detail: { originalEvent: ReactEvent } & (E extends CustomEvent<infer D>
861
- ? D
862
- : never),
863
- { discrete }: { discrete: boolean },
864
- ) {
865
- const currentTarget = detail.originalEvent.currentTarget as HTMLElement
866
- const event = new CustomEvent(name, { bubbles: true, cancelable: true, detail })
867
- if (handler)
868
- currentTarget.addEventListener(name, handler as EventListener, { once: true })
869
-
870
- if (discrete) {
871
- // dispatchDiscreteCustomEvent(currentTarget, event)
872
- } else {
873
- currentTarget.dispatchEvent(event)
874
- }
875
- }
173
+ const ToastComponent = React.forwardRef<TamaguiElement, ToastProps>(
174
+ ToastImplFrame.extractable((props: ScopedProps<ToastProps>, forwardedRef) => {
175
+ const { forceMount, open: openProp, defaultOpen, onOpenChange, ...toastProps } = props
176
+ const [open, setOpen] = useControllableState({
177
+ prop: openProp,
178
+ defaultProp: defaultOpen ?? true,
179
+ onChange: onOpenChange,
180
+ })
876
181
 
877
- const isDeltaInDirection = (
878
- delta: { x: number; y: number },
879
- direction: SwipeDirection,
880
- threshold = 0,
881
- ) => {
882
- const deltaX = Math.abs(delta.x)
883
- const deltaY = Math.abs(delta.y)
884
- const isDeltaX = deltaX > deltaY
885
- if (direction === 'left' || direction === 'right') {
886
- return isDeltaX && deltaX > threshold
887
- } else {
888
- return !isDeltaX && deltaY > threshold
889
- }
890
- }
182
+ const id = useId()
183
+ const onPause = useEvent(props.onPause)
184
+ const onResume = useEvent(props.onResume)
891
185
 
892
- function useNextFrame(callback = () => {}) {
893
- // useCallbackRef
894
- const fn = useEvent(callback)
895
- useIsomorphicLayoutEffect(() => {
896
- let raf1 = 0
897
- let raf2 = 0
898
- raf1 = window.requestAnimationFrame(
899
- () => (raf2 = window.requestAnimationFrame(fn)),
900
- )
901
- return () => {
902
- window.cancelAnimationFrame(raf1)
903
- window.cancelAnimationFrame(raf2)
904
- }
905
- }, [fn])
906
- }
186
+ if (!forceMount && !open) return null
907
187
 
908
- function getAnnounceTextContent(container: HTMLElement) {
909
- const textContent: string[] = []
910
- const childNodes = Array.from(container.childNodes)
911
-
912
- childNodes.forEach((node) => {
913
- if (node.nodeType === node.TEXT_NODE && node.textContent)
914
- textContent.push(node.textContent)
915
- if (isHTMLElement(node)) {
916
- const isHidden =
917
- node.ariaHidden || node.hidden || node.style.display === 'none'
918
- if (!isHidden) textContent.push(...getAnnounceTextContent(node))
919
- }
188
+ return (
189
+ <ToastImpl
190
+ id={id}
191
+ open={open}
192
+ {...toastProps}
193
+ ref={forwardedRef}
194
+ onClose={() => setOpen(false)}
195
+ onPause={onPause}
196
+ onResume={onResume}
197
+ onSwipeEnd={composeEventHandlers(props.onSwipeEnd, (event) => {
198
+ setOpen(false)
199
+ })}
200
+ />
201
+ )
920
202
  })
203
+ )
921
204
 
922
- // We return a collection of text rather than a single concatenated string.
923
- // This allows SR VO to naturally pause break between nodes while announcing.
924
- return textContent
925
- }
205
+ ToastComponent.displayName = TOAST_NAME
926
206
 
927
- function isHTMLElement(node: any): node is HTMLElement {
928
- return node.nodeType === node.ELEMENT_NODE
929
- }
930
-
931
- /**
932
- * Returns a list of potential tabbable candidates.
933
- *
934
- * NOTE: This is only a close approximation. For example it doesn't take into account cases like when
935
- * elements are not visible. This cannot be worked out easily by just reading a property, but rather
936
- * necessitate runtime knowledge (computed styles, etc). We deal with these cases separately.
937
- *
938
- * See: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
939
- * Credit: https://github.com/discord/focus-layers/blob/master/src/util/wrapFocus.tsx#L1
940
- */
941
- function getTabbableCandidates(container: HTMLElement) {
942
- const nodes: HTMLElement[] = []
943
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
944
- acceptNode: (node: any) => {
945
- const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden'
946
- if (node.disabled || node.hidden || isHiddenInput)
947
- return NodeFilter.FILTER_SKIP
948
- // `.tabIndex` is not the same as the `tabindex` attribute. It works on the
949
- // runtime's understanding of tabbability, so this automatically accounts
950
- // for any kind of element that could be tabbed to.
951
- return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
952
- },
953
- })
954
- while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement)
955
- // we do not take into account the order of nodes with positive `tabIndex` as it
956
- // hinders accessibility to have tab order different from visual order.
957
- return nodes
958
- }
959
-
960
- function focusFirst(candidates: HTMLElement[]) {
961
- const previouslyFocusedElement = document.activeElement
962
- return candidates.some((candidate) => {
963
- // if focus is already where we want to go, we don't want to keep going through the candidates
964
- if (candidate === previouslyFocusedElement) return true
965
- candidate.focus()
966
- return document.activeElement !== previouslyFocusedElement
967
- })
968
- }
207
+ const Toast = withStaticProperties(
208
+ themeable(ToastComponent, ToastImplFrame.staticConfig),
209
+ {
210
+ Title: ToastTitle,
211
+ Description: ToastDescription,
212
+ Action: ToastAction,
213
+ Close: ToastClose,
214
+ }
215
+ )
969
216
 
970
- const Provider = ToastProvider
971
- const Viewport = ToastViewport
972
- const Root = Toast
973
- const Title = ToastTitle
974
- const Description = ToastDescription
975
- const Action = ToastAction
976
- const Close = ToastClose
217
+ /* ---------------------------------------------------------------------------------------------- */
977
218
 
978
219
  export {
979
- // createToastScope,
980
- //
220
+ Toast,
981
221
  ToastProvider,
982
222
  ToastViewport,
983
- Toast,
984
- ToastTitle,
985
- ToastDescription,
986
- ToastAction,
987
- ToastClose,
988
223
  //
989
- Provider,
990
- Viewport,
991
- Root,
992
- Title,
993
- Description,
994
- Action,
995
- Close,
224
+ createToast,
225
+ //
226
+ createToastScope,
996
227
  }
997
228
  export type {
998
- ToastProviderProps,
999
- ToastViewportProps,
1000
- ToastProps,
1001
- ToastTitleProps,
1002
- ToastDescriptionProps,
1003
229
  ToastActionProps,
1004
230
  ToastCloseProps,
231
+ ToastDescriptionProps,
232
+ ToastProps,
233
+ ToastProviderProps,
234
+ ToastTitleProps,
235
+ ToastViewportProps,
1005
236
  }