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 docs/reference/generated/toast-provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"toastManager": {
"type": "ToastManager",
"description": "A global manager for toasts to use outside of a React component.",
"detailedType": "| {\n subscribe: (\n listener: (data: ToastManagerEvent) => void,\n ) => () => void\n add: (options: ToastManagerAddOptions<any>) => string\n close: (id: string) => void\n update: (\n id: string,\n updates: ToastManagerUpdateOptions<any>,\n ) => void\n promise: (\n promiseValue: Promise<Value>,\n options: ToastManagerPromiseOptions<Value, any>,\n ) => Promise<Value>\n }\n| undefined"
"detailedType": "| {\n subscribe: (\n listener: (data: ToastManagerEvent) => void,\n ) => () => void\n add: (options: ToastManagerAddOptions<any>) => string\n close: (id: string | undefined) => void\n update: (\n id: string,\n updates: ToastManagerUpdateOptions<any>,\n ) => void\n promise: (\n promiseValue: Promise<Value>,\n options: ToastManagerPromiseOptions<Value, any>,\n ) => Promise<Value>\n }\n| undefined"
},
"timeout": {
"type": "number",
Expand Down
11 changes: 9 additions & 2 deletions docs/src/app/(docs)/react/components/toast/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,9 @@ const toastManager = Toast.useToastManager();
description: 'Add a toast to the toast list.',
},
close: {
type: '(toastId: string) => void',
description: 'Closes and removes a toast from the toast list.',
type: '(toastId?: string) => void',
description:
'Closes and removes a toast from the toast list. If no ID is passed, all toasts will be closed.',
},
update: {
type: '(toastId: string, options: ToastManagerUpdateOptions) => void',
Expand Down Expand Up @@ -397,6 +398,12 @@ Closes the toast, removing it from the toast list after any animations complete.
toastManager.close(toastId);
```

Or you can close all toasts at once by not passing an ID:

```jsx title="Close all toasts"
toastManager.close();
```

### `promise` method

Creates an asynchronous toast with three possible states: `loading`, `success`, and `error`.
Expand Down
44 changes: 44 additions & 0 deletions packages/react/src/toast/createToastManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -628,5 +628,49 @@ describe.skipIf(!isJSDOM)('createToastManager', () => {

expect(screen.queryByTestId('title')).to.equal(null);
});

it('closes all toasts', async () => {
const toastManager = Toast.createToastManager();

function add() {
toastManager.add({ title: 'title' });
}

function close() {
toastManager.close();
}

function Buttons() {
return (
<React.Fragment>
<button type="button" onClick={add}>
add
</button>
<button type="button" onClick={close}>
close
</button>
</React.Fragment>
);
}

await render(
<Toast.Provider toastManager={toastManager}>
<Toast.Viewport>
<List />
</Toast.Viewport>
<Buttons />
</Toast.Provider>,
);

const button = screen.getByRole('button', { name: 'add' });
Array.from({ length: 5 }).forEach(() => {
fireEvent.click(button);
});

const closeButton = screen.getByRole('button', { name: 'close' });
fireEvent.click(closeButton);

expect(screen.queryByTestId('title')).to.equal(null);
});
});
});
4 changes: 2 additions & 2 deletions packages/react/src/toast/createToastManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function createToastManager<Data extends object = any>(): ToastManager<Da
return id;
},

close(id: string): void {
close(id?: string): void {
emit({
action: 'close',
options: { id },
Expand Down Expand Up @@ -84,7 +84,7 @@ export function createToastManager<Data extends object = any>(): ToastManager<Da
export interface ToastManager<Data extends object = any> {
' subscribe': (listener: (data: ToastManagerEvent) => void) => () => void;
add: <T extends Data = Data>(options: ToastManagerAddOptions<T>) => string;
close: (id: string) => void;
close: (id?: string) => void;
update: <T extends Data = Data>(id: string, updates: ToastManagerUpdateOptions<T>) => void;
promise: <Value, T extends Data = Data>(
promiseValue: Promise<Value>,
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/toast/provider/ToastProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const ToastProvider: React.FC<ToastProvider.Props> = function ToastProvid
store.promiseToast(options.promise, options);
} else if (action === 'update' && id) {
store.updateToast(id, options);
} else if (action === 'close' && id) {
} else if (action === 'close') {
store.closeToast(id);
} else {
store.addToast(options);
Expand Down
50 changes: 38 additions & 12 deletions packages/react/src/toast/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,36 @@ export class ToastStore extends ReactStore<State, {}, typeof selectors> {
}
};

closeToast = (toastId: string) => {
const toast = selectors.toast(this.state, toastId);
toast?.onClose?.();
closeToast = (toastId?: string) => {
const closeAll = toastId === undefined;
const toast = closeAll ? undefined : selectors.toast(this.state, toastId);
if (!closeAll && !toast) {
return;
}

const { limit, toasts } = this.state;

if (closeAll) {
toasts.forEach((item) => {
item.onClose?.();
});
this.timers.forEach((timer) => {
timer.timeout?.clear();
});
this.timers.clear();
} else {
toast?.onClose?.();

const timer = this.timers.get(toastId);
if (timer?.timeout) {
timer.timeout.clear();
this.timers.delete(toastId);
}
}

let activeIndex = 0;
const newToasts = toasts.map((item) => {
if (item.id === toastId) {
if (closeAll || item.id === toastId) {
return { ...item, transitionStatus: 'ending' as const, height: 0 };
}
if (item.transitionStatus === 'ending') {
Expand All @@ -252,14 +273,14 @@ export class ToastStore extends ReactStore<State, {}, typeof selectors> {
return item.limited !== isLimited ? { ...item, limited: isLimited } : item;
});

const timer = this.timers.get(toastId);
if (timer && timer.timeout) {
timer.timeout.clear();
this.timers.delete(toastId);
}

this.handleFocusManagement(toastId);
this.setToasts(newToasts);

const updates: Partial<State> = { toasts: newToasts };
if (closeAll || toasts.length === 1) {
updates.hovering = false;
updates.focused = false;
}
this.update(updates);
};

promiseToast = <Value, Data extends object>(
Expand Down Expand Up @@ -381,7 +402,7 @@ export class ToastStore extends ReactStore<State, {}, typeof selectors> {
this.update(updates);
}

private handleFocusManagement(toastId: string) {
private handleFocusManagement(toastId: string | undefined) {
const activeEl = activeElement(ownerDocument(this.state.viewport));
if (
!this.state.viewport ||
Expand All @@ -391,6 +412,11 @@ export class ToastStore extends ReactStore<State, {}, typeof selectors> {
return;
}

if (toastId === undefined) {
this.restoreFocusToPrevElement();
return;
}

const toasts = selectors.toasts(this.state);
const currentIndex = selectors.toastIndex(this.state, toastId);
let nextToast: ToastObject<any> | null = null;
Expand Down
45 changes: 45 additions & 0 deletions packages/react/src/toast/useToastManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,51 @@ describe.skipIf(!isJSDOM)('useToast', () => {

expect(screen.queryByTestId('root')).to.equal(null);
});

it('closes all toasts', async () => {
function AddButton() {
const { add, close } = useToastManager();
return (
<React.Fragment>
<button
onClick={() => {
add({ title: 'test' });
}}
>
add
</button>
<button
onClick={() => {
close();
}}
>
close
</button>
</React.Fragment>
);
}

await render(
<Toast.Provider>
<Toast.Viewport>
<CustomList />
</Toast.Viewport>
<AddButton />
</Toast.Provider>,
);

const addButton = screen.getByRole('button', { name: 'add' });
Array.from({ length: 5 }).forEach(() => {
fireEvent.click(addButton);
});

expect(screen.getAllByTestId('root')).to.have.length(5);

const closeButton = screen.getByRole('button', { name: 'close' });
fireEvent.click(closeButton);

expect(screen.queryByTestId('root')).to.equal(null);
});
});

describe('prop: limit', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/toast/useToastManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export interface ToastManagerPositionerProps extends Omit<
export interface UseToastManagerReturnValue<Data extends object = any> {
toasts: ToastObject<Data>[];
add: <T extends Data = Data>(options: ToastManagerAddOptions<T>) => string;
close: (toastId: string) => void;
close: (toastId?: string) => void;
update: <T extends Data = Data>(toastId: string, options: ToastManagerUpdateOptions<T>) => void;
promise: <Value, T extends Data = Data>(
promise: Promise<Value>,
Expand Down
Loading