Skip to content

feat: Add Toast to React Aria Components #7783

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 21, 2025
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
20 changes: 20 additions & 0 deletions lib/css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

declare module 'react' {
interface CSSProperties {
viewTransitionName?: string,
viewTransitionClass?: string
}
}

export {};
19 changes: 19 additions & 0 deletions lib/viewTransitions.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

interface Document {
startViewTransition(fn: () => void): ViewTransition;
}

interface ViewTransition {
ready: Promise<void>;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These declarations allow us to avoid @ts-expect-error or @ts-ignore in documentation examples. document.startViewTransition is included in TS 5.6 so this should go away when we update. Not sure about the React one.

8 changes: 4 additions & 4 deletions packages/@react-aria/toast/docs/useToast.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ keywords: [toast, notifications, alert, aria]
packageData={packageData}
componentNames={['useToastRegion', 'useToast']}
sourceData={[
{type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/alert/'}
{type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/'}
]} />

## API
Expand All @@ -45,17 +45,17 @@ keywords: [toast, notifications, alert, aria]

## Features

There is no built in way to toast notifications in HTML. <TypeLink links={docs.links} type={docs.exports.useToastRegion} /> and <TypeLink links={docs.links} type={docs.exports.useToast} /> help achieve accessible toasts that can be styled as needed.
There is no built in way to display toast notifications in HTML. <TypeLink links={docs.links} type={docs.exports.useToastRegion} /> and <TypeLink links={docs.links} type={docs.exports.useToast} /> help achieve accessible toasts that can be styled as needed.

* **Accessible** – Toasts follow the [ARIA alert pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alert/). They are rendered in a [landmark region](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/), which keyboard and screen reader users can easily jump to when an alert is announced.
* **Accessible** – Toasts follow the [ARIA alertdialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/). They are rendered in a [landmark region](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/), which keyboard and screen reader users can easily jump to when an alert is announced.
* **Focus management** – When a toast unmounts, focus is moved to the next toast if any. Otherwise, focus is restored to where it was before navigating to the toast region.
* **Priority queue** – Toasts are displayed according to a priority queue, displaying a configurable number of toasts at a time. The queue can either be owned by a provider component, or global.

## Anatomy

<Anatomy role="img" aria-label="Toast anatomy diagram, showing the toast's title and close button within the toast region." />

A toast region is an ARIA landmark region labeled "Notifications" by default. A toast region contains one or more visible toasts, in priority order. When the limit is reached, additional toasts are queued until the user dismisses one. Each toast is an ARIA alert element, containing the content of the notification and a close button.
A toast region is an [ARIA landmark region](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/) labeled "Notifications" by default. A toast region contains one or more visible toasts, in priority order. When the limit is reached, additional toasts are queued until the user dismisses one. Each toast is a non-modal ARIA [alertdialog](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/), containing the content of the notification and a close button.

Landmark regions including the toast container can be navigated using the keyboard by pressing the <Keyboard>F6</Keyboard> key to move forward, and the <Keyboard>Shift</Keyboard> + <Keyboard>F6</Keyboard> key to move backward. This provides an easy way for keyboard users to jump to the toasts from anywhere in the app. When the last toast is closed, keyboard focus is restored.

Expand Down
15 changes: 4 additions & 11 deletions packages/@react-spectrum/toast/src/ToastContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,13 @@ export interface SpectrumToastOptions extends Omit<ToastOptions, 'priority'>, DO

type CloseFunction = () => void;

function wrapInViewTransition<R>(fn: () => R): R {
function wrapInViewTransition(fn: () => void): void {
if ('startViewTransition' in document) {
let result: R;
// @ts-expect-error
document.startViewTransition(() => {
flushSync(() => {
result = fn();
});
flushSync(fn);
}).ready.catch(() => {});
// @ts-ignore
return result;
} else {
return fn();
fn();
}
}

Expand Down Expand Up @@ -141,8 +135,7 @@ export function ToastContainer(props: SpectrumToastContainerProps): ReactElement
key={toast.key}
className={classNames(toastContainerStyles, 'spectrum-ToastContainer-listitem')}
style={{
// @ts-expect-error
viewTransitionName: `_${toast.key.slice(2)}`,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this hack into the hooks themselves to toast.key is always a valid CSS identifier.

viewTransitionName: toast.key,
viewTransitionClass: classNames(
toastContainerStyles,
'toast',
Expand Down
14 changes: 7 additions & 7 deletions packages/@react-stately/toast/src/useToastState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ToastStateProps {
/** The maximum number of toasts to display at a time. */
maxVisibleToasts?: number,
/** Function to wrap updates in (i.e. document.startViewTransition()). */
wrapUpdate?: <R>(fn: () => R) => R
wrapUpdate?: (fn: () => void) => void
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't actually use the return value of this function so void is fine here

}

export interface ToastOptions {
Expand Down Expand Up @@ -88,20 +88,20 @@ export class ToastQueue<T> {
private queue: QueuedToast<T>[] = [];
private subscriptions: Set<() => void> = new Set();
private maxVisibleToasts: number;
private wrapUpdate?: <R>(fn: () => R) => R;
private wrapUpdate?: (fn: () => void) => void;
/** The currently visible toasts. */
visibleToasts: QueuedToast<T>[] = [];

constructor(options?: ToastStateProps) {
this.maxVisibleToasts = options?.maxVisibleToasts ?? 1;
this.maxVisibleToasts = options?.maxVisibleToasts ?? Infinity;
this.wrapUpdate = options?.wrapUpdate;
}

private runWithWrapUpdate<R>(fn: () => R): R {
private runWithWrapUpdate(fn: () => void): void {
if (this.wrapUpdate) {
return this.wrapUpdate(fn);
this.wrapUpdate(fn);
} else {
return fn();
fn();
}
}

Expand All @@ -113,7 +113,7 @@ export class ToastQueue<T> {

/** Adds a new toast to the queue. */
add(content: T, options: ToastOptions = {}) {
let toastKey = Math.random().toString(36);
let toastKey = '_' + Math.random().toString(36).slice(2);
let toast: QueuedToast<T> = {
...options,
content,
Expand Down
Loading