Skip to content

Commit

Permalink
Merge pull request #89 from heyfuaad/main
Browse files Browse the repository at this point in the history
Feature: configurable dismiss timer duration
  • Loading branch information
timolins authored Dec 30, 2024
2 parents 0763f76 + 525aee8 commit c3d6739
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 54 deletions.
21 changes: 21 additions & 0 deletions site/pages/docs/toast.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ toast('Hello World', {
role: 'status',
'aria-live': 'polite',
},

// Additional Configuration
removeDelay: 1000,
});
```

Expand Down Expand Up @@ -177,6 +180,24 @@ toast.dismiss();

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

#### Configure remove delay

```js
toast.success('Successfully created!', { removeDelay: 500 });
```

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.

Or, for all toasts, using the Toaster like so:

```js
<Toaster
toastOptions={{
removeDelay: 500,
}}
/>
```

#### Remove toasts instantly

```js
Expand Down
1 change: 1 addition & 0 deletions site/pages/docs/toaster.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This component will render all toasts. Alternatively you can create own renderer
// Define default options
className: '',
duration: 5000,
removeDelay: 1000,
style: {
background: '#363636',
color: '#fff',
Expand Down
50 changes: 8 additions & 42 deletions src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,33 +48,6 @@ interface State {
pausedAt: number | undefined;
}

const toastTimeouts = new Map<Toast['id'], ReturnType<typeof setTimeout>>();

export const TOAST_EXPIRE_DISMISS_DELAY = 1000;

const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}

const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: ActionType.REMOVE_TOAST,
toastId: toastId,
});
}, TOAST_EXPIRE_DISMISS_DELAY);

toastTimeouts.set(toastId, timeout);
};

const clearFromRemoveQueue = (toastId: string) => {
const timeout = toastTimeouts.get(toastId);
if (timeout) {
clearTimeout(timeout);
}
};

export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case ActionType.ADD_TOAST:
Expand All @@ -84,15 +57,12 @@ export const reducer = (state: State, action: Action): State => {
};

case ActionType.UPDATE_TOAST:
// ! Side effects !
if (action.toast.id) {
clearFromRemoveQueue(action.toast.id);
}

return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
t.id === action.toast.id
? { ...t, dismissed: false, visible: true, ...action.toast }
: t
),
};

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

// ! Side effects ! - This could be execrated into a dismissToast() action, but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}

return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
dismissed: true,
visible: false,
}
: t
Expand Down Expand Up @@ -194,6 +156,10 @@ export const useStore = (toastOptions: DefaultToastOptions = {}): State => {
...toastOptions,
...toastOptions[t.type],
...t,
removeDelay:
t.removeDelay ||
toastOptions[t.type]?.removeDelay ||
toastOptions?.removeDelay,
duration:
t.duration ||
toastOptions[t.type]?.duration ||
Expand Down
1 change: 1 addition & 0 deletions src/core/toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const createToast = (
): Toast => ({
createdAt: Date.now(),
visible: true,
dismissed: false,
type,
ariaProps: {
role: 'status',
Expand Down
3 changes: 3 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface Toast {
duration?: number;
pauseDuration: number;
position?: ToastPosition;
removeDelay?: number;

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

createdAt: number;
visible: boolean;
dismissed: boolean;
height?: number;
}

Expand All @@ -65,6 +67,7 @@ export type ToastOptions = Partial<
| 'style'
| 'position'
| 'iconTheme'
| 'removeDelay'
>
>;

Expand Down
36 changes: 36 additions & 0 deletions src/core/use-toaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ const startPause = () => {
});
};

const toastTimeouts = new Map<Toast['id'], ReturnType<typeof setTimeout>>();

export const REMOVE_DELAY = 1000;

const addToRemoveQueue = (toastId: string, removeDelay = REMOVE_DELAY) => {
if (toastTimeouts.has(toastId)) {
return;
}

const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: ActionType.REMOVE_TOAST,
toastId: toastId,
});
}, removeDelay);

toastTimeouts.set(toastId, timeout);
};

export const useToaster = (toastOptions?: DefaultToastOptions) => {
const { toasts, pausedAt } = useStore(toastOptions);

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

useEffect(() => {
// Add dismissed toasts to remove queue
toasts.forEach((toast) => {
if (toast.dismissed) {
addToRemoveQueue(toast.id, toast.removeDelay);
} else {
// If toast becomes visible again, remove it from the queue
const timeout = toastTimeouts.get(toast.id);
if (timeout) {
clearTimeout(timeout);
toastTimeouts.delete(toast.id);
}
}
});
}, [toasts]);

return {
toasts,
handlers: {
Expand Down
26 changes: 14 additions & 12 deletions test/toast.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
} from '@testing-library/react';

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

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

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

waitTime(TOAST_EXPIRE_DISMISS_DELAY);
waitTime(REMOVE_DELAY);

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

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

waitTime(TOAST_DURATION + TOAST_EXPIRE_DISMISS_DELAY);
waitTime(TOAST_DURATION);

waitTime(REMOVE_DELAY);

expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
});
Expand Down Expand Up @@ -211,7 +214,6 @@ test('different toasts types with dismiss', async () => {
icon: <span>ICON</span>,
});
});

let loadingToastId: string;
act(() => {
loadingToastId = toast.loading('Loading!');
Expand All @@ -223,25 +225,24 @@ test('different toasts types with dismiss', async () => {
expect(screen.queryByText('✅')).toBeInTheDocument();
expect(screen.queryByText('ICON')).toBeInTheDocument();

const successDismissTime =
defaultTimeouts.success + TOAST_EXPIRE_DISMISS_DELAY;
waitTime(defaultTimeouts.success);

waitTime(successDismissTime);
waitTime(REMOVE_DELAY);

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

waitTime(
defaultTimeouts.error + TOAST_EXPIRE_DISMISS_DELAY - successDismissTime
);
waitTime(defaultTimeouts.error);

waitTime(REMOVE_DELAY);

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

act(() => {
toast.dismiss(loadingToastId);
});

waitTime(TOAST_EXPIRE_DISMISS_DELAY);
waitTime(REMOVE_DELAY);

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

fireEvent.mouseLeave(toastElement);

waitTime(2000);
waitTime(1000);
waitTime(1000);

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

0 comments on commit c3d6739

Please sign in to comment.