Skip to content

Commit 95a9e9a

Browse files
committed
Use aria-hidden to mask Service Navigation toggle from VoiceOver
`aria-expanded` and `aria-controls` make VoiceOver keep the button visible in its rotor, so we add `aria-hidden` as a complement to `hidden`. This is prefered to manually adding and removing each of `aria-expanded` and `aria-controls` as we'd need to remember their value when resetting them. It also future proofs in case other `aria` attributes make VoiceOver keep the button in the rotor.
1 parent 394bb70 commit 95a9e9a

File tree

2 files changed

+65
-2
lines changed

2 files changed

+65
-2
lines changed

packages/govuk-frontend/src/govuk/components/service-navigation/service-navigation.mjs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ export class ServiceNavigation extends Component {
127127

128128
if (this.mql.matches) {
129129
this.$menu.removeAttribute('hidden')
130-
this.$menuButton.setAttribute('hidden', '')
130+
setAttributes(this.$menuButton, attributesForHidingButton)
131131
} else {
132-
this.$menuButton.removeAttribute('hidden')
132+
removeAttributes(this.$menuButton, Object.keys(attributesForHidingButton))
133133
this.$menuButton.setAttribute('aria-expanded', this.menuIsOpen.toString())
134134

135135
if (this.menuIsOpen) {
@@ -158,3 +158,39 @@ export class ServiceNavigation extends Component {
158158
*/
159159
static moduleName = 'govuk-service-navigation'
160160
}
161+
162+
/**
163+
* Collection of attributes that needs setting on a `<button>`
164+
* to fully hide it, both visually and from screen-readers,
165+
* and prevent its activation while hidden
166+
*/
167+
const attributesForHidingButton = {
168+
hidden: '',
169+
// Fix button still appearing in VoiceOver's form control's menu despite being hidden
170+
// https://bugs.webkit.org/show_bug.cgi?id=300899
171+
'aria-hidden': 'true'
172+
}
173+
174+
/**
175+
* Sets a group of attributes on the given element
176+
*
177+
* @param {Element} $element - The element to set the attribute on
178+
* @param {{[attributeName: string]: string}} attributes - The attributes to set
179+
*/
180+
function setAttributes($element, attributes) {
181+
for (const attributeName in attributes) {
182+
$element.setAttribute(attributeName, attributes[attributeName])
183+
}
184+
}
185+
186+
/**
187+
* Removes a list of attributes from the given element
188+
*
189+
* @param {Element} $element - The element to remove the attributes from
190+
* @param {string[]} attributeNames - The names of the attributes to remove
191+
*/
192+
function removeAttributes($element, attributeNames) {
193+
for (const attributeName of attributeNames) {
194+
$element.removeAttribute(attributeName)
195+
}
196+
}

packages/govuk-frontend/src/govuk/components/service-navigation/service-navigation.puppeteer.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ describe('/components/service-navigation', () => {
169169
expect(buttonHiddenAttribute).toBeFalsy()
170170
})
171171

172+
it('does not add an `aria-hidden` attribute to the button for Voice Over', async () => {
173+
const buttonHiddenAttribute = await page.$eval(
174+
toggleButtonSelector,
175+
(el) => el.hasAttribute('aria-hidden')
176+
)
177+
178+
expect(buttonHiddenAttribute).toBeFalsy()
179+
})
180+
172181
it('renders the toggle button with `aria-expanded` set to false', async () => {
173182
const toggleExpandedAttribute = await page.$eval(
174183
toggleButtonSelector,
@@ -277,6 +286,15 @@ describe('/components/service-navigation', () => {
277286

278287
expect(buttonHiddenAttribute).toBeTruthy()
279288
})
289+
290+
it('adds an `aria-hidden` attribute for Voice Over', async () => {
291+
const buttonHiddenAttribute = await page.$eval(
292+
toggleButtonSelector,
293+
(el) => el.getAttribute('aria-hidden')
294+
)
295+
296+
expect(buttonHiddenAttribute).toBe('true')
297+
})
280298
})
281299

282300
describe('when page is resized to a narrow viewport', () => {
@@ -312,6 +330,15 @@ describe('/components/service-navigation', () => {
312330

313331
expect(buttonHiddenAttribute).toBeFalsy()
314332
})
333+
334+
it('removes the `aria-hidden` attribute on the toggle', async () => {
335+
const buttonHiddenAttribute = await page.$eval(
336+
toggleButtonSelector,
337+
(el) => el.hasAttribute('aria-hidden')
338+
)
339+
340+
expect(buttonHiddenAttribute).toBeFalsy()
341+
})
315342
})
316343
})
317344
})

0 commit comments

Comments
 (0)