Skip to content

Commit 26722fc

Browse files
committed
feat(toast): handle global toast props
1 parent 9c3f79b commit 26722fc

File tree

5 files changed

+180
-50
lines changed

5 files changed

+180
-50
lines changed

src/components/toast/toast.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { ViewRef } from '../../helpers/types';
99
import { createContext } from '../../helpers/utils';
1010
import * as ToastPrimitive from '../../primitives/toast';
1111
import type { ToastComponentProps } from '../../providers/toast';
12+
import { useToastConfig } from '../../providers/toast';
1213
import { Button } from '../button';
1314
import type { PressableFeedbackHighlightRootAnimation } from '../pressable-feedback';
1415
import { useToastRootAnimation } from './toast.animation';
@@ -33,22 +34,32 @@ const [ToastProvider, useToast] = createContext<ToastContextValue>({
3334
// --------------------------------------------------
3435

3536
const ToastRoot = forwardRef<ViewRef, ToastRootProps>((props, ref) => {
37+
const globalConfig = useToastConfig();
38+
3639
const {
3740
children,
38-
variant = 'default',
39-
placement = 'top',
41+
variant: localVariant,
42+
placement: localPlacement,
4043
index,
4144
total,
4245
heights,
4346
maxVisibleToasts,
4447
className,
4548
style,
46-
animation,
47-
isSwipeable,
49+
animation: localAnimation,
50+
isSwipeable: localIsSwipeable,
4851
hide,
4952
...restProps
5053
} = props;
5154

55+
/**
56+
* Merge global config with local props, ensuring local props take precedence
57+
*/
58+
const variant = localVariant ?? globalConfig?.variant ?? 'default';
59+
const placement = localPlacement ?? globalConfig?.placement ?? 'top';
60+
const animation = localAnimation ?? globalConfig?.animation;
61+
const isSwipeable = localIsSwipeable ?? globalConfig?.isSwipeable;
62+
5263
// Access id from props (id is omitted from ToastRootProps type but available at runtime)
5364
const toastProps = props as ToastRootProps & Pick<ToastComponentProps, 'id'>;
5465
const { id } = toastProps;
@@ -288,11 +299,14 @@ const ToastClose = forwardRef<View, ToastCloseProps>((props, ref) => {
288299
* Used internally when showing toasts with string or config object (without component)
289300
*/
290301
export function DefaultToast(props: DefaultToastProps) {
302+
const globalConfig = useToastConfig();
303+
291304
const {
292305
id,
293-
variant = 'default',
294-
placement = 'top',
295-
isSwipeable,
306+
variant: localVariant,
307+
placement: localPlacement,
308+
isSwipeable: localIsSwipeable,
309+
animation: localAnimation,
296310
label,
297311
description,
298312
actionLabel,
@@ -303,6 +317,14 @@ export function DefaultToast(props: DefaultToastProps) {
303317
...toastComponentProps
304318
} = props;
305319

320+
/**
321+
* Merge global config with local props, ensuring local props take precedence
322+
*/
323+
const variant = localVariant ?? globalConfig?.variant ?? 'default';
324+
const placement = localPlacement ?? globalConfig?.placement ?? 'top';
325+
const isSwipeable = localIsSwipeable ?? globalConfig?.isSwipeable;
326+
const animation = localAnimation ?? globalConfig?.animation;
327+
306328
const handleActionPress = () => {
307329
if (onActionPress) {
308330
onActionPress({ show, hide });
@@ -315,6 +337,7 @@ export function DefaultToast(props: DefaultToastProps) {
315337
variant={variant}
316338
placement={placement}
317339
isSwipeable={isSwipeable}
340+
animation={animation}
318341
className="flex-row gap-3"
319342
hide={hide}
320343
show={show}

src/components/toast/toast.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ export interface DefaultToastProps extends ToastComponentProps {
246246
* @default 'top'
247247
*/
248248
placement?: ToastRootProps['placement'];
249+
/**
250+
* Animation configuration for toast
251+
*/
252+
animation?: ToastRootProps['animation'];
249253
/**
250254
* Whether the toast can be swiped to dismiss and dragged with rubber effect
251255
*/

src/providers/toast/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export { ToastProvider, useToast } from './provider';
1+
export { ToastProvider, useToast, useToastConfig } from './provider';
22
export type * from './types';

src/providers/toast/provider.tsx

Lines changed: 124 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ToastItemRenderer } from './toast-item-renderer';
1515
import type {
1616
ToastComponentProps,
1717
ToasterContextValue,
18+
ToastGlobalConfig,
1819
ToastProviderProps,
1920
ToastShowConfig,
2021
ToastShowOptions,
@@ -28,50 +29,116 @@ const DEFAULT_DURATION = 4000;
2829
*/
2930
const ToasterContext = createContext<ToasterContextValue | null>(null);
3031

32+
/**
33+
* Context for global toast configuration
34+
*/
35+
const ToastConfigContext = createContext<ToastGlobalConfig | undefined>(
36+
undefined
37+
);
38+
39+
/**
40+
* Merges global config with local config, ensuring local config takes precedence
41+
* Only includes defined values from localConfig to avoid overriding global config with undefined
42+
*/
43+
function mergeToastConfig(
44+
globalConfig: ToastGlobalConfig | undefined,
45+
localConfig: Partial<ToastGlobalConfig>
46+
): Partial<ToastGlobalConfig> {
47+
const result: Partial<ToastGlobalConfig> = { ...globalConfig };
48+
49+
// Only override with defined values from localConfig
50+
if (localConfig.variant !== undefined) {
51+
result.variant = localConfig.variant;
52+
}
53+
if (localConfig.placement !== undefined) {
54+
result.placement = localConfig.placement;
55+
}
56+
if (localConfig.isSwipeable !== undefined) {
57+
result.isSwipeable = localConfig.isSwipeable;
58+
}
59+
if (localConfig.animation !== undefined) {
60+
result.animation = localConfig.animation;
61+
}
62+
63+
return result;
64+
}
65+
3166
/**
3267
* Creates a component function for simple string toast
3368
*/
3469
function createStringToastComponent(
35-
label: string
70+
label: string,
71+
globalConfig: ToastGlobalConfig | undefined
3672
): (props: ToastComponentProps) => React.ReactElement {
37-
return (props: ToastComponentProps) => (
38-
<DefaultToast {...props} label={label} variant="default" />
39-
);
73+
return (props: ToastComponentProps) => {
74+
const mergedConfig = mergeToastConfig(globalConfig, {
75+
variant: 'default',
76+
});
77+
return (
78+
<DefaultToast
79+
{...props}
80+
label={label}
81+
variant={mergedConfig.variant}
82+
placement={mergedConfig.placement}
83+
isSwipeable={mergedConfig.isSwipeable}
84+
animation={mergedConfig.animation}
85+
/>
86+
);
87+
};
4088
}
4189

4290
/**
4391
* Creates a component function for config-based toast
4492
*/
4593
function createConfigToastComponent(
46-
config: ToastShowConfig
94+
config: ToastShowConfig,
95+
globalConfig: ToastGlobalConfig | undefined
4796
): (props: ToastComponentProps) => React.ReactElement {
48-
return (props: ToastComponentProps) => (
49-
<DefaultToast
50-
{...props}
51-
variant={config.variant}
52-
placement={config.placement}
53-
isSwipeable={config.isSwipeable}
54-
label={config.label}
55-
description={config.description}
56-
actionLabel={config.actionLabel}
57-
onActionPress={config.onActionPress}
58-
icon={config.icon}
59-
/>
60-
);
97+
return (props: ToastComponentProps) => {
98+
const mergedConfig = mergeToastConfig(globalConfig, {
99+
variant: config.variant,
100+
placement: config.placement,
101+
isSwipeable: config.isSwipeable,
102+
animation: config.animation,
103+
});
104+
return (
105+
<DefaultToast
106+
{...props}
107+
variant={mergedConfig.variant}
108+
placement={mergedConfig.placement}
109+
isSwipeable={mergedConfig.isSwipeable}
110+
animation={mergedConfig.animation}
111+
label={config.label}
112+
description={config.description}
113+
actionLabel={config.actionLabel}
114+
onActionPress={config.onActionPress}
115+
icon={config.icon}
116+
/>
117+
);
118+
};
61119
}
62120

63121
/**
64122
* Toast provider component
65123
* Wraps your app to enable toast functionality
66124
*/
67125
export function ToastProvider({
126+
defaultProps,
68127
insets,
69128
maxVisibleToasts = 3,
70129
contentWrapper,
71130
children,
72131
}: ToastProviderProps) {
73132
const [toasts, dispatch] = useReducer(toastReducer, []);
74133

134+
/**
135+
* Memoize global config to prevent unnecessary re-renders
136+
*/
137+
const globalConfig = useMemo<ToastGlobalConfig | undefined>(
138+
() => defaultProps,
139+
[defaultProps]
140+
);
141+
75142
const isToastVisible = toasts.length > 0;
76143

77144
const heights = useSharedValue<Record<string, number>>({});
@@ -206,7 +273,7 @@ export function ToastProvider({
206273
if (typeof options === 'string') {
207274
normalizedOptions = {
208275
id: undefined,
209-
component: createStringToastComponent(options),
276+
component: createStringToastComponent(options, globalConfig),
210277
duration: DEFAULT_DURATION,
211278
};
212279
duration = DEFAULT_DURATION;
@@ -219,7 +286,7 @@ export function ToastProvider({
219286
explicitId = config.id;
220287
normalizedOptions = {
221288
id: config.id,
222-
component: createConfigToastComponent(config),
289+
component: createConfigToastComponent(config, globalConfig),
223290
duration,
224291
onShow: config.onShow,
225292
onHide: config.onHide,
@@ -289,7 +356,7 @@ export function ToastProvider({
289356

290357
return id;
291358
},
292-
[total, toasts]
359+
[total, toasts, globalConfig]
293360
);
294361

295362
const contextValue = useMemo<ToasterContextValue>(
@@ -304,25 +371,27 @@ export function ToastProvider({
304371
);
305372

306373
return (
307-
<ToasterContext.Provider value={contextValue}>
308-
{children}
309-
<InsetsContainer insets={insets} contentWrapper={contentWrapper}>
310-
<View className="flex-1">
311-
{toasts.map((toastItem, index) => (
312-
<ToastItemRenderer
313-
key={toastItem.id}
314-
toastItem={toastItem}
315-
show={show}
316-
hide={hide}
317-
index={index}
318-
total={total}
319-
heights={heights}
320-
maxVisibleToasts={maxVisibleToasts}
321-
/>
322-
))}
323-
</View>
324-
</InsetsContainer>
325-
</ToasterContext.Provider>
374+
<ToastConfigContext.Provider value={globalConfig}>
375+
<ToasterContext.Provider value={contextValue}>
376+
{children}
377+
<InsetsContainer insets={insets} contentWrapper={contentWrapper}>
378+
<View className="flex-1">
379+
{toasts.map((toastItem, index) => (
380+
<ToastItemRenderer
381+
key={toastItem.id}
382+
toastItem={toastItem}
383+
show={show}
384+
hide={hide}
385+
index={index}
386+
total={total}
387+
heights={heights}
388+
maxVisibleToasts={maxVisibleToasts}
389+
/>
390+
))}
391+
</View>
392+
</InsetsContainer>
393+
</ToasterContext.Provider>
394+
</ToastConfigContext.Provider>
326395
);
327396
}
328397

@@ -359,3 +428,18 @@ export function useToast() {
359428
isToastVisible: context.isToastVisible,
360429
};
361430
}
431+
432+
/**
433+
* Hook to access global toast configuration
434+
*
435+
* @returns Global toast configuration or undefined if not set
436+
*
437+
* @example
438+
* ```tsx
439+
* const globalConfig = useToastConfig();
440+
* // Use globalConfig.variant, globalConfig.placement, etc.
441+
* ```
442+
*/
443+
export function useToastConfig(): ToastGlobalConfig | undefined {
444+
return useContext(ToastConfigContext);
445+
}

src/providers/toast/types.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import type { SharedValue } from 'react-native-reanimated';
22
import type { ToastRootProps } from '../../components/toast/toast.types';
33

4+
/**
5+
* Global toast configuration
6+
* These values are used as defaults for all toasts unless overridden locally
7+
*/
8+
export interface ToastGlobalConfig
9+
extends Pick<
10+
ToastRootProps,
11+
'variant' | 'placement' | 'isSwipeable' | 'animation'
12+
> {}
13+
414
/**
515
* Insets for spacing from screen edges
616
*/
@@ -12,7 +22,7 @@ export interface ToastInsets {
1222
top?: number;
1323
/**
1424
* Inset from the bottom edge in pixels (added to safe area inset)
15-
* @default Platform-specific: iOS = 0, Android = 12
25+
* @default Platform-specific: iOS = 6, Android = 12
1626
*/
1727
bottom?: number;
1828
/**
@@ -31,6 +41,12 @@ export interface ToastInsets {
3141
* Props for the ToastProvider component
3242
*/
3343
export interface ToastProviderProps {
44+
/**
45+
* Global toast configuration
46+
* These values are used as defaults for all toasts unless overridden locally
47+
* Local configs have precedence over global config
48+
*/
49+
defaultProps?: ToastGlobalConfig;
3450
/**
3551
* Insets for spacing from screen edges (added to safe area insets)
3652
* @default Platform-specific:
@@ -117,7 +133,10 @@ export interface ToastComponentProps {
117133
* Used when component is not provided
118134
*/
119135
export interface ToastShowConfig
120-
extends Pick<ToastRootProps, 'variant' | 'placement' | 'isSwipeable'> {
136+
extends Pick<
137+
ToastRootProps,
138+
'variant' | 'placement' | 'isSwipeable' | 'animation'
139+
> {
121140
/**
122141
* Duration in milliseconds before the toast automatically disappears
123142
* Set to `'persistent'` to prevent auto-hide (toast will remain until manually dismissed)

0 commit comments

Comments
 (0)