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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/a11y-form-controls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiny-design/react': minor
---

Improve form-control accessibility. `Form.Item` now generates ids that wire `aria-labelledby`, `aria-describedby`, and `aria-invalid` on the wrapped control automatically, so a label always announces with its input and screen readers hear validation errors. `Cascader` forwards the consumer's ref to the wrapper and pipes `id` and `aria-*` props through to the combobox element. `InputNumber` omits `min`, `max`, `aria-valuemin`, and `aria-valuemax` when the bounds are not finite (previously emitted `Infinity` / `-Infinity` strings) and forwards remaining native input props. DatePicker's weekday header and dim/disabled cell text now use `--ty-color-text-secondary` (with the previous `--ty-*-color-muted` token as fallback) so the picker meets WCAG color-contrast on the popup.
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,20 @@ jobs:

- name: Test
run: pnpm test -- --coverage

- name: Install browser for Playwright
run: pnpm exec playwright install --with-deps chrome

- name: Accessibility tests
run: pnpm test:accessibility

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
playwright-report/
apps/docs/playwright-report/
apps/docs/test-results/
if-no-files-found: ignore
33 changes: 33 additions & 0 deletions apps/docs/playwright.accessibility.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests/accessibility',
outputDir: './test-results/accessibility',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI
? [
['html', { outputFolder: 'apps/docs/playwright-report/accessibility', open: 'never' }],
['list'],
]
: 'list',
webServer: {
command: 'pnpm exec vite serve --host 127.0.0.1 --port 3004',
url: 'http://127.0.0.1:3004',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
use: {
...devices['Desktop Chrome'],
baseURL: 'http://127.0.0.1:3004',
channel: 'chrome',
colorScheme: 'light',
deviceScaleFactor: 1,
locale: 'en-US',
timezoneId: 'UTC',
viewport: { width: 1280, height: 900 },
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
},
});
84 changes: 84 additions & 0 deletions apps/docs/tests/accessibility/component-accessibility.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test, type Locator, type Page } from '@playwright/test';
import { gotoComponent, openFromDemo, previewByTitle, scrollDemoIntoView } from '../visual/helpers';

const WCAG_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];

let scanId = 0;

const formatViolations = (violations: Awaited<ReturnType<AxeBuilder['analyze']>>['violations']) =>
violations.map(({ id, impact, description, nodes }) => ({
id,
impact,
description,
targets: nodes.slice(0, 5).map((node) => node.target.join(' ')),
}));

const markLocator = async (locator: Locator) => {
const marker = `a11y-scan-${++scanId}`;
await locator.evaluate((node, value) => {
node.setAttribute('data-a11y-scan', value);
}, marker);

return `[data-a11y-scan="${marker}"]`;
};

const scan = async (page: Page, target: Locator | string) => {
const selector = typeof target === 'string' ? target : await markLocator(target);
const results = await new AxeBuilder({ page })
.withTags(WCAG_TAGS)
.disableRules(['region'])
.include(selector)
.analyze();

expect(formatViolations(results.violations)).toEqual([]);
};

test.describe('component accessibility checks', () => {
test('form controls and table demos have no WCAG violations', async ({ page }) => {
await gotoComponent(page, 'form');
await scrollDemoIntoView(page, 'Basic usage');
await scan(page, previewByTitle(page, 'Basic usage'));

await gotoComponent(page, 'table');
await scrollDemoIntoView(page, 'Basic');
await scan(page, previewByTitle(page, 'Basic'));

await scrollDemoIntoView(page, 'Row Selection');
await scan(page, previewByTitle(page, 'Row Selection'));
});

test('overlay components have no WCAG violations when open', async ({ page }) => {
await gotoComponent(page, 'modal');
await openFromDemo(page, 'Basic', 'button');
await expect(page.locator('.ty-modal__content')).toBeVisible();
await scan(page, '.ty-modal__content');

await gotoComponent(page, 'drawer');
await openFromDemo(page, 'Basic', 'button');
await expect(page.locator('.ty-drawer__content')).toBeVisible();
await scan(page, '.ty-drawer__content');

await gotoComponent(page, 'tour');
await openFromDemo(page, 'Basic', 'button:has-text("Start Tour")');
await expect(page.locator('.ty-tour')).toBeVisible();
await scan(page, '.ty-tour');
});

test('select and picker popups have no WCAG violations when open', async ({ page }) => {
await gotoComponent(page, 'select');
await openFromDemo(page, 'Search', '.ty-select__selector');
await expect(page.locator('.ty-select__dropdown')).toBeVisible();
await scan(page, '.ty-select__dropdown');

await gotoComponent(page, 'date-picker');
await openFromDemo(page, 'Date Range', '.ty-date-picker__input');
await expect(page.locator('.ty-date-picker__dropdown')).toBeVisible();
await scan(page, '.ty-date-picker__dropdown');

await gotoComponent(page, 'cascader');
await openFromDemo(page, 'Change On Select', '.ty-cascader__selector');
await expect(page.locator('.ty-cascader__dropdown')).toBeVisible();
await scan(page, '.ty-cascader__dropdown');
});
});
7 changes: 7 additions & 0 deletions apps/docs/tests/visual/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ pnpm test:visual:update

The suite targets the docs app and uses the local Chrome channel to avoid storing
browser binaries in the repository workflow.

Accessibility coverage for the same high-risk component families lives in
`apps/docs/tests/accessibility` and runs with:

```sh
pnpm test:accessibility
```
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"test:accessibility": "playwright test -c apps/docs/playwright.accessibility.config.ts",
"test:visual": "playwright test -c apps/docs/playwright.config.ts",
"test:visual:update": "playwright test -c apps/docs/playwright.config.ts --update-snapshots",
"lint": "turbo run lint",
Expand All @@ -27,11 +28,13 @@
"release": "turbo run build && changeset publish"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.3",
"@changesets/changelog-github": "^0.6.0",
"@changesets/cli": "^2.30.0",
"@changesets/get-github-info": "^0.8.0",
"@eslint/js": "^9.0.0",
"@playwright/test": "^1.59.1",
"autoprefixer": "^10.4.27",
"eslint": "^9.0.0",
"eslint-plugin-jest": "^28.0.0",
"eslint-plugin-jest-dom": "^5.0.0",
Expand Down
6 changes: 0 additions & 6 deletions packages/mcp/src/data/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -3521,12 +3521,6 @@
"required": false,
"description": "Determine whether always display the control button"
},
{
"name": "children",
"type": "React.ReactNode",
"required": false,
"description": ""
},
{
"name": "style",
"type": "CSSProperties",
Expand Down
Loading
Loading