@tamagui/toast 2.0.0-rc.8 → 2.0.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.
- package/dist/cjs/Toast.cjs +149 -141
- package/dist/cjs/Toast.native.js +149 -142
- package/dist/cjs/Toast.native.js.map +1 -1
- package/dist/cjs/ToastAnnounce.cjs +78 -72
- package/dist/cjs/ToastAnnounce.native.js +85 -79
- package/dist/cjs/ToastAnnounce.native.js.map +1 -1
- package/dist/cjs/ToastComposable.cjs +780 -591
- package/dist/cjs/ToastComposable.native.js +863 -706
- package/dist/cjs/ToastComposable.native.js.map +1 -1
- package/dist/cjs/ToastImperative.cjs +99 -80
- package/dist/cjs/ToastImperative.native.js +104 -96
- package/dist/cjs/ToastImperative.native.js.map +1 -1
- package/dist/cjs/ToastImpl.cjs +300 -238
- package/dist/cjs/ToastImpl.native.js +309 -271
- package/dist/cjs/ToastImpl.native.js.map +1 -1
- package/dist/cjs/ToastItemFrame.cjs +143 -0
- package/dist/cjs/ToastItemFrame.native.js +148 -0
- package/dist/cjs/ToastItemFrame.native.js.map +1 -0
- package/dist/cjs/ToastPortal.cjs +23 -18
- package/dist/cjs/ToastPortal.native.js +27 -22
- package/dist/cjs/ToastPortal.native.js.map +1 -1
- package/dist/cjs/ToastProvider.cjs +102 -98
- package/dist/cjs/ToastProvider.native.js +108 -106
- package/dist/cjs/ToastProvider.native.js.map +1 -1
- package/dist/cjs/ToastState.cjs +218 -155
- package/dist/cjs/ToastState.native.js +270 -203
- package/dist/cjs/ToastState.native.js.map +1 -1
- package/dist/cjs/ToastViewport.cjs +274 -233
- package/dist/cjs/ToastViewport.native.js +301 -273
- package/dist/cjs/ToastViewport.native.js.map +1 -1
- package/dist/cjs/Toaster.cjs +71 -233
- package/dist/cjs/Toaster.native.js +72 -289
- package/dist/cjs/Toaster.native.js.map +1 -1
- package/dist/cjs/constants.cjs +14 -12
- package/dist/cjs/constants.native.js +14 -12
- package/dist/cjs/constants.native.js.map +1 -1
- package/dist/cjs/createNativeToast.cjs +43 -35
- package/dist/cjs/createNativeToast.native.js +42 -30
- package/dist/cjs/createNativeToast.native.js.map +1 -1
- package/dist/cjs/dispatchNativeToast.cjs +47 -0
- package/dist/cjs/dispatchNativeToast.native.js +52 -0
- package/dist/cjs/dispatchNativeToast.native.js.map +1 -0
- package/dist/cjs/index.cjs +7 -5
- package/dist/cjs/index.native.js +7 -5
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/types.cjs +7 -5
- package/dist/cjs/types.native.js +7 -5
- package/dist/cjs/types.native.js.map +1 -1
- package/dist/cjs/useAnimatedDragGesture.cjs +184 -83
- package/dist/cjs/useAnimatedDragGesture.native.js +193 -79
- package/dist/cjs/useAnimatedDragGesture.native.js.map +1 -1
- package/dist/cjs/useReducedMotion.cjs +44 -30
- package/dist/cjs/useReducedMotion.native.js +52 -43
- package/dist/cjs/useReducedMotion.native.js.map +1 -1
- package/dist/cjs/useToastAnimations.cjs +233 -155
- package/dist/cjs/useToastAnimations.native.js +246 -170
- package/dist/cjs/useToastAnimations.native.js.map +1 -1
- package/dist/cjs/v2.cjs +36 -0
- package/dist/cjs/v2.native.js +39 -0
- package/dist/cjs/v2.native.js.map +1 -0
- package/dist/esm/Toast.mjs +112 -106
- package/dist/esm/Toast.mjs.map +1 -1
- package/dist/esm/Toast.native.js +112 -107
- package/dist/esm/Toast.native.js.map +1 -1
- package/dist/esm/ToastAnnounce.mjs +46 -42
- package/dist/esm/ToastAnnounce.mjs.map +1 -1
- package/dist/esm/ToastAnnounce.native.js +52 -48
- package/dist/esm/ToastAnnounce.native.js.map +1 -1
- package/dist/esm/ToastComposable.mjs +742 -555
- package/dist/esm/ToastComposable.mjs.map +1 -1
- package/dist/esm/ToastComposable.native.js +825 -670
- package/dist/esm/ToastComposable.native.js.map +1 -1
- package/dist/esm/ToastImperative.mjs +71 -54
- package/dist/esm/ToastImperative.mjs.map +1 -1
- package/dist/esm/ToastImperative.native.js +76 -70
- package/dist/esm/ToastImperative.native.js.map +1 -1
- package/dist/esm/ToastImpl.mjs +261 -201
- package/dist/esm/ToastImpl.mjs.map +1 -1
- package/dist/esm/ToastImpl.native.js +270 -234
- package/dist/esm/ToastImpl.native.js.map +1 -1
- package/dist/esm/ToastItemFrame.mjs +114 -0
- package/dist/esm/ToastItemFrame.mjs.map +1 -0
- package/dist/esm/ToastItemFrame.native.js +116 -0
- package/dist/esm/ToastItemFrame.native.js.map +1 -0
- package/dist/esm/ToastPortal.mjs +8 -5
- package/dist/esm/ToastPortal.mjs.map +1 -1
- package/dist/esm/ToastPortal.native.js +12 -9
- package/dist/esm/ToastPortal.native.js.map +1 -1
- package/dist/esm/ToastProvider.mjs +71 -69
- package/dist/esm/ToastProvider.mjs.map +1 -1
- package/dist/esm/ToastProvider.native.js +77 -77
- package/dist/esm/ToastProvider.native.js.map +1 -1
- package/dist/esm/ToastState.mjs +205 -144
- package/dist/esm/ToastState.mjs.map +1 -1
- package/dist/esm/ToastState.native.js +258 -193
- package/dist/esm/ToastState.native.js.map +1 -1
- package/dist/esm/ToastViewport.mjs +238 -199
- package/dist/esm/ToastViewport.mjs.map +1 -1
- package/dist/esm/ToastViewport.native.js +265 -239
- package/dist/esm/ToastViewport.native.js.map +1 -1
- package/dist/esm/Toaster.mjs +45 -209
- package/dist/esm/Toaster.mjs.map +1 -1
- package/dist/esm/Toaster.native.js +46 -265
- package/dist/esm/Toaster.native.js.map +1 -1
- package/dist/esm/constants.mjs +2 -2
- package/dist/esm/constants.mjs.map +1 -1
- package/dist/esm/constants.native.js +2 -2
- package/dist/esm/constants.native.js.map +1 -1
- package/dist/esm/createNativeToast.mjs +29 -24
- package/dist/esm/createNativeToast.mjs.map +1 -1
- package/dist/esm/createNativeToast.native.js +27 -18
- package/dist/esm/createNativeToast.native.js.map +1 -1
- package/dist/esm/dispatchNativeToast.mjs +22 -0
- package/dist/esm/dispatchNativeToast.mjs.map +1 -0
- package/dist/esm/dispatchNativeToast.native.js +24 -0
- package/dist/esm/dispatchNativeToast.native.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -6
- package/dist/esm/useAnimatedDragGesture.mjs +160 -61
- package/dist/esm/useAnimatedDragGesture.mjs.map +1 -1
- package/dist/esm/useAnimatedDragGesture.native.js +167 -55
- package/dist/esm/useAnimatedDragGesture.native.js.map +1 -1
- package/dist/esm/useReducedMotion.mjs +18 -6
- package/dist/esm/useReducedMotion.mjs.map +1 -1
- package/dist/esm/useReducedMotion.native.js +25 -18
- package/dist/esm/useReducedMotion.native.js.map +1 -1
- package/dist/esm/useToastAnimations.mjs +206 -130
- package/dist/esm/useToastAnimations.mjs.map +1 -1
- package/dist/esm/useToastAnimations.native.js +218 -144
- package/dist/esm/useToastAnimations.native.js.map +1 -1
- package/dist/esm/v2.mjs +6 -0
- package/dist/esm/v2.mjs.map +1 -0
- package/dist/esm/v2.native.js +6 -0
- package/dist/esm/v2.native.js.map +1 -0
- package/dist/jsx/Toast.mjs +112 -106
- package/dist/jsx/Toast.mjs.map +1 -1
- package/dist/jsx/Toast.native.js +149 -142
- package/dist/jsx/Toast.native.js.map +1 -1
- package/dist/jsx/ToastAnnounce.mjs +46 -42
- package/dist/jsx/ToastAnnounce.mjs.map +1 -1
- package/dist/jsx/ToastAnnounce.native.js +85 -79
- package/dist/jsx/ToastAnnounce.native.js.map +1 -1
- package/dist/jsx/ToastComposable.mjs +742 -555
- package/dist/jsx/ToastComposable.mjs.map +1 -1
- package/dist/jsx/ToastComposable.native.js +863 -706
- package/dist/jsx/ToastComposable.native.js.map +1 -1
- package/dist/jsx/ToastImperative.mjs +71 -54
- package/dist/jsx/ToastImperative.mjs.map +1 -1
- package/dist/jsx/ToastImperative.native.js +104 -96
- package/dist/jsx/ToastImperative.native.js.map +1 -1
- package/dist/jsx/ToastImpl.mjs +261 -201
- package/dist/jsx/ToastImpl.mjs.map +1 -1
- package/dist/jsx/ToastImpl.native.js +309 -271
- package/dist/jsx/ToastImpl.native.js.map +1 -1
- package/dist/jsx/ToastItemFrame.mjs +114 -0
- package/dist/jsx/ToastItemFrame.mjs.map +1 -0
- package/dist/jsx/ToastItemFrame.native.js +148 -0
- package/dist/jsx/ToastItemFrame.native.js.map +1 -0
- package/dist/jsx/ToastPortal.mjs +8 -5
- package/dist/jsx/ToastPortal.mjs.map +1 -1
- package/dist/jsx/ToastPortal.native.js +27 -22
- package/dist/jsx/ToastPortal.native.js.map +1 -1
- package/dist/jsx/ToastProvider.mjs +71 -69
- package/dist/jsx/ToastProvider.mjs.map +1 -1
- package/dist/jsx/ToastProvider.native.js +108 -106
- package/dist/jsx/ToastProvider.native.js.map +1 -1
- package/dist/jsx/ToastState.mjs +205 -144
- package/dist/jsx/ToastState.mjs.map +1 -1
- package/dist/jsx/ToastState.native.js +270 -203
- package/dist/jsx/ToastState.native.js.map +1 -1
- package/dist/jsx/ToastViewport.mjs +238 -199
- package/dist/jsx/ToastViewport.mjs.map +1 -1
- package/dist/jsx/ToastViewport.native.js +301 -273
- package/dist/jsx/ToastViewport.native.js.map +1 -1
- package/dist/jsx/Toaster.mjs +45 -209
- package/dist/jsx/Toaster.mjs.map +1 -1
- package/dist/jsx/Toaster.native.js +72 -289
- package/dist/jsx/Toaster.native.js.map +1 -1
- package/dist/jsx/constants.mjs +2 -2
- package/dist/jsx/constants.mjs.map +1 -1
- package/dist/jsx/constants.native.js +14 -12
- package/dist/jsx/constants.native.js.map +1 -1
- package/dist/jsx/createNativeToast.mjs +29 -24
- package/dist/jsx/createNativeToast.mjs.map +1 -1
- package/dist/jsx/createNativeToast.native.js +42 -30
- package/dist/jsx/createNativeToast.native.js.map +1 -1
- package/dist/jsx/dispatchNativeToast.mjs +22 -0
- package/dist/jsx/dispatchNativeToast.mjs.map +1 -0
- package/dist/jsx/dispatchNativeToast.native.js +52 -0
- package/dist/jsx/dispatchNativeToast.native.js.map +1 -0
- package/dist/jsx/index.js +1 -1
- package/dist/jsx/index.js.map +1 -6
- package/dist/jsx/index.native.js +7 -5
- package/dist/jsx/types.native.js +7 -5
- package/dist/jsx/useAnimatedDragGesture.mjs +160 -61
- package/dist/jsx/useAnimatedDragGesture.mjs.map +1 -1
- package/dist/jsx/useAnimatedDragGesture.native.js +193 -79
- package/dist/jsx/useAnimatedDragGesture.native.js.map +1 -1
- package/dist/jsx/useReducedMotion.mjs +18 -6
- package/dist/jsx/useReducedMotion.mjs.map +1 -1
- package/dist/jsx/useReducedMotion.native.js +52 -43
- package/dist/jsx/useReducedMotion.native.js.map +1 -1
- package/dist/jsx/useToastAnimations.mjs +206 -130
- package/dist/jsx/useToastAnimations.mjs.map +1 -1
- package/dist/jsx/useToastAnimations.native.js +246 -170
- package/dist/jsx/useToastAnimations.native.js.map +1 -1
- package/dist/jsx/v2.mjs +6 -0
- package/dist/jsx/v2.mjs.map +1 -0
- package/dist/jsx/v2.native.js +39 -0
- package/dist/jsx/v2.native.js.map +1 -0
- package/package.json +42 -29
- package/src/ToastComposable.tsx +1380 -0
- package/src/ToastImpl.tsx +14 -2
- package/src/ToastItemFrame.tsx +136 -0
- package/src/ToastPortal.tsx +2 -2
- package/src/ToastProvider.tsx +8 -1
- package/src/ToastState.ts +398 -0
- package/src/ToastViewport.tsx +4 -3
- package/src/Toaster.tsx +181 -0
- package/src/createNativeToast.native.tsx +4 -0
- package/src/createNativeToast.tsx +18 -24
- package/src/dispatchNativeToast.ts +43 -0
- package/src/useAnimatedDragGesture.native.ts +255 -0
- package/src/useAnimatedDragGesture.ts +319 -0
- package/src/useReducedMotion.ts +59 -0
- package/src/useToastAnimations.ts +372 -0
- package/src/v2.ts +31 -0
- package/types/ToastComposable.d.ts +199 -0
- package/types/ToastComposable.d.ts.map +1 -1
- package/types/ToastImpl.d.ts.map +1 -1
- package/types/ToastItemFrame.d.ts +25 -0
- package/types/ToastItemFrame.d.ts.map +1 -0
- package/types/ToastPortal.d.ts.map +1 -1
- package/types/ToastProvider.d.ts +1 -1
- package/types/ToastProvider.d.ts.map +1 -1
- package/types/ToastState.d.ts +179 -0
- package/types/ToastState.d.ts.map +1 -1
- package/types/ToastViewport.d.ts.map +1 -1
- package/types/Toaster.d.ts +112 -0
- package/types/Toaster.d.ts.map +1 -1
- package/types/createNativeToast.d.ts +5 -0
- package/types/createNativeToast.d.ts.map +1 -1
- package/types/createNativeToast.native.d.ts +1 -0
- package/types/createNativeToast.native.d.ts.map +1 -1
- package/types/dispatchNativeToast.d.ts +12 -0
- package/types/dispatchNativeToast.d.ts.map +1 -0
- package/types/useAnimatedDragGesture.d.ts +33 -0
- package/types/useAnimatedDragGesture.d.ts.map +1 -1
- package/types/useAnimatedDragGesture.native.d.ts +33 -0
- package/types/useAnimatedDragGesture.native.d.ts.map +1 -1
- package/types/useReducedMotion.d.ts +6 -0
- package/types/useToastAnimations.d.ts +50 -0
- package/types/useToastAnimations.d.ts.map +1 -1
- package/types/v2.d.ts +10 -0
- package/types/v2.d.ts.map +1 -0
- package/v2/index.cjs +2 -0
- package/v2/index.js +2 -0
- package/v2/index.native.cjs +2 -0
- package/v2/index.native.js +2 -0
- package/LICENSE +0 -21
- package/dist/cjs/Toast.js +0 -119
- package/dist/cjs/Toast.js.map +0 -6
- package/dist/cjs/ToastAnnounce.js +0 -72
- package/dist/cjs/ToastAnnounce.js.map +0 -6
- package/dist/cjs/ToastComposable.js +0 -548
- package/dist/cjs/ToastComposable.js.map +0 -6
- package/dist/cjs/ToastImperative.js +0 -71
- package/dist/cjs/ToastImperative.js.map +0 -6
- package/dist/cjs/ToastImpl.js +0 -227
- package/dist/cjs/ToastImpl.js.map +0 -6
- package/dist/cjs/ToastItem.cjs +0 -526
- package/dist/cjs/ToastItem.js +0 -409
- package/dist/cjs/ToastItem.js.map +0 -6
- package/dist/cjs/ToastItem.native.js +0 -614
- package/dist/cjs/ToastItem.native.js.map +0 -1
- package/dist/cjs/ToastPortal.js +0 -26
- package/dist/cjs/ToastPortal.js.map +0 -6
- package/dist/cjs/ToastProvider.js +0 -105
- package/dist/cjs/ToastProvider.js.map +0 -6
- package/dist/cjs/ToastState.js +0 -160
- package/dist/cjs/ToastState.js.map +0 -6
- package/dist/cjs/ToastViewport.js +0 -263
- package/dist/cjs/ToastViewport.js.map +0 -6
- package/dist/cjs/Toaster.js +0 -204
- package/dist/cjs/Toaster.js.map +0 -6
- package/dist/cjs/constants.js +0 -22
- package/dist/cjs/constants.js.map +0 -6
- package/dist/cjs/createNativeToast.js +0 -44
- package/dist/cjs/createNativeToast.js.map +0 -6
- package/dist/cjs/index.js +0 -15
- package/dist/cjs/index.js.map +0 -6
- package/dist/cjs/types.js +0 -14
- package/dist/cjs/types.js.map +0 -6
- package/dist/cjs/useAnimatedDragGesture.js +0 -97
- package/dist/cjs/useAnimatedDragGesture.js.map +0 -6
- package/dist/cjs/useDragGesture.cjs +0 -129
- package/dist/cjs/useDragGesture.js +0 -100
- package/dist/cjs/useDragGesture.js.map +0 -6
- package/dist/cjs/useDragGesture.native.js +0 -146
- package/dist/cjs/useDragGesture.native.js.map +0 -1
- package/dist/cjs/useReducedMotion.js +0 -53
- package/dist/cjs/useReducedMotion.js.map +0 -6
- package/dist/cjs/useToastAnimations.js +0 -144
- package/dist/cjs/useToastAnimations.js.map +0 -6
- package/dist/cjs/v1.cjs +0 -31
- package/dist/cjs/v1.js +0 -26
- package/dist/cjs/v1.js.map +0 -6
- package/dist/cjs/v1.native.js +0 -34
- package/dist/cjs/v1.native.js.map +0 -1
- package/dist/esm/Toast.js +0 -107
- package/dist/esm/Toast.js.map +0 -6
- package/dist/esm/ToastAnnounce.js +0 -55
- package/dist/esm/ToastAnnounce.js.map +0 -6
- package/dist/esm/ToastComposable.js +0 -543
- package/dist/esm/ToastComposable.js.map +0 -6
- package/dist/esm/ToastImperative.js +0 -50
- package/dist/esm/ToastImperative.js.map +0 -6
- package/dist/esm/ToastImpl.js +0 -225
- package/dist/esm/ToastImpl.js.map +0 -6
- package/dist/esm/ToastItem.js +0 -393
- package/dist/esm/ToastItem.js.map +0 -6
- package/dist/esm/ToastItem.mjs +0 -492
- package/dist/esm/ToastItem.mjs.map +0 -1
- package/dist/esm/ToastItem.native.js +0 -577
- package/dist/esm/ToastItem.native.js.map +0 -1
- package/dist/esm/ToastPortal.js +0 -13
- package/dist/esm/ToastPortal.js.map +0 -6
- package/dist/esm/ToastProvider.js +0 -87
- package/dist/esm/ToastProvider.js.map +0 -6
- package/dist/esm/ToastState.js +0 -144
- package/dist/esm/ToastState.js.map +0 -6
- package/dist/esm/ToastViewport.js +0 -250
- package/dist/esm/ToastViewport.js.map +0 -6
- package/dist/esm/Toaster.js +0 -188
- package/dist/esm/Toaster.js.map +0 -6
- package/dist/esm/constants.js +0 -6
- package/dist/esm/constants.js.map +0 -6
- package/dist/esm/createNativeToast.js +0 -28
- package/dist/esm/createNativeToast.js.map +0 -6
- package/dist/esm/types.js +0 -1
- package/dist/esm/types.js.map +0 -6
- package/dist/esm/useAnimatedDragGesture.js +0 -73
- package/dist/esm/useAnimatedDragGesture.js.map +0 -6
- package/dist/esm/useDragGesture.js +0 -76
- package/dist/esm/useDragGesture.js.map +0 -6
- package/dist/esm/useDragGesture.mjs +0 -95
- package/dist/esm/useDragGesture.mjs.map +0 -1
- package/dist/esm/useDragGesture.native.js +0 -109
- package/dist/esm/useDragGesture.native.js.map +0 -1
- package/dist/esm/useReducedMotion.js +0 -30
- package/dist/esm/useReducedMotion.js.map +0 -6
- package/dist/esm/useToastAnimations.js +0 -122
- package/dist/esm/useToastAnimations.js.map +0 -6
- package/dist/esm/v1.js +0 -17
- package/dist/esm/v1.js.map +0 -6
- package/dist/esm/v1.mjs +0 -3
- package/dist/esm/v1.mjs.map +0 -1
- package/dist/esm/v1.native.js +0 -3
- package/dist/esm/v1.native.js.map +0 -1
- package/dist/jsx/Toast.js +0 -107
- package/dist/jsx/Toast.js.map +0 -6
- package/dist/jsx/ToastAnnounce.js +0 -55
- package/dist/jsx/ToastAnnounce.js.map +0 -6
- package/dist/jsx/ToastComposable.js +0 -543
- package/dist/jsx/ToastComposable.js.map +0 -6
- package/dist/jsx/ToastImperative.js +0 -50
- package/dist/jsx/ToastImperative.js.map +0 -6
- package/dist/jsx/ToastImpl.js +0 -225
- package/dist/jsx/ToastImpl.js.map +0 -6
- package/dist/jsx/ToastItem.js +0 -393
- package/dist/jsx/ToastItem.js.map +0 -6
- package/dist/jsx/ToastItem.mjs +0 -492
- package/dist/jsx/ToastItem.mjs.map +0 -1
- package/dist/jsx/ToastItem.native.js +0 -614
- package/dist/jsx/ToastItem.native.js.map +0 -1
- package/dist/jsx/ToastPortal.js +0 -13
- package/dist/jsx/ToastPortal.js.map +0 -6
- package/dist/jsx/ToastProvider.js +0 -87
- package/dist/jsx/ToastProvider.js.map +0 -6
- package/dist/jsx/ToastState.js +0 -144
- package/dist/jsx/ToastState.js.map +0 -6
- package/dist/jsx/ToastViewport.js +0 -250
- package/dist/jsx/ToastViewport.js.map +0 -6
- package/dist/jsx/Toaster.js +0 -188
- package/dist/jsx/Toaster.js.map +0 -6
- package/dist/jsx/constants.js +0 -6
- package/dist/jsx/constants.js.map +0 -6
- package/dist/jsx/createNativeToast.js +0 -28
- package/dist/jsx/createNativeToast.js.map +0 -6
- package/dist/jsx/types.js +0 -1
- package/dist/jsx/types.js.map +0 -6
- package/dist/jsx/useAnimatedDragGesture.js +0 -73
- package/dist/jsx/useAnimatedDragGesture.js.map +0 -6
- package/dist/jsx/useDragGesture.js +0 -76
- package/dist/jsx/useDragGesture.js.map +0 -6
- package/dist/jsx/useDragGesture.mjs +0 -95
- package/dist/jsx/useDragGesture.mjs.map +0 -1
- package/dist/jsx/useDragGesture.native.js +0 -146
- package/dist/jsx/useDragGesture.native.js.map +0 -1
- package/dist/jsx/useReducedMotion.js +0 -30
- package/dist/jsx/useReducedMotion.js.map +0 -6
- package/dist/jsx/useToastAnimations.js +0 -122
- package/dist/jsx/useToastAnimations.js.map +0 -6
- package/dist/jsx/v1.js +0 -17
- package/dist/jsx/v1.js.map +0 -6
- package/dist/jsx/v1.mjs +0 -3
- package/dist/jsx/v1.mjs.map +0 -1
- package/dist/jsx/v1.native.js +0 -34
- package/dist/jsx/v1.native.js.map +0 -1
- package/types/ToastItem.d.ts.map +0 -1
- package/types/useDragGesture.d.ts.map +0 -1
- package/types/useDragGesture.native.d.ts.map +0 -1
- package/types/v1.d.ts.map +0 -1
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
import { AnimatePresence } from '@tamagui/animate-presence'
|
|
2
|
+
import { isWeb } from '@tamagui/constants'
|
|
3
|
+
import { getGestureHandler } from '@tamagui/native'
|
|
4
|
+
import type { GetProps, TamaguiElement } from '@tamagui/core'
|
|
5
|
+
import {
|
|
6
|
+
createStyledContext,
|
|
7
|
+
styled,
|
|
8
|
+
Theme,
|
|
9
|
+
useConfiguration,
|
|
10
|
+
useEvent,
|
|
11
|
+
useThemeName,
|
|
12
|
+
View,
|
|
13
|
+
} from '@tamagui/core'
|
|
14
|
+
import { withStaticProperties } from '@tamagui/helpers'
|
|
15
|
+
import { Portal } from '@tamagui/portal'
|
|
16
|
+
import { XStack, YStack } from '@tamagui/stacks'
|
|
17
|
+
import { SizableText } from '@tamagui/text'
|
|
18
|
+
import * as React from 'react'
|
|
19
|
+
import type { SwipeDirection } from './ToastProvider'
|
|
20
|
+
import type { ExternalToast, ToastT, ToastToDismiss, ToastType } from './ToastState'
|
|
21
|
+
import { ToastState } from './ToastState'
|
|
22
|
+
import type { BurntToastOptions } from './types'
|
|
23
|
+
import { dispatchNativeToast } from './dispatchNativeToast'
|
|
24
|
+
import { useAnimatedDragGesture } from './useAnimatedDragGesture'
|
|
25
|
+
import { useToastAnimations } from './useToastAnimations'
|
|
26
|
+
import { useReducedMotion } from './useReducedMotion'
|
|
27
|
+
import {
|
|
28
|
+
DefaultCloseIcon,
|
|
29
|
+
ToastActionFrame,
|
|
30
|
+
ToastCloseFrame,
|
|
31
|
+
ToastItemFrame,
|
|
32
|
+
ToastPositionWrapper,
|
|
33
|
+
} from './ToastItemFrame'
|
|
34
|
+
|
|
35
|
+
// defaults
|
|
36
|
+
const VISIBLE_TOASTS_AMOUNT = 4
|
|
37
|
+
const VIEWPORT_OFFSET = 16
|
|
38
|
+
const TOAST_GAP = 14
|
|
39
|
+
const TOAST_LIFETIME = 4000
|
|
40
|
+
const FIXED_TOAST_HEIGHT = 72
|
|
41
|
+
const TIME_BEFORE_UNMOUNT = 200
|
|
42
|
+
const DEFAULT_HOTKEY: string[] = ['altKey', 'KeyT']
|
|
43
|
+
|
|
44
|
+
export type ToastPosition =
|
|
45
|
+
| 'top-left'
|
|
46
|
+
| 'top-center'
|
|
47
|
+
| 'top-right'
|
|
48
|
+
| 'bottom-left'
|
|
49
|
+
| 'bottom-center'
|
|
50
|
+
| 'bottom-right'
|
|
51
|
+
|
|
52
|
+
/* -------------------------------------------------------------------------------------------------
|
|
53
|
+
* Context
|
|
54
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
55
|
+
|
|
56
|
+
// Map of toastId -> height (keyed storage prevents ordering drift)
|
|
57
|
+
type HeightsMap = Record<string | number, number>
|
|
58
|
+
|
|
59
|
+
interface ToastContextValue {
|
|
60
|
+
toasts: ToastT[]
|
|
61
|
+
heights: HeightsMap
|
|
62
|
+
setToastHeight: (toastId: string | number, height: number) => void
|
|
63
|
+
removeToastHeight: (toastId: string | number) => void
|
|
64
|
+
expanded: boolean
|
|
65
|
+
setExpanded: React.Dispatch<React.SetStateAction<boolean>>
|
|
66
|
+
interacting: boolean
|
|
67
|
+
setInteracting: React.Dispatch<React.SetStateAction<boolean>>
|
|
68
|
+
/** Trigger cooldown period after dismiss - prevents collapse during stack rebalance */
|
|
69
|
+
triggerDismissCooldown: () => void
|
|
70
|
+
/** Check if currently in dismiss cooldown */
|
|
71
|
+
isInDismissCooldown: () => boolean
|
|
72
|
+
removeToast: (toast: ToastT) => void
|
|
73
|
+
position: ToastPosition
|
|
74
|
+
duration: number
|
|
75
|
+
gap: number
|
|
76
|
+
visibleToasts: number
|
|
77
|
+
swipeDirection: SwipeDirection
|
|
78
|
+
swipeThreshold: number
|
|
79
|
+
closeButton: boolean
|
|
80
|
+
reducedMotion: boolean
|
|
81
|
+
toastHeight: number
|
|
82
|
+
native: boolean
|
|
83
|
+
burntOptions?: Omit<BurntToastOptions, 'title' | 'message' | 'duration'>
|
|
84
|
+
notificationOptions?: NotificationOptions
|
|
85
|
+
icons?: ToastIcons
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ToastContext = createStyledContext<ToastContextValue>(
|
|
89
|
+
{} as ToastContextValue,
|
|
90
|
+
'Toast__'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const useToastContext = ToastContext.useStyledContext
|
|
94
|
+
|
|
95
|
+
/* -------------------------------------------------------------------------------------------------
|
|
96
|
+
* ToastItemContext - for auto-wiring Toast.Close (web only)
|
|
97
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
98
|
+
|
|
99
|
+
interface ToastItemContextValue {
|
|
100
|
+
toast: ToastT
|
|
101
|
+
handleClose: () => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ToastItemContext = React.createContext<ToastItemContextValue | null>(null)
|
|
105
|
+
|
|
106
|
+
function useToastItemContext() {
|
|
107
|
+
const ctx = React.useContext(ToastItemContext)
|
|
108
|
+
if (!ctx) {
|
|
109
|
+
throw new Error('useToastItemContext must be used within Toast.Item or Toast.List')
|
|
110
|
+
}
|
|
111
|
+
return ctx
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* -------------------------------------------------------------------------------------------------
|
|
115
|
+
* Icons
|
|
116
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
117
|
+
|
|
118
|
+
export interface ToastIcons {
|
|
119
|
+
success?: React.ReactNode
|
|
120
|
+
error?: React.ReactNode
|
|
121
|
+
warning?: React.ReactNode
|
|
122
|
+
info?: React.ReactNode
|
|
123
|
+
loading?: React.ReactNode
|
|
124
|
+
close?: React.ReactNode
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Icons - users provide their own via icons prop, no built-in defaults */
|
|
128
|
+
|
|
129
|
+
/* -------------------------------------------------------------------------------------------------
|
|
130
|
+
* Toast (Root)
|
|
131
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
132
|
+
|
|
133
|
+
export interface ToastRootProps {
|
|
134
|
+
children: React.ReactNode
|
|
135
|
+
/**
|
|
136
|
+
* Position of the toasts on screen
|
|
137
|
+
* @default 'bottom-right'
|
|
138
|
+
*/
|
|
139
|
+
position?: ToastPosition
|
|
140
|
+
/**
|
|
141
|
+
* Default duration for toasts in ms
|
|
142
|
+
* @default 4000
|
|
143
|
+
*/
|
|
144
|
+
duration?: number
|
|
145
|
+
/**
|
|
146
|
+
* Gap between toasts in pixels
|
|
147
|
+
* @default 14
|
|
148
|
+
*/
|
|
149
|
+
gap?: number
|
|
150
|
+
/**
|
|
151
|
+
* Number of toasts visible at once
|
|
152
|
+
* @default 4
|
|
153
|
+
*/
|
|
154
|
+
visibleToasts?: number
|
|
155
|
+
/**
|
|
156
|
+
* Direction toasts can be swiped to dismiss
|
|
157
|
+
* @default 'auto'
|
|
158
|
+
*/
|
|
159
|
+
swipeDirection?: SwipeDirection
|
|
160
|
+
/**
|
|
161
|
+
* Distance in pixels swipe must pass to dismiss
|
|
162
|
+
* @default 50
|
|
163
|
+
*/
|
|
164
|
+
swipeThreshold?: number
|
|
165
|
+
/**
|
|
166
|
+
* Fixed toast height in pixels for native stacking calculations.
|
|
167
|
+
* On web, heights are measured dynamically.
|
|
168
|
+
* @default 56
|
|
169
|
+
*/
|
|
170
|
+
toastHeight?: number
|
|
171
|
+
/**
|
|
172
|
+
* Show close button on toasts
|
|
173
|
+
* @default false
|
|
174
|
+
*/
|
|
175
|
+
closeButton?: boolean
|
|
176
|
+
/**
|
|
177
|
+
* When true, toasts are always expanded (fanned out) instead of stacked.
|
|
178
|
+
* @default false
|
|
179
|
+
*/
|
|
180
|
+
expand?: boolean
|
|
181
|
+
/**
|
|
182
|
+
* Theme for toasts
|
|
183
|
+
*/
|
|
184
|
+
theme?: 'light' | 'dark' | 'system'
|
|
185
|
+
/**
|
|
186
|
+
* Force reduced motion mode
|
|
187
|
+
*/
|
|
188
|
+
reducedMotion?: boolean
|
|
189
|
+
/**
|
|
190
|
+
* When true, uses burnt native OS toasts on mobile instead of RN views.
|
|
191
|
+
* @default false
|
|
192
|
+
*/
|
|
193
|
+
native?: boolean
|
|
194
|
+
/**
|
|
195
|
+
* Options for burnt native toasts on mobile
|
|
196
|
+
*/
|
|
197
|
+
burntOptions?: Omit<BurntToastOptions, 'title' | 'message' | 'duration'>
|
|
198
|
+
/**
|
|
199
|
+
* Options for web Notification API when native is true on web
|
|
200
|
+
*/
|
|
201
|
+
notificationOptions?: NotificationOptions
|
|
202
|
+
/**
|
|
203
|
+
* Custom icons for toast types
|
|
204
|
+
*/
|
|
205
|
+
icons?: ToastIcons
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function resolveSwipeDirection(
|
|
209
|
+
direction: SwipeDirection,
|
|
210
|
+
position: ToastPosition
|
|
211
|
+
): Exclude<SwipeDirection, 'auto'> {
|
|
212
|
+
if (direction !== 'auto') return direction
|
|
213
|
+
const [yPosition, xPosition] = position.split('-') as [
|
|
214
|
+
'top' | 'bottom',
|
|
215
|
+
'left' | 'center' | 'right',
|
|
216
|
+
]
|
|
217
|
+
if (!isWeb) {
|
|
218
|
+
// on native, always use vertical swipe to avoid conflicting with
|
|
219
|
+
// iOS/Android navigation back gesture (horizontal edge swipe)
|
|
220
|
+
return yPosition === 'top' ? 'up' : 'down'
|
|
221
|
+
}
|
|
222
|
+
if (xPosition === 'left') return 'left'
|
|
223
|
+
if (xPosition === 'right') return 'right'
|
|
224
|
+
// center positions: horizontal swipe feels most natural
|
|
225
|
+
return 'horizontal'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const ToastRoot = React.forwardRef<TamaguiElement, ToastRootProps>(
|
|
229
|
+
function ToastRoot(props, _ref) {
|
|
230
|
+
const {
|
|
231
|
+
children,
|
|
232
|
+
position = 'bottom-right',
|
|
233
|
+
duration = TOAST_LIFETIME,
|
|
234
|
+
gap = TOAST_GAP,
|
|
235
|
+
visibleToasts = VISIBLE_TOASTS_AMOUNT,
|
|
236
|
+
swipeDirection: swipeDirectionProp = 'auto',
|
|
237
|
+
swipeThreshold = 50,
|
|
238
|
+
toastHeight = FIXED_TOAST_HEIGHT,
|
|
239
|
+
closeButton = false,
|
|
240
|
+
expand = false,
|
|
241
|
+
theme: themeProp,
|
|
242
|
+
reducedMotion: reducedMotionProp,
|
|
243
|
+
native = false,
|
|
244
|
+
burntOptions,
|
|
245
|
+
notificationOptions,
|
|
246
|
+
icons,
|
|
247
|
+
} = props
|
|
248
|
+
|
|
249
|
+
const reducedMotion = useReducedMotion(reducedMotionProp)
|
|
250
|
+
const [toasts, setToasts] = React.useState<ToastT[]>([])
|
|
251
|
+
const [heights, setHeights] = React.useState<HeightsMap>({})
|
|
252
|
+
const [localExpanded, setExpanded] = React.useState(false)
|
|
253
|
+
const expanded = expand || localExpanded
|
|
254
|
+
const [interacting, setInteracting] = React.useState(false)
|
|
255
|
+
|
|
256
|
+
// Lock height updates during expand/collapse CSS transition to prevent
|
|
257
|
+
// font-loading onLayout corrections from restarting the animation mid-flight.
|
|
258
|
+
// useLayoutEffect fires before paint, so the lock is set before any onLayout callbacks.
|
|
259
|
+
const heightsLockedRef = React.useRef(false)
|
|
260
|
+
const prevExpandedRef = React.useRef(expanded)
|
|
261
|
+
|
|
262
|
+
React.useLayoutEffect(() => {
|
|
263
|
+
if (prevExpandedRef.current !== expanded) {
|
|
264
|
+
heightsLockedRef.current = true
|
|
265
|
+
prevExpandedRef.current = expanded
|
|
266
|
+
}
|
|
267
|
+
const timer = setTimeout(() => {
|
|
268
|
+
heightsLockedRef.current = false
|
|
269
|
+
}, 350)
|
|
270
|
+
return () => clearTimeout(timer)
|
|
271
|
+
}, [expanded])
|
|
272
|
+
|
|
273
|
+
// Round + skip small changes to prevent cascading re-renders from
|
|
274
|
+
// sub-pixel onLayout jitter during font loading or CSS transitions
|
|
275
|
+
const setToastHeight = React.useCallback(
|
|
276
|
+
(toastId: string | number, height: number) => {
|
|
277
|
+
if (heightsLockedRef.current) return
|
|
278
|
+
const rounded = Math.round(height)
|
|
279
|
+
setHeights((prev) => {
|
|
280
|
+
const existing = prev[toastId]
|
|
281
|
+
if (existing != null && Math.abs(existing - rounded) <= 2) return prev
|
|
282
|
+
return { ...prev, [toastId]: rounded }
|
|
283
|
+
})
|
|
284
|
+
},
|
|
285
|
+
[]
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
const removeToastHeight = React.useCallback((toastId: string | number) => {
|
|
289
|
+
setHeights((prev) => {
|
|
290
|
+
if (!(toastId in prev)) return prev
|
|
291
|
+
const next = { ...prev }
|
|
292
|
+
delete next[toastId]
|
|
293
|
+
return next
|
|
294
|
+
})
|
|
295
|
+
}, [])
|
|
296
|
+
|
|
297
|
+
// Cooldown after dismiss - prevents collapse while stack rebalances
|
|
298
|
+
const dismissCooldownRef = React.useRef(false)
|
|
299
|
+
const dismissCooldownTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(
|
|
300
|
+
null
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
const triggerDismissCooldown = React.useCallback(() => {
|
|
304
|
+
dismissCooldownRef.current = true
|
|
305
|
+
if (dismissCooldownTimerRef.current) {
|
|
306
|
+
clearTimeout(dismissCooldownTimerRef.current)
|
|
307
|
+
}
|
|
308
|
+
dismissCooldownTimerRef.current = setTimeout(() => {
|
|
309
|
+
dismissCooldownRef.current = false
|
|
310
|
+
}, 800)
|
|
311
|
+
}, [])
|
|
312
|
+
|
|
313
|
+
const isInDismissCooldown = React.useCallback(() => dismissCooldownRef.current, [])
|
|
314
|
+
|
|
315
|
+
// Store object props in refs so the subscription effect doesn't
|
|
316
|
+
// re-subscribe on every render when consumers pass inline objects.
|
|
317
|
+
const burntOptionsRef = React.useRef(burntOptions)
|
|
318
|
+
const notificationOptionsRef = React.useRef(notificationOptions)
|
|
319
|
+
React.useEffect(() => {
|
|
320
|
+
burntOptionsRef.current = burntOptions
|
|
321
|
+
}, [burntOptions])
|
|
322
|
+
React.useEffect(() => {
|
|
323
|
+
notificationOptionsRef.current = notificationOptions
|
|
324
|
+
}, [notificationOptions])
|
|
325
|
+
|
|
326
|
+
// subscribe to toast state
|
|
327
|
+
React.useEffect(() => {
|
|
328
|
+
return ToastState.subscribe((toast) => {
|
|
329
|
+
if ((toast as ToastToDismiss).dismiss) {
|
|
330
|
+
setToasts((toasts) =>
|
|
331
|
+
toasts.map((t) => (t.id === toast.id ? { ...t, delete: true } : t))
|
|
332
|
+
)
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Native dispatch: intercept before entering state so no in-app toast renders.
|
|
337
|
+
// On failure (e.g. permission denied), falls through to in-app.
|
|
338
|
+
if (native) {
|
|
339
|
+
const handled = dispatchNativeToast(toast as ToastT, {
|
|
340
|
+
duration,
|
|
341
|
+
burntOptions: burntOptionsRef.current,
|
|
342
|
+
notificationOptions: notificationOptionsRef.current,
|
|
343
|
+
})
|
|
344
|
+
if (handled) return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
setToasts((toasts) => {
|
|
348
|
+
const idx = toasts.findIndex((t) => t.id === toast.id)
|
|
349
|
+
if (idx !== -1) {
|
|
350
|
+
return [
|
|
351
|
+
...toasts.slice(0, idx),
|
|
352
|
+
{ ...toasts[idx], ...toast },
|
|
353
|
+
...toasts.slice(idx + 1),
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
return [toast as ToastT, ...toasts]
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
}, [native, duration])
|
|
360
|
+
|
|
361
|
+
// collapse when 1 toast left, or when a new toast is added while expanded
|
|
362
|
+
const prevToastCountRef = React.useRef(toasts.length)
|
|
363
|
+
React.useEffect(() => {
|
|
364
|
+
const prevCount = prevToastCountRef.current
|
|
365
|
+
prevToastCountRef.current = toasts.length
|
|
366
|
+
|
|
367
|
+
if (toasts.length <= 1 && !dismissCooldownRef.current) {
|
|
368
|
+
setExpanded(false)
|
|
369
|
+
} else if (toasts.length > prevCount && expanded) {
|
|
370
|
+
// new toast added while expanded — collapse to show the new front toast
|
|
371
|
+
setExpanded(false)
|
|
372
|
+
}
|
|
373
|
+
}, [toasts.length, expanded])
|
|
374
|
+
|
|
375
|
+
const removeToast = React.useCallback((toastToRemove: ToastT) => {
|
|
376
|
+
setToasts((toasts) => {
|
|
377
|
+
if (!toasts.find((t) => t.id === toastToRemove.id)?.delete) {
|
|
378
|
+
ToastState.dismiss(toastToRemove.id)
|
|
379
|
+
}
|
|
380
|
+
return toasts.filter(({ id }) => id !== toastToRemove.id)
|
|
381
|
+
})
|
|
382
|
+
}, [])
|
|
383
|
+
|
|
384
|
+
const swipeDirection = resolveSwipeDirection(swipeDirectionProp, position)
|
|
385
|
+
|
|
386
|
+
const currentTheme = useThemeName()
|
|
387
|
+
const resolvedTheme =
|
|
388
|
+
themeProp === 'system' || !themeProp
|
|
389
|
+
? currentTheme?.includes('dark')
|
|
390
|
+
? 'dark'
|
|
391
|
+
: 'light'
|
|
392
|
+
: themeProp
|
|
393
|
+
|
|
394
|
+
const contextValue: ToastContextValue = {
|
|
395
|
+
toasts,
|
|
396
|
+
heights,
|
|
397
|
+
setToastHeight,
|
|
398
|
+
removeToastHeight,
|
|
399
|
+
expanded,
|
|
400
|
+
setExpanded,
|
|
401
|
+
interacting,
|
|
402
|
+
setInteracting,
|
|
403
|
+
triggerDismissCooldown,
|
|
404
|
+
isInDismissCooldown,
|
|
405
|
+
removeToast,
|
|
406
|
+
position,
|
|
407
|
+
duration,
|
|
408
|
+
gap,
|
|
409
|
+
visibleToasts,
|
|
410
|
+
swipeDirection,
|
|
411
|
+
swipeThreshold,
|
|
412
|
+
toastHeight,
|
|
413
|
+
closeButton,
|
|
414
|
+
reducedMotion,
|
|
415
|
+
native,
|
|
416
|
+
burntOptions,
|
|
417
|
+
notificationOptions,
|
|
418
|
+
icons,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<ToastContext.Provider {...contextValue}>
|
|
423
|
+
<Theme name={resolvedTheme as any}>{children}</Theme>
|
|
424
|
+
</ToastContext.Provider>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
/* -------------------------------------------------------------------------------------------------
|
|
430
|
+
* ToastViewport
|
|
431
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
432
|
+
|
|
433
|
+
const ToastViewportFrame = styled(View, {
|
|
434
|
+
name: 'ToastViewport',
|
|
435
|
+
|
|
436
|
+
variants: {
|
|
437
|
+
unstyled: {
|
|
438
|
+
false: {
|
|
439
|
+
position: isWeb ? ('fixed' as any) : 'absolute',
|
|
440
|
+
zIndex: 100000,
|
|
441
|
+
pointerEvents: 'box-none',
|
|
442
|
+
maxWidth: '100%',
|
|
443
|
+
...(isWeb && { width: 356 }),
|
|
444
|
+
minHeight: 1,
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
} as const,
|
|
448
|
+
|
|
449
|
+
defaultVariants: {
|
|
450
|
+
unstyled: process.env.TAMAGUI_HEADLESS === '1',
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
export interface ToastViewportProps extends GetProps<typeof ToastViewportFrame> {
|
|
455
|
+
/**
|
|
456
|
+
* Offset from screen edge
|
|
457
|
+
* @default 24
|
|
458
|
+
*/
|
|
459
|
+
offset?: number | { top?: number; right?: number; bottom?: number; left?: number }
|
|
460
|
+
/**
|
|
461
|
+
* Hotkey to focus viewport
|
|
462
|
+
*/
|
|
463
|
+
hotkey?: string[]
|
|
464
|
+
/**
|
|
465
|
+
* Aria label
|
|
466
|
+
* @default 'Notifications'
|
|
467
|
+
*/
|
|
468
|
+
label?: string
|
|
469
|
+
/**
|
|
470
|
+
* Portal to root
|
|
471
|
+
* @default true
|
|
472
|
+
*/
|
|
473
|
+
portalToRoot?: boolean
|
|
474
|
+
/**
|
|
475
|
+
* z-index for the portal container when portalToRoot is true
|
|
476
|
+
* @default Number.MAX_SAFE_INTEGER
|
|
477
|
+
*/
|
|
478
|
+
portalZIndex?: number
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const ToastViewport = ToastViewportFrame.styleable<ToastViewportProps>(
|
|
482
|
+
function ToastViewport(props, ref) {
|
|
483
|
+
const {
|
|
484
|
+
offset = VIEWPORT_OFFSET,
|
|
485
|
+
hotkey = DEFAULT_HOTKEY,
|
|
486
|
+
label = 'Notifications',
|
|
487
|
+
portalToRoot = true,
|
|
488
|
+
portalZIndex = Number.MAX_SAFE_INTEGER,
|
|
489
|
+
children,
|
|
490
|
+
...rest
|
|
491
|
+
} = props
|
|
492
|
+
|
|
493
|
+
const ctx = useToastContext()
|
|
494
|
+
const listRef = React.useRef<TamaguiElement>(null)
|
|
495
|
+
const hoverTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
496
|
+
const hoverCooldownRef = React.useRef(false)
|
|
497
|
+
const deferredCollapseRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
498
|
+
const mouseInsideRef = React.useRef(false)
|
|
499
|
+
|
|
500
|
+
const [yPosition, xPosition] = ctx.position.split('-') as [
|
|
501
|
+
'top' | 'bottom',
|
|
502
|
+
'left' | 'center' | 'right',
|
|
503
|
+
]
|
|
504
|
+
|
|
505
|
+
// offset styles
|
|
506
|
+
// on native, get safe area insets to avoid status bar / Dynamic Island / home indicator
|
|
507
|
+
// use insets from TamaguiProvider (passed via useConfiguration)
|
|
508
|
+
// same pattern as Slider — works on native when TamaguiProvider has insets prop
|
|
509
|
+
const { insets: safeInsets } = useConfiguration()
|
|
510
|
+
|
|
511
|
+
const offsetStyles = React.useMemo(() => {
|
|
512
|
+
const styles: any = {}
|
|
513
|
+
const defaultOffset = typeof offset === 'number' ? offset : VIEWPORT_OFFSET
|
|
514
|
+
const offsetObj =
|
|
515
|
+
typeof offset === 'object'
|
|
516
|
+
? offset
|
|
517
|
+
: {
|
|
518
|
+
top: defaultOffset,
|
|
519
|
+
right: defaultOffset,
|
|
520
|
+
bottom: defaultOffset,
|
|
521
|
+
left: defaultOffset,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const safeTop = safeInsets?.top ?? 0
|
|
525
|
+
const safeBottom = safeInsets?.bottom ?? 0
|
|
526
|
+
|
|
527
|
+
// if safe area already provides spacing, skip the offset to avoid double padding
|
|
528
|
+
const topOffset = safeTop > 0 ? safeTop : (offsetObj.top ?? defaultOffset)
|
|
529
|
+
const bottomOffset =
|
|
530
|
+
safeBottom > 0 ? safeBottom : (offsetObj.bottom ?? defaultOffset)
|
|
531
|
+
|
|
532
|
+
if (yPosition === 'top') styles.top = topOffset
|
|
533
|
+
else styles.bottom = bottomOffset
|
|
534
|
+
|
|
535
|
+
if (isWeb) {
|
|
536
|
+
if (xPosition === 'left') styles.left = offsetObj.left ?? defaultOffset
|
|
537
|
+
else if (xPosition === 'right') styles.right = offsetObj.right ?? defaultOffset
|
|
538
|
+
else {
|
|
539
|
+
styles.left = '50%'
|
|
540
|
+
styles.transform = 'translateX(-50%)'
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
// native: always set both left + right so viewport fills screen
|
|
544
|
+
// (no fixed width on native — left/right offsets define the width)
|
|
545
|
+
styles.left = offsetObj.left ?? defaultOffset
|
|
546
|
+
styles.right = offsetObj.right ?? defaultOffset
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return styles
|
|
550
|
+
}, [offset, yPosition, xPosition])
|
|
551
|
+
|
|
552
|
+
// hotkey
|
|
553
|
+
React.useEffect(() => {
|
|
554
|
+
if (!isWeb) return
|
|
555
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
556
|
+
const isHotkeyPressed =
|
|
557
|
+
hotkey.length > 0 &&
|
|
558
|
+
hotkey.every((key) => (event as any)[key] || event.code === key)
|
|
559
|
+
if (isHotkeyPressed) {
|
|
560
|
+
ctx.setExpanded(true)
|
|
561
|
+
;(listRef.current as HTMLElement)?.focus()
|
|
562
|
+
}
|
|
563
|
+
// Escape is handled by individual toast items via onKeyDown
|
|
564
|
+
// which dismisses the focused toast with cooldown (keeps stack expanded)
|
|
565
|
+
}
|
|
566
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
567
|
+
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
568
|
+
}, [hotkey])
|
|
569
|
+
|
|
570
|
+
if (ctx.toasts.length === 0) return null
|
|
571
|
+
|
|
572
|
+
const hotkeyLabel = hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, '')
|
|
573
|
+
|
|
574
|
+
const content = (
|
|
575
|
+
<ToastViewportFrame
|
|
576
|
+
ref={listRef}
|
|
577
|
+
aria-label={`${label} ${hotkeyLabel}`}
|
|
578
|
+
tabIndex={-1}
|
|
579
|
+
aria-live="polite"
|
|
580
|
+
style={offsetStyles}
|
|
581
|
+
data-y-position={yPosition}
|
|
582
|
+
data-x-position={xPosition}
|
|
583
|
+
{...(isWeb
|
|
584
|
+
? {
|
|
585
|
+
onMouseEnter: () => {
|
|
586
|
+
mouseInsideRef.current = true
|
|
587
|
+
if (deferredCollapseRef.current) {
|
|
588
|
+
clearTimeout(deferredCollapseRef.current)
|
|
589
|
+
deferredCollapseRef.current = null
|
|
590
|
+
}
|
|
591
|
+
if (
|
|
592
|
+
ctx.toasts.length > 1 &&
|
|
593
|
+
!ctx.interacting &&
|
|
594
|
+
!hoverCooldownRef.current
|
|
595
|
+
) {
|
|
596
|
+
hoverTimeoutRef.current = setTimeout(() => ctx.setExpanded(true), 50)
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
onMouseLeave: () => {
|
|
600
|
+
mouseInsideRef.current = false
|
|
601
|
+
if (hoverTimeoutRef.current) {
|
|
602
|
+
clearTimeout(hoverTimeoutRef.current)
|
|
603
|
+
hoverTimeoutRef.current = null
|
|
604
|
+
}
|
|
605
|
+
if (!ctx.interacting && !ctx.isInDismissCooldown()) {
|
|
606
|
+
ctx.setExpanded(false)
|
|
607
|
+
} else if (ctx.isInDismissCooldown()) {
|
|
608
|
+
// During dismiss cooldown, defer collapse until well after
|
|
609
|
+
// the exit animation completes to prevent mid-animation bounce.
|
|
610
|
+
// The cooldown is 800ms, spring exit is ~500ms — 1200ms covers both.
|
|
611
|
+
if (deferredCollapseRef.current) {
|
|
612
|
+
clearTimeout(deferredCollapseRef.current)
|
|
613
|
+
}
|
|
614
|
+
deferredCollapseRef.current = setTimeout(() => {
|
|
615
|
+
deferredCollapseRef.current = null
|
|
616
|
+
if (!mouseInsideRef.current) {
|
|
617
|
+
ctx.setExpanded(false)
|
|
618
|
+
}
|
|
619
|
+
}, 1200)
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
onPointerDown: () => {
|
|
623
|
+
if (hoverTimeoutRef.current) {
|
|
624
|
+
clearTimeout(hoverTimeoutRef.current)
|
|
625
|
+
hoverTimeoutRef.current = null
|
|
626
|
+
}
|
|
627
|
+
ctx.setInteracting(true)
|
|
628
|
+
},
|
|
629
|
+
onPointerUp: () => ctx.setInteracting(false),
|
|
630
|
+
onPointerCancel: () => ctx.setInteracting(false),
|
|
631
|
+
}
|
|
632
|
+
: {
|
|
633
|
+
onPress: () => {
|
|
634
|
+
if (ctx.toasts.length > 1) {
|
|
635
|
+
ctx.setExpanded((prev) => !prev)
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
})}
|
|
639
|
+
{...(isWeb && {
|
|
640
|
+
onFocus: (event: React.FocusEvent) => {
|
|
641
|
+
// keyboard focus entered — expand stack and pause timers
|
|
642
|
+
if (
|
|
643
|
+
!(event.currentTarget as HTMLElement).contains(
|
|
644
|
+
event.relatedTarget as HTMLElement
|
|
645
|
+
)
|
|
646
|
+
) {
|
|
647
|
+
if (ctx.toasts.length > 1) {
|
|
648
|
+
ctx.setExpanded(true)
|
|
649
|
+
}
|
|
650
|
+
ctx.setInteracting(true)
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
onBlur: (event: React.FocusEvent) => {
|
|
654
|
+
// focus left the toaster — collapse and resume timers
|
|
655
|
+
if (
|
|
656
|
+
!(event.currentTarget as HTMLElement).contains(
|
|
657
|
+
event.relatedTarget as HTMLElement
|
|
658
|
+
)
|
|
659
|
+
) {
|
|
660
|
+
ctx.setInteracting(false)
|
|
661
|
+
if (!ctx.isInDismissCooldown()) {
|
|
662
|
+
ctx.setExpanded(false)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
})}
|
|
667
|
+
{...rest}
|
|
668
|
+
>
|
|
669
|
+
{children}
|
|
670
|
+
</ToastViewportFrame>
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
if (portalToRoot) {
|
|
674
|
+
return <Portal zIndex={portalZIndex}>{content}</Portal>
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return content
|
|
678
|
+
}
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
/* -------------------------------------------------------------------------------------------------
|
|
682
|
+
* ToastList - handles iteration and AnimatePresence
|
|
683
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
684
|
+
|
|
685
|
+
export interface ToastItemRenderProps {
|
|
686
|
+
toast: ToastT
|
|
687
|
+
index: number
|
|
688
|
+
handleClose: () => void
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export interface ToastListProps {
|
|
692
|
+
/**
|
|
693
|
+
* Custom render function for each toast item
|
|
694
|
+
*/
|
|
695
|
+
renderItem?: (props: ToastItemRenderProps) => React.ReactNode
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function ToastList({ renderItem }: ToastListProps) {
|
|
699
|
+
const ctx = useToastContext()
|
|
700
|
+
|
|
701
|
+
// render all toasts — hidden ones have opacity 0 but stay mounted
|
|
702
|
+
// so they smoothly transition when visible toasts are dismissed
|
|
703
|
+
const maxRender = ctx.toasts.length
|
|
704
|
+
|
|
705
|
+
return (
|
|
706
|
+
<AnimatePresence>
|
|
707
|
+
{ctx.toasts.slice(0, maxRender).map((toast, index) => {
|
|
708
|
+
const handleClose = () => {
|
|
709
|
+
if (toast.dismissible === false) return
|
|
710
|
+
toast.onDismiss?.(toast)
|
|
711
|
+
ctx.removeToast(toast)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const itemContextValue: ToastItemContextValue = {
|
|
715
|
+
toast,
|
|
716
|
+
handleClose,
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (!renderItem) {
|
|
720
|
+
return (
|
|
721
|
+
<ToastItemContext.Provider key={toast.id} value={itemContextValue}>
|
|
722
|
+
<ToastItemInner toast={toast} index={index}>
|
|
723
|
+
<DefaultToastContent toast={toast} />
|
|
724
|
+
</ToastItemInner>
|
|
725
|
+
</ToastItemContext.Provider>
|
|
726
|
+
)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return (
|
|
730
|
+
<ToastItemContext.Provider key={toast.id} value={itemContextValue}>
|
|
731
|
+
{renderItem({ toast, index, handleClose })}
|
|
732
|
+
</ToastItemContext.Provider>
|
|
733
|
+
)
|
|
734
|
+
})}
|
|
735
|
+
</AnimatePresence>
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/* -------------------------------------------------------------------------------------------------
|
|
740
|
+
* DefaultToastContent - default rendering for toast items
|
|
741
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
742
|
+
|
|
743
|
+
function DefaultToastContent({ toast }: { toast: ToastT }) {
|
|
744
|
+
const ctx = useToastContext()
|
|
745
|
+
const { handleClose } = useToastItemContext()
|
|
746
|
+
const toastType = toast.type ?? 'default'
|
|
747
|
+
const dismissible = toast.dismissible !== false
|
|
748
|
+
|
|
749
|
+
const title = typeof toast.title === 'function' ? toast.title() : toast.title
|
|
750
|
+
const description =
|
|
751
|
+
typeof toast.description === 'function' ? toast.description() : toast.description
|
|
752
|
+
|
|
753
|
+
return (
|
|
754
|
+
<XStack alignItems="flex-start" gap="$3">
|
|
755
|
+
<ToastIcon />
|
|
756
|
+
|
|
757
|
+
<YStack flex={1} gap="$1">
|
|
758
|
+
{title && <ToastTitle>{title}</ToastTitle>}
|
|
759
|
+
{description && <ToastDescription>{description}</ToastDescription>}
|
|
760
|
+
|
|
761
|
+
{(toast.action || toast.cancel) && (
|
|
762
|
+
<XStack gap="$2" marginTop="$2">
|
|
763
|
+
{toast.cancel && (
|
|
764
|
+
<ToastActionFrame
|
|
765
|
+
backgroundColor="transparent"
|
|
766
|
+
onPress={(e: any) => {
|
|
767
|
+
toast.cancel?.onClick?.(e)
|
|
768
|
+
handleClose()
|
|
769
|
+
}}
|
|
770
|
+
>
|
|
771
|
+
<SizableText size="$2" color="$color11">
|
|
772
|
+
{toast.cancel.label}
|
|
773
|
+
</SizableText>
|
|
774
|
+
</ToastActionFrame>
|
|
775
|
+
)}
|
|
776
|
+
{toast.action && (
|
|
777
|
+
<ToastActionFrame
|
|
778
|
+
backgroundColor="$color12"
|
|
779
|
+
hoverStyle={{ backgroundColor: '$color11' }}
|
|
780
|
+
pressStyle={{ backgroundColor: '$color10' }}
|
|
781
|
+
onPress={(e: any) => {
|
|
782
|
+
toast.action?.onClick?.(e)
|
|
783
|
+
if (!(e as any).defaultPrevented) {
|
|
784
|
+
handleClose()
|
|
785
|
+
}
|
|
786
|
+
}}
|
|
787
|
+
>
|
|
788
|
+
<SizableText size="$2" fontWeight="600" color="$background">
|
|
789
|
+
{toast.action.label}
|
|
790
|
+
</SizableText>
|
|
791
|
+
</ToastActionFrame>
|
|
792
|
+
)}
|
|
793
|
+
</XStack>
|
|
794
|
+
)}
|
|
795
|
+
</YStack>
|
|
796
|
+
|
|
797
|
+
{ctx.closeButton && dismissible && <ToastClose />}
|
|
798
|
+
</XStack>
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/* ToastPositionWrapper, ToastItemFrame imported from ./ToastItemFrame */
|
|
803
|
+
|
|
804
|
+
/* -------------------------------------------------------------------------------------------------
|
|
805
|
+
* DragWrapper - handles drag gestures with proper event handling
|
|
806
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
807
|
+
|
|
808
|
+
interface DragWrapperProps {
|
|
809
|
+
animatedStyle: any
|
|
810
|
+
gestureHandlers: any
|
|
811
|
+
gesture: any
|
|
812
|
+
AnimatedView: any
|
|
813
|
+
dragRef: React.RefObject<HTMLDivElement | null>
|
|
814
|
+
children: React.ReactNode
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function DragWrapper({
|
|
818
|
+
animatedStyle,
|
|
819
|
+
gestureHandlers,
|
|
820
|
+
gesture,
|
|
821
|
+
AnimatedView,
|
|
822
|
+
dragRef,
|
|
823
|
+
children,
|
|
824
|
+
}: DragWrapperProps) {
|
|
825
|
+
if (isWeb) {
|
|
826
|
+
return (
|
|
827
|
+
<div
|
|
828
|
+
ref={dragRef}
|
|
829
|
+
style={{
|
|
830
|
+
flex: 1,
|
|
831
|
+
display: 'flex',
|
|
832
|
+
flexDirection: 'column',
|
|
833
|
+
userSelect: 'none',
|
|
834
|
+
WebkitUserSelect: 'none',
|
|
835
|
+
touchAction: 'none',
|
|
836
|
+
cursor: 'default',
|
|
837
|
+
}}
|
|
838
|
+
{...gestureHandlers}
|
|
839
|
+
>
|
|
840
|
+
{children}
|
|
841
|
+
</div>
|
|
842
|
+
)
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// when RNGH gesture is available, wrap with GestureDetector + plain View
|
|
846
|
+
// (GestureDetector needs a native View to attach handlers to)
|
|
847
|
+
if (gesture) {
|
|
848
|
+
const gh = getGestureHandler()
|
|
849
|
+
const GestureDetector = gh.state.GestureDetector
|
|
850
|
+
if (GestureDetector) {
|
|
851
|
+
return (
|
|
852
|
+
<GestureDetector gesture={gesture}>
|
|
853
|
+
<View style={{ flex: 1 }} {...({ collapsable: false } as any)}>
|
|
854
|
+
<AnimatedView style={[{ flex: 1 }, animatedStyle]}>{children}</AnimatedView>
|
|
855
|
+
</View>
|
|
856
|
+
</GestureDetector>
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// fallback: PanResponder handlers
|
|
862
|
+
return (
|
|
863
|
+
<AnimatedView style={[{ flex: 1 }, animatedStyle]} {...gestureHandlers}>
|
|
864
|
+
{children}
|
|
865
|
+
</AnimatedView>
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/* -------------------------------------------------------------------------------------------------
|
|
870
|
+
* ToastItem (the wrapper with stacking/drag)
|
|
871
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
872
|
+
|
|
873
|
+
export interface ToastItemProps extends GetProps<typeof ToastItemFrame> {
|
|
874
|
+
toast: ToastT
|
|
875
|
+
index: number
|
|
876
|
+
children: React.ReactNode
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const ToastItemInner = ToastItemFrame.styleable<ToastItemProps>(
|
|
880
|
+
function ToastItem(props, ref) {
|
|
881
|
+
const { toast, index, children, ...rest } = props
|
|
882
|
+
const ctx = useToastContext()
|
|
883
|
+
|
|
884
|
+
const [mounted, setMounted] = React.useState(false)
|
|
885
|
+
const [removed, setRemoved] = React.useState(false)
|
|
886
|
+
const [swipeOut, setSwipeOut] = React.useState(false)
|
|
887
|
+
// Freeze the Y offset when dismiss starts so the exiting toast doesn't jump
|
|
888
|
+
// as other toasts rebalance
|
|
889
|
+
const [offsetBeforeRemove, setOffsetBeforeRemove] = React.useState(0)
|
|
890
|
+
// Freeze stackY at swipe time — context re-renders recalculate stackY
|
|
891
|
+
// but the exiting toast should stay at its pre-removal position
|
|
892
|
+
const swipeExitYRef = React.useRef<number | null>(null)
|
|
893
|
+
|
|
894
|
+
const closeTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
895
|
+
const closeTimerStartRef = React.useRef(0)
|
|
896
|
+
const lastPauseTimeRef = React.useRef(0)
|
|
897
|
+
const remainingTimeRef = React.useRef(toast.duration ?? ctx.duration)
|
|
898
|
+
|
|
899
|
+
const isFront = index === 0
|
|
900
|
+
const isVisible = index < ctx.visibleToasts
|
|
901
|
+
const toastType = toast.type ?? 'default'
|
|
902
|
+
const dismissible = toast.dismissible !== false
|
|
903
|
+
const duration = toast.duration ?? ctx.duration
|
|
904
|
+
|
|
905
|
+
const [yPosition] = ctx.position.split('-') as ['top' | 'bottom', string]
|
|
906
|
+
const isTop = yPosition === 'top'
|
|
907
|
+
|
|
908
|
+
// web: dynamic heights (CSS transitions run off main thread, no FPS concern)
|
|
909
|
+
// native: fixed height (avoids React state re-render cascade on JS thread)
|
|
910
|
+
const expandedOffset = isWeb
|
|
911
|
+
? (() => {
|
|
912
|
+
let totalHeight = 0
|
|
913
|
+
let activeCount = 0
|
|
914
|
+
for (let i = 0; i < index; i++) {
|
|
915
|
+
const toastId = ctx.toasts[i]?.id
|
|
916
|
+
if (toastId == null) continue
|
|
917
|
+
const h = ctx.heights[toastId]
|
|
918
|
+
if (h === 0) continue
|
|
919
|
+
totalHeight += h ?? ctx.toastHeight
|
|
920
|
+
activeCount++
|
|
921
|
+
}
|
|
922
|
+
return totalHeight + activeCount * ctx.gap
|
|
923
|
+
})()
|
|
924
|
+
: index * (ctx.toastHeight + ctx.gap)
|
|
925
|
+
|
|
926
|
+
// Refs for stable access in callbacks — avoids putting expandedOffset/expanded
|
|
927
|
+
// in deps which would cause timer restarts on every height measurement
|
|
928
|
+
const expandedOffsetRef = React.useRef(expandedOffset)
|
|
929
|
+
expandedOffsetRef.current = expandedOffset
|
|
930
|
+
const isExpandedRef = React.useRef(ctx.expanded)
|
|
931
|
+
isExpandedRef.current = ctx.expanded
|
|
932
|
+
|
|
933
|
+
// timer — no height-zeroing needed here because auto-dismiss only fires
|
|
934
|
+
// when not expanded/interacting (timer is paused during hover)
|
|
935
|
+
const startTimer = React.useCallback(() => {
|
|
936
|
+
if (duration === Number.POSITIVE_INFINITY || toastType === 'loading') return
|
|
937
|
+
closeTimerStartRef.current = Date.now()
|
|
938
|
+
closeTimerRef.current = setTimeout(() => {
|
|
939
|
+
toast.onAutoClose?.(toast)
|
|
940
|
+
setRemoved(true)
|
|
941
|
+
setTimeout(() => ctx.removeToast(toast), TIME_BEFORE_UNMOUNT)
|
|
942
|
+
}, remainingTimeRef.current)
|
|
943
|
+
}, [duration, toastType, toast, ctx.removeToast])
|
|
944
|
+
|
|
945
|
+
const pauseTimer = useEvent(() => {
|
|
946
|
+
if (closeTimerRef.current) {
|
|
947
|
+
clearTimeout(closeTimerRef.current)
|
|
948
|
+
}
|
|
949
|
+
if (lastPauseTimeRef.current < closeTimerStartRef.current) {
|
|
950
|
+
const elapsed = Date.now() - closeTimerStartRef.current
|
|
951
|
+
remainingTimeRef.current = Math.max(0, remainingTimeRef.current - elapsed)
|
|
952
|
+
}
|
|
953
|
+
lastPauseTimeRef.current = Date.now()
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
const resumeTimer = useEvent(() => {
|
|
957
|
+
if (ctx.expanded || ctx.interacting) return
|
|
958
|
+
remainingTimeRef.current = duration
|
|
959
|
+
startTimer()
|
|
960
|
+
})
|
|
961
|
+
|
|
962
|
+
React.useEffect(() => {
|
|
963
|
+
setMounted(true)
|
|
964
|
+
}, [])
|
|
965
|
+
|
|
966
|
+
// handle deletion — only zero height when expanded (Sonner rebalance)
|
|
967
|
+
React.useEffect(() => {
|
|
968
|
+
if (toast.delete) {
|
|
969
|
+
setRemoved(true)
|
|
970
|
+
if (isExpandedRef.current) {
|
|
971
|
+
setOffsetBeforeRemove(expandedOffsetRef.current)
|
|
972
|
+
}
|
|
973
|
+
setTimeout(() => ctx.removeToast(toast), TIME_BEFORE_UNMOUNT)
|
|
974
|
+
}
|
|
975
|
+
}, [toast.delete, toast, ctx.removeToast])
|
|
976
|
+
|
|
977
|
+
React.useEffect(() => {
|
|
978
|
+
// all toasts have independent timers (same as Sonner)
|
|
979
|
+
// stagger comes from creation time differences
|
|
980
|
+
if (ctx.expanded || ctx.interacting) {
|
|
981
|
+
pauseTimer()
|
|
982
|
+
} else {
|
|
983
|
+
startTimer()
|
|
984
|
+
}
|
|
985
|
+
return () => {
|
|
986
|
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current)
|
|
987
|
+
}
|
|
988
|
+
}, [ctx.expanded, ctx.interacting, startTimer])
|
|
989
|
+
|
|
990
|
+
// reset remaining time when duration changes
|
|
991
|
+
React.useEffect(() => {
|
|
992
|
+
remainingTimeRef.current = duration
|
|
993
|
+
}, [duration])
|
|
994
|
+
|
|
995
|
+
// animations
|
|
996
|
+
const {
|
|
997
|
+
setDragOffset,
|
|
998
|
+
springBack,
|
|
999
|
+
animateOut,
|
|
1000
|
+
animatedStyle,
|
|
1001
|
+
AnimatedView,
|
|
1002
|
+
dragRef,
|
|
1003
|
+
} = useToastAnimations({
|
|
1004
|
+
reducedMotion: ctx.reducedMotion,
|
|
1005
|
+
swipeAxis:
|
|
1006
|
+
ctx.swipeDirection === 'up' ||
|
|
1007
|
+
ctx.swipeDirection === 'down' ||
|
|
1008
|
+
ctx.swipeDirection === 'vertical'
|
|
1009
|
+
? 'vertical'
|
|
1010
|
+
: 'horizontal',
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
const { isDragging, gestureHandlers, gesture } = useAnimatedDragGesture({
|
|
1014
|
+
direction: ctx.swipeDirection,
|
|
1015
|
+
threshold: ctx.swipeThreshold,
|
|
1016
|
+
disabled: !dismissible || toastType === 'loading',
|
|
1017
|
+
expanded: ctx.expanded,
|
|
1018
|
+
onDragStart: pauseTimer,
|
|
1019
|
+
onDragMove: setDragOffset,
|
|
1020
|
+
onDismiss: (exitDirection, velocity) => {
|
|
1021
|
+
// Trigger cooldown to prevent collapse while stack rebalances
|
|
1022
|
+
ctx.triggerDismissCooldown()
|
|
1023
|
+
setSwipeOut(true)
|
|
1024
|
+
toast.onDismiss?.(toast)
|
|
1025
|
+
// freeze stackY at swipe time — after removeToast, context re-renders
|
|
1026
|
+
// recalculate expandedOffset with the wrong toast array
|
|
1027
|
+
swipeExitYRef.current = isExpandedRef.current
|
|
1028
|
+
? isTop
|
|
1029
|
+
? expandedOffsetRef.current
|
|
1030
|
+
: -expandedOffsetRef.current
|
|
1031
|
+
: isFront
|
|
1032
|
+
? 0
|
|
1033
|
+
: isTop
|
|
1034
|
+
? ctx.gap * index
|
|
1035
|
+
: -ctx.gap * index
|
|
1036
|
+
setRemoved(true)
|
|
1037
|
+
ctx.removeToast(toast)
|
|
1038
|
+
animateOut(exitDirection, velocity)
|
|
1039
|
+
},
|
|
1040
|
+
onCancel: () => {
|
|
1041
|
+
springBack(() => {
|
|
1042
|
+
resumeTimer()
|
|
1043
|
+
})
|
|
1044
|
+
},
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
// measure height (web only — native uses fixed height)
|
|
1048
|
+
const handleLayout = React.useCallback(
|
|
1049
|
+
(event: any) => {
|
|
1050
|
+
if (!isWeb) return
|
|
1051
|
+
if (removed) return
|
|
1052
|
+
if (!ctx.expanded && index !== 0) return
|
|
1053
|
+
const { height } = event.nativeEvent.layout
|
|
1054
|
+
ctx.setToastHeight(toast.id, height)
|
|
1055
|
+
},
|
|
1056
|
+
[toast.id, ctx.setToastHeight, index, ctx.expanded, removed]
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
// remove height on unmount (web only)
|
|
1060
|
+
React.useEffect(() => {
|
|
1061
|
+
if (!isWeb) return
|
|
1062
|
+
return () => {
|
|
1063
|
+
ctx.removeToastHeight(toast.id)
|
|
1064
|
+
}
|
|
1065
|
+
}, [toast.id, ctx.removeToastHeight])
|
|
1066
|
+
|
|
1067
|
+
const handleClose = React.useCallback(() => {
|
|
1068
|
+
if (!dismissible) return
|
|
1069
|
+
ctx.triggerDismissCooldown()
|
|
1070
|
+
toast.onDismiss?.(toast)
|
|
1071
|
+
setRemoved(true)
|
|
1072
|
+
if (isExpandedRef.current) {
|
|
1073
|
+
setOffsetBeforeRemove(expandedOffsetRef.current)
|
|
1074
|
+
}
|
|
1075
|
+
setTimeout(() => ctx.removeToast(toast), TIME_BEFORE_UNMOUNT)
|
|
1076
|
+
}, [dismissible, toast, ctx.removeToast, ctx.triggerDismissCooldown])
|
|
1077
|
+
|
|
1078
|
+
const itemContextValue = React.useMemo<ToastItemContextValue>(
|
|
1079
|
+
() => ({ toast, handleClose }),
|
|
1080
|
+
[toast, handleClose]
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
// front toast height for collapsed stacking (web only)
|
|
1084
|
+
let frontToastHeight = -1
|
|
1085
|
+
if (isWeb) {
|
|
1086
|
+
for (const t of ctx.toasts) {
|
|
1087
|
+
const h = ctx.heights[t.id]
|
|
1088
|
+
if (h != null && h > 0) {
|
|
1089
|
+
frontToastHeight = h
|
|
1090
|
+
break
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const stackScale = !ctx.expanded && !isFront ? 1 - index * 0.05 : 1
|
|
1096
|
+
|
|
1097
|
+
// When removed, freeze Y at the saved offset so the exiting toast doesn't jump
|
|
1098
|
+
// as other toasts rebalance (Sonner: --offset uses offsetBeforeRemove when removed)
|
|
1099
|
+
const activeExpandedOffset = removed ? offsetBeforeRemove : expandedOffset
|
|
1100
|
+
|
|
1101
|
+
const stackY = ctx.expanded
|
|
1102
|
+
? isTop
|
|
1103
|
+
? activeExpandedOffset
|
|
1104
|
+
: -activeExpandedOffset
|
|
1105
|
+
: isFront
|
|
1106
|
+
? 0
|
|
1107
|
+
: isTop
|
|
1108
|
+
? ctx.gap * index
|
|
1109
|
+
: -ctx.gap * index
|
|
1110
|
+
|
|
1111
|
+
const computedOpacity = removed && !swipeOut ? 0 : index >= ctx.visibleToasts ? 0 : 1
|
|
1112
|
+
const computedZIndex = removed ? 0 : ctx.visibleToasts - index + 1
|
|
1113
|
+
// web: use measured height for smooth expand/collapse transitions
|
|
1114
|
+
// native: fixed height, no constraint needed
|
|
1115
|
+
const computedHeight = isWeb
|
|
1116
|
+
? ctx.expanded
|
|
1117
|
+
? ctx.heights[toast.id] || undefined
|
|
1118
|
+
: !isFront && frontToastHeight > 0
|
|
1119
|
+
? frontToastHeight
|
|
1120
|
+
: undefined
|
|
1121
|
+
: undefined
|
|
1122
|
+
const computedPointerEvents = index >= ctx.visibleToasts ? 'none' : 'auto'
|
|
1123
|
+
|
|
1124
|
+
// gap filler for hover stability
|
|
1125
|
+
const gapFillerHeight = ctx.expanded ? ctx.gap + 1 : 0
|
|
1126
|
+
|
|
1127
|
+
// data attributes
|
|
1128
|
+
const dataAttributes = {
|
|
1129
|
+
'data-mounted': mounted ? 'true' : 'false',
|
|
1130
|
+
'data-removed': removed ? 'true' : 'false',
|
|
1131
|
+
'data-swipe-out': swipeOut ? 'true' : 'false',
|
|
1132
|
+
'data-visible': isVisible ? 'true' : 'false',
|
|
1133
|
+
'data-front': isFront ? 'true' : 'false',
|
|
1134
|
+
'data-index': String(index),
|
|
1135
|
+
'data-type': toastType,
|
|
1136
|
+
'data-expanded': ctx.expanded ? 'true' : 'false',
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return (
|
|
1140
|
+
<ToastPositionWrapper
|
|
1141
|
+
ref={ref}
|
|
1142
|
+
testID={rest.testID}
|
|
1143
|
+
accessibilityLabel={rest.accessibilityLabel}
|
|
1144
|
+
{...dataAttributes}
|
|
1145
|
+
transition={
|
|
1146
|
+
isDragging || ctx.reducedMotion ? undefined : removed ? '200ms' : '400ms'
|
|
1147
|
+
}
|
|
1148
|
+
animateOnly={
|
|
1149
|
+
isWeb ? ['transform', 'opacity', 'height'] : ['transform', 'opacity']
|
|
1150
|
+
}
|
|
1151
|
+
y={stackY}
|
|
1152
|
+
scale={stackScale}
|
|
1153
|
+
opacity={computedOpacity}
|
|
1154
|
+
zIndex={computedZIndex}
|
|
1155
|
+
height={computedHeight}
|
|
1156
|
+
overflow="visible"
|
|
1157
|
+
pointerEvents={computedPointerEvents as any}
|
|
1158
|
+
top={isTop ? 0 : undefined}
|
|
1159
|
+
bottom={isTop ? undefined : 0}
|
|
1160
|
+
{...(isWeb &&
|
|
1161
|
+
!isFront && {
|
|
1162
|
+
style: { transformOrigin: isTop ? 'top center' : 'bottom center' },
|
|
1163
|
+
})}
|
|
1164
|
+
enterStyle={
|
|
1165
|
+
ctx.reducedMotion ? { opacity: 0 } : { opacity: 0, y: isTop ? -80 : 80 }
|
|
1166
|
+
}
|
|
1167
|
+
exitStyle={
|
|
1168
|
+
ctx.reducedMotion
|
|
1169
|
+
? { opacity: 0 }
|
|
1170
|
+
: swipeOut
|
|
1171
|
+
? { opacity: 0, y: swipeExitYRef.current ?? stackY, scale: stackScale }
|
|
1172
|
+
: { opacity: 0, y: stackY, scale: stackScale }
|
|
1173
|
+
}
|
|
1174
|
+
>
|
|
1175
|
+
<DragWrapper
|
|
1176
|
+
animatedStyle={animatedStyle}
|
|
1177
|
+
gestureHandlers={gestureHandlers}
|
|
1178
|
+
gesture={gesture}
|
|
1179
|
+
AnimatedView={AnimatedView}
|
|
1180
|
+
dragRef={dragRef}
|
|
1181
|
+
>
|
|
1182
|
+
<ToastItemFrame
|
|
1183
|
+
role="status"
|
|
1184
|
+
aria-live="polite"
|
|
1185
|
+
aria-atomic
|
|
1186
|
+
tabIndex={0}
|
|
1187
|
+
onLayout={handleLayout}
|
|
1188
|
+
{...(isWeb && {
|
|
1189
|
+
onKeyDown: (event: React.KeyboardEvent) => {
|
|
1190
|
+
if (event.key === 'Escape' && dismissible) {
|
|
1191
|
+
// move focus to the next toast before dismissing
|
|
1192
|
+
const current = event.currentTarget as HTMLElement
|
|
1193
|
+
const container = current.closest('[aria-label]') as HTMLElement
|
|
1194
|
+
if (container) {
|
|
1195
|
+
const focusables = container.querySelectorAll('[tabindex="0"]')
|
|
1196
|
+
const arr = Array.from(focusables)
|
|
1197
|
+
const idx = arr.indexOf(current)
|
|
1198
|
+
const next = arr[idx + 1] || arr[idx - 1]
|
|
1199
|
+
;(next as HTMLElement)?.focus()
|
|
1200
|
+
}
|
|
1201
|
+
handleClose()
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
})}
|
|
1205
|
+
{...rest}
|
|
1206
|
+
>
|
|
1207
|
+
{/* gap filler to prevent hover flicker */}
|
|
1208
|
+
{ctx.expanded && gapFillerHeight > 0 && (
|
|
1209
|
+
<View
|
|
1210
|
+
position="absolute"
|
|
1211
|
+
left={0}
|
|
1212
|
+
right={0}
|
|
1213
|
+
height={gapFillerHeight}
|
|
1214
|
+
pointerEvents="auto"
|
|
1215
|
+
{...(isTop ? { top: '100%' } : { bottom: '100%' })}
|
|
1216
|
+
/>
|
|
1217
|
+
)}
|
|
1218
|
+
<ToastItemContext.Provider value={itemContextValue}>
|
|
1219
|
+
{children}
|
|
1220
|
+
</ToastItemContext.Provider>
|
|
1221
|
+
</ToastItemFrame>
|
|
1222
|
+
</DragWrapper>
|
|
1223
|
+
</ToastPositionWrapper>
|
|
1224
|
+
)
|
|
1225
|
+
}
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
/* -------------------------------------------------------------------------------------------------
|
|
1229
|
+
* ToastTitle
|
|
1230
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
1231
|
+
|
|
1232
|
+
const ToastTitle = styled(SizableText, {
|
|
1233
|
+
name: 'ToastTitle',
|
|
1234
|
+
|
|
1235
|
+
variants: {
|
|
1236
|
+
unstyled: {
|
|
1237
|
+
false: {
|
|
1238
|
+
color: '$color',
|
|
1239
|
+
fontWeight: '600',
|
|
1240
|
+
size: '$4',
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
} as const,
|
|
1244
|
+
|
|
1245
|
+
defaultVariants: {
|
|
1246
|
+
unstyled: process.env.TAMAGUI_HEADLESS === '1',
|
|
1247
|
+
},
|
|
1248
|
+
})
|
|
1249
|
+
|
|
1250
|
+
/* -------------------------------------------------------------------------------------------------
|
|
1251
|
+
* ToastDescription
|
|
1252
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
1253
|
+
|
|
1254
|
+
const ToastDescription = styled(SizableText, {
|
|
1255
|
+
name: 'ToastDescription',
|
|
1256
|
+
|
|
1257
|
+
variants: {
|
|
1258
|
+
unstyled: {
|
|
1259
|
+
false: {
|
|
1260
|
+
color: '$color11',
|
|
1261
|
+
size: '$2',
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
} as const,
|
|
1265
|
+
|
|
1266
|
+
defaultVariants: {
|
|
1267
|
+
unstyled: process.env.TAMAGUI_HEADLESS === '1',
|
|
1268
|
+
},
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
/* -------------------------------------------------------------------------------------------------
|
|
1272
|
+
* ToastClose - auto-wired to dismiss current toast
|
|
1273
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
1274
|
+
|
|
1275
|
+
const ToastClose = ToastCloseFrame.styleable(function ToastClose(props, ref) {
|
|
1276
|
+
// try to get handleClose from context, but allow manual override
|
|
1277
|
+
let handleClose: (() => void) | undefined
|
|
1278
|
+
try {
|
|
1279
|
+
const itemCtx = useToastItemContext()
|
|
1280
|
+
handleClose = itemCtx.handleClose
|
|
1281
|
+
} catch {
|
|
1282
|
+
// not inside a Toast.Item context, require manual onPress
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const ctx = useToastContext()
|
|
1286
|
+
|
|
1287
|
+
return (
|
|
1288
|
+
<ToastCloseFrame ref={ref} aria-label="Close toast" onPress={handleClose} {...props}>
|
|
1289
|
+
{props.children ?? ctx.icons?.close ?? <DefaultCloseIcon />}
|
|
1290
|
+
</ToastCloseFrame>
|
|
1291
|
+
)
|
|
1292
|
+
})
|
|
1293
|
+
|
|
1294
|
+
/* -------------------------------------------------------------------------------------------------
|
|
1295
|
+
* ToastAction
|
|
1296
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
1297
|
+
|
|
1298
|
+
const ToastAction = ToastActionFrame.styleable(function ToastAction(props, ref) {
|
|
1299
|
+
return <ToastActionFrame ref={ref} {...props} />
|
|
1300
|
+
})
|
|
1301
|
+
|
|
1302
|
+
/* -------------------------------------------------------------------------------------------------
|
|
1303
|
+
* ToastIcon - renders icon based on toast type
|
|
1304
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
1305
|
+
|
|
1306
|
+
function ToastIcon(props: { children?: React.ReactNode }) {
|
|
1307
|
+
const ctx = useToastContext()
|
|
1308
|
+
let toast: ToastT | undefined
|
|
1309
|
+
|
|
1310
|
+
try {
|
|
1311
|
+
const itemCtx = useToastItemContext()
|
|
1312
|
+
toast = itemCtx.toast
|
|
1313
|
+
} catch {
|
|
1314
|
+
// not inside a Toast.Item context
|
|
1315
|
+
return null
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (!toast) return null
|
|
1319
|
+
|
|
1320
|
+
// if custom icon provided on toast, use it
|
|
1321
|
+
if (toast.icon !== undefined) {
|
|
1322
|
+
return (
|
|
1323
|
+
<View flexShrink={0} marginTop="$0.5">
|
|
1324
|
+
{toast.icon}
|
|
1325
|
+
</View>
|
|
1326
|
+
)
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const toastType = toast.type ?? 'default'
|
|
1330
|
+
|
|
1331
|
+
// only show icons if explicitly provided via icons prop (no built-in defaults)
|
|
1332
|
+
const icon = ctx.icons?.[toastType] ?? null
|
|
1333
|
+
if (!icon) return null
|
|
1334
|
+
|
|
1335
|
+
return (
|
|
1336
|
+
<View flexShrink={0} marginTop="$0.5">
|
|
1337
|
+
{icon}
|
|
1338
|
+
</View>
|
|
1339
|
+
)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/* -------------------------------------------------------------------------------------------------
|
|
1343
|
+
* useToasts hook for rendering
|
|
1344
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
1345
|
+
|
|
1346
|
+
export function useToasts() {
|
|
1347
|
+
const ctx = useToastContext()
|
|
1348
|
+
return {
|
|
1349
|
+
toasts: ctx.toasts,
|
|
1350
|
+
expanded: ctx.expanded,
|
|
1351
|
+
position: ctx.position,
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/* -------------------------------------------------------------------------------------------------
|
|
1356
|
+
* useToastItem hook for accessing current toast in custom content
|
|
1357
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
1358
|
+
|
|
1359
|
+
export function useToastItem() {
|
|
1360
|
+
return useToastItemContext()
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/* -------------------------------------------------------------------------------------------------
|
|
1364
|
+
* Export
|
|
1365
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
1366
|
+
|
|
1367
|
+
ToastRoot.displayName = 'Toast'
|
|
1368
|
+
|
|
1369
|
+
export const Toast = withStaticProperties(ToastRoot, {
|
|
1370
|
+
Viewport: ToastViewport,
|
|
1371
|
+
List: ToastList,
|
|
1372
|
+
Item: ToastItemInner,
|
|
1373
|
+
Title: ToastTitle,
|
|
1374
|
+
Description: ToastDescription,
|
|
1375
|
+
Close: ToastClose,
|
|
1376
|
+
Action: ToastAction,
|
|
1377
|
+
Icon: ToastIcon,
|
|
1378
|
+
})
|
|
1379
|
+
|
|
1380
|
+
export type { ToastT, ExternalToast }
|