Skip to content

Commit 79ff936

Browse files
committed
feat(toast): add provider contentWrapper prop
1 parent 7649547 commit 79ff936

File tree

6 files changed

+157
-18
lines changed

6 files changed

+157
-18
lines changed

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

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Feather from '@expo/vector-icons/Feather';
22
import Octicons from '@expo/vector-icons/Octicons';
33
import { Button, useToast, type ToastComponentProps } from 'heroui-native';
4-
import { useCallback } from 'react';
5-
import { View } from 'react-native';
4+
import { useCallback, useRef, useState } from 'react';
5+
import { TextInput, View } from 'react-native';
66
import { withUniwind } from 'uniwind';
77
import type { UsageVariant } from '../../../components/component-presentation/types';
88
import { UsageVariantFlatList } from '../../../components/component-presentation/usage-variant-flatlist';
@@ -199,6 +199,62 @@ const DifferentContentSizesContent = () => {
199199

200200
// ------------------------------------------------------------------------------
201201

202+
const KeyboardAvoidingContent = () => {
203+
const [isFocused, setIsFocused] = useState(false);
204+
205+
const { toast } = useToast();
206+
207+
const inputRef = useRef<TextInput>(null);
208+
209+
return (
210+
<View className="flex-1 items-center justify-center px-5 gap-5">
211+
<Button
212+
variant="secondary"
213+
onPress={() => {
214+
toast.show({
215+
variant: 'success',
216+
duration: 'persistent',
217+
placement: 'bottom',
218+
label: 'Payment successful',
219+
description:
220+
'Your subscription has been renewed. You will be charged $9.99/month. Thank you for your continued support.',
221+
actionLabel: 'Close',
222+
onActionPress: ({ hide }) => {
223+
hide();
224+
inputRef.current?.blur();
225+
},
226+
});
227+
}}
228+
>
229+
Show toast
230+
</Button>
231+
<Button
232+
onPress={() => {
233+
if (isFocused) {
234+
inputRef.current?.blur();
235+
} else {
236+
inputRef.current?.focus();
237+
}
238+
}}
239+
variant="secondary"
240+
>
241+
Toggle keyboard
242+
</Button>
243+
<Button onPress={() => toast.hide('all')} variant="destructive-soft">
244+
Hide toast
245+
</Button>
246+
<TextInput
247+
ref={inputRef}
248+
className="opacity-0 pointer-events-none"
249+
onFocus={() => setIsFocused(true)}
250+
onBlur={() => setIsFocused(false)}
251+
/>
252+
</View>
253+
);
254+
};
255+
256+
// ------------------------------------------------------------------------------
257+
202258
const CustomToastsContent = () => {
203259
const { toast } = useToast();
204260
const LOADING_TOAST_ID = 'loading-toast';
@@ -350,6 +406,11 @@ const TOAST_VARIANTS: UsageVariant[] = [
350406
label: 'Different content sizes',
351407
content: <DifferentContentSizesContent />,
352408
},
409+
{
410+
value: 'keyboard-avoiding',
411+
label: 'Keyboard avoiding',
412+
content: <KeyboardAvoidingContent />,
413+
},
353414
{
354415
value: 'custom-toasts',
355416
label: 'Custom toasts',

example/src/app/_layout.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import {
77
} from '@expo-google-fonts/inter';
88
import { Slot } from 'expo-router';
99
import { HeroUINativeProvider } from 'heroui-native';
10+
import { useCallback } from 'react';
1011
import { StyleSheet } from 'react-native';
1112
import { GestureHandlerRootView } from 'react-native-gesture-handler';
12-
import { KeyboardProvider } from 'react-native-keyboard-controller';
13+
import {
14+
KeyboardAvoidingView,
15+
KeyboardProvider,
16+
} from 'react-native-keyboard-controller';
1317
import {
1418
configureReanimatedLogger,
1519
ReanimatedLogLevel,
@@ -23,6 +27,41 @@ configureReanimatedLogger({
2327
strict: false,
2428
});
2529

30+
/**
31+
* Component that wraps app content inside KeyboardProvider
32+
* Contains the contentWrapper and HeroUINativeProvider configuration
33+
*/
34+
function AppContent() {
35+
const contentWrapper = useCallback(
36+
(children: React.ReactNode) => (
37+
<KeyboardAvoidingView
38+
pointerEvents="box-none"
39+
behavior="padding"
40+
keyboardVerticalOffset={12}
41+
className="flex-1"
42+
>
43+
{children}
44+
</KeyboardAvoidingView>
45+
),
46+
[]
47+
);
48+
49+
return (
50+
<AppThemeProvider>
51+
<HeroUINativeProvider
52+
config={{
53+
toast: {
54+
contentWrapper,
55+
},
56+
}}
57+
>
58+
<Slot />
59+
<Toaster />
60+
</HeroUINativeProvider>
61+
</AppThemeProvider>
62+
);
63+
}
64+
2665
export default function Layout() {
2766
const fonts = useFonts({
2867
Inter_400Regular,
@@ -38,12 +77,7 @@ export default function Layout() {
3877
return (
3978
<GestureHandlerRootView style={styles.root}>
4079
<KeyboardProvider>
41-
<AppThemeProvider>
42-
<HeroUINativeProvider>
43-
<Slot />
44-
<Toaster />
45-
</HeroUINativeProvider>
46-
</AppThemeProvider>
80+
<AppContent />
4781
</KeyboardProvider>
4882
</GestureHandlerRootView>
4983
);

src/components/toast/toast.animation.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,17 @@ export function useToastRootAnimation(options: UseToastRootAnimationOptions) {
308308
scale = gestureScale.get();
309309
} else {
310310
// Normal state: use stack-based interpolation
311-
translateY = interpolate(index, inputRange, [
312-
translateYValue[0],
313-
translateYValue[1] * sign,
314-
]);
315-
scale = interpolate(index, inputRange, scaleValue);
311+
translateY = interpolate(
312+
index,
313+
inputRange,
314+
[translateYValue[0], translateYValue[1] * sign],
315+
{
316+
extrapolateLeft: Extrapolation.CLAMP,
317+
}
318+
);
319+
scale = interpolate(index, inputRange, scaleValue, {
320+
extrapolateLeft: Extrapolation.CLAMP,
321+
});
316322
}
317323

318324
if (isAnimationDisabled) {

src/providers/toast/insets-container.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ interface InsetsContainerProps {
1212
* - Android: safe area insets + 12px (all edges)
1313
*/
1414
insets?: ToastInsets;
15+
/**
16+
* Custom wrapper function to wrap the toast content
17+
* Receives children and should return a component that wraps them
18+
* The wrapper should apply flex: 1 (via className or style) to ensure proper layout
19+
* Can be any component wrapper - KeyboardAvoidingView, View, or any custom component
20+
*/
21+
contentWrapper?: (children: ReactNode) => React.ReactElement;
1522
/**
1623
* Children to render inside the container
1724
*/
@@ -28,15 +35,19 @@ interface InsetsContainerProps {
2835
* - iOS: safe area inset + 0px for top/bottom, + 12px for left/right
2936
* - Android: safe area inset + 12px for all edges
3037
*/
31-
export function InsetsContainer({ insets, children }: InsetsContainerProps) {
38+
export function InsetsContainer({
39+
insets,
40+
contentWrapper,
41+
children,
42+
}: InsetsContainerProps) {
3243
const safeAreaInsets = useSafeAreaInsets();
3344

3445
const finalInsets = useMemo(() => {
3546
return {
3647
top: insets?.top ?? safeAreaInsets.top + (Platform.OS === 'ios' ? 0 : 12),
3748
bottom:
3849
insets?.bottom ??
39-
safeAreaInsets.bottom + (Platform.OS === 'ios' ? 0 : 12),
50+
safeAreaInsets.bottom + (Platform.OS === 'ios' ? 6 : 12),
4051
left: insets?.left ?? safeAreaInsets.left + 12,
4152
right: insets?.right ?? safeAreaInsets.right + 12,
4253
};
@@ -52,7 +63,11 @@ export function InsetsContainer({ insets, children }: InsetsContainerProps) {
5263
paddingRight: finalInsets.right,
5364
}}
5465
>
55-
{children}
66+
{contentWrapper ? (
67+
contentWrapper(children)
68+
) : (
69+
<View className="flex-1">{children}</View>
70+
)}
5671
</View>
5772
);
5873
}

src/providers/toast/provider.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function createConfigToastComponent(
6767
export function ToastProvider({
6868
insets,
6969
maxVisibleToasts = 3,
70+
contentWrapper,
7071
children,
7172
}: ToastProviderProps) {
7273
const [toasts, dispatch] = useReducer(toastReducer, []);
@@ -305,7 +306,7 @@ export function ToastProvider({
305306
return (
306307
<ToasterContext.Provider value={contextValue}>
307308
{children}
308-
<InsetsContainer insets={insets}>
309+
<InsetsContainer insets={insets} contentWrapper={contentWrapper}>
309310
<View className="flex-1">
310311
{toasts.map((toastItem, index) => (
311312
<ToastItemRenderer

src/providers/toast/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,28 @@ export interface ToastProviderProps {
4444
* @default 3
4545
*/
4646
maxVisibleToasts?: number;
47+
/**
48+
* Custom wrapper function to wrap the toast content
49+
* Receives children and should return a component that wraps them
50+
* The wrapper should apply flex: 1 (via className or style) to ensure proper layout
51+
* Can be any component wrapper - KeyboardAvoidingView, View, or any custom component
52+
*
53+
* @example
54+
* ```tsx
55+
* <ToastProvider
56+
* contentWrapper={(children) => (
57+
* <KeyboardAvoidingView
58+
* behavior="padding"
59+
* keyboardVerticalOffset={24}
60+
* className="flex-1"
61+
* >
62+
* {children}
63+
* </KeyboardAvoidingView>
64+
* )}
65+
* >
66+
* ```
67+
*/
68+
contentWrapper?: (children: React.ReactNode) => React.ReactElement;
4769
/**
4870
* Children to render
4971
*/

0 commit comments

Comments
 (0)