Skip to content

Commit 8efed80

Browse files
authored
chore: remove toast priority queue (#7805)
* chore: remove toast priority queue * reverse the order so dom order is correct by default * remove counter from toasts * remove priority references * fix verdaccio * turn on verdaccio * Revert "turn on verdaccio" This reverts commit 18b4975.
1 parent 90b6fd4 commit 8efed80

File tree

10 files changed

+43
-108
lines changed

10 files changed

+43
-108
lines changed

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

+4-31
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,13 @@ keywords: [toast, notifications, alert, aria]
4848
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

5050
* **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.
51-
* **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.
52-
* **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.
51+
* **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. Tabbing through the Toast region will move from newest to oldest.
5352

5453
## Anatomy
5554

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

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.
57+
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 chronological 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.
5958

6059
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.
6160

@@ -169,7 +168,7 @@ function Toast<T extends React.ReactNode>({state, ...props}: ToastProps<T>) {
169168
bottom: 16px;
170169
right: 16px;
171170
display: flex;
172-
flex-direction: column;
171+
flex-direction: column-reverse;
173172
gap: 8px;
174173
}
175174

@@ -231,32 +230,6 @@ function Button(props) {
231230

232231
The following examples show how to use the `ToastProvider` component created in the above example.
233232

234-
### Toast priorities
235-
236-
Toasts are displayed according to a priority queue. The priority of a toast can be set using the `priority` option, passed to the `state.add` function. Priorities are arbitrary numbers defined by your implementation.
237-
238-
```tsx example
239-
<ToastProvider>
240-
{state => (<>
241-
{/*- begin highlight -*/}
242-
<Button onPress={() => state.add('Toasting…', {priority: 1})}>
243-
{/*- end highlight -*/}
244-
Show low priority toast
245-
</Button>
246-
{/*- begin highlight -*/}
247-
<Button onPress={() => state.add('Toast is done!', {priority: 2})}>
248-
{/*- end highlight -*/}
249-
Show medium priority toast
250-
</Button>
251-
{/*- begin highlight -*/}
252-
<Button onPress={() => state.add('Toast is burned!', {priority: 3})}>
253-
{/*- end highlight -*/}
254-
Show high priority toast
255-
</Button>
256-
</>)}
257-
</ToastProvider>
258-
```
259-
260233
### Auto-dismiss
261234

262235
Toasts support a `timeout` option to automatically hide them after a certain amount of time. For accessibility, toasts should have a minimum timeout of 5 seconds to give users enough time to read them. If a toast includes action buttons or other interactive elements it should not auto dismiss. In addition, timers will automatically pause when the user focuses or hovers over a toast.
@@ -267,7 +240,7 @@ Be sure only to automatically dismiss toasts when the information is not importa
267240
<ToastProvider>
268241
{state => (
269242
///- begin highlight -///
270-
<Button onPress={() => state.add('Toast is done!', {timeout: 5000})}>
243+
<Button onPress={() => state.add('Toast still toasting!', {timeout: 5000})}>
271244
{/*- end highlight -*/}
272245
Show toast
273246
</Button>

packages/@react-aria/toast/src/useToastRegion.ts

-2
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ export function useToastRegion<T>(props: AriaToastRegionProps, state: ToastState
7777

7878
// Manage focus within the toast region.
7979
// If a focused containing toast is removed, move focus to the next toast, or the previous toast if there is no next toast.
80-
// We might be making an assumption with how this works if someone implements the priority queue differently, or
81-
// if they only show one toast at a time.
8280
let toasts = useRef<FocusableElement[]>([]);
8381
let prevVisibleToasts = useRef(state.visibleToasts);
8482
let focusedToast = useRef<number | null>(null);

packages/@react-aria/toast/stories/useToast.stories.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ let count = 0;
3232
export const Default = args => (
3333
<ToastContainer {...args}>
3434
{state => (<>
35-
<button onClick={() => state.add('High ' + ++count, {priority: 10, timeout: args.timeout})}>Add high priority toast</button>
36-
<button onClick={() => state.add('Medium ' + ++count, {priority: 5, timeout: args.timeout})}>Add medium priority toast</button>
37-
<button onClick={() => state.add('Low ' + ++count, {priority: 1, timeout: args.timeout})}>Add low priority toast</button>
35+
<button onClick={() => state.add('Mmmmm toast ' + ++count, {timeout: args.timeout})}>Add toast</button>
3836
</>)}
3937
</ToastContainer>
4038
);

packages/@react-aria/toast/test/useToast.test.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,24 @@ describe('single toast at a time', () => {
6666

6767
it('moves focus to the next toast when it appears', async () => {
6868
let tree = render(<Default />);
69-
// eslint-disable-next-line
70-
let [bLow, bMedium, bHigh] = tree.getAllByRole('button');
69+
let button = tree.getByRole('button');
7170

72-
await user.click(bHigh);
73-
await user.click(bLow);
71+
await user.click(button);
72+
await user.click(button);
7473

7574
let toast = tree.getByRole('alertdialog');
76-
expect(toast.textContent).toContain('High');
75+
expect(toast.textContent).toContain('Mmmmm toast 2x');
7776
let closeButton = within(toast).getByRole('button');
7877
await user.click(closeButton);
7978

8079
toast = tree.getByRole('alertdialog');
81-
expect(toast.textContent).toContain('Low');
80+
expect(toast.textContent).toContain('Mmmmm toast 1x');
8281
expect(toast).toHaveFocus();
8382

8483
closeButton = within(toast).getByRole('button');
8584
await user.click(closeButton);
8685

8786
expect(tree.queryByRole('alertdialog')).toBeNull();
88-
expect(bLow).toHaveFocus();
87+
expect(button).toHaveFocus();
8988
});
9089
});

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

+1-5
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,7 @@ export const Toast = React.forwardRef(function Toast(props: SpectrumToastProps,
102102
'spectrum-Toast',
103103
{'focus-ring': isFocusVisible}
104104
)
105-
)}
106-
style={{
107-
...styleProps.style,
108-
zIndex: props.toast.priority
109-
}}>
105+
)}>
110106
<div
111107
{...contentProps}
112108
className={classNames(toastContainerStyles, 'spectrum-Toast-contentWrapper')}>

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface SpectrumToastContainerProps extends AriaToastRegionProps {
2828
placement?: ToastPlacement
2929
}
3030

31-
export interface SpectrumToastOptions extends Omit<ToastOptions, 'priority'>, DOMProps {
31+
export interface SpectrumToastOptions extends ToastOptions, DOMProps {
3232
/** A label for the action button within the toast. */
3333
actionLabel?: string,
3434
/** Handler that is called when the action button is pressed. */
@@ -128,7 +128,7 @@ export function ToastContainer(props: SpectrumToastContainerProps): ReactElement
128128
return (
129129
<Toaster state={state} {...props}>
130130
<ol className={classNames(toastContainerStyles, 'spectrum-ToastContainer-list')}>
131-
{state.visibleToasts.slice().reverse().map((toast, index) => {
131+
{state.visibleToasts.map((toast, index) => {
132132
let shouldFade = isCentered && index !== 0;
133133
return (
134134
<li
@@ -148,7 +148,7 @@ export function ToastContainer(props: SpectrumToastContainerProps): ReactElement
148148
state={state} />
149149
</li>
150150
);
151-
})}
151+
})}
152152
</ol>
153153
</Toaster>
154154
);

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

+3-16
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ export interface ToastOptions {
2525
/** Handler that is called when the toast is closed, either by the user or after a timeout. */
2626
onClose?: () => void,
2727
/** A timeout to automatically close the toast after, in milliseconds. */
28-
timeout?: number,
29-
/** The priority of the toast relative to other toasts. Larger numbers indicate higher priority. */
30-
priority?: number
28+
timeout?: number
3129
}
3230

3331
export interface QueuedToast<T> extends ToastOptions {
@@ -82,7 +80,7 @@ export function useToastQueue<T>(queue: ToastQueue<T>): ToastState<T> {
8280
}
8381

8482
/**
85-
* A ToastQueue is a priority queue of toasts.
83+
* A ToastQueue manages the order of toasts.
8684
*/
8785
export class ToastQueue<T> {
8886
private queue: QueuedToast<T>[] = [];
@@ -121,18 +119,7 @@ export class ToastQueue<T> {
121119
timer: options.timeout ? new Timer(() => this.close(toastKey), options.timeout) : undefined
122120
};
123121

124-
let low = 0;
125-
let high = this.queue.length;
126-
while (low < high) {
127-
let mid = Math.floor((low + high) / 2);
128-
if ((toast.priority || 0) > (this.queue[mid].priority || 0)) {
129-
high = mid;
130-
} else {
131-
low = mid + 1;
132-
}
133-
}
134-
135-
this.queue.splice(low, 0, toast);
122+
this.queue.unshift(toast);
136123

137124
this.updateVisibleToasts();
138125
return toastKey;

packages/@react-stately/toast/test/useToastState.test.js

+20-36
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ describe('useToastState', () => {
6464

6565
act(() => {result.current.add(secondToast.content, secondToast.props);});
6666
expect(result.current.visibleToasts.length).toBe(2);
67-
expect(result.current.visibleToasts[0].content).toBe(newValue[0].content);
68-
expect(result.current.visibleToasts[1].content).toBe(secondToast.content);
67+
expect(result.current.visibleToasts[0].content).toBe(secondToast.content);
68+
expect(result.current.visibleToasts[1].content).toBe(newValue[0].content);
6969
});
7070

7171
it('should be able to display three toasts and remove the middle toast via timeout then the visible toast', () => {
@@ -83,7 +83,7 @@ describe('useToastState', () => {
8383
result.current.add('Second Toast', {timeout: 1000});
8484
});
8585
expect(result.current.visibleToasts).toHaveLength(2);
86-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
86+
expect(result.current.visibleToasts[0].content).toBe('Second Toast');
8787

8888
result.current.resumeAll();
8989

@@ -92,21 +92,21 @@ describe('useToastState', () => {
9292
result.current.add('Third Toast', {timeout: 0});
9393
});
9494
expect(result.current.visibleToasts).toHaveLength(3);
95-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
95+
expect(result.current.visibleToasts[0].content).toBe('Third Toast');
9696
expect(result.current.visibleToasts[1].content).toBe('Second Toast');
97-
expect(result.current.visibleToasts[2].content).toBe('Third Toast');
97+
expect(result.current.visibleToasts[2].content).toBe('First Toast');
9898

9999
act(() => jest.advanceTimersByTime(500));
100100
expect(result.current.visibleToasts).toHaveLength(3);
101101

102102
act(() => jest.advanceTimersByTime(1000));
103103
expect(result.current.visibleToasts).toHaveLength(2);
104-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
105-
expect(result.current.visibleToasts[1].content).toBe('Third Toast');
104+
expect(result.current.visibleToasts[0].content).toBe('Third Toast');
105+
expect(result.current.visibleToasts[1].content).toBe('First Toast');
106106

107107
act(() => {result.current.close(result.current.visibleToasts[0].key);});
108108
expect(result.current.visibleToasts.length).toBe(1);
109-
expect(result.current.visibleToasts[0].content).toBe('Third Toast');
109+
expect(result.current.visibleToasts[0].content).toBe('First Toast');
110110
});
111111

112112
it('should be able to display one toast, add multiple toasts, and remove the middle not visible one programmatically', () => {
@@ -125,24 +125,24 @@ describe('useToastState', () => {
125125
secondToastKey = result.current.add('Second Toast', {timeout: 0});
126126
});
127127
expect(result.current.visibleToasts).toHaveLength(1);
128-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
128+
expect(result.current.visibleToasts[0].content).toBe('Second Toast');
129129

130130
// Add the third toast
131131
act(() => {
132132
result.current.add('Third Toast', {timeout: 0});
133133
});
134134
expect(result.current.visibleToasts).toHaveLength(1);
135-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
135+
expect(result.current.visibleToasts[0].content).toBe('Third Toast');
136136

137137
// Remove a toast that isn't visible
138138
act(() => {result.current.close(secondToastKey);});
139139
expect(result.current.visibleToasts).toHaveLength(1);
140-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
140+
expect(result.current.visibleToasts[0].content).toBe('Third Toast');
141141

142142
// Remove the visible toast to confirm the middle toast was removed
143143
act(() => {result.current.close(result.current.visibleToasts[0].key);});
144144
expect(result.current.visibleToasts.length).toBe(1);
145-
expect(result.current.visibleToasts[0].content).toBe('Third Toast');
145+
expect(result.current.visibleToasts[0].content).toBe('First Toast');
146146
});
147147

148148
it('should be able to display one toast, add multiple toasts', () => {
@@ -160,14 +160,14 @@ describe('useToastState', () => {
160160
result.current.add('Second Toast', {timeout: 0});
161161
});
162162
expect(result.current.visibleToasts).toHaveLength(1);
163-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
163+
expect(result.current.visibleToasts[0].content).toBe('Second Toast');
164164

165165
// Add the third toast
166166
act(() => {
167167
result.current.add('Third Toast', {timeout: 0});
168168
});
169169
expect(result.current.visibleToasts).toHaveLength(1);
170-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
170+
expect(result.current.visibleToasts[0].content).toBe('Third Toast');
171171
});
172172

173173
it('should maintain the toast queue order on close', () => {
@@ -179,19 +179,19 @@ describe('useToastState', () => {
179179

180180
act(() => {result.current.add('Second Toast');});
181181
expect(result.current.visibleToasts).toHaveLength(2);
182-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
183-
expect(result.current.visibleToasts[1].content).toBe('Second Toast');
182+
expect(result.current.visibleToasts[0].content).toBe('Second Toast');
183+
expect(result.current.visibleToasts[1].content).toBe('First Toast');
184184

185185
act(() => {result.current.add('Third Toast');});
186186
expect(result.current.visibleToasts).toHaveLength(3);
187-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
187+
expect(result.current.visibleToasts[0].content).toBe('Third Toast');
188188
expect(result.current.visibleToasts[1].content).toBe('Second Toast');
189-
expect(result.current.visibleToasts[2].content).toBe('Third Toast');
189+
expect(result.current.visibleToasts[2].content).toBe('First Toast');
190190

191191
act(() => {result.current.close(result.current.visibleToasts[1].key);});
192192
expect(result.current.visibleToasts).toHaveLength(2);
193-
expect(result.current.visibleToasts[0].content).toBe('First Toast');
194-
expect(result.current.visibleToasts[1].content).toBe('Third Toast');
193+
expect(result.current.visibleToasts[0].content).toBe('Third Toast');
194+
expect(result.current.visibleToasts[1].content).toBe('First Toast');
195195
});
196196

197197
it('should close a toast', () => {
@@ -211,22 +211,6 @@ describe('useToastState', () => {
211211

212212
act(() => {result.current.add('Second Toast');});
213213
expect(result.current.visibleToasts.length).toBe(1);
214-
expect(result.current.visibleToasts[0].content).toBe(newValue[0].content);
215-
216-
act(() => {result.current.close(result.current.visibleToasts[0].key);});
217-
expect(result.current.visibleToasts.length).toBe(1);
218-
expect(result.current.visibleToasts[0].content).toBe('Second Toast');
219-
});
220-
221-
it('should queue toasts with priority', () => {
222-
let {result} = renderHook(() => useToastState());
223-
expect(result.current.visibleToasts).toStrictEqual([]);
224-
225-
act(() => {result.current.add(newValue[0].content, newValue[0].props);});
226-
expect(result.current.visibleToasts[0].content).toBe(newValue[0].content);
227-
228-
act(() => {result.current.add('Second Toast', {priority: 1});});
229-
expect(result.current.visibleToasts.length).toBe(1);
230214
expect(result.current.visibleToasts[0].content).toBe('Second Toast');
231215

232216
act(() => {result.current.close(result.current.visibleToasts[0].key);});

packages/react-aria-components/docs/Toast.mdx

+4-4
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function App() {
8484
Then, you can trigger a toast from anywhere using the exported `queue`.
8585

8686
```tsx example
87-
<Button
87+
<Button
8888
onPress={() => queue.add({
8989
title: 'Toast complete!',
9090
description: 'Great success.'
@@ -158,7 +158,7 @@ Then, you can trigger a toast from anywhere using the exported `queue`.
158158
color: white;
159159
padding: 0;
160160
outline: none;
161-
161+
162162
&[data-focus-visible] {
163163
box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white;
164164
}
@@ -183,7 +183,7 @@ There is no built in way to display toast notifications in HTML. `<ToastRegion>`
183183

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

186-
A `<ToastRegion>` is an [ARIA landmark region](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/) labeled "Notifications" by default. A `<ToastRegion>` accepts a function to render 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.
186+
A `<ToastRegion>` is an [ARIA landmark region](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/) labeled "Notifications" by default. A `<ToastRegion>` accepts a function to render one or more visible toasts, in chronological 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.
187187

188188
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.
189189

@@ -210,7 +210,7 @@ Toasts support a `timeout` option to automatically hide them after a certain amo
210210
Be sure only to automatically dismiss toasts when the information is not important, or may be found elsewhere. Some users may require additional time to read a toast message, and screen zoom users may miss toasts entirely.
211211

212212
```tsx example
213-
<Button
213+
<Button
214214
/*- begin highlight -*/
215215
onPress={() => queue.add({title: 'Toast is done!'}, {timeout: 5000})}
216216
/*- end highlight -*/

scripts/extractStarter.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ fs.mkdirSync(`starters/docs/src`, {recursive: true});
2929
fs.mkdirSync(`starters/docs/stories`, {recursive: true});
3030

3131
for (let file of glob.sync('packages/react-aria-components/docs/*.mdx')) {
32-
if (!/^[A-Z]/.test(basename(file)) || /^Autocomplete|^Virtualizer/.test(basename(file))) {
32+
if (!/^[A-Z]/.test(basename(file)) || /^Autocomplete|^Virtualizer|^Toast/.test(basename(file))) {
3333
continue;
3434
}
3535
console.log('Processing ' + file);

0 commit comments

Comments
 (0)