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

Toast focus management take 2 #6223

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

Toast focus management take 2 #6223

wants to merge 31 commits into from

Conversation

snowystinger
Copy link
Member

@snowystinger snowystinger commented Apr 17, 2024

Closes

In association with #6011

There are still some known issues, they are listed in our GA ticket https://github.com/orgs/adobe/projects/19/views/4?pane=issue&itemId=55618129

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

Test instructions in #6011 description

🧢 Your Project:

@rspbot
Copy link

rspbot commented Apr 23, 2024

@rspbot
Copy link

rspbot commented Apr 29, 2024

@rspbot
Copy link

rspbot commented Apr 30, 2024

@rspbot
Copy link

rspbot commented May 2, 2024

@rspbot
Copy link

rspbot commented May 3, 2024

@snowystinger snowystinger marked this pull request as ready for review May 3, 2024 18:23
@rspbot
Copy link

rspbot commented May 3, 2024

@rspbot
Copy link

rspbot commented May 8, 2024

@majornista
Copy link
Collaborator

@snowystinger With recent changes, focus is getting lost to the document.body when the top toast, the last item in the list gets removed and other toasts remain.

@rspbot
Copy link

rspbot commented May 9, 2024

@rspbot
Copy link

rspbot commented May 10, 2024

@rspbot
Copy link

rspbot commented May 10, 2024

@rspbot
Copy link

rspbot commented May 16, 2024

@rspbot
Copy link

rspbot commented May 16, 2024

@rspbot
Copy link

rspbot commented May 20, 2024

@rspbot
Copy link

rspbot commented May 20, 2024

@rspbot
Copy link

rspbot commented May 20, 2024

@rspbot
Copy link

rspbot commented May 21, 2024

majornista
majornista previously approved these changes May 21, 2024
Copy link
Collaborator

@majornista majornista left a comment

Choose a reason for hiding this comment

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

I tested this change using NVDA in Firefox, Chrome and Edge, and the Toast messages are being announced.

@rspbot
Copy link

rspbot commented May 21, 2024

@snowystinger snowystinger mentioned this pull request May 22, 2024
5 tasks
@rspbot
Copy link

rspbot commented May 22, 2024


let titleId = useId();
let descriptionId = useSlotId();
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/toast');

return {
toastProps: {
role: 'alert',
role: 'alertdialog',
'aria-modal': 'false',
Copy link
Member

Choose a reason for hiding this comment

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

non-action, just interested that this is specifically set to false. I'm just used to omitting the attribute but I'm guessing this explicit false made a difference in the screen reader behavior?

Copy link
Member Author

Choose a reason for hiding this comment

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

@majornista correct me if I'm wrong, but this is just to possibly future proof? since alertdialog would be considered a modal by default, and so we use aria-modal to communicate that it's not, in fact, a modal

Copy link
Collaborator

Choose a reason for hiding this comment

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

aria-modal="false" doesn't really make a difference for screen reader behavior; the default is for a dialog to be non-modal. However, the APG design pattern describes an alertdialog as modal, so I thought it best to be explicit:

An alert dialog is a modal dialog that interrupts the user's workflow to communicate an important message and acquire a response. Examples include action confirmation prompts and error message confirmations. The alertdialog role enables assistive technologies and browsers to distinguish alert dialogs from other dialogs so they have the option of giving alert dialogs special treatment, such as playing a system alert sound.

I use role="alertdialog", as a container for the toast message content, which has role="alert" to announce as a live region, and the button to dismiss the Toast, for a few reasons.

  1. A Toast is kind of a tiny non-modal dialog, so it didn't seem like much of a stretch.
  2. Using a dialog as a container helps disambiguate between the Dismiss buttons when more than one Toast is displayed.
  3. When we close a Toast, unlike the Dismiss button for an adjacent Toast, the dialog provides a uniquely labeled widget element to receive focus, and is easy to style using the React-Spectrum focus ring.
  4. Depending on the assistive technology being used, an alert dialog will open with an earcon, which I see as nice feature to distinguish a Toast from other content the screen reader may be announcing.

@@ -39,6 +41,7 @@ export interface ToastAria {
* Provides the behavior and accessibility implementation for a toast component.
* Toasts display brief, temporary notifications of actions, errors, or other events in an application.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Copy link
Member

Choose a reason for hiding this comment

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

Can we just remove the ref from the args even though it would be breaking? It is still in beta so might be ok?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's more of a, we've needed to add it back in later in other cases. I'd prefer to know we'll have access to it

@@ -26,7 +26,7 @@ function Text(props: TextProps, ref: DOMRef) {
let domRef = useDOMRef(ref);

return (
<span {...filterDOMProps(otherProps)} {...styleProps} ref={domRef}>
<span role="none" {...filterDOMProps(otherProps)} {...styleProps} ref={domRef}>
Copy link
Member

Choose a reason for hiding this comment

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

I see this was added to prevent double announcements, but where do we use Text in toasts?

Copy link
Member

Choose a reason for hiding this comment

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

Ah I see, the Button for the toast action. Will have to double check everywhere we use Text to see if there are any changes to the screen reader performance

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, i haven't noticed anything yet

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, this was something I noticed in testing with VoiceOver. When a Toast included an action button, the button text was announced twice. This has to do with how live regions work, announcing both element "additions" and "text." VoiceOver seem to interpret the Text component's span element child of the Button as an addition, and announces the text more than once.

Comment on lines 72 to 88
if (removedToastIndex > -1) {
let i = 0;
let nextToast;
let prevToast;
while (i <= removedToastIndex) {
if (!removedToasts[i].isRemoved) {
prevToast = Math.max(0, i - 1);
}
i++;
}
while (i < removedToasts.length) {
if (!removedToasts[i].isRemoved) {
nextToast = i - 1;
break;
}
i++;
}
Copy link
Member

Choose a reason for hiding this comment

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

bit of a nit, but I found this a bit hard to follow, could we instead just slice the removedToasts list into two (one going from the 0 -> removedToastIndex and the other going from removedToastIndex -> end) and use findLastIndex with the same !removedToasts.isRemoved condition on the first slice to get the index of the prevToast and findIndex + the removedToastIndex on the second slice to get the nextToast index?

Thinking on this, it might just be the "removedToasts" naming that is tripping me up since that list contains all the toasts but just adds extra data to mark the removed ones.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, it was hard to write too
I've clarified with renames and some more comments

@rspbot
Copy link

rspbot commented May 24, 2024

@rspbot
Copy link

rspbot commented May 24, 2024

@rspbot
Copy link

rspbot commented May 24, 2024

## API Changes

unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any', access: 'private' }
unknown top level export { type: 'any', access: 'private' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'identifier', name: 'Column' }
unknown top level export { type: 'identifier', name: 'Column' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }
unknown top level export { type: 'any' }

@react-aria/toast

ToastAria

 ToastAria {
   closeButtonProps: AriaButtonProps
+  contentProps: DOMAttributes
   descriptionProps: DOMAttributes
   titleProps: DOMAttributes
   toastProps: DOMAttributes
 }

it changed:

  • useToast

Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

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

LGTM

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.

None yet

4 participants