Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 18 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
13 changes: 13 additions & 0 deletions .github/instructions/html.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

159 changes: 159 additions & 0 deletions .github/prompts/accessibility.prompt.md
Original file line number Diff line number Diff line change
@@ -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 `<button>` over `<div role="button">`
- **Conditional values** — `aria-pressed={selected ? 'true' : 'false'}`, use `undefined` to omit
- **Dynamic IDs** — ``aria-controls={`${id}-listbox`}``; IDs must reference real DOM elements
- **Icon-only buttons** — require `aria-label`
- **`disabled` propagation** — pass to all interactive children
- **No structural changes** — only add attributes; never add or remove HTML elements
- **`tests/` folder** — do not modify unless specifically needed for missing coverage

#### Patterns

**Attribute placement** — always after `className`:
```tsx
// ❌ BAD
<button role="button" className="k-button">

// ✅ GOOD
<button className="k-button" role="button" aria-label="Close">
```

**Conditional / state-based attributes:**
```tsx
<button
className="k-button"
aria-expanded={opened ? 'true' : 'false'}
aria-disabled={disabled ? 'true' : undefined}
{...(loading && { 'aria-busy': 'true' })}
/>
```

**Dynamic ID references:**
```tsx
const { id = 'combobox' } = props;

<input
className="k-input-inner"
role="combobox"
aria-controls={opened ? `${id}-listbox` : undefined}
aria-activedescendant={opened ? `${id}-item-${focusedIndex}` : undefined}
/>
```

**Icon-only buttons:**
```tsx
// ❌ BAD — no accessible name
<Button icon="close" />

// ✅ GOOD
<Button icon="close" aria-label="Close dialog" />
```

**Attribute-only changes — never add/remove elements:**
```tsx
// ❌ BAD — adding wrapper element
<div role="region" aria-label="Actions">
<Button>Save</Button>
</div>

// ✅ GOOD — attributes on existing element
<Button aria-label="Save document">Save</Button>
```

**Coverage gaps — add templates for untested states:**
```tsx
// templates/combobox-disabled.tsx
export const ComboboxDisabled = (props) => (
<Combobox disabled {...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
2 changes: 1 addition & 1 deletion .github/prompts/variable-create.prompt.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
mode: "agent"
agent: "agent"
tools: ["codebase", "editFiles"]
description: "Create new SCSS variables using Kendo UI themes best practices"
---
Expand Down
2 changes: 1 addition & 1 deletion .github/prompts/variable-docs.prompt.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
mode: "agent"
agent: "agent"
tools: ["codebase", "editFiles"]
description: "Create comprehensive SCSS variable documentation using Kendo UI themes best practices"
---
Expand Down
89 changes: 89 additions & 0 deletions .github/workflows/_test-a11y-specs.yml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions aria/action-sheet_aria.md
Original file line number Diff line number Diff line change
@@ -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.

Loading
Loading