Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ I know what you might be thinking (*jeez, another toast library?*). Trust me her
- **Multiple toasts, multiple options**. Want a toast on the top, bottom, different colors, or different types at the same time? Got it.
- **Keyboard handling** (both iOS and Android). Move those toasts out of the way and into view when the user opens the keyboard
- **Swipe to dismiss**
- **Positional toasts** (top & bottom)
- **Positional toasts** (top, bottom, top-left, top-right, bottom-left, bottom-right)
- **Customizable** (custom styles, dimensions, duration, and even create your own component to be used in the toast)
- Add support for **promises** <-- Really! Call `toast.promise(my_promise)` and watch react-native-toast work its magic, automatically updating the toast with a custom message on success -- or an error message on reject.
- Runs on **web**
Expand Down
6 changes: 6 additions & 0 deletions __tests__/src/components/Toasts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ jest.mock('react-native-reanimated', () => {
jest.mock('react-native-safe-area-context', () => {
return {
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
useSafeAreaFrame: () => ({
x: 0,
y: 0,
width: 350,
height: 650,
}),
};
});

Expand Down
13 changes: 6 additions & 7 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import {
View,
} from 'react-native';
import Modal from 'react-native-modal';
import { Toasts } from '../../src/components/Toasts';
import { toast } from '../../src/headless';
import { ToastPosition } from '../../src/core/types';
import { colors } from '../../src/utils';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';

const LoadingMessage = ({ msg }: { msg: string }) => {
const isDarkMode = useColorScheme() === 'dark';
Expand All @@ -31,13 +37,6 @@ const LoadingMessage = ({ msg }: { msg: string }) => {
);
};

import { Toasts } from '../../src/components/Toasts';
import { toast } from '../../src/headless';
import { ToastPosition } from '../../src/core/types';
import { colors } from '../../src/utils';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';

export default function App() {
const { width: screenWidth } = useWindowDimensions();
const isSystemDarkMode = useColorScheme() === 'dark';
Expand Down
100 changes: 76 additions & 24 deletions src/components/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,72 @@ export const Toast: FC<Props> = ({
const isDarkMode =
overrideDarkMode !== undefined ? overrideDarkMode : isSystemDarkMode;

const startingY = useMemo(
() =>
toast.position === ToastPosition.TOP
? -(toast.height || DEFAULT_TOAST_HEIGHT) - insets.top - 50
: height - insets.bottom - Platform.select({ ios: 0, default: 32 }),
[height, toast.position, insets.bottom, insets.top, toast.height]
);
const getStartingPosition = useMemo(() => {
let leftPosition = (width - toastWidth) / 2; // Default to center

if (
toast.position === ToastPosition.TOP_LEFT ||
toast.position === ToastPosition.BOTTOM_LEFT
) {
leftPosition = insets.left + 16 + (extraInsets?.left ?? 0);
}

if (
toast.position === ToastPosition.TOP_RIGHT ||
toast.position === ToastPosition.BOTTOM_RIGHT
) {
leftPosition =
width - toastWidth - insets.right - 16 - (extraInsets?.right ?? 0);
}

let startY = 0;

if (
toast.position === ToastPosition.TOP ||
toast.position === ToastPosition.TOP_LEFT ||
toast.position === ToastPosition.TOP_RIGHT
) {
startY = -(toast.height || DEFAULT_TOAST_HEIGHT) - insets.top - 50;
} else {
startY =
height - insets.bottom - Platform.select({ ios: 16, default: 32 });
}

return { startY, leftPosition };
}, [
height,
width,
toastWidth,
toast.position,
toast.height,
insets,
extraInsets,
]);

const { startY, leftPosition } = getStartingPosition;

const opacity = useSharedValue(0);
const position = useSharedValue(startingY);
const offsetY = useSharedValue(startingY);
const position = useSharedValue(startY);
const offsetY = useSharedValue(startY);

const onPress = () => onToastPress?.(toast);

const dismiss = useCallback((id: string) => {
toasting.dismiss(id);
}, []);

const getSwipeDirection = useCallback(() => {
if (
toast.position === ToastPosition.TOP ||
toast.position === ToastPosition.TOP_LEFT ||
toast.position === ToastPosition.TOP_RIGHT
) {
return Directions.UP;
} else {
return Directions.DOWN;
}
}, [toast.position]);

const setPosition = useCallback(() => {
let timingConfig: WithTimingConfig = { duration: 300 };
let springConfig: WithSpringConfig = { stiffness: 80 };
Expand All @@ -122,29 +170,35 @@ export const Toast: FC<Props> = ({
}

const useSpringAnimation = toast.animationType === 'spring';

const animation = useSpringAnimation ? withSpring : withTiming;

if (toast.position === ToastPosition.TOP) {
if (
toast.position === ToastPosition.TOP ||
toast.position === ToastPosition.TOP_LEFT ||
toast.position === ToastPosition.TOP_RIGHT
) {
offsetY.value = animation(
toast.visible ? offset : startingY,
toast.visible ? offset : startY,
useSpringAnimation ? springConfig : timingConfig
);
position.value = animation(
toast.visible ? offset : startingY,
toast.visible ? offset : startY,
useSpringAnimation ? springConfig : timingConfig
);
} else {
let kbHeight = keyboardVisible ? keyboardHeight : 0;
const val = toast.visible
? startingY -
? startY -
toastHeight -
offset -
kbHeight -
insets.bottom -
(extraInsets?.bottom ?? 0) -
24
: startingY;
Platform.select({
ios: 32,
default: 24,
})
: startY;

offsetY.value = animation(
val,
Expand All @@ -163,7 +217,7 @@ export const Toast: FC<Props> = ({
toastHeight,
insets.bottom,
position,
startingY,
startY,
toast.position,
offsetY,
extraInsets,
Expand All @@ -181,11 +235,9 @@ export const Toast: FC<Props> = ({
});

const flingGesture = Gesture.Fling()
.direction(
toast.position === ToastPosition.TOP ? Directions.UP : Directions.DOWN
)
.direction(getSwipeDirection())
.onEnd(() => {
offsetY.value = withTiming(startingY, {
offsetY.value = withTiming(startY, {
duration: toast?.animationConfig?.flingPositionReturnDuration ?? 40,
});
runOnJS(dismiss)(toast.id);
Expand All @@ -196,14 +248,14 @@ export const Toast: FC<Props> = ({
: panGesture;
}, [
offsetY,
startingY,
startY,
position,
setPosition,
toast.position,
toast.id,
dismiss,
toast.isSwipeable,
toast.animationConfig,
getSwipeDirection,
]);

useVisibilityChange(
Expand Down Expand Up @@ -275,7 +327,7 @@ export const Toast: FC<Props> = ({
: undefined,
borderRadius: 8,
position: 'absolute',
left: (width - toastWidth) / 2,
left: leftPosition,
zIndex: toast.visible ? 9999 : undefined,
alignItems: 'center',
justifyContent: 'center',
Expand Down
32 changes: 28 additions & 4 deletions src/components/Toasts.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import React, { FunctionComponent } from 'react';
import { TextStyle, View, ViewStyle } from 'react-native';
import {
Platform,
TextStyle,
View,
ViewStyle,
useWindowDimensions,
} from 'react-native';

import { Toast as T, useToaster } from '../headless';
import { Toast } from './Toast';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import {
useSafeAreaInsets,
useSafeAreaFrame,
} from 'react-native-safe-area-context';
import {
ExtraInsets,
ToastAnimationConfig,
Expand All @@ -28,6 +37,7 @@ type Props = {
};
globalAnimationType?: ToastAnimationType;
globalAnimationConfig?: ToastAnimationConfig;
fixAndroidInsets?: boolean;
};

export const Toasts: FunctionComponent<Props> = ({
Expand All @@ -41,13 +51,24 @@ export const Toasts: FunctionComponent<Props> = ({
defaultStyle,
globalAnimationType,
globalAnimationConfig,
fixAndroidInsets = true,
}) => {
const { toasts, handlers } = useToaster({ providerKey });
const { startPause, endPause } = handlers;
const insets = useSafeAreaInsets();
const safeAreaFrame = useSafeAreaFrame();
const dimensions = useWindowDimensions();
const isScreenReaderEnabled = useScreenReader();
const { keyboardShown: keyboardVisible, keyboardHeight } = useKeyboard();

// Fix for Android bottom inset bug: https://github.com/facebook/react-native/issues/47080
const bugFixDelta =
fixAndroidInsets &&
Platform.OS === 'android' &&
Math.abs(safeAreaFrame.height - dimensions.height) > 1
? insets.bottom
: 0;

if (isScreenReaderEnabled && !preventScreenReaderFromHiding) {
return null;
}
Expand All @@ -59,7 +80,7 @@ export const Toasts: FunctionComponent<Props> = ({
top: insets.top + (extraInsets?.top ?? 0) + 16,
left: insets.left + (extraInsets?.left ?? 0),
right: insets.right + (extraInsets?.right ?? 0),
bottom: insets.bottom + (extraInsets?.bottom ?? 0) + 16,
bottom: insets.bottom + bugFixDelta + (extraInsets?.bottom ?? 0) + 16,
pointerEvents: 'box-none',
}}
>
Expand All @@ -82,7 +103,10 @@ export const Toasts: FunctionComponent<Props> = ({
onToastHide={onToastHide}
onToastPress={onToastPress}
onToastShow={onToastShow}
extraInsets={extraInsets}
extraInsets={{
...extraInsets,
bottom: (extraInsets?.bottom ?? 0) + bugFixDelta,
}}
defaultStyle={defaultStyle}
keyboardVisible={keyboardVisible}
keyboardHeight={keyboardHeight}
Expand Down
4 changes: 4 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export type ToastType = 'success' | 'error' | 'loading' | 'blank';
export enum ToastPosition {
TOP = 1,
BOTTOM = 2,
TOP_LEFT = 3,
TOP_RIGHT = 4,
BOTTOM_LEFT = 5,
BOTTOM_RIGHT = 6,
}

export type Element = JSX.Element | string | null;
Expand Down
45 changes: 17 additions & 28 deletions website/docs/api/toast.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,36 +309,25 @@ toast('my toast', {
where `AutoWidthStyles` holds the actual styles for auto width.


## Maximum Width
You can set a maximum width for the toast by using the `maxWidth` option. This is useful when you want to limit the width of the toast to a certain value, regardless of the content size.
*Note* If you provide a width to the toast, it will override the `maxWidth` option.
```js
toast('This toast has a maximum width', {
duration: 4000,
position: ToastPosition.TOP,
maxWidth: 100, // <-- Add here
})
```

<br />

## 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 or ToastPosition.BOTTOM. |
| `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. |
| `maxWidth` | number | | Maximum width of 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. |


6 changes: 6 additions & 0 deletions website/docs/components/toasts.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ useEffect(() => {
}, [isModalVisible]);
```

### fixAndroidInsets
`boolean` *Defaults to true*

Fix for Android bottom inset bug: https://github.com/facebook/react-native/issues/47080




## Example
Expand Down
4 changes: 4 additions & 0 deletions website/docs/features/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ toast('Bottom Position', {
position: ToastPosition.BOTTOM,
});

toast('Bottom Right Position', {
position: ToastPosition.BOTTOM_RIGHT,
});

```

### Extra Insets
Expand Down