Skip to content

Fix a11y issues in sidebar menu component#579

Open
fateeand wants to merge 8 commits intomasterfrom
555-fix-a11y-issues-in-sidebar-menu-component
Open

Fix a11y issues in sidebar menu component#579
fateeand wants to merge 8 commits intomasterfrom
555-fix-a11y-issues-in-sidebar-menu-component

Conversation

@fateeand
Copy link
Copy Markdown
Collaborator

@fateeand fateeand commented May 6, 2026

Fixing accessibility issues in sidebar menu component

Validation rules:

Validated using Playwright accessibility tests, Lighthouse tool, axe DevTools extension, Accessibility Insights for Web extension, and manual checks including keyboard tab navigation and screen reader testing.

Full doc with rules


Playwright axe-core validation results:

State before:

Component aria-roles button-name color-contrast tabindex
Sidebar menu - - -

State after:

Component aria-roles button-name color-contrast tabindex
Sidebar menu

Checklist

  • Keyboard Navigation
    All interactive elements are fully operable via keyboard only, including buttons, inputs, menus, dialogs, sliders, drag-and-drop, tree views, multi-selects, and composite widgets. No traps or dead ends.

  • Focus Management
    Focus is visible, logical, moves in predictable order, trapped where necessary (modals/popovers), and restored after closing. Focus is perceivable in all interactive widgets.

  • Semantics / ARIA

    • Semantic HTML is used correctly.
    • ARIA roles, states, and properties are applied only when needed.
    • All form fields, tables, and widgets (including autocomplete, tree selects, tree tables, drag-and-drop, sliders, and multi-selects) are properly labeled and accessible.
  • Color / Contrast

    • Text and interactive elements meet contrast requirements (≥4.5:1 normal text, ≥3:1 large text).
    • Focus and selection indicators are visually perceivable.
    • Color is not the only indicator of state.
  • Screen Reader / Assistive Technology

    • All content, labels, and dynamic updates are perceivable via screen readers.
    • Live regions announce status messages, alerts, modals, notifications, and dynamic changes.
    • Interactive widgets provide proper announcements of selection and updates.
  • Responsive & Zoom

    • Components function correctly and remain readable at all viewport sizes and up to 200% zoom, including mobile and touch devices.
    • Prefer em/rem units over px where scaling is required.
  • [N/A] Error Handling

    • Errors are clearly identified visually and programmatically.
    • Form inputs use aria-describedby or aria-invalid for inline messages.
    • Instructions and suggestions are accessible.
  • Dynamic Content / Updates

    • Status updates, alerts, notifications, and modals use live regions.
    • Updates do not disrupt focus or user control unexpectedly.
  • Interaction Feedback / States

    • All interactive states (hover, focus, active, disabled, drag-and-drop, reordering, multi-select) are visually perceivable.
  • [N/A] Authentication & Sensitive Actions

    • Inputs and actions involving sensitive data provide accessible instructions, feedback, and error messages.
  • Predictable & Controllable UI

    • Components behave consistently and predictably.
    • Popups, modals, autocomplete suggestions, drag-and-drop, and dynamic content allow user control.

Additional changes

CpsInputModalityService has been implemented and is currently only used by the cps-menu component. It tracks whether the most recent input event came from a keyboard or a pointer. This helps prevent focus rings from being shown on menu trigger elements while still maintaining proper focus order. So when a menu is closed using the Escape key and the target element wasn't reached through tab navigation, we won't show the focus ring. The service is exposed via an injection token in the public API, allowing consuming apps to override or disable it if necessary.

Created a work item about showcasing public services API in the composition app: #582


Release notes:

  • Fix a11y issues in sidebar menu component

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings May 6, 2026 14:01
@fateeand fateeand linked an issue May 6, 2026 that may be closed by this pull request
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Playwright test results

passed  58 passed

Details

stats  58 tests across 4 suites
duration  1 minute, 51 seconds
commit  d9379fa
info  For details, download the Playwright report

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR targets accessibility improvements in the sidebar menu and related menu behaviors by adding explicit ARIA attributes/roles, improving focus-ring behavior based on input modality, and expanding a11y validation coverage in both unit and Playwright tests.

Changes:

  • Added input-modality tracking and focus-ring suppression to reduce unwanted focus indicators after pointer interactions.
  • Reworked cps-sidebar-menu markup/styles for improved semantics, keyboard operability, and contrast/focus styling.
  • Updated API docs generation + composition examples, and re-enabled Playwright a11y coverage for the sidebar menu page.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
projects/cps-ui-kit/styles/_mixins.scss Extends focus-ring mixin to support suppressing focus-visible styling.
projects/cps-ui-kit/src/lib/services/input-modality.service.ts Adds a modality-tracking service/token for keyboard vs pointer heuristics.
projects/cps-ui-kit/src/lib/components/cps-sidebar-menu/cps-sidebar-menu.component.ts Updates sidebar menu behavior (signals for height, focus/menu open handling).
projects/cps-ui-kit/src/lib/components/cps-sidebar-menu/cps-sidebar-menu.component.spec.ts Adds unit tests for sidebar menu behavior and accessibility-related rendering.
projects/cps-ui-kit/src/lib/components/cps-sidebar-menu/cps-sidebar-menu.component.scss Updates sizing to rem units and adds focus/disabled styling improvements.
projects/cps-ui-kit/src/lib/components/cps-sidebar-menu/cps-sidebar-menu.component.html Refactors template structure, adds ARIA attributes/roles, replaces div triggers with buttons/links.
projects/cps-ui-kit/src/lib/components/cps-menu/cps-menu.component.ts Tracks how the menu was opened and restores focus with optional focus-ring suppression.
projects/cps-ui-kit/src/lib/components/cps-menu/cps-menu.component.html Adds optional aria-label support for menu items.
projects/composition/src/app/pages/sidebar-menu-page/sidebar-menu-page.component.scss Converts spacing/typography to rem units on the sidebar menu docs page.
projects/composition/src/app/pages/menu-page/menu-page.component.ts Adds ariaLabel to loading menu items in examples.
projects/composition/src/app/api-data/cps-table.json Updates generated type docs to reflect optional properties.
projects/composition/src/app/api-data/cps-sidebar-menu.json Documents new ariaLabel prop + optionality updates for sidebar menu item type.
projects/composition/src/app/api-data/cps-radio-group.json Updates generated type docs to reflect optional properties.
projects/composition/src/app/api-data/cps-menu.json Updates generated type docs to include ariaLabel and optional properties.
projects/composition/src/app/api-data/cps-button-toggle.json Updates generated type docs to reflect optional properties.
playwright/cps-accessibility.spec.ts Re-enables Playwright accessibility coverage for cps-sidebar-menu.
api-generator/api-generator.js Skips emitting empty docs and marks optional properties with ? in generated type values.

Comment thread projects/cps-ui-kit/src/lib/services/input-modality.service.ts Outdated
Comment thread projects/cps-ui-kit/src/lib/services/input-modality.service.ts Outdated
Co-authored-by: Copilot <copilot@github.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Coverage report for library

St.
Category Percentage Covered / Total
🔴 Statements 31.03% 2148/6922
🔴 Branches 25.06% 794/3169
🔴 Functions 27.07% 396/1463
🔴 Lines 31.75% 2021/6366

Test suite run success

522 tests passing in 24 suites.

Report generated by 🧪jest coverage report action from d9379fa

fateeand and others added 5 commits May 6, 2026 17:06
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
if (!isPlatformBrowser(this._platformId)) return;

this._document.addEventListener('keydown', (e) => {
const navigationKeys = new Set([
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does this set need to be recreated on every event trigger? Maybe it can be moved to be a private readonly field.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines +134 to +141
if (menu.isVisible()) {
this.focusedItemWithMenu = item;
menu.hide();
} else {
this.focusedItemWithMenu = item;
this.allMenus?.forEach((m) => m.hide());
menu.show(null, event.currentTarget as HTMLElement, 'tr');
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if (menu.isVisible()) {
this.focusedItemWithMenu = item;
menu.hide();
} else {
this.focusedItemWithMenu = item;
this.allMenus?.forEach((m) => m.hide());
menu.show(null, event.currentTarget as HTMLElement, 'tr');
}
this.focusedItemWithMenu = item;
if (menu.isVisible()) {
menu.hide();
} else {
this.allMenus?.forEach((m) => m.hide());
menu.show(null, event.currentTarget as HTMLElement, 'tr');
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines +284 to +292
const hasItemsA11yViolation = this.items.some(
(item) => !item.title?.trim() && !item.ariaLabel?.trim()
);

if (hasItemsA11yViolation) {
console.error(
'CpsMenuComponent: all untitled menu items must have an ariaLabel for accessibility.'
);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this could be moved under an if (changes.items) condition

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

[class.disabled]="item.disabled"
[attr.aria-disabled]="item.disabled || null"
[attr.aria-label]="item.title">
<cps-icon [icon]="item.icon" size="normal"> </cps-icon>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this icon be aria-hidden? Same for lines 58, 74

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

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.

Fix a11y issues in sidebar menu component

3 participants