Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/lib/components/filesharing/RecipientSelection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
title={$_('filesharing.encryptPanel.RecipientsHelpToggle')}
content={$_('filesharing.encryptPanel.RecipientsText')}
/>
<p id="required-fields-legend" class="required-legend">
{$_('filesharing.encryptPanel.requiredFieldsLegend')}
</p>
{/if}

{#each recipients as recipient, index (recipient)}
Expand Down Expand Up @@ -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);
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
for="recipient-email-{recipient.email}"
>
{$_('filesharing.encryptPanel.emailRecipient')}
<span class="required-asterisk" aria-hidden="true">*</span>
</label>
</div>
<input
Expand All @@ -61,6 +62,8 @@
)}
type="email"
required
aria-required="true"
aria-describedby="required-fields-legend"
Comment thread
rubenhensen marked this conversation as resolved.
class="pg-input"
class:is-confirming-bg={isConfirming}
bind:value={recipient.email}
Expand Down Expand Up @@ -164,6 +167,11 @@
font-family: var(--pg-font-family);
}

.required-asterisk {
color: var(--pg-error, #e53e3e);
margin-left: 0.125rem;
}

.optionals-container {
display: flex;
flex-direction: column;
Expand Down
8 changes: 8 additions & 0 deletions src/lib/components/filesharing/inputs/MultiInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@
<div class="input-wrapper">
<label for={randomId}>
{$_(translation_key)}
<span class="optional-text"
>({$_('filesharing.attributes.optional')})</span
>
</label>
<div class="optional-value" class:removed-del-border={isConfirming}>
{#if translation_key === 'filesharing.attributes.pbdf.sidn-pbdf.mobilenumber.mobilenumber'}
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/lib/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
66 changes: 66 additions & 0 deletions tests/attribute-form-a11y.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})