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

Banner accessibility fix #771

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft

Conversation

KaliedaRik
Copy link
Contributor

@KaliedaRik KaliedaRik commented Oct 12, 2022

What does this change?

We need to improve the accessibility of the Banner. Currently:

  • Page focus is not transferred to the banner's close button when the banner appears;
  • Some banner close buttons do not include the appropriate ARIA label attribute;
  • User tab and shift + tab keyboard actions are not constrained to the banner while it is displayed;
  • Some focusable elements within the banner do not update their presentation in an appropriate way (or at all) when they receive keyboard navigation focus; and
  • Page focus does not return to the top of the page after the user dismisses the banner

[TODO: complete PR details]

@KaliedaRik KaliedaRik marked this pull request as draft October 12, 2022 16:06
Copy link
Contributor

@i-hardy i-hardy left a comment

Choose a reason for hiding this comment

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

I hate to be so negative as this has clearly been a lot of work, but I feel like this solution is very brittle and relies on working around React instead of with it.

I've put together a minimal example of a more idiomatic approach to managing element auto-focus on render using a 'callback ref' in this CodeSandbox: https://codesandbox.io/s/dark-voice-oiho69, based on this blog post about refs and side effects.

If we need to be able to pass refs to Source buttons but currently can't then that's not an issue we should have to mess around the edges of, it's something the design system ought to support.

Comment on lines +122 to +123
// Timeout required to allow component to settle before identifying focussable elements within it
setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Arbitrary timeouts to let some other process finish are usually not a good idea. What does the component 'settling' mean, and are we confident it will always take less than 1000 milliseconds? Is there a better way to detect this?

Comment on lines +124 to +141
const closeButtonElements: HTMLElement[] = [
...node.querySelectorAll<HTMLElement>('[aria-label="Close"]'),
].filter(
el =>
!el.hasAttribute('disabled') &&
!el.getAttribute('aria-hidden') &&
!checkIfElementIsHidden(el),
);
const focusableElements: HTMLElement[] = [
...node.querySelectorAll<HTMLElement>(
'button, a[href], input, [tabindex]:not([tabindex="-1"]',
),
].filter(
el =>
!el.hasAttribute('disabled') &&
!el.getAttribute('aria-hidden') &&
!checkIfElementIsHidden(el),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like it would be much more straightforward to use React refs to get instances of the elements we might want to focus, rather than having to do a DOM query for stuff we already control the creation and rendering of.

If we can't currently pass refs to focusable Source components this is something we need to bring up with CSTI and potentially make a PR to Source to implement.

Comment on lines +18 to 26
<Button
aria-label="Close"
onClick={onClick}
css={button}
size="small"
icon={<SvgCross />}
hideLabel
>
Dismiss the banner
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a degradation of the experience for screen reader users- 'Dismiss the banner' is a much richer and more useful prompt than 'Close' (close what, specifically?). It's generally best to avoid using ARIA if regular ol' HTML will do the job, which a button with hideLabel does.

If there's no way around doing DOM queries to get focusable elements and you need an arbitrary selector for these buttons, it would be better to use a data attribute.

if (el.style.display === 'none' || el.style.visibility === 'hidden') {
return true;
}
const computed = window.getComputedStyle(el);
Copy link
Contributor

Choose a reason for hiding this comment

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

Calling getComputedStyle will cause style recalculation and potentially layout reflow. Determining visibility programmatically is tricky, but traversing up a DOM tree and calling this on every node will be terrible for performance, especially as this effectively has no exit condition until you reach document.body.

If we absolutely cannot avoid doing this, I think it would be better to use a TreeWalker (generally much more performant than a JS while loop) with the main container element of the banner as its root node, so it won't work its way up the entire DOM tree.

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

Successfully merging this pull request may close these issues.

2 participants