Skip to content
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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open

feat: S2 toast #7975

wants to merge 22 commits into from

Conversation

devongovett
Copy link
Member

@devongovett devongovett commented Mar 21, 2025

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:

  • I split out a new ToastList component from ToastRegion 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 the ToastRegion as before then we'll render a ToastList for you. Not sure I'm totally happy with this API.
  • I needed to add a hover state to the ToastRegion so I could implement an animation.
  • I added an 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:

  • To implement clicking on the region to expand it, I attached a click event to the ref for now. Wasn't sure if we wanted to add press events to the region or not since it's kind of an unusual pattern.
  • 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.

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.

# 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
@rspbot

This comment was marked as outdated.

@rspbot
Copy link

rspbot commented Mar 21, 2025

@rspbot
Copy link

rspbot commented Mar 21, 2025

## API Changes

react-aria-components

/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
   portalContainer?: Element = document.body
   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: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
   portalContainer?: Element = document.body
   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
+  portalContainer?: Element = document.body
+  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
+  portalContainer?: Element = document.body
+  style?: CSSProperties | ((ToastRegionRenderProps<T> & {
+    defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+}

@react-spectrum/s2

/@react-spectrum/s2:ToastContainer

+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"
+  portalContainer?: Element = document.body
+  style?: CSSProperties | ((ToastRegionRenderProps<T> & {
+    defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+}

/@react-spectrum/s2:ToastQueue

+ToastQueue {
+  info: (string, ToastOptions) => CloseFunction
+  negative: (string, ToastOptions) => CloseFunction
+  neutral: (string, ToastOptions) => CloseFunction
+  positive: (string, ToastOptions) => CloseFunction
+}

/@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"
+  portalContainer?: Element = document.body
+  style?: CSSProperties | ((ToastRegionRenderProps<T> & {
+    defaultStyle: CSSProperties
+})) => CSSProperties | undefined
+}

@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
 }

Copy link
Member

@snowystinger snowystinger left a 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

@@ -73,6 +73,7 @@ export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext} from
export {Tabs, TabList, Tab, TabPanel, TabsContext} from './Tabs';
export {TagGroup, Tag, TagGroupContext} from './TagGroup';
export {TextArea, TextField, TextAreaContext, TextFieldContext} from './TextField';
export {ToastContainer, ToastQueue} from './Toast';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToastContainerContext

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants