Skip to content

Commit 3cb1bce

Browse files
authored
docs: RAC Toast docs for new docs site (#9219)
* add toast starter for vanilla css * update to use tint and other color vars * tailwind example * fix centering * toast docs rough draft * split out toast animation code * im dumb, fix animation * double check colors * update to get controls working and copy changes * fix animation break due to multiple toast regions * fix flicker * review comments
1 parent f61e75c commit 3cb1bce

File tree

8 files changed

+696
-0
lines changed

8 files changed

+696
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
export interface MyToastContent {
3+
title: string;
4+
description?: string;
5+
timeout?: number
6+
}
7+
8+
// only added so we can have a component/type for the props we want to send to the actual toast example
9+
export function MyToast(props: MyToastContent) {
10+
return <div />;
11+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import {Layout} from '../../src/Layout';
2+
export default Layout;
3+
4+
import docs from 'docs:react-aria-components';
5+
import toastDocs from 'docs:./ExampleToast';
6+
import '../../tailwind/tailwind.css';
7+
import Anatomy from '@react-aria/toast/docs/toast-anatomy.svg';
8+
import {InlineAlert, Heading, Content} from '@react-spectrum/s2';
9+
10+
export const tags = ['notifications'];
11+
export const version = 'alpha';
12+
13+
# Toast
14+
15+
<PageDescription>{docs.exports.UNSTABLE_Toast.description}</PageDescription>
16+
17+
<ExampleSwitcher>
18+
```tsx render docs={toastDocs.exports.MyToast} links={toastDocs.links} props={['title', 'description', 'timeout']} initialProps={{title: 'Files uploaded', description: '3 files uploaded successfully.', timeout: 0}} type="vanilla" files={["starters/docs/src/Toast.tsx", "starters/docs/src/Toast.css"]}
19+
"use client";
20+
import {MyToastRegion, queue} from 'vanilla-starter/Toast';
21+
import {Button} from 'vanilla-starter/Button';
22+
23+
function Example(props) {
24+
return (
25+
<div>
26+
<MyToastRegion />
27+
<Button onPress={() => queue.add(
28+
{
29+
title: props.title || 'Files uploaded',
30+
description: props.description || '3 files uploaded successfully.'
31+
},
32+
props.timeout ? {timeout: props.timeout} : undefined
33+
)}>
34+
Upload files
35+
</Button>
36+
</div>
37+
);
38+
}
39+
```
40+
41+
```tsx render docs={toastDocs.exports.MyToast} links={toastDocs.links} props={['title', 'description', 'timeout']} initialProps={{title: 'Files uploaded', description: '3 files uploaded successfully.', timeout: 0}} type="tailwind" files={["starters/tailwind/src/Toast.tsx"]}
42+
"use client";
43+
import {MyToastRegion, queue} from 'tailwind-starter/Toast';
44+
import {Button} from 'tailwind-starter/Button';
45+
46+
function Example(props) {
47+
return (
48+
<div>
49+
<MyToastRegion />
50+
<Button onPress={() => queue.add(
51+
{
52+
title: props.title || 'Files uploaded',
53+
description: props.description || '3 files uploaded successfully.'
54+
},
55+
props.timeout ? {timeout: props.timeout} : undefined
56+
)}>
57+
Upload files
58+
</Button>
59+
</div>
60+
);
61+
}
62+
```
63+
64+
</ExampleSwitcher>
65+
66+
## Content
67+
68+
### Title and description
69+
70+
Use the `"title"` and `"description"` slots within `<ToastContent>` to provide structured content for the toast. The title is required, and description is optional.
71+
72+
```tsx render hideImports
73+
"use client";
74+
import {queue} from 'vanilla-starter/Toast';
75+
import {Button} from 'vanilla-starter/Button';
76+
77+
function Example() {
78+
return (
79+
<Button
80+
///- begin highlight -///
81+
onPress={() => queue.add({
82+
title: 'Update available',
83+
description: 'A new version is ready to install.'
84+
})}
85+
///- end highlight -///
86+
>
87+
Check for updates
88+
</Button>
89+
);
90+
}
91+
```
92+
93+
### Close button
94+
95+
Include a `<Button slot="close">` to allow users to dismiss the toast manually. This is important for accessibility.
96+
97+
<InlineAlert variant="notice">
98+
<Heading>Accessibility</Heading>
99+
<Content>We recommend that that the close button should be rendered as a sibling of `<ToastContent>` rather than inside it, so that screen readers announce the toast content without the close button first.</Content>
100+
</InlineAlert>
101+
102+
## Dismissal
103+
104+
Use the `timeout` option to automatically dismiss toasts after a period of time. For accessibility, toasts should have a minimum timeout of **5 seconds**. Timers automatically pause when the user focuses or hovers over a toast.
105+
106+
```tsx render hideImports
107+
"use client";
108+
import {queue} from 'vanilla-starter/Toast';
109+
import {Button} from 'vanilla-starter/Button';
110+
111+
function Example() {
112+
return (
113+
<Button
114+
///- begin highlight -///
115+
onPress={() => queue.add(
116+
{title: 'File has been saved!'},
117+
{timeout: 5000}
118+
)}
119+
///- end highlight -///
120+
>
121+
Save file
122+
</Button>
123+
);
124+
}
125+
```
126+
127+
<InlineAlert variant="notice">
128+
<Heading>Accessibility</Heading>
129+
<Content>Only auto-dismiss toasts when the information is not critical, or may be found elsewhere. Some users may require additional time to read toasts, and screen zoom users may miss them entirely.</Content>
130+
</InlineAlert>
131+
132+
### Programmatic dismissal
133+
134+
Toasts can be programmatically dismissed using the key returned from `queue.add()`. This is useful when a toast becomes irrelevant before the user manually closes it.
135+
136+
```tsx render hideImports
137+
"use client";
138+
import {queue} from 'vanilla-starter/Toast';
139+
import {Button} from 'vanilla-starter/Button';
140+
import {useState} from 'react';
141+
142+
function Example() {
143+
let [toastKey, setToastKey] = useState(null);
144+
145+
return (
146+
<Button
147+
///- begin highlight -///
148+
onPress={() => {
149+
if (!toastKey) {
150+
setToastKey(queue.add(
151+
{title: 'Processing...'},
152+
{onClose: () => setToastKey(null)}
153+
));
154+
} else {
155+
queue.close(toastKey);
156+
}
157+
}}
158+
///- end highlight -///
159+
>
160+
{toastKey ? 'Cancel' : 'Process'}
161+
</Button>
162+
);
163+
}
164+
```
165+
166+
## Accessibility
167+
168+
Toast regions are [landmark regions](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/) that can be navigated using <Keyboard>F6</Keyboard> to move forward and <Keyboard>Shift</Keyboard> + <Keyboard>F6</Keyboard> to move backward. This provides an easy way for keyboard users to jump to toasts from anywhere in the app.
169+
170+
When a toast is closed, focus moves to the next toast if any. When the last toast is closed, focus is restored to where it was before.
171+
172+
## API
173+
174+
<Anatomy role="img" aria-label="Toast anatomy diagram, showing the toast's title and close button within the toast region." />
175+
176+
```tsx links={{ToastRegion: '#toastregion', Toast: '#toast', ToastContent: '#toastcontent', ToastQueue: '#toastqueue', Button: 'Button.html'}}
177+
<ToastRegion>
178+
{({toast}) => (
179+
<Toast toast={toast}>
180+
<ToastContent>
181+
<Text slot="title" />
182+
<Text slot="description" />
183+
</ToastContent>
184+
<Button slot="close" />
185+
</Toast>
186+
)}
187+
</ToastRegion>
188+
```
189+
190+
### ToastRegion
191+
192+
<PropTable component={docs.exports.UNSTABLE_ToastRegion} links={docs.links} showDescription />
193+
194+
### Toast
195+
196+
<PropTable component={docs.exports.UNSTABLE_Toast} links={docs.links} showDescription />
197+
198+
### ToastContent
199+
200+
`<ToastContent>` renders the main content of a toast, including the title and description slots. It accepts all HTML attributes.
201+
202+
### ToastQueue
203+
204+
A `ToastQueue` manages the state for a `<ToastRegion>`. The state is stored outside React so you can trigger toasts from anywhere in your application.
205+
206+
<ClassAPI links={docs.links} class={docs.exports.UNSTABLE_ToastQueue} />

starters/docs/src/Toast.css

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
@import "./theme.css";
2+
3+
.react-aria-ToastRegion {
4+
position: fixed;
5+
bottom: var(--spacing-4);
6+
right: var(--spacing-4);
7+
display: flex;
8+
flex-direction: column-reverse;
9+
gap: var(--spacing-2);
10+
border-radius: var(--radius-lg);
11+
outline: none;
12+
13+
&[data-focus-visible] {
14+
outline: 2px solid var(--focus-ring-color);
15+
outline-offset: 2px;
16+
}
17+
}
18+
19+
.react-aria-Toast {
20+
display: flex;
21+
align-items: center;
22+
gap: var(--spacing-4);
23+
background: var(--tint-1000);
24+
padding: var(--spacing-3) var(--spacing-4);
25+
border-radius: var(--radius-lg);
26+
outline: none;
27+
forced-color-adjust: none;
28+
view-transition-class: toast;
29+
30+
&[data-focus-visible] {
31+
outline: 2px solid var(--focus-ring-color);
32+
outline-offset: 2px;
33+
}
34+
35+
.react-aria-ToastContent {
36+
display: flex;
37+
flex-direction: column;
38+
flex: 1 1 auto;
39+
min-width: 0;
40+
font: var(--font-size) system-ui;
41+
42+
[slot=title] {
43+
font-weight: 600;
44+
color: var(--highlight-foreground);
45+
}
46+
47+
[slot=description] {
48+
font-size: var(--font-size-sm);
49+
color: var(--highlight-foreground);
50+
}
51+
}
52+
53+
.react-aria-Button[slot=close] {
54+
flex: 0 0 auto;
55+
background: none;
56+
border: none;
57+
appearance: none;
58+
border-radius: var(--radius-sm);
59+
height: var(--spacing-8);
60+
width: var(--spacing-8);
61+
color: var(--highlight-foreground);
62+
padding: 0;
63+
outline: none;
64+
-webkit-tap-highlight-color: transparent;
65+
66+
&[data-hovered] {
67+
background: var(--tint-900);
68+
box-shadow: none;
69+
}
70+
71+
&[data-focus-visible] {
72+
outline: 2px solid var(--highlight-foreground);
73+
outline-offset: 2px;
74+
}
75+
76+
&[data-pressed] {
77+
background: var(--highlight-pressed);
78+
}
79+
}
80+
}
81+
82+
::view-transition-new(.toast):only-child {
83+
animation: slide-in 400ms;
84+
}
85+
86+
::view-transition-old(.toast):only-child {
87+
animation: slide-out 400ms;
88+
animation-fill-mode: forwards;
89+
}
90+
91+
@keyframes slide-out {
92+
to {
93+
translate: 100% 0;
94+
opacity: 0;
95+
visibility: hidden;
96+
}
97+
}
98+
99+
@keyframes slide-in {
100+
from {
101+
translate: 100% 0;
102+
opacity: 0;
103+
}
104+
}

starters/docs/src/Toast.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use client';
2+
import {
3+
UNSTABLE_ToastRegion as ToastRegion,
4+
UNSTABLE_Toast as Toast,
5+
UNSTABLE_ToastQueue as ToastQueue,
6+
UNSTABLE_ToastContent as ToastContent,
7+
ToastProps,
8+
Text
9+
} from 'react-aria-components';
10+
import {Button} from './Button';
11+
import {X} from 'lucide-react';
12+
import './Toast.css';
13+
import {flushSync} from 'react-dom';
14+
15+
// Define the type for your toast content. This interface defines the properties of your toast content, affecting what you
16+
// pass to the queue calls as arguments.
17+
interface MyToastContent {
18+
title: string;
19+
description?: string;
20+
}
21+
22+
// This is a global toast queue, to be imported and called where ever you want to queue a toast via queue.add().
23+
export const queue = new ToastQueue<MyToastContent>({
24+
// Wrap state updates in a CSS view transition.
25+
wrapUpdate(fn) {
26+
if ('startViewTransition' in document) {
27+
document.startViewTransition(() => {
28+
flushSync(fn);
29+
});
30+
} else {
31+
fn();
32+
}
33+
}
34+
});
35+
36+
export function MyToastRegion() {
37+
return (
38+
// The ToastRegion should be rendered at the root of your app.
39+
<ToastRegion queue={queue}>
40+
{({toast}) => (
41+
<MyToast toast={toast} style={{viewTransitionName: toast.key}}>
42+
<ToastContent>
43+
<Text slot="title">{toast.content.title}</Text>
44+
{toast.content.description && (
45+
<Text slot="description">{toast.content.description}</Text>
46+
)}
47+
</ToastContent>
48+
<Button slot="close" aria-label="Close" variant="quiet">
49+
<X size={16} />
50+
</Button>
51+
</MyToast>
52+
)}
53+
</ToastRegion>
54+
);
55+
}
56+
57+
export function MyToast(props: ToastProps<MyToastContent>) {
58+
return <Toast {...props} />;
59+
}

0 commit comments

Comments
 (0)