From 9114b8233c8b2542aa69b86a9f30a9e26f6948c3 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 21 Feb 2025 10:18:31 +1100 Subject: [PATCH 1/7] chore: remove toast priority queue --- packages/@react-aria/toast/docs/useToast.mdx | 29 +------------------ .../@react-aria/toast/src/useToastRegion.ts | 2 -- .../toast/stories/useToast.stories.tsx | 4 +-- .../@react-aria/toast/test/useToast.test.js | 13 ++++----- packages/@react-spectrum/toast/src/Toast.tsx | 6 +--- .../toast/src/ToastContainer.tsx | 4 +-- .../@react-stately/toast/src/useToastState.ts | 19 ++---------- .../toast/test/useToastState.test.js | 16 ---------- 8 files changed, 14 insertions(+), 79 deletions(-) diff --git a/packages/@react-aria/toast/docs/useToast.mdx b/packages/@react-aria/toast/docs/useToast.mdx index b5ee5856a96..1b82fd68291 100644 --- a/packages/@react-aria/toast/docs/useToast.mdx +++ b/packages/@react-aria/toast/docs/useToast.mdx @@ -49,13 +49,12 @@ There is no built in way to toast notifications in HTML. -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 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 an ARIA alert element, 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 F6 key to move forward, and the Shift + F6 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. @@ -231,32 +230,6 @@ function Button(props) { The following examples show how to use the `ToastProvider` component created in the above example. -### Toast priorities - -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. - -```tsx example - - {state => (<> - {/*- begin highlight -*/} - - {/*- begin highlight -*/} - - {/*- begin highlight -*/} - - )} - -``` - ### Auto-dismiss 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. diff --git a/packages/@react-aria/toast/src/useToastRegion.ts b/packages/@react-aria/toast/src/useToastRegion.ts index 28138eff761..5e9925bd09b 100644 --- a/packages/@react-aria/toast/src/useToastRegion.ts +++ b/packages/@react-aria/toast/src/useToastRegion.ts @@ -77,8 +77,6 @@ export function useToastRegion(props: AriaToastRegionProps, state: ToastState // Manage focus within the toast region. // If a focused containing toast is removed, move focus to the next toast, or the previous toast if there is no next toast. - // We might be making an assumption with how this works if someone implements the priority queue differently, or - // if they only show one toast at a time. let toasts = useRef([]); let prevVisibleToasts = useRef(state.visibleToasts); let focusedToast = useRef(null); diff --git a/packages/@react-aria/toast/stories/useToast.stories.tsx b/packages/@react-aria/toast/stories/useToast.stories.tsx index 6e71d8423e2..308a954570e 100644 --- a/packages/@react-aria/toast/stories/useToast.stories.tsx +++ b/packages/@react-aria/toast/stories/useToast.stories.tsx @@ -32,9 +32,7 @@ let count = 0; export const Default = args => ( {state => (<> - - - + )} ); diff --git a/packages/@react-aria/toast/test/useToast.test.js b/packages/@react-aria/toast/test/useToast.test.js index 08ff56253f2..a548b64390a 100644 --- a/packages/@react-aria/toast/test/useToast.test.js +++ b/packages/@react-aria/toast/test/useToast.test.js @@ -66,25 +66,24 @@ describe('single toast at a time', () => { it('moves focus to the next toast when it appears', async () => { let tree = render(); - // eslint-disable-next-line - let [bLow, bMedium, bHigh] = tree.getAllByRole('button'); + let button = tree.getByRole('button'); - await user.click(bHigh); - await user.click(bLow); + await user.click(button); + await user.click(button); let toast = tree.getByRole('alertdialog'); - expect(toast.textContent).toContain('High'); + expect(toast.textContent).toContain('Mmmmm toast 1x'); let closeButton = within(toast).getByRole('button'); await user.click(closeButton); toast = tree.getByRole('alertdialog'); - expect(toast.textContent).toContain('Low'); + expect(toast.textContent).toContain('Mmmmm toast 2x'); expect(toast).toHaveFocus(); closeButton = within(toast).getByRole('button'); await user.click(closeButton); expect(tree.queryByRole('alertdialog')).toBeNull(); - expect(bLow).toHaveFocus(); + expect(button).toHaveFocus(); }); }); diff --git a/packages/@react-spectrum/toast/src/Toast.tsx b/packages/@react-spectrum/toast/src/Toast.tsx index 7a4f0cbb6eb..fc1b4687bf5 100644 --- a/packages/@react-spectrum/toast/src/Toast.tsx +++ b/packages/@react-spectrum/toast/src/Toast.tsx @@ -102,11 +102,7 @@ export const Toast = React.forwardRef(function Toast(props: SpectrumToastProps, 'spectrum-Toast', {'focus-ring': isFocusVisible} ) - )} - style={{ - ...styleProps.style, - zIndex: props.toast.priority - }}> + )}>
diff --git a/packages/@react-spectrum/toast/src/ToastContainer.tsx b/packages/@react-spectrum/toast/src/ToastContainer.tsx index 52d07aa9601..1b7e2f847cd 100644 --- a/packages/@react-spectrum/toast/src/ToastContainer.tsx +++ b/packages/@react-spectrum/toast/src/ToastContainer.tsx @@ -28,7 +28,7 @@ export interface SpectrumToastContainerProps extends AriaToastRegionProps { placement?: ToastPlacement } -export interface SpectrumToastOptions extends Omit, DOMProps { +export interface SpectrumToastOptions extends ToastOptions, DOMProps { /** A label for the action button within the toast. */ actionLabel?: string, /** Handler that is called when the action button is pressed. */ @@ -155,7 +155,7 @@ export function ToastContainer(props: SpectrumToastContainerProps): ReactElement state={state} /> ); - })} + })} ); diff --git a/packages/@react-stately/toast/src/useToastState.ts b/packages/@react-stately/toast/src/useToastState.ts index 56850277ffa..5b74f830e94 100644 --- a/packages/@react-stately/toast/src/useToastState.ts +++ b/packages/@react-stately/toast/src/useToastState.ts @@ -25,9 +25,7 @@ export interface ToastOptions { /** Handler that is called when the toast is closed, either by the user or after a timeout. */ onClose?: () => void, /** A timeout to automatically close the toast after, in milliseconds. */ - timeout?: number, - /** The priority of the toast relative to other toasts. Larger numbers indicate higher priority. */ - priority?: number + timeout?: number } export interface QueuedToast extends ToastOptions { @@ -82,7 +80,7 @@ export function useToastQueue(queue: ToastQueue): ToastState { } /** - * A ToastQueue is a priority queue of toasts. + * A ToastQueue manages the order of toasts. */ export class ToastQueue { private queue: QueuedToast[] = []; @@ -121,18 +119,7 @@ export class ToastQueue { timer: options.timeout ? new Timer(() => this.close(toastKey), options.timeout) : undefined }; - let low = 0; - let high = this.queue.length; - while (low < high) { - let mid = Math.floor((low + high) / 2); - if ((toast.priority || 0) > (this.queue[mid].priority || 0)) { - high = mid; - } else { - low = mid + 1; - } - } - - this.queue.splice(low, 0, toast); + this.queue.push(toast); this.updateVisibleToasts(); return toastKey; diff --git a/packages/@react-stately/toast/test/useToastState.test.js b/packages/@react-stately/toast/test/useToastState.test.js index 87d005a6bad..c308de0538a 100644 --- a/packages/@react-stately/toast/test/useToastState.test.js +++ b/packages/@react-stately/toast/test/useToastState.test.js @@ -217,20 +217,4 @@ describe('useToastState', () => { expect(result.current.visibleToasts.length).toBe(1); expect(result.current.visibleToasts[0].content).toBe('Second Toast'); }); - - it('should queue toasts with priority', () => { - let {result} = renderHook(() => useToastState()); - expect(result.current.visibleToasts).toStrictEqual([]); - - act(() => {result.current.add(newValue[0].content, newValue[0].props);}); - expect(result.current.visibleToasts[0].content).toBe(newValue[0].content); - - act(() => {result.current.add('Second Toast', {priority: 1});}); - expect(result.current.visibleToasts.length).toBe(1); - expect(result.current.visibleToasts[0].content).toBe('Second Toast'); - - act(() => {result.current.close(result.current.visibleToasts[0].key);}); - expect(result.current.visibleToasts.length).toBe(1); - expect(result.current.visibleToasts[0].content).toBe(newValue[0].content); - }); }); From f318b366ea979d1355211668ed8d34e460e48d90 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 21 Feb 2025 10:57:59 +1100 Subject: [PATCH 2/7] reverse the order so dom order is correct by default --- packages/@react-aria/toast/docs/useToast.mdx | 16 ++++--- .../@react-aria/toast/test/useToast.test.js | 4 +- .../toast/src/ToastContainer.tsx | 2 +- .../@react-stately/toast/src/useToastState.ts | 2 +- .../toast/test/useToastState.test.js | 44 +++++++++---------- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/packages/@react-aria/toast/docs/useToast.mdx b/packages/@react-aria/toast/docs/useToast.mdx index 1b82fd68291..da0b562b065 100644 --- a/packages/@react-aria/toast/docs/useToast.mdx +++ b/packages/@react-aria/toast/docs/useToast.mdx @@ -48,7 +48,7 @@ keywords: [toast, notifications, alert, aria] There is no built in way to toast notifications in HTML. and 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. -* **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. +* **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. ## Anatomy @@ -152,9 +152,11 @@ function Toast({state, ...props}: ToastProps) { ``` ```tsx example +let i = 0; + {state => ( - + )} ``` @@ -168,7 +170,7 @@ function Toast({state, ...props}: ToastProps) { bottom: 16px; right: 16px; display: flex; - flex-direction: column; + flex-direction: column-reverse; gap: 8px; } @@ -237,10 +239,12 @@ Toasts support a `timeout` option to automatically hide them after a certain amo 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. ```tsx example +let i = 0; + {state => ( ///- begin highlight -/// - @@ -253,6 +257,8 @@ Be sure only to automatically dismiss toasts when the information is not importa Toasts may be programmatically dismissed if they become irrelevant before the user manually closes them. `state.add` returns a key for the toast which may be passed to `state.close` to dismiss the toast. ```tsx example +let i = 0; + function Example() { let [toastKey, setToastKey] = React.useState(null); @@ -263,7 +269,7 @@ function Example() { onPress={() => { if (!toastKey) { ///- begin highlight -/// - setToastKey(state.add('Unable to save', {onClose: () => setToastKey(null)})); + setToastKey(state.add('Unable to save ' + i++, {onClose: () => setToastKey(null)})); ///- end highlight -/// } else { ///- begin highlight -/// diff --git a/packages/@react-aria/toast/test/useToast.test.js b/packages/@react-aria/toast/test/useToast.test.js index a548b64390a..f22f40a1c57 100644 --- a/packages/@react-aria/toast/test/useToast.test.js +++ b/packages/@react-aria/toast/test/useToast.test.js @@ -72,12 +72,12 @@ describe('single toast at a time', () => { await user.click(button); let toast = tree.getByRole('alertdialog'); - expect(toast.textContent).toContain('Mmmmm toast 1x'); + expect(toast.textContent).toContain('Mmmmm toast 2x'); let closeButton = within(toast).getByRole('button'); await user.click(closeButton); toast = tree.getByRole('alertdialog'); - expect(toast.textContent).toContain('Mmmmm toast 2x'); + expect(toast.textContent).toContain('Mmmmm toast 1x'); expect(toast).toHaveFocus(); closeButton = within(toast).getByRole('button'); diff --git a/packages/@react-spectrum/toast/src/ToastContainer.tsx b/packages/@react-spectrum/toast/src/ToastContainer.tsx index 1b7e2f847cd..491658b3c6a 100644 --- a/packages/@react-spectrum/toast/src/ToastContainer.tsx +++ b/packages/@react-spectrum/toast/src/ToastContainer.tsx @@ -134,7 +134,7 @@ export function ToastContainer(props: SpectrumToastContainerProps): ReactElement return (
    - {state.visibleToasts.slice().reverse().map((toast, index) => { + {state.visibleToasts.map((toast, index) => { let shouldFade = isCentered && index !== 0; return (
  1. { timer: options.timeout ? new Timer(() => this.close(toastKey), options.timeout) : undefined }; - this.queue.push(toast); + this.queue.unshift(toast); this.updateVisibleToasts(); return toastKey; diff --git a/packages/@react-stately/toast/test/useToastState.test.js b/packages/@react-stately/toast/test/useToastState.test.js index c308de0538a..d76d0bcc6bb 100644 --- a/packages/@react-stately/toast/test/useToastState.test.js +++ b/packages/@react-stately/toast/test/useToastState.test.js @@ -64,8 +64,8 @@ describe('useToastState', () => { act(() => {result.current.add(secondToast.content, secondToast.props);}); expect(result.current.visibleToasts.length).toBe(2); - expect(result.current.visibleToasts[0].content).toBe(newValue[0].content); - expect(result.current.visibleToasts[1].content).toBe(secondToast.content); + expect(result.current.visibleToasts[0].content).toBe(secondToast.content); + expect(result.current.visibleToasts[1].content).toBe(newValue[0].content); }); 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', () => { result.current.add('Second Toast', {timeout: 1000}); }); expect(result.current.visibleToasts).toHaveLength(2); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); + expect(result.current.visibleToasts[0].content).toBe('Second Toast'); result.current.resumeAll(); @@ -92,21 +92,21 @@ describe('useToastState', () => { result.current.add('Third Toast', {timeout: 0}); }); expect(result.current.visibleToasts).toHaveLength(3); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); + expect(result.current.visibleToasts[0].content).toBe('Third Toast'); expect(result.current.visibleToasts[1].content).toBe('Second Toast'); - expect(result.current.visibleToasts[2].content).toBe('Third Toast'); + expect(result.current.visibleToasts[2].content).toBe('First Toast'); act(() => jest.advanceTimersByTime(500)); expect(result.current.visibleToasts).toHaveLength(3); act(() => jest.advanceTimersByTime(1000)); expect(result.current.visibleToasts).toHaveLength(2); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); - expect(result.current.visibleToasts[1].content).toBe('Third Toast'); + expect(result.current.visibleToasts[0].content).toBe('Third Toast'); + expect(result.current.visibleToasts[1].content).toBe('First Toast'); act(() => {result.current.close(result.current.visibleToasts[0].key);}); expect(result.current.visibleToasts.length).toBe(1); - expect(result.current.visibleToasts[0].content).toBe('Third Toast'); + expect(result.current.visibleToasts[0].content).toBe('First Toast'); }); 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', () => { secondToastKey = result.current.add('Second Toast', {timeout: 0}); }); expect(result.current.visibleToasts).toHaveLength(1); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); + expect(result.current.visibleToasts[0].content).toBe('Second Toast'); // Add the third toast act(() => { result.current.add('Third Toast', {timeout: 0}); }); expect(result.current.visibleToasts).toHaveLength(1); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); + expect(result.current.visibleToasts[0].content).toBe('Third Toast'); // Remove a toast that isn't visible act(() => {result.current.close(secondToastKey);}); expect(result.current.visibleToasts).toHaveLength(1); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); + expect(result.current.visibleToasts[0].content).toBe('Third Toast'); // Remove the visible toast to confirm the middle toast was removed act(() => {result.current.close(result.current.visibleToasts[0].key);}); expect(result.current.visibleToasts.length).toBe(1); - expect(result.current.visibleToasts[0].content).toBe('Third Toast'); + expect(result.current.visibleToasts[0].content).toBe('First Toast'); }); it('should be able to display one toast, add multiple toasts', () => { @@ -160,14 +160,14 @@ describe('useToastState', () => { result.current.add('Second Toast', {timeout: 0}); }); expect(result.current.visibleToasts).toHaveLength(1); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); + expect(result.current.visibleToasts[0].content).toBe('Second Toast'); // Add the third toast act(() => { result.current.add('Third Toast', {timeout: 0}); }); expect(result.current.visibleToasts).toHaveLength(1); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); + expect(result.current.visibleToasts[0].content).toBe('Third Toast'); }); it('should maintain the toast queue order on close', () => { @@ -179,19 +179,19 @@ describe('useToastState', () => { act(() => {result.current.add('Second Toast');}); expect(result.current.visibleToasts).toHaveLength(2); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); - expect(result.current.visibleToasts[1].content).toBe('Second Toast'); + expect(result.current.visibleToasts[0].content).toBe('Second Toast'); + expect(result.current.visibleToasts[1].content).toBe('First Toast'); act(() => {result.current.add('Third Toast');}); expect(result.current.visibleToasts).toHaveLength(3); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); + expect(result.current.visibleToasts[0].content).toBe('Third Toast'); expect(result.current.visibleToasts[1].content).toBe('Second Toast'); - expect(result.current.visibleToasts[2].content).toBe('Third Toast'); + expect(result.current.visibleToasts[2].content).toBe('First Toast'); act(() => {result.current.close(result.current.visibleToasts[1].key);}); expect(result.current.visibleToasts).toHaveLength(2); - expect(result.current.visibleToasts[0].content).toBe('First Toast'); - expect(result.current.visibleToasts[1].content).toBe('Third Toast'); + expect(result.current.visibleToasts[0].content).toBe('Third Toast'); + expect(result.current.visibleToasts[1].content).toBe('First Toast'); }); it('should close a toast', () => { @@ -211,10 +211,10 @@ describe('useToastState', () => { act(() => {result.current.add('Second Toast');}); expect(result.current.visibleToasts.length).toBe(1); - expect(result.current.visibleToasts[0].content).toBe(newValue[0].content); + expect(result.current.visibleToasts[0].content).toBe('Second Toast'); act(() => {result.current.close(result.current.visibleToasts[0].key);}); expect(result.current.visibleToasts.length).toBe(1); - expect(result.current.visibleToasts[0].content).toBe('Second Toast'); + expect(result.current.visibleToasts[0].content).toBe(newValue[0].content); }); }); From 947da19eeeca7158b218f46e632e5c731bde9b66 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 21 Feb 2025 11:03:10 +1100 Subject: [PATCH 3/7] remove counter from toasts --- packages/@react-aria/toast/docs/useToast.mdx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/@react-aria/toast/docs/useToast.mdx b/packages/@react-aria/toast/docs/useToast.mdx index da0b562b065..705de798d7a 100644 --- a/packages/@react-aria/toast/docs/useToast.mdx +++ b/packages/@react-aria/toast/docs/useToast.mdx @@ -152,11 +152,9 @@ function Toast({state, ...props}: ToastProps) { ``` ```tsx example -let i = 0; - {state => ( - + )} ``` @@ -239,12 +237,10 @@ Toasts support a `timeout` option to automatically hide them after a certain amo 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. ```tsx example -let i = 0; - {state => ( ///- begin highlight -/// - @@ -257,8 +253,6 @@ let i = 0; Toasts may be programmatically dismissed if they become irrelevant before the user manually closes them. `state.add` returns a key for the toast which may be passed to `state.close` to dismiss the toast. ```tsx example -let i = 0; - function Example() { let [toastKey, setToastKey] = React.useState(null); @@ -269,7 +263,7 @@ function Example() { onPress={() => { if (!toastKey) { ///- begin highlight -/// - setToastKey(state.add('Unable to save ' + i++, {onClose: () => setToastKey(null)})); + setToastKey(state.add('Unable to save', {onClose: () => setToastKey(null)})); ///- end highlight -/// } else { ///- begin highlight -/// From d5ede4c2494568a845adf0e9e05a8b52b4ab1c09 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 21 Feb 2025 11:36:58 +1100 Subject: [PATCH 4/7] remove priority references --- packages/@react-aria/toast/docs/useToast.mdx | 2 +- packages/react-aria-components/docs/Toast.mdx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/toast/docs/useToast.mdx b/packages/@react-aria/toast/docs/useToast.mdx index f9042dbb39f..ce83237e078 100644 --- a/packages/@react-aria/toast/docs/useToast.mdx +++ b/packages/@react-aria/toast/docs/useToast.mdx @@ -54,7 +54,7 @@ There is no built in way to display toast notifications in HTML. -A toast region is an ARIA landmark region 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 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 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. Landmark regions including the toast container can be navigated using the keyboard by pressing the F6 key to move forward, and the Shift + F6 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. diff --git a/packages/react-aria-components/docs/Toast.mdx b/packages/react-aria-components/docs/Toast.mdx index e6e61863d50..d581fd2874c 100644 --- a/packages/react-aria-components/docs/Toast.mdx +++ b/packages/react-aria-components/docs/Toast.mdx @@ -84,7 +84,7 @@ export function App() { Then, you can trigger a toast from anywhere using the exported `queue`. ```tsx example -