@cdx-ui/components 0.0.1-beta.79 → 0.0.1-beta.80
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/lib/commonjs/animation/hooks/index.js +17 -0
- package/lib/commonjs/animation/hooks/index.js.map +1 -0
- package/lib/commonjs/animation/hooks/useCombinedAnimationDisabledState.js +50 -0
- package/lib/commonjs/animation/hooks/useCombinedAnimationDisabledState.js.map +1 -0
- package/lib/commonjs/animation/index.js +39 -0
- package/lib/commonjs/animation/index.js.map +1 -0
- package/lib/commonjs/animation/types.js +6 -0
- package/lib/commonjs/animation/types.js.map +1 -0
- package/lib/commonjs/animation/utils.js +223 -0
- package/lib/commonjs/animation/utils.js.map +1 -0
- package/lib/commonjs/components/CLAUDE.md +1 -1
- package/lib/commonjs/components/Card/index.js +45 -17
- package/lib/commonjs/components/Card/index.js.map +1 -1
- package/lib/commonjs/components/Toast/animation.js +329 -0
- package/lib/commonjs/components/Toast/animation.js.map +1 -0
- package/lib/commonjs/components/Toast/hooks.js +66 -0
- package/lib/commonjs/components/Toast/hooks.js.map +1 -0
- package/lib/commonjs/components/Toast/index.js +493 -0
- package/lib/commonjs/components/Toast/index.js.map +1 -0
- package/lib/commonjs/components/Toast/styles.js +141 -0
- package/lib/commonjs/components/Toast/styles.js.map +1 -0
- package/lib/commonjs/components/Toast/types.js +6 -0
- package/lib/commonjs/components/Toast/types.js.map +1 -0
- package/lib/commonjs/components/index.js +12 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/index.js +15 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/providers/animation-settings/index.js +31 -0
- package/lib/commonjs/providers/animation-settings/index.js.map +1 -0
- package/lib/commonjs/providers/animation-settings/provider.js +59 -0
- package/lib/commonjs/providers/animation-settings/provider.js.map +1 -0
- package/lib/commonjs/providers/animation-settings/types.js +6 -0
- package/lib/commonjs/providers/animation-settings/types.js.map +1 -0
- package/lib/commonjs/providers/forge-design/index.js +29 -0
- package/lib/commonjs/providers/forge-design/index.js.map +1 -0
- package/lib/commonjs/providers/forge-design/provider.js +71 -0
- package/lib/commonjs/providers/forge-design/provider.js.map +1 -0
- package/lib/commonjs/providers/forge-design/types.js +6 -0
- package/lib/commonjs/providers/forge-design/types.js.map +1 -0
- package/lib/commonjs/providers/text-component/index.js +28 -0
- package/lib/commonjs/providers/text-component/index.js.map +1 -0
- package/lib/commonjs/providers/text-component/provider.js +11 -0
- package/lib/commonjs/providers/text-component/provider.js.map +1 -0
- package/lib/commonjs/providers/text-component/types.js +6 -0
- package/lib/commonjs/providers/text-component/types.js.map +1 -0
- package/lib/commonjs/providers/toast/index.js +33 -0
- package/lib/commonjs/providers/toast/index.js.map +1 -0
- package/lib/commonjs/providers/toast/insets-container.js +160 -0
- package/lib/commonjs/providers/toast/insets-container.js.map +1 -0
- package/lib/commonjs/providers/toast/lifecycle.context.js +36 -0
- package/lib/commonjs/providers/toast/lifecycle.context.js.map +1 -0
- package/lib/commonjs/providers/toast/provider.js +508 -0
- package/lib/commonjs/providers/toast/provider.js.map +1 -0
- package/lib/commonjs/providers/toast/reducer.js +46 -0
- package/lib/commonjs/providers/toast/reducer.js.map +1 -0
- package/lib/commonjs/providers/toast/toast-config.context.js +29 -0
- package/lib/commonjs/providers/toast/toast-config.context.js.map +1 -0
- package/lib/commonjs/providers/toast/toast-item-renderer.js +43 -0
- package/lib/commonjs/providers/toast/toast-item-renderer.js.map +1 -0
- package/lib/commonjs/providers/toast/types.js +6 -0
- package/lib/commonjs/providers/toast/types.js.map +1 -0
- package/lib/module/animation/hooks/index.js +4 -0
- package/lib/module/animation/hooks/index.js.map +1 -0
- package/lib/module/animation/hooks/useCombinedAnimationDisabledState.js +47 -0
- package/lib/module/animation/hooks/useCombinedAnimationDisabledState.js.map +1 -0
- package/lib/module/animation/index.js +6 -0
- package/lib/module/animation/index.js.map +1 -0
- package/lib/module/animation/types.js +4 -0
- package/lib/module/animation/types.js.map +1 -0
- package/lib/module/animation/utils.js +212 -0
- package/lib/module/animation/utils.js.map +1 -0
- package/lib/module/components/CLAUDE.md +1 -1
- package/lib/module/components/Card/index.js +47 -19
- package/lib/module/components/Card/index.js.map +1 -1
- package/lib/module/components/Toast/animation.js +324 -0
- package/lib/module/components/Toast/animation.js.map +1 -0
- package/lib/module/components/Toast/hooks.js +61 -0
- package/lib/module/components/Toast/hooks.js.map +1 -0
- package/lib/module/components/Toast/index.js +487 -0
- package/lib/module/components/Toast/index.js.map +1 -0
- package/lib/module/components/Toast/styles.js +138 -0
- package/lib/module/components/Toast/styles.js.map +1 -0
- package/lib/module/components/Toast/types.js +4 -0
- package/lib/module/components/Toast/types.js.map +1 -0
- package/lib/module/components/index.js +1 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/animation-settings/index.js +4 -0
- package/lib/module/providers/animation-settings/index.js.map +1 -0
- package/lib/module/providers/animation-settings/provider.js +52 -0
- package/lib/module/providers/animation-settings/provider.js.map +1 -0
- package/lib/module/providers/animation-settings/types.js +4 -0
- package/lib/module/providers/animation-settings/types.js.map +1 -0
- package/lib/module/providers/forge-design/index.js +5 -0
- package/lib/module/providers/forge-design/index.js.map +1 -0
- package/lib/module/providers/forge-design/provider.js +66 -0
- package/lib/module/providers/forge-design/provider.js.map +1 -0
- package/lib/module/providers/forge-design/types.js +4 -0
- package/lib/module/providers/forge-design/types.js.map +1 -0
- package/lib/module/providers/text-component/index.js +5 -0
- package/lib/module/providers/text-component/index.js.map +1 -0
- package/lib/module/providers/text-component/provider.js +6 -0
- package/lib/module/providers/text-component/provider.js.map +1 -0
- package/lib/module/providers/text-component/types.js +4 -0
- package/lib/module/providers/text-component/types.js.map +1 -0
- package/lib/module/providers/toast/index.js +6 -0
- package/lib/module/providers/toast/index.js.map +1 -0
- package/lib/module/providers/toast/insets-container.js +156 -0
- package/lib/module/providers/toast/insets-container.js.map +1 -0
- package/lib/module/providers/toast/lifecycle.context.js +32 -0
- package/lib/module/providers/toast/lifecycle.context.js.map +1 -0
- package/lib/module/providers/toast/provider.js +503 -0
- package/lib/module/providers/toast/provider.js.map +1 -0
- package/lib/module/providers/toast/reducer.js +42 -0
- package/lib/module/providers/toast/reducer.js.map +1 -0
- package/lib/module/providers/toast/toast-config.context.js +25 -0
- package/lib/module/providers/toast/toast-config.context.js.map +1 -0
- package/lib/module/providers/toast/toast-item-renderer.js +39 -0
- package/lib/module/providers/toast/toast-item-renderer.js.map +1 -0
- package/lib/module/providers/toast/types.js +4 -0
- package/lib/module/providers/toast/types.js.map +1 -0
- package/lib/typescript/animation/hooks/index.d.ts +2 -0
- package/lib/typescript/animation/hooks/index.d.ts.map +1 -0
- package/lib/typescript/animation/hooks/useCombinedAnimationDisabledState.d.ts +22 -0
- package/lib/typescript/animation/hooks/useCombinedAnimationDisabledState.d.ts.map +1 -0
- package/lib/typescript/animation/index.d.ts +4 -0
- package/lib/typescript/animation/index.d.ts.map +1 -0
- package/lib/typescript/animation/types.d.ts +105 -0
- package/lib/typescript/animation/types.d.ts.map +1 -0
- package/lib/typescript/animation/utils.d.ts +120 -0
- package/lib/typescript/animation/utils.d.ts.map +1 -0
- package/lib/typescript/components/Card/index.d.ts +4 -0
- package/lib/typescript/components/Card/index.d.ts.map +1 -1
- package/lib/typescript/components/Toast/animation.d.ts +21 -0
- package/lib/typescript/components/Toast/animation.d.ts.map +1 -0
- package/lib/typescript/components/Toast/hooks.d.ts +17 -0
- package/lib/typescript/components/Toast/hooks.d.ts.map +1 -0
- package/lib/typescript/components/Toast/index.d.ts +28 -0
- package/lib/typescript/components/Toast/index.d.ts.map +1 -0
- package/lib/typescript/components/Toast/styles.d.ts +73 -0
- package/lib/typescript/components/Toast/styles.d.ts.map +1 -0
- package/lib/typescript/components/Toast/types.d.ts +303 -0
- package/lib/typescript/components/Toast/types.d.ts.map +1 -0
- package/lib/typescript/components/index.d.ts +1 -0
- package/lib/typescript/components/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/providers/animation-settings/index.d.ts +3 -0
- package/lib/typescript/providers/animation-settings/index.d.ts.map +1 -0
- package/lib/typescript/providers/animation-settings/provider.d.ts +28 -0
- package/lib/typescript/providers/animation-settings/provider.d.ts.map +1 -0
- package/lib/typescript/providers/animation-settings/types.d.ts +35 -0
- package/lib/typescript/providers/animation-settings/types.d.ts.map +1 -0
- package/lib/typescript/providers/forge-design/index.d.ts +3 -0
- package/lib/typescript/providers/forge-design/index.d.ts.map +1 -0
- package/lib/typescript/providers/forge-design/provider.d.ts +24 -0
- package/lib/typescript/providers/forge-design/provider.d.ts.map +1 -0
- package/lib/typescript/providers/forge-design/types.d.ts +82 -0
- package/lib/typescript/providers/forge-design/types.d.ts.map +1 -0
- package/lib/typescript/providers/text-component/index.d.ts +3 -0
- package/lib/typescript/providers/text-component/index.d.ts.map +1 -0
- package/lib/typescript/providers/text-component/provider.d.ts +7 -0
- package/lib/typescript/providers/text-component/provider.d.ts.map +1 -0
- package/lib/typescript/providers/text-component/types.d.ts +51 -0
- package/lib/typescript/providers/text-component/types.d.ts.map +1 -0
- package/lib/typescript/providers/toast/index.d.ts +5 -0
- package/lib/typescript/providers/toast/index.d.ts.map +1 -0
- package/lib/typescript/providers/toast/insets-container.d.ts +72 -0
- package/lib/typescript/providers/toast/insets-container.d.ts.map +1 -0
- package/lib/typescript/providers/toast/lifecycle.context.d.ts +32 -0
- package/lib/typescript/providers/toast/lifecycle.context.d.ts.map +1 -0
- package/lib/typescript/providers/toast/provider.d.ts +32 -0
- package/lib/typescript/providers/toast/provider.d.ts.map +1 -0
- package/lib/typescript/providers/toast/reducer.d.ts +14 -0
- package/lib/typescript/providers/toast/reducer.d.ts.map +1 -0
- package/lib/typescript/providers/toast/toast-config.context.d.ts +20 -0
- package/lib/typescript/providers/toast/toast-config.context.d.ts.map +1 -0
- package/lib/typescript/providers/toast/toast-item-renderer.d.ts +7 -0
- package/lib/typescript/providers/toast/toast-item-renderer.d.ts.map +1 -0
- package/lib/typescript/providers/toast/types.d.ts +422 -0
- package/lib/typescript/providers/toast/types.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/animation/hooks/index.ts +1 -0
- package/src/animation/hooks/useCombinedAnimationDisabledState.ts +46 -0
- package/src/animation/index.ts +3 -0
- package/src/animation/types.ts +128 -0
- package/src/animation/utils.ts +249 -0
- package/src/components/CLAUDE.md +1 -1
- package/src/components/Card/index.tsx +58 -28
- package/src/components/Toast/animation.ts +387 -0
- package/src/components/Toast/hooks.ts +92 -0
- package/src/components/Toast/index.tsx +520 -0
- package/src/components/Toast/styles.ts +186 -0
- package/src/components/Toast/types.ts +332 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/providers/animation-settings/index.ts +11 -0
- package/src/providers/animation-settings/provider.tsx +54 -0
- package/src/providers/animation-settings/types.ts +37 -0
- package/src/providers/forge-design/index.ts +2 -0
- package/src/providers/forge-design/provider.tsx +61 -0
- package/src/providers/forge-design/types.ts +84 -0
- package/src/providers/text-component/index.ts +2 -0
- package/src/providers/text-component/provider.tsx +7 -0
- package/src/providers/text-component/types.ts +52 -0
- package/src/providers/toast/index.ts +4 -0
- package/src/providers/toast/insets-container.tsx +230 -0
- package/src/providers/toast/lifecycle.context.tsx +37 -0
- package/src/providers/toast/provider.tsx +567 -0
- package/src/providers/toast/reducer.ts +40 -0
- package/src/providers/toast/toast-config.context.ts +25 -0
- package/src/providers/toast/toast-item-renderer.tsx +40 -0
- package/src/providers/toast/types.ts +431 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useReducer,
|
|
8
|
+
useRef,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { Platform, View } from 'react-native';
|
|
11
|
+
import { useSharedValue } from 'react-native-reanimated';
|
|
12
|
+
import { DefaultToast } from '../../components/Toast';
|
|
13
|
+
import { InsetsContainer } from './insets-container';
|
|
14
|
+
import { ToastLifecycleContext, type ToastLifecycleContextValue } from './lifecycle.context';
|
|
15
|
+
import { toastReducer } from './reducer';
|
|
16
|
+
import { ToastConfigContext } from './toast-config.context';
|
|
17
|
+
import { ToastItemRenderer } from './toast-item-renderer';
|
|
18
|
+
import type {
|
|
19
|
+
ToastComponentProps,
|
|
20
|
+
ToasterContextValue,
|
|
21
|
+
ToastGlobalConfig,
|
|
22
|
+
ToastProviderProps,
|
|
23
|
+
ToastShowConfig,
|
|
24
|
+
ToastShowOptions,
|
|
25
|
+
ToastShowOptionsWithComponent,
|
|
26
|
+
} from './types';
|
|
27
|
+
|
|
28
|
+
const DEFAULT_DURATION = 4000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Context for toast manager
|
|
32
|
+
*/
|
|
33
|
+
const ToasterContext = createContext<ToasterContextValue | null>(null);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Merges global config with local config, ensuring local config takes precedence
|
|
37
|
+
* Only includes defined values from localConfig to avoid overriding global config with undefined
|
|
38
|
+
*/
|
|
39
|
+
function mergeToastConfig(
|
|
40
|
+
globalConfig: ToastGlobalConfig | undefined,
|
|
41
|
+
localConfig: Partial<ToastGlobalConfig>,
|
|
42
|
+
): Partial<ToastGlobalConfig> {
|
|
43
|
+
const result: Partial<ToastGlobalConfig> = { ...globalConfig };
|
|
44
|
+
|
|
45
|
+
// Only override with defined values from localConfig
|
|
46
|
+
if (localConfig.color !== undefined) {
|
|
47
|
+
result.color = localConfig.color;
|
|
48
|
+
}
|
|
49
|
+
if (localConfig.placement !== undefined) {
|
|
50
|
+
result.placement = localConfig.placement;
|
|
51
|
+
}
|
|
52
|
+
if (localConfig.isSwipeable !== undefined) {
|
|
53
|
+
result.isSwipeable = localConfig.isSwipeable;
|
|
54
|
+
}
|
|
55
|
+
if (localConfig.animation !== undefined) {
|
|
56
|
+
result.animation = localConfig.animation;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a component function for simple string toast
|
|
64
|
+
*/
|
|
65
|
+
function createStringToastComponent(
|
|
66
|
+
label: string,
|
|
67
|
+
globalConfig: ToastGlobalConfig | undefined,
|
|
68
|
+
): (props: ToastComponentProps) => React.ReactElement {
|
|
69
|
+
return (props: ToastComponentProps) => {
|
|
70
|
+
const mergedConfig = mergeToastConfig(globalConfig, {
|
|
71
|
+
color: 'default',
|
|
72
|
+
});
|
|
73
|
+
return (
|
|
74
|
+
<DefaultToast
|
|
75
|
+
{...props}
|
|
76
|
+
label={label}
|
|
77
|
+
color={mergedConfig.color}
|
|
78
|
+
placement={mergedConfig.placement}
|
|
79
|
+
isSwipeable={mergedConfig.isSwipeable}
|
|
80
|
+
animation={mergedConfig.animation}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a component function for config-based toast
|
|
88
|
+
*/
|
|
89
|
+
function createConfigToastComponent(
|
|
90
|
+
config: ToastShowConfig,
|
|
91
|
+
globalConfig: ToastGlobalConfig | undefined,
|
|
92
|
+
): (props: ToastComponentProps) => React.ReactElement {
|
|
93
|
+
return (props: ToastComponentProps) => {
|
|
94
|
+
const mergedConfig = mergeToastConfig(globalConfig, {
|
|
95
|
+
color: config.color,
|
|
96
|
+
placement: config.placement,
|
|
97
|
+
isSwipeable: config.isSwipeable,
|
|
98
|
+
animation: config.animation,
|
|
99
|
+
});
|
|
100
|
+
return (
|
|
101
|
+
<DefaultToast
|
|
102
|
+
{...props}
|
|
103
|
+
color={mergedConfig.color}
|
|
104
|
+
placement={mergedConfig.placement}
|
|
105
|
+
isSwipeable={mergedConfig.isSwipeable}
|
|
106
|
+
animation={mergedConfig.animation}
|
|
107
|
+
label={config.label}
|
|
108
|
+
description={config.description}
|
|
109
|
+
actionLabel={config.actionLabel}
|
|
110
|
+
onActionPress={config.onActionPress}
|
|
111
|
+
icon={config.icon}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Toast provider component
|
|
119
|
+
* Wraps your app to enable toast functionality
|
|
120
|
+
*/
|
|
121
|
+
export function ToastProvider({
|
|
122
|
+
defaultProps,
|
|
123
|
+
insets,
|
|
124
|
+
maxVisibleToasts = 3,
|
|
125
|
+
contentWrapper,
|
|
126
|
+
children,
|
|
127
|
+
disableFullWindowOverlay = false,
|
|
128
|
+
unstable_accessibilityContainerViewIsModal,
|
|
129
|
+
announceLabel,
|
|
130
|
+
}: Readonly<ToastProviderProps>) {
|
|
131
|
+
const [toasts, dispatch] = useReducer(toastReducer, []);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Memoize global config to prevent unnecessary re-renders
|
|
135
|
+
*/
|
|
136
|
+
const globalConfig = useMemo<ToastGlobalConfig | undefined>(() => defaultProps, [defaultProps]);
|
|
137
|
+
|
|
138
|
+
const isToastVisible = toasts.length > 0;
|
|
139
|
+
|
|
140
|
+
const heights = useSharedValue<Record<string, number>>({});
|
|
141
|
+
|
|
142
|
+
const total = useSharedValue<number>(0);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Derive total from toasts.length so the animated opacity/scale/translateY
|
|
146
|
+
* interpolations always use the real count. Manual increment/decrement
|
|
147
|
+
* was prone to drift when hide + show ran in the same tick or when
|
|
148
|
+
* auto-dismiss raced with a manual hide (stale-closure mismatch).
|
|
149
|
+
*/
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
total.set(toasts.length);
|
|
152
|
+
}, [toasts.length, total]);
|
|
153
|
+
|
|
154
|
+
const idCounter = useRef(0);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Timer state per toast — tracks remaining time so pause/resume can
|
|
158
|
+
* restart with only the leftover duration (WCAG 2.2.1).
|
|
159
|
+
*/
|
|
160
|
+
const timersRef = useRef<
|
|
161
|
+
Map<
|
|
162
|
+
string,
|
|
163
|
+
{ startedAt: number; remaining: number; timer: NodeJS.Timeout | null; duration: number }
|
|
164
|
+
>
|
|
165
|
+
>(new Map());
|
|
166
|
+
const pausedRef = useRef(false);
|
|
167
|
+
const hideRef = useRef<((ids?: (string & {}) | string[] | 'all') => void) | null>(null);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Safety reset: when all toasts are gone the InsetsContainer unmounts and
|
|
171
|
+
* no resume event can fire. Clear the paused flag and any orphaned timer
|
|
172
|
+
* entries so the next toast's timer starts normally.
|
|
173
|
+
*/
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (toasts.length === 0) {
|
|
176
|
+
pausedRef.current = false;
|
|
177
|
+
timersRef.current.forEach((entry) => {
|
|
178
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
179
|
+
});
|
|
180
|
+
timersRef.current.clear();
|
|
181
|
+
}
|
|
182
|
+
}, [toasts.length]);
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Remove one or more toasts from the array. On web this is the second half
|
|
186
|
+
* of the two-phase hide flow — the toast component calls it from
|
|
187
|
+
* `onTransitionEnd` (or a safety timeout) after the exit transition plays.
|
|
188
|
+
* Also cleans up per-id height state.
|
|
189
|
+
*/
|
|
190
|
+
const remove = useCallback(
|
|
191
|
+
(ids: string | string[]) => {
|
|
192
|
+
const idsArray = Array.isArray(ids) ? ids : [ids];
|
|
193
|
+
if (idsArray.length === 0) return;
|
|
194
|
+
|
|
195
|
+
dispatch({ type: 'REMOVE', payload: { ids: idsArray } });
|
|
196
|
+
|
|
197
|
+
heights.modify(<T extends Record<string, number>>(value: T): T => {
|
|
198
|
+
'worklet';
|
|
199
|
+
const result = { ...value };
|
|
200
|
+
for (const id of idsArray) {
|
|
201
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
202
|
+
delete result[id];
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
[heights],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Hide one or more toasts
|
|
212
|
+
* - No argument: hides the last toast in the array
|
|
213
|
+
* - "all": hides all toasts
|
|
214
|
+
* - Single ID: hides that toast
|
|
215
|
+
* - Array of IDs: hides those toasts
|
|
216
|
+
*
|
|
217
|
+
* On web this only marks toasts as `pendingRemoval` so their CSS exit
|
|
218
|
+
* transition can play; the actual removal happens when the toast
|
|
219
|
+
* component calls `remove(id)` on transition end.
|
|
220
|
+
* On native this dispatches removal directly — Reanimated's `exiting`
|
|
221
|
+
* layout animation plays before unmount.
|
|
222
|
+
*/
|
|
223
|
+
const hide = useCallback(
|
|
224
|
+
(ids?: (string & {}) | string[] | 'all') => {
|
|
225
|
+
const isWeb = Platform.OS === 'web';
|
|
226
|
+
|
|
227
|
+
if (ids === undefined) {
|
|
228
|
+
if (toasts.length === 0) return;
|
|
229
|
+
// Hide the last non-pending toast in the array. Using the last
|
|
230
|
+
// pending toast would be a no-op on web.
|
|
231
|
+
let lastToast = null;
|
|
232
|
+
for (let i = toasts.length - 1; i >= 0; i--) {
|
|
233
|
+
const candidate = toasts[i];
|
|
234
|
+
if (!candidate.pendingRemoval) {
|
|
235
|
+
lastToast = candidate;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (!lastToast) return;
|
|
240
|
+
|
|
241
|
+
const timerEntry = timersRef.current.get(lastToast.id);
|
|
242
|
+
if (timerEntry?.timer) {
|
|
243
|
+
clearTimeout(timerEntry.timer);
|
|
244
|
+
}
|
|
245
|
+
timersRef.current.delete(lastToast.id);
|
|
246
|
+
|
|
247
|
+
if (lastToast.onHide) {
|
|
248
|
+
lastToast.onHide();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isWeb) {
|
|
252
|
+
dispatch({ type: 'MARK_CLOSED', payload: { ids: [lastToast.id] } });
|
|
253
|
+
} else {
|
|
254
|
+
remove([lastToast.id]);
|
|
255
|
+
}
|
|
256
|
+
} else if (ids === 'all') {
|
|
257
|
+
timersRef.current.forEach((entry) => {
|
|
258
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
259
|
+
});
|
|
260
|
+
timersRef.current.clear();
|
|
261
|
+
|
|
262
|
+
const targetIds: string[] = [];
|
|
263
|
+
toasts.forEach((toast) => {
|
|
264
|
+
if (toast.pendingRemoval) return;
|
|
265
|
+
targetIds.push(toast.id);
|
|
266
|
+
if (toast.onHide) {
|
|
267
|
+
toast.onHide();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (targetIds.length === 0) return;
|
|
272
|
+
|
|
273
|
+
if (isWeb) {
|
|
274
|
+
dispatch({ type: 'MARK_CLOSED', payload: { ids: targetIds } });
|
|
275
|
+
} else {
|
|
276
|
+
remove(targetIds);
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
const idsArray = Array.isArray(ids) ? ids : [ids];
|
|
280
|
+
const targetIds: string[] = [];
|
|
281
|
+
|
|
282
|
+
idsArray.forEach((id) => {
|
|
283
|
+
const toast = toasts.find((t) => t.id === id);
|
|
284
|
+
if (!toast || toast.pendingRemoval) return;
|
|
285
|
+
|
|
286
|
+
const timerEntry = timersRef.current.get(id);
|
|
287
|
+
if (timerEntry?.timer) {
|
|
288
|
+
clearTimeout(timerEntry.timer);
|
|
289
|
+
}
|
|
290
|
+
timersRef.current.delete(id);
|
|
291
|
+
|
|
292
|
+
targetIds.push(id);
|
|
293
|
+
if (toast.onHide) {
|
|
294
|
+
toast.onHide();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (targetIds.length === 0) return;
|
|
299
|
+
|
|
300
|
+
if (isWeb) {
|
|
301
|
+
dispatch({ type: 'MARK_CLOSED', payload: { ids: targetIds } });
|
|
302
|
+
} else {
|
|
303
|
+
remove(targetIds);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
[remove, toasts],
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Keep hide ref up to date
|
|
311
|
+
hideRef.current = hide;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Pause all running auto-dismiss timers (WCAG 2.2.1).
|
|
315
|
+
* Stores elapsed time so resumeAll can restart with the remainder.
|
|
316
|
+
*/
|
|
317
|
+
const pauseAll = useCallback(() => {
|
|
318
|
+
if (pausedRef.current) return;
|
|
319
|
+
pausedRef.current = true;
|
|
320
|
+
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
timersRef.current.forEach((entry) => {
|
|
323
|
+
if (entry.timer) {
|
|
324
|
+
clearTimeout(entry.timer);
|
|
325
|
+
entry.timer = null;
|
|
326
|
+
}
|
|
327
|
+
const elapsed = now - entry.startedAt;
|
|
328
|
+
entry.remaining = Math.max(0, entry.remaining - elapsed);
|
|
329
|
+
});
|
|
330
|
+
}, []);
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Resume all paused auto-dismiss timers with their remaining duration.
|
|
334
|
+
*/
|
|
335
|
+
const resumeAll = useCallback(() => {
|
|
336
|
+
pausedRef.current = false;
|
|
337
|
+
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
timersRef.current.forEach((entry, id) => {
|
|
340
|
+
if (entry.timer) return;
|
|
341
|
+
if (entry.remaining <= 0) {
|
|
342
|
+
if (hideRef.current) hideRef.current(id);
|
|
343
|
+
timersRef.current.delete(id);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
entry.startedAt = now;
|
|
347
|
+
entry.timer = setTimeout(() => {
|
|
348
|
+
if (hideRef.current) hideRef.current(id);
|
|
349
|
+
timersRef.current.delete(id);
|
|
350
|
+
}, entry.remaining);
|
|
351
|
+
});
|
|
352
|
+
}, []);
|
|
353
|
+
|
|
354
|
+
const lifecycleContextValue = useMemo<ToastLifecycleContextValue>(() => {
|
|
355
|
+
const pending = new Set<string>();
|
|
356
|
+
let frontmostId: string | null = null;
|
|
357
|
+
for (const toast of toasts) {
|
|
358
|
+
if (toast.pendingRemoval) {
|
|
359
|
+
pending.add(toast.id);
|
|
360
|
+
} else {
|
|
361
|
+
frontmostId = toast.id;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
remove: (id: string) => remove([id]),
|
|
366
|
+
pendingRemovalIds: pending,
|
|
367
|
+
frontmostId,
|
|
368
|
+
};
|
|
369
|
+
}, [remove, toasts]);
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Show a toast
|
|
373
|
+
* Supports three usage patterns:
|
|
374
|
+
* 1. Simple string: toast.show('This is toast')
|
|
375
|
+
* 2. Config object: toast.show({ label, color, ... })
|
|
376
|
+
* 3. Custom component: toast.show({ component: (props) => <Toast>...</Toast> })
|
|
377
|
+
*/
|
|
378
|
+
const show = useCallback(
|
|
379
|
+
(options: string | ToastShowOptions): string => {
|
|
380
|
+
let normalizedOptions: ToastShowOptionsWithComponent;
|
|
381
|
+
let duration: number | 'persistent' | undefined = DEFAULT_DURATION; // Default duration
|
|
382
|
+
let explicitId: string | undefined;
|
|
383
|
+
|
|
384
|
+
// Case 1: Simple string
|
|
385
|
+
if (typeof options === 'string') {
|
|
386
|
+
normalizedOptions = {
|
|
387
|
+
id: undefined,
|
|
388
|
+
component: createStringToastComponent(options, globalConfig),
|
|
389
|
+
duration: DEFAULT_DURATION,
|
|
390
|
+
};
|
|
391
|
+
duration = DEFAULT_DURATION;
|
|
392
|
+
explicitId = undefined;
|
|
393
|
+
}
|
|
394
|
+
// Case 2: Config object without component
|
|
395
|
+
else if (!('component' in options) || options.component === undefined) {
|
|
396
|
+
const config = options as ToastShowConfig;
|
|
397
|
+
duration = config.duration ?? DEFAULT_DURATION;
|
|
398
|
+
explicitId = config.id;
|
|
399
|
+
normalizedOptions = {
|
|
400
|
+
id: config.id,
|
|
401
|
+
component: createConfigToastComponent(config, globalConfig),
|
|
402
|
+
duration,
|
|
403
|
+
onShow: config.onShow,
|
|
404
|
+
onHide: config.onHide,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// Case 3: Config object with component (existing behavior)
|
|
408
|
+
else {
|
|
409
|
+
normalizedOptions = options;
|
|
410
|
+
duration = normalizedOptions.duration ?? DEFAULT_DURATION;
|
|
411
|
+
explicitId = normalizedOptions.id;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const id =
|
|
415
|
+
normalizedOptions.id ?? `toast-${String(Date.now())}-${String(idCounter.current++)}`;
|
|
416
|
+
|
|
417
|
+
// If an explicit ID was provided, check if a toast with that ID already exists
|
|
418
|
+
// If it exists, skip adding a new toast and return the existing ID
|
|
419
|
+
if (explicitId !== undefined) {
|
|
420
|
+
const existingToast = toasts.find((toast) => toast.id === explicitId);
|
|
421
|
+
if (existingToast) {
|
|
422
|
+
return existingToast.id;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
dispatch({
|
|
427
|
+
type: 'SHOW',
|
|
428
|
+
payload: {
|
|
429
|
+
id,
|
|
430
|
+
component: normalizedOptions.component,
|
|
431
|
+
duration,
|
|
432
|
+
onShow: normalizedOptions.onShow,
|
|
433
|
+
onHide: normalizedOptions.onHide,
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
if (normalizedOptions.onShow) {
|
|
438
|
+
normalizedOptions.onShow();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Set up auto-dismiss timeout synchronously
|
|
442
|
+
if (
|
|
443
|
+
duration !== 'persistent' &&
|
|
444
|
+
typeof duration === 'number' &&
|
|
445
|
+
!Number.isNaN(duration) &&
|
|
446
|
+
duration >= 0 &&
|
|
447
|
+
duration !== Infinity
|
|
448
|
+
) {
|
|
449
|
+
// Handle immediate dismissal
|
|
450
|
+
if (duration === 0) {
|
|
451
|
+
if (hideRef.current) {
|
|
452
|
+
hideRef.current(id);
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
const now = Date.now();
|
|
456
|
+
const timer = setTimeout(() => {
|
|
457
|
+
if (hideRef.current) {
|
|
458
|
+
hideRef.current(id);
|
|
459
|
+
}
|
|
460
|
+
timersRef.current.delete(id);
|
|
461
|
+
}, duration);
|
|
462
|
+
const entry = {
|
|
463
|
+
startedAt: now,
|
|
464
|
+
remaining: duration,
|
|
465
|
+
timer: timer as NodeJS.Timeout | null,
|
|
466
|
+
duration,
|
|
467
|
+
};
|
|
468
|
+
timersRef.current.set(id, entry);
|
|
469
|
+
|
|
470
|
+
if (pausedRef.current) {
|
|
471
|
+
clearTimeout(timer);
|
|
472
|
+
entry.timer = null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return id;
|
|
478
|
+
},
|
|
479
|
+
[toasts, globalConfig],
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const contextValue = useMemo<ToasterContextValue>(
|
|
483
|
+
() => ({
|
|
484
|
+
toast: {
|
|
485
|
+
show,
|
|
486
|
+
hide,
|
|
487
|
+
pauseAll,
|
|
488
|
+
resumeAll,
|
|
489
|
+
},
|
|
490
|
+
isToastVisible,
|
|
491
|
+
}),
|
|
492
|
+
[show, hide, pauseAll, resumeAll, isToastVisible],
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<ToastConfigContext.Provider value={globalConfig}>
|
|
497
|
+
<ToasterContext.Provider value={contextValue}>
|
|
498
|
+
<ToastLifecycleContext.Provider value={lifecycleContextValue}>
|
|
499
|
+
{children}
|
|
500
|
+
{toasts.length > 0 && (
|
|
501
|
+
<InsetsContainer
|
|
502
|
+
insets={insets}
|
|
503
|
+
announceLabel={announceLabel}
|
|
504
|
+
contentWrapper={contentWrapper}
|
|
505
|
+
disableFullWindowOverlay={disableFullWindowOverlay}
|
|
506
|
+
unstable_accessibilityContainerViewIsModal={
|
|
507
|
+
unstable_accessibilityContainerViewIsModal
|
|
508
|
+
}
|
|
509
|
+
pauseAll={pauseAll}
|
|
510
|
+
resumeAll={resumeAll}
|
|
511
|
+
hide={hide}
|
|
512
|
+
>
|
|
513
|
+
<View className="flex-1">
|
|
514
|
+
{toasts.map((toastItem, index) => (
|
|
515
|
+
<ToastItemRenderer
|
|
516
|
+
key={toastItem.id}
|
|
517
|
+
toastItem={toastItem}
|
|
518
|
+
show={show}
|
|
519
|
+
hide={hide}
|
|
520
|
+
index={index}
|
|
521
|
+
total={total}
|
|
522
|
+
heights={heights}
|
|
523
|
+
maxVisibleToasts={maxVisibleToasts}
|
|
524
|
+
/>
|
|
525
|
+
))}
|
|
526
|
+
</View>
|
|
527
|
+
</InsetsContainer>
|
|
528
|
+
)}
|
|
529
|
+
</ToastLifecycleContext.Provider>
|
|
530
|
+
</ToasterContext.Provider>
|
|
531
|
+
</ToastConfigContext.Provider>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Hook to access toast functionality
|
|
537
|
+
*
|
|
538
|
+
* @returns Object containing toast manager and visibility state
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* ```tsx
|
|
542
|
+
* const { toast, isToastVisible } = useToast();
|
|
543
|
+
*
|
|
544
|
+
* // Show a toast
|
|
545
|
+
* toast.show({ component: <Toast>Hello</Toast> });
|
|
546
|
+
*
|
|
547
|
+
* // Hide a toast
|
|
548
|
+
* toast.hide('my-toast');
|
|
549
|
+
*
|
|
550
|
+
* // Check if any toast is visible
|
|
551
|
+
* if (isToastVisible) {
|
|
552
|
+
* console.log('A toast is currently displayed');
|
|
553
|
+
* }
|
|
554
|
+
* ```
|
|
555
|
+
*/
|
|
556
|
+
export function useToast() {
|
|
557
|
+
const context = useContext(ToasterContext);
|
|
558
|
+
|
|
559
|
+
if (!context) {
|
|
560
|
+
throw new Error('useToast must be used within a ToastProvider provider');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
toast: context.toast,
|
|
565
|
+
isToastVisible: context.isToastVisible,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ToastAction, ToastItem } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reducer for managing toast state
|
|
5
|
+
*
|
|
6
|
+
* Removal is a two-phase flow so web can play its CSS exit transition
|
|
7
|
+
* before the DOM node is unmounted:
|
|
8
|
+
* `MARK_CLOSED` → flips `pendingRemoval: true` (node stays mounted).
|
|
9
|
+
* `REMOVE` → filters the toast out of the array (node unmounts).
|
|
10
|
+
*
|
|
11
|
+
* Native dispatches `REMOVE` directly since Reanimated's `exiting`
|
|
12
|
+
* layout animation plays before unmount without a lingering state.
|
|
13
|
+
*/
|
|
14
|
+
export function toastReducer(state: ToastItem[], action: ToastAction): ToastItem[] {
|
|
15
|
+
switch (action.type) {
|
|
16
|
+
case 'SHOW': {
|
|
17
|
+
const filtered = state.filter((toast) => toast.id !== action.payload.id);
|
|
18
|
+
return [...filtered, action.payload];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
case 'MARK_CLOSED': {
|
|
22
|
+
const idsSet = new Set(action.payload.ids);
|
|
23
|
+
// Early out preserves the array reference when nothing would change,
|
|
24
|
+
// which keeps `useMemo(..., [toasts])` consumers stable.
|
|
25
|
+
const hasChange = state.some((toast) => idsSet.has(toast.id) && !toast.pendingRemoval);
|
|
26
|
+
if (!hasChange) return state;
|
|
27
|
+
return state.map((toast) =>
|
|
28
|
+
idsSet.has(toast.id) && !toast.pendingRemoval ? { ...toast, pendingRemoval: true } : toast,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
case 'REMOVE': {
|
|
33
|
+
const idsSet = new Set(action.payload.ids);
|
|
34
|
+
return state.filter((toast) => !idsSet.has(toast.id));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
default:
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { ToastGlobalConfig } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context for global toast configuration
|
|
6
|
+
* Extracted to separate file to avoid circular dependencies with toast component
|
|
7
|
+
*/
|
|
8
|
+
const ToastConfigContext = createContext<ToastGlobalConfig | undefined>(undefined);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to access global toast configuration
|
|
12
|
+
*
|
|
13
|
+
* @returns Global toast configuration or undefined if not set
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const globalConfig = useToastConfig();
|
|
18
|
+
* // Use globalConfig.color, globalConfig.placement, etc.
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useToastConfig(): ToastGlobalConfig | undefined {
|
|
22
|
+
return useContext(ToastConfigContext);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { ToastConfigContext };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import type { ToastItemRendererProps } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Memoized toast item component to prevent unnecessary re-renders
|
|
6
|
+
* Only re-renders when the toast item itself changes
|
|
7
|
+
*/
|
|
8
|
+
export const ToastItemRenderer = memo(
|
|
9
|
+
({ toastItem, show, hide, index, total, heights, maxVisibleToasts }: ToastItemRendererProps) => {
|
|
10
|
+
if (typeof toastItem.component !== 'function') {
|
|
11
|
+
throw new Error('Toast component must be a function that receives ToastComponentProps');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const content = toastItem.component({
|
|
15
|
+
id: toastItem.id,
|
|
16
|
+
index,
|
|
17
|
+
total,
|
|
18
|
+
heights,
|
|
19
|
+
maxVisibleToasts,
|
|
20
|
+
show,
|
|
21
|
+
hide,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return content;
|
|
25
|
+
},
|
|
26
|
+
(prevProps, nextProps) => {
|
|
27
|
+
// Only re-render if the toast ID, component reference, index, or
|
|
28
|
+
// pending-removal flag changed. `pendingRemoval` flips when the
|
|
29
|
+
// provider marks a toast closed on web so the Toast component can
|
|
30
|
+
// play its exit transition — the renderer must propagate that
|
|
31
|
+
// change even though the id and component reference are unchanged.
|
|
32
|
+
// show, hide, total, and heights are stable references.
|
|
33
|
+
return (
|
|
34
|
+
prevProps.toastItem.id === nextProps.toastItem.id &&
|
|
35
|
+
prevProps.toastItem.component === nextProps.toastItem.component &&
|
|
36
|
+
prevProps.toastItem.pendingRemoval === nextProps.toastItem.pendingRemoval &&
|
|
37
|
+
prevProps.index === nextProps.index
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
);
|