react-native-toast-message-library 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +333 -2
- package/package.json +82 -4
- package/src/animations/toastAnimations.js +85 -0
- package/src/components/ToastAction.js +39 -0
- package/src/components/ToastContainer.js +109 -0
- package/src/components/ToastIcon.js +65 -0
- package/src/components/ToastItem.js +357 -0
- package/src/components/ToastProgress.js +69 -0
- package/src/components/ToastProvider.js +107 -0
- package/src/context/ToastContext.js +5 -0
- package/src/global.js +38 -0
- package/src/hooks/useReducedMotion.js +28 -0
- package/src/hooks/useToast.js +13 -0
- package/src/index.js +4 -0
- package/src/manager/ToastManager.js +399 -0
- package/src/manager/createToastApi.js +33 -0
- package/src/themes/dark.js +64 -0
- package/src/themes/index.js +73 -0
- package/src/themes/light.js +64 -0
- package/src/utils/constants.js +40 -0
- package/src/utils/createAnnouncer.js +46 -0
- package/src/utils/generateToastId.js +10 -0
- package/src/utils/normalizeToastOptions.js +42 -0
- package/src/utils/safeArea.js +34 -0
- package/src/utils/validateToastOptions.js +127 -0
- package/types/index.d.ts +203 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - Unreleased
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `ToastProvider`, `useToast()` and an opt-in global `Toast` API.
|
|
11
|
+
- Toast types: success, error, warning, info, custom.
|
|
12
|
+
- Seven positions, safe-area aware, with provider and per-toast offsets.
|
|
13
|
+
- `fade`, `slide`, `zoom` and `none` animations (native driver) with reduced-motion fallback.
|
|
14
|
+
- Auto-dismiss, persistent toasts (`duration: 0`), pause/resume while interacting.
|
|
15
|
+
- FIFO queue with `maxVisible`, unique ids, update-in-place for repeated ids.
|
|
16
|
+
- Close button, leading icon, action button, progress bar, swipe-to-dismiss (built-in `PanResponder`).
|
|
17
|
+
- `onShow`, `onPress`, `onAction`, `onDismiss(reason)` callbacks.
|
|
18
|
+
- Light, dark and system themes, `createTheme()`, per-toast style overrides.
|
|
19
|
+
- Accessibility roles/labels, live regions, throttled iOS announcements.
|
|
20
|
+
- TypeScript declarations in `types/index.d.ts`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Denish Dabhi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,334 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-native-toast-message-library
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lightweight, customizable toast notifications for React Native. Pure JavaScript (no native modules, no
|
|
4
|
+
required dependencies), built on the `Animated` API, with a FIFO queue, themes, actions, swipe-to-dismiss
|
|
5
|
+
and accessibility support. TypeScript declarations included.
|
|
6
|
+
|
|
7
|
+
> **Publishing note:** `react-native-toast-message-library` was not registered on npm when checked (2026-10-24). Re-check with `npm view react-native-toast-message-library` right before publishing.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Types: `success`, `error`, `warning`, `info`, `custom`
|
|
12
|
+
- Seven positions: `top`, `bottom`, `center`, `top-left`, `top-right`, `bottom-left`, `bottom-right` (per toast)
|
|
13
|
+
- Animations: `fade`, `slide`, `zoom`, `none` (native driver; reduced-motion aware)
|
|
14
|
+
- Auto-dismiss, persistent toasts (`duration: 0`), timer pauses while the user touches or swipes a toast
|
|
15
|
+
- FIFO queue with `maxVisible`; dismiss by id, `hideAll()`, `isVisible()`
|
|
16
|
+
- Close button, leading icon, action button, progress bar, swipe-to-dismiss, tap handler
|
|
17
|
+
- `onShow`, `onPress`, `onAction`, `onDismiss(reason)` callbacks
|
|
18
|
+
- Light / dark / system themes, `createTheme()`, per-toast style overrides, custom icons and content
|
|
19
|
+
- Safe-area aware (optional `react-native-safe-area-context`)
|
|
20
|
+
- Accessibility roles and labels, Android live region, throttled iOS announcements
|
|
21
|
+
- Optional vibration feedback (`haptics`)
|
|
22
|
+
|
|
23
|
+
## Compatibility
|
|
24
|
+
|
|
25
|
+
| What | Status |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| React Native 0.88.0 / React 19.3.0 | **Verified**: unit tests (Jest) and Metro bundling for Android and iOS |
|
|
28
|
+
| Real devices / emulators | **Not verified in this repository's CI environment** (no Android SDK or macOS available when it was built). Run the demo app before relying on visuals. |
|
|
29
|
+
| Other RN / React versions | **Not verified.** Peer ranges (`react >=18`, `react-native >=0.72`) reflect the APIs used (`useSyncExternalStore` needs React 18) and are lower bounds, not test results. |
|
|
30
|
+
| New Architecture / Old Architecture | Not separately tested. No native code is involved. |
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install react-native-toast-message-library
|
|
36
|
+
# optional, recommended: exact safe-area insets
|
|
37
|
+
npm install react-native-safe-area-context
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
No pods, no linking, no Gradle changes. If you use `react-native-safe-area-context`, wrap your app in its
|
|
41
|
+
`SafeAreaProvider`; without it the library falls back to the Android status-bar height and conservative
|
|
42
|
+
iOS notch values (47 top / 34 bottom).
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```jsx
|
|
47
|
+
import { ToastProvider, useToast } from 'react-native-toast-message-library';
|
|
48
|
+
|
|
49
|
+
export default function App() {
|
|
50
|
+
return (
|
|
51
|
+
<ToastProvider position="top" duration={3000} animation="slide" maxVisible={3}>
|
|
52
|
+
<Home />
|
|
53
|
+
</ToastProvider>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Home() {
|
|
58
|
+
const toast = useToast();
|
|
59
|
+
return (
|
|
60
|
+
<Button
|
|
61
|
+
title="Save"
|
|
62
|
+
onPress={() => toast.success('Your changes have been saved.')}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The provider renders its children followed by the toast overlay in a wrapper `View` with `flex: 1`
|
|
69
|
+
(change with the `style` prop). Place it near the root, inside `SafeAreaProvider` if you use one.
|
|
70
|
+
|
|
71
|
+
## Toast methods
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const toast = useToast();
|
|
75
|
+
|
|
76
|
+
toast.show(options | 'message'); // -> id
|
|
77
|
+
toast.success(message, options); // -> id (also error / warning / info)
|
|
78
|
+
toast.hide(id); // -> boolean (true if a visible or queued toast was dismissed)
|
|
79
|
+
toast.hideAll(); // -> number of toasts dismissed
|
|
80
|
+
toast.isVisible(id?); // -> boolean (no id: is any toast visible?)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- `success/error/warning/info` accept a string **or** an options object as the first argument; the type is
|
|
84
|
+
always forced to the method's type. By default they show a title ("Success", "Error", …); pass
|
|
85
|
+
`title: ''` to hide it.
|
|
86
|
+
- Invalid options **throw a `TypeError`** with an explanation (unknown `position`, negative `duration`,
|
|
87
|
+
non-function callbacks, no `message`/`title`/`render`, …). Nothing is silently ignored.
|
|
88
|
+
- `useToast()` outside a `ToastProvider` throws an error telling you to add one.
|
|
89
|
+
- The returned object is stable: components using `useToast()` do not re-render when toasts change.
|
|
90
|
+
|
|
91
|
+
### Global API (opt-in)
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
import { ToastProvider, Toast } from 'react-native-toast-message-library';
|
|
95
|
+
|
|
96
|
+
<ToastProvider global>…</ToastProvider>
|
|
97
|
+
|
|
98
|
+
Toast.error('Network request failed'); // usable from services, outside React
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`global` registers that provider when it mounts and unregisters on unmount. If no global provider is
|
|
102
|
+
mounted, `Toast.*` **throws** (it never fails silently). With several global providers the most recently
|
|
103
|
+
mounted one wins. Prefer the hook inside components.
|
|
104
|
+
|
|
105
|
+
## Options (per toast)
|
|
106
|
+
|
|
107
|
+
| Option | Type | Default | Description |
|
|
108
|
+
| --- | --- | --- | --- |
|
|
109
|
+
| `id` | string \| number | generated | Reusing the id of a visible/queued toast **updates it in place** and restarts its timer |
|
|
110
|
+
| `type` | `'success' \| 'error' \| 'warning' \| 'info' \| 'custom'` | `'custom'` | Controls colours, icon and default title |
|
|
111
|
+
| `title` | string \| null | type label | `''`/`null` hides the title |
|
|
112
|
+
| `message` | string | – | At least one of `message`, `title`, `render` is required |
|
|
113
|
+
| `duration` | number (ms) | `3000` | `0` = persistent |
|
|
114
|
+
| `position` | see Positions | `'top'` | |
|
|
115
|
+
| `animation` | `'fade' \| 'slide' \| 'zoom' \| 'none'` | `'slide'` | |
|
|
116
|
+
| `animationDuration` | number (ms) | `250` | |
|
|
117
|
+
| `offsetX`, `offsetY` | number | – | Extra margin on top of the provider `offset` |
|
|
118
|
+
| `icon` | `false` \| element \| `({type,color,size}) => node` | built-in glyph | |
|
|
119
|
+
| `render` | `({id,type,hide,colors,theme}) => node` | – | Replaces icon + title + message |
|
|
120
|
+
| `closeButton` | boolean | `false` | Accessible "Dismiss notification" button |
|
|
121
|
+
| `swipeToDismiss` | boolean | `true` | Horizontal swipe |
|
|
122
|
+
| `progress` | boolean | `false` | Remaining-time bar (only when `duration > 0`) |
|
|
123
|
+
| `haptics` | boolean | `false` | Vibration, see Limitations |
|
|
124
|
+
| `action` | `{ label, accessibilityLabel? }` | – | Action button |
|
|
125
|
+
| `dismissOnAction` | boolean | `true` | |
|
|
126
|
+
| `dismissOnPress` | boolean | `false` | |
|
|
127
|
+
| `backgroundColor`, `textColor`, `borderColor`, `iconColor` | string | theme | |
|
|
128
|
+
| `borderRadius`, `padding` | number | theme | |
|
|
129
|
+
| `shadow` | `false` \| `{color,opacity,radius,offsetY,elevation}` | theme | |
|
|
130
|
+
| `containerStyle`, `titleStyle`, `messageStyle` | style | – | |
|
|
131
|
+
| `accessibilityLabel`, `closeAccessibilityLabel` | string | generated | |
|
|
132
|
+
| `onShow(id)` | function | – | Called when the toast becomes visible (after any time in the queue) |
|
|
133
|
+
| `onPress({id,type,hide})` | function | – | Toast body pressed |
|
|
134
|
+
| `onAction({id,type,hide})` | function | – | Action button pressed |
|
|
135
|
+
| `onDismiss(reason, id)` | function | – | See below |
|
|
136
|
+
|
|
137
|
+
Provider props: `theme`, `position`, `duration`, `animation`, `animationDuration`, `maxVisible` (3),
|
|
138
|
+
`closeButton`, `swipeToDismiss`, `progress`, `haptics`, `spacing`, `offset={{x,y}}`, `insets`, `global`, `style`.
|
|
139
|
+
Defaults set on the provider apply to every toast that does not override them; changing a provider prop
|
|
140
|
+
affects toasts shown afterwards.
|
|
141
|
+
|
|
142
|
+
### Callbacks
|
|
143
|
+
|
|
144
|
+
- `onDismiss(reason, id)` is called **exactly once**, at the moment dismissal starts (before the exit
|
|
145
|
+
animation), for visible toasts and for toasts cancelled while still queued. `reason` is one of
|
|
146
|
+
`'timeout' | 'manual' | 'swipe' | 'close' | 'action' | 'press' | 'hideAll'`.
|
|
147
|
+
- `onAction` fires once when `dismissOnAction` is true (default) even if the button is tapped repeatedly;
|
|
148
|
+
with `dismissOnAction: false` it fires on every press.
|
|
149
|
+
- Exceptions thrown inside callbacks are caught and logged with `console.error`; they cannot corrupt the queue.
|
|
150
|
+
|
|
151
|
+
## Positions
|
|
152
|
+
|
|
153
|
+
`top`, `bottom`, `center`, `top-left`, `top-right`, `bottom-left`, `bottom-right`. Each toast can choose its
|
|
154
|
+
own; toasts sharing a position stack (oldest first). Toasts are at most 480 dp wide and always keep a
|
|
155
|
+
16 dp side margin. Insets come from the `insets` prop, else `react-native-safe-area-context`, else the
|
|
156
|
+
fallback described above.
|
|
157
|
+
|
|
158
|
+
## Queue behaviour
|
|
159
|
+
|
|
160
|
+
- At most `maxVisible` toasts are on screen; further requests wait in a **FIFO** queue.
|
|
161
|
+
- A toast occupies its slot until its **exit animation has finished**, then the oldest queued toast is promoted.
|
|
162
|
+
Its timer starts when it becomes visible, not when it was requested.
|
|
163
|
+
- `hide(id)` also cancels a queued toast; `hideAll()` clears the queue and dismisses visible toasts.
|
|
164
|
+
- Raising `maxVisible` at runtime promotes queued toasts immediately.
|
|
165
|
+
- Generated ids are unique and never collide with ids you pass.
|
|
166
|
+
|
|
167
|
+
## Themes
|
|
168
|
+
|
|
169
|
+
```jsx
|
|
170
|
+
import { ToastProvider, createTheme } from 'react-native-toast-message-library';
|
|
171
|
+
|
|
172
|
+
<ToastProvider theme="dark" /> // 'light' | 'dark' | 'system'
|
|
173
|
+
|
|
174
|
+
<ToastProvider
|
|
175
|
+
theme={createTheme('dark', {
|
|
176
|
+
borderRadius: 4,
|
|
177
|
+
typography: { fontFamily: 'Inter' },
|
|
178
|
+
colors: { success: { background: '#052E1B', text: '#fff' } },
|
|
179
|
+
})}
|
|
180
|
+
/>
|
|
181
|
+
|
|
182
|
+
<ToastProvider theme={{ extends: 'dark', padding: 20 }} /> // plain object also works
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A theme object is deep-merged over the light theme (or the one named by `extends`). See `src/themes/light.js`
|
|
186
|
+
for every key (`colors.<type>.{background,text,border,icon,iconText}`, `borderRadius`, `borderWidth`,
|
|
187
|
+
`padding`, `spacing`, `iconSize`, `typography`, `shadow`, `actionColor`, `closeColor`, `progressTrack`).
|
|
188
|
+
|
|
189
|
+
## Custom icons and content
|
|
190
|
+
|
|
191
|
+
```jsx
|
|
192
|
+
toast.show({
|
|
193
|
+
type: 'custom',
|
|
194
|
+
message: 'Deployed',
|
|
195
|
+
icon: ({ color, size }) => <RocketIcon color={color} size={size} />,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
toast.show({
|
|
199
|
+
type: 'custom',
|
|
200
|
+
render: ({ hide, colors }) => (
|
|
201
|
+
<Text style={{ color: colors.text }} onPress={hide}>Fully custom — tap to close</Text>
|
|
202
|
+
),
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Action buttons
|
|
207
|
+
|
|
208
|
+
```jsx
|
|
209
|
+
toast.show({
|
|
210
|
+
type: 'success',
|
|
211
|
+
message: 'Message deleted',
|
|
212
|
+
duration: 6000,
|
|
213
|
+
action: { label: 'Undo' },
|
|
214
|
+
onAction: () => restoreMessage(),
|
|
215
|
+
onDismiss: reason => reason === 'timeout' && permanentlyDelete(),
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Animations and reduced motion
|
|
220
|
+
|
|
221
|
+
All animations animate only `opacity` and `transform`, so they use the native driver. The progress bar
|
|
222
|
+
animates `width` (a layout property) and therefore runs on the JS thread; it is a 3 dp element. When the
|
|
223
|
+
system "reduce motion" setting is on, `slide` and `zoom` are replaced by `fade`; `none` stays `none`.
|
|
224
|
+
After a toast exits, toasts stacked below it move into place without animation.
|
|
225
|
+
|
|
226
|
+
## Accessibility
|
|
227
|
+
|
|
228
|
+
- Each toast is an `alert` (or a `button` when `onPress` is set) with a label such as
|
|
229
|
+
`"Error. Disk full"`, so the type is available to screen readers and not only conveyed by colour/icon.
|
|
230
|
+
The decorative icon glyph is hidden from assistive technology.
|
|
231
|
+
- Android: `accessibilityLiveRegion="polite"`. iOS: announcements via `announceForAccessibility`, throttled
|
|
232
|
+
to one per 1.5 s so a burst of queued toasts does not interrupt VoiceOver repeatedly.
|
|
233
|
+
- Close and action buttons are labelled and have ≥ 44 dp touch targets (including `hitSlop`).
|
|
234
|
+
- Text scales with the system font size up to 1.6×.
|
|
235
|
+
|
|
236
|
+
**Not covered:** the screen-reader behaviour has been implemented following platform guidelines but has not
|
|
237
|
+
been tested on real TalkBack/VoiceOver. Toasts that disappear on a timer can be missed by assistive
|
|
238
|
+
technology users; use `duration: 0` plus a close button for important messages.
|
|
239
|
+
|
|
240
|
+
## TypeScript
|
|
241
|
+
|
|
242
|
+
Declarations ship in `types/index.d.ts` (checked by `npm run typecheck`).
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { useToast, type ToastOptions } from 'react-native-toast-message-library';
|
|
246
|
+
|
|
247
|
+
const opts: ToastOptions = { type: 'info', message: 'Hi', position: 'bottom-right' };
|
|
248
|
+
useToast().show(opts);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Troubleshooting
|
|
252
|
+
|
|
253
|
+
| Symptom | Cause / fix |
|
|
254
|
+
| --- | --- |
|
|
255
|
+
| `useToast() was called outside of a <ToastProvider>` | Render the component below the provider (usually in `App.js`). |
|
|
256
|
+
| `Toast.* was called but no <ToastProvider global> is mounted` | Add the `global` prop, or use `useToast()`. |
|
|
257
|
+
| `Invalid position "…"` / other `TypeError` | Fix the option; the message lists the allowed values. |
|
|
258
|
+
| Toast hidden behind a modal / native screen | See Limitations. |
|
|
259
|
+
| Toast under the notch / status bar | Install `react-native-safe-area-context` and use `SafeAreaProvider`, or pass `insets`. |
|
|
260
|
+
| Nothing vibrates with `haptics` | Android needs `<uses-permission android:name="android.permission.VIBRATE" />`. |
|
|
261
|
+
| Layout broken because of the wrapper View | Pass `style` to the provider. |
|
|
262
|
+
|
|
263
|
+
## Limitations
|
|
264
|
+
|
|
265
|
+
- Toasts render inside the React tree. They appear **under** React Native `Modal`s and native-presented
|
|
266
|
+
screens (e.g. native-stack modal presentations). Render another `ToastProvider` inside such a modal.
|
|
267
|
+
- `haptics` uses the built-in `Vibration` API: Android honours short vibrations (needs the `VIBRATE`
|
|
268
|
+
permission), iOS ignores the duration and vibrates for a fixed ~0.4 s. It is not a Taptic Engine integration.
|
|
269
|
+
- Swipe-to-dismiss uses `PanResponder` (horizontal swipes only). It is implemented but **not covered by
|
|
270
|
+
automated tests** (gesture simulation is not available in the Jest setup); verify it on a device.
|
|
271
|
+
- Visual rendering, animation smoothness, and screen-reader behaviour have not been verified on a device or emulator.
|
|
272
|
+
- Toasts below a removed toast jump into position (no layout animation).
|
|
273
|
+
- `onDismiss` fires when dismissal starts, not when the exit animation ends.
|
|
274
|
+
|
|
275
|
+
## Development
|
|
276
|
+
|
|
277
|
+
The repository root is both the library and a React Native CLI demo app (no Expo).
|
|
278
|
+
|
|
279
|
+
```sh
|
|
280
|
+
npm install
|
|
281
|
+
npm test # Jest
|
|
282
|
+
npm run lint
|
|
283
|
+
npm run typecheck
|
|
284
|
+
npm pack --dry-run # inspect the published file list
|
|
285
|
+
|
|
286
|
+
npm start # Metro (terminal 1)
|
|
287
|
+
npm run android # terminal 2; needs Android SDK + JDK
|
|
288
|
+
npm run ios # macOS + Xcode; cd ios && bundle exec pod install first
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Layout: `src/` (the library: `manager` = queue + timers, `components`, `animations`, `themes`, `utils`),
|
|
292
|
+
`types/` (TypeScript declarations), `__tests__/`, and `App.js` + `demo/` (the demo app).
|
|
293
|
+
|
|
294
|
+
## Publishing
|
|
295
|
+
|
|
296
|
+
```sh
|
|
297
|
+
# 0. one-time: set repository/bugs/homepage in package.json
|
|
298
|
+
npm ci
|
|
299
|
+
npm run validate # lint + typecheck + tests + npm pack --dry-run
|
|
300
|
+
npm pack --dry-run # should list only src/, types/, README.md, CHANGELOG.md, LICENSE, package.json
|
|
301
|
+
|
|
302
|
+
npm login # interactive; never commit tokens
|
|
303
|
+
npm publish # scoped public packages: npm publish --access public (already set in publishConfig)
|
|
304
|
+
|
|
305
|
+
npm view <your-package-name> version # verify
|
|
306
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Future releases (semantic versioning: patch = fixes, minor = backwards-compatible features, major = breaking):
|
|
310
|
+
|
|
311
|
+
```sh
|
|
312
|
+
# update CHANGELOG.md first
|
|
313
|
+
npm version patch # or minor / major — bumps package.json and creates the git tag
|
|
314
|
+
npm publish
|
|
315
|
+
git push --follow-tags
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Release checklist
|
|
319
|
+
|
|
320
|
+
- [ ] `npm view react-native-toast-message-library` still returns 404 (name free)
|
|
321
|
+
- [ ] `npm run validate` passes
|
|
322
|
+
- [ ] Example app run on an Android device/emulator **and** an iOS simulator (swipe, queue, themes, a11y spot check)
|
|
323
|
+
- [ ] `CHANGELOG.md` entry dated; version bumped
|
|
324
|
+
- [ ] `repository`, `bugs`, `homepage` fields filled in `package.json`
|
|
325
|
+
- [ ] `npm pack --dry-run` file list reviewed
|
|
326
|
+
- [ ] Published, verified with `npm view`, tag pushed
|
|
327
|
+
|
|
328
|
+
## Contributing
|
|
329
|
+
|
|
330
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
331
|
+
|
|
332
|
+
## License
|
|
333
|
+
|
|
334
|
+
MIT © Denish Dabhi
|
package/package.json
CHANGED
|
@@ -1,6 +1,84 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-toast-message-library",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight, customizable toast notifications for React Native: queue, themes, animations, actions, swipe-to-dismiss and accessibility. Pure JavaScript, no native modules.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"react-native": "src/index.js",
|
|
7
|
+
"types": "types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./types/index.d.ts",
|
|
11
|
+
"react-native": "./src/index.js",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"types/index.d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"android": "react-native run-android",
|
|
26
|
+
"ios": "react-native run-ios",
|
|
27
|
+
"start": "react-native start",
|
|
28
|
+
"test": "jest",
|
|
29
|
+
"test:coverage": "jest --coverage",
|
|
30
|
+
"lint": "eslint src demo __tests__ App.js index.js --ext .js",
|
|
31
|
+
"typecheck": "tsc --noEmit -p types/tsconfig.json",
|
|
32
|
+
"validate": "npm run lint && npm run typecheck && npm test && npm pack --dry-run",
|
|
33
|
+
"prepublishOnly": "npm run lint && npm run typecheck && npm test"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"react-native",
|
|
37
|
+
"toast",
|
|
38
|
+
"notification",
|
|
39
|
+
"snackbar",
|
|
40
|
+
"ios",
|
|
41
|
+
"android",
|
|
42
|
+
"toast-notification"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"author": "Denish Dabhi <dabhidenish57@gmail.com>",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": ">=18.0.0",
|
|
51
|
+
"react-native": ">=0.72.0",
|
|
52
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"react-native-safe-area-context": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@babel/core": "^7.25.2",
|
|
61
|
+
"@babel/preset-env": "^7.25.3",
|
|
62
|
+
"@babel/runtime": "^7.25.0",
|
|
63
|
+
"@react-native-community/cli": "20.2.0",
|
|
64
|
+
"@react-native-community/cli-platform-android": "20.2.0",
|
|
65
|
+
"@react-native-community/cli-platform-ios": "20.2.0",
|
|
66
|
+
"@react-native/babel-preset": "0.88.0",
|
|
67
|
+
"@react-native/eslint-config": "0.88.0",
|
|
68
|
+
"@react-native/jest-preset": "0.88.0",
|
|
69
|
+
"@react-native/metro-config": "0.88.0",
|
|
70
|
+
"@testing-library/react-native": "^14.1.0",
|
|
71
|
+
"@types/react": "^19.2.0",
|
|
72
|
+
"eslint": "^8.19.0",
|
|
73
|
+
"jest": "^29.6.3",
|
|
74
|
+
"prettier": "2.8.8",
|
|
75
|
+
"react": "19.3.0",
|
|
76
|
+
"react-native": "0.88.0",
|
|
77
|
+
"react-native-safe-area-context": "^5.5.2",
|
|
78
|
+
"test-renderer": "^1.0.0",
|
|
79
|
+
"typescript": "^6.0.3"
|
|
80
|
+
},
|
|
81
|
+
"engines": {
|
|
82
|
+
"node": ">= 22.11.0"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Animated, Easing } from 'react-native';
|
|
2
|
+
|
|
3
|
+
const SLIDE_DISTANCE = 64;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every animation is driven by a single Animated.Value `progress` that goes
|
|
7
|
+
* 0 (hidden) -> 1 (shown). Only opacity and transforms are animated, so the
|
|
8
|
+
* native driver can be used and no layout pass is needed per frame.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function resolveAnimation(animation, reducedMotion) {
|
|
12
|
+
if (reducedMotion && (animation === 'slide' || animation === 'zoom')) {
|
|
13
|
+
// Movement and scaling are replaced by a plain cross-fade.
|
|
14
|
+
return 'fade';
|
|
15
|
+
}
|
|
16
|
+
return animation;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function slideOffset(position) {
|
|
20
|
+
if (position.startsWith('top')) {
|
|
21
|
+
return -SLIDE_DISTANCE;
|
|
22
|
+
}
|
|
23
|
+
if (position.startsWith('bottom')) {
|
|
24
|
+
return SLIDE_DISTANCE;
|
|
25
|
+
}
|
|
26
|
+
return SLIDE_DISTANCE / 2;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getAnimatedStyle(animation, progress, position) {
|
|
30
|
+
switch (animation) {
|
|
31
|
+
case 'fade':
|
|
32
|
+
return { opacity: progress };
|
|
33
|
+
case 'slide':
|
|
34
|
+
return {
|
|
35
|
+
opacity: progress,
|
|
36
|
+
transform: [
|
|
37
|
+
{
|
|
38
|
+
translateY: progress.interpolate({
|
|
39
|
+
inputRange: [0, 1],
|
|
40
|
+
outputRange: [slideOffset(position), 0],
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
case 'zoom':
|
|
46
|
+
return {
|
|
47
|
+
opacity: progress,
|
|
48
|
+
transform: [
|
|
49
|
+
{
|
|
50
|
+
scale: progress.interpolate({
|
|
51
|
+
inputRange: [0, 1],
|
|
52
|
+
outputRange: [0.85, 1],
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
default:
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Starts an enter (toValue 1) or exit (toValue 0) animation and returns the
|
|
64
|
+
* Animated handle so the caller can stop it. With animation 'none' or a zero
|
|
65
|
+
* duration the value is set immediately and `onEnd({ finished: true })` is
|
|
66
|
+
* called synchronously.
|
|
67
|
+
*/
|
|
68
|
+
export function runToastAnimation(progress, toValue, options, onEnd) {
|
|
69
|
+
const { animation, duration } = options;
|
|
70
|
+
if (animation === 'none' || !(duration > 0)) {
|
|
71
|
+
progress.setValue(toValue);
|
|
72
|
+
if (onEnd) {
|
|
73
|
+
onEnd({ finished: true });
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const handle = Animated.timing(progress, {
|
|
78
|
+
toValue,
|
|
79
|
+
duration,
|
|
80
|
+
easing: toValue === 1 ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),
|
|
81
|
+
useNativeDriver: true,
|
|
82
|
+
});
|
|
83
|
+
handle.start(onEnd);
|
|
84
|
+
return handle;
|
|
85
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export default function ToastAction({
|
|
5
|
+
label,
|
|
6
|
+
accessibilityLabel,
|
|
7
|
+
color,
|
|
8
|
+
onPress,
|
|
9
|
+
fontFamily,
|
|
10
|
+
}) {
|
|
11
|
+
return (
|
|
12
|
+
<Pressable
|
|
13
|
+
accessibilityRole="button"
|
|
14
|
+
accessibilityLabel={accessibilityLabel || label}
|
|
15
|
+
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
|
16
|
+
onPress={onPress}
|
|
17
|
+
style={({ pressed }) => [styles.button, { opacity: pressed ? 0.6 : 1 }]}
|
|
18
|
+
>
|
|
19
|
+
<Text
|
|
20
|
+
maxFontSizeMultiplier={1.4}
|
|
21
|
+
style={[styles.label, { color }, fontFamily && { fontFamily }]}
|
|
22
|
+
>
|
|
23
|
+
{label}
|
|
24
|
+
</Text>
|
|
25
|
+
</Pressable>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const styles = StyleSheet.create({
|
|
30
|
+
button: {
|
|
31
|
+
minHeight: 36,
|
|
32
|
+
minWidth: 44,
|
|
33
|
+
paddingHorizontal: 10,
|
|
34
|
+
marginLeft: 8,
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
},
|
|
38
|
+
label: { fontSize: 14, fontWeight: '700', textTransform: 'none' },
|
|
39
|
+
});
|