diff --git a/__tests__/src/components/Toast.test.tsx b/__tests__/src/components/Toast.test.tsx
index 4a67066..3d3c15b 100644
--- a/__tests__/src/components/Toast.test.tsx
+++ b/__tests__/src/components/Toast.test.tsx
@@ -7,6 +7,7 @@ import {
Toast as ToastType,
ToastType as ToastingType,
resolveValue,
+ DismissReason,
} from '../../../src/core/types';
import { toast as toasting } from '../../../src/headless';
import { View, Text } from 'react-native';
@@ -92,10 +93,14 @@ describe('', () => {
toast={{ ...defaultProps.toast, visible: false }}
/>
);
- expect(onToastHide).toHaveBeenCalledWith({
- ...defaultProps.toast,
- visible: false,
- });
+ expect(onToastHide).toHaveBeenCalledTimes(1);
+ expect(onToastHide).toHaveBeenCalledWith(
+ {
+ ...defaultProps.toast,
+ visible: false,
+ },
+ DismissReason.PROGRAMMATIC
+ );
});
it('handles press events', () => {
diff --git a/__tests__/src/core/store.test.ts b/__tests__/src/core/store.test.ts
index 5c8ee8c..53c9463 100644
--- a/__tests__/src/core/store.test.ts
+++ b/__tests__/src/core/store.test.ts
@@ -4,8 +4,12 @@ import {
reducer,
useStore,
} from '../../../src/core/store';
-import { renderHook, act } from '@testing-library/react-native';
-import { ToastPosition, ToastType } from '../../../src/core/types';
+import { act, renderHook } from '@testing-library/react-native';
+import {
+ DismissReason,
+ ToastPosition,
+ ToastType,
+} from '../../../src/core/types';
describe('Toast Store', () => {
const initialState = { toasts: [], pausedAt: undefined };
@@ -76,6 +80,7 @@ describe('Toast Store', () => {
const updatedState = reducer(stateWithToast, {
type: ActionType.DISMISS_TOAST,
toastId: '1',
+ reason: DismissReason.TIMEOUT,
});
expect(updatedState.toasts[0]?.visible).toBe(false);
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 5ebee94..751c955 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -131,6 +131,38 @@ export default function App() {
+ {
+ toast(Math.floor(Math.random() * 1000).toString(), {
+ position,
+ duration,
+ height,
+ width,
+ providerKey: 'PERSISTS',
+ onPress: (toast) => {
+ console.log('Toast pressed: ', toast);
+ },
+ onShow: (toast) => {
+ console.log('Toast shown: ', toast);
+ },
+ onHide: (toast, reason) => {
+ console.log('Toast hidden: ', toast, reason);
+ },
+ });
+ }}
+ >
+
+ Toast With Handlers
+
+
+
{
@@ -294,8 +326,8 @@ export default function App() {
onToastShow={(t) => {
console.log('SHOW: ', t);
}}
- onToastHide={(t) => {
- console.log('HIDE: ', t);
+ onToastHide={(t, reason) => {
+ console.log('HIDE: ', t, reason);
}}
onToastPress={(t) => {
console.log('PRESS: ', t);
diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx
index 7561f64..3c58b04 100644
--- a/src/components/Toast.tsx
+++ b/src/components/Toast.tsx
@@ -27,7 +27,7 @@ import {
GestureDetector,
} from 'react-native-gesture-handler';
-import type { ExtraInsets, Toast as ToastType } from '../core/types';
+import { DismissReason, ExtraInsets, Toast as ToastType } from '../core/types';
import { resolveValue, Toast as T, ToastPosition } from '../core/types';
import { colors, ConstructShadow, useVisibilityChange } from '../utils';
import { toast as toasting } from '../headless';
@@ -45,7 +45,7 @@ type Props = {
customRenderer?: (toast: ToastType) => React.ReactNode;
overrideDarkMode?: boolean;
onToastShow?: (toast: T) => void;
- onToastHide?: (toast: T) => void;
+ onToastHide?: (toast: T, reason?: DismissReason) => void;
onToastPress?: (toast: T) => void;
extraInsets?: ExtraInsets;
keyboardVisible?: boolean;
@@ -136,10 +136,18 @@ export const Toast: FC = ({
const position = useSharedValue(startY);
const offsetY = useSharedValue(startY);
- const onPress = () => onToastPress?.(toast);
+ const onPress = useCallback(() => {
+ if (toast.onPress) {
+ toast.onPress(toast);
+ }
+
+ if (onToastPress) {
+ onToastPress(toast);
+ }
+ }, [toast, onToastPress]);
- const dismiss = useCallback((id: string) => {
- toasting.dismiss(id);
+ const dismiss = useCallback((id: string, reason: DismissReason) => {
+ toasting.dismiss(id, reason);
}, []);
const getSwipeDirection = useCallback(() => {
@@ -240,7 +248,7 @@ export const Toast: FC = ({
offsetY.value = withTiming(startY, {
duration: toast?.animationConfig?.flingPositionReturnDuration ?? 40,
});
- runOnJS(dismiss)(toast.id);
+ runOnJS(dismiss)(toast.id, DismissReason.SWIPE);
});
return toast.isSwipeable
@@ -260,12 +268,19 @@ export const Toast: FC = ({
useVisibilityChange(
() => {
+ if (toast.onShow) {
+ toast.onShow(toast);
+ }
onToastShow?.(toast);
},
- () => {
- onToastHide?.(toast);
+ (reason) => {
+ if (toast.onHide) {
+ toast.onHide(toast, reason || DismissReason.PROGRAMMATIC);
+ }
+ onToastHide?.(toast, reason || DismissReason.PROGRAMMATIC);
},
- toast.visible
+ toast.visible,
+ toast.dismissReason
);
useEffect(() => {
diff --git a/src/components/Toasts.tsx b/src/components/Toasts.tsx
index 8e64440..d9f4fd2 100644
--- a/src/components/Toasts.tsx
+++ b/src/components/Toasts.tsx
@@ -14,6 +14,7 @@ import {
useSafeAreaFrame,
} from 'react-native-safe-area-context';
import {
+ DismissReason,
ExtraInsets,
ToastAnimationConfig,
ToastAnimationType,
@@ -25,7 +26,7 @@ type Props = {
overrideDarkMode?: boolean;
extraInsets?: ExtraInsets;
onToastShow?: (toast: T) => void;
- onToastHide?: (toast: T) => void;
+ onToastHide?: (toast: T, reason?: DismissReason) => void;
onToastPress?: (toast: T) => void;
providerKey?: string;
preventScreenReaderFromHiding?: boolean;
diff --git a/src/core/store.ts b/src/core/store.ts
index c93855d..d31022a 100644
--- a/src/core/store.ts
+++ b/src/core/store.ts
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
-import type { DefaultToastOptions, Toast, ToastType } from './types';
+import { DefaultToastOptions, DismissReason, Toast, ToastType } from './types';
const TOAST_LIMIT = 20;
@@ -29,10 +29,12 @@ export type Action =
| {
type: ActionType.DISMISS_TOAST;
toastId?: string;
+ reason: DismissReason;
}
| {
type: ActionType.REMOVE_TOAST;
toastId?: string;
+ reason?: DismissReason;
}
| {
type: ActionType.START_PAUSE;
@@ -50,7 +52,7 @@ interface State {
const toastTimeouts = new Map>();
-const addToRemoveQueue = (toastId: string) => {
+const addToRemoveQueue = (toastId: string, reason: DismissReason) => {
if (toastTimeouts.has(toastId)) {
return;
}
@@ -60,6 +62,7 @@ const addToRemoveQueue = (toastId: string) => {
dispatch({
type: ActionType.REMOVE_TOAST,
toastId: toastId,
+ reason,
});
}, 1000);
@@ -101,14 +104,14 @@ export const reducer = (state: State, action: Action): State => {
: reducer(state, { type: ActionType.ADD_TOAST, toast });
case ActionType.DISMISS_TOAST:
- const { toastId } = action;
+ const { toastId, reason } = action;
// ! Side effects ! - This could be execrated into a dismissToast() action, but I'll keep it here for simplicity
if (toastId) {
- addToRemoveQueue(toastId);
+ addToRemoveQueue(toastId, reason);
} else {
state.toasts.forEach((toast) => {
- addToRemoveQueue(toast.id);
+ addToRemoveQueue(toast.id, reason);
});
}
@@ -119,6 +122,7 @@ export const reducer = (state: State, action: Action): State => {
? {
...t,
visible: false,
+ dismissReason: reason,
}
: t
),
diff --git a/src/core/toast.ts b/src/core/toast.ts
index ebe4ea7..2ec70f2 100644
--- a/src/core/toast.ts
+++ b/src/core/toast.ts
@@ -1,5 +1,6 @@
import {
DefaultToastOptions,
+ DismissReason,
Element,
resolveValue,
Toast,
@@ -55,10 +56,14 @@ toast.error = createHandler('error');
toast.success = createHandler('success');
toast.loading = createHandler('loading');
-toast.dismiss = (toastId?: string) => {
+toast.dismiss = (
+ toastId?: string,
+ reason: DismissReason = DismissReason.PROGRAMMATIC
+) => {
dispatch({
type: ActionType.DISMISS_TOAST,
toastId,
+ reason,
});
};
diff --git a/src/core/types.ts b/src/core/types.ts
index 5a62263..2f05832 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -13,6 +13,13 @@ export enum ToastPosition {
export type Element = JSX.Element | string | null;
+export enum DismissReason {
+ TIMEOUT = 'timeout',
+ SWIPE = 'swipe',
+ PROGRAMMATIC = 'programmatic',
+ TAP = 'tap',
+}
+
export interface IconTheme {
primary: string;
secondary: string;
@@ -55,6 +62,10 @@ export interface Toast {
height?: number;
width?: number;
maxWidth?: number;
+ onPress?: (toast: Toast) => void;
+ onHide?: (toast: Toast, reason: DismissReason) => void;
+ onShow?: (toast: Toast) => void;
+ dismissReason?: DismissReason;
styles?: {
pressable?: ViewStyle;
view?: ViewStyle;
@@ -86,6 +97,9 @@ export type ToastOptions = Partial<
| 'animationConfig'
| 'animationType'
| 'maxWidth'
+ | 'onPress'
+ | 'onHide'
+ | 'onShow'
>
>;
diff --git a/src/core/use-toaster.ts b/src/core/use-toaster.ts
index 2795eff..d210280 100644
--- a/src/core/use-toaster.ts
+++ b/src/core/use-toaster.ts
@@ -1,7 +1,12 @@
import { useEffect, useMemo } from 'react';
import { ActionType, dispatch, useStore } from './store';
import { toast } from './toast';
-import type { DefaultToastOptions, Toast, ToastPosition } from './types';
+import {
+ DefaultToastOptions,
+ DismissReason,
+ Toast,
+ ToastPosition,
+} from './types';
export const useToaster = (toastOptions?: DefaultToastOptions) => {
const { toasts, pausedAt } = useStore(toastOptions);
@@ -22,11 +27,14 @@ export const useToaster = (toastOptions?: DefaultToastOptions) => {
if (durationLeft < 0) {
if (t.visible) {
- toast.dismiss(t.id);
+ toast.dismiss(t.id, DismissReason.TIMEOUT);
}
return;
}
- return setTimeout(() => toast.dismiss(t.id), durationLeft);
+ return setTimeout(
+ () => toast.dismiss(t.id, DismissReason.TIMEOUT),
+ durationLeft
+ );
});
return () => {
diff --git a/src/utils/useVisibilityChange.ts b/src/utils/useVisibilityChange.ts
index cb6c985..09f297c 100644
--- a/src/utils/useVisibilityChange.ts
+++ b/src/utils/useVisibilityChange.ts
@@ -1,9 +1,11 @@
import { useEffect, useState } from 'react';
+import { DismissReason } from '../core/types';
export const useVisibilityChange = (
onShow: () => void,
- onHide: () => void,
- visible: boolean
+ onHide: (reason?: DismissReason) => void,
+ visible: boolean,
+ dismissReason?: DismissReason
) => {
const [mounted, setMounted] = useState(false);
@@ -15,9 +17,9 @@ export const useVisibilityChange = (
if (mounted && !visible) {
setMounted(false);
- onHide();
+ onHide(dismissReason);
}
- }, [visible, mounted, onShow, onHide]);
+ }, [visible, mounted, dismissReason, onShow, onHide]);
return undefined;
};
diff --git a/website/docs/api/toast.md b/website/docs/api/toast.md
index b2b6398..f0c5222 100644
--- a/website/docs/api/toast.md
+++ b/website/docs/api/toast.md
@@ -103,6 +103,127 @@ toast(Math.floor(Math.random() * 1000).toString(), {
```
+### onPress
+You can add a function to be called when the toast is pressed. This is useful for performing an action.
+
+```js
+toast('Hello World', {
+ onPress: () => {
+ console.log('Toast pressed!');
+ },
+});
+```
+
+### Toast Handlers and Dismiss Reasons
+
+You can add individual handlers to each toast for greater control over behavior and responses.
+
+```js
+import { DismissReason } from "@backpackapp-io/react-native-toast";
+
+const id = toast('Hello World', {
+ // Handler for when toast is pressed
+ onPress: (toast) => {
+ console.log('Toast pressed!', toast.id);
+ // Access component-specific methods or state here
+ navigation.navigate('Details', { id: toast.id });
+ },
+
+ // Handler for when toast appears
+ onShow: (toast) => {
+ console.log('Toast shown!', toast.id);
+ analytics.logEvent('toast_shown', { id: toast.id });
+ },
+
+ // Handler for when toast is dismissed with reason
+ onHide: (toast, reason) => {
+ console.log(`Toast ${toast.id} dismissed because: ${reason}`);
+
+ // Handle different dismiss reasons
+ switch(reason) {
+ case DismissReason.TIMEOUT:
+ console.log('Toast timed out');
+ break;
+ case DismissReason.SWIPE:
+ console.log('User swiped toast away');
+ break;
+ case DismissReason.PROGRAMMATIC:
+ console.log('Toast was programmatically dismissed');
+ break;
+ case DismissReason.TAP:
+ console.log('User tapped to dismiss toast');
+ break;
+ }
+ }
+});
+```
+
+#### Dismiss Reasons
+
+When a toast is dismissed, you can now determine why it was dismissed:
+
+| Reason | Description |
+|--------|-------------|
+| `DismissReason.TIMEOUT` | The toast was dismissed because its duration elapsed |
+| `DismissReason.SWIPE` | The toast was dismissed because the user swiped it away |
+| `DismissReason.PROGRAMMATIC` | The toast was dismissed programmatically via `toast.dismiss()` |
+| `DismissReason.TAP` | The toast was dismissed because the user tapped it |
+
+#### Individual vs Global Handlers
+
+You can use both individual handlers (attached to each toast) and global handlers (provided to the `` component):
+
+```js
+// Individual handler (specific to this toast)
+toast('Hello', {
+ onPress: (toast) => {
+ // This runs only for this specific toast
+ console.log('This specific toast was pressed');
+ }
+});
+
+// In your app component
+ {
+ // This runs for ALL toasts
+ console.log('A toast was pressed:', toast.id);
+ }}
+/>
+```
+
+When both are provided, both handlers will be executed when the event occurs.
+
+#### Usage with Component-Specific Logic
+
+Individual handlers are especially useful when you need to access component-specific state, hooks, or navigation:
+
+```js
+const OrderConfirmationScreen = () => {
+ const navigation = useNavigation();
+ const { orderId } = useRoute().params;
+ const { mutate: cancelOrder } = useCancelOrderMutation();
+
+ const showUndoToast = () => {
+ toast('Order placed!', {
+ onPress: () => {
+ // Access component-specific state and functions
+ cancelOrder(orderId);
+ navigation.navigate('OrderCanceled');
+ },
+ duration: 5000,
+ });
+ };
+
+ return (
+
+
+
+ );
+};
+```
+
+
+
### Loading
```js
@@ -313,21 +434,24 @@ where `AutoWidthStyles` holds the actual styles for auto width.
## All toast() Options
-| Name | Type | Default | Description |
-|-----------------|----------|-----------|-------------------------------------------------------------------------------------------------------------|
-| `duration` | number | 3000 | Duration in milliseconds. Set to `Infinity` to keep the toast open until dismissed manually. |
-| `position` | enum | 1 | Position of the toast. Can be `ToastPosition.{TOP, BOTTOM, TOP_LEFT, BOTTOM_LEFT, TOP_RIGHT, BOTTOM_RIGHT}` |
-| `id` | string | | Unique id for the toast. |
-| `icon` | Element | | Icon to display on the left of the toast. |
-| `animationType` | string | 'timing' | Animation type. Can be 'timing' or 'spring'. |
-| `animationConfig`| object | | Animation configuration. |
-| `customToast` | function | | Custom toast component. |
-| `width` | number | | Width of the toast. |
-| `height` | number | | Height of the toast. |
-| `disableShadow` | boolean | false | Disable shadow on the toast. |
-| `isSwipeable` | boolean | true | Disable/Enable swipe to dismiss the toast. |
-| `providerKey` | string | 'DEFAULT' | Provider key for the toast. |
-| `accessibilityMessage`| string | | Accessibility message for screen readers. |
-| `styles` | object | | Styles for the toast. |
+| Name | Type | Default | Description |
+|------------------------|--------------|-----------|-------------------------------------------------------------------------------------------------------------|
+| `duration` | number | 3000 | Duration in milliseconds. Set to `Infinity` to keep the toast open until dismissed manually. |
+| `position` | enum | 1 | Position of the toast. Can be `ToastPosition.{TOP, BOTTOM, TOP_LEFT, BOTTOM_LEFT, TOP_RIGHT, BOTTOM_RIGHT}` |
+| `id` | string | | Unique id for the toast. |
+| `icon` | Element | | Icon to display on the left of the toast. |
+| `animationType` | string | 'timing' | Animation type. Can be 'timing' or 'spring'. |
+| `animationConfig` | object | | Animation configuration. |
+| `customToast` | function | | Custom toast component. |
+| `width` | number | | Width of the toast. |
+| `height` | number | | Height of the toast. |
+| `disableShadow` | boolean | false | Disable shadow on the toast. |
+| `isSwipeable` | boolean | true | Disable/Enable swipe to dismiss the toast. |
+| `providerKey` | string | 'DEFAULT' | Provider key for the toast. |
+| `accessibilityMessage` | string | | Accessibility message for screen readers. |
+| `styles` | object | | Styles for the toast. |
+| `onShow` | function | | Function called when this specific toast is shown |
+| `onHide` | function | | Function called when this specific toast is hidden, with dismiss reason |
+| `onPress` | function | | Function called when this specific toast is pressed |
diff --git a/website/docs/features/toast-handlers.md b/website/docs/features/toast-handlers.md
new file mode 100644
index 0000000..aee42dc
--- /dev/null
+++ b/website/docs/features/toast-handlers.md
@@ -0,0 +1,190 @@
+---
+id: toast-handlers
+title: Toast Handlers Examples
+hide_title: false
+sidebar_label: Toast Handler Examples
+slug: /api/toast-toast-handlers
+---
+# Usage Examples
+
+## Basic Usage with Individual Handlers
+
+```jsx
+import { toast, DismissReason } from '@backpackapp-io/react-native-toast';
+
+// Simple toast with individual handlers
+toast('Operation completed', {
+ onShow: (toast) => {
+ console.log(`Toast ${toast.id} appeared`);
+ analytics.track('toast_shown', { id: toast.id });
+ },
+ onHide: (toast, reason) => {
+ console.log(`Toast ${toast.id} disappeared because: ${reason}`);
+ analytics.track('toast_hidden', { id: toast.id, reason });
+ },
+ onPress: (toast) => {
+ console.log(`Toast ${toast.id} was pressed`);
+ navigation.navigate('Details');
+ }
+});
+```
+
+## Handling Different Dismiss Reasons
+
+```jsx
+toast.success('Item added to cart', {
+ onHide: (toast, reason) => {
+ switch (reason) {
+ case DismissReason.TIMEOUT:
+ // User didn't interact with the toast
+ console.log('User didn\'t interact with cart notification');
+ break;
+
+ case DismissReason.SWIPE:
+ // User actively dismissed the toast
+ console.log('User dismissed cart notification');
+ break;
+
+ case DismissReason.TAP:
+ // User tapped the toast
+ console.log('User tapped cart notification');
+ break;
+
+ case DismissReason.PROGRAMMATIC:
+ // Toast was dismissed by code
+ console.log('Cart notification was programmatically dismissed');
+ break;
+ }
+ }
+});
+```
+
+## Accessing Component State in Handlers
+
+```jsx
+function ProductScreen() {
+ const [quantity, setQuantity] = useState(1);
+ const { addToCart } = useCart();
+ const navigation = useNavigation();
+
+ const handleAddToCart = (productId) => {
+ // Add product to cart
+ addToCart(productId, quantity);
+
+ // Show toast with access to component state
+ toast.success('Added to cart', {
+ onPress: (toast) => {
+ // Access component state and navigation here
+ navigation.navigate('Cart', {
+ recentlyAdded: productId,
+ quantity
+ });
+ },
+ duration: 3000
+ });
+ };
+
+ return (
+
+ Quantity: {quantity}
+
+ );
+}
+```
+
+## Toast with Undo Functionality
+
+```jsx
+function EmailList() {
+ const { deleteEmail, restoreEmail } = useEmails();
+
+ const handleDelete = (emailId) => {
+ // Delete the email
+ deleteEmail(emailId);
+
+ // Show toast with undo button
+ toast('Email deleted', {
+ duration: 5000,
+ onPress: (toast) => {
+ // Restore the email when toast is pressed
+ restoreEmail(emailId);
+
+ // Dismiss the toast
+ toast.dismiss(toast.id);
+
+ // Show confirmation
+ toast.success('Email restored');
+ },
+ // Can also check dismiss reason
+ onHide: (toast, reason) => {
+ if (reason !== DismissReason.TAP) {
+ // If toast wasn't tapped (undo wasn't clicked),
+ // permanently delete the email
+ console.log('Email permanently deleted');
+ }
+ }
+ });
+ };
+
+ return (
+ (
+ handleDelete(item.id)}
+ />
+ )}
+ />
+ );
+}
+```
+
+## Programmatically Dismissing with Reason
+
+```jsx
+function UploadScreen() {
+ const [uploadId, setUploadId] = useState(null);
+
+ const startUpload = async () => {
+ // Show loading toast
+ const id = toast.loading('Uploading file...');
+ setUploadId(id);
+
+ try {
+ await uploadFile();
+ // Success - update toast
+ toast.success('Upload complete!', { id });
+ } catch (error) {
+ // Error - dismiss with custom reason
+ toast.dismiss(id, DismissReason.PROGRAMMATIC);
+
+ // Show error toast
+ toast.error('Upload failed');
+ }
+ };
+
+ const cancelUpload = () => {
+ if (uploadId) {
+ // Dismiss with custom reason
+ toast.dismiss(uploadId, DismissReason.PROGRAMMATIC);
+ toast('Upload cancelled');
+ }
+ };
+
+ return (
+
+
+
+
+ );
+}
+```
diff --git a/website/sidebars.ts b/website/sidebars.ts
index aba0921..274b559 100644
--- a/website/sidebars.ts
+++ b/website/sidebars.ts
@@ -33,6 +33,7 @@ const sidebars: SidebarsConfig = {
'features/animations',
'features/accessibility',
'features/customization',
+ 'features/toast-handlers',
],
},
],