Skip to content

Commit e312857

Browse files
fix(filesharing): convey required vs optional fields to assistive tech
The recipient attribute-entry form did not clearly communicate which fields are required or optional to screenreader users. - Email (required): add `aria-required="true"`, a visible red asterisk in the label (hidden from the a11y tree, since the state is conveyed by aria-required), an explanatory "Fields marked with * are required" legend, and `aria-describedby` linking the input to that legend. - Optional attributes (phone number, date of birth, full name): append a muted "(optional)" suffix to each label so the accessible name announces optionality. - Add EN/NL translations and a Playwright a11y regression test. Closes #268 Closes #269 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bc1a1b5 commit e312857

6 files changed

Lines changed: 98 additions & 2 deletions

File tree

src/lib/components/filesharing/RecipientSelection.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
title={$_('filesharing.encryptPanel.RecipientsHelpToggle')}
4949
content={$_('filesharing.encryptPanel.RecipientsText')}
5050
/>
51+
<p id="required-fields-legend" class="required-legend">
52+
{$_('filesharing.encryptPanel.requiredFieldsLegend')}
53+
</p>
5154
{/if}
5255

5356
{#each recipients as recipient, index (recipient)}
@@ -84,4 +87,11 @@
8487
.remove-border {
8588
border: none;
8689
}
90+
91+
.required-legend {
92+
font-size: var(--pg-font-size-sm);
93+
color: var(--pg-text-secondary);
94+
margin: 0.5rem 0 0;
95+
font-family: var(--pg-font-family);
96+
}
8797
</style>

src/lib/components/filesharing/RecipientSelectionFields.svelte

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
for="recipient-email-{recipient.email}"
5353
>
5454
{$_('filesharing.encryptPanel.emailRecipient')}
55+
<span class="required-asterisk" aria-hidden="true">*</span>
5556
</label>
5657
</div>
5758
<input
@@ -61,6 +62,8 @@
6162
)}
6263
type="email"
6364
required
65+
aria-required="true"
66+
aria-describedby="required-fields-legend"
6467
class="pg-input"
6568
class:is-confirming-bg={isConfirming}
6669
bind:value={recipient.email}
@@ -164,6 +167,11 @@
164167
font-family: var(--pg-font-family);
165168
}
166169
170+
.required-asterisk {
171+
color: var(--pg-error, #e53e3e);
172+
margin-left: 0.125rem;
173+
}
174+
167175
.optionals-container {
168176
display: flex;
169177
flex-direction: column;

src/lib/components/filesharing/inputs/MultiInput.svelte

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@
121121
<div class="input-wrapper">
122122
<label for={randomId}>
123123
{$_(translation_key)}
124+
<span class="optional-text"
125+
>({$_('filesharing.attributes.optional')})</span
126+
>
124127
</label>
125128
<div class="optional-value" class:removed-del-border={isConfirming}>
126129
{#if translation_key === 'filesharing.attributes.pbdf.sidn-pbdf.mobilenumber.mobilenumber'}
@@ -215,6 +218,11 @@
215218
display: block;
216219
}
217220
221+
.optional-text {
222+
font-weight: var(--pg-font-weight-regular);
223+
color: var(--pg-text-secondary);
224+
}
225+
218226
.btn-delete {
219227
all: unset;
220228
aspect-ratio: 1 / 1;

src/lib/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@
207207
"emailRecipient": "Email address",
208208
"emailRecipientPlaceholder": "recipient@example.com",
209209
"RecipientsText": "Specify the recipients and what attributes they must prove to decrypt the files. Only the recipients will see this data.",
210+
"requiredFieldsLegend": "Fields marked with * are required.",
210211
"RecipientsOptionalHeading": "Additional required attributes",
211212
"emailSender": "Email address",
212213
"emailSenderHeading": "Your information",
@@ -289,7 +290,8 @@
289290
"pbdf.gemeente.personalData.fullname": "Full name",
290291
"pbdf.gemeente.personalData.fullname.placeholder": "John Doe",
291292
"pbdf.gemeente.personalData.dateofbirth": "Date of birth",
292-
"pbdf.gemeente.personalData.dateofbirth.placeholder": "15-01-1990"
293+
"pbdf.gemeente.personalData.dateofbirth.placeholder": "15-01-1990",
294+
"optional": "optional"
293295
}
294296
},
295297
"error": {

src/lib/locales/nl.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@
206206
"emailRecipient": "E-mailadres",
207207
"emailRecipientPlaceholder": "ontvanger@voorbeeld.nl",
208208
"RecipientsText": "Geef de ontvangers op en welke attributen ze moeten aantonen om de bestanden te ontsleutelen. Alleen de ontvangers kunnen deze gegevens zien.",
209+
"requiredFieldsLegend": "Velden gemarkeerd met * zijn verplicht.",
209210
"RecipientsOptionalHeading": "Extra vereiste attributen",
210211
"emailSender": "E-mailadres",
211212
"emailSenderHeading": "Jouw gegevens",
@@ -288,7 +289,8 @@
288289
"pbdf.gemeente.personalData.fullname": "Volledige naam",
289290
"pbdf.gemeente.personalData.fullname.placeholder": "Jan Jansen",
290291
"pbdf.gemeente.personalData.dateofbirth": "Geboortedatum",
291-
"pbdf.gemeente.personalData.dateofbirth.placeholder": "15-01-1990"
292+
"pbdf.gemeente.personalData.dateofbirth.placeholder": "15-01-1990",
293+
"optional": "optioneel"
292294
}
293295
},
294296
"error": {

tests/attribute-form-a11y.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { expect, test } from '@playwright/test'
2+
3+
// Accessibility regression tests for the recipient attribute-entry form
4+
// (issues encryption4all/postguard-website#268 and #269).
5+
//
6+
// #268: a screenreader did not clearly announce whether a field was required.
7+
// The required email field must expose `aria-required`, carry a visible
8+
// asterisk, and the form must show an explanatory legend for the asterisk.
9+
// #269: it was not clear that the phone number and birthday attributes are
10+
// optional. Each optional attribute label must say "(optional)" so it is
11+
// announced to screenreader users.
12+
13+
test.beforeEach(async ({ page }) => {
14+
await page.goto('/fileshare/')
15+
// The recipient form renders in the initial FileSelection state.
16+
await expect(
17+
page.getByRole('textbox', { name: /email address/i })
18+
).toBeVisible()
19+
})
20+
21+
test('required email field exposes aria-required and a visible asterisk', async ({
22+
page,
23+
}) => {
24+
const email = page.getByRole('textbox', { name: /email address/i })
25+
await expect(email).toHaveAttribute('aria-required', 'true')
26+
// Native required is still present so browser validation also fires.
27+
await expect(email).toHaveAttribute('required', '')
28+
29+
// The asterisk is rendered in the label and hidden from the a11y tree
30+
// (the required state itself is conveyed by aria-required).
31+
const asterisk = page.locator('label.field-label .required-asterisk')
32+
await expect(asterisk).toHaveText('*')
33+
await expect(asterisk).toHaveAttribute('aria-hidden', 'true')
34+
})
35+
36+
test('the form shows a legend explaining the required-field asterisk', async ({
37+
page,
38+
}) => {
39+
const legend = page.locator('#required-fields-legend')
40+
await expect(legend).toBeVisible()
41+
await expect(legend).toContainText('*')
42+
43+
// The required email input points at the legend for assistive tech.
44+
const email = page.getByRole('textbox', { name: /email address/i })
45+
await expect(email).toHaveAttribute(
46+
'aria-describedby',
47+
'required-fields-legend'
48+
)
49+
})
50+
51+
test('optional attributes are announced as optional via their label', async ({
52+
page,
53+
}) => {
54+
// Add the mobile-number attribute (#269 phone number).
55+
await page.getByRole('button', { name: /mobile phone number/i }).click()
56+
const phone = page.getByLabel(/mobile phone number/i)
57+
await expect(phone).toBeVisible()
58+
// The accessible name of the field must include "(optional)".
59+
await expect(phone).toHaveAccessibleName(/\(optional\)/i)
60+
61+
// Add the date-of-birth attribute (#269 birthday).
62+
await page.getByRole('button', { name: /date of birth/i }).click()
63+
const birthday = page.getByLabel(/date of birth/i)
64+
await expect(birthday).toBeVisible()
65+
await expect(birthday).toHaveAccessibleName(/\(optional\)/i)
66+
})

0 commit comments

Comments
 (0)