From f95a35a1518a0b33fe6bebef8b4748adb3cf6fcf Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 30 Oct 2026 14:34:07 +0100 Subject: [PATCH] fix(a11y): give all form fields a proper accessible name Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Arthur Schiwon --- playwright/e2e/a11y-question-inputs.spec.ts | 34 ++++++++++++++++++- playwright/support/sections/FormSection.ts | 1 + src/FormsSettings.vue | 1 + src/components/OptionInputDialog.vue | 1 - src/components/Questions/Question.vue | 5 +++ src/components/Questions/QuestionDate.vue | 1 + src/components/Questions/QuestionDropdown.vue | 2 +- src/components/Questions/QuestionFile.vue | 2 ++ src/components/Questions/QuestionGrid.vue | 4 +++ .../Questions/QuestionLinearScale.vue | 6 ++-- src/components/Questions/QuestionMultiple.vue | 1 + .../SidebarTabs/SettingsSidebarTab.vue | 1 + .../SidebarTabs/SharingSearchDiv.vue | 2 +- .../SidebarTabs/TransferOwnership.vue | 2 +- 14 files changed, 56 insertions(+), 7 deletions(-) diff --git a/playwright/e2e/a11y-question-inputs.spec.ts b/playwright/e2e/a11y-question-inputs.spec.ts index 66df3a141..55488836e 100644 --- a/playwright/e2e/a11y-question-inputs.spec.ts +++ b/playwright/e2e/a11y-question-inputs.spec.ts @@ -8,7 +8,7 @@ import { test as formTest } from '../support/fixtures/form.ts' import { test as appNavigationTest } from '../support/fixtures/navigation.ts' import { test as randomUserTest } from '../support/fixtures/random-user.ts' import { test as topBarTest } from '../support/fixtures/topBar.ts' -import { QuestionType } from '../support/sections/QuestionType.ts' +import { GridSubtype, QuestionType } from '../support/sections/QuestionType.ts' import { FormsView } from '../support/sections/TopBarSection.ts' const test = mergeTests(randomUserTest, appNavigationTest, formTest, topBarTest) @@ -32,6 +32,9 @@ test.describe('Accessibility: aria attributes on question inputs', () => { const questions = await form.getQuestions() await questions[0].fillTitle('My question') await questions[0].fillDescription('Some context') + await expect(questions[0].descriptionInput).toHaveAccessibleName( + 'Description of question number 1', + ) await topBar.toggleView(FormsView.View) @@ -148,6 +151,9 @@ test.describe('Accessibility: aria attributes on question inputs', () => { page.getByRole('heading', { name: 'My dropdown question' }), ).toHaveId('q1_title') await expect(page.locator('#q1_desc')).toContainText('Choose an option') + await expect(question.getByRole('combobox')).toHaveAccessibleName( + 'My dropdown question', + ) }) test('Date question with description has aria-labelledby and aria-describedby', async ({ @@ -176,6 +182,9 @@ test.describe('Accessibility: aria attributes on question inputs', () => { page.getByRole('heading', { name: 'My date question' }), ).toHaveId('q1_title') await expect(page.locator('#q1_desc')).toContainText('Pick a date') + await expect(question.getByRole('textbox')).toHaveAccessibleName( + 'My date question', + ) }) test('Linear scale question with description has aria-labelledby and aria-describedby', async ({ @@ -261,4 +270,27 @@ test.describe('Accessibility: aria attributes on question inputs', () => { ).toHaveId('q1_title') await expect(page.locator('#q1_desc')).toContainText('Pick a color') }) + + test('Grid cells are named by row and column', async ({ + appNavigation, + form, + topBar, + page, + }) => { + await appNavigation.clickNewForm() + await form.fillTitle('Test form') + + await form.addQuestion(QuestionType.Grid, GridSubtype.RadioButtons) + const questions = await form.getQuestions() + await questions[0].fillTitle('Satisfaction') + await questions[0].addColumn('Good') + await questions[0].addRow('Service') + + await topBar.toggleView(FormsView.View) + + const question = page.getByRole('listitem', { name: /Question number 1/ }) + await expect( + question.getByRole('radio', { name: 'Service, Good' }), + ).toBeAttached() + }) }) diff --git a/playwright/support/sections/FormSection.ts b/playwright/support/sections/FormSection.ts index f934ca7ed..ab7d3e8d0 100644 --- a/playwright/support/sections/FormSection.ts +++ b/playwright/support/sections/FormSection.ts @@ -25,6 +25,7 @@ export class FormSection { }) this.descriptionField = this.mainContent.getByRole('textbox', { name: 'Description', + exact: true, }) } diff --git a/src/FormsSettings.vue b/src/FormsSettings.vue index d5b935242..fccb5007a 100644 --- a/src/FormsSettings.vue +++ b/src/FormsSettings.vue @@ -20,6 +20,7 @@ multiple :options="availableGroups" :placeholder="t('forms', 'Select groups')" + :aria-label-combobox="t('forms', 'Select groups')" class="forms-settings__creation__multiselect" label="displayName" @update:modelValue="onCreationAllowedGroupsChange" /> diff --git a/src/components/OptionInputDialog.vue b/src/components/OptionInputDialog.vue index 955827e7e..201c0110f 100644 --- a/src/components/OptionInputDialog.vue +++ b/src/components/OptionInputDialog.vue @@ -15,7 +15,6 @@ diff --git a/src/components/Questions/Question.vue b/src/components/Questions/Question.vue index ebd9f8a7c..353fad731 100644 --- a/src/components/Questions/Question.vue +++ b/src/components/Questions/Question.vue @@ -130,6 +130,11 @@ ref="descriptionField" dir="auto" :value="description" + :aria-label=" + t('forms', 'Description of question number {index}', { + index, + }) + " :placeholder=" t( 'forms', diff --git a/src/components/Questions/QuestionDate.vue b/src/components/Questions/QuestionDate.vue index 9636ffd64..0d8707011 100644 --- a/src/components/Questions/QuestionDate.vue +++ b/src/components/Questions/QuestionDate.vue @@ -92,6 +92,7 @@ :disabled="!readOnly" :format="stringify" :placeholder="datetimePickerPlaceholder" + :aria-label="text" :showSecond="false" :type="dateTimePickerType" :disabledDate="disabledDates" diff --git a/src/components/Questions/QuestionDropdown.vue b/src/components/Questions/QuestionDropdown.vue index 77db95a1c..a22aea366 100644 --- a/src/components/Questions/QuestionDropdown.vue +++ b/src/components/Questions/QuestionDropdown.vue @@ -40,7 +40,7 @@ :options="choices" :searchable="false" label="text" - :aria-label-combobox="selectOptionPlaceholder" + :aria-label-combobox="text" :aria-errormessage="hasError ? errorId : undefined" :aria-invalid="hasError ? 'true' : undefined" @invalid.prevent="validate" diff --git a/src/components/Questions/QuestionFile.vue b/src/components/Questions/QuestionFile.vue index 5d7f9a006..8d2245da0 100644 --- a/src/components/Questions/QuestionFile.vue +++ b/src/components/Questions/QuestionFile.vue @@ -39,6 +39,7 @@ type="multiselect" :modelValue="maxFileSizeUnit" :options="availableUnits" + :aria-label-combobox="t('forms', 'File size unit')" required :clearable="false" :searchable="false" @@ -68,6 +69,7 @@ diff --git a/src/components/SidebarTabs/TransferOwnership.vue b/src/components/SidebarTabs/TransferOwnership.vue index 47eb3c0a7..bb2d221b4 100644 --- a/src/components/SidebarTabs/TransferOwnership.vue +++ b/src/components/SidebarTabs/TransferOwnership.vue @@ -43,7 +43,7 @@ class="modal-content__select" :loading="loading" :options="options" - :placeholder="t('forms', 'Search for a user')" + :inputLabel="t('forms', 'Search for a user')" @search=" (query) => asyncSearch(query, [SHARE_TYPES.SHARE_TYPE_USER]) ">