@tamagui/toast 1.7.11 → 1.8.1

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
@@ -0,0 +1,354 @@
1
+ import { AnimatePresence } from '@tamagui/animate-presence'
2
+ import { useComposedRefs } from '@tamagui/compose-refs'
3
+ import { GetProps, TamaguiElement, isWeb, styled } from '@tamagui/core'
4
+ import { PortalHost } from '@tamagui/portal'
5
+ import { YStack } from '@tamagui/stacks'
6
+ import { VisuallyHidden } from '@tamagui/visually-hidden'
7
+ import * as React from 'react'
8
+
9
+ import {
10
+ Collection,
11
+ ScopedProps,
12
+ useCollection,
13
+ useToastProviderContext,
14
+ } from './ToastProvider'
15
+
16
+ const VIEWPORT_NAME = 'ToastViewport'
17
+ const VIEWPORT_DEFAULT_HOTKEY = ['F8']
18
+ const VIEWPORT_PAUSE = 'toast.viewportPause'
19
+ const VIEWPORT_RESUME = 'toast.viewportResume'
20
+
21
+ const ToastViewportWrapperFrame = styled(YStack, {
22
+ name: 'ViewportWrapper',
23
+ pointerEvents: 'box-none',
24
+ top: 0,
25
+ bottom: 0,
26
+ left: 0,
27
+ right: 0,
28
+ // @ts-ignore
29
+ position: isWeb ? 'fixed' : 'absolute',
30
+ maxWidth: '100%',
31
+ tabIndex: 0,
32
+ zIndex: 100000,
33
+ })
34
+
35
+ const ToastViewportFrame = styled(YStack, {
36
+ name: VIEWPORT_NAME,
37
+ pointerEvents: 'box-none',
38
+ // @ts-ignore
39
+ position: isWeb ? 'fixed' : 'absolute',
40
+ maxWidth: '100%',
41
+ })
42
+ type ToastViewportFrameProps = GetProps<typeof ToastViewportFrame>
43
+ type ToastViewportProps = ToastViewportFrameProps & {
44
+ /**
45
+ * The keys to use as the keyboard shortcut that will move focus to the toast viewport.
46
+ * @defaultValue ['F8']
47
+ */
48
+ hotkey?: string[]
49
+ /**
50
+ * An author-localized label for the toast viewport to provide context for screen reader users
51
+ * when navigating page landmarks. The available `{hotkey}` placeholder will be replaced for you.
52
+ * @defaultValue 'Notifications ({hotkey})'
53
+ */
54
+ label?: string
55
+ /**
56
+ * Used to reference the viewport if you want to have multiple viewports in the same provider.
57
+ */
58
+ name?: string
59
+ }
60
+
61
+ const ToastViewport = React.forwardRef<HTMLDivElement, ToastViewportProps>(
62
+ (props: ScopedProps<ToastViewportProps>, forwardedRef) => {
63
+ const {
64
+ __scopeToast,
65
+ hotkey = VIEWPORT_DEFAULT_HOTKEY,
66
+ label = 'Notifications ({hotkey})',
67
+ name,
68
+ ...viewportProps
69
+ } = props
70
+ const context = useToastProviderContext(VIEWPORT_NAME, __scopeToast)
71
+ const getItems = useCollection(__scopeToast)
72
+ const headFocusProxyRef = React.useRef<FocusProxyElement>(null)
73
+ const tailFocusProxyRef = React.useRef<FocusProxyElement>(null)
74
+ const wrapperRef = React.useRef<HTMLDivElement>(null)
75
+ const ref = React.useRef<HTMLDivElement>(null)
76
+ const composedRefs = useComposedRefs(forwardedRef, ref, context.onViewportChange)
77
+ const hotkeyLabel = hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, '')
78
+ const hasToasts = context.toastCount > 0
79
+
80
+ React.useEffect(() => {
81
+ if (!isWeb) return
82
+ const handleKeyDown = (event: KeyboardEvent) => {
83
+ // we use `event.code` as it is consistent regardless of meta keys that were pressed.
84
+ // for example, `event.key` for `Control+Alt+t` is `†` and `t !== †`
85
+ const isHotkeyPressed = hotkey.every(
86
+ (key) => (event as any)[key] || event.code === key
87
+ )
88
+ if (isHotkeyPressed) ref.current?.focus()
89
+ console.log(isHotkeyPressed)
90
+ }
91
+ document.addEventListener('keydown', handleKeyDown)
92
+ return () => {
93
+ document.removeEventListener('keydown', handleKeyDown)
94
+ }
95
+ }, [hotkey])
96
+
97
+ React.useEffect(() => {
98
+ if (!isWeb) return
99
+ const wrapper = wrapperRef.current
100
+ const viewport = ref.current
101
+ if (hasToasts && wrapper && viewport) {
102
+ const handlePause = () => {
103
+ if (!context.isClosePausedRef.current) {
104
+ const pauseEvent = new CustomEvent(VIEWPORT_PAUSE)
105
+ viewport.dispatchEvent(pauseEvent)
106
+ context.isClosePausedRef.current = true
107
+ }
108
+ }
109
+
110
+ const handleResume = () => {
111
+ if (context.isClosePausedRef.current) {
112
+ const resumeEvent = new CustomEvent(VIEWPORT_RESUME)
113
+ viewport.dispatchEvent(resumeEvent)
114
+ context.isClosePausedRef.current = false
115
+ }
116
+ }
117
+
118
+ const handleFocusOutResume = (event: FocusEvent) => {
119
+ const isFocusMovingOutside = !wrapper.contains(
120
+ event.relatedTarget as HTMLElement
121
+ )
122
+ if (isFocusMovingOutside) handleResume()
123
+ }
124
+
125
+ const handlePointerLeaveResume = () => {
126
+ const isFocusInside = wrapper.contains(document.activeElement)
127
+ if (!isFocusInside) handleResume()
128
+ }
129
+ // Toasts are not in the viewport React tree so we need to bind DOM events
130
+
131
+ wrapper.addEventListener('focusin', handlePause)
132
+ wrapper.addEventListener('focusout', handleFocusOutResume)
133
+ wrapper.addEventListener('pointermove', handlePause)
134
+ wrapper.addEventListener('pointerleave', handlePointerLeaveResume)
135
+ window.addEventListener('blur', handlePause)
136
+ window.addEventListener('focus', handleResume)
137
+ return () => {
138
+ wrapper.removeEventListener('focusin', handlePause)
139
+ wrapper.removeEventListener('focusout', handleFocusOutResume)
140
+ wrapper.removeEventListener('pointermove', handlePause)
141
+ wrapper.removeEventListener('pointerleave', handlePointerLeaveResume)
142
+ window.removeEventListener('blur', handlePause)
143
+ window.removeEventListener('focus', handleResume)
144
+ }
145
+ }
146
+ }, [hasToasts, context.isClosePausedRef])
147
+
148
+ const getSortedTabbableCandidates = React.useCallback(
149
+ ({ tabbingDirection }: { tabbingDirection: 'forwards' | 'backwards' }) => {
150
+ const toastItems = getItems()
151
+ const tabbableCandidates = toastItems.map((toastItem) => {
152
+ const toastNode = toastItem.ref.current!
153
+ const toastTabbableCandidates = [toastNode, ...getTabbableCandidates(toastNode)]
154
+ return tabbingDirection === 'forwards'
155
+ ? toastTabbableCandidates
156
+ : toastTabbableCandidates.reverse()
157
+ })
158
+ return (
159
+ tabbingDirection === 'forwards'
160
+ ? tabbableCandidates.reverse()
161
+ : tabbableCandidates
162
+ ).flat()
163
+ },
164
+ [getItems]
165
+ )
166
+
167
+ React.useEffect(() => {
168
+ if (!isWeb) return
169
+ const viewport = ref.current
170
+ // We programmatically manage tabbing as we are unable to influence
171
+ // the source order with portals, this allows us to reverse the
172
+ // tab order so that it runs from most recent toast to least
173
+ if (viewport) {
174
+ const handleKeyDown = (event: KeyboardEvent) => {
175
+ const isMetaKey = event.altKey || event.ctrlKey || event.metaKey
176
+ const isTabKey = event.key === 'Tab' && !isMetaKey
177
+
178
+ if (isTabKey) {
179
+ const focusedElement = document.activeElement
180
+ const isTabbingBackwards = event.shiftKey
181
+ const targetIsViewport = event.target === viewport
182
+
183
+ // If we're back tabbing after jumping to the viewport then we simply
184
+ // proxy focus out to the preceding document
185
+ if (targetIsViewport && isTabbingBackwards) {
186
+ headFocusProxyRef.current?.focus()
187
+ return
188
+ }
189
+
190
+ const tabbingDirection = isTabbingBackwards ? 'backwards' : 'forwards'
191
+ const sortedCandidates = getSortedTabbableCandidates({ tabbingDirection })
192
+ const index = sortedCandidates.findIndex(
193
+ (candidate) => candidate === focusedElement
194
+ )
195
+ if (focusFirst(sortedCandidates.slice(index + 1))) {
196
+ event.preventDefault()
197
+ } else {
198
+ // If we can't focus that means we're at the edges so we
199
+ // proxy to the corresponding exit point and let the browser handle
200
+ // tab/shift+tab keypress and implicitly pass focus to the next valid element in the document
201
+ isTabbingBackwards
202
+ ? headFocusProxyRef.current?.focus()
203
+ : tailFocusProxyRef.current?.focus()
204
+ }
205
+ }
206
+ }
207
+
208
+ // Toasts are not in the viewport React tree so we need to bind DOM events
209
+ viewport.addEventListener('keydown', handleKeyDown)
210
+ return () => viewport.removeEventListener('keydown', handleKeyDown)
211
+ }
212
+ }, [getItems, getSortedTabbableCandidates])
213
+
214
+ return (
215
+ <ToastViewportWrapperFrame
216
+ ref={wrapperRef}
217
+ role="region"
218
+ aria-label={label.replace('{hotkey}', hotkeyLabel)}
219
+ // // Ensure virtual cursor from landmarks menus triggers focus/blur for pause/resume
220
+ tabIndex={-1}
221
+ // // incase list has size when empty (e.g. padding), we remove pointer events so
222
+ // // it doesn't prevent interactions with page elements that it overlays
223
+ // pointerEvents={hasToasts ? undefined : 'none'}
224
+ >
225
+ {hasToasts && (
226
+ <FocusProxy
227
+ ref={headFocusProxyRef}
228
+ onFocusFromOutsideViewport={() => {
229
+ const tabbableCandidates = getSortedTabbableCandidates({
230
+ tabbingDirection: 'forwards',
231
+ })
232
+ focusFirst(tabbableCandidates)
233
+ }}
234
+ />
235
+ )}
236
+ {/**
237
+ * tabindex on the the list so that it can be focused when items are removed. we focus
238
+ * the list instead of the viewport so it announces number of items remaining.
239
+ */}
240
+ <Collection.Slot scope={__scopeToast}>
241
+ <ToastViewportFrame ref={composedRefs} {...viewportProps}>
242
+ <PortalHost
243
+ // TODO: user should be able to control this prop to customize AnimatePresence
244
+ render={(children) => <AnimatePresence>{children}</AnimatePresence>}
245
+ name={name ?? 'default'}
246
+ />
247
+ </ToastViewportFrame>
248
+ </Collection.Slot>
249
+ {hasToasts && (
250
+ <FocusProxy
251
+ ref={tailFocusProxyRef}
252
+ onFocusFromOutsideViewport={() => {
253
+ const tabbableCandidates = getSortedTabbableCandidates({
254
+ tabbingDirection: 'backwards',
255
+ })
256
+ focusFirst(tabbableCandidates)
257
+ }}
258
+ />
259
+ )}
260
+ </ToastViewportWrapperFrame>
261
+ )
262
+ }
263
+ )
264
+
265
+ ToastViewport.displayName = VIEWPORT_NAME
266
+
267
+ /* -----------------------------------------------------------------------------------------------*/
268
+
269
+ const FOCUS_PROXY_NAME = 'ToastFocusProxy'
270
+
271
+ type FocusProxyElement = React.ElementRef<typeof VisuallyHidden>
272
+ type VisuallyHiddenProps = GetProps<typeof VisuallyHidden>
273
+ interface FocusProxyProps extends VisuallyHiddenProps {
274
+ onFocusFromOutsideViewport(): void
275
+ }
276
+
277
+ const FocusProxy = React.forwardRef<FocusProxyElement, ScopedProps<FocusProxyProps>>(
278
+ (props, forwardedRef) => {
279
+ const { __scopeToast, onFocusFromOutsideViewport, ...proxyProps } = props
280
+ const context = useToastProviderContext(FOCUS_PROXY_NAME, __scopeToast)
281
+
282
+ return (
283
+ <VisuallyHidden
284
+ aria-hidden
285
+ tabIndex={0}
286
+ {...proxyProps}
287
+ ref={forwardedRef}
288
+ // Avoid page scrolling when focus is on the focus proxy
289
+ position={isWeb ? ('fixed' as any) : 'absolute'}
290
+ onFocus={(event) => {
291
+ if (!isWeb) return
292
+ const prevFocusedElement = event.relatedTarget as HTMLElement | null
293
+ const isFocusFromOutsideViewport = !(context.viewport as HTMLElement)?.contains(
294
+ prevFocusedElement
295
+ )
296
+ if (isFocusFromOutsideViewport) onFocusFromOutsideViewport()
297
+ }}
298
+ />
299
+ )
300
+ }
301
+ )
302
+
303
+ FocusProxy.displayName = FOCUS_PROXY_NAME
304
+
305
+ /* -----------------------------------------------------------------------------------------------*/
306
+
307
+ function focusFirst(candidates: TamaguiElement[]) {
308
+ if (!isWeb) return
309
+ const previouslyFocusedElement = document.activeElement
310
+ return candidates.some((candidate) => {
311
+ // if focus is already where we want to go, we don't want to keep going through the candidates
312
+ if (candidate === previouslyFocusedElement) return true
313
+ candidate.focus()
314
+ return document.activeElement !== previouslyFocusedElement
315
+ })
316
+ }
317
+
318
+ /**
319
+ * Returns a list of potential tabbable candidates.
320
+ *
321
+ * NOTE: This is only a close approximation. For example it doesn't take into account cases like when
322
+ * elements are not visible. This cannot be worked out easily by just reading a property, but rather
323
+ * necessitate runtime knowledge (computed styles, etc). We deal with these cases separately.
324
+ *
325
+ * See: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
326
+ * Credit: https://github.com/discord/focus-layers/blob/master/src/util/wrapFocus.tsx#L1
327
+ */
328
+ function getTabbableCandidates(container: TamaguiElement) {
329
+ if (!isWeb) return []
330
+ const containerHtml = container as HTMLElement
331
+ const nodes: HTMLElement[] = []
332
+ const walker = document.createTreeWalker(containerHtml, NodeFilter.SHOW_ELEMENT, {
333
+ acceptNode: (node: any) => {
334
+ const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden'
335
+ if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP
336
+ // `.tabIndex` is not the same as the `tabindex` attribute. It works on the
337
+ // runtime's understanding of tabbability, so this automatically accounts
338
+ // for any kind of element that could be tabbed to.
339
+ return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
340
+ },
341
+ })
342
+ while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement)
343
+ // we do not take into account the order of nodes with positive `tabIndex` as it
344
+ // hinders accessibility to have tab order different from visual order.
345
+ return nodes
346
+ }
347
+
348
+ export {
349
+ ToastViewport,
350
+ ToastViewportProps,
351
+ VIEWPORT_DEFAULT_HOTKEY,
352
+ VIEWPORT_PAUSE,
353
+ VIEWPORT_RESUME,
354
+ }
@@ -0,0 +1 @@
1
+ export const TOAST_NAME = 'Toast'
@@ -0,0 +1,16 @@
1
+ import { CreateNativeToastsOptions, CreateNativeToastsOptionsFn } from './types'
2
+
3
+ export const createNativeToast: CreateNativeToastsOptionsFn = (
4
+ title,
5
+ { message, duration, preset = 'done' }: CreateNativeToastsOptions
6
+ ) => {
7
+ // import inline to allow lazy usage of native dependecy:
8
+ const Burnt = require('burnt')
9
+
10
+ Burnt.toast({
11
+ title,
12
+ message: message ?? '',
13
+ preset,
14
+ duration,
15
+ })
16
+ }
@@ -0,0 +1,25 @@
1
+ import { CreateNativeToastsOptionsFn } from './types'
2
+
3
+ export const createNativeToast: CreateNativeToastsOptionsFn = (title, { message }) => {
4
+ if (!('Notification' in window)) {
5
+ throw Error('This browser does not support desktop notification')
6
+ }
7
+
8
+ if (Notification.permission === 'denied') return {}
9
+
10
+ const showNotification = () => {
11
+ new Notification(title, {
12
+ body: message,
13
+ })
14
+ }
15
+
16
+ if (Notification.permission === 'granted') {
17
+ showNotification()
18
+ } else {
19
+ Notification.requestPermission().then((permission) => {
20
+ if (permission === 'granted') {
21
+ showNotification()
22
+ }
23
+ })
24
+ }
25
+ }
@@ -0,0 +1,93 @@
1
+ import { isWeb } from '@tamagui/core'
2
+ import { Scope, createContextScope } from '@tamagui/create-context'
3
+ import React, { useRef } from 'react'
4
+
5
+ import { createNativeToast } from './createNativeToast'
6
+ import { CreateNativeToastsOptions, CreateNativeToastsOptionsFn } from './types'
7
+
8
+ type NativeValue = boolean | 'web' | 'mobile'
9
+ interface CreateToastOptions {
10
+ /**
11
+ * Will show a native toast if is true or is set to the current platform. On iOS, it wraps `SPIndicator` and `SPAlert`. On Android, it wraps `ToastAndroid`. On web, it wraps Notification API. Mobile's native features are handled by `burnt`.
12
+ */
13
+ native?: NativeValue
14
+ }
15
+ interface ToastOptions extends CreateNativeToastsOptions {
16
+ /**
17
+ * Used when need custom data
18
+ */
19
+ additionalInfo?: Record<string, any>
20
+ /**
21
+ * Overrides the native option on `createToast()`
22
+ */
23
+ native?: NativeValue
24
+ }
25
+ type ScopedProps<P> = P & { __scopeToast?: Scope }
26
+
27
+ type ToastData = { title: string; id: string } & CreateNativeToastsOptions
28
+
29
+ interface ImperativeToastContextValue {
30
+ currentToast: ToastData | null
31
+ addToast: CreateNativeToastsOptionsFn
32
+ }
33
+
34
+ const [createImperativeToastContext, createImperativeToastScope] =
35
+ createContextScope('ImperativeToast')
36
+
37
+ const [ImperativeToastProvider, useToastProviderContext] =
38
+ createImperativeToastContext<ImperativeToastContextValue>('ImperativeToastProvider')
39
+
40
+ const createToast = (options = {} as CreateToastOptions) => {
41
+ return {
42
+ ImperativeToastProvider: ({
43
+ __scopeToast,
44
+ children,
45
+ }: ScopedProps<{ children: React.ReactNode }>) => {
46
+ const counterRef = useRef(0)
47
+ const [toast, setToast] =
48
+ React.useState<ImperativeToastContextValue['currentToast']>(null)
49
+ const addToast = React.useCallback<CreateNativeToastsOptionsFn>(
50
+ (title, options) => {
51
+ counterRef.current++
52
+ setToast({ title, id: counterRef.current.toString(), ...options })
53
+ },
54
+ [setToast]
55
+ )
56
+
57
+ return (
58
+ <ImperativeToastProvider
59
+ scope={__scopeToast}
60
+ addToast={addToast}
61
+ currentToast={toast}
62
+ >
63
+ {children}
64
+ </ImperativeToastProvider>
65
+ )
66
+ },
67
+ useToast: () => {
68
+ const context = useToastProviderContext('useToast', undefined)
69
+
70
+ return {
71
+ /**
72
+ * The toast to get and show if not using native
73
+ */
74
+ currentToast: context.currentToast,
75
+ show(title: string, showOptions?: ToastOptions) {
76
+ const native = showOptions?.native || options.native
77
+ if (
78
+ native === true ||
79
+ (Array.isArray(options.native) &&
80
+ ((isWeb && options.native.includes('web')) ||
81
+ (!isWeb && options.native.includes('mobile'))))
82
+ ) {
83
+ return createNativeToast(title, showOptions ?? {})
84
+ }
85
+ context.addToast(title, showOptions ?? {})
86
+ },
87
+ }
88
+ },
89
+ }
90
+ }
91
+
92
+ export { createToast }
93
+ export type { CreateToastOptions }
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { ToastOptions as BurntToastOptions } from 'burnt/build/types'
2
+
3
+ export interface CreateNativeToastsOptions {
4
+ /**
5
+ * Body of the toast
6
+ */
7
+ message?: BurntToastOptions['message']
8
+ /**
9
+ * Only supported on native iOS driver
10
+ */
11
+ preset?: BurntToastOptions['preset']
12
+ /**
13
+ * Duration of toast
14
+ */
15
+ duration?: BurntToastOptions['duration']
16
+ }
17
+
18
+ export type CreateNativeToastsOptionsFn = (
19
+ title: string,
20
+ options: CreateNativeToastsOptions
21
+ ) => void