diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8b628ceba24..749eb86b660 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -67,6 +67,7 @@ packages/ ├── fluent/ # Fluent theme ├── utils/ # Utility CSS classes (standalone) └── html/ # React component specs & tests +aria/ # ARIA accessibility specifications (per-component) scripts/ # Build utilities and automation tests/ # Visual test outputs build/ # CI/CD scripts @@ -143,6 +144,11 @@ npm run clean # Full cleanup (includes node_modules) - Refer to ${variable-docs.prompt.md} for SassDoc documentation standards +### Accessibility + +- Use the `/accessibility` prompt for applying ARIA to components +- Refer to `.github/prompts/accessibility.prompt.md` for patterns and rules + ### SCSS Standards - Use **dart-sass syntax** - avoid deprecated node-sass features @@ -168,6 +174,18 @@ npm run clean # Full cleanup (includes node_modules) - **Linting** - Enforced on all commits via Husky + lint-staged - **Accessibility tests** - Run automatically on default-ocean-blue-a11y swatch +### Accessibility Automation + +ARIA specifications are defined as `ariaSpec.rules` on TSX spec components (single source of truth). Reference docs live in `aria/[component]_aria.md`. + +Use the `/accessibility` prompt for the full workflow and rules. Key commands: + +```bash +npm run test:a11y [component] # Validate ARIA + WCAG (recommended) +npm run test:a11y:affected # Test only git-changed components +npm run test:contrast # Validate color contrast ratios +``` + ## Common Issues - **Unit tests failing**: Run `npm run docs` first to generate metadata diff --git a/.github/instructions/html.instructions.md b/.github/instructions/html.instructions.md index 69d1c9f838d..46742e8ff9e 100644 --- a/.github/instructions/html.instructions.md +++ b/.github/instructions/html.instructions.md @@ -145,8 +145,21 @@ npm test # Run Jest tests npm start # Start dev server ``` +## Accessibility + +All components must be WCAG 2.2 Level AA compliant. ARIA attributes are applied directly in `.spec.tsx` and `templates/*.tsx` files. + +For applying ARIA to a component, use the `/accessibility` prompt — it contains the full workflow, rules, and patterns. + +Quick reference: +- ARIA specs per component: `aria/[component]_aria.md` +- Edit only `.spec.tsx` and `templates/*.tsx` (avoid `tests/` unless needed for coverage) +- Validate: `npm run test:a11y [component]` +- Completed components with `ariaSpec` serve as reference for new work + ## Related Documentation - Root instructions: `../../.github/copilot-instructions.md` - Package README: `../README.md` +- Accessibility prompt: `../../.github/prompts/accessibility.prompt.md` diff --git a/.github/prompts/accessibility.prompt.md b/.github/prompts/accessibility.prompt.md new file mode 100644 index 00000000000..1214d06e68a --- /dev/null +++ b/.github/prompts/accessibility.prompt.md @@ -0,0 +1,159 @@ +--- +agent: "agent" +description: "Apply ARIA accessibility attributes to an HTML package component" +--- + +# Accessibility + +Given a component name or `.spec.tsx` file, apply WAI-ARIA attributes to make it WCAG 2.2 Level AA compliant. + +## Steps + +### 1. Gather context + +- Read `aria/[component]_aria.md` if it exists (reference documentation) +- Read `packages/html/src/[component]/[component].spec.tsx` and `templates/*.tsx` +- Look at similarly completed components (those with an `ariaSpec.rules` array) for reference patterns + +### 2. Build the `ariaSpec` rules + +The `ariaSpec` static object on the spec component is the **single source of truth** for ARIA testing. It must include a `rules` array — each entry maps a CSS selector to an expected attribute. + +If `aria/[component]_aria.md` exists, migrate its rule table into `ariaSpec.rules`. If no markdown spec exists, create rules based on: +- WAI-ARIA 1.2 Authoring Practices and WCAG 2.2 +- The component's rendered HTML structure and interactive behavior +- Specs from similar components (e.g. `combobox` ↔ `autocomplete`) + +Flag and fix issues before applying (wrong selectors, contradictory roles like `role="alert"` + `aria-live="polite"`, missing states). + +**Format:** + +```tsx +Component.ariaSpec = { + selector: '.k-component', + implicitRole: 'button', + rules: [ + { selector: '.k-component', attribute: 'role=button', usage: 'Required' }, + { selector: '.k-component', attribute: 'aria-label or aria-labelledby or title', usage: 'Required when icon-only' }, + { selector: '.k-component', attribute: 'aria-pressed', usage: 'When togglable' }, + { selector: '.k-component', attribute: 'disabled', usage: 'When disabled' }, + { selector: '.k-component .k-icon', attribute: 'aria-hidden=true', usage: 'Required' }, + ] +}; +``` + +Each rule: `{ selector, attribute, usage }` — same columns as the markdown tables. The test runner reads `ariaSpec.rules` directly; legacy fields (`requiredAttributes`, `childSelectors`) are still supported as fallback but should be migrated to `rules`. + +**Composite components:** Complex components that embed other components (e.g. Grid contains Pager, Toolbar, ColumnMenu) must include the child component rules in their own `ariaSpec.rules`. The test validates the **full rendered HTML** from each template, so all nested selectors must be accounted for. Reference the child component's `ariaSpec.rules` and adapt selectors to the parent's DOM structure. Building blocks that don't have their own spec (internal abstractions) must still produce accessible HTML — ensure correct attributes flow via props from parent or are set directly in templates. + +### 3. Apply ARIA to TSX files + +Edit `.spec.tsx` and `templates/*.tsx`. Rules: + +- **Attributes after `className`** — always place ARIA props after the className prop +- **Semantic HTML first** — prefer ` + + +// ✅ GOOD — attributes on existing element + +``` + +**Coverage gaps — add templates for untested states:** +```tsx +// templates/combobox-disabled.tsx +export const ComboboxDisabled = (props) => ( + +); +// then export from index.ts +``` + +Add **TSDoc** on props that affect ARIA: + +```tsx +export type KendoComponentProps = { + /** @aria aria-pressed="true" when selected */ + selected?: boolean; + /** @aria aria-label required for icon-only usage */ + icon?: string; +}; +``` + +### 4. Validate iteratively + +Run after every edit round: + +```bash +npm run build --prefix packages/html && \ +npm run test:a11y [component] +``` + +Fix violations and re-run until clean. Also run `npm run typecheck --prefix packages/html` to catch type errors. + +### Known acceptable violations + +These are out of scope — note but don't try to fix: +- `label` — form labels provided by consuming apps +- `target-size` (2.5.8) — controlled by product implementations +- jQuery legacy specs — excluded from compliance testing + +### Must-fix violations + +Always resolve these: +- `button-name` — buttons without accessible text +- `aria-valid-attr-value` — invalid ARIA attribute values +- `aria-required-attr` — missing required ARIA attributes +- Any other axe-core WCAG 2.2 Level AA violations diff --git a/.github/prompts/variable-create.prompt.md b/.github/prompts/variable-create.prompt.md index fbe33547c7c..5d063280821 100644 --- a/.github/prompts/variable-create.prompt.md +++ b/.github/prompts/variable-create.prompt.md @@ -1,5 +1,5 @@ --- -mode: "agent" +agent: "agent" tools: ["codebase", "editFiles"] description: "Create new SCSS variables using Kendo UI themes best practices" --- diff --git a/.github/prompts/variable-docs.prompt.md b/.github/prompts/variable-docs.prompt.md index 524f8f66953..e8e6fb44561 100644 --- a/.github/prompts/variable-docs.prompt.md +++ b/.github/prompts/variable-docs.prompt.md @@ -1,5 +1,5 @@ --- -mode: "agent" +agent: "agent" tools: ["codebase", "editFiles"] description: "Create comprehensive SCSS variable documentation using Kendo UI themes best practices" --- diff --git a/.github/workflows/_test-a11y-specs.yml b/.github/workflows/_test-a11y-specs.yml new file mode 100644 index 00000000000..5302ccb72c0 --- /dev/null +++ b/.github/workflows/_test-a11y-specs.yml @@ -0,0 +1,89 @@ +name: Test A11y Specs + +on: + workflow_call: + +concurrency: + group: test-a11y-specs-${{ github.ref }} + cancel-in-progress: true + +jobs: + + run: + runs-on: ubuntu-latest + + steps: + + - name: Checkout branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 # Needed for affected detection + + - name: Install virtual display on Linux + run: sudo apt-get install xvfb + + - name: Setup node + id: setup-node + uses: actions/setup-node@v6 + with: + node-version: 24.x + + - name: Use cache for root node_modules + id: cache-root-node_modules + uses: actions/cache@v5 + with: + path: node_modules + key: root-node_modules-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('package-lock.json') }} + + - name: Install + if: steps.cache-root-node_modules.outputs.cache-hit != 'true' + run: | + npm ci --no-audit --no-fund + + - name: Download artifacts + uses: actions/download-artifact@v7 + with: + path: .tmp + + - name: Unpack artifacts + run: find .tmp -name "*.tar" -type f -exec tar -xf {} \; + + - name: Build HTML package + run: npm run build --prefix packages/html + + - name: Run unified A11y tests (affected components) + id: a11y-tests + continue-on-error: true + run: | + xvfb-run --auto-servernum --server-args="-screen 0, 1366x768x24" npm run test:a11y -- --affected --verbose 2>&1 | tee a11y-test-output.txt + + - name: Report test results + run: | + echo "## A11y Spec Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ steps.a11y-tests.outcome }}" == "success" ]; then + echo ":white_check_mark: Unified A11y tests passed (ARIA + WCAG)" >> $GITHUB_STEP_SUMMARY + else + echo ":x: A11y tests failed" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "_Tested affected components only for faster CI._" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Test Output" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat a11y-test-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "_Note: This check is non-blocking and does not prevent PR merging._" >> $GITHUB_STEP_SUMMARY + + - name: Check for failures + run: | + if [ "${{ steps.a11y-tests.outcome }}" == "failure" ]; then + echo "A11y spec tests failed. See summary above for details." + exit 1 + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c60daa06413..f3438469834 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,6 +191,22 @@ jobs: - name: Done run: echo "Done!" + a11y-specs: + name: A11y Specs + needs: [ compile-themes ] + uses: ./.github/workflows/_test-a11y-specs.yml + + ci-a11y-specs: + name: Status check > A11y Specs (non-blocking) + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [ a11y-specs ] + steps: + - name: Report status + run: | + echo "A11y specs check completed (outcome: ${{ needs.a11y-specs.result }})" + echo "This check is informational and does not block PR merging." + docs: name: Docs diff --git a/.gitignore b/.gitignore index 648e4a9edc7..d9e6a765a35 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ debug.log # NX .nx/cache .nx/workspace-data + +# Generated a11y test reports (not tracked; screenshots in tests/_output/ ARE tracked) +tests/_output/*.json diff --git a/.husky/pre-push b/.husky/pre-push index 057c505f829..2d547012288 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1 +1,4 @@ npm run sass && npm run docs:check + +# Run a11y tests on affected components (prompt to skip) +node scripts/test-a11y-unified.mjs --affected --prompt --build diff --git a/aria/action-sheet_aria.md b/aria/action-sheet_aria.md new file mode 100644 index 00000000000..81b0fc72c6b --- /dev/null +++ b/aria/action-sheet_aria.md @@ -0,0 +1,18 @@ +## WAI-ARIA + +This section lists the selectors, attributes, and behavior patterns supported by the component. + +### ActionSheet Dialog Wrapper + +| Selector | Attribute | Usage | +| -------- | --------- | ----- | +| `.k-actionsheet` | `role=dialog` | Announces the dialog role of the component. | +| | `aria-labelledby=.k-actionsheet-title id` | Associates the title of the action sheet. | +| | `aria-hidden=true/false` | Announces the hidden state of the ActionSheet container. | +| | `aria-modal=true` | Announces that the action sheet is modal. | +| `.k-actionsheet .k-actionsheet-title` | `id` | Used to associate the title with the action sheet wrapper element. | + +### Embedded Content + +ActionSheet is a container component with arbitrary content. When components use ActionSheet in adaptive mode (e.g., dropdowns rendering lists), those components are responsible for ensuring their internal content has proper ARIA attributes. + diff --git a/aria/ai-prompt_aria.md b/aria/ai-prompt_aria.md new file mode 100644 index 00000000000..1cbe6b5fe92 --- /dev/null +++ b/aria/ai-prompt_aria.md @@ -0,0 +1,85 @@ +## WAI-ARIA + + +This section lists the selectors, attributes, and behavior patterns supported by the component and its composite elements, if any. + +### AI Prompt + + +The AI Prompt component is a composite one and integrates the accessibility of the Toolbar, TextArea, Card, Chip and FloatingActionButton. + +### TextArea Component + +[TextArea accessibility specification](textarea_aria.md) + +### TextArea Adornments + +| Selector | Attribute | Usage | +| -------- | --------- | ----- | +| `.k-textarea-prefix>.k-button` | `role=button` or `nodeName=button` | The buttons must have appropriate role. | +| | `aria-label` or `title` | The buttons must be properly labelled. | +| `.k-textarea-suffix>.k-button` | `role=button` or `nodeName=button` | The buttons must have appropriate role. | +| | `aria-label` or `title` | The buttons must be properly labelled. | +| `.k-textarea-suffix>.k-prompt-send.k-disabled` | `aria-disabled=true` | Announces send action as disabled if necessary. | + +### Suggestion Component + + +The Suggestion list implements roving tabindex navigation. Meaning that only one suggestion has tabindex=0. The display of the suggestion list is controlled by the expand button. + +| Selector | Attribute | Usage | +| -------- | --------- | ----- | +| `.k-prompt-expander .k-button` | `aria-controls=.k-prompt-expander-content id` | Points to the controlled element based on the given `id`. | +| | `aria-expanded=true/false` | Indicates the expanded state of the prompt expander content. | +| `.k-prompt-expander .k-suggestion-group` | `role=group` | Indicates that the suggestion container element is a group. | +| `.k-prompt-expander .k-suggestion` | `role=button` | Indicates that the suggestion element is a button. | +| | `aria-label` or `title` | The suggestion elements must be properly labelled. | +| | `tabindex=0` | The suggestion element should be focusable. | + +### Button Component + +[Button accessibility specification](button_aria.md) + +### Adaptive Mode + + +When the AI Prompt component is in adaptive mode, the popup element follows the specifications of the ActionSheet component. + +[ActionSheet accessibility specification](actionsheet_aria.md) + +### Toolbar Component + +[ToolBar accessibility specification](toolbar_aria.md) + +### Card List Container + +[CardList accessibility specification](cardlist_aria.md) + +### Card Component + +[Card accessibility specification](card_aria.md) + +### Chip Component + +[Chip accessibility specification](chip_aria.md) + +### ChipList Component + +[ChipList accessibility specification](chiplist_aria.md) + +### ContextMenu Component + +[ContextMenu accessibility specification](contextmenu_aria.md) + +### FloatingActionButton Component + +[FloatingActionButton accessibility specification](floatingactionbutton_aria.md) + +### SpeechToTextButton Component + +[SpeechToTextButton accessibility specification](speechtotextbutton_aria.md) + +### More Actions View - PanelBar Component + +[PanelBar accessibility specification](panelbar_aria.md) + diff --git a/aria/appbar_aria.md b/aria/appbar_aria.md new file mode 100644 index 00000000000..5e2109fc2f6 --- /dev/null +++ b/aria/appbar_aria.md @@ -0,0 +1,8 @@ +## WAI-ARIA + + +This section lists the selectors, attributes, and behavior patterns supported by the component and its composite elements, if any. + + +The AppBar component is a container for elements and does not implement any wai-aria attributes. + diff --git a/aria/autocomplete_aria.md b/aria/autocomplete_aria.md new file mode 100644 index 00000000000..1be227c0e72 --- /dev/null +++ b/aria/autocomplete_aria.md @@ -0,0 +1,54 @@ +## WAI-ARIA + + +This section lists the selectors, attributes, and behavior patterns supported by the component and its composite elements, if any. + +### AutoComplete Wrapping Element + + +The following table summarizes the selectors and attributes supported by the AutoComplete wrapper element: + +| Selector | Attribute | Usage | +| -------- | --------- | ----- | +| `.k-autocomplete .k-input-inner` | `role=combobox` | Announces the presence of an AutoComplete as the inner element of the AutoComplete that is used for filtering. | +| | `label for` or `aria-label` or `aria-labelledby` | The input needs an accessible name that will be assigned to it. | +| | `aria-haspopup=listbox` | Indicates that the component has a listbox popup. | +| | `aria-expanded=true/false` | Announces the state of the popup visibility. | +| `.k-autocomplete .k-input-inner[aria-expanded="true"]` | `aria-controls=.k-list-ul id` | Points to the `listbox` element when the popup is open and the element exists. Signifies that the `combobox` element controls the `listbox` one. | +| `.k-autocomplete .k-input-inner[aria-expanded="true"][aria-activedescendant]` | `aria-activedescendant=.k-list-item.k-focus id` | Points to the focused item in the popup. The focused item is changed with keyboard navigation. If the popup is not visible or no item is focused, the attribute must be removed. | +| | `aria-autocomplete=list` or `aria-autocomplete=both` or `aria-autocomplete=inline` | The attribute value depends on enabled features: `list` for filtering, `both` for filtering + suggest, `inline` for suggest only. | +| | `readonly` or `aria-readonly` | The attribute is rendered only when the AutoComplete is read-only. | +| | `aria-busy=true` | The attribute is rendered only when the AutoComplete is loading data. | +| | `tabindex=0` | The element must be focusable. | +| `.k-autocomplete.k-invalid .k-input-inner,.k-autocomplete.ng-invalid .k-input-inner` | `aria-invalid=true` | The attribute is rendered only when the AutoComplete is in a form and announces the valid state of the component. | +| `.k-autocomplete.k-disabled .k-input-inner` | `disabled=disabled` or `aria-disabled=true` | The attribute is rendered only when the AutoComplete is disabled. | + +### Popup Listbox + + +The popup element of the AutoComplete has to implement the WAI-ARIA specification for a Popup List component. The following table summarizes the selectors and attributes supported by the listbox popup of the AutoComplete: + +| Selector | Attribute | Usage | +| -------- | --------- | ----- | +| `.k-autocomplete-popup-container` | `role=region` | When the component container is appended to the `` element of the document, it requires you to assing a `landmark` role to it. Otherwise, append it to an element with an appropriate `landmark` role. | +| | `aria-label` or `aria-labelledby` | When the container has a `region` role assigned, povides a label. | +| `.k-list .k-no-data` | `aria-live=polite` | Identifies the element as a live region in the `polite` state, meaning assistive technology users are informed about changes to the region at the next available opportunity. | +| `.k-list-item-icon` | `aria-hidden=true` | Ensures that the icon itself is hidden from assistive technologies since it is decorative. | +| `.k-list-content[role="listbox"]` | `role=listbox` | For grouped lists, the wrapper must have `role=listbox`. For ungrouped lists, the UL element has this role instead. | +| | `aria-label` or `aria-labelledby` | Provides a label for grouped listboxes. | +| `.k-list-ul[role="listbox"]` | `role=listbox` | For ungrouped lists, the `ul` element has `role=listbox` with appropriate aria-label/labelledby. | +| `.k-list-ul[role="group"]` | `role=group` | For grouped lists, each `ul` element has `role=group` and `aria-labelledby` referencing the group header id. | +| `.k-list-item` | `role=option` | Identifies the `li` element as a listbox option. | +| | `id` | List items should have an `id` attribute for `aria-activedescendant` navigation. | +| `.k-list-item:not(.k-focus)` | `tabindex=-1` | Only the focused option should have `tabindex=0`, all others should be `-1`. | +| `.k-list-item.k-selected` | `aria-selected=true` | Indicates the selected state of the item. | +| `.k-list-group-item` | `role=presentation` | For grouped lists, the group header element should have `role=presentation`. | +| | `id` | The group header must have an id that the corresponding `ul` element references via `aria-labelledby`. | + +### Adaptive Mode + + +When the component is in adaptive mode, the popup element follows the specifications of the ActionSheet component. + +[ActionSheet accessibility specification](actionsheet_aria.md) + diff --git a/aria/avatar_aria.md b/aria/avatar_aria.md new file mode 100644 index 00000000000..16cef89f34e --- /dev/null +++ b/aria/avatar_aria.md @@ -0,0 +1,9 @@ +## WAI-ARIA + + +This section lists the selectors, attributes, and behavior patterns supported by the component and its composite elements, if any. + +| Selector | Attribute | Usage | +| -------- | --------- | ----- | +| `.k-avatar img` | `alt` | Assures the presence of an `alt` attribute in a nested `img` tag inside the Avatar. | + diff --git a/aria/bottom-navigation_aria.md b/aria/bottom-navigation_aria.md new file mode 100644 index 00000000000..ec73fa272d3 --- /dev/null +++ b/aria/bottom-navigation_aria.md @@ -0,0 +1,14 @@ +## WAI-ARIA + + +This section lists the selectors, attributes, and behavior patterns supported by the component and its composite elements, if any. + + +The Bottom Navigation component is a landmark `