Toast 🍞#3868
Conversation
|
|
||
| const SpectrumToastQueue = { | ||
| /** Queues a neutral toast. */ | ||
| neutral(children: ReactNode, options: SpectrumToastOptions = {}): CloseFunction { |
There was a problem hiding this comment.
Should we limit children to type string?
snowystinger
left a comment
There was a problem hiding this comment.
Tested in Chrome and Safari w/ VO, working as expected
| 'aria-label': props['aria-label'], | ||
| 'aria-labelledby': props['aria-labelledby'] || titleId, |
There was a problem hiding this comment.
Do these need to go through useLabels to construct a aria-labelledby that announces the user provided aria labeling + the title/description of the toast itself? Perhaps a non-issue/minor point, not sure when someone would have a toast title and still provide a aria-label/aria-labelledby
There was a problem hiding this comment.
Maybe. TBH I'm not sure if this is even working. The goal was to prevent screen readers from announcing the close button after reading the content of the toast (by pointing aria-labelledby directly to the title), but in my testing VoiceOver at least still reads everything. So this might not actually do anything at all. Further testing in other screen readers needed. Will handle later.
| onBlurWithin: () => { | ||
| state.resumeAll(); | ||
| lastFocused.current = null; | ||
| } |
There was a problem hiding this comment.
Random thought: what if the user changes windows while focused inside the toast? I kinda feel like that could be a case where the toast timers should pause, but not sure there is a great way to distinguish between that scenario and if focus is being lost to the body (both have relatedTarget = null).
There was a problem hiding this comment.
there are page visibility events we could use but not gonna do that for this PR :D
| state: ToastState<unknown> | ||
| } | ||
|
|
||
| export function Toaster(props: ToastContainerProps): ReactElement { |
| import {SpectrumToastValue, Toast} from './Toast'; | ||
| import {Toaster} from './Toaster'; | ||
| import {ToastOptions, ToastQueue, useToastQueue} from '@react-stately/toast'; | ||
| import {useSyncExternalStore} from 'use-sync-external-store/shim'; |
There was a problem hiding this comment.
Can you add a comment about why we are using this shim? Either here, in its usage or in the stately hook
Co-authored-by: Daniel Lu <dl1644@gmail.com>
🎄🎁🌟 Happy holidays! 🌟🎁🎄
This implements the Toast component in React Spectrum and React Aria. It's implemented as a labeled landmark region using our existing landmark hooks for keyboard navigation. Following the Spectrum guidelines, a single toast is displayed at a time, with a priority queue system. In React Aria, the visible toast limit is configurable.
There is a global queue of toasts that toasts are added to, and a
<ToastContainer>component to render them. Only oneToastContainerwill render at a time, even if an app renders more than one. If one ToastContainer unmounts, another one will take over displaying the toasts if rendered somewhere else. This is so components such as those in quarry can render their own ToastContainer and not depend on the app providing one. But if multiple components do so, the toasts are still only displayed once.Users can navigate to the toast region using landmark navigation (F6). When a toast closes, focus is moved to the next toast in the queue if any. Otherwise, it moves back to wherever focus was last before entering the toast container.
We ensure that focus management works, and toasts are never
aria-hidden, even when containing FocusScopes such as dialogs are open. This is implemented by assigning adata-react-aria-top-layerattribute to the toast region element. Elements with this attribute will not be aria-hidden by our utilities, always allow focus to them even when not inside a containing FocusScope, and will not cause overlays to close when clicking on them.This will be complicated somewhat by apps like Unified Shell which use iframes. That breaks landmark navigation, and also may cause toasts outside the frame to overlap with those inside the iframe rather than being merged into a single queue. To handle this, when a toast is queued, a custom DOM event is emitted on the window. Shell or others could handle this event and call
preventDefaulton it. If that happens, the toast is not added to the local queue. Shell would be responsible for post messaging out of the iframe and re-queuing the toast outside. They would also be responsible for managing focus to navigate both out and back into the frame.