Skip to content

Commit 90b6fd4

Browse files
feat: Add Toast to React Aria Components (#7783)
* feat: Add Toast to React Aria Components * Update useToast docs to reflect new aria pattern * Apply suggestions from code review Co-authored-by: Robert Snow <[email protected]> * review updates --------- Co-authored-by: Robert Snow <[email protected]>
1 parent 4f47a68 commit 90b6fd4

File tree

13 files changed

+889
-23
lines changed

13 files changed

+889
-23
lines changed

lib/css.d.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
declare module 'react' {
14+
interface CSSProperties {
15+
viewTransitionName?: string,
16+
viewTransitionClass?: string
17+
}
18+
}
19+
20+
export {};

lib/viewTransitions.d.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
interface Document {
14+
startViewTransition(fn: () => void): ViewTransition;
15+
}
16+
17+
interface ViewTransition {
18+
ready: Promise<void>;
19+
}

packages/@react-aria/toast/docs/useToast.mdx

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ keywords: [toast, notifications, alert, aria]
3535
packageData={packageData}
3636
componentNames={['useToastRegion', 'useToast']}
3737
sourceData={[
38-
{type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/alert/'}
38+
{type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/'}
3939
]} />
4040

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

4646
## Features
4747

48-
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.
48+
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.
4949

50-
* **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.
50+
* **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.
5151
* **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.
5252
* **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.
5353

5454
## Anatomy
5555

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

58-
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.
58+
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.
5959

6060
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.
6161

packages/@react-spectrum/toast/src/ToastContainer.tsx

+4-11
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,13 @@ export interface SpectrumToastOptions extends Omit<ToastOptions, 'priority'>, DO
3939

4040
type CloseFunction = () => void;
4141

42-
function wrapInViewTransition<R>(fn: () => R): R {
42+
function wrapInViewTransition(fn: () => void): void {
4343
if ('startViewTransition' in document) {
44-
let result: R;
45-
// @ts-expect-error
4644
document.startViewTransition(() => {
47-
flushSync(() => {
48-
result = fn();
49-
});
45+
flushSync(fn);
5046
}).ready.catch(() => {});
51-
// @ts-ignore
52-
return result;
5347
} else {
54-
return fn();
48+
fn();
5549
}
5650
}
5751

@@ -141,8 +135,7 @@ export function ToastContainer(props: SpectrumToastContainerProps): ReactElement
141135
key={toast.key}
142136
className={classNames(toastContainerStyles, 'spectrum-ToastContainer-listitem')}
143137
style={{
144-
// @ts-expect-error
145-
viewTransitionName: `_${toast.key.slice(2)}`,
138+
viewTransitionName: toast.key,
146139
viewTransitionClass: classNames(
147140
toastContainerStyles,
148141
'toast',

packages/@react-stately/toast/src/useToastState.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface ToastStateProps {
1818
/** The maximum number of toasts to display at a time. */
1919
maxVisibleToasts?: number,
2020
/** Function to wrap updates in (i.e. document.startViewTransition()). */
21-
wrapUpdate?: <R>(fn: () => R) => R
21+
wrapUpdate?: (fn: () => void) => void
2222
}
2323

2424
export interface ToastOptions {
@@ -88,20 +88,20 @@ export class ToastQueue<T> {
8888
private queue: QueuedToast<T>[] = [];
8989
private subscriptions: Set<() => void> = new Set();
9090
private maxVisibleToasts: number;
91-
private wrapUpdate?: <R>(fn: () => R) => R;
91+
private wrapUpdate?: (fn: () => void) => void;
9292
/** The currently visible toasts. */
9393
visibleToasts: QueuedToast<T>[] = [];
9494

9595
constructor(options?: ToastStateProps) {
96-
this.maxVisibleToasts = options?.maxVisibleToasts ?? 1;
96+
this.maxVisibleToasts = options?.maxVisibleToasts ?? Infinity;
9797
this.wrapUpdate = options?.wrapUpdate;
9898
}
9999

100-
private runWithWrapUpdate<R>(fn: () => R): R {
100+
private runWithWrapUpdate(fn: () => void): void {
101101
if (this.wrapUpdate) {
102-
return this.wrapUpdate(fn);
102+
this.wrapUpdate(fn);
103103
} else {
104-
return fn();
104+
fn();
105105
}
106106
}
107107

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

114114
/** Adds a new toast to the queue. */
115115
add(content: T, options: ToastOptions = {}) {
116-
let toastKey = Math.random().toString(36);
116+
let toastKey = '_' + Math.random().toString(36).slice(2);
117117
let toast: QueuedToast<T> = {
118118
...options,
119119
content,

0 commit comments

Comments
 (0)