Skip to content

Commit 00dfb79

Browse files
committed
feat(toast): handle different layers of show function
1 parent ebcd360 commit 00dfb79

File tree

4 files changed

+228
-43
lines changed

4 files changed

+228
-43
lines changed

example/src/app/(home)/components/toast.tsx

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from 'heroui-native';
88
import { useCallback } from 'react';
99
import { View } from 'react-native';
10+
import { toast as sonnerToast } from 'sonner-native';
1011
import type { UsageVariant } from '../../../components/component-presentation/types';
1112
import { UsageVariantFlatList } from '../../../components/component-presentation/usage-variant-flatlist';
1213

@@ -79,13 +80,7 @@ const MyToast1 = (props: ToastComponentProps) => {
7980
const { id, hide } = props;
8081

8182
return (
82-
<Toast
83-
variant="accent"
84-
placement="top"
85-
duration="persistent"
86-
className="flex-row items-center gap-3"
87-
{...props}
88-
>
83+
<Toast variant="accent" className="flex-row items-center gap-3" {...props}>
8984
<View className="flex-1">
9085
<Toast.Label>{id}</Toast.Label>
9186
<Toast.Description>
@@ -101,13 +96,7 @@ const MyToast2 = (props: ToastComponentProps) => {
10196
const { id, hide } = props;
10297

10398
return (
104-
<Toast
105-
variant="success"
106-
placement="top"
107-
duration="persistent"
108-
className="flex-row items-center gap-3"
109-
{...props}
110-
>
99+
<Toast variant="success" className="flex-row items-center gap-3" {...props}>
111100
<View className="flex-1">
112101
<Toast.Label>{id}</Toast.Label>
113102
<Toast.Description>
@@ -125,13 +114,7 @@ const MyToast3 = (props: ToastComponentProps) => {
125114
const { id, hide } = props;
126115

127116
return (
128-
<Toast
129-
variant="warning"
130-
placement="top"
131-
duration="persistent"
132-
className="flex-row items-center gap-3"
133-
{...props}
134-
>
117+
<Toast variant="warning" className="flex-row items-center gap-3" {...props}>
135118
<View className="flex-1">
136119
<Toast.Label>{id}</Toast.Label>
137120
<Toast.Description>
@@ -166,11 +149,11 @@ const InteractiveDemoContent = () => {
166149
<View className="flex-1 px-5">
167150
<View className="flex-1 justify-center gap-3">
168151
<Button
169-
onPress={() => {
152+
onPress={() =>
170153
toast.show({
171154
component: _renderToast1,
172-
});
173-
}}
155+
})
156+
}
174157
variant="primary"
175158
>
176159
Show Toast 1
@@ -206,9 +189,9 @@ const InteractiveDemoContent = () => {
206189
Hide All Toasts
207190
</Button>
208191

209-
{/* <Button onPress={() => sonnerToast('Hello, World!')}>
192+
<Button onPress={() => sonnerToast('Hello, World!')}>
210193
Sonner Toast
211-
</Button> */}
194+
</Button>
212195
</View>
213196
</View>
214197
);

src/components/toast/toast.tsx

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { cn } from '../../helpers/theme';
88
import type { ViewRef } from '../../helpers/types';
99
import { createContext } from '../../helpers/utils';
1010
import * as ToastPrimitive from '../../primitives/toast';
11-
import type { ToastComponentProps } from '../../providers/toast';
11+
import type {
12+
ToastComponentProps,
13+
ToastShowOptions,
14+
} from '../../providers/toast';
1215
import { Button } from '../button';
1316
import { useToastRootAnimation } from './toast.animation';
1417
import { DISPLAY_NAME } from './toast.constants';
@@ -224,6 +227,71 @@ const ToastClose = forwardRef<View, ToastCloseProps>((props, ref) => {
224227

225228
// --------------------------------------------------
226229

230+
/**
231+
* Default styled toast component for simplified toast.show() API
232+
* Used internally when showing toasts with string or config object (without component)
233+
*/
234+
export function DefaultToast(
235+
props: ToastComponentProps & {
236+
variant?: ToastRootProps['variant'];
237+
placement?: ToastRootProps['placement'];
238+
duration?: ToastRootProps['duration'];
239+
isSwipable?: ToastRootProps['isSwipable'];
240+
label?: string;
241+
description?: string;
242+
actionLabel?: string;
243+
onActionPress?: (helpers: {
244+
show: (options: string | ToastShowOptions) => string;
245+
hide: (ids?: string | string[] | 'all') => void;
246+
}) => void;
247+
}
248+
) {
249+
const {
250+
id,
251+
variant = 'default',
252+
placement = 'top',
253+
duration = 4000,
254+
isSwipable,
255+
label,
256+
description,
257+
actionLabel,
258+
onActionPress,
259+
hide,
260+
show,
261+
...toastComponentProps
262+
} = props;
263+
264+
const handleActionPress = () => {
265+
if (onActionPress) {
266+
onActionPress({ show, hide });
267+
}
268+
};
269+
270+
return (
271+
<ToastRoot
272+
id={id}
273+
variant={variant}
274+
placement={placement}
275+
duration={duration}
276+
isSwipable={isSwipable}
277+
className="flex-row items-center gap-3"
278+
hide={hide}
279+
show={show}
280+
{...toastComponentProps}
281+
>
282+
<View className="flex-1">
283+
{label && <ToastLabel>{label}</ToastLabel>}
284+
{description && <ToastDescription>{description}</ToastDescription>}
285+
</View>
286+
{actionLabel && (
287+
<ToastAction onPress={handleActionPress}>{actionLabel}</ToastAction>
288+
)}
289+
</ToastRoot>
290+
);
291+
}
292+
293+
// --------------------------------------------------
294+
227295
ToastRoot.displayName = DISPLAY_NAME.TOAST_ROOT;
228296
ToastLabel.displayName = DISPLAY_NAME.TOAST_LABEL;
229297
ToastDescription.displayName = DISPLAY_NAME.TOAST_DESCRIPTION;

src/providers/toast/provider.tsx

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,56 @@ import {
88
} from 'react';
99
import { View } from 'react-native';
1010
import { useSharedValue } from 'react-native-reanimated';
11+
import { DefaultToast } from '../../components/toast/toast';
1112
import { InsetsContainer } from './insets-container';
1213
import { toastReducer } from './reducer';
1314
import { ToastItemRenderer } from './toast-item-renderer';
1415
import type {
16+
ToastComponentProps,
1517
ToasterContextValue,
1618
ToastProviderProps,
19+
ToastShowConfig,
1720
ToastShowOptions,
21+
ToastShowOptionsWithComponent,
1822
} from './types';
1923

2024
/**
2125
* Context for toast manager
2226
*/
2327
const ToasterContext = createContext<ToasterContextValue | null>(null);
2428

29+
/**
30+
* Creates a component function for simple string toast
31+
*/
32+
function createStringToastComponent(
33+
label: string
34+
): (props: ToastComponentProps) => React.ReactElement {
35+
return (props: ToastComponentProps) => (
36+
<DefaultToast {...props} label={label} variant="default" />
37+
);
38+
}
39+
40+
/**
41+
* Creates a component function for config-based toast
42+
*/
43+
function createConfigToastComponent(
44+
config: ToastShowConfig
45+
): (props: ToastComponentProps) => React.ReactElement {
46+
return (props: ToastComponentProps) => (
47+
<DefaultToast
48+
{...props}
49+
variant={config.variant}
50+
placement={config.placement}
51+
duration={config.duration}
52+
isSwipable={config.isSwipable}
53+
label={config.label}
54+
description={config.description}
55+
actionLabel={config.actionLabel}
56+
onActionPress={config.onActionPress}
57+
/>
58+
);
59+
}
60+
2561
/**
2662
* Toast provider component
2763
* Wraps your app to enable toast functionality
@@ -43,25 +79,54 @@ export function ToastProvider({
4379

4480
/**
4581
* Show a toast
82+
* Supports three usage patterns:
83+
* 1. Simple string: toast.show('This is toast')
84+
* 2. Config object: toast.show({ label, variant, ... })
85+
* 3. Custom component: toast.show({ component: (props) => <Toast>...</Toast> })
4686
*/
4787
const show = useCallback(
48-
(options: ToastShowOptions): string => {
49-
const id = options.id ?? `toast-${Date.now()}-${idCounter.current++}`;
88+
(options: string | ToastShowOptions): string => {
89+
let normalizedOptions: ToastShowOptionsWithComponent;
90+
91+
// Case 1: Simple string
92+
if (typeof options === 'string') {
93+
normalizedOptions = {
94+
id: undefined,
95+
component: createStringToastComponent(options),
96+
};
97+
}
98+
// Case 2: Config object without component
99+
else if (!('component' in options) || options.component === undefined) {
100+
const config = options as ToastShowConfig;
101+
normalizedOptions = {
102+
id: config.id,
103+
component: createConfigToastComponent(config),
104+
onShow: config.onShow,
105+
onHide: config.onHide,
106+
};
107+
}
108+
// Case 3: Config object with component (existing behavior)
109+
else {
110+
normalizedOptions = options as ToastShowOptionsWithComponent;
111+
}
112+
113+
const id =
114+
normalizedOptions.id ?? `toast-${Date.now()}-${idCounter.current++}`;
50115

51116
dispatch({
52117
type: 'SHOW',
53118
payload: {
54119
id,
55-
component: options.component,
56-
onShow: options.onShow,
57-
onHide: options.onHide,
120+
component: normalizedOptions.component,
121+
onShow: normalizedOptions.onShow,
122+
onHide: normalizedOptions.onHide,
58123
},
59124
});
60125

61126
total.set((value) => value + 1);
62127

63-
if (options.onShow) {
64-
options.onShow();
128+
if (normalizedOptions.onShow) {
129+
normalizedOptions.onShow();
65130
}
66131

67132
return id;

0 commit comments

Comments
 (0)