Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
134f619
chore(docs): add Storybook a11y addon and test-runner
ajirthan Jul 16, 2026
54dcb57
fix(a11y): give form controls accessible names
ajirthan Jul 16, 2026
f67575b
fix(a11y): fix Header, Sidebar, breadcrumbs, and UserMenu keyboard ac…
ajirthan Jul 16, 2026
840846e
fix(a11y): label ListingTable controls and restore focus rings
ajirthan Jul 16, 2026
87799b5
fix(a11y): label notification dismiss controls and fix banner contrast
ajirthan Jul 16, 2026
49c0114
fix(a11y): improve CodeBlock, footer, and theme syntax contrast
ajirthan Jul 16, 2026
90bf4ce
fix(a11y): respect reduced motion in ParticleBackground and hide deco…
ajirthan Jul 16, 2026
5bd174a
test(a11y): add accessibility regression coverage
ajirthan Jul 16, 2026
3f9c5a3
docs(a11y): label Checkbox, Switch, Slider, and Select story demos
ajirthan Jul 16, 2026
6803191
docs(a11y): document SearchBar, ComplexSelect, and ThemeSwitcher acce…
ajirthan Jul 16, 2026
7570018
docs(a11y): label Progress demos and scrollable regions
ajirthan Jul 16, 2026
d8bea1e
docs(a11y): add Accessibility docs to App Elements and ParticleBackgr…
ajirthan Jul 16, 2026
44b3e39
docs(a11y): label CreateServiceFormTemplate adornment controls
ajirthan Jul 16, 2026
dc8db51
docs(a11y): document brand contrast exceptions for intro, inputs, and…
ajirthan Jul 16, 2026
96032ef
docs(a11y): document brand contrast exceptions for templates and util…
ajirthan Jul 16, 2026
3da973e
docs(a11y): document Form.CardButton nested-interactive exception
ajirthan Jul 16, 2026
4033e09
docs(a11y): add Accessibility guide and contributor policy
ajirthan Jul 16, 2026
a46a52d
ci(docs): fail PRs on Storybook accessibility violations
ajirthan Jul 16, 2026
5d20831
docs(a11y): point exception and audit links at upstream issues
ajirthan Jul 16, 2026
43a07ae
feat(a11y): implement reduced motion support in ParticleBackground an…
ajirthan Jul 16, 2026
6e91648
fix(a11y): sync expandedMenus when toggling collapsed sidebar nested …
ajirthan Jul 16, 2026
1fd0430
fix(a11y): focus collapsed sidebar nested menu and wire aria-controls
ajirthan Jul 16, 2026
2bb5487
fix(a11y): omit empty aria-label on collapsed Sidebar.Item
ajirthan Jul 16, 2026
3b403b5
fix(a11y): compose onClick on UserMenu.Trigger and ColorSchemeToggle
ajirthan Jul 16, 2026
e098288
fix(a11y): skip SearchBar placeholder aria-label when a visible label…
ajirthan Jul 16, 2026
0b330da
fix(a11y): respect SearchBar inputProps accessible names
ajirthan Jul 16, 2026
e0c8884
fix(a11y): skip empty SearchBar placeholder aria-label
ajirthan Jul 16, 2026
4573231
fix(a11y): sync collapsed sidebar expand state without wrong toggles
ajirthan Jul 16, 2026
a565012
fix(a11y): make ImageList story scroll regions keyboard focusable
ajirthan Jul 16, 2026
bb1a4e9
fix(a11y): keep SearchBar placeholder label over empty aria-label
ajirthan Jul 16, 2026
79e6e35
fix(a11y): keep ThemeSelect outlined label in sync when hidden
ajirthan Jul 16, 2026
d83ca23
fix(a11y): honor SearchBar top-level aria-label props
ajirthan Jul 16, 2026
9b110f2
fix(a11y): ensure ListingTableToolbar uses default aria-label when se…
ajirthan Jul 16, 2026
bd2a228
refactor(a11y): update components to use slotProps for aria-labels in…
ajirthan Jul 16, 2026
2b1c80c
fix(a11y): keep collapsed sidebar nested menu open across pointer bridge
ajirthan Jul 16, 2026
65c1563
fix(a11y): sync expandedMenus when hover-opening collapsed sidebar menu
ajirthan Jul 16, 2026
a4411c6
fix(a11y): ignore whitespace-only SearchBar aria-label when naming
ajirthan Jul 16, 2026
1cc4572
fix(a11y): keep UserMenu.Trigger menu ARIA after consumer props
ajirthan Jul 16, 2026
de21f62
fix(a11y): keep ColorSchemeToggle default aria-label over empty overr…
ajirthan Jul 16, 2026
8429ea2
fix(a11y): clear collapsed sidebar nested popover when expanding
ajirthan Jul 16, 2026
ef16ee8
fix(a11y): ignore whitespace-only SearchBar label when naming
ajirthan Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/pr-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ jobs:
id: build-storybook
run: pnpm build:storybook

- name: 🎭 Install Playwright Browsers
id: install-playwright
run: pnpm --filter @wso2/oxygen-ui-docs exec playwright install --with-deps chromium

- name: ♿ Run Storybook Accessibility Tests
id: storybook-a11y
run: pnpm --filter @wso2/oxygen-ui-docs test:storybook:ci

build:
name: 🚧 Build
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions packages/oxygen-ui-docs/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default {
addons: [
'@storybook/addon-docs',
'@storybook/addon-links',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-webpack5',
Expand Down
18 changes: 16 additions & 2 deletions packages/oxygen-ui-docs/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
useThemeSwitcher,
AcrylicOrangeTheme,
AcrylicPurpleTheme,
ChoreoTheme,
ClassicTheme,
HighContrastTheme,
PaleGrayTheme,
Expand Down Expand Up @@ -134,7 +133,6 @@ const preview = {
const themes = React.useMemo(() => [
{ key: 'acrylicOrange', label: 'Acrylic Orange', theme: AcrylicOrangeTheme },
{ key: 'acrylicPurple', label: 'Acrylic Purple', theme: AcrylicPurpleTheme },
{ key: 'choreo', label: 'Choreo', theme: ChoreoTheme },
{ key: 'classic', label: 'Classic', theme: ClassicTheme },
{ key: 'highContrast', label: 'High Contrast', theme: HighContrastTheme },
{ key: 'paleGray', label: 'Pale Gray', theme: PaleGrayTheme },
Expand Down Expand Up @@ -164,6 +162,21 @@ const preview = {
],

parameters: {
a11y: {
// Run axe against the WCAG 2.1 AA ruleset (plus its A prerequisites).
config: {},
options: {
runOnly: {
type: 'tag',
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'],
},
},
// Violations fail the story when run via the test-runner / CI.
// Individual stories with known, tracked issues may override this
// with 'todo' and a comment linking to the follow-up issue.
test: 'error',
},

backgrounds: {
disable: true,
},
Expand All @@ -173,6 +186,7 @@ const preview = {
'Welcome',
'Getting Started',
'How To Contribute',
'Accessibility',
'App Elements', [
'App Shell',
'App Breadcrumbs',
Expand Down
93 changes: 93 additions & 0 deletions packages/oxygen-ui-docs/ACCESSIBILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Oxygen UI — WCAG 2.1 AA Accessibility Audit

**Date:** July 2026
**Scope:** All first-party components in `packages/oxygen-ui/src` (components, layouts, animations), the themed MUI surface exercised by the 103 Storybook story files in `packages/oxygen-ui-docs`, and the shipped themes.
**Standard:** [WCAG 2.1 AA](https://www.w3.org/TR/WCAG21/), automated checks via [axe-core](https://github.com/dequelabs/axe-core) rule tags `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`.

## 1. Methodology

1. **Automated baseline** — `@storybook/addon-a11y` was added to the docs Storybook with `parameters.a11y.test: 'error'` as the global default, and the full story suite (533 stories) was executed with `@storybook/test-runner` + Playwright against a static Storybook build. Every failure was triaged as: component bug, story (documentation) bug, or theme/palette issue.
2. **Wrapper integrity audit** — every first-party component was reviewed at source level for: `ref` forwarding, `...props` spreading to the correct DOM node (prop swallowing), icon-only controls without accessible names, non-semantic interactive elements, missing `aria-hidden` on decorative icons, focus-outline suppression, and motion without `prefers-reduced-motion` handling.
3. **Keyboard and focus review** — interactive composites (breadcrumb overflow menu, collapsed sidebar nested menus, user menu, notification panel, listing table, complex select, dialogs/menus/popovers) were reviewed for keyboard operability and focus management. Composites built on MUI `Menu`, `Popover`, `Drawer`, `Dialog`, `Tabs` inherit MUI's focus trapping, restoration, and arrow-key behavior; custom-built interaction paths were audited individually.
4. **Regression protection** — Vitest + Testing Library tests (`packages/oxygen-ui/src/components/accessibility.test.tsx`) lock in accessible names, prop forwarding, and ref forwarding. A CI job in `pr-builder.yml` runs the Storybook accessibility suite on every PR and fails on new violations.

> Automated tooling catches roughly a quarter to a third of WCAG issues. Screen reader passes (VoiceOver/NVDA/JAWS) and 200%/400% zoom-reflow verification on real assistive technology still require a human pass and are listed under [Section 6](#6-remaining-manual-verification).

## 2. Baseline results

| Run | Failed stories | Notes |
| --- | --- | --- |
| Initial baseline | 48 / 533 | 36× `color-contrast`, 7× `aria-input-field-name`, 3× `label`, 1× `aria-progressbar-name`, 2× `scrollable-region-focusable` |
| After remediation | 0 / 533 | 35 stories carry documented, issue-tracked rule exceptions (see §5) |

## 3. Issues found and fixed

### Components (`@wso2/oxygen-ui`)

| Component | Issue | WCAG | Fix |
| --- | --- | --- | --- |
| `ComplexSelect` | `labelAnchor="inside"` rendered a visual label only; the combobox had no accessible name. Label ids were generated with `Math.random()` (unstable, SSR-unsafe). | 4.1.2 | Visually hidden `InputLabel` for inside mode; `useId()` for label ids. |
| `ThemeSwitcher` / `ThemeSelect` | With `showLabel={false}` (default) the select had no accessible name; hardcoded label id broke with multiple instances. | 4.1.2 | Always render the `InputLabel` (visually hidden when `showLabel` is false); `useId()`. |
| `Form.ElementWrapper` | `FormLabel htmlFor` cannot label MUI `Select` (renders a non-labelable `div[role="combobox"]`). | 1.3.1, 4.1.2 | Label gets an id; `Select` children are cloned with a matching `labelId`. |
| `ColorSchemeToggle` | Icon-only button relied on `Tooltip` for its name; no ref forwarding. | 4.1.2 | Default `aria-label="Switch to <next> mode"` (overridable); `forwardRef`. |
| `Header.Toggle` | Icon-only sidebar toggle had no `aria-label` or state exposure. | 4.1.2 | `aria-label` from the existing expand/collapse label props; `aria-expanded`; decorative icons hidden. |
| `Header.Brand` | Clickable `Box` with `onClick` — not focusable, no role, no keyboard handler. | 2.1.1, 4.1.2 | Renders a `ButtonBase` when `onClick` is set (plain `div` otherwise); accepts `aria-label`. |
| `AppBreadcrumbs` | Overflow ellipsis was a `Typography` with `onClick` only; overflow menu was a raw `Popper`+`MenuList` with no focus management, arrow keys, or Escape. Decorative separators unlabeled. | 2.1.1, 2.1.2, 4.1.2 | Ellipsis is a native `<button>` with `aria-haspopup`/`aria-expanded`/`aria-controls`; menu replaced with MUI `Menu` (focus trap, arrow keys, Escape, focus restore); separator `aria-hidden`. |
| `Sidebar.Item` | Collapsed nested menus opened on hover only — unusable by keyboard. Collapsed items had no accessible name (tooltip only); no `aria-expanded` on parents. | 2.1.1, 4.1.2 | Enter/Space now toggles the nested-items popover when collapsed; `aria-label` on collapsed items; `aria-expanded` + `aria-haspopup` wired; chevrons `aria-hidden`. |
| `ListingTable.DataGrid` | Cell/header focus outlines were removed with no replacement. | 2.4.7 | `:focus-visible` outline restored using the theme primary color (mouse `:focus` stays clean). |
| `ListingTable.Toolbar` | Clear-search icon button unlabeled; search input named only by placeholder; container had no toolbar semantics. | 4.1.2 | `aria-label="Clear search"`; input `aria-label`; `role="toolbar"` + label. |
| `ListingTable.DensityControl` | Icon-only toggle buttons relied on tooltips. | 4.1.2 | `aria-label` per toggle and on the group; icons hidden. |
| `NotificationPanel` | Header close and item dismiss icon buttons unlabeled. | 4.1.2 | `aria-label="Close notifications"` / `"Dismiss notification"`; icons hidden. |
| `NotificationBanner` | MUI light-mode filled `info`/`warning` Alerts fail 4.5:1 with white text (3.85:1 / 3.11:1). | 1.4.3 | Light mode: `info` uses `info.dark` background; `warning` switches to black text/icons. |
| `CodeBlock` + theme `syntax` tokens | Light-mode syntax colors `#d73a49` (4.19:1) and `#6a737d` (4.41:1) fail on `#f5f5f5`. | 1.4.3 | Darkened to `#cf222e` (4.91:1) and `#57606a` (5.86:1) in both the theme tokens and component fallbacks. |
| `Footer.Version` | `text.disabled` at 11px = 2.67:1 on white. | 1.4.3 | Uses `text.secondary`. |
| `ParticleBackground` | Continuous canvas animation ignored `prefers-reduced-motion`; canvas not marked decorative. | 2.3.3 | Renders a single static frame under reduced motion (live-updates when the preference changes); `aria-hidden="true"`. |
| `OxygenThemeBase` | No global reduced-motion handling for MUI transitions (sidebar width, card hover, collapse animations). | 2.3.3 | `MuiCssBaseline` override collapses all animations/transitions under `prefers-reduced-motion: reduce`. |
| `SearchBar` / `SearchBarBase` | Input named only by placeholder; decorative search icon unlabeled; no ref forwarding. | 4.1.2 | Default `aria-label` from placeholder (overridable via `slotProps.htmlInput`); icon `aria-hidden`; `forwardRef`. |
| `UserMenu.Trigger` | No prop spreading (consumers could not pass `aria-*`/`data-*`); no ref forwarding. | — | Extends `IconButtonProps`, spreads props, `forwardRef`. (Trigger already had correct `aria-haspopup`/`aria-expanded`/`aria-controls`.) |
| `PageTitle.BackButton`, `StatCard` | Decorative icons not hidden from screen readers. | 1.1.1 | `aria-hidden` on icons. |

### Stories (documentation demonstrating accessible usage)

- `Checkbox`, `Switch`, `Slider` — standalone controls now demonstrate `aria-label` / `slotProps` labeling (axe `label` rule).
- `Select` — `InputLabel id` ↔ `Select labelId` linkage added to all variants (axe `aria-input-field-name`).
- `Progress` — all `CircularProgress`/`LinearProgress` instances have `aria-label` (axe `aria-progressbar-name`).
- `ImageList`, `useThemeContent` — fixed-height scrollable regions given `tabIndex={0}` and labels (axe `scrollable-region-focusable`); `warning.dark`-on-`warning.light` heading fixed.
- `AppShell`, `ComplexSelect` stories — header switcher selects labeled via `slotProps={{ input: { 'aria-label': … } }}`.
- `CreateServiceFormTemplate` — adornment icon buttons labeled (axe `button-name`).
- The Storybook preview also dropped a broken `ChoreoTheme` import (not exported by the library).

## 4. Follow-up issues (tracked on GitHub)

| Issue | Title | Severity |
| --- | --- | --- |
| [#557](https://github.com/wso2/oxygen-ui/issues/557) | **Epic:** Ensure WCAG 2.2 AA compliance across Oxygen UI | — |
| [#558](https://github.com/wso2/oxygen-ui/issues/558) | Brand primary `#FF7300` fails WCAG AA 4.5:1 text contrast (Classic/WSO2 themes) | High (needs design decision) |
| [#559](https://github.com/wso2/oxygen-ui/issues/559) | `forwardRef` + full prop forwarding rollout for remaining compound components | Medium |
| [#560](https://github.com/wso2/oxygen-ui/issues/560) | NotificationPanel live-region announcements and drawer labeling | Medium |
| [#561](https://github.com/wso2/oxygen-ui/issues/561) | Medium/low severity wrapper-audit follow-ups (SVG roles, landmark guidance, `aria-describedby` for form errors, hardcoded colors, decorative icons) | Medium/Low |
| [#562](https://github.com/wso2/oxygen-ui/issues/562) | `Form.CardButton` nests interactive controls inside a `<button>` (`nested-interactive`) | High |
| [#563](https://github.com/wso2/oxygen-ui/issues/563) | Manual AT pass (VoiceOver/NVDA, zoom 200%/400%) | High |

## 5. Documented exceptions

Stories that would otherwise fail carry a `parameters.a11y.options.rules` override with a comment linking the tracking issue. They remain visible in the Storybook a11y addon panel.

- **`color-contrast` (34 stories)** — every occurrence traces to the brand primary `#FF7300` used by the default `Classic`/`WSO2` themes (2.61:1 as text on `#fafafa`, 2.72:1 with white text on orange). Changing the brand palette needs design sign-off → [#558](https://github.com/wso2/oxygen-ui/issues/558). The `Theming/Colors` stories are additionally excluded because they are raw Material palette swatch demos.
- **`nested-interactive` (2 stories)** — `Form.CardButton` design flaw → [#562](https://github.com/wso2/oxygen-ui/issues/562).

## 6. Remaining manual verification

These require a human with real assistive technology and are tracked in [#563](https://github.com/wso2/oxygen-ui/issues/563):

1. **Screen reader pass** (VoiceOver on macOS, NVDA on Windows) over: `AppShell` + `Sidebar` navigation, `UserMenu`, `NotificationPanel` (see [#560](https://github.com/wso2/oxygen-ui/issues/560)), `ListingTable` (table semantics and sort announcements), `ComplexSelect`, Form templates (error announcement flow — `aria-describedby` wiring is tracked in [#561](https://github.com/wso2/oxygen-ui/issues/561)).
2. **Zoom/reflow** at 200% and 400% (WCAG 1.4.10) on `AppShell`, `Layout`, `Sidebar`, and the Templates stories.
3. **Focus-indicator contrast** (≥ 3:1, WCAG 2.4.7/1.4.11) spot-checks per theme in both color schemes; the theme files do not suppress `:focus-visible` anywhere (verified by source audit), so MUI defaults apply.
4. **Keyboard passes on themed MUI composites** (Dialog, Menu, Tabs, Accordion, Drawer, DataGrid) — expected to inherit MUI behavior; verify no theme override interferes.

## 7. Ongoing workflow

- **Local:** the a11y addon panel shows violations for every story while developing (`pnpm storybook`). All stories run with WCAG 2.1 A/AA rules and `test: 'error'`.
- **CI:** the `storybook` job in `.github/workflows/pr-builder.yml` builds Storybook and runs `pnpm --filter @wso2/oxygen-ui-docs test:storybook:ci`; any new violation fails the PR.
- **Exceptions policy:** a story may only disable a rule with (a) a code comment explaining why and (b) a linked GitHub issue. See `CONTRIBUTING.md`.
- **Unit level:** `packages/oxygen-ui/src/components/accessibility.test.tsx` guards accessible names and prop/ref forwarding; extend it when touching wrapper roots.
18 changes: 13 additions & 5 deletions packages/oxygen-ui-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"scripts": {
"dev": "node ./scripts/copy-sample-to-storybook.js --watch & storybook dev -p 6006",
"build": "node ./scripts/copy-sample-to-storybook.js && storybook build -o dist",
"preview": "http-server dist"
"preview": "http-server dist",
"test:storybook": "test-storybook",
"test:storybook:ci": "concurrently --kill-others --success first --names \"SERVE,TEST\" \"http-server dist --port 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && test-storybook --url http://127.0.0.1:6006 --maxWorkers=2\""
},
"keywords": [
"wso2",
Expand All @@ -22,32 +24,38 @@
"url": "https://github.com/wso2/oxygen-ui.git"
},
"dependencies": {
"@hookform/resolvers": "5.2.1",
"@wso2/oxygen-ui": "workspace:^",
"@wso2/oxygen-ui-icons-react": "workspace:^",
"@wso2/oxygen-ui-charts-react": "workspace:^",
"@wso2/oxygen-ui-icons-react": "workspace:^",
"react-hook-form": "7.62.0",
"@hookform/resolvers": "5.2.1",
"zod": "4.0.10"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@babel/core": "7.28.5",
"@babel/preset-env": "7.28.5",
"@babel/preset-react": "7.28.5",
"@babel/preset-typescript": "7.28.5",
"@mdx-js/react": "3.1.1",
"@storybook/addon-a11y": "10.4.6",
"@storybook/addon-docs": "10.4.6",
"@storybook/addon-links": "10.4.6",
"@storybook/react": "10.4.6",
"@storybook/react-webpack5": "10.4.6",
"@storybook/test-runner": "^0.24.4",
"@svgr/webpack": "8.1.0",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"babel-loader": "10.0.0",
"concurrently": "^10.0.3",
"http-server": "^14.1.1",
"lucide-react": "catalog:",
"lucide-static": "1.16.0",
"playwright": "1.61.1",
"react": "catalog:",
"react-dom": "catalog:",
"storybook": "10.4.6"
"storybook": "10.4.6",
"wait-on": "^9.0.10"
},
"publishConfig": {
"access": "restricted"
Expand Down
Loading
Loading