Skip to content

Apply user updates for mobile navigation accessibility #31401

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

Open
wants to merge 15 commits into
base: next
Choose a base branch
from

Conversation

yatishgoel
Copy link
Contributor

@yatishgoel yatishgoel commented May 7, 2025

Closes #31380

What I did

This PR significantly enhances the accessibility of the mobile navigation and addresses focus management issues, resolving the core concerns raised in #31380. It also incorporates the DOM order fix for desktop and mobile reading consistency from #31076.

Key improvements:

  1. ARIA Attributes for Mobile Navigation:

    • The mobile menu toggle button now correctly uses aria-label, aria-expanded, and aria-controls to properly communicate its state and relationship with the menu drawer (MobileNavigation.tsx).
    • The addon panel toggle button also includes aria-label and aria-expanded.
  2. Accessible Mobile Menu Drawer (MobileMenuDrawer.tsx):

    • The menu drawer now has role="dialog", aria-modal="true", and an aria-label for clear anouncement to screen reader users.
    • The content area within the drawer (holding the navigation links) is marked with role="navigation".
  3. Focus Management and Trapping:

    • A robust useFocusTrap hook has been implemented and applied to MobileMenuDrawer.tsx.
    • When the mobile menu opens, focus is now programmatically moved into the drawer, targeting the first focusable element.
    • Focus is trapped within the open menu, allowing navigation via Tab and Shift+Tab, and closing via the Escape key.
    • Focus is correctly restored to the previously focused element (the menu button) when the drawer is closed.
    • The main content area is marked with aria-hidden="true" and tabIndex="-1" when the mobile menu is open to prevent interaction with background content (Layout.tsx).
  4. DOM Order for Reading Consistency (Addresses Fix Storybook accessibility order #31076):

    • Desktop View: The DOM order in Layout.tsx ensures <SidebarContainer> renders before the main content (<MainContentMatcher>) for a logical reading order.
    • Mobile View: The <MobileNavigation> trigger component is rendered first in the DOM within its mobile layout block, while CSS order is used for its visual placement at the bottom. This ensures screen readers encounter the navigation trigger in a logical sequence.
  5. General Code Refinements:

    • Centralized initial focus logic within useFocusTrap.
    • Improved focusable element detection in useFocusTrap.
    • Added id props and aria-controls for the MobileAddonsDrawer as well, preparing it for similar focus trap implementation if it contains interactive elements.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!
This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

  1. Run a sandbox for a relevant template, e.g., yarn task --task sandbox --start-from auto --template react-vite/default-ts
  2. Open Storybook in your browser.
  3. Desktop View:
    • Using a screen reader (e.g., NVDA, VoiceOver), verify that the sidebar content is announced before the main story/docs content.
  4. Mobile View (resize browser or use dev tools):
    • Verify the mobile navigation bar appears visually at the bottom.
    • Using a screen reader:
      • Check that the "Open navigation menu" button is focusable and correctly announces its purpose and state (collapsed).
      • Activate the button.
      • Verify the "Navigation menu" dialog is announced as open and modal.
      • Verify focus moves into the menu (e.g., to the first navigation item).
      • Navigate within the menu using Tab and Shift+Tab. Ensure focus stays within the menu.
      • Press Escape. Verify the menu closes and focus returns to the "Open navigation menu" button.
      • Verify the main content behind the menu was hidden from the screen reader (aria-hidden="true") when the menu was open.
    • Repeat similar checks for the "Open addon panel" button and its drawer if it contains interactive elements (note: focus trap for addon drawer is suggested but might be implemented separately if not part of this direct change).

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr=<PR_NUMBER>

Greptile Summary

Here's a concise summary of the key accessibility improvements made to the mobile navigation components:

Enhanced mobile navigation components with comprehensive accessibility features and proper DOM ordering.

  • Added proper ARIA attributes to navigation elements (role="dialog", aria-modal, aria-controls, aria-expanded) in MobileNavigation.tsx and MobileMenuDrawer.tsx
  • Implemented focus trap management with new useFocusTrap hook for modal dialogs
  • Fixed DOM order in Layout.tsx to ensure sidebar renders before main content for logical screen reader flow
  • Added aria-hidden and tabIndex controls to main content when mobile menu is open
  • Improved keyboard navigation with proper focus restoration and trap management within modal dialogs

The changes significantly improve screen reader compatibility and keyboard navigation while maintaining visual layout through CSS ordering rather than DOM manipulation.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 file(s) reviewed, 6 comment(s)
Edit PR Review Bot Settings | Greptile

Copy link

nx-cloud bot commented May 7, 2025

View your CI Pipeline Execution ↗ for commit c6daf4e.

Command Status Duration Result
nx run-many -t build --parallel=3 ✅ Succeeded 1m 11s View ↗

☁️ Nx Cloud last updated this comment at 2025-06-14 10:11:54 UTC

@yannbf yannbf added bug accessibility ci:merged Run the CI jobs that normally run when merged. labels May 7, 2025
@ndelangen
Copy link
Member

@MichaelArestad Do you have any input here?

Copy link
Contributor

@MichaelArestad MichaelArestad left a comment

Choose a reason for hiding this comment

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

@yatishgoel Nice!

@ndelangen These changes seem great. I am not an expert on aria attributes and I have not implemented anything like useFocusTrap though I understand the reasoning.

I believe this breaks a few Interaction tests in Navigation that were targeting elements with a specific title attribute. We shouldn't be using title attributes for this anyway. title attributes are not designed to be used for anything other than iframes.

Regarding the title attributes, they were being used as tooltips, which isn't great. In the near future, I do think we should probably:

  1. Create new tooltips that are accessible, specifically the tooltips designed to show a short string (black background, white text).
  2. Implement those tooltips in almost every situation where we use title.

@MichaelArestad
Copy link
Contributor

@ndelangen Can you code review this when you get a chance? Thank you!

@MichaelArestad MichaelArestad requested a review from ndelangen May 16, 2025 15:59
Copy link
Member

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

Thanks so much for undertaking this work, @yatishgoel! This is hard work so please don't take anything in my review as a criticism of your effort. I very much appreciate you giving this a go!

You applied a dialog role with an aria-modal attribute. That's great, and it's what I wrote in the issue so really this is my own fault! It's unfortunately missing a powerful element of the Dialog element: showModal. The showModal function lets you instruct the browser to open a modal and install a focus trap for you, which greatly reduces the amount of code to write and makes better use of standards.

You also used the navigation role in your code. It would be preferrable to use the nav element directly instead. Generally speaking (and as reminded on https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/navigation_role), it's preferable to use the HTML standard element rather than its inferred role and attributes.

I shouldn't have mentioned role in my issue. I sometimes refer to roles instead of elements out of habit because I don't know if the HTML markup is controlled and can be edited, but when a role can be fulfilled through the use of an element, it should. That allows for better future compatibility and easier maintenance.

To move forward, I suggest removing the focus trap implementation and using showModal instead, and switching to actual nav/dialog elements. Then, there would only be minor details left to address.

…ate management. Updated MobileAddonsDrawer and MobileMenuDrawer to use dialog elements for better focus control. Removed unused useFocusTrap hook. Simplified MainContentMatcher by removing unnecessary props. Enhanced MobileNavigation to manage addon panel state more effectively.
@yatishgoel yatishgoel requested a review from Sidnioulz May 19, 2025 18:47
Copy link
Member

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

A small bug left to fix and a refactor could be made to regroup the modal hooks into a single file, but this is great overall and would definitely fix the linked issue. Kudos, @yatishgoel!

…ialog hook for improved dialog management. Introduced useModalDialog for better encapsulation of dialog logic and enhanced accessibility. Updated shortcut handling to skip events when a dialog is open.
@yatishgoel yatishgoel requested a review from Sidnioulz May 29, 2025 17:01
Comment on lines 27 to 39
export const MobileAddonsDrawer: FC<MobileAddonsDrawerProps> = ({
children,
id,
isOpen,
onClose,
}) => {
const dialogRef = useModalDialog({ isOpen, onClose });

return (
<Container ref={dialogRef} id={id} aria-label="Addon panel">
{children}
</Container>
);
Copy link
Member

Choose a reason for hiding this comment

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

As it is no longer a div, you'll need to adjust positioning.

Your branch
image

Copy link
Member

Choose a reason for hiding this comment

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

@yatishgoel also, the Close button does not work on the addon panel.

…ling

- Introduced `forceCloseDialog` function in MobileMenuDrawer to encapsulate dialog closing logic.
- Updated onExited callback to use `forceCloseDialog` for cleaner code.
- Enhanced useModalDialog to handle Escape key for closing the dialog and prevent default behavior.
- Removed redundant dialog close logic from useModalDialog to streamline the component's functionality.
Copy link
Member

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

Congrats on fixing the animations! Can you please test the mobile addon panel again? I see a few remaining issues:

  • It's positioned in the middle of the screen instead of the bottom
  • The close button doesn't close the panel
  • It seems I have to press Esc 2-3 times to close the panel

- Fix MobileMenuDrawer and MobileAddonsDrawer appearing centered instead of bottom
- Add full width positioning for mobile dialogs
- Fix Esc key requiring double press in addon drawer
- Fix close button not working properly
- Ensure smooth exit animations with proper timing coordination
- Override browser default modal positioning with &[open] selector
@yatishgoel
Copy link
Contributor Author

Congrats on fixing the animations! Can you please test the mobile addon panel again? I see a few remaining issues:

  • It's positioned in the middle of the screen instead of the bottom
  • The close button doesn't close the panel
  • It seems I have to press Esc 2-3 times to close the panel

Thanks for the feedback! I actually just fixed those exact issues in both MobileAddonsDrawer.tsx and MobileMenuDrawer.tsx:

Positioning: Added position: 'fixed', bottom: 0, right: 0, left: 0, top: 'auto' with &[open] selector to override browser default modal positioning, plus maxWidth: '100vw' for full width.

Close Behavior: Converted MobileAddonsDrawer to use React Transition with the same working pattern as MobileMenuDrawer - added forceCloseDialog function that runs after exit animation completes. This should fix both the close button and single Esc key press.

Both mobile drawers now use identical patterns: React Transition + useModalDialog + forceCloseDialog for behavior, and the same CSS positioning overrides. MobileMenuDrawer was working correctly, so I used it as the reference to fix MobileAddonsDrawer. Both should now appear at bottom with full width and proper close behavior.

@storybook-pr-benchmarking
Copy link

Package Benchmarks

Commit: c6daf4e, ran on 14 June 2025 at 10:17:57 UTC

The following packages have significant changes to their size or dependencies:

storybook

Before After Difference
Dependency count 49 49 0
Self size 31.85 MB 31.87 MB 🚨 +18 KB 🚨
Dependency size 17.41 MB 17.41 MB 0 B
Bundle Size Analyzer Link Link

sb

Before After Difference
Dependency count 50 50 0
Self size 1 KB 1 KB 0 B
Dependency size 49.26 MB 49.28 MB 🚨 +18 KB 🚨
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 216 216 0
Self size 582 KB 582 KB 0 B
Dependency size 94.57 MB 94.59 MB 🚨 +18 KB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 185 185 0
Self size 31 KB 31 KB 0 B
Dependency size 78.71 MB 78.73 MB 🚨 +18 KB 🚨
Bundle Size Analyzer Link Link

Copy link
Member

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

Code does everything I'd like it to do! Now this is just a matter of getting the CI passing.

We can now turn our attention to the CI :) I haven't looked in detail at the CI reports, but if you get stuck, feel free to ask me to take over and I will have a look. It seems there are a few tests that need to be updated, maybe you could start with this? https://app.circleci.com/jobs/github/storybookjs/storybook/843273

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accessibility bug ci:merged Run the CI jobs that normally run when merged.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: HTML markup accessibility issues with mobile navigation button
5 participants