Skip to content

Commit b129fbe

Browse files
committed
feat(toast): add toast components
1 parent 0c90384 commit b129fbe

File tree

9 files changed

+441
-0
lines changed

9 files changed

+441
-0
lines changed

example/src/app/(home)/_layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export default function Layout() {
134134
name="components/text-field"
135135
options={{ title: 'TextField' }}
136136
/>
137+
<Stack.Screen name="components/toast" options={{ title: 'Toast' }} />
137138
<Stack.Screen name="themes/index" options={{ headerTitle: 'Themes' }} />
138139
<Stack.Screen
139140
name="showcases"

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ const components: Component[] = [
9898
title: 'Text Field',
9999
path: 'text-field',
100100
},
101+
{
102+
title: 'Toast',
103+
path: 'toast',
104+
},
101105
];
102106

103107
export default function App() {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Toast } from 'heroui-native';
2+
import { View } from 'react-native';
3+
import type { UsageVariant } from '../../../components/component-presentation/types';
4+
import { UsageVariantFlatList } from '../../../components/component-presentation/usage-variant-flatlist';
5+
6+
const AllVariantsContent = () => {
7+
return (
8+
<View className="flex-1 px-5">
9+
<View className="flex-1 justify-center gap-4">
10+
{/* Default variant */}
11+
<Toast variant="default" className="flex-row items-center gap-3">
12+
<View className="flex-1">
13+
<Toast.Label>Default notification</Toast.Label>
14+
<Toast.Description>
15+
This is a default toast message
16+
</Toast.Description>
17+
</View>
18+
<Toast.Action>Action</Toast.Action>
19+
</Toast>
20+
21+
{/* Accent variant */}
22+
<Toast variant="accent" className="flex-row items-center gap-3">
23+
<View className="flex-1">
24+
<Toast.Label>Accent notification</Toast.Label>
25+
<Toast.Description>
26+
This is an accent toast message
27+
</Toast.Description>
28+
</View>
29+
<Toast.Action>Action</Toast.Action>
30+
</Toast>
31+
32+
{/* Success variant */}
33+
<Toast variant="success" className="flex-row items-center gap-3">
34+
<View className="flex-1">
35+
<Toast.Label>Success notification</Toast.Label>
36+
<Toast.Description>
37+
This is a success toast message
38+
</Toast.Description>
39+
</View>
40+
<Toast.Action>Action</Toast.Action>
41+
</Toast>
42+
43+
{/* Warning variant */}
44+
<Toast variant="warning" className="flex-row items-center gap-3">
45+
<View className="flex-1">
46+
<Toast.Label>Warning notification</Toast.Label>
47+
<Toast.Description>
48+
This is a warning toast message
49+
</Toast.Description>
50+
</View>
51+
<Toast.Action>Action</Toast.Action>
52+
</Toast>
53+
54+
{/* Danger variant */}
55+
<Toast variant="danger" className="flex-row items-center gap-3">
56+
<View className="flex-1">
57+
<Toast.Label>Danger notification</Toast.Label>
58+
<Toast.Description>
59+
This is a danger toast message
60+
</Toast.Description>
61+
</View>
62+
<Toast.Action>Action</Toast.Action>
63+
</Toast>
64+
</View>
65+
</View>
66+
);
67+
};
68+
69+
// ------------------------------------------------------------------------------
70+
71+
const TOAST_VARIANTS: UsageVariant[] = [
72+
{
73+
value: 'all-variants',
74+
label: 'All variants',
75+
content: <AllVariantsContent />,
76+
},
77+
];
78+
79+
export default function ToastScreen() {
80+
return <UsageVariantFlatList data={TOAST_VARIANTS} />;
81+
}

src/components/toast/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as Toast, useToast } from './toast';
2+
export type * from './toast.types';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Display names for Toast components
3+
*/
4+
export const DISPLAY_NAME = {
5+
TOAST_ROOT: 'HeroUINative.Toast.Root',
6+
TOAST_LABEL: 'HeroUINative.Toast.Label',
7+
TOAST_DESCRIPTION: 'HeroUINative.Toast.Description',
8+
TOAST_ACTION: 'HeroUINative.Toast.Action',
9+
TOAST_CLOSE: 'HeroUINative.Toast.Close',
10+
} as const;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { StyleSheet } from 'react-native';
2+
import { tv } from 'tailwind-variants';
3+
import { combineStyles } from '../../helpers/theme/utils/combine-styles';
4+
5+
const root = tv({
6+
base: 'rounded-3xl p-4 bg-surface border border-border shadow-2xl shadow-black/5',
7+
});
8+
9+
const label = tv({
10+
base: 'text-base font-medium',
11+
variants: {
12+
variant: {
13+
default: 'text-foreground',
14+
accent: 'text-foreground',
15+
success: 'text-success',
16+
warning: 'text-warning',
17+
danger: 'text-danger',
18+
},
19+
},
20+
defaultVariants: {
21+
variant: 'default',
22+
},
23+
});
24+
25+
const description = tv({
26+
base: 'text-sm text-muted',
27+
});
28+
29+
const action = tv({
30+
base: '',
31+
variants: {
32+
variant: {
33+
default: '',
34+
accent: '',
35+
success: 'bg-success',
36+
warning: 'bg-warning',
37+
danger: '',
38+
},
39+
},
40+
defaultVariants: {
41+
variant: 'default',
42+
},
43+
});
44+
45+
const toastStyles = combineStyles({
46+
root,
47+
label,
48+
description,
49+
action,
50+
});
51+
52+
export const styleSheet = StyleSheet.create({
53+
root: {
54+
borderCurve: 'continuous',
55+
},
56+
});
57+
58+
export default toastStyles;

src/components/toast/toast.tsx

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { forwardRef, useMemo } from 'react';
2+
import { View } from 'react-native';
3+
import { CloseIcon } from '../../helpers/components/close-icon';
4+
import { Text } from '../../helpers/components/text';
5+
import type { ViewRef } from '../../helpers/types';
6+
import { createContext } from '../../helpers/utils';
7+
import * as ToastPrimitive from '../../primitives/toast';
8+
import { Button } from '../button';
9+
import { DISPLAY_NAME } from './toast.constants';
10+
import toastStyles, { styleSheet } from './toast.styles';
11+
import type {
12+
ToastActionProps,
13+
ToastCloseProps,
14+
ToastContextValue,
15+
ToastDescriptionProps,
16+
ToastLabelProps,
17+
ToastRootProps,
18+
} from './toast.types';
19+
20+
const [ToastProvider, useToast] = createContext<ToastContextValue>({
21+
name: 'ToastContext',
22+
});
23+
24+
// --------------------------------------------------
25+
26+
const ToastRoot = forwardRef<ViewRef, ToastRootProps>((props, ref) => {
27+
const {
28+
children,
29+
variant = 'default',
30+
className,
31+
style,
32+
...restProps
33+
} = props;
34+
35+
const tvStyles = toastStyles.root({
36+
className,
37+
});
38+
39+
const contextValue = useMemo(
40+
() => ({
41+
variant,
42+
}),
43+
[variant]
44+
);
45+
46+
return (
47+
<ToastProvider value={contextValue}>
48+
<ToastPrimitive.Root
49+
ref={ref}
50+
className={tvStyles}
51+
style={[styleSheet.root, style]}
52+
{...restProps}
53+
>
54+
{children}
55+
</ToastPrimitive.Root>
56+
</ToastProvider>
57+
);
58+
});
59+
60+
// --------------------------------------------------
61+
62+
const ToastLabel = forwardRef<View, ToastLabelProps>((props, ref) => {
63+
const { children, className, ...restProps } = props;
64+
65+
const { variant } = useToast();
66+
67+
const tvStyles = toastStyles.label({
68+
variant,
69+
className,
70+
});
71+
72+
return (
73+
<Text ref={ref} className={tvStyles} {...restProps}>
74+
{children}
75+
</Text>
76+
);
77+
});
78+
79+
// --------------------------------------------------
80+
81+
const ToastDescription = forwardRef<View, ToastDescriptionProps>(
82+
(props, ref) => {
83+
const { children, className, ...restProps } = props;
84+
85+
const tvStyles = toastStyles.description({
86+
className,
87+
});
88+
89+
return (
90+
<Text ref={ref} className={tvStyles} {...restProps}>
91+
{children}
92+
</Text>
93+
);
94+
}
95+
);
96+
97+
// --------------------------------------------------
98+
99+
const ToastAction = forwardRef<View, ToastActionProps>((props, ref) => {
100+
const { children, variant, size = 'sm', className, ...restProps } = props;
101+
102+
const { variant: toastVariant } = useToast();
103+
104+
const buttonVariant = useMemo(() => {
105+
if (variant) return variant;
106+
107+
switch (toastVariant) {
108+
case 'accent':
109+
return 'primary';
110+
case 'danger':
111+
return 'destructive';
112+
default:
113+
return 'tertiary';
114+
}
115+
}, [toastVariant, variant]);
116+
117+
const tvStyles = toastStyles.action({
118+
variant: toastVariant,
119+
className,
120+
});
121+
122+
return (
123+
<Button
124+
ref={ref}
125+
variant={buttonVariant}
126+
size={size}
127+
className={tvStyles}
128+
{...restProps}
129+
>
130+
{children}
131+
</Button>
132+
);
133+
});
134+
135+
// --------------------------------------------------
136+
137+
const ToastClose = forwardRef<View, ToastCloseProps>((props, ref) => {
138+
const { children, iconProps, size = 'sm', className, ...restProps } = props;
139+
140+
return (
141+
<Button
142+
ref={ref}
143+
variant="ghost"
144+
size={size}
145+
isIconOnly
146+
aria-label="Close"
147+
className={className}
148+
{...restProps}
149+
>
150+
{children ?? (
151+
<CloseIcon size={iconProps?.size ?? 16} color={iconProps?.color} />
152+
)}
153+
</Button>
154+
);
155+
});
156+
157+
// --------------------------------------------------
158+
159+
ToastRoot.displayName = DISPLAY_NAME.TOAST_ROOT;
160+
ToastLabel.displayName = DISPLAY_NAME.TOAST_LABEL;
161+
ToastDescription.displayName = DISPLAY_NAME.TOAST_DESCRIPTION;
162+
ToastAction.displayName = DISPLAY_NAME.TOAST_ACTION;
163+
ToastClose.displayName = DISPLAY_NAME.TOAST_CLOSE;
164+
165+
/**
166+
* Compound Toast component with sub-components
167+
*
168+
* @component Toast - Main toast container that displays notification messages with various variants.
169+
*
170+
* @component Toast.Label - Title/heading text of the toast notification.
171+
*
172+
* @component Toast.Description - Descriptive text content of the toast.
173+
*
174+
* @component Toast.Action - Action button within the toast. Variant is automatically determined
175+
* based on toast variant but can be overridden.
176+
*
177+
* @component Toast.Close - Close button for dismissing the toast. Renders as an icon-only button.
178+
*
179+
* Props flow from Toast to sub-components via context (variant).
180+
*
181+
* @see Full documentation: https://heroui.com/components/toast
182+
*/
183+
const CompoundToast = Object.assign(ToastRoot, {
184+
/** Toast label/title - renders text content */
185+
Label: ToastLabel,
186+
/** Toast description - renders descriptive text */
187+
Description: ToastDescription,
188+
/** Toast action button - renders action with appropriate variant */
189+
Action: ToastAction,
190+
/** Toast close button - renders icon-only close button */
191+
Close: ToastClose,
192+
});
193+
194+
export { useToast };
195+
export default CompoundToast;

0 commit comments

Comments
 (0)