-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: S2 toast #7975
base: main
Are you sure you want to change the base?
feat: S2 toast #7975
Conversation
# Conflicts: # packages/@react-aria/toast/src/useToast.ts # packages/@react-aria/toast/test/useToast.test.js # packages/@react-spectrum/s2/package.json # packages/@react-spectrum/toast/test/ToastContainer.test.js # packages/@react-stately/toast/src/useToastState.ts # packages/react-aria-components/docs/Toast.mdx # packages/react-aria-components/package.json # packages/react-aria-components/src/Toast.tsx # packages/react-aria-components/src/index.ts # yarn.lock
# Conflicts: # packages/react-aria-components/src/Toast.tsx
This comment was marked as outdated.
This comment was marked as outdated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really really good, only have a couple comments so far.
When keyboard navigating, it's not always clear that focus has gone to the region. Especially when the region is the same focus scope, just behind all the others. So it looks like there is a bug taking two Tabs to get back into the newest toast after dismissing one. Other times, the region focus ring doesn't show at all.
First one:
Open a few toasts
Keyboard navigate to them
Expand all
Esc
Focus is on the region but kind of looks like it's on the top toast, and it takes two tabs to get back inside the toast
Second:
Open a few toasts
Keyboard navigate to them
Keyboard navigate to the close button
Top toast dismisses
Focus goes to the toast region, but is not visible
was breaking animations
# Conflicts: # lib/viewTransitions.d.ts # packages/react-aria-components/src/Toast.tsx
# Conflicts: # lib/viewTransitions.d.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a heads up that I'm seeing a content jump depending on the toast I open/close. This is present on both my monitors and likely because my scrollbars are always on. I'm not sure why though. I can't find anything about the viewTransition hiding the scrollbars, so I'm not sure why they flicker briefly.
I'm assuming it has something to do with:
In the expanded state, the whole toast lists acts kind of like a modal. There's a transparent backdrop that covers the rest of the page, we aria hide outside, prevent page scrolling, and contain focus.
So I'm trying to see if it's briefly thinking that it's expanded. Update, it doesn't think it's briefly expanded, there is no rerender, as expected, of the ToastContainer. It's also not calling usePreventScroll.
Possibly related if I understood it better? [css-view-transitions-2] Define behavior of scrollbars and overflow with layered capture
Screen.Recording.2025-04-09.at.3.55.44.pm.mov
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing I see blocking, the reduced motion looks good. Only after removing one in an expanded state does it still motion animate a bit. But I don't think we can realistically do much about that. If we instantly change the content I think that would be confusing.
children: (renderProps: {toast: QueuedToast<T>}) => ReactElement | ||
} | ||
|
||
export const ToastList = /*#__PURE__*/ (forwardRef as forwardRefType)(function ToastList<T>(props: ToastListProps<T>, ref: ForwardedRef<HTMLOListElement>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have a test for the newly split out ToastList setup? It could be a chromatic of the s2 Toast, so long as there is something so we know if S2 Toasts stopped working catastrophically. We should get some actual s2 tests as well.
staticColor="white" | ||
styles={style({gridArea: 'expand'})} | ||
// Make the chevron line up with the toast text, even though there is padding within the button. | ||
UNSAFE_style={{marginInlineStart: variant === 'neutral' ? -10 : 14}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
## API Changes
react-aria-components/react-aria-components:ColorWheelTrack ColorWheelTrack {
className?: string | ((ColorWheelTrackRenderProps & {
defaultClassName: string | undefined
})) => string
+ id?: string
style?: CSSProperties | ((ColorWheelTrackRenderProps & {
defaultStyle: CSSProperties
})) => CSSProperties | undefined
} /react-aria-components:UNSTABLE_ToastRegion UNSTABLE_ToastRegion <T> {
aria-describedby?: string
aria-details?: string
aria-label?: string = "Notifications"
aria-labelledby?: string
- children: ({
+ children: ReactNode | ({
toast: QueuedToast<T>
}) => ReactElement
className?: string | ((ToastRegionRenderProps<T> & {
defaultClassName: string | undefined
queue: ToastQueue<T>
style?: CSSProperties | ((ToastRegionRenderProps<T> & {
defaultStyle: CSSProperties
})) => CSSProperties | undefined
} /react-aria-components:UNSTABLE_ToastQueue UNSTABLE_ToastQueue <T> {
add: (T, ToastOptions) => string
+ clear: () => void
close: (string) => void
constructor: (ToastStateProps) => void
pauseAll: () => void
resumeAll: () => void
- subscribe: (() => void) => () => boolean
+ subscribe: (() => void) => () => void
visibleToasts: Array<QueuedToast<T>>
} /react-aria-components:GridLayout GridLayout <O extends GridLayoutOptions = GridLayoutOptions, T> {
getContentSize: () => Size
getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
- getLayoutInfo: (Key) => LayoutInfo | null
+ getLayoutInfo: (Key) => LayoutInfo
getVisibleLayoutInfos: (Rect) => Array<LayoutInfo>
shouldInvalidate: (Rect, Rect) => boolean
shouldInvalidateLayoutOptions: (GridLayoutOptions, GridLayoutOptions) => boolean
update: (InvalidationContext<GridLayoutOptions>) => void
virtualizer: Virtualizer<{}, any> | null
} /react-aria-components:ColorWheelTrackProps ColorWheelTrackProps {
className?: string | ((ColorWheelTrackRenderProps & {
defaultClassName: string | undefined
})) => string
+ id?: string
style?: CSSProperties | ((ColorWheelTrackRenderProps & {
defaultStyle: CSSProperties
})) => CSSProperties | undefined
} /react-aria-components:ToastRegionProps ToastRegionProps <T> {
aria-describedby?: string
aria-details?: string
aria-label?: string = "Notifications"
aria-labelledby?: string
- children: ({
+ children: ReactNode | ({
toast: QueuedToast<T>
}) => ReactElement
className?: string | ((ToastRegionRenderProps<T> & {
defaultClassName: string | undefined
queue: ToastQueue<T>
style?: CSSProperties | ((ToastRegionRenderProps<T> & {
defaultStyle: CSSProperties
})) => CSSProperties | undefined
} /react-aria-components:ToastRegionRenderProps ToastRegionRenderProps <T> {
isFocusVisible: boolean
isFocused: boolean
+ isHovered: boolean
visibleToasts: Array<QueuedToast<T>>
} /react-aria-components:UNSTABLE_ToastList+UNSTABLE_ToastList <T> {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string = "Notifications"
+ aria-labelledby?: string
+ children: ({
+ toast: QueuedToast<T>
+}) => ReactElement
+ className?: string | ((ToastRegionRenderProps<T> & {
+ defaultClassName: string | undefined
+})) => string
+ style?: CSSProperties | ((ToastRegionRenderProps<T> & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /react-aria-components:UNSTABLE_ToastStateContext+UNSTABLE_ToastStateContext {
+ UNTYPED
+} /react-aria-components:ToastListProps+ToastListProps <T> {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string = "Notifications"
+ aria-labelledby?: string
+ children: ({
+ toast: QueuedToast<T>
+}) => ReactElement
+ className?: string | ((ToastRegionRenderProps<T> & {
+ defaultClassName: string | undefined
+})) => string
+ style?: CSSProperties | ((ToastRegionRenderProps<T> & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} @react-aria/overlays/@react-aria/overlays:Overlay Overlay {
children: ReactNode
disableFocusManagement?: boolean
isExiting?: boolean
- portalContainer?: Element = document.body
shouldContainFocus?: boolean
} /@react-aria/overlays:OverlayProps OverlayProps {
children: ReactNode
disableFocusManagement?: boolean
isExiting?: boolean
- portalContainer?: Element = document.body
shouldContainFocus?: boolean
} @react-spectrum/s2/@react-spectrum/s2:UNSTABLE_ToastContainer+UNSTABLE_ToastContainer {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string = "Notifications"
+ aria-labelledby?: string
+ className?: string | ((ToastRegionRenderProps<T> & {
+ defaultClassName: string | undefined
+})) => string
+ placement?: ToastPlacement = "bottom"
+ style?: CSSProperties | ((ToastRegionRenderProps<T> & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} /@react-spectrum/s2:UNSTABLE_ToastQueue+UNSTABLE_ToastQueue {
+ info: (string, ToastOptions) => CloseFunction
+ negative: (string, ToastOptions) => CloseFunction
+ neutral: (string, ToastOptions) => CloseFunction
+ positive: (string, ToastOptions) => CloseFunction
+} /@react-spectrum/s2:AutocompleteContext+AutocompleteContext {
+ UNTYPED
+} /@react-spectrum/s2:AutocompleteStateContext+AutocompleteStateContext {
+ UNTYPED
+} /@react-spectrum/s2:ToastOptions+ToastOptions {
+ actionLabel?: string
+ id?: string
+ onAction?: () => void
+ onClose?: () => void
+ shouldCloseOnAction?: boolean
+ timeout?: number
+} /@react-spectrum/s2:ToastContainerProps+ToastContainerProps {
+ aria-describedby?: string
+ aria-details?: string
+ aria-label?: string = "Notifications"
+ aria-labelledby?: string
+ className?: string | ((ToastRegionRenderProps<T> & {
+ defaultClassName: string | undefined
+})) => string
+ placement?: ToastPlacement = "bottom"
+ style?: CSSProperties | ((ToastRegionRenderProps<T> & {
+ defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+} @react-stately/layout/@react-stately/layout:GridLayout GridLayout <O extends GridLayoutOptions = GridLayoutOptions, T> {
getContentSize: () => Size
getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
- getLayoutInfo: (Key) => LayoutInfo | null
+ getLayoutInfo: (Key) => LayoutInfo
getVisibleLayoutInfos: (Rect) => Array<LayoutInfo>
shouldInvalidate: (Rect, Rect) => boolean
shouldInvalidateLayoutOptions: (GridLayoutOptions, GridLayoutOptions) => boolean
update: (InvalidationContext<GridLayoutOptions>) => void
virtualizer: Virtualizer<{}, any> | null
} @react-stately/toast/@react-stately/toast:ToastQueue ToastQueue <T> {
add: (T, ToastOptions) => string
+ clear: () => void
close: (string) => void
constructor: (ToastStateProps) => void
pauseAll: () => void
resumeAll: () => void
- subscribe: (() => void) => () => boolean
+ subscribe: (() => void) => () => void
visibleToasts: Array<QueuedToast<T>>
} /@react-stately/toast:ToastStateProps ToastStateProps {
maxVisibleToasts?: number
- wrapUpdate?: (() => void) => void
+ wrapUpdate?: (() => void, ToastAction) => void
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a few comments from local testing but otherwise seems good enough for an alpha
export interface ToastOptions extends Omit<RACToastOptions, 'priority'>, DOMProps { | ||
/** A label for the action button within the toast. */ | ||
actionLabel?: string, | ||
/** Handler that is called when the action button is pressed. */ | ||
onAction?: () => void, | ||
/** Whether the toast should automatically close when an action is performed. */ | ||
shouldCloseOnAction?: boolean | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to mention that the minimum time that the toast stays open is 5 seconds in the prop description, otherwise people might get confused why their provided timeout doesn't seem to work. Would also be nice to tie the storybook onAction to a control but that can be followup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that the view transition prevents subsequent clicks from actually happening while the transition is in effect which seems to be an intentional behavior of view transitions. Do we want to hack around it and keep the page interactive while transitioning or do we deem it a unlikely case that someone might try to click on a different element while a Toast is appearing/exiting?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, see
#7838 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah derp, forgot it was mentioned there. Seems we could do something like this though: https://www.bram.us/2025/01/29/view-transitions-page-interactivity/ perhaps?
This implements the Toast component in Spectrum 2, with the new toast stacking animation implemented with CSS view transitions. Style macros don't have an API for view transitions yet, and I haven't come up with a good API for that yet. In the meantime, I used a CSS module file for the toast animations.
A couple RAC Toast changes were needed:
ToastList
component fromToastRegion
so that I could add some additional elements adjacent to the list, namely the clear/collapse buttons when the stack is expanded. If you pass a function directly to theToastRegion
as before then we'll render aToastList
for you. Not sure I'm totally happy with this API.ToastRegion
so I could implement an animation.action
parameter to the toast queue subscription function, which tells you what happened (e.g. a toast was added, removed, etc.). This allows us to change the animations accordingly.Some stuff that's specific to Spectrum:
Test instructions
Make sure to test the animations with reduce motion settings enabled as well. In this mode, the toasts fade in/out instead of animating their positions.