Skip to content

Commit c3d6739

Browse files
authoredDec 30, 2024··
Merge pull request #89 from heyfuaad/main
Feature: configurable dismiss timer duration
2 parents 0763f76 + 525aee8 commit c3d6739

File tree

7 files changed

+84
-54
lines changed

7 files changed

+84
-54
lines changed
 

‎site/pages/docs/toast.mdx

+21
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ toast('Hello World', {
3636
role: 'status',
3737
'aria-live': 'polite',
3838
},
39+
40+
// Additional Configuration
41+
removeDelay: 1000,
3942
});
4043
```
4144

@@ -177,6 +180,24 @@ toast.dismiss();
177180

178181
To remove toasts instantly without any animations, use `toast.remove`.
179182

183+
#### Configure remove delay
184+
185+
```js
186+
toast.success('Successfully created!', { removeDelay: 500 });
187+
```
188+
189+
By default, the remove operation is delayed by 1000ms. This is how long a toast should be kept in the DOM after being dismissed. It is used to play the exit animation. This duration (number in milliseconds) can be configured when calling the toast.
190+
191+
Or, for all toasts, using the Toaster like so:
192+
193+
```js
194+
<Toaster
195+
toastOptions={{
196+
removeDelay: 500,
197+
}}
198+
/>
199+
```
200+
180201
#### Remove toasts instantly
181202

182203
```js

‎site/pages/docs/toaster.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This component will render all toasts. Alternatively you can create own renderer
2424
// Define default options
2525
className: '',
2626
duration: 5000,
27+
removeDelay: 1000,
2728
style: {
2829
background: '#363636',
2930
color: '#fff',

‎src/core/store.ts

+8-42
Original file line numberDiff line numberDiff line change
@@ -48,33 +48,6 @@ interface State {
4848
pausedAt: number | undefined;
4949
}
5050

51-
const toastTimeouts = new Map<Toast['id'], ReturnType<typeof setTimeout>>();
52-
53-
export const TOAST_EXPIRE_DISMISS_DELAY = 1000;
54-
55-
const addToRemoveQueue = (toastId: string) => {
56-
if (toastTimeouts.has(toastId)) {
57-
return;
58-
}
59-
60-
const timeout = setTimeout(() => {
61-
toastTimeouts.delete(toastId);
62-
dispatch({
63-
type: ActionType.REMOVE_TOAST,
64-
toastId: toastId,
65-
});
66-
}, TOAST_EXPIRE_DISMISS_DELAY);
67-
68-
toastTimeouts.set(toastId, timeout);
69-
};
70-
71-
const clearFromRemoveQueue = (toastId: string) => {
72-
const timeout = toastTimeouts.get(toastId);
73-
if (timeout) {
74-
clearTimeout(timeout);
75-
}
76-
};
77-
7851
export const reducer = (state: State, action: Action): State => {
7952
switch (action.type) {
8053
case ActionType.ADD_TOAST:
@@ -84,15 +57,12 @@ export const reducer = (state: State, action: Action): State => {
8457
};
8558

8659
case ActionType.UPDATE_TOAST:
87-
// ! Side effects !
88-
if (action.toast.id) {
89-
clearFromRemoveQueue(action.toast.id);
90-
}
91-
9260
return {
9361
...state,
9462
toasts: state.toasts.map((t) =>
95-
t.id === action.toast.id ? { ...t, ...action.toast } : t
63+
t.id === action.toast.id
64+
? { ...t, dismissed: false, visible: true, ...action.toast }
65+
: t
9666
),
9767
};
9868

@@ -105,21 +75,13 @@ export const reducer = (state: State, action: Action): State => {
10575
case ActionType.DISMISS_TOAST:
10676
const { toastId } = action;
10777

108-
// ! Side effects ! - This could be execrated into a dismissToast() action, but I'll keep it here for simplicity
109-
if (toastId) {
110-
addToRemoveQueue(toastId);
111-
} else {
112-
state.toasts.forEach((toast) => {
113-
addToRemoveQueue(toast.id);
114-
});
115-
}
116-
11778
return {
11879
...state,
11980
toasts: state.toasts.map((t) =>
12081
t.id === toastId || toastId === undefined
12182
? {
12283
...t,
84+
dismissed: true,
12385
visible: false,
12486
}
12587
: t
@@ -194,6 +156,10 @@ export const useStore = (toastOptions: DefaultToastOptions = {}): State => {
194156
...toastOptions,
195157
...toastOptions[t.type],
196158
...t,
159+
removeDelay:
160+
t.removeDelay ||
161+
toastOptions[t.type]?.removeDelay ||
162+
toastOptions?.removeDelay,
197163
duration:
198164
t.duration ||
199165
toastOptions[t.type]?.duration ||

‎src/core/toast.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const createToast = (
2121
): Toast => ({
2222
createdAt: Date.now(),
2323
visible: true,
24+
dismissed: false,
2425
type,
2526
ariaProps: {
2627
role: 'status',

‎src/core/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface Toast {
3939
duration?: number;
4040
pauseDuration: number;
4141
position?: ToastPosition;
42+
removeDelay?: number;
4243

4344
ariaProps: {
4445
role: 'status' | 'alert';
@@ -51,6 +52,7 @@ export interface Toast {
5152

5253
createdAt: number;
5354
visible: boolean;
55+
dismissed: boolean;
5456
height?: number;
5557
}
5658

@@ -65,6 +67,7 @@ export type ToastOptions = Partial<
6567
| 'style'
6668
| 'position'
6769
| 'iconTheme'
70+
| 'removeDelay'
6871
>
6972
>;
7073

‎src/core/use-toaster.ts

+36
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,26 @@ const startPause = () => {
1616
});
1717
};
1818

19+
const toastTimeouts = new Map<Toast['id'], ReturnType<typeof setTimeout>>();
20+
21+
export const REMOVE_DELAY = 1000;
22+
23+
const addToRemoveQueue = (toastId: string, removeDelay = REMOVE_DELAY) => {
24+
if (toastTimeouts.has(toastId)) {
25+
return;
26+
}
27+
28+
const timeout = setTimeout(() => {
29+
toastTimeouts.delete(toastId);
30+
dispatch({
31+
type: ActionType.REMOVE_TOAST,
32+
toastId: toastId,
33+
});
34+
}, removeDelay);
35+
36+
toastTimeouts.set(toastId, timeout);
37+
};
38+
1939
export const useToaster = (toastOptions?: DefaultToastOptions) => {
2040
const { toasts, pausedAt } = useStore(toastOptions);
2141

@@ -84,6 +104,22 @@ export const useToaster = (toastOptions?: DefaultToastOptions) => {
84104
[toasts]
85105
);
86106

107+
useEffect(() => {
108+
// Add dismissed toasts to remove queue
109+
toasts.forEach((toast) => {
110+
if (toast.dismissed) {
111+
addToRemoveQueue(toast.id, toast.removeDelay);
112+
} else {
113+
// If toast becomes visible again, remove it from the queue
114+
const timeout = toastTimeouts.get(toast.id);
115+
if (timeout) {
116+
clearTimeout(timeout);
117+
toastTimeouts.delete(toast.id);
118+
}
119+
}
120+
});
121+
}, [toasts]);
122+
87123
return {
88124
toasts,
89125
handlers: {

‎test/toast.test.tsx

+14-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
} from '@testing-library/react';
99

1010
import toast, { resolveValue, Toaster, ToastIcon } from '../src';
11-
import { TOAST_EXPIRE_DISMISS_DELAY, defaultTimeouts } from '../src/core/store';
11+
import { defaultTimeouts } from '../src/core/store';
12+
import { REMOVE_DELAY } from '../src/core/use-toaster';
1213

1314
beforeEach(() => {
1415
// Tests should run in serial for improved isolation
@@ -70,7 +71,7 @@ test('close notification', async () => {
7071

7172
fireEvent.click(await screen.findByRole('button', { name: /close/i }));
7273

73-
waitTime(TOAST_EXPIRE_DISMISS_DELAY);
74+
waitTime(REMOVE_DELAY);
7475

7576
expect(screen.queryByText(/example/i)).not.toBeInTheDocument();
7677
});
@@ -180,7 +181,9 @@ test('error toast with custom duration', async () => {
180181

181182
expect(screen.queryByText(/error/i)).toBeInTheDocument();
182183

183-
waitTime(TOAST_DURATION + TOAST_EXPIRE_DISMISS_DELAY);
184+
waitTime(TOAST_DURATION);
185+
186+
waitTime(REMOVE_DELAY);
184187

185188
expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
186189
});
@@ -211,7 +214,6 @@ test('different toasts types with dismiss', async () => {
211214
icon: <span>ICON</span>,
212215
});
213216
});
214-
215217
let loadingToastId: string;
216218
act(() => {
217219
loadingToastId = toast.loading('Loading!');
@@ -223,25 +225,24 @@ test('different toasts types with dismiss', async () => {
223225
expect(screen.queryByText('✅')).toBeInTheDocument();
224226
expect(screen.queryByText('ICON')).toBeInTheDocument();
225227

226-
const successDismissTime =
227-
defaultTimeouts.success + TOAST_EXPIRE_DISMISS_DELAY;
228+
waitTime(defaultTimeouts.success);
228229

229-
waitTime(successDismissTime);
230+
waitTime(REMOVE_DELAY);
230231

231232
expect(screen.queryByText(/success/i)).not.toBeInTheDocument();
232233
expect(screen.queryByText(/error/i)).toBeInTheDocument();
233234

234-
waitTime(
235-
defaultTimeouts.error + TOAST_EXPIRE_DISMISS_DELAY - successDismissTime
236-
);
235+
waitTime(defaultTimeouts.error);
236+
237+
waitTime(REMOVE_DELAY);
237238

238239
expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
239240

240241
act(() => {
241242
toast.dismiss(loadingToastId);
242243
});
243244

244-
waitTime(TOAST_EXPIRE_DISMISS_DELAY);
245+
waitTime(REMOVE_DELAY);
245246

246247
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
247248
});
@@ -313,7 +314,8 @@ test('pause toast', async () => {
313314

314315
fireEvent.mouseLeave(toastElement);
315316

316-
waitTime(2000);
317+
waitTime(1000);
318+
waitTime(1000);
317319

318320
expect(toastElement).not.toBeInTheDocument();
319321
});

0 commit comments

Comments
 (0)
Please sign in to comment.