diff --git a/src/lib/components/filesharing/RecipientSelection.svelte b/src/lib/components/filesharing/RecipientSelection.svelte index d6de026..8385333 100644 --- a/src/lib/components/filesharing/RecipientSelection.svelte +++ b/src/lib/components/filesharing/RecipientSelection.svelte @@ -48,6 +48,9 @@ title={$_('filesharing.encryptPanel.RecipientsHelpToggle')} content={$_('filesharing.encryptPanel.RecipientsText')} /> +

+ {$_('filesharing.encryptPanel.requiredFieldsLegend')} +

{/if} {#each recipients as recipient, index (recipient)} @@ -84,4 +87,11 @@ .remove-border { border: none; } + + .required-legend { + font-size: var(--pg-font-size-sm); + color: var(--pg-text-secondary); + margin: 0.5rem 0 0; + font-family: var(--pg-font-family); + } diff --git a/src/lib/components/filesharing/RecipientSelectionFields.svelte b/src/lib/components/filesharing/RecipientSelectionFields.svelte index 5f8213e..f2b4c9f 100644 --- a/src/lib/components/filesharing/RecipientSelectionFields.svelte +++ b/src/lib/components/filesharing/RecipientSelectionFields.svelte @@ -52,6 +52,7 @@ for="recipient-email-{recipient.email}" > {$_('filesharing.encryptPanel.emailRecipient')} +
{#if translation_key === 'filesharing.attributes.pbdf.sidn-pbdf.mobilenumber.mobilenumber'} @@ -215,6 +218,11 @@ display: block; } + .optional-text { + font-weight: var(--pg-font-weight-regular); + color: var(--pg-text-secondary); + } + .btn-delete { all: unset; aspect-ratio: 1 / 1; diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index f542d50..259ad6a 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -207,6 +207,7 @@ "emailRecipient": "Email address", "emailRecipientPlaceholder": "recipient@example.com", "RecipientsText": "Specify the recipients and what attributes they must prove to decrypt the files. Only the recipients will see this data.", + "requiredFieldsLegend": "Fields marked with * are required.", "RecipientsOptionalHeading": "Additional required attributes", "emailSender": "Email address", "emailSenderHeading": "Your information", @@ -289,7 +290,8 @@ "pbdf.gemeente.personalData.fullname": "Full name", "pbdf.gemeente.personalData.fullname.placeholder": "John Doe", "pbdf.gemeente.personalData.dateofbirth": "Date of birth", - "pbdf.gemeente.personalData.dateofbirth.placeholder": "15-01-1990" + "pbdf.gemeente.personalData.dateofbirth.placeholder": "15-01-1990", + "optional": "optional" } }, "error": { diff --git a/src/lib/locales/nl.json b/src/lib/locales/nl.json index 0067ebe..17fce1c 100644 --- a/src/lib/locales/nl.json +++ b/src/lib/locales/nl.json @@ -206,6 +206,7 @@ "emailRecipient": "E-mailadres", "emailRecipientPlaceholder": "ontvanger@voorbeeld.nl", "RecipientsText": "Geef de ontvangers op en welke attributen ze moeten aantonen om de bestanden te ontsleutelen. Alleen de ontvangers kunnen deze gegevens zien.", + "requiredFieldsLegend": "Velden gemarkeerd met * zijn verplicht.", "RecipientsOptionalHeading": "Extra vereiste attributen", "emailSender": "E-mailadres", "emailSenderHeading": "Jouw gegevens", @@ -288,7 +289,8 @@ "pbdf.gemeente.personalData.fullname": "Volledige naam", "pbdf.gemeente.personalData.fullname.placeholder": "Jan Jansen", "pbdf.gemeente.personalData.dateofbirth": "Geboortedatum", - "pbdf.gemeente.personalData.dateofbirth.placeholder": "15-01-1990" + "pbdf.gemeente.personalData.dateofbirth.placeholder": "15-01-1990", + "optional": "optioneel" } }, "error": { diff --git a/tests/attribute-form-a11y.test.ts b/tests/attribute-form-a11y.test.ts new file mode 100644 index 0000000..e0cfc6b --- /dev/null +++ b/tests/attribute-form-a11y.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from '@playwright/test' + +// Accessibility regression tests for the recipient attribute-entry form +// (issues encryption4all/postguard-website#268 and #269). +// +// #268: a screenreader did not clearly announce whether a field was required. +// The required email field must expose `aria-required`, carry a visible +// asterisk, and the form must show an explanatory legend for the asterisk. +// #269: it was not clear that the phone number and birthday attributes are +// optional. Each optional attribute label must say "(optional)" so it is +// announced to screenreader users. + +test.beforeEach(async ({ page }) => { + await page.goto('/fileshare/') + // The recipient form renders in the initial FileSelection state. + await expect( + page.getByRole('textbox', { name: /email address/i }) + ).toBeVisible() +}) + +test('required email field exposes aria-required and a visible asterisk', async ({ + page, +}) => { + const email = page.getByRole('textbox', { name: /email address/i }) + await expect(email).toHaveAttribute('aria-required', 'true') + // Native required is still present so browser validation also fires. + await expect(email).toHaveAttribute('required', '') + + // The asterisk is rendered in the label and hidden from the a11y tree + // (the required state itself is conveyed by aria-required). + const asterisk = page.locator('label.field-label .required-asterisk') + await expect(asterisk).toHaveText('*') + await expect(asterisk).toHaveAttribute('aria-hidden', 'true') +}) + +test('the form shows a legend explaining the required-field asterisk', async ({ + page, +}) => { + const legend = page.locator('#required-fields-legend') + await expect(legend).toBeVisible() + await expect(legend).toContainText('*') + + // The required email input points at the legend for assistive tech. + const email = page.getByRole('textbox', { name: /email address/i }) + await expect(email).toHaveAttribute( + 'aria-describedby', + 'required-fields-legend' + ) +}) + +test('optional attributes are announced as optional via their label', async ({ + page, +}) => { + // Add the mobile-number attribute (#269 phone number). + await page.getByRole('button', { name: /mobile phone number/i }).click() + const phone = page.getByLabel(/mobile phone number/i) + await expect(phone).toBeVisible() + // The accessible name of the field must include "(optional)". + await expect(phone).toHaveAccessibleName(/\(optional\)/i) + + // Add the date-of-birth attribute (#269 birthday). + await page.getByRole('button', { name: /date of birth/i }).click() + const birthday = page.getByLabel(/date of birth/i) + await expect(birthday).toBeVisible() + await expect(birthday).toHaveAccessibleName(/\(optional\)/i) +})