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

fix(action menu): keyboard accessibility omnibus #5031

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

Conversation

nikkimk
Copy link
Contributor

@nikkimk nikkimk commented Jan 16, 2025

Action menu items are not reading for screen readers.

Description

Action menu should be using a roving tabindex not aria-activedescendant because of cross-root ARIA limitations as well as lack of iOS support.

The sp-menu that action menu uses was refactored to use a roving tabindex, and the numpad keys fix that was made in action menu are now applied to the focus group controller which the roving tabindex controller uses.

Related issue(s)

Motivation and context

VoiceOver could not read the menu items when navigated via keyboard because of the cross-root aria issues above. Using the same roving tabindex controller that other components in our repo use, allows us to ensure roving tabindex and keyboard navigation is accessible and consistent across all components.

How has this been tested?

  • By default a Menu should follow the Navigation Menu Example from the WAI ARIA APG.
  • Menus with selects and menu groups should function like the menu groups in the Editor Menubar Example from the WAI ARIA APG
  • Please note that there will be VRT differences where open menus that didn't used to have focus now do. Menus that are opened via keyboard are supposed to set focus on a menu item.

Does screenreader read menuitems? (resolves #4556 and without regression on #3751)

  1. Go to https://nikkimk-fix-menu-a11y--spectrum-web-components.netlify.app/components/action-menu/#sizes
  2. Open Voice Over
  3. Tab to the More Actions... menu button.
  4. Press Down Arrow repeatedly to scroll through the menu items
  5. Press Up Arrow repeatedly to scroll through the menu items
  6. Press Numpad Down Arrow repeatedly to and scroll through the menu items
  7. Press Numpad Up Arrow repeatedly to and scroll through the menu items
  8. Screenreader should read menu items

Can you use a screenreader to click a menuitem? (resolves #4997)

  1. Go to https://nikkimk-fix-menu-a11y--spectrum-web-components.netlify.app/components/action-menu/#sizes
  2. Open Voice Over
  3. Tab to the menuitem.
  4. Tab focus to the button.
  5. Press CRTL+Option+Space to click the link.
  6. Link should be activated.

Does keyboard navigation of menuitems work as it should? (closes #4557)

  1. Go to this WAI ARIA APG Menu Button example.
  2. Click on the Actions menu button.
  3. Notice that the menu opens and focus is on the first item.
  4. Press Down Arrow.
  5. Notice that the focus is on the second item.
  6. Now go to https://nikkimk-fix-menu-a11y--spectrum-web-components.netlify.app/storybook/iframe.html?args=&id=action-menu--default&viewMode=story
  7. Click on the More Actions... menu button.
  8. Notice that the menu opens and focus is on the first item.
  9. Press Down Arrow.
  10. Notice that the focus is on the second item.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • [] Breaking change (fix or feature that would cause existing functionality to change) _This change will affect combobox, but combobox was already inaccessible. See RFC for Accessible Menu navigation for more information. _
  • Chore (minor updates related to the tooling or maintenance of the repository, does not impact compiled assets)

Checklist

  • I have signed the Adobe Open Source CLA.
  • My code follows the code style of this project.
  • If my change required a change to the documentation, I have updated the documentation in this pull request.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have reviewed at the Accessibility Practices for this feature, see: Aria Practices

Best practices

This repository uses conventional commit syntax for each commit message; note that the GitHub UI does not use this by default so be cautious when accepting suggested changes. Avoid the "Update branch" button on the pull request and opt instead for rebasing your branch against main.

@nikkimk nikkimk linked an issue Jan 16, 2025 that may be closed by this pull request
1 task
Copy link

changeset-bot bot commented Jan 16, 2025

🦋 Changeset detected

Latest commit: 645342d

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

Branch preview

Review the following VRT differences

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

Copy link

Tachometer results

Currently, no packages are changed by this PR...

Copy link

github-actions bot commented Jan 16, 2025

Lighthouse scores

Category Latest (report) Main (report) Branch (report)
Performance 0.99 0.99 0.98
Accessibility 1 1 1
Best Practices 1 1 1
SEO 1 0.92 0.92
PWA 1 1 1
What is this?

Lighthouse scores comparing the documentation site built from the PR ("Branch") to that of the production documentation site ("Latest") and the build currently on main ("Main"). Higher scores are better, but note that the SEO scores on Netlify URLs are artifically constrained to 0.92.

Transfer Size

Category Latest Main Branch
Total 249.797 kB 236.281 kB 🏆 236.664 kB
Scripts 60.802 kB 54.239 kB 🏆 54.642 kB
Stylesheet 53.089 kB 47.585 kB 47.565 kB 🏆
Document 6.222 kB 5.472 kB 🏆 5.474 kB
Font 126.849 kB 126.633 kB 126.633 kB

Request Count

Category Latest Main Branch
Total 52 52 52
Scripts 41 41 41
Stylesheet 5 5 5
Document 1 1 1
Font 2 2 2

@coveralls
Copy link
Collaborator

coveralls commented Jan 16, 2025

Pull Request Test Coverage Report for Build 13062627835

Details

  • 295 of 361 (81.72%) changed or added relevant lines in 7 files are covered.
  • 18 unchanged lines in 4 files lost coverage.
  • Overall coverage decreased (-0.2%) to 97.959%

Changes Missing Coverage Covered Lines Changed/Added Lines %
packages/menu/src/Menu.ts 109 110 99.09%
tools/reactive-controllers/src/FocusGroup.ts 9 10 90.0%
packages/menu/src/MenuItem.ts 122 136 89.71%
packages/action-menu/src/ActionMenu.ts 4 28 14.29%
packages/picker/src/Picker.ts 29 55 52.73%
Files with Coverage Reduction New Missed Lines %
packages/picker/src/InteractionController.ts 2 94.02%
packages/picker/src/DesktopController.ts 2 90.74%
packages/menu/src/Menu.ts 6 94.71%
packages/picker/src/Picker.ts 8 95.25%
Totals Coverage Status
Change from base Build 13051780704: -0.2%
Covered Lines: 33067
Relevant Lines: 33571

💛 - Coveralls

@@ -95,12 +95,35 @@ Content assigned to the `value` slot will be placed at the end of the `<sp-menu-
An `<sp-menu-item>` can also accept content addressed to its `submenu` slot. Using the `<sp-menu>` element with this slot name the options will be surfaced in flyout menu that can be activated by hovering over the root menu item with your pointer or focusing the menu item and pressing the appropriate `ArrowRight` or `ArrowLeft` key based on text direction to move into the submenu.

```html
<sp-menu style="width: 200px;">
<p>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a more detailed example to docs to see menu item with submenus.

@@ -87,7 +92,7 @@ export class MenuGroup extends Menu {
<span class="header" ?hidden=${!this.headerElement}>
<slot name="header" @slotchange=${this.updateLabel}></slot>
</span>
<sp-menu ignore>${this.renderMenuItemSlot()}</sp-menu>
${this.renderMenuItemSlot()}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A menu group should not have a nested menu.

@property({ type: Boolean, reflect: true })
public open = false;

public override click(): void {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the source of the issues where Voice Over users can't click menu items.

@@ -489,10 +589,18 @@ export class MenuItem extends LikeAnchor(
this.active = false;
}

public async openOverlay(): Promise<void> {
public async openOverlay(focus: boolean = false): Promise<void> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When navigating via keyboard, opening the menu should also set focus on an item within a menu.

@@ -518,6 +626,19 @@ export class MenuItem extends LikeAnchor(
this.updateAriaSelected();
}

protected override willUpdate(changes: PropertyValues<this>): void {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If focus is inside the submenu when it close, move focus back to its parent item.

@@ -117,11 +117,59 @@ describe('Menu item', () => {
).anchorElement.dispatchEvent(new FocusEvent('focus'));

await elementUpdated(item);
expect(el === document.activeElement).to.be.true;

expect(item === document.activeElement).to.be.true;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Menus should delegate focus to an item

item.click();

expect(clickTargetSpy.calledWith(anchorElement)).to.be.true;
});
it('allows link click', async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added test to verify that menuitems with href were clickable.

@@ -638,16 +630,17 @@ describe('Menu w/ groups [selects]', () => {
input.focus();
expect(document.activeElement === input).to.be.true;
await sendKeys({ press: 'Shift+Tab' });
expect(document.activeElement === groupA).to.be.true;
expect(document.activeElement === options[0]).to.be.true;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Groups should delegate focus to items

}
});
}
super.update(changes);
}

protected hasAccessibleLabel(): boolean {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Action Menu was throwing the no label warning for a test that used the label-only slot, so I decided to override Picker's accessible label check and warning with a warning specific to action menu.

@@ -853,7 +886,7 @@ export class Picker extends PickerBase {
return;
}
if (code === 'ArrowUp' || code === 'ArrowDown') {
this.toggle(true);
this.keyboardOpen();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Focus should be on menu item after ArrowDown opens menu

acceptsEventCode(code: string): boolean {
if (code === 'End' || code === 'Home') {
acceptsEventKey(key: string): boolean {
if (key === 'End' || key === 'Home') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using key instead of code to allow for Numpad keys.

@@ -64,19 +61,42 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
return [menuStyles];
}

static override shadowRootOptions = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Menu should delegate focus to item

@@ -575,55 +558,34 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
}
}

protected navigateWithinMenu(event: KeyboardEvent): void {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

RTI now handles this.


protected navigateBetweenRelatedMenus(event: KeyboardEvent): void {
const { key } = event;
protected navigateBetweenRelatedMenus(event: MenuItemKeydownEvent): void {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since menu items have focus, menuitems need to forward keyboard event info to manage this.

return;
}
}
if (key === ' ' || key === 'Enter') {
const childItem = this.childItems[this.focusedItemIndex];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

handled by RTI

this.navigateBetweenRelatedMenus(event);
}

public focusMenuItemByOffset(offset: number): MenuItem {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

handled by RTI

@@ -811,26 +699,6 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
this.descendentOverlays = new Map<Overlay, Overlay>();
}

private forwardFocusVisibleToItem(item: MenuItem): void {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Item is now focusable so no need to forward

@nikkimk nikkimk marked this pull request as ready for review January 30, 2025 16:36
@nikkimk nikkimk requested a review from a team as a code owner January 30, 2025 16:36
@nikkimk nikkimk requested review from caseyisonit, jnurthen, majornista and Rajdeepc and removed request for Rajdeepc January 30, 2025 16:36
@jnurthen
Copy link
Member

This seems to have broken the preventDefault behaviour when pressing space on a menu item. The page now scrolls.

@majornista
Copy link
Contributor

majornista commented Jan 31, 2025

  1. In the Picker > Matching value and Picker > Matching itemText examples, when the Picker opens with a value selected for the first time, focus is going to the first menuitem rather than the selected value. On subsequent opening, the selected value will receive focus. It seems like the selected value for the menu does not initialize until it is rendered.

  2. With MenuItem > Submenu, Space key scrolls (per @jnurthen comment above), and with Enter key, the submenu opens, but focus does not move to the submenu. Subsequent keyboard navigation using arrow keys, navigates the current menu rather than the opened submenu. Similarly, activating a submenu parent item using the VoiceOver cursor fails to focus the submenu.

  3. Using the VoiceOver on iOS to test the ActionMenu > Submenu story, after opening the menu in a tray, double tapping "Select some items" to activate the submenu closes the tray rather than opening the submenu. The submenu story is similarly broken without VoiceOver on.

  4. Using VoiceOver on iOS to test any of the examples that open the menu in a tray like ActionMenu >Default story, double tapping to open fails to focus an item in the menu, and double tapping to select an item or to dismiss the menu using the visually hidden dismiss button fails to restore focus to the button element that opened the menu.

Copy link
Contributor

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

See comment #5031 (comment)

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