From 3e810f3cb9f38572ad31cee954eb0f938615da39 Mon Sep 17 00:00:00 2001 From: fbasso Date: Mon, 5 Feb 2024 16:12:51 +0100 Subject: [PATCH] test: add tests and fix accessibility issues for the select --- .../src/components/select/select.component.ts | 5 ++++- core/src/components/select/select.spec.ts | 13 ++++++++---- core/src/components/select/select.ts | 4 ++++ core/src/utils/internal/dom.ts | 8 +++++++ .../components/select/examples/+page.svelte | 7 +++++++ e2e/demo.spec.ts | 21 +++++++++++++++++++ .../select-custom.html | 2 ++ .../select-default.html | 5 ++++- .../select-playground.html | 2 ++ .../select-select.html | 2 ++ e2e/select/select.e2e-spec.ts | 8 +++---- react/lib/src/components/select/select.tsx | 20 +++++++++++++++--- .../lib/src/components/select/Select.svelte | 10 ++++++--- 13 files changed, 91 insertions(+), 16 deletions(-) diff --git a/angular/lib/src/components/select/select.component.ts b/angular/lib/src/components/select/select.component.ts index bd332f5761..04d5c2fc92 100644 --- a/angular/lib/src/components/select/select.component.ts +++ b/angular/lib/src/components/select/select.component.ts @@ -44,6 +44,7 @@ export class SelectItemDirective { [auUse]="controlContainerDirective" role="combobox" class="d-flex align-items-center flex-wrap" + [attr.aria-controls]="state.id + '-menu'" aria-haspopup="listbox" [attr.aria-expanded]="state.open" > @@ -75,6 +76,7 @@ export class SelectItemDirective { @if (state.open && state.visibleItems.length) {
    { > @for (itemContext of state.visibleItems; track itemCtxTrackBy($index, itemContext)) {
  • diff --git a/core/src/components/select/select.spec.ts b/core/src/components/select/select.spec.ts index ddfeb97e2a..e086cf7b92 100644 --- a/core/src/components/select/select.spec.ts +++ b/core/src/components/select/select.spec.ts @@ -9,7 +9,8 @@ import {createSelect, getSelectDefaultConfig} from './select'; type ExtractReadable = T extends ReadableSignal ? U : never; type ExtractState = T extends SelectWidget ? ExtractReadable['state$']> : never; -const normalizeState = createTraversal((_key, value) => { +const generatedIdRegExp = /^auId-/; +const normalizeState = createTraversal((path, value) => { const constructor = value?.constructor; switch (constructor) { case RegExp: @@ -21,6 +22,10 @@ const normalizeState = createTraversal((_key, value) => { return '(function)'; } + if (path === 'id') { + return generatedIdRegExp.test(value) ? '(generated)' : value; + } + return value; }); @@ -105,7 +110,7 @@ describe(`Select model`, () => { disabled: false, filterText: '', highlighted: undefined, - id: undefined, + id: '(generated)', loading: false, menuClassName: '', menuItemClassName: '', @@ -130,7 +135,7 @@ describe(`Select model`, () => { disabled: false, filterText: '', highlighted: {item: 'aa', id: 'aa', selected: false}, - id: undefined, + id: '(generated)', loading: false, menuClassName: '', menuItemClassName: '', @@ -723,7 +728,7 @@ describe(`Select model`, () => { selectWidget.patch({items: [item1, item2]}); open(); const expectedState: ReturnType = { - id: undefined, + id: '(generated)', ariaLabel: 'Select', visibleItems: [{item: item2, id: '1', selected: false}], highlighted: {item: item2, id: '1', selected: false}, diff --git a/core/src/components/select/select.ts b/core/src/components/select/select.ts index 4e0cb1530a..2bdd305d3a 100644 --- a/core/src/components/select/select.ts +++ b/core/src/components/select/select.ts @@ -9,6 +9,7 @@ import type {NavManagerItemConfig} from '../../services/navManager'; import {createNavManager} from '../../services/navManager'; import type {Directive, PropsConfig, SlotContent, Widget, WidgetSlotContext} from '../../types'; import {bindDirective} from '../../utils/directive'; +import {getGeneratedId} from '../../utils/internal/dom'; import {noop} from '../../utils/internal/func'; import {bindableDerived, bindableProp, stateStores, writablesForProps} from '../../utils/stores'; import type {WidgetsCommonPropsAndState} from '../commonProps'; @@ -343,6 +344,7 @@ export function createSelect(config?: PropsConfig>): Sel // Props const [ { + id$: _dirtyId$, open$: _dirtyOpen$, filterText$: _dirtyFilterText$, items$, @@ -358,6 +360,7 @@ export function createSelect(config?: PropsConfig>): Sel ] = writablesForProps>(defaultConfig, config); const {selected$} = stateProps; + const id$ = computed(() => _dirtyId$() ?? getGeneratedId()); const filterText$ = bindableProp(_dirtyFilterText$, onFilterTextChange$); const {hasFocus$, directive: hasFocusDirective} = createHasFocus(); @@ -471,6 +474,7 @@ export function createSelect(config?: PropsConfig>): Sel const widget: SelectWidget = { ...stateStores({ + id$, visibleItems$, highlighted$, open$, diff --git a/core/src/utils/internal/dom.ts b/core/src/utils/internal/dom.ts index 3510315eb6..2857092cf9 100644 --- a/core/src/utils/internal/dom.ts +++ b/core/src/utils/internal/dom.ts @@ -87,3 +87,11 @@ export function addEvent(element: Element, type: string, fn: EventListenerOrEven element.removeEventListener(type, fn); }; } + +let idCount = 0; +/** + * Generates a unique ID with the format 'auId-[counter]'. + * + * @returns The generated ID. + */ +export const getGeneratedId = () => `auId-${idCount++}`; diff --git a/demo/src/routes/docs/[framework]/components/select/examples/+page.svelte b/demo/src/routes/docs/[framework]/components/select/examples/+page.svelte index e11c2d06c7..8b8a693991 100644 --- a/demo/src/routes/docs/[framework]/components/select/examples/+page.svelte +++ b/demo/src/routes/docs/[framework]/components/select/examples/+page.svelte @@ -13,3 +13,10 @@ An example that allows to select pages from the WikipediaService +
    +

    + The select component implements the ARIA combobox role. +

    +
    diff --git a/e2e/demo.spec.ts b/e2e/demo.spec.ts index b7582ec2a4..4f1e67b7bd 100644 --- a/e2e/demo.spec.ts +++ b/e2e/demo.spec.ts @@ -5,6 +5,7 @@ import type {AxeResults} from 'axe-core'; import {globSync} from 'glob'; import path from 'path'; import {normalizePath} from './utils'; +import {SelectPO} from '@agnos-ui/page-objects'; const pathToFrameworkDir = normalizePath(path.join(__dirname, '../demo/src/routes')); const pathToDocsDir = normalizePath(path.join(__dirname, '../docs')); @@ -44,6 +45,26 @@ test.describe.parallel('Demo Website', () => { expect((await analyze(page, route)).violations).toEqual([]); }); } + + const frameworks = [ + {name: 'Angular', url: '/angular/samples/'}, + {name: 'React', url: '/react/samples/'}, + {name: 'Svelte', url: '/svelte/samples/app/'}, + ]; + + test.describe.parallel('Select tests', () => { + frameworks.forEach(({name, url}) => { + test(`[${name}] Select accessibility `, async ({page}) => { + const route = `${url}#/select/default`; + await page.goto(route); + const selectPO = new SelectPO(page); + const locatorInput = selectPO.locatorInput; + await locatorInput.fill('a'); + await locatorInput.press('Enter'); + expect((await analyze(page, route)).violations).toEqual([]); + }); + }); + }); }); test.describe('Sitemap', () => { diff --git a/e2e/samplesMarkup.e2e-spec.ts-snapshots/select-custom.html b/e2e/samplesMarkup.e2e-spec.ts-snapshots/select-custom.html index d0d3a054c4..5703a0781d 100644 --- a/e2e/samplesMarkup.e2e-spec.ts-snapshots/select-custom.html +++ b/e2e/samplesMarkup.e2e-spec.ts-snapshots/select-custom.html @@ -12,6 +12,7 @@ class="au-select border border-1 d-block dropdown mb-3 p-1" >