Skip to content

Commit 251396b

Browse files
committed
feat(toast): add reducer
1 parent d5c14c7 commit 251396b

File tree

7 files changed

+296
-17
lines changed

7 files changed

+296
-17
lines changed

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

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Toast } from 'heroui-native';
1+
import { Button, Toast, useToaster } from 'heroui-native';
2+
import { useEffect, useState } from 'react';
23
import { View } from 'react-native';
34
import type { UsageVariant } from '../../../components/component-presentation/types';
45
import { UsageVariantFlatList } from '../../../components/component-presentation/usage-variant-flatlist';
@@ -68,12 +69,143 @@ const AllVariantsContent = () => {
6869

6970
// ------------------------------------------------------------------------------
7071

72+
const InteractiveDemoContent = () => {
73+
const toast = useToaster();
74+
75+
const myToast = toast.prepare({
76+
component: (id) => (
77+
<Toast variant="accent" className="flex-row items-center gap-3">
78+
<View className="flex-1">
79+
<Toast.Label>Interactive Toast</Toast.Label>
80+
<Toast.Description>
81+
Use buttons below to control this toast
82+
</Toast.Description>
83+
</View>
84+
<Toast.Action onPress={() => toast.hide(id)}>Close</Toast.Action>
85+
</Toast>
86+
),
87+
});
88+
89+
return (
90+
<View className="flex-1 px-5">
91+
<View className="flex-1 justify-center gap-3">
92+
<Button onPress={() => toast.show(myToast)} variant="primary">
93+
Show Toast
94+
</Button>
95+
96+
<Button onPress={() => toast.hide(myToast)} variant="secondary">
97+
Hide Toast
98+
</Button>
99+
100+
<Button onPress={() => toast.remove(myToast)} variant="destructive">
101+
Remove Toast
102+
</Button>
103+
</View>
104+
</View>
105+
);
106+
};
107+
108+
// ------------------------------------------------------------------------------
109+
110+
const MultipleToastsContent = () => {
111+
const toast = useToaster();
112+
113+
const toast1 = toast.prepare({
114+
component: (id: string) => (
115+
<Toast
116+
variant="default"
117+
placement="top"
118+
className="flex-row items-center gap-3"
119+
>
120+
<View className="flex-1">
121+
<Toast.Label>Toast 1</Toast.Label>
122+
<Toast.Description>First toast at top</Toast.Description>
123+
</View>
124+
<Toast.Action onPress={() => toast.hide(id)}>Close</Toast.Action>
125+
</Toast>
126+
),
127+
});
128+
129+
const toast2 = toast.prepare({
130+
component: (id: string) => (
131+
<Toast
132+
variant="accent"
133+
placement="top"
134+
className="flex-row items-center gap-3"
135+
>
136+
<View className="flex-1">
137+
<Toast.Label>Toast 2</Toast.Label>
138+
<Toast.Description>Second toast at top</Toast.Description>
139+
</View>
140+
<Toast.Action onPress={() => toast.hide(id)}>Close</Toast.Action>
141+
</Toast>
142+
),
143+
});
144+
145+
const toast3 = toast.prepare({
146+
component: (id: string) => (
147+
<Toast
148+
variant="success"
149+
placement="bottom"
150+
className="flex-row items-center gap-3"
151+
>
152+
<View className="flex-1">
153+
<Toast.Label>Toast 3</Toast.Label>
154+
<Toast.Description>Third toast at bottom</Toast.Description>
155+
</View>
156+
<Toast.Action onPress={() => toast.hide(id)}>Close</Toast.Action>
157+
</Toast>
158+
),
159+
});
160+
161+
const allToasts = [toast1, toast2, toast3];
162+
163+
return (
164+
<View className="flex-1 px-5">
165+
<View className="flex-1 justify-center gap-3">
166+
<Button
167+
onPress={() => allToasts.forEach((id) => toast.show(id))}
168+
variant="primary"
169+
>
170+
Show All Toasts
171+
</Button>
172+
173+
<Button
174+
onPress={() => allToasts.forEach((id) => toast.hide(id))}
175+
variant="secondary"
176+
>
177+
Hide All Toasts
178+
</Button>
179+
180+
<Button
181+
onPress={() => allToasts.forEach((id) => toast.remove(id))}
182+
variant="destructive"
183+
>
184+
Remove All Toasts
185+
</Button>
186+
</View>
187+
</View>
188+
);
189+
};
190+
191+
// ------------------------------------------------------------------------------
192+
71193
const TOAST_VARIANTS: UsageVariant[] = [
194+
// {
195+
// value: 'all-variants',
196+
// label: 'All variants',
197+
// content: <AllVariantsContent />,
198+
// },
72199
{
73-
value: 'all-variants',
74-
label: 'All variants',
75-
content: <AllVariantsContent />,
200+
value: 'interactive-demo',
201+
label: 'Interactive Demo',
202+
content: <InteractiveDemoContent />,
76203
},
204+
// {
205+
// value: 'multiple-toasts',
206+
// label: 'Multiple Toasts',
207+
// content: <MultipleToastsContent />,
208+
// },
77209
];
78210

79211
export default function ToastScreen() {

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ export * from './helpers/theme';
2929

3030
// Provider
3131
export * from './providers/hero-ui-native';
32+
export * from './providers/toast';

src/providers/hero-ui-native/provider.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ export const HeroUINativeProvider: React.FC<HeroUINativeProviderProps> = ({
3131

3232
return (
3333
<TextComponentProvider value={{ textProps }}>
34-
{children}
35-
{!isToastDisabled && <Toaster {...toastProps} />}
34+
<Toaster {...toastProps}>{children}</Toaster>
3635
<PortalHost />
3736
</TextComponentProvider>
3837
);

src/providers/toast/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Toaster, useToaster } from './provider';
2+
export type * from './types';

src/providers/toast/provider.tsx

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
import { useMemo } from 'react';
1+
import { Fragment, useCallback, useId, useMemo, useReducer } from 'react';
22
import { View } from 'react-native';
33
import { useSafeAreaInsets } from 'react-native-safe-area-context';
44
import { createContext } from '../../helpers/utils';
5-
import type { ToastContextValue, ToastProviderProps } from './types';
5+
import { toastReducer } from './reducer';
6+
import type { ToasterContextValue, ToastProviderProps } from './types';
67

7-
const [ToasterProvider, useToaster] = createContext<ToastContextValue>({
8-
name: 'ToastContext',
8+
const [ToasterProvider, useToaster] = createContext<ToasterContextValue>({
9+
name: 'ToasterContext',
910
});
1011

11-
export function Toaster({ insets }: ToastProviderProps) {
12+
export function Toaster({ insets, children }: ToastProviderProps) {
1213
const safeAreaInsets = useSafeAreaInsets();
14+
const baseId = useId();
15+
const [toasts, dispatch] = useReducer(toastReducer, []);
16+
console.log('🔴 🔴', toasts); // VS remove
1317

1418
const finalInsets = useMemo(() => {
1519
return {
@@ -20,12 +24,55 @@ export function Toaster({ insets }: ToastProviderProps) {
2024
};
2125
}, [safeAreaInsets, insets]);
2226

23-
const contextValue = useMemo<ToastContextValue>(() => {
24-
return {};
27+
const prepare = useCallback(
28+
(options: { component: (id: string) => React.ReactElement }) => {
29+
const id = `${baseId}-${Date.now()}-${Math.random()}`;
30+
dispatch({
31+
type: 'ADD',
32+
payload: {
33+
id,
34+
component: options.component,
35+
visible: false,
36+
},
37+
});
38+
return id;
39+
},
40+
[baseId]
41+
);
42+
43+
const show = useCallback((id: string) => {
44+
dispatch({
45+
type: 'UPDATE',
46+
payload: { id, visible: true },
47+
});
2548
}, []);
2649

50+
const hide = useCallback((id: string) => {
51+
dispatch({
52+
type: 'UPDATE',
53+
payload: { id, visible: false },
54+
});
55+
}, []);
56+
57+
const remove = useCallback((id: string) => {
58+
dispatch({
59+
type: 'REMOVE',
60+
payload: { id },
61+
});
62+
}, []);
63+
64+
const contextValue = useMemo<ToasterContextValue>(() => {
65+
return {
66+
prepare,
67+
show,
68+
hide,
69+
remove,
70+
};
71+
}, [prepare, show, hide, remove]);
72+
2773
return (
2874
<ToasterProvider value={contextValue}>
75+
{children}
2976
<View
3077
className="absolute inset-0 pointer-events-box-none"
3178
style={{
@@ -36,7 +83,12 @@ export function Toaster({ insets }: ToastProviderProps) {
3683
}}
3784
>
3885
<View className="flex-1">
39-
{/* Visible toasts will be rendered here */}
86+
{toasts.map((toast) => {
87+
if (!toast.visible) return null;
88+
return (
89+
<Fragment key={toast.id}>{toast.component(toast.id)}</Fragment>
90+
);
91+
})}
4092
</View>
4193
</View>
4294
</ToasterProvider>

src/providers/toast/reducer.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { ToastAction, ToastItem } from './types';
2+
3+
export function toastReducer(
4+
state: ToastItem[],
5+
action: ToastAction
6+
): ToastItem[] {
7+
switch (action.type) {
8+
case 'ADD':
9+
return [...state, action.payload];
10+
11+
case 'UPDATE': {
12+
const index = state.findIndex((toast) => toast.id === action.payload.id);
13+
if (index === -1) return state;
14+
15+
const newState = [...state];
16+
const existingToast = newState[index];
17+
if (existingToast) {
18+
newState[index] = {
19+
...existingToast,
20+
visible: action.payload.visible,
21+
};
22+
}
23+
return newState;
24+
}
25+
26+
case 'REMOVE':
27+
return state.filter((toast) => toast.id !== action.payload.id);
28+
29+
default:
30+
return state;
31+
}
32+
}

src/providers/toast/types.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,72 @@ export interface ToastProviderProps {
3333
* @default { top: 12, bottom: 12, left: 12, right: 12 }
3434
*/
3535
insets?: ToastInsets;
36+
/**
37+
* Children to render
38+
*/
39+
children?: React.ReactNode;
40+
}
41+
42+
/**
43+
* Options for preparing a toast
44+
*/
45+
export interface ToastPrepareOptions {
46+
/**
47+
* Function that returns the React element to render
48+
* The function receives the toast ID as a parameter
49+
*/
50+
component: (id: string) => React.ReactElement;
3651
}
3752

3853
/**
39-
* Context value for the Toast provider
54+
* Represents a single toast item in the state
4055
*/
41-
export interface ToastContextValue {
42-
// PLACEHOLDER
56+
export interface ToastItem {
57+
/**
58+
* Unique identifier for the toast
59+
*/
60+
id: string;
61+
/**
62+
* Function that returns the React element to render
63+
*/
64+
component: (id: string) => React.ReactElement;
65+
/**
66+
* Whether the toast is currently visible
67+
*/
68+
visible: boolean;
69+
}
70+
71+
/**
72+
* Actions for the toast reducer
73+
*/
74+
export type ToastAction =
75+
| { type: 'ADD'; payload: ToastItem }
76+
| { type: 'UPDATE'; payload: { id: string; visible: boolean } }
77+
| { type: 'REMOVE'; payload: { id: string } };
78+
79+
/**
80+
* Context value for the Toaster provider
81+
*/
82+
export interface ToasterContextValue {
83+
/**
84+
* Prepare a toast for later display
85+
* @param options - Options for the toast
86+
* @returns The unique ID of the prepared toast
87+
*/
88+
prepare: (options: ToastPrepareOptions) => string;
89+
/**
90+
* Show a prepared toast
91+
* @param id - The ID of the toast to show
92+
*/
93+
show: (id: string) => void;
94+
/**
95+
* Hide a visible toast
96+
* @param id - The ID of the toast to hide
97+
*/
98+
hide: (id: string) => void;
99+
/**
100+
* Remove a toast from memory
101+
* @param id - The ID of the toast to remove
102+
*/
103+
remove: (id: string) => void;
43104
}

0 commit comments

Comments
 (0)