From dea4f0ed2f0118e3e1b3a64c8ce784223db5b0e4 Mon Sep 17 00:00:00 2001 From: Suin Kim Date: Mon, 3 Nov 2025 07:29:28 +0900 Subject: [PATCH 1/3] docs: add Korean translation (README-ko.md) and language switch links (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add Korean translation for README * docs: add language switch links to README and README-ko.md - Added English ↔ Korean navigation links at the top of both files - Helps users easily switch between README.md and README-ko.md --- README-ko.md | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 155 insertions(+) create mode 100644 README-ko.md diff --git a/README-ko.md b/README-ko.md new file mode 100644 index 0000000..97ba33e --- /dev/null +++ b/README-ko.md @@ -0,0 +1,153 @@ +# React Native Toast +[![runs with expo](https://img.shields.io/badge/Runs%20with%20Expo-4630EB.svg?style=flat-square&logo=EXPO&labelColor=f3f3f3&logoColor=000)](https://expo.io/) [![GitHub license](https://img.shields.io/github/license/backpackapp-io/react-native-toast)](https://github.com/backpackapp-io/react-native-toast/blob/master/LICENSE) [![npm](https://img.shields.io/badge/types-included-blue?style=flat-square)](https://www.npmjs.com/package/@backpackapp-io/react-native-toast) + +[English](./README.md) · 한국어 + +**React Native Toast**는 [react-hot-toast](https://react-hot-toast.com/docs)를 기반으로 구축된 React Native용 토스트(Toast) 라이브러리입니다. +iOS, Android, Web 환경에서 모두 작동하며, 다중 토스트 표시, 키보드 대응, 스와이프 닫기, 위치 지정, 그리고 Promise 기반 처리와 같은 기능을 제공합니다. + +
+ +[video](https://user-images.githubusercontent.com/27066041/180588807-1ca73f29-56d7-4e44-ac0c-9f2e2cdeb94c.mp4) + +
+ +### 왜 이 라이브러리를 만들었나요? +아마 이렇게 생각하실 수도 있습니다. (*또 다른 토스트 라이브러리라고? 정말 필요할까?*). 하지만 믿어주세요. **이 라이브러리 하나면 충분합니다.** 제가 직접 앱을 개발하면서 필요한 기능을 모두 담기 위해 만들었고, 사용해보니 완성도가 높아 오픈소스로 공개하게 되었습니다. + +## 주요 기능 + +- **다중 토스트 및 다양한 옵션** 여러 개의 토스트를 동시에 표시하거나, 위치·색상·유형을 각각 다르게 설정할 수 있습니다. +- **키보드 대응** iOS와 Android에서 키보드가 열릴 때 토스트가 자동으로 화면에 잘 보이도록 위치를 조정합니다. +- **스와이프로 닫기** +- **위치 지정 지원** (`top`, `bottom`, `top-left`, `top-right`, `bottom-left`, `bottom-right`) +- **높은 수준의 커스터마이징** (스타일, 크기, 지속 시간 등을 조정하거나, 사용자 정의 컴포넌트를 표시할 수 있습니다.) +- **Promise 지원** (`toast.promise()`를 사용하면 Promise의 상태에 따라 토스트 메시지가 자동으로 업데이트됩니다.) +- **Web 지원** +- **Native Modal 지원** +- **onPress, onShow, onHide 콜백 지원** + +# 문서 +전체 문서는 [여기](https://nickdebaise.github.io/packages/react-native-toast/)에서 확인할 수 있습니다. + +# 시작하기 + +## 설치 +```sh +yarn add @backpackapp-io/react-native-toast +# 또는 +npm i @backpackapp-io/react-native-toast +``` +#### 필수 의존성 +다음 패키지를 함께 설치해야 합니다. +- [react-native-reanimated](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/) +- [react-native-safe-area-context](https://github.com/th3rdwave/react-native-safe-area-context) +- [react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/installation) + +```sh +yarn add react-native-reanimated react-native-safe-area-context react-native-gesture-handler +``` +*각 패키지의 설치 가이드를 반드시 참고하세요.* + + +*Expo를 사용하는 경우* +```sh +npx expo install react-native-reanimated react-native-safe-area-context react-native-gesture-handler +``` +
+ +### 사용 방법 +앱의 루트 컴포넌트를 ```` 및 ````로 감싸고, 루트 레벨에 ```` 컴포넌트를 추가합니다. + +이후 앱의 어느 위치에서든 ``toast("메시지")``를 호출할 수 있습니다. + +```js +import { View, StyleSheet, Text } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { toast, Toasts } from '@backpackapp-io/react-native-toast'; +import { useEffect } from 'react'; + +export default function App() { + useEffect(() => { + toast('Hello'); + }, []); + + return ( + + + {/* 앱의 나머지 컴포넌트 */} + {/* 여기에 추가 */} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, +}); +``` + +
+ +## 예제 + +#### Regular Toast +```js +toast("This is my first toast", { + duration: 3000, +}); +``` + +#### Promise Toast +```js +const sleep = new Promise((resolve, reject) => { + setTimeout(() => { + if (Math.random() > 0.5) { + resolve({ + username: 'Nick', + }); + } else { + reject('Username is undefined'); + } + }, 2500); +}); + +toast.promise( + sleep, + { + loading: 'Loading...', + success: (data: any) => 'Welcome ' + data.username, + error: (err) => err.toString(), + }, + { + position: ToastPosition.BOTTOM, + } +); +``` + +#### Loading Toast +```js +const id = toast.loading('I am loading. Dismiss me whenever...'); + +setTimeout(() => { + toast.dismiss(id); +}, 3000); +``` + +#### Success Toast +```js +toast.success('Success!', { + width: 300 +}); +``` +#### Error Toast +```js +toast.error('Wow. That Sucked!'); +``` + +
diff --git a/README.md b/README.md index 5ec0ed6..f97083e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # React Native Toast [![runs with expo](https://img.shields.io/badge/Runs%20with%20Expo-4630EB.svg?style=flat-square&logo=EXPO&labelColor=f3f3f3&logoColor=000)](https://expo.io/) [![GitHub license](https://img.shields.io/github/license/backpackapp-io/react-native-toast)](https://github.com/backpackapp-io/react-native-toast/blob/master/LICENSE) [![npm](https://img.shields.io/badge/types-included-blue?style=flat-square)](https://www.npmjs.com/package/@backpackapp-io/react-native-toast) +English · [한국어](./README-ko.md) + A toast library for react-native, built on [react-hot-toast](https://react-hot-toast.com/docs). It supports features such as multiple toasts, keyboard handling, swipe to dismiss, positional toasts, and JS promises. It runs on iOS, android, and web.
From 4c38abe12f1b01513579b93ff12f4f80627332a6 Mon Sep 17 00:00:00 2001 From: Micael Dias Date: Sun, 2 Nov 2025 23:59:28 +0100 Subject: [PATCH 2/3] fix: web support (#78) * fix: web support * fix: fix interactivity on mobile, add param for screen reader, update docs --------- Co-authored-by: Nick DeBaise --- src/components/Toast.tsx | 4 ++++ src/components/Toasts.tsx | 9 ++++++-- src/utils/useScreenReader.ts | 32 ++++++++++++++++++++++---- website/docs/components/toasts.md | 18 +++++++++++++++ website/docs/features/accessibility.md | 23 ++++++++++++++++++ 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 1a6d02b..5d98dd3 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -374,6 +374,10 @@ export const Toast: FC = ({ zIndex: toast.visible ? 9999 : undefined, alignItems: 'center', justifyContent: 'center', + pointerEvents: Platform.select({ + web: 'auto', + default: undefined, + }), }, style, !toast.disableShadow && ConstructShadow('#181821', 0.15, false), diff --git a/src/components/Toasts.tsx b/src/components/Toasts.tsx index f906f2e..5870a52 100644 --- a/src/components/Toasts.tsx +++ b/src/components/Toasts.tsx @@ -25,6 +25,7 @@ import { useKeyboard } from '../utils'; type Props = { overrideDarkMode?: boolean; + overrideScreenReaderEnabled?: boolean; extraInsets?: ExtraInsets; onToastShow?: (toast: T) => void; onToastHide?: (toast: T, reason?: DismissReason) => void; @@ -46,6 +47,7 @@ type Props = { export const Toasts: FunctionComponent = ({ overrideDarkMode, + overrideScreenReaderEnabled, extraInsets, onToastHide, onToastPress, @@ -69,7 +71,7 @@ export const Toasts: FunctionComponent = ({ const insets = useSafeAreaInsets(); const safeAreaFrame = useSafeAreaFrame(); const dimensions = useWindowDimensions(); - const isScreenReaderEnabled = useScreenReader(); + const isScreenReaderEnabled = useScreenReader(overrideScreenReaderEnabled); const { keyboardShown: keyboardVisible, keyboardHeight } = useKeyboard(); // Fix for Android bottom inset bug: https://github.com/facebook/react-native/issues/47080 @@ -92,7 +94,10 @@ export const Toasts: FunctionComponent = ({ left: insets.left + (extraInsets?.left ?? 0), right: insets.right + (extraInsets?.right ?? 0), bottom: insets.bottom + bugFixDelta + (extraInsets?.bottom ?? 0) + 16, - pointerEvents: 'box-none', + pointerEvents: Platform.select({ + web: 'none', + default: 'box-none', + }), }} > {toasts.map((t) => ( diff --git a/src/utils/useScreenReader.ts b/src/utils/useScreenReader.ts index 02506af..6c68919 100644 --- a/src/utils/useScreenReader.ts +++ b/src/utils/useScreenReader.ts @@ -1,16 +1,40 @@ import { useEffect, useState } from 'react'; -import { AccessibilityInfo } from 'react-native'; +import { AccessibilityInfo, Platform } from 'react-native'; -export const useScreenReader = () => { +/** + * Hook to detect if a screen reader is enabled on the device. + * + * @param override - Optional boolean to override the detected screen reader state. + * When provided, this value will be returned instead of the detected state. + * Useful for testing or forcing a specific screen reader state. + * @returns {boolean} True if screen reader is enabled (or overridden to true), false otherwise. + * + * @example + * // Auto-detect screen reader + * const isScreenReaderEnabled = useScreenReader(); + * + * @example + * // Force screen reader to be enabled + * const isScreenReaderEnabled = useScreenReader(true); + * + * @example + * // Force screen reader to be disabled + * const isScreenReaderEnabled = useScreenReader(false); + */ +export const useScreenReader = (override?: boolean) => { const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false); useEffect(() => { AccessibilityInfo.isScreenReaderEnabled() - .then(setIsScreenReaderEnabled) + .then((isEnabled) => { + if (Platform.OS !== 'web') { + setIsScreenReaderEnabled(isEnabled); + } + }) .catch(() => { setIsScreenReaderEnabled(false); }); }, []); - return isScreenReaderEnabled; + return override !== undefined ? override : isScreenReaderEnabled; }; export const announceForAccessibility = (message: string) => { diff --git a/website/docs/components/toasts.md b/website/docs/components/toasts.md index 0322b82..a98f6f1 100644 --- a/website/docs/components/toasts.md +++ b/website/docs/components/toasts.md @@ -73,6 +73,24 @@ toast("Quick message", { duration: 2000 }); Override the system dark mode. If a value is supplied (I.e. `true` or `false`), then the toast components will use that value for the dark mode. For example, if `overrideDarkMode = {false}`, dark mode will be disabled, regardless of the system's preferences. +### overrideScreenReaderEnabled +`boolean | undefined` + +Override the detected screen reader state. When set, this forces the component to behave as if the screen reader is enabled or disabled, regardless of the actual device state. + +- `true`: Component behaves as if screen reader is enabled (toasts will be hidden unless `preventScreenReaderFromHiding` is also `true`) +- `false`: Component behaves as if screen reader is disabled (toasts will always be shown) +- `undefined` (default): Auto-detect screen reader state from the device + +This is useful for testing or forcing specific behavior in your app. + +```js +// Force toasts to show even if screen reader is enabled + +``` + +See the [Accessibility](/features/accessibility#overriding-screen-reader-detection) section for more details. + ### extraInsets `object` diff --git a/website/docs/features/accessibility.md b/website/docs/features/accessibility.md index 2328f23..a00ae23 100644 --- a/website/docs/features/accessibility.md +++ b/website/docs/features/accessibility.md @@ -11,3 +11,26 @@ Bottom-positioned toasts will automatically move with the keyboard. This behavio ## Screen Reader Support If the device's screen reader is enabled, react-native-toast will not show the toast. It will announce the toast message to the user using the announceAccessibility method. + +### Overriding Screen Reader Detection + +You can override the detected screen reader state using the `overrideScreenReaderEnabled` prop on the `` component. This is useful for testing or when you want to force a specific behavior regardless of the actual screen reader state. + +```js +// Force the component to behave as if screen reader is disabled + +``` + +```js +// Force the component to behave as if screen reader is enabled + +``` + +When `overrideScreenReaderEnabled` is set: +- `true`: Toasts will be hidden (unless `preventScreenReaderFromHiding` is also `true`) +- `false`: Toasts will always be shown, even if a screen reader is actually enabled +- `undefined` (default): Auto-detect the screen reader state from the device + +This prop works independently from `preventScreenReaderFromHiding`: +- If you set `overrideScreenReaderEnabled={true}` and `preventScreenReaderFromHiding={true}`, toasts will still be shown +- If you set `overrideScreenReaderEnabled={false}`, toasts will always be shown regardless of `preventScreenReaderFromHiding` From 71056f74ff6382a6b77c377d10129ab2b7a31dea Mon Sep 17 00:00:00 2001 From: "daeun.yang" <75559282+dana-y@users.noreply.github.com> Date: Mon, 3 Nov 2025 07:59:51 +0900 Subject: [PATCH 3/3] feat: Add globalLimit prop to control maximum visible toasts per provider (#76) * feat: add globalLimit option to control the maximum number of toasts displayed * Use optional chaining for toastOptions.limit * fix: fix up merge conflict. Note-to-self: Do not use Github web UI for merginggit add * feat: add unit test for toast limit functionality based on globalLimit prop * fix: update toast dismissal method in tests from dismiss() to remove() for consistency --------- Co-authored-by: Nick DeBaise --- __tests__/src/components/Toasts.test.tsx | 16 +++++++++++++++- src/components/Toasts.tsx | 3 +++ src/core/store.ts | 6 +++++- src/core/types.ts | 2 ++ website/docs/components/toasts.md | 6 ++++++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/__tests__/src/components/Toasts.test.tsx b/__tests__/src/components/Toasts.test.tsx index c14fc8a..6be92fb 100644 --- a/__tests__/src/components/Toasts.test.tsx +++ b/__tests__/src/components/Toasts.test.tsx @@ -32,7 +32,7 @@ describe('', () => { afterEach(() => { jest.clearAllMocks(); act(() => { - toast.dismiss(); + toast.remove(); }); }); @@ -135,4 +135,18 @@ describe('', () => { const toastElement = getByText('Top Toast'); expect(toastElement).toBeTruthy(); }); + + it('applies toast limit from globalLimit prop', () => { + const { getByText, queryByText } = render(); + + act(() => { + toast('Toast 1', { id: '1' }); + toast('Toast 2', { id: '2' }); + toast('Toast 3', { id: '3' }); + }); + + expect(getByText('Toast 2')).toBeTruthy(); + expect(getByText('Toast 3')).toBeTruthy(); + expect(queryByText('Toast 1')).toBeNull(); + }); }); diff --git a/src/components/Toasts.tsx b/src/components/Toasts.tsx index 5870a52..25faea1 100644 --- a/src/components/Toasts.tsx +++ b/src/components/Toasts.tsx @@ -40,6 +40,7 @@ type Props = { }; globalAnimationType?: ToastAnimationType; globalAnimationConfig?: ToastAnimationConfig; + globalLimit?: number; defaultPosition?: ToastPosition; defaultDuration?: number; fixAndroidInsets?: boolean; @@ -59,6 +60,7 @@ export const Toasts: FunctionComponent = ({ globalAnimationConfig, defaultPosition, defaultDuration, + globalLimit, fixAndroidInsets = true, }) => { const { toasts, handlers } = useToaster({ @@ -66,6 +68,7 @@ export const Toasts: FunctionComponent = ({ duration: defaultDuration, position: defaultPosition, animationType: globalAnimationType, + limit: globalLimit, }); const { startPause, endPause } = handlers; const insets = useSafeAreaInsets(); diff --git a/src/core/store.ts b/src/core/store.ts index 69173b0..e849185 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -87,7 +87,7 @@ export const reducer = (state: State, action: Action): State => { case ActionType.ADD_TOAST: return { ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + toasts: [action.toast, ...state.toasts], }; case ActionType.UPDATE_TOAST: @@ -187,6 +187,7 @@ const defaultTimeouts: { export const useStore = (toastOptions: DefaultToastOptions = {}): State => { const [state, setState] = useState(memoryState); + useEffect(() => { listeners.push(setState); return () => { @@ -197,6 +198,8 @@ export const useStore = (toastOptions: DefaultToastOptions = {}): State => { }; }, [state]); + const limit = toastOptions?.limit ?? TOAST_LIMIT; + const mergedToasts = state.toasts .filter( (t) => @@ -204,6 +207,7 @@ export const useStore = (toastOptions: DefaultToastOptions = {}): State => { t.providerKey === toastOptions?.providerKey || t.providerKey === 'PERSISTS' ) + .slice(0, limit) .map((t) => ({ ...toastOptions, ...toastOptions[t.type], diff --git a/src/core/types.ts b/src/core/types.ts index 0bee466..48ec478 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -62,6 +62,7 @@ export interface Toast { height?: number; width?: number; maxWidth?: number; + limit?: number; onPress?: (toast: Toast) => void; onHide?: (toast: Toast, reason: DismissReason) => void; onShow?: (toast: Toast) => void; @@ -90,6 +91,7 @@ export type ToastOptions = Partial< | 'styles' | 'height' | 'width' + | 'limit' | 'customToast' | 'disableShadow' | 'providerKey' diff --git a/website/docs/components/toasts.md b/website/docs/components/toasts.md index a98f6f1..76b368e 100644 --- a/website/docs/components/toasts.md +++ b/website/docs/components/toasts.md @@ -28,6 +28,12 @@ Set the global animation config for all toasts. This can be overridden by the to ``` +### globalLimit +`number | undefined` + +Set the global limit for the number of toasts that can be shown at once. When this limit is reached, the oldest toast will be removed to make room for the new one. + + ### defaultPosition `ToastPosition | undefined`