Skip to content

Commit

Permalink
Plat UI 3352 maintain links to error and hints (#434)
Browse files Browse the repository at this point in the history
maintain aria-describedby links to error and hint associated with the select that's been enhanced

before this change, if you were using the separate polyfill from adam then the links to the error and hint would be added for you but would then be lost after you filled in the field
  • Loading branch information
oscarduignan authored Jan 9, 2025
1 parent 2f2dbc3 commit a16c7db
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 20 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [6.51.0] - 2025-01-09

### Changed

- accessibility related fix to maintain aria-describedby links to error and hints on autocompletes after interaction

## [6.50.0] - 2025-01-08

### Changed
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions lib/browser-tests/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,20 @@ expect.extend({
};
}
},
async toBeAccessibleAutocomplete(element) {
const elementDetails = await element.evaluate((el) => ({
tagName: el.tagName.toLowerCase(),
role: el.getAttribute('role'),
parent: el.parentElement.classList.toString(),
}));

const isAccessibleAutocomplete = elementDetails.tagName === 'input'
&& elementDetails.role === 'combobox'
&& elementDetails.parent === 'autocomplete__wrapper';

return {
message: () => 'expected element to be accessible autocomplete',
pass: isAccessibleAutocomplete,
};
},
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hmrc-frontend",
"version": "6.50.0",
"version": "6.51.0",
"description": "Design patterns for HMRC frontends",
"scripts": {
"start": "gulp dev",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ async function interceptNextFormPost(page) {
}

describe('Patched accessible autocomplete', () => {
it('should announce the hint and error message linked to the underlying select', async () => {
/**
* We don't need to explicitly check there are no javascript errors
* in the browser because we use jestPuppeteer to exitOnPageError
* which does it for us.
*/

it('should announce the hints and error message linked to the underlying select', async () => {
await render(page, withGovukSelect({
id: 'location',
name: 'location',
Expand All @@ -71,13 +77,83 @@ describe('Patched accessible autocomplete', () => {
}));

const element = await page.$('#location');
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
await expect(element).toBeAccessibleAutocomplete();
const ariaDescribedBy = await element.evaluate((el) => el.getAttribute('aria-describedby'));

expect(tagName).not.toBe('select'); // or select element was not enhanced to be an autocomplete component
expect(ariaDescribedBy).toBe('location-hint location-error location__assistiveHint');
});

// This wasn't something covered by the original polyfill we based our
// patches on, so we've checked this should be the behaviour with
// HMRC's Digital Inclusion and Accessibility Standards team (DIAS).
it('should still announce the hints and error message linked to the underlying select after interaction with field', async () => {
await render(page, withGovukSelect({
id: 'location',
name: 'location',
attributes: {
'data-module': 'hmrc-accessible-autocomplete',
},
label: {
text: 'Choose location',
},
errorMessage: {
text: 'You must choose a location',
},
hint: {
text: 'This can be different to where you went before',
},
items: [
{
value: ' ',
text: 'Choose location',
},
// omitted other options for brevity of test
],
}));

const currentAriaDescribedByFrom = (element) => element.evaluate((el) => el.getAttribute('aria-describedby'));

const element = await page.$('#location');
await expect(element).toBeAccessibleAutocomplete();
await expect(page).toFill('#location', 'London');
await element.evaluate((input) => input.blur());
expect(await currentAriaDescribedByFrom(element)).toBe('location-hint location-error ');
await expect(page).toFill('#location', '');
// following shows that hint is retained if input is empty, and updates immediately
expect(await currentAriaDescribedByFrom(element)).toBe('location-hint location-error location__assistiveHint');
});

it('should still announce the assistiveHint correctly if there is no hint or error on the select', async () => {
await render(page, withGovukSelect({
id: 'location',
name: 'location',
attributes: {
'data-module': 'hmrc-accessible-autocomplete',
},
label: {
text: 'Choose location',
},
items: [
{
value: ' ',
text: 'Choose location',
},
// omitted other options for brevity of test
],
}));

const currentAriaDescribedByFrom = (element) => element.evaluate((el) => el.getAttribute('aria-describedby'));

const element = await page.$('#location');
await expect(element).toBeAccessibleAutocomplete();
expect(await currentAriaDescribedByFrom(element)).toBe('location__assistiveHint');
await expect(page).toFill('#location', 'London');
await element.evaluate((input) => input.blur());
expect(await currentAriaDescribedByFrom(element)).toBe(null);
await expect(page).toFill('#location', '');
// following shows that hint is retained if input is empty, and updates immediately
expect(await currentAriaDescribedByFrom(element)).toBe('location__assistiveHint');
});

it('should inherit the error state of the underlying select', async () => {
await render(page, withGovukSelect({
id: 'location',
Expand All @@ -104,10 +180,8 @@ describe('Patched accessible autocomplete', () => {
}));

const element = await page.$('#location');
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
await expect(element).toBeAccessibleAutocomplete();
const borderColor = await element.evaluate((el) => getComputedStyle(el).getPropertyValue('border-color'));

expect(tagName).not.toBe('select'); // or select element was not enhanced to be an autocomplete component
expect(borderColor).toBe('rgb(212, 53, 28)');
});

Expand Down Expand Up @@ -141,6 +215,8 @@ describe('Patched accessible autocomplete', () => {
],
}));

const element = await page.$('#location');
await expect(element).toBeAccessibleAutocomplete();
await expect(page).toFill('#location', 'Lon');
await acceptFirstSuggestionFor('#location');
expect(await page.$eval('select', (select) => select.value)).toBe('london');
Expand Down Expand Up @@ -179,11 +255,13 @@ describe('Patched accessible autocomplete', () => {
],
}));

const element = await page.$('#location');
await expect(element).toBeAccessibleAutocomplete();
await expect(page).toFill('#location', 'Lon');
await acceptFirstSuggestionFor('#location');
expect(await page.$eval('select', (select) => select.value)).toBe('london');
await expect(page).toFill('#location', 'South West');
await page.$eval('#location', (input) => input.blur());
await element.evaluate((input) => input.blur());
expect(await page.$eval('select', (select) => select.value)).toBe('southwest');
});

Expand Down Expand Up @@ -218,6 +296,8 @@ describe('Patched accessible autocomplete', () => {
],
}));

const element = await page.$('#location');
await expect(element).toBeAccessibleAutocomplete();
await expect(page).toFill('#location', 'Lon');
await acceptFirstSuggestionFor('#location');
expect(await page.$eval('select', (select) => select.value)).toBe('london');
Expand Down Expand Up @@ -258,10 +338,8 @@ describe('Patched accessible autocomplete', () => {
await delay(100); // because it takes ~50ms for adam's polyfill to apply

const element = await page.$('#location');
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
await expect(element).toBeAccessibleAutocomplete();
const ariaDescribedBy = await element.evaluate((el) => el.getAttribute('aria-describedby'));

expect(tagName).not.toBe('select'); // or select element was not enhanced to be an autocomplete component
expect(ariaDescribedBy).toBe('location-hint location-error location__assistiveHint');
});

Expand Down Expand Up @@ -296,6 +374,8 @@ describe('Patched accessible autocomplete', () => {
await page.evaluate(adamsPolyfill);
await delay(100); // because it takes ~50ms for adam's polyfill to apply

const element = await page.$('#location');
await expect(element).toBeAccessibleAutocomplete();
await expect(page).toFill('#location', 'Lon');
await acceptFirstSuggestionFor('#location');
const { postedFormData } = await interceptNextFormPost(page);
Expand Down Expand Up @@ -334,6 +414,8 @@ describe('Patched accessible autocomplete', () => {
await page.evaluate(adamsPolyfill);
await delay(100); // because it takes ~50ms for adam's polyfill to apply

const element = await page.$('#location');
await expect(element).toBeAccessibleAutocomplete();
await expect(page).toFill('#location', 'Lon');
await acceptFirstSuggestionFor('#location');
const { postedFormData } = await interceptNextFormPost(page);
Expand Down
28 changes: 24 additions & 4 deletions src/components/accessible-autocomplete/accessible-autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,42 @@ AccessibleAutoComplete.prototype.init = function init() {

const selectElementAriaDescribedBy = selectElement.getAttribute('aria-describedby') || '';
const autocompleteElement = document.getElementById(autocompleteId);
const autocompleteElementAriaDescribedBy = (autocompleteElement && autocompleteElement.getAttribute('aria-describedby')) || '';
const autocompleteElementAriaDescribedBy = (autocompleteElement?.getAttribute('aria-describedby')) || '';
const autocompleteElementMissingAriaDescribedAttrs = (
autocompleteElement
&& autocompleteElement.tagName !== 'select'
&& !autocompleteElementAriaDescribedBy.includes(selectElementAriaDescribedBy)
);
if (autocompleteElementMissingAriaDescribedAttrs) {
// if there is a hint and/or error then the autocomplete element
// needs to be aria-describedby these, which it isn't be default
// we need to check if it hasn't already been done to avoid
// needs to be aria-describedby these, which it isn't be default.
// we need to check if it hasn't already been done to avoid adding
// them twice if someone has added a separate patch.
autocompleteElement.setAttribute(
'aria-describedby',
`${selectElementAriaDescribedBy} ${autocompleteElementAriaDescribedBy}`,
);

if (window.MutationObserver != null) {
// when the input is empty, the autocomplete adds a link to a hint
// that explains how to interact with the input via aria-describedby
// and when it's not empty it removes it. These changes cause the
// removal of the links to the error and hint, so we need to add
// those links back, as well as maintain the link to the hint if it
// was present because the input is empty.
new MutationObserver(() => {
const currentAriaDescribedBy = autocompleteElement.getAttribute('aria-describedby') || '';
if (!currentAriaDescribedBy?.includes(selectElementAriaDescribedBy)) {
autocompleteElement.setAttribute('aria-describedby', `${selectElementAriaDescribedBy} ${currentAriaDescribedBy}`);
}
}).observe(autocompleteElement, {
attributes: true,
attributeFilter: ['aria-describedby'],
});
}

// and in case page is still using adam's patch, this should stop
// the select elements aria described by being added to the
// the select elements aria-describedby from being added to the
// autocomplete element twice when that runs (though unsure if a
// screen reader would actually announce the elements twice if same
// element was listed twice in the aria-describedby attribute)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ examples:
text: Select country
hint:
text: "Please select a country"
errorMessage: "An error occured"
errorMessage: "An error occurred"
id: location-picker
select: |
<select id="location-picker" data-show-all-values="false" data-auto-select="false" data-default-value="" data-module="hmrc-accessible-autocomplete">
<select class="govuk-select govuk-select--error" aria-describedby="location-picker-error location-picker-hint" id="location-picker" data-show-all-values="false" data-auto-select="false" data-default-value="" data-module="hmrc-accessible-autocomplete">
<option value="fr">France</option>
<option value="de">Germany</option>
<option value="gb">United Kingdom</option>
Expand Down

0 comments on commit a16c7db

Please sign in to comment.