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 0000000000000..57a0d2853b636 --- /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" +} diff --git a/package.json b/package.json index 18cdbba8782a4..9f3adcd2be889 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "devDependencies": { "@actions/core": "1.9.1", "@actions/github": "5.0.3", + "@axe-core/playwright": "^4.10.0", "@babel/core": "7.24.6", "@babel/generator": "7.24.6", "@babel/parser": "7.24.6", diff --git a/packages/web-components/src/accordion/accordion.spec.ts b/packages/web-components/src/accordion/accordion.spec.ts index 26e9733fbedc4..a46a939257dec 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 0c02e9a52d3cf..5760d4d06e619 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 18e38a1bb38e4..3473dda59c6c0 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'; +import type { 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')); }); @@ -81,6 +83,10 @@ test.describe('Avatar Component', () => { test('should have a role of img', async ({ page }) => { const element = page.locator('fluent-avatar'); + await page.setContent(/* html */ ` + + `); + await expect(element).toHaveJSProperty('elementInternals.role', 'img'); }); @@ -119,6 +125,10 @@ test.describe('Avatar Component', () => { test('should render correctly in active state', async ({ page }) => { const element = page.locator('fluent-avatar'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Avatar) => { node.active = 'active'; }); @@ -129,6 +139,10 @@ test.describe('Avatar Component', () => { test('should render correctly in inactive state', async ({ page }) => { const element = page.locator('fluent-avatar'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Avatar) => { node.active = 'inactive'; }); @@ -139,6 +153,10 @@ test.describe('Avatar Component', () => { test('default color should be neutral', async ({ page }) => { const element = page.locator('fluent-avatar'); + await page.setContent(/* html */ ` + + `); + await expect(element).toHaveCustomState('neutral'); }); @@ -165,6 +183,10 @@ test.describe('Avatar Component', () => { test(`should set the color attribute on the internal control`, async ({ page }) => { const element = page.locator('fluent-avatar'); + await page.setContent(/* html */ ` + + `); + for (const [, value] of Object.entries(colorAttributes)) { await test.step(value, async () => { await element.evaluate((node: Avatar, colorValue: string) => { @@ -181,6 +203,10 @@ test.describe('Avatar Component', () => { test(`should set the size attribute on the internal control`, async ({ page }) => { const element = page.locator('fluent-avatar'); + await page.setContent(/* html */ ` + + `); + for (const [, value] of Object.entries(sizeAttributes)) { await test.step(`${value}`, async () => { await element.evaluate((node: Avatar, sizeValue: number) => { @@ -197,6 +223,10 @@ test.describe('Avatar Component', () => { test(`should set and reflect the appearance attribute on the internal control`, async ({ page }) => { const element = page.locator('fluent-avatar'); + await page.setContent(/* html */ ` + + `); + for (const [, value] of Object.entries(appearanceAttributes)) { await test.step(value, async () => { await element.evaluate((node: Avatar, appearanceValue: string) => { @@ -209,3 +239,14 @@ test.describe('Avatar 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-avatar')); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/avatar/avatar.stories.ts b/packages/web-components/src/avatar/avatar.stories.ts index 20f842e677c4b..09c97fb887535 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 +189,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 +226,36 @@ 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/badge/badge.spec.ts b/packages/web-components/src/badge/badge.spec.ts index 9482c86f25bda..bb8d7d960861d 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 7348cb470abc5..9761c40b82dab 100644 --- a/packages/web-components/src/button/button.spec.ts +++ b/packages/web-components/src/button/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-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 +651,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 analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/button/button.ts b/packages/web-components/src/button/button.ts index bff33807e42ad..a3c4e59981533 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/checkbox/checkbox.spec.ts b/packages/web-components/src/checkbox/checkbox.spec.ts index 1d945a1f09bd4..4cdd23c69dad7 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')); }); @@ -12,6 +14,10 @@ test.describe('Checkbox', () => { test('should set and retrieve the `shape` property correctly', async ({ page }) => { const element = page.locator('fluent-checkbox'); + await page.setContent(/* html */ ` + + `); + await expect(element).toHaveCount(1); await test.step('should set the `shape` property to `circular`', async () => { @@ -70,6 +76,10 @@ test.describe('Checkbox', () => { test('should set and retrieve the `size` property correctly', async ({ page }) => { const element = page.locator('fluent-checkbox'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Checkbox) => { node.size = 'medium'; }); @@ -480,3 +490,14 @@ test.describe('Checkbox', () => { expect(page.url()).toContain('?checkbox=foo&checkbox=bar'); }); }); + +test('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/checkbox/checkbox.stories.ts b/packages/web-components/src/checkbox/checkbox.stories.ts index f17debf141537..a525ddc0e11b2 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,12 @@ export const Required: Story = { render: renderComponent(html>`
- - + +
${fieldStoryTemplate} Submit 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 dada9b854fd54..4cce6a80d6219 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 c791aa6dce0e6..16d07d7117f4d 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('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-body/dialog-body.stories.ts b/packages/web-components/src/dialog-body/dialog-body.stories.ts index ef5d8a17cd8a7..80ea24515ade8 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,9 @@ export const Actions: Story = {

`, titleActionSlottedContent: () => html` - ${dismissed20Regular} + + ${dismissed20Regular} + `, titleSlottedContent: () => html`
Actions
`, }, @@ -165,6 +171,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 f8d636956d90f..b6e158030eac5 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 eeb70e03c5eae..012818793db79 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 = ''; } diff --git a/packages/web-components/src/dialog/dialog.spec.ts b/packages/web-components/src/dialog/dialog.spec.ts index 15d2ec0ae824d..949f9a3bc914c 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 b4e4d237f57e1..15d9dda5e84b5 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')); }); @@ -73,6 +75,10 @@ test.describe('Divider', () => { test('should add a custom state matching the `orientation` attribute when provided', async ({ page }) => { const element = page.locator('fluent-divider'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Divider) => { node.orientation = 'vertical'; }); @@ -146,6 +152,10 @@ test.describe('Divider', () => { test('should add a custom state matching the `appearance` attribute when provided', async ({ page }) => { const element = page.locator('fluent-divider'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Divider) => { node.appearance = 'strong'; }); @@ -172,6 +182,10 @@ test.describe('Divider', () => { test('should add a custom state of `inset` when the value is true', async ({ page }) => { const element = page.locator('fluent-divider'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Divider) => { node.inset = true; }); @@ -188,6 +202,10 @@ test.describe('Divider', () => { test('should add a custom state matching the `align-content` attribute value when provided', async ({ page }) => { const element = page.locator('fluent-divider'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Divider) => { node.alignContent = 'start'; }); @@ -211,3 +229,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 2d167763e7594..8b83150c09282 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('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/drawer/drawer.stories.ts b/packages/web-components/src/drawer/drawer.stories.ts index 260e400e8907d..596571edec0fa 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 b223c8de96911..54de12b3638c4 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 e6b0332ab9ce7..604520876a220 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). diff --git a/packages/web-components/src/field/field.spec.ts b/packages/web-components/src/field/field.spec.ts index d661608b8a2e4..4776bade169ad 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 46d72c8403a92..b96913328d96f 100644 --- a/packages/web-components/src/helpers.tests.ts +++ b/packages/web-components/src/helpers.tests.ts @@ -1,5 +1,6 @@ import qs from 'qs'; -import { expect as baseExpect, type ExpectMatcherState, type Locator } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; +import { expect as baseExpect, type ExpectMatcherState, type Locator, type Page } from '@playwright/test'; /** * Returns a formatted URL for a given Storybook fixture. @@ -84,3 +85,81 @@ 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(..)`. 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(() => { + 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) { + 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); + const host = originalInternals.shadowRoot?.host; + if (value !== null && value !== undefined) { + host?.setAttribute(attrName, value.toString()); + } else { + host?.removeAttribute(attrName); + } + } + + return Reflect.set(originalInternals, prop, value); + }, + }); + }; + }); +} + +interface AnalyzePageWithAxeOptions { + exclude: string[]; +} +/** + * 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, + options?: AnalyzePageWithAxeOptions, +): Promise> { + let builder = new AxeBuilder({ page }).include('.sb-story'); + if (options?.exclude?.length) { + for (const exclude of options.exclude) { + builder = builder.exclude(exclude); + console.log(builder); + } + } + return await builder.analyze(); +} diff --git a/packages/web-components/src/image/image.spec.ts b/packages/web-components/src/image/image.spec.ts index 5ff754a011e07..d3bf50d3e7904 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')); }); @@ -30,6 +32,12 @@ test.describe('Image', () => { test('should add a custom state of `block` when a value of true is provided', async ({ page }) => { const element = page.locator('fluent-image'); + await page.setContent(/* html */ ` + + Short image description + + `); + await element.evaluate((node: Image) => { node.block = true; }); @@ -64,6 +72,12 @@ test.describe('Image', () => { test('should add a custom state of `bordered` when a value of true is provided', async ({ page }) => { const element = page.locator('fluent-image'); + await page.setContent(/* html */ ` + + Short image description + + `); + await element.evaluate((node: Image) => { node.bordered = true; }); @@ -98,6 +112,12 @@ test.describe('Image', () => { test('should add a custom state of `shadow` when a value of true is provided', async ({ page }) => { const element = page.locator('fluent-image'); + await page.setContent(/* html */ ` + + Short image description + + `); + await element.evaluate((node: Image) => { node.shadow = true; }); @@ -150,6 +170,12 @@ test.describe('Image', () => { test('should add a custom state matching the `fit` attribute when provided', async ({ page }) => { const element = page.locator('fluent-image'); + await page.setContent(/* html */ ` + + Short image description + + `); + await element.evaluate((node: Image) => { node.fit = 'contain'; }); @@ -200,6 +226,12 @@ test.describe('Image', () => { test('should add a custom state matching the `shape` attribute when provided', async ({ page }) => { const element = page.locator('fluent-image'); + await page.setContent(/* html */ ` + + Short image description + + `); + await element.evaluate((node: Image) => { node.shape = 'circular'; }); @@ -233,3 +265,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 f71399fe13723..86ebe50223906 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')); }); @@ -60,6 +62,10 @@ test.describe('Link', () => { test('should add a custom state matching the `appearance` attribute when provided', async ({ page }) => { const element = page.locator('fluent-link'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Link) => { node.appearance = 'subtle'; }); @@ -76,6 +82,10 @@ test.describe('Link', () => { test('should add a custom state of `inline` when true', async ({ page }) => { const element = page.locator('fluent-link'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Link) => { node.inline = true; }); @@ -89,3 +99,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 c099c3138bce6..f4e88e0e60216 100644 --- a/packages/web-components/src/menu-list/menu-list.spec.ts +++ b/packages/web-components/src/menu-list/menu-list.spec.ts @@ -1,14 +1,18 @@ import { once } from 'events'; 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'; import { MenuItem } from '../menu-item/menu-item.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')); + await page.waitForFunction(() => + Promise.all([customElements.whenDefined('fluent-menu-list'), customElements.whenDefined('fluent-menu-item')]), + ); }); test('should have a role of `menu`', async ({ page }) => { @@ -589,3 +593,16 @@ test.describe('Menu', () => { }); }); }); + +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-list'), customElements.whenDefined('fluent-menu-item')]), + ); + + 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 6fe3a3b4fc5a8..559f62016bcf9 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([ @@ -382,3 +384,30 @@ test.describe('Menu', () => { await expect(menuButton).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 32e82043e951f..e842bc37f89d1 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,11 +1,16 @@ 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')); + await page.setContent(/* html */ ` + + `); }); test('should include a role of status', async ({ page }) => { @@ -101,3 +106,14 @@ test.describe('Message Bar', () => { await expect(element).toHaveAttribute('dismissed', 'true'); }); }); + +test('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/message-bar/message-bar.stories.ts b/packages/web-components/src/message-bar/message-bar.stories.ts index b198bcfd66109..5f6e0751920ab 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 a39baa7ef63d8..f947683421d49 100644 --- a/packages/web-components/src/progress-bar/progress-bar.spec.ts +++ b/packages/web-components/src/progress-bar/progress-bar.spec.ts @@ -1,12 +1,18 @@ 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')); + + await page.setContent(/* html */ ` + + `); }); test('should include a role of progressbar', async ({ page }) => { @@ -166,3 +172,14 @@ test.describe('Progress Bar', () => { await expect(element).not.toHaveCustomState('error'); }); }); + +test('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/progress-bar/progress-bar.stories.ts b/packages/web-components/src/progress-bar/progress-bar.stories.ts index 818efb31bdb36..fd4d5de677f2d 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-group/radio-group.spec.ts b/packages/web-components/src/radio-group/radio-group.spec.ts index efda17debdf0e..66d14cf86bee8 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 6e8cf6ae07491..be1a76e6bcfaa 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,14 @@ test.describe('Radio', () => { expect(page.url()).not.toContain('?radio=foo'); }); }); + +test('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/radio/radio.stories.ts b/packages/web-components/src/radio/radio.stories.ts index f71842259faae..72c6ab81dde86 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/rating-display/rating-display.spec.ts b/packages/web-components/src/rating-display/rating-display.spec.ts index 4215afb18bf95..cd4ae86ee3515 100644 --- a/packages/web-components/src/rating-display/rating-display.spec.ts +++ b/packages/web-components/src/rating-display/rating-display.spec.ts @@ -1,15 +1,21 @@ -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')); + await page.setContent(/* html */ ` + + `); + element = page.locator('fluent-rating-display'); }); @@ -204,3 +210,14 @@ test.describe('Rating Display', () => { await expect(icon.locator('> use')).toBeHidden(); }); }); + +test('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/rating-display/rating-display.stories.ts b/packages/web-components/src/rating-display/rating-display.stories.ts index 5ac7976f77d8f..507fd98b67adf 100644 --- a/packages/web-components/src/rating-display/rating-display.stories.ts +++ b/packages/web-components/src/rating-display/rating-display.stories.ts @@ -13,6 +13,7 @@ const storyTemplate = html>` max=${story => story.max} size=${story => story.size} value=${story => story.value} + aria-label="${story => `Rating: ${story.value}`}" >${story => story.iconSlottedContent?.()} `; diff --git a/packages/web-components/src/slider/slider.spec.ts b/packages/web-components/src/slider/slider.spec.ts index ac63388a34df0..7cb6313b8c2e9 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')); }); @@ -220,6 +222,10 @@ test.describe('Slider', () => { test('should set and retrieve the `size` property correctly', async ({ page }) => { const element = page.locator('fluent-slider'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Slider) => { node.size = 'small'; }); @@ -774,3 +780,14 @@ test.describe('Slider', () => { }); }); }); + +test('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/slider/slider.stories.ts b/packages/web-components/src/slider/slider.stories.ts index bd807a032ae0d..22c0f9ea57782 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 a509677369d4c..d3b79e98ed144 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')); }); @@ -14,6 +16,10 @@ test.describe('Spinner', () => { test(`should set and retrieve the \`appearance\` property correctly to ${thisAppearance}`, async ({ page }) => { const element = page.locator('fluent-spinner'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Spinner, appearance) => { node.appearance = appearance; }, thisAppearance as SpinnerAppearance); @@ -37,6 +43,10 @@ test.describe('Spinner', () => { test(`should set and retrieve the \`size\` property correctly to ${thisSize}`, async ({ page }) => { const element = page.locator('fluent-spinner'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: Spinner, size) => { node.size = size; }, thisSize as SpinnerSize); @@ -56,3 +66,14 @@ test.describe('Spinner', () => { }); } }); + +test('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/spinner/spinner.stories.ts b/packages/web-components/src/spinner/spinner.stories.ts index 522c4bcb2842e..5f118a572310e 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 3b9617428ea86..33e5d7063aa28 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,14 @@ test.describe('Switch', () => { expect(page.url()).toContain('?switch=foo&switch=bar'); }); }); + +test('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/switch/switch.stories.ts b/packages/web-components/src/switch/switch.stories.ts index 97a8ee1e477ce..be0137f484e1e 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,12 @@ export const Required: Story = { render: renderComponent(html>`
- - + +
${fieldStoryTemplate}
Submit diff --git a/packages/web-components/src/tab/tab.ts b/packages/web-components/src/tab/tab.ts index d059e0dd88562..738d5e24e2925 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 c5acaddae169a..6c648ce45edf0 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')]), @@ -17,7 +19,7 @@ test.describe('Tablist', () => { const element = page.locator('fluent-tablist'); const tabs = element.locator('fluent-tab'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -37,7 +39,7 @@ test.describe('Tablist', () => { const tabs = element.locator('fluent-tab'); const tab = tabs.nth(2); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -55,7 +57,7 @@ test.describe('Tablist', () => { test('should have reflect disabled attribute on control', async ({ page }) => { const element = page.locator('fluent-tablist'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -75,7 +77,7 @@ test.describe('Tablist', () => { test('should have role of `tablist`', async ({ page }) => { const element = page.locator('fluent-tablist'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -91,7 +93,7 @@ test.describe('Tablist', () => { }) => { const element = page.locator('fluent-tablist'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -106,7 +108,7 @@ test.describe('Tablist', () => { const element = page.locator('fluent-tablist'); const tabs = element.locator('fluent-tab'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -134,7 +136,7 @@ test.describe('Tablist', () => { const element = page.locator('fluent-tablist'); const tabs = element.locator('fluent-tab'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -162,7 +164,7 @@ test.describe('Tablist', () => { const element = page.locator('fluent-tablist'); const tabs = element.locator('fluent-tab'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -181,7 +183,7 @@ test.describe('Tablist', () => { const element = page.locator('fluent-tablist'); const tabs = element.locator('fluent-tab'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -204,7 +206,7 @@ test.describe('Tablist', () => { const element = page.locator('fluent-tablist'); const tabs = element.locator('fluent-tab'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -230,7 +232,7 @@ test.describe('Tablist', () => { test(`should set appearance to \`${appearance}\``, async ({ page }) => { const element = page.locator('fluent-tablist'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -248,7 +250,7 @@ test.describe('Tablist', () => { test(`should set size to \`${size}\``, async ({ page }) => { const element = page.locator('fluent-tablist'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -349,7 +351,7 @@ test.describe('Tablist', () => { const element = page.locator('fluent-tablist'); const tabs = element.locator('fluent-tab'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one @@ -377,7 +379,7 @@ test.describe('Tablist', () => { const element = page.locator('fluent-tablist'); const tabs = element.locator('fluent-tab'); - page.setContent(/* html */ ` + await page.setContent(/* html */ ` Tab one Tab two @@ -401,3 +403,16 @@ test.describe('Tablist', () => { await expect(element).toHaveJSProperty('activeid', secondTabId); }); }); + +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-tablist'), customElements.whenDefined('fluent-tab')]), + ); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/tablist/tablist.stories.ts b/packages/web-components/src/tablist/tablist.stories.ts index 4c19127f834db..73f0da69e43da 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 6c870412d61d9..1215f3d8e00ed 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(); } /** 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 f16fa96736308..8cd5879100138 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')); }); @@ -199,6 +201,10 @@ test.describe('TextInput', () => { test('should reflect `appearance` attribute values', async ({ page }) => { const element = page.locator('fluent-text-input'); + await page.setContent(/* html */ ` + + `); + await element.evaluate((node: TextInput) => { node.appearance = 'outline'; }); @@ -834,3 +840,16 @@ test.describe('TextInput', () => { }); }); }); + +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-input')); + + const results = await analyzePageWithAxe(page, { + exclude: ['#story--components-textinput--without-label-inner'], + }); + + 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 52a1dc3c313cf..0b150cbf4b318 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')); }); @@ -74,9 +76,9 @@ test.describe('Text Component', () => { test(`should set and reflect the align attribute to \`${value}\` when provided`, async ({ page }) => { const element = page.locator('fluent-text'); - await element.evaluate((node: Text, alignValue: string) => { - node.align = alignValue as TextAlign; - }, value as string); + await page.setContent(/* html */ ` + Text + `); await expect(element).toHaveJSProperty('align', value); @@ -90,9 +92,9 @@ test.describe('Text Component', () => { test(`should set and reflect the font attribute to \`${value}\` when provided`, async ({ page }) => { const element = page.locator('fluent-text'); - await element.evaluate((node: Text, fontValue: string) => { - node.font = fontValue as TextFont; - }, value as string); + await page.setContent(/* html */ ` + Text + `); await expect(element).toHaveJSProperty('font', value); @@ -102,3 +104,14 @@ 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 6828d283b9cb2..0cf07cbdb8408 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,16 @@ test.describe('TextArea', () => { }); }); }); + +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-textarea'), customElements.whenDefined('fluent-label')]), + ); + + const results = await analyzePageWithAxe(page); + + expect(results.violations).toEqual([]); +}); diff --git a/packages/web-components/src/textarea/textarea.stories.ts b/packages/web-components/src/textarea/textarea.stories.ts index a18ac24e64383..8c11f8e94ad81 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.