From 504522d42391fe6270b25709a6538bf47a64da29 Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Thu, 3 Oct 2024 00:44:37 -0700 Subject: [PATCH 01/21] use axe for automated a11y testing --- packages/web-components/package.json | 1 + .../web-components/src/button/button.spec.ts | 18 +++++++- packages/web-components/src/button/button.ts | 4 +- packages/web-components/src/helpers.tests.ts | 44 ++++++++++++++++++- yarn.lock | 43 +++++++----------- 5 files changed, 77 insertions(+), 33 deletions(-) diff --git a/packages/web-components/package.json b/packages/web-components/package.json index dfdad1b7c8f38c..f9a75eb4ac57cc 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -90,6 +90,7 @@ "test:dev": "playwright test" }, "devDependencies": { + "@axe-core/playwright": "^4.10.0", "@microsoft/fast-element": "2.0.0", "@tensile-perf/web-components": "~0.2.0", "@storybook/html": "7.6.20", diff --git a/packages/web-components/src/button/button.spec.ts b/packages/web-components/src/button/button.spec.ts index 7348cb470abc50..ac48bc31e9285b 100644 --- a/packages/web-components/src/button/button.spec.ts +++ b/packages/web-components/src/button/button.spec.ts @@ -1,9 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import AxeBuilder from '@axe-core/playwright'; +import { createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; + +const storybookDocId = 'components-button-button--docs'; test.describe('Button', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-button-button--button')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-button')); }); @@ -649,3 +652,14 @@ test.describe('Button', () => { expect(wasInvalid).toBeTruthy(); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-button')); + + const results = await new AxeBuilder({ page }).include('.sb-story').analyze(); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/button/button.ts b/packages/web-components/src/button/button.ts index bff33807e42ad1..a3c4e599815337 100644 --- a/packages/web-components/src/button/button.ts +++ b/packages/web-components/src/button/button.ts @@ -77,7 +77,7 @@ export class BaseButton extends FASTElement { */ public disabledFocusableChanged(previous: boolean, next: boolean): void { if (this.$fastController.isConnected) { - this.elementInternals.ariaDisabled = `${!!next}`; + this.elementInternals.ariaDisabled = `${!!next || !!this.disabled}`; } } @@ -260,7 +260,7 @@ export class BaseButton extends FASTElement { connectedCallback(): void { super.connectedCallback(); - this.elementInternals.ariaDisabled = `${!!this.disabledFocusable}`; + this.elementInternals.ariaDisabled = `${!!this.disabledFocusable || !!this.disabled}`; } constructor() { diff --git a/packages/web-components/src/helpers.tests.ts b/packages/web-components/src/helpers.tests.ts index 46d72c8403a92f..6bc6711df3fc6e 100644 --- a/packages/web-components/src/helpers.tests.ts +++ b/packages/web-components/src/helpers.tests.ts @@ -1,5 +1,5 @@ import qs from 'qs'; -import { expect as baseExpect, type ExpectMatcherState, type Locator } from '@playwright/test'; +import { expect as baseExpect, type ExpectMatcherState, type Locator, type Page } from '@playwright/test'; /** * Returns a formatted URL for a given Storybook fixture. @@ -84,3 +84,45 @@ async function toHaveCustomState( export const expect = baseExpect.extend({ toHaveCustomState, }); + +/** + * A helper function to override the built-in `HTMLElement.prototype.attachInternals()` + * method, the overridden `attachInternals()` would set ARIA-related attributes + * on the host element when they are set on the `ElementInternals` object, so that + * Axe is able to assess the element’s accessibility properly. + * @see https://github.com/dequelabs/axe-core/issues/4259 + * + * This function should be called before calling `page.goto(..)`. + */ +export async function createElementInternalsTrapsForAxe(page: Page) { + await page.addInitScript(() => { + function getAriaAttrName(prop: string | symbol): string | null { + return typeof prop === 'string' && (prop === 'role' || prop.startsWith('aria')) ? + prop.replace(/(?:aria)(\w+)/, (_, w) => `aria-${w.toLowerCase()}`) : + null; + } + + const original =HTMLElement.prototype.attachInternals; + HTMLElement.prototype.attachInternals = function() { + const originalInternals = original.call(this); + + return new Proxy(({} as ElementInternals), { + get(target, prop) { + return getAriaAttrName(prop) ? + Reflect.get(target, prop) ?? null : + Reflect.get(originalInternals, prop); + }, + set(target, prop, value) { + const attrName = getAriaAttrName(prop); + if (attrName) { + Reflect.set(target, prop, value); + originalInternals.shadowRoot?.host.setAttribute(attrName, value); + } else { + Reflect.set(originalInternals, prop, value); + } + return true; + }, + }); + }; + }); +} diff --git a/yarn.lock b/yarn.lock index 7d6ab50361cd48..e9796ecd48d400 100644 --- a/yarn.lock +++ b/yarn.lock @@ -52,6 +52,13 @@ dependencies: default-browser-id "3.0.0" +"@axe-core/playwright@^4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@axe-core/playwright/-/playwright-4.10.0.tgz#c9e8d0d694e8c0bd18c4636516dc22496fc03bfe" + integrity sha512-kEr3JPEVUSnKIYp/egV2jvFj+chIjCjPp3K3zlpJMza/CB3TFw8UZNbI9agEC2uMz4YbgAOyzlbUy0QS+OofFA== + dependencies: + axe-core "~4.10.0" + "@azure/abort-controller@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.1.0.tgz#788ee78457a55af8a1ad342acb182383d2119249" @@ -7161,6 +7168,11 @@ axe-core@^4.2.0, axe-core@^4.9.1: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.9.1.tgz#fcd0f4496dad09e0c899b44f6c4bb7848da912ae" integrity sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw== +axe-core@~4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59" + integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== + axios@^1.6.7, axios@^1.7.4: version "1.7.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" @@ -21522,7 +21534,7 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -21557,15 +21569,6 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -21666,7 +21669,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -21701,13 +21704,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -23952,7 +23948,7 @@ workspace-tools@^0.27.0: js-yaml "^4.1.0" micromatch "^4.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -23987,15 +23983,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From cd1252a284d9f5808a563093838830fcbe7e9a4b Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Thu, 3 Oct 2024 11:11:46 -0700 Subject: [PATCH 02/21] update change file --- ...eb-components-c7f76868-02ae-4651-82cc-a43a6f56208c.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-web-components-c7f76868-02ae-4651-82cc-a43a6f56208c.json diff --git a/change/@fluentui-web-components-c7f76868-02ae-4651-82cc-a43a6f56208c.json b/change/@fluentui-web-components-c7f76868-02ae-4651-82cc-a43a6f56208c.json new file mode 100644 index 00000000000000..57a0d2853b636b --- /dev/null +++ b/change/@fluentui-web-components-c7f76868-02ae-4651-82cc-a43a6f56208c.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "add axe automated a11y testing", + "packageName": "@fluentui/web-components", + "email": "machi@microsoft.com", + "dependentChangeType": "patch" +} From 3327ef48c644c113b8ec650387419984130725a1 Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Thu, 3 Oct 2024 17:04:54 -0700 Subject: [PATCH 03/21] add a11y tests to all components --- .../src/accordion/accordion.spec.ts | 23 +++++++++++- .../src/anchor-button/anchor-button.spec.ts | 17 ++++++++- .../web-components/src/avatar/avatar.spec.ts | 17 ++++++++- .../web-components/src/badge/badge.spec.ts | 17 ++++++++- .../web-components/src/button/button.spec.ts | 5 +-- .../src/checkbox/checkbox.spec.ts | 17 ++++++++- .../src/counter-badge/counter-badge.spec.ts | 17 ++++++++- .../src/dialog-body/dialog-body.spec.ts | 23 +++++++++++- .../web-components/src/dialog/dialog.spec.ts | 27 ++++++++++++-- .../src/divider/divider.spec.ts | 19 ++++++++-- .../web-components/src/drawer/drawer.spec.ts | 28 ++++++++++++-- .../web-components/src/field/field.spec.ts | 23 ++++++++++-- packages/web-components/src/helpers.tests.ts | 37 ++++++++++++++++--- .../web-components/src/image/image.spec.ts | 17 ++++++++- packages/web-components/src/link/link.spec.ts | 19 ++++++++-- .../src/menu-list/menu-list.spec.ts | 19 ++++++++-- packages/web-components/src/menu/menu.spec.ts | 33 ++++++++++++++++- .../message-bar.integration.spec.ts | 17 ++++++++- .../src/progress-bar/progress-bar.spec.ts | 18 ++++++++- .../src/radio-group/radio-group.spec.ts | 19 +++++++++- .../web-components/src/radio/radio.spec.ts | 18 ++++++++- .../src/rating-display/rating-display.spec.ts | 22 +++++++++-- .../web-components/src/slider/slider.spec.ts | 18 ++++++++- .../src/spinner/spinner.spec.ts | 18 ++++++++- .../web-components/src/switch/switch.spec.ts | 18 ++++++++- .../src/tablist/tablist.spec.ts | 21 ++++++++++- .../src/text-input/text-input.spec.ts | 19 +++++++++- packages/web-components/src/text/text.spec.ts | 18 ++++++++- .../src/textarea/textarea.spec.ts | 21 ++++++++++- .../src/toggle-button/toggle-button.spec.ts | 17 ++++++++- 30 files changed, 528 insertions(+), 74 deletions(-) diff --git a/packages/web-components/src/accordion/accordion.spec.ts b/packages/web-components/src/accordion/accordion.spec.ts index 26e9733fbedc41..a46a939257dece 100644 --- a/packages/web-components/src/accordion/accordion.spec.ts +++ b/packages/web-components/src/accordion/accordion.spec.ts @@ -1,10 +1,12 @@ import type { Locator } from '@playwright/test'; import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; + +const storybookDocId = 'components-accordion--docs'; test.describe('Accordion', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-accordion--accordion')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => Promise.all([ @@ -435,3 +437,20 @@ test.describe('Accordion', () => { await expect(item).toHaveAttribute('expanded'); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => + Promise.all([ + customElements.whenDefined('fluent-accordion'), + customElements.whenDefined('fluent-accordion-item'), + customElements.whenDefined('fluent-checkbox'), + ]), + ); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/anchor-button/anchor-button.spec.ts b/packages/web-components/src/anchor-button/anchor-button.spec.ts index 0c02e9a52d3cff..5760d4d06e6197 100644 --- a/packages/web-components/src/anchor-button/anchor-button.spec.ts +++ b/packages/web-components/src/anchor-button/anchor-button.spec.ts @@ -1,6 +1,8 @@ import { spinalCase } from '@microsoft/fast-web-utilities'; import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; + +const storybookDocId = 'components-button-anchor--docs'; const proxyAttributes = { href: 'href', @@ -27,7 +29,7 @@ const booleanAttributes = { test.describe('Anchor Button', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-button-anchor--anchor-button')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-anchor-button')); }); @@ -143,3 +145,14 @@ test.describe('Anchor Button', () => { expect(newPage.url()).toContain(expectedUrl); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-anchor-button')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/avatar/avatar.spec.ts b/packages/web-components/src/avatar/avatar.spec.ts index 18e38a1bb38e47..35f1b3b4a6059d 100644 --- a/packages/web-components/src/avatar/avatar.spec.ts +++ b/packages/web-components/src/avatar/avatar.spec.ts @@ -1,11 +1,13 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Avatar } from './avatar.js'; import { AvatarAppearance, AvatarColor, AvatarSize } from './avatar.options.js'; +const storybookDocId = 'components-avatar--docs'; + test.describe('Avatar Component', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-avatar--image')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-avatar')); }); @@ -209,3 +211,14 @@ test.describe('Avatar Component', () => { } }); }); + +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-avatar')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/badge/badge.spec.ts b/packages/web-components/src/badge/badge.spec.ts index 9482c86f25bda7..bb8d7d960861d6 100644 --- a/packages/web-components/src/badge/badge.spec.ts +++ b/packages/web-components/src/badge/badge.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Badge } from './badge.js'; +const storybookDocId = 'components-badge-badge--docs'; + test.describe('Badge component', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-badge--badge')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-badge')); }); @@ -209,3 +211,14 @@ test.describe('Badge component', () => { await expect(element).toHaveJSProperty('shape', 'square'); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-badge')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/button/button.spec.ts b/packages/web-components/src/button/button.spec.ts index ac48bc31e9285b..9761c40b82dab3 100644 --- a/packages/web-components/src/button/button.spec.ts +++ b/packages/web-components/src/button/button.spec.ts @@ -1,6 +1,5 @@ import { test } from '@playwright/test'; -import AxeBuilder from '@axe-core/playwright'; -import { createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; const storybookDocId = 'components-button-button--docs'; @@ -659,7 +658,7 @@ test('should not have auto detectable accessibility issues', async ({ page }) => await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-button')); - const results = await new AxeBuilder({ page }).include('.sb-story').analyze(); + const results = await analyzePageWithAxe(page); expect(results.violations).toEqual([]); }); diff --git a/packages/web-components/src/checkbox/checkbox.spec.ts b/packages/web-components/src/checkbox/checkbox.spec.ts index 1d945a1f09bd44..4c4ebae4e44ceb 100644 --- a/packages/web-components/src/checkbox/checkbox.spec.ts +++ b/packages/web-components/src/checkbox/checkbox.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Checkbox } from './checkbox.js'; +const storybookDocId = 'components-checkbox--docs'; + test.describe('Checkbox', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-checkbox--checkbox')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-checkbox')); }); @@ -480,3 +482,14 @@ test.describe('Checkbox', () => { expect(page.url()).toContain('?checkbox=foo&checkbox=bar'); }); }); + +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-checkbox')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/counter-badge/counter-badge.spec.ts b/packages/web-components/src/counter-badge/counter-badge.spec.ts index dada9b854fd54d..4cce6a80d6219a 100644 --- a/packages/web-components/src/counter-badge/counter-badge.spec.ts +++ b/packages/web-components/src/counter-badge/counter-badge.spec.ts @@ -1,5 +1,5 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { CounterBadge } from './counter-badge.js'; import { CounterBadgeAppearance, @@ -8,9 +8,11 @@ import { CounterBadgeSize, } from './counter-badge.options.js'; +const storybookDocId = 'components-badge-counter-badge--docs'; + test.describe('CounterBadge component', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-badge-counter-badge--counter-badge')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-counter-badge')); }); @@ -248,3 +250,14 @@ test.describe('CounterBadge component', () => { }); } }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-counter-badge')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/dialog-body/dialog-body.spec.ts b/packages/web-components/src/dialog-body/dialog-body.spec.ts index c791aa6dce0e66..6a18b9e0fdcaa9 100644 --- a/packages/web-components/src/dialog-body/dialog-body.spec.ts +++ b/packages/web-components/src/dialog-body/dialog-body.spec.ts @@ -1,11 +1,13 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Dialog } from '../dialog/dialog.js'; import type { DialogBody } from './dialog-body.js'; +const storybookDocId = 'components-dialog-dialog-body--docs'; + test.describe('Dialog Body', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-dialog-dialog-body--default')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => Promise.all([ @@ -66,3 +68,20 @@ test.describe('Dialog Body', () => { }); }); }); + +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => + Promise.all([ + customElements.whenDefined('fluent-button'), + customElements.whenDefined('fluent-dialog'), + customElements.whenDefined('fluent-dialog-body'), + ]), + ); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/dialog/dialog.spec.ts b/packages/web-components/src/dialog/dialog.spec.ts index 15d2ec0ae824df..949f9a3bc914c8 100644 --- a/packages/web-components/src/dialog/dialog.spec.ts +++ b/packages/web-components/src/dialog/dialog.spec.ts @@ -1,7 +1,9 @@ -import { expect, test } from '@playwright/test'; +import { test } from '@playwright/test'; import type { Locator } from '@playwright/test'; -import { fixtureURL } from '../helpers.tests.js'; -import { Dialog } from './dialog.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; +import type { Dialog } from './dialog.js'; + +const storybookDocId = 'components-dialog-dialog--docs'; async function getPointOutside(element: Locator) { // Get the bounding box of the element @@ -16,7 +18,7 @@ async function getPointOutside(element: Locator) { test.describe('Dialog', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-dialog-dialog--default')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => Promise.all([ @@ -237,3 +239,20 @@ test.describe('Dialog', () => { }); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => + Promise.all([ + customElements.whenDefined('fluent-button'), + customElements.whenDefined('fluent-dialog'), + customElements.whenDefined('fluent-dialog-body'), + ]), + ); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/divider/divider.spec.ts b/packages/web-components/src/divider/divider.spec.ts index b4e4d237f57e11..6f4ff343d3d7cc 100644 --- a/packages/web-components/src/divider/divider.spec.ts +++ b/packages/web-components/src/divider/divider.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; -import { Divider } from './divider.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; +import type { Divider } from './divider.js'; + +const storybookDocId = 'components-divider--docs'; test.describe('Divider', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-divider--divider')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-divider')); }); @@ -211,3 +213,14 @@ test.describe('Divider', () => { await expect(element).not.toHaveCustomState('align-end'); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-divider')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/drawer/drawer.spec.ts b/packages/web-components/src/drawer/drawer.spec.ts index 2d167763e75940..84024dc6ad9c1f 100644 --- a/packages/web-components/src/drawer/drawer.spec.ts +++ b/packages/web-components/src/drawer/drawer.spec.ts @@ -1,11 +1,13 @@ -import { expect, test } from '@playwright/test'; +import { test } from '@playwright/test'; -import { fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Drawer } from './drawer.js'; +const storybookDocId = 'components-drawer--docs'; + test.describe('Drawer', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-drawer--drawer')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-drawer')); }); @@ -205,3 +207,23 @@ test.describe('Drawer', () => { expect(wasDismissed).toBe(true); }); }); + +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-drawer')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); + + const button = page.getByText('Toggle Drawer'); + const drawer = page.locator('fluent-drawer-body'); + await button.click(); + await drawer.waitFor({ state: 'visible' }); + + const openedResult = await analyzePageWithAxe(page); + + expect(openedResult.violations).toEqual([]); +}); diff --git a/packages/web-components/src/field/field.spec.ts b/packages/web-components/src/field/field.spec.ts index d661608b8a2e4c..4776bade169ad4 100644 --- a/packages/web-components/src/field/field.spec.ts +++ b/packages/web-components/src/field/field.spec.ts @@ -1,11 +1,13 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; -import { TextInput } from '../index.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; +import type { TextInput } from '../index.js'; import type { Field } from './field.js'; +const storybookDocId = 'components-field--docs'; + test.describe('Field', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-field--field')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-field')); }); @@ -482,3 +484,18 @@ test.describe('Field', () => { await expect(element.locator('input:right-of(label)')).toHaveCount(1); }); }); + +// FIXME: Should remove all examples of using `` because it’s not +// currently supported and it’s causing ARIA issues since `` would add +// `aria-labelledby` on the `` element but it doesn’t have a valid +// labellable role. +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-field')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/helpers.tests.ts b/packages/web-components/src/helpers.tests.ts index 6bc6711df3fc6e..3244f7306921e8 100644 --- a/packages/web-components/src/helpers.tests.ts +++ b/packages/web-components/src/helpers.tests.ts @@ -1,4 +1,5 @@ import qs from 'qs'; +import AxeBuilder from '@axe-core/playwright'; import { expect as baseExpect, type ExpectMatcherState, type Locator, type Page } from '@playwright/test'; /** @@ -92,7 +93,9 @@ export const expect = baseExpect.extend({ * Axe is able to assess the element’s accessibility properly. * @see https://github.com/dequelabs/axe-core/issues/4259 * - * This function should be called before calling `page.goto(..)`. + * This function should be called before calling `page.goto(..)`. And it should + * be contained in 1-2 tests per suite. It shouldn’t be used casually due to the + * heavy-handedness of modifying built-in API. */ export async function createElementInternalsTrapsForAxe(page: Page) { await page.addInitScript(() => { @@ -102,21 +105,34 @@ export async function createElementInternalsTrapsForAxe(page: Page) { null; } - const original =HTMLElement.prototype.attachInternals; + const original = HTMLElement.prototype.attachInternals; HTMLElement.prototype.attachInternals = function() { const originalInternals = original.call(this); return new Proxy(({} as ElementInternals), { get(target, prop) { - return getAriaAttrName(prop) ? - Reflect.get(target, prop) ?? null : - Reflect.get(originalInternals, prop); + if (getAriaAttrName(prop)) { + return Reflect.get(target, prop) ?? null; + } + + const propValue = Reflect.get(originalInternals, prop); + + if (typeof propValue === 'function') { + return propValue.bind(originalInternals); + } + + return propValue; }, set(target, prop, value) { const attrName = getAriaAttrName(prop); if (attrName) { Reflect.set(target, prop, value); - originalInternals.shadowRoot?.host.setAttribute(attrName, value); + const host = originalInternals.shadowRoot?.host; + if (value) { + host?.setAttribute(attrName, value); + } else { + host?.removeAttribute(attrName); + } } else { Reflect.set(originalInternals, prop, value); } @@ -126,3 +142,12 @@ export async function createElementInternalsTrapsForAxe(page: Page) { }; }); } + +/** + * Helper function to run Axe analysis. The main motivation of creating this + * function is to centralize the `.include('.sb-story')` call in case Storybook + * changes the class name in future. + */ +export async function analyzePageWithAxe(page: Page): Promise> { + return await new AxeBuilder({ page }).include('.sb-story').analyze(); +} diff --git a/packages/web-components/src/image/image.spec.ts b/packages/web-components/src/image/image.spec.ts index 5ff754a011e079..4d89c3e28b8f82 100644 --- a/packages/web-components/src/image/image.spec.ts +++ b/packages/web-components/src/image/image.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Image } from './image.js'; +const storybookDocId = 'components-image--docs'; + test.describe('Image', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-image--image')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-image')); }); @@ -233,3 +235,14 @@ test.describe('Image', () => { await expect(element).not.toHaveCustomState('circular'); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-image')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/link/link.spec.ts b/packages/web-components/src/link/link.spec.ts index f71399fe13723e..6d5381e181ed3a 100644 --- a/packages/web-components/src/link/link.spec.ts +++ b/packages/web-components/src/link/link.spec.ts @@ -1,7 +1,9 @@ import { spinalCase } from '@microsoft/fast-web-utilities'; import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; -import { Link } from './link.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; +import type { Link } from './link.js'; + +const storybookDocId = 'components-link--docs'; const proxyAttributes = { href: 'href', @@ -21,7 +23,7 @@ const attributes = { test.describe('Link', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-link--appearance')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-link')); }); @@ -89,3 +91,14 @@ test.describe('Link', () => { await expect(element).not.toHaveCustomState('inline'); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-link')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/menu-list/menu-list.spec.ts b/packages/web-components/src/menu-list/menu-list.spec.ts index d4dcf9cd0447e4..463faceb01276c 100644 --- a/packages/web-components/src/menu-list/menu-list.spec.ts +++ b/packages/web-components/src/menu-list/menu-list.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import { MenuItemRole } from '../menu-item/menu-item.options.js'; -test.describe('Menu', () => { +const storybookDocId = 'components-menulist--docs'; + +test.describe('MenuList', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-menulist--menu-list')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-menu-list')); }); @@ -522,3 +524,14 @@ test.describe('Menu', () => { } }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-menu-list')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/menu/menu.spec.ts b/packages/web-components/src/menu/menu.spec.ts index 87aa9a9af1770f..8b6dde7b26d6db 100644 --- a/packages/web-components/src/menu/menu.spec.ts +++ b/packages/web-components/src/menu/menu.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Menu } from './menu.js'; +const storybookDocId = 'components-menu--docs'; + test.describe('Menu', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-menu--default')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => Promise.all([ @@ -343,3 +345,30 @@ test.describe('Menu', () => { await expect(menuItems.first()).toBeFocused(); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => + Promise.all([ + customElements.whenDefined('fluent-menu'), + customElements.whenDefined('fluent-menu-list'), + customElements.whenDefined('fluent-menu-item'), + customElements.whenDefined('fluent-menu-button'), + ]), + ); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); + + const menu = page.locator('fluent-menu').nth(0); + const menuButton = menu.locator('fluent-menu-button'); + const menuList = menu.locator('fluent-menu-list'); + await menuButton.click(); + await menuList.waitFor({ state: 'visible' }); + const openedResults = await analyzePageWithAxe(page); + + expect(openedResults.violations).toEqual([]); +}); diff --git a/packages/web-components/src/message-bar/message-bar.integration.spec.ts b/packages/web-components/src/message-bar/message-bar.integration.spec.ts index 32e82043e951f4..a5219d62e483fe 100644 --- a/packages/web-components/src/message-bar/message-bar.integration.spec.ts +++ b/packages/web-components/src/message-bar/message-bar.integration.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { MessageBar } from './message-bar.js'; +const storybookDocId = 'components-messagebar--docs'; + test.describe('Message Bar', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-messagebar--default')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-message-bar')); }); @@ -101,3 +103,14 @@ test.describe('Message Bar', () => { await expect(element).toHaveAttribute('dismissed', 'true'); }); }); + +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-message-bar')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/progress-bar/progress-bar.spec.ts b/packages/web-components/src/progress-bar/progress-bar.spec.ts index a39baa7ef63d85..124c202eb7aaa5 100644 --- a/packages/web-components/src/progress-bar/progress-bar.spec.ts +++ b/packages/web-components/src/progress-bar/progress-bar.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { ProgressBar } from './progress-bar.js'; +const storybookDocId = 'components-progressbar--docs'; + test.describe('Progress Bar', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-progressbar--default')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-progress-bar')); }); @@ -166,3 +168,15 @@ test.describe('Progress Bar', () => { await expect(element).not.toHaveCustomState('error'); }); }); + +// FIXME: Story examples needs accessible names. +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-progress-bar')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/radio-group/radio-group.spec.ts b/packages/web-components/src/radio-group/radio-group.spec.ts index efda17debdf0e4..66d14cf86bee8d 100644 --- a/packages/web-components/src/radio-group/radio-group.spec.ts +++ b/packages/web-components/src/radio-group/radio-group.spec.ts @@ -1,11 +1,13 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Radio } from '../radio/index.js'; import type { RadioGroup } from './radio-group.js'; +const storybookDocId = 'components-radiogroup--docs'; + test.describe('RadioGroup', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-radiogroup--radio-group')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => Promise.all([customElements.whenDefined('fluent-radio'), customElements.whenDefined('fluent-radio-group')]), @@ -639,3 +641,16 @@ test.describe('RadioGroup', () => { await expect(page).not.toHaveURL(/radio=/); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => + Promise.all([customElements.whenDefined('fluent-radio'), customElements.whenDefined('fluent-radio-group')]), + ); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/radio/radio.spec.ts b/packages/web-components/src/radio/radio.spec.ts index 6e8cf6ae074913..5f424805174fca 100644 --- a/packages/web-components/src/radio/radio.spec.ts +++ b/packages/web-components/src/radio/radio.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Radio } from './radio.js'; +const storybookDocId = 'components-radio--docs'; + test.describe('Radio', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-radio--radio')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-radio')); }); @@ -349,3 +351,15 @@ test.describe('Radio', () => { expect(page.url()).not.toContain('?radio=foo'); }); }); + +// FIXME: radio examples need accessible names +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-radio')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/rating-display/rating-display.spec.ts b/packages/web-components/src/rating-display/rating-display.spec.ts index 4215afb18bf950..657dce8d94822b 100644 --- a/packages/web-components/src/rating-display/rating-display.spec.ts +++ b/packages/web-components/src/rating-display/rating-display.spec.ts @@ -1,13 +1,15 @@ -import { Locator, test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { type Locator, test } from '@playwright/test'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import { RatingDisplaySize } from './rating-display.options.js'; -import { RatingDisplay } from './rating-display.js'; +import type { RatingDisplay } from './rating-display.js'; + +const storybookDocId ='components-rating-display--docs'; test.describe('Rating Display', () => { let element: Locator; test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-rating-display--default')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-rating-display')); element = page.locator('fluent-rating-display'); @@ -204,3 +206,15 @@ test.describe('Rating Display', () => { await expect(icon.locator('> use')).toBeHidden(); }); }); + +// FIXME: Star images need accessible names. +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-rating-display')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/slider/slider.spec.ts b/packages/web-components/src/slider/slider.spec.ts index ac63388a34df0a..1656326e6c3add 100644 --- a/packages/web-components/src/slider/slider.spec.ts +++ b/packages/web-components/src/slider/slider.spec.ts @@ -1,11 +1,13 @@ import type { Direction } from '@microsoft/fast-web-utilities'; import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Slider } from './slider.js'; +const storybookDocId = 'components-slider--docs'; + test.describe('Slider', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-slider--slider')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-slider')); }); @@ -774,3 +776,15 @@ test.describe('Slider', () => { }); }); }); + +// FIXME: slider examples need accesible names. +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-slider')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/spinner/spinner.spec.ts b/packages/web-components/src/spinner/spinner.spec.ts index a509677369d4c6..281c332723a6ef 100644 --- a/packages/web-components/src/spinner/spinner.spec.ts +++ b/packages/web-components/src/spinner/spinner.spec.ts @@ -1,11 +1,13 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Spinner } from './spinner.js'; import { SpinnerAppearance, SpinnerSize } from './spinner.options.js'; +const storybookDocId = 'components-spinner--docs'; + test.describe('Spinner', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-spinner--default')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-spinner')); }); @@ -56,3 +58,15 @@ test.describe('Spinner', () => { }); } }); + +// FIXME: examples need accessible names +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-spinner')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/switch/switch.spec.ts b/packages/web-components/src/switch/switch.spec.ts index 3b9617428ea86c..1e6b36ea400fc6 100644 --- a/packages/web-components/src/switch/switch.spec.ts +++ b/packages/web-components/src/switch/switch.spec.ts @@ -1,10 +1,12 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Switch } from './switch.js'; +const storybookDocId = 'components-switch--docs'; + test.describe('Switch', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-switch--switch')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-switch')); }); @@ -289,3 +291,15 @@ test.describe('Switch', () => { expect(page.url()).toContain('?switch=foo&switch=bar'); }); }); + +// FIXME: examples need accessible names. +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-switch')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/tablist/tablist.spec.ts b/packages/web-components/src/tablist/tablist.spec.ts index c5acaddae169ac..3bcd043fd60522 100644 --- a/packages/web-components/src/tablist/tablist.spec.ts +++ b/packages/web-components/src/tablist/tablist.spec.ts @@ -1,12 +1,14 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Tab } from '../tab/tab.js'; import type { Tablist } from './tablist.js'; import { TablistAppearance, TablistSize } from './tablist.options.js'; +const storybookDocId = 'components-tablist--docs'; + test.describe('Tablist', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-tabs--tabs-default')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => Promise.all([customElements.whenDefined('fluent-tablist'), customElements.whenDefined('fluent-tab')]), @@ -401,3 +403,18 @@ test.describe('Tablist', () => { await expect(element).toHaveJSProperty('activeid', secondTabId); }); }); + +// FIXME: When tablist is disabled, either itself or all of its tab children may need +// `aria-disabled=true`. +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => + Promise.all([customElements.whenDefined('fluent-tablist'), customElements.whenDefined('fluent-tab')]), + ); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/text-input/text-input.spec.ts b/packages/web-components/src/text-input/text-input.spec.ts index f16fa96736308c..8f072976f05964 100644 --- a/packages/web-components/src/text-input/text-input.spec.ts +++ b/packages/web-components/src/text-input/text-input.spec.ts @@ -1,11 +1,13 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { TextInput } from './text-input.js'; import { ImplicitSubmissionBlockingTypes } from './text-input.options.js'; +const storybookDocId = 'components-textinput--docs'; + test.describe('TextInput', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-textinput--default')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-text-input')); }); @@ -834,3 +836,16 @@ test.describe('TextInput', () => { }); }); }); + +// FIXME: examples need accessible names. +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-text-input')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); + diff --git a/packages/web-components/src/text/text.spec.ts b/packages/web-components/src/text/text.spec.ts index 52a1dc3c313cf9..5c37353d393f66 100644 --- a/packages/web-components/src/text/text.spec.ts +++ b/packages/web-components/src/text/text.spec.ts @@ -1,11 +1,13 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Text } from './text.js'; import { TextAlign, TextFont, TextSize, TextWeight } from './text.options.js'; +const storybookDocId = 'components-text--docs'; + test.describe('Text Component', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-text--text')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-text')); }); @@ -102,3 +104,15 @@ test.describe('Text Component', () => { }); } }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-text')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); + diff --git a/packages/web-components/src/textarea/textarea.spec.ts b/packages/web-components/src/textarea/textarea.spec.ts index 6828d283b9cb2c..71c230f957946f 100644 --- a/packages/web-components/src/textarea/textarea.spec.ts +++ b/packages/web-components/src/textarea/textarea.spec.ts @@ -1,12 +1,14 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import { LabelSize } from '../label/label.options.js'; import { TextAreaAppearance, TextAreaResize, TextAreaSize } from './textarea.options.js'; import type { TextArea } from './textarea.js'; +const storybookDocId = 'components-textarea--docs'; + test.describe('TextArea', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-textarea--text-area')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-textarea')); }); @@ -1027,3 +1029,18 @@ test.describe('TextArea', () => { }); }); }); + +// FIXME: examples need accessible names. +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => Promise.all([ + customElements.whenDefined('fluent-textarea'), + customElements.whenDefined('fluent-label'), + ])); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/toggle-button/toggle-button.spec.ts b/packages/web-components/src/toggle-button/toggle-button.spec.ts index 07e46ca5f9f12e..2233ec90a37aa3 100644 --- a/packages/web-components/src/toggle-button/toggle-button.spec.ts +++ b/packages/web-components/src/toggle-button/toggle-button.spec.ts @@ -1,9 +1,11 @@ import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; +import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; + +const storybookDocId = 'components-button-toggle-button--docs'; test.describe('Toggle Button', () => { test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-button-toggle-button--button')); + await page.goto(fixtureURL(storybookDocId)); await page.waitForFunction(() => customElements.whenDefined('fluent-toggle-button')); }); @@ -160,3 +162,14 @@ test.describe('Toggle Button', () => { await expect(element).toHaveCustomState('pressed'); }); }); + +test('should not have auto detectable accessibility issues', async ({ page }) => { + await createElementInternalsTrapsForAxe(page); + + await page.goto(fixtureURL(storybookDocId)); + await page.waitForFunction(() => customElements.whenDefined('fluent-toggle-button')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); From 33052b56f05bda519a06562f33a687ec55b460fa Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Thu, 3 Oct 2024 17:10:31 -0700 Subject: [PATCH 04/21] fix formatting --- .../src/rating-display/rating-display.spec.ts | 2 +- packages/web-components/src/text-input/text-input.spec.ts | 1 - packages/web-components/src/text/text.spec.ts | 1 - packages/web-components/src/textarea/textarea.spec.ts | 7 +++---- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/web-components/src/rating-display/rating-display.spec.ts b/packages/web-components/src/rating-display/rating-display.spec.ts index 657dce8d94822b..7c31ea7113b308 100644 --- a/packages/web-components/src/rating-display/rating-display.spec.ts +++ b/packages/web-components/src/rating-display/rating-display.spec.ts @@ -3,7 +3,7 @@ import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureU import { RatingDisplaySize } from './rating-display.options.js'; import type { RatingDisplay } from './rating-display.js'; -const storybookDocId ='components-rating-display--docs'; +const storybookDocId = 'components-rating-display--docs'; test.describe('Rating Display', () => { let element: Locator; diff --git a/packages/web-components/src/text-input/text-input.spec.ts b/packages/web-components/src/text-input/text-input.spec.ts index 8f072976f05964..59c835f2e7b04f 100644 --- a/packages/web-components/src/text-input/text-input.spec.ts +++ b/packages/web-components/src/text-input/text-input.spec.ts @@ -848,4 +848,3 @@ test.fixme('should not have auto detectable accessibility issues', async ({ page expect(results.violations).toEqual([]); }); - diff --git a/packages/web-components/src/text/text.spec.ts b/packages/web-components/src/text/text.spec.ts index 5c37353d393f66..04e190743b5542 100644 --- a/packages/web-components/src/text/text.spec.ts +++ b/packages/web-components/src/text/text.spec.ts @@ -115,4 +115,3 @@ test('should not have auto detectable accessibility issues', async ({ page }) => expect(results.violations).toEqual([]); }); - diff --git a/packages/web-components/src/textarea/textarea.spec.ts b/packages/web-components/src/textarea/textarea.spec.ts index 71c230f957946f..63a3c9d61cdfdf 100644 --- a/packages/web-components/src/textarea/textarea.spec.ts +++ b/packages/web-components/src/textarea/textarea.spec.ts @@ -1035,10 +1035,9 @@ test.fixme('should not have auto detectable accessibility issues', async ({ page await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); - await page.waitForFunction(() => Promise.all([ - customElements.whenDefined('fluent-textarea'), - customElements.whenDefined('fluent-label'), - ])); + await page.waitForFunction(() => + Promise.all([customElements.whenDefined('fluent-textarea'), customElements.whenDefined('fluent-label')]), + ); const results = await analyzePageWithAxe(page); From df9d70c6103260d7193f773aaa89eece76096720 Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Thu, 3 Oct 2024 17:27:29 -0700 Subject: [PATCH 05/21] allow aria attributes to be set as boolean false --- packages/web-components/src/helpers.tests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web-components/src/helpers.tests.ts b/packages/web-components/src/helpers.tests.ts index 3244f7306921e8..f9c6a590845524 100644 --- a/packages/web-components/src/helpers.tests.ts +++ b/packages/web-components/src/helpers.tests.ts @@ -128,8 +128,8 @@ export async function createElementInternalsTrapsForAxe(page: Page) { if (attrName) { Reflect.set(target, prop, value); const host = originalInternals.shadowRoot?.host; - if (value) { - host?.setAttribute(attrName, value); + if (value !== null || value !== undefined) { + host?.setAttribute(attrName, value.toString()); } else { host?.removeAttribute(attrName); } From 23cbc9b384fccd2db0ba738b41367b9138c41e1a Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Fri, 4 Oct 2024 11:02:06 -0700 Subject: [PATCH 06/21] also set aria props on element internals --- packages/web-components/src/helpers.tests.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/web-components/src/helpers.tests.ts b/packages/web-components/src/helpers.tests.ts index f9c6a590845524..ae2bc3f0bd3ebc 100644 --- a/packages/web-components/src/helpers.tests.ts +++ b/packages/web-components/src/helpers.tests.ts @@ -133,10 +133,9 @@ export async function createElementInternalsTrapsForAxe(page: Page) { } else { host?.removeAttribute(attrName); } - } else { - Reflect.set(originalInternals, prop, value); } - return true; + + return Reflect.set(originalInternals, prop, value); }, }); }; From f27e8cc9f63c2e1e0f6885c8e6cd5545976d2acb Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Fri, 4 Oct 2024 15:49:17 -0700 Subject: [PATCH 07/21] fix exiting accessibility issues in components and stories --- .../web-components/src/avatar/avatar.spec.ts | 4 +- .../src/avatar/avatar.stories.ts | 149 +++++++++--------- .../src/checkbox/checkbox.spec.ts | 2 +- .../src/checkbox/checkbox.stories.ts | 5 +- .../src/dialog-body/dialog-body.spec.ts | 2 +- .../src/dialog-body/dialog-body.stories.ts | 9 +- .../src/dialog-body/dialog-body.template.ts | 1 + .../src/dialog-body/dialog-body.ts | 9 +- 8 files changed, 99 insertions(+), 82 deletions(-) diff --git a/packages/web-components/src/avatar/avatar.spec.ts b/packages/web-components/src/avatar/avatar.spec.ts index 35f1b3b4a6059d..cd7640a2547c51 100644 --- a/packages/web-components/src/avatar/avatar.spec.ts +++ b/packages/web-components/src/avatar/avatar.spec.ts @@ -1,7 +1,7 @@ import { test } from '@playwright/test'; import { analyzePageWithAxe, createElementInternalsTrapsForAxe, expect, fixtureURL } from '../helpers.tests.js'; import type { Avatar } from './avatar.js'; -import { AvatarAppearance, AvatarColor, AvatarSize } from './avatar.options.js'; +import type { AvatarAppearance, AvatarColor, AvatarSize } from './avatar.options.js'; const storybookDocId = 'components-avatar--docs'; @@ -212,7 +212,7 @@ test.describe('Avatar Component', () => { }); }); -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/avatar/avatar.stories.ts b/packages/web-components/src/avatar/avatar.stories.ts index 20f842e677c4b8..229ad139e9b282 100644 --- a/packages/web-components/src/avatar/avatar.stories.ts +++ b/packages/web-components/src/avatar/avatar.stories.ts @@ -14,6 +14,7 @@ const storyTemplate = html>` size="${story => story.size}" initials="${story => story.initials}" name="${story => story.name}" + aria-label="avatar" > ${story => story.slottedContent?.()} ${story => story.badgeSlottedContent?.()} @@ -102,7 +103,7 @@ export default { export const Default: Story = {}; export const Image: Story = { - render: renderComponent(html>` + render: renderComponent(html>` Persona test>` - +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
`), }; @@ -184,35 +187,35 @@ export const Color: Story = { export const Colorful: Story = { render: renderComponent(html>`
- - - - - - - - - - - - + + + + + + + + + + + +
`), }; export const Shape: Story = { render: renderComponent(html>` - - + + `), }; export const Active: Story = { render: renderComponent(html>`
- U - A - I + U + A + I
`), @@ -221,34 +224,34 @@ export const Active: Story = { export const ActiveAppearance: Story = { render: renderComponent(html>`
- R - S - RS + R + S + RS
`), }; export const CustomInitials: Story = { - render: renderComponent(html>` `), + render: renderComponent(html>` `), }; export const Size: Story = { render: renderComponent(html>`
- 16 - 20 - 24 - 28 - 32 - 36 - 40 - 48 - 56 - 64 - 72 - 96 - 120 - 128 + 16 + 20 + 24 + 28 + 32 + 36 + 40 + 48 + 56 + 64 + 72 + 96 + 120 + 128
`), }; diff --git a/packages/web-components/src/checkbox/checkbox.spec.ts b/packages/web-components/src/checkbox/checkbox.spec.ts index 4c4ebae4e44ceb..a3723ca78da35b 100644 --- a/packages/web-components/src/checkbox/checkbox.spec.ts +++ b/packages/web-components/src/checkbox/checkbox.spec.ts @@ -483,7 +483,7 @@ test.describe('Checkbox', () => { }); }); -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/checkbox/checkbox.stories.ts b/packages/web-components/src/checkbox/checkbox.stories.ts index f17debf1415376..777d4ffb5d8d3b 100644 --- a/packages/web-components/src/checkbox/checkbox.stories.ts +++ b/packages/web-components/src/checkbox/checkbox.stories.ts @@ -18,6 +18,7 @@ const storyTemplate = html>` shape="${story => story.shape}" size="${story => story.size}" slot="${story => story.slot}" + aria-label="${story => story.label ? null : 'Checkbox example'}" > ${story => story.checkedIndicatorContent?.()} ${story => story.indeterminateIndicatorContent?.()} @@ -252,8 +253,8 @@ export const Required: Story = { render: renderComponent(html>`
- - + +
${fieldStoryTemplate} Submit diff --git a/packages/web-components/src/dialog-body/dialog-body.spec.ts b/packages/web-components/src/dialog-body/dialog-body.spec.ts index 6a18b9e0fdcaa9..16d07d7117f4d8 100644 --- a/packages/web-components/src/dialog-body/dialog-body.spec.ts +++ b/packages/web-components/src/dialog-body/dialog-body.spec.ts @@ -69,7 +69,7 @@ test.describe('Dialog Body', () => { }); }); -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/dialog-body/dialog-body.stories.ts b/packages/web-components/src/dialog-body/dialog-body.stories.ts index ef5d8a17cd8a70..2acdbdb13c23fd 100644 --- a/packages/web-components/src/dialog-body/dialog-body.stories.ts +++ b/packages/web-components/src/dialog-body/dialog-body.stories.ts @@ -35,7 +35,7 @@ const dismissCircle20Regular = html``; const storyTemplate = html>` - + ${x => x.titleSlottedContent?.()} ${x => x.titleActionSlottedContent?.()} ${x => x.slottedContent?.()} @@ -57,6 +57,10 @@ export default { type: { summary: 'boolean' }, }, }, + titleActionLabel: { + description: 'ARIA label for the default title action button.', + table: { category: 'attributes' } + }, slottedContent: { control: false, name: '', @@ -136,7 +140,7 @@ export const Actions: Story = {

`, titleActionSlottedContent: () => html` - ${dismissed20Regular} + ${dismissed20Regular} `, titleSlottedContent: () => html`
Actions
`, }, @@ -165,6 +169,7 @@ export const CustomTitleAction: Story = { appearance="transparent" icon-only @click="${() => alert('This is a custom action')}" + aria-label="Dismiss" > ${dismissCircle20Regular} diff --git a/packages/web-components/src/dialog-body/dialog-body.template.ts b/packages/web-components/src/dialog-body/dialog-body.template.ts index f8d636956d90f5..b6e158030eac57 100644 --- a/packages/web-components/src/dialog-body/dialog-body.template.ts +++ b/packages/web-components/src/dialog-body/dialog-body.template.ts @@ -31,6 +31,7 @@ export const template: ElementViewTemplate = html` class="title-action" appearance="transparent" icon-only + aria-label="${x => x.titleActionLabel}" @click=${x => x.parentNode?.hide()} ${ref('defaultTitleAction')} > diff --git a/packages/web-components/src/dialog-body/dialog-body.ts b/packages/web-components/src/dialog-body/dialog-body.ts index eeb70e03c5eae8..012818793db79d 100644 --- a/packages/web-components/src/dialog-body/dialog-body.ts +++ b/packages/web-components/src/dialog-body/dialog-body.ts @@ -7,9 +7,16 @@ import { attr, FASTElement } from '@microsoft/fast-element'; */ export class DialogBody extends FASTElement { /** - * @public * Indicates whether the dialog has a title action + * @public */ @attr({ mode: 'boolean', attribute: 'no-title-action' }) public noTitleAction: boolean = false; + + /** + * ARIA label for the default title action button. + * @public + */ + @attr({ attribute: 'title-action-label' }) + public titleActionLabel = ''; } From d910db2bf79cb501887dcf0f68ebe0fdefff8058 Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Fri, 4 Oct 2024 16:19:51 -0700 Subject: [PATCH 08/21] fix drawer accessibility issues --- packages/web-components/src/drawer/drawer.spec.ts | 2 +- packages/web-components/src/drawer/drawer.stories.ts | 1 + .../web-components/src/drawer/drawer.template.ts | 2 +- packages/web-components/src/drawer/drawer.ts | 12 +++++++++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/web-components/src/drawer/drawer.spec.ts b/packages/web-components/src/drawer/drawer.spec.ts index 84024dc6ad9c1f..8b83150c092825 100644 --- a/packages/web-components/src/drawer/drawer.spec.ts +++ b/packages/web-components/src/drawer/drawer.spec.ts @@ -208,7 +208,7 @@ test.describe('Drawer', () => { }); }); -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/drawer/drawer.stories.ts b/packages/web-components/src/drawer/drawer.stories.ts index 260e400e8907d8..596571edec0fa9 100644 --- a/packages/web-components/src/drawer/drawer.stories.ts +++ b/packages/web-components/src/drawer/drawer.stories.ts @@ -65,6 +65,7 @@ const storyTemplate = html>` position="${story => story.position}" size="${story => story.size}" type="${story => story.type}" + dialog-label="Drawer example" style="${story => story['--drawer-width'] !== '' ? `--drawer-width: ${story['--drawer-width']};` : ''} ${story => story['--dialog-backdrop'] !== '' ? `--dialog-backdrop: ${story['--dialog-backdrop']};` : ''}" diff --git a/packages/web-components/src/drawer/drawer.template.ts b/packages/web-components/src/drawer/drawer.template.ts index b223c8de96911a..54de12b3638c4c 100644 --- a/packages/web-components/src/drawer/drawer.template.ts +++ b/packages/web-components/src/drawer/drawer.template.ts @@ -14,7 +14,7 @@ export function drawerTemplate(): ElementViewTemplate { aria-modal="${x => (x.type === 'modal' ? 'true' : void 0)}" aria-describedby="${x => x.ariaDescribedby}" aria-labelledby="${x => x.ariaLabelledby}" - aria-label="${x => x.ariaLabel}" + aria-label="${x => x.dialogLabel}" size="${x => x.size}" position="${x => x.position}" type="${x => x.type}" diff --git a/packages/web-components/src/drawer/drawer.ts b/packages/web-components/src/drawer/drawer.ts index e6b0332ab9ce7a..604520876a220b 100644 --- a/packages/web-components/src/drawer/drawer.ts +++ b/packages/web-components/src/drawer/drawer.ts @@ -38,6 +38,9 @@ export class Drawer extends FASTElement { public type: DrawerType = DrawerType.modal; /** + * FIXME: this should be deprecated and removed, because it’s not valid to + * use `aria-labelledby` and `aria-label` on an element that doesn’t have a + * valid labellable role (the host element doesn’t have any role). * @public * The ID of the element that labels the drawer. */ @@ -51,7 +54,14 @@ export class Drawer extends FASTElement { @attr({ attribute: 'aria-describedby' }) public ariaDescribedby?: string; - /**"" + /** + * @public + * The accessible label of the dialog. + */ + @attr({ attribute: 'dialog-label' }) + public dialogLabel = ''; + + /** * @public * @defaultValue start * Sets the position of the drawer (start/end). From 002b0d5ded3c6326241a357e770ca4734030acbe Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Fri, 4 Oct 2024 16:22:06 -0700 Subject: [PATCH 09/21] temporarily disable menu and menulist axe tests --- packages/web-components/src/menu-list/menu-list.spec.ts | 4 +++- packages/web-components/src/menu/menu.spec.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/web-components/src/menu-list/menu-list.spec.ts b/packages/web-components/src/menu-list/menu-list.spec.ts index 463faceb01276c..4f7c58486edb6e 100644 --- a/packages/web-components/src/menu-list/menu-list.spec.ts +++ b/packages/web-components/src/menu-list/menu-list.spec.ts @@ -525,7 +525,9 @@ test.describe('MenuList', () => { }); }); -test('should not have auto detectable accessibility issues', async ({ page }) => { +// FIXME: For some reason, Axe complains that the menu item doesn’t have an +// accessible label while it clearly does: `Menu item` +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/menu/menu.spec.ts b/packages/web-components/src/menu/menu.spec.ts index 559f62016bcf96..427164d7fe8671 100644 --- a/packages/web-components/src/menu/menu.spec.ts +++ b/packages/web-components/src/menu/menu.spec.ts @@ -385,7 +385,9 @@ test.describe('Menu', () => { }); }); -test('should not have auto detectable accessibility issues', async ({ page }) => { +// FIXME: For some reason, Axe complains that the menu item doesn’t have an +// accessible label while it clearly does: `Menu item` +test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); From d2e17bf8dc2563472aef4f95fd50c8f567af6c03 Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Fri, 4 Oct 2024 16:36:25 -0700 Subject: [PATCH 10/21] fix exiting accessibility issues in components and stories batch 2 --- .../src/message-bar/message-bar.integration.spec.ts | 2 +- .../web-components/src/message-bar/message-bar.stories.ts | 2 +- .../web-components/src/progress-bar/progress-bar.spec.ts | 3 +-- .../web-components/src/progress-bar/progress-bar.stories.ts | 1 + packages/web-components/src/radio/radio.spec.ts | 3 +-- packages/web-components/src/radio/radio.stories.ts | 1 + packages/web-components/src/slider/slider.spec.ts | 3 +-- packages/web-components/src/slider/slider.stories.ts | 1 + packages/web-components/src/spinner/spinner.spec.ts | 3 +-- packages/web-components/src/spinner/spinner.stories.ts | 6 +++++- packages/web-components/src/switch/switch.spec.ts | 3 +-- packages/web-components/src/switch/switch.stories.ts | 5 +++-- 12 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/web-components/src/message-bar/message-bar.integration.spec.ts b/packages/web-components/src/message-bar/message-bar.integration.spec.ts index a5219d62e483fe..63057053224061 100644 --- a/packages/web-components/src/message-bar/message-bar.integration.spec.ts +++ b/packages/web-components/src/message-bar/message-bar.integration.spec.ts @@ -104,7 +104,7 @@ test.describe('Message Bar', () => { }); }); -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/message-bar/message-bar.stories.ts b/packages/web-components/src/message-bar/message-bar.stories.ts index b198bcfd661096..5f6e0751920ab8 100644 --- a/packages/web-components/src/message-bar/message-bar.stories.ts +++ b/packages/web-components/src/message-bar/message-bar.stories.ts @@ -72,7 +72,7 @@ export default { actionsSlottedContent: () => html` Action `, dismissSlottedContent: () => html` - + ${dismiss20Regular} `, diff --git a/packages/web-components/src/progress-bar/progress-bar.spec.ts b/packages/web-components/src/progress-bar/progress-bar.spec.ts index 124c202eb7aaa5..1f518516cc66a0 100644 --- a/packages/web-components/src/progress-bar/progress-bar.spec.ts +++ b/packages/web-components/src/progress-bar/progress-bar.spec.ts @@ -169,8 +169,7 @@ test.describe('Progress Bar', () => { }); }); -// FIXME: Story examples needs accessible names. -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/progress-bar/progress-bar.stories.ts b/packages/web-components/src/progress-bar/progress-bar.stories.ts index 818efb31bdb36f..fd4d5de677f2d8 100644 --- a/packages/web-components/src/progress-bar/progress-bar.stories.ts +++ b/packages/web-components/src/progress-bar/progress-bar.stories.ts @@ -13,6 +13,7 @@ const storyTemplate = html>` min="${story => story.min}" value="${story => story.value}" validation-state="${story => story.validationState}" + aria-label="Example progress" > `; diff --git a/packages/web-components/src/radio/radio.spec.ts b/packages/web-components/src/radio/radio.spec.ts index 5f424805174fca..be1a76e6bcfaae 100644 --- a/packages/web-components/src/radio/radio.spec.ts +++ b/packages/web-components/src/radio/radio.spec.ts @@ -352,8 +352,7 @@ test.describe('Radio', () => { }); }); -// FIXME: radio examples need accessible names -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/radio/radio.stories.ts b/packages/web-components/src/radio/radio.stories.ts index f71842259faaec..72c6ab81dde86b 100644 --- a/packages/web-components/src/radio/radio.stories.ts +++ b/packages/web-components/src/radio/radio.stories.ts @@ -12,6 +12,7 @@ const storyTemplate = html>` name="${story => story.name}" ?required="${story => story.required}" value="${story => story.value}" + aria-label="Example radio button" > `; diff --git a/packages/web-components/src/slider/slider.spec.ts b/packages/web-components/src/slider/slider.spec.ts index 1656326e6c3add..1affb53ef40851 100644 --- a/packages/web-components/src/slider/slider.spec.ts +++ b/packages/web-components/src/slider/slider.spec.ts @@ -777,8 +777,7 @@ test.describe('Slider', () => { }); }); -// FIXME: slider examples need accesible names. -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/slider/slider.stories.ts b/packages/web-components/src/slider/slider.stories.ts index bd807a032ae0de..22c0f9ea57782d 100644 --- a/packages/web-components/src/slider/slider.stories.ts +++ b/packages/web-components/src/slider/slider.stories.ts @@ -16,6 +16,7 @@ const storyTemplate = html>` orientation="${story => story.orientation}" value="${story => story.value}" slot="${story => story.slot}" + aria-label="Example slider" > `; diff --git a/packages/web-components/src/spinner/spinner.spec.ts b/packages/web-components/src/spinner/spinner.spec.ts index 281c332723a6ef..fc5cd19afcb178 100644 --- a/packages/web-components/src/spinner/spinner.spec.ts +++ b/packages/web-components/src/spinner/spinner.spec.ts @@ -59,8 +59,7 @@ test.describe('Spinner', () => { } }); -// FIXME: examples need accessible names -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/spinner/spinner.stories.ts b/packages/web-components/src/spinner/spinner.stories.ts index 522c4bcb2842e4..5f118a572310ef 100644 --- a/packages/web-components/src/spinner/spinner.stories.ts +++ b/packages/web-components/src/spinner/spinner.stories.ts @@ -6,7 +6,11 @@ import { Spinner as FluentSpinner } from './spinner.js'; type Story = StoryObj; const storyTemplate = html>` - + `; export default { diff --git a/packages/web-components/src/switch/switch.spec.ts b/packages/web-components/src/switch/switch.spec.ts index 1e6b36ea400fc6..33e5d7063aa286 100644 --- a/packages/web-components/src/switch/switch.spec.ts +++ b/packages/web-components/src/switch/switch.spec.ts @@ -292,8 +292,7 @@ test.describe('Switch', () => { }); }); -// FIXME: examples need accessible names. -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/switch/switch.stories.ts b/packages/web-components/src/switch/switch.stories.ts index 97a8ee1e477ce7..5b8767928204a0 100644 --- a/packages/web-components/src/switch/switch.stories.ts +++ b/packages/web-components/src/switch/switch.stories.ts @@ -13,6 +13,7 @@ const storyTemplate = html>` name="${story => story.name}" ?required="${story => story.required}" slot="${story => story.slot}" + aria-label="Example switch" > `; @@ -93,8 +94,8 @@ export const Required: Story = { render: renderComponent(html>`
- - + +
${fieldStoryTemplate}
Submit From dd714d5e63b8890e9f85b156c8526b10f559587e Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Mon, 7 Oct 2024 13:08:33 -0700 Subject: [PATCH 11/21] fix accessibility issue in tablist --- packages/web-components/src/tab/tab.ts | 10 +++++++ .../src/tablist/tablist.spec.ts | 4 +-- .../src/tablist/tablist.stories.ts | 26 ++++++++++++++----- .../web-components/src/tablist/tablist.ts | 1 + 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/web-components/src/tab/tab.ts b/packages/web-components/src/tab/tab.ts index d059e0dd88562f..738d5e24e2925a 100644 --- a/packages/web-components/src/tab/tab.ts +++ b/packages/web-components/src/tab/tab.ts @@ -12,6 +12,13 @@ export type TabOptions = StartEndOptions; * Tab extends the FASTTab and is a child of the TabList */ export class Tab extends FASTElement { + /** + * The internal {@link https://developer.mozilla.org/docs/Web/API/ElementInternals | `ElementInternals`} instance for the component. + * + * @internal + */ + public elementInternals: ElementInternals = this.attachInternals(); + /** * When true, the control will be immutable by user interaction. See {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled | disabled HTML attribute} for more information. * @public @@ -20,6 +27,9 @@ export class Tab extends FASTElement { */ @attr({ mode: 'boolean' }) public disabled!: boolean; + protected disabledChanged() { + this.elementInternals.ariaDisabled = this.disabled.toString(); + } private styles: ElementStyles | undefined; diff --git a/packages/web-components/src/tablist/tablist.spec.ts b/packages/web-components/src/tablist/tablist.spec.ts index 3bcd043fd60522..f0c7eba6eec303 100644 --- a/packages/web-components/src/tablist/tablist.spec.ts +++ b/packages/web-components/src/tablist/tablist.spec.ts @@ -404,9 +404,7 @@ test.describe('Tablist', () => { }); }); -// FIXME: When tablist is disabled, either itself or all of its tab children may need -// `aria-disabled=true`. -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/tablist/tablist.stories.ts b/packages/web-components/src/tablist/tablist.stories.ts index 4c19127f834db9..73f0da69e43da9 100644 --- a/packages/web-components/src/tablist/tablist.stories.ts +++ b/packages/web-components/src/tablist/tablist.stories.ts @@ -1,4 +1,5 @@ import { html, ref } from '@microsoft/fast-element'; +import { uniqueId } from '@microsoft/fast-web-utilities'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; import type { Tablist as FluentTablist } from './tablist.js'; import { TablistAppearance as TablistAppearanceValues, TablistOrientation, TablistSize } from './tablist.options.js'; @@ -18,12 +19,12 @@ const storyTemplate = html>` }}" ${ref('tablist')} > - First Tab - Second Tab - Third Tab - Fourth Tab + First Tab + Second Tab + Third Tab + Fourth Tab -
+
`; @@ -73,14 +74,20 @@ export default { type: { summary: Object.values(TablistSize).join('|') }, }, }, + panelId: { table: { disalbe: true } }, }, } as Meta; -export const Default: Story = {}; +export const Default: Story = { + args: { + panelId: uniqueId('tabpanel-'), + }, +}; export const VerticalOrientation: Story = { args: { orientation: TablistOrientation.vertical, + panelId: uniqueId('tabpanel-'), }, decorators: [ Story => { @@ -94,30 +101,35 @@ export const VerticalOrientation: Story = { export const Disabled: Story = { args: { disabled: true, + panelId: uniqueId('tabpanel-'), }, }; export const ActiveId: Story = { args: { activeid: 'third-tab', + panelId: uniqueId('tabpanel-'), }, }; export const SubtleAppearance: Story = { args: { appearance: 'subtle', + panelId: uniqueId('tabpanel-'), }, }; export const SmallSize: Story = { args: { size: 'small', + panelId: uniqueId('tabpanel-'), }, }; export const LargeSize: Story = { args: { size: 'small', + panelId: uniqueId('tabpanel-'), }, }; @@ -125,6 +137,7 @@ export const SmallSizeVerticalOrientation: Story = { args: { orientation: TablistOrientation.vertical, size: 'small', + panelId: uniqueId('tabpanel-'), }, decorators: [ Story => { @@ -139,6 +152,7 @@ export const LargeSizeVerticalOrientation: Story = { args: { orientation: TablistOrientation.vertical, size: 'large', + panelId: uniqueId('tabpanel-'), }, decorators: [ Story => { diff --git a/packages/web-components/src/tablist/tablist.ts b/packages/web-components/src/tablist/tablist.ts index 6c870412d61d9b..1215f3d8e00ed0 100644 --- a/packages/web-components/src/tablist/tablist.ts +++ b/packages/web-components/src/tablist/tablist.ts @@ -46,6 +46,7 @@ export class BaseTablist extends FASTElement { */ protected disabledChanged(prev: boolean, next: boolean): void { toggleState(this.elementInternals, 'disabled', next); + this.elementInternals.ariaDisabled = next.toString(); } /** From 45ff924806c07066266c7ba52f7ebaba25beed90 Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Mon, 7 Oct 2024 13:19:54 -0700 Subject: [PATCH 12/21] fix accessibility issue in textarea stories --- .../web-components/src/textarea/textarea.spec.ts | 3 +-- .../src/textarea/textarea.stories.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/web-components/src/textarea/textarea.spec.ts b/packages/web-components/src/textarea/textarea.spec.ts index 63a3c9d61cdfdf..0cf07cbdb84085 100644 --- a/packages/web-components/src/textarea/textarea.spec.ts +++ b/packages/web-components/src/textarea/textarea.spec.ts @@ -1030,8 +1030,7 @@ test.describe('TextArea', () => { }); }); -// FIXME: examples need accessible names. -test.fixme('should not have auto detectable accessibility issues', async ({ page }) => { +test('should not have auto detectable accessibility issues', async ({ page }) => { await createElementInternalsTrapsForAxe(page); await page.goto(fixtureURL(storybookDocId)); diff --git a/packages/web-components/src/textarea/textarea.stories.ts b/packages/web-components/src/textarea/textarea.stories.ts index a18ac24e643835..8c11f8e94ad818 100644 --- a/packages/web-components/src/textarea/textarea.stories.ts +++ b/packages/web-components/src/textarea/textarea.stories.ts @@ -187,7 +187,12 @@ export default { }, } as Meta; -export const Default: Story = {}; +export const Default: Story = { + args: { + labelSlottedContent: () => + html`Sample textarea`, + }, +}; export const TextArea: Story = { args: { @@ -199,6 +204,8 @@ export const TextArea: Story = { export const Placeholder: Story = { args: { placeholder: 'This is a placeholder', + labelSlottedContent: () => + html`Sample textarea`, }, }; @@ -276,6 +283,8 @@ export const Size: Story = { export const AutoResize: Story = { args: { autoResize: true, + labelSlottedContent: () => + html`Sample textarea`, }, }; @@ -338,6 +347,8 @@ export const Disabled: Story = { disabled: true, resize: TextAreaResize.both, slottedContent: () => 'This textarea is disabled', + labelSlottedContent: () => + html`Sample textarea`, }, }; @@ -357,12 +368,15 @@ export const ReadOnly: Story = { readOnly: true, resize: TextAreaResize.both, slottedContent: () => 'Some content', + labelSlottedContent: () => + html`Sample textarea`, }, }; export const WithHTMLCode: Story = { render: renderComponent(html>` + Sample textarea

This text should show up as plain text.