Skip to content

Commit

Permalink
Label the <button> using aria-labelledby
Browse files Browse the repository at this point in the history
Reference the `<label>` and the `<button>` itself so there's no duplication
of content, ensuring that when translated, the accessible name matches what's
visible on the screen.
  • Loading branch information
romaricpascal committed Jan 30, 2025
1 parent ede849e commit e09c607
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ export class FileUpload extends ConfigurableComponent {
locale: closestAttributeValue(this.$root, 'lang')
})

this.$label = this.findLabel()
const $label = this.findLabel()
$label.setAttribute('for', `${this.id}-input`)
// Add an ID to the label if it doesn't have one already
// so it can be referenced by `aria-labelledby`
if (!$label.id) {
$label.id = `${this.id}-label`
}

// we need to copy the 'id' of the root element
// to the new button replacement element
Expand All @@ -72,10 +78,6 @@ export class FileUpload extends ConfigurableComponent {
const $wrapper = document.createElement('div')
$wrapper.className = 'govuk-file-upload-wrapper'

const commaSpan = document.createElement('span')
commaSpan.className = 'govuk-visually-hidden'
commaSpan.innerText = ', '

// Create the file selection button
const $button = document.createElement('button')
$button.classList.add('govuk-file-upload__button')
Expand All @@ -93,11 +95,16 @@ export class FileUpload extends ConfigurableComponent {
const $status = document.createElement('span')
$status.className = 'govuk-body govuk-file-upload__status'
$status.innerText = this.i18n.t('filesSelectedDefault')
$status.setAttribute('aria-hidden', 'true')
$status.classList.add('govuk-file-upload__status--empty')

$button.appendChild($status)
$button.appendChild(commaSpan.cloneNode(true))

const commaSpan = document.createElement('span')
commaSpan.className = 'govuk-visually-hidden'
commaSpan.innerText = ', '
commaSpan.id = `${this.id}-comma`

$button.appendChild(commaSpan)

const containerSpan = document.createElement('span')
containerSpan.className = 'govuk-file-upload__pseudo-button-container'
Expand All @@ -106,10 +113,8 @@ export class FileUpload extends ConfigurableComponent {
buttonSpan.className =
'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button'
buttonSpan.innerText = this.i18n.t('selectFilesButton')
buttonSpan.setAttribute('aria-hidden', 'true')

containerSpan.appendChild(buttonSpan)
containerSpan.appendChild(commaSpan.cloneNode(true))

const instructionSpan = document.createElement('span')
instructionSpan.className = 'govuk-body govuk-file-upload__instruction'
Expand All @@ -119,8 +124,8 @@ export class FileUpload extends ConfigurableComponent {

$button.appendChild(containerSpan)
$button.setAttribute(
'aria-label',
`${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${$status.innerText}`
'aria-labelledby',
`${$label.id} ${commaSpan.id} ${$button.id}`
)
$button.addEventListener('click', this.onClick.bind(this))

Expand All @@ -144,7 +149,7 @@ export class FileUpload extends ConfigurableComponent {
// Bind change event to the underlying input
this.$root.addEventListener('change', this.onChange.bind(this))

// Syncronise the `disabled` state between the button and underlying input
// Synchronise the `disabled` state between the button and underlying input
this.updateDisabledState()
this.observeDisabledState()

Expand Down Expand Up @@ -271,11 +276,6 @@ export class FileUpload extends ConfigurableComponent {

this.$status.classList.remove('govuk-file-upload__status--empty')
}

this.$button.setAttribute(
'aria-label',
`${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${this.$status.innerText}`
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* eslint-disable no-new */

const { render } = require('@govuk-frontend/helpers/puppeteer')
const {
render,
getAccessibleName
} = require('@govuk-frontend/helpers/puppeteer')
const { getExamples } = require('@govuk-frontend/lib/components')

const inputSelector = '.govuk-file-upload'
Expand Down Expand Up @@ -104,15 +107,8 @@ describe('/components/file-upload', () => {
(el) => el.innerHTML.trim()
)

const buttonAriaText = await page.$eval(buttonSelector, (el) =>
el.getAttribute('aria-label')
)

expect(buttonElementText).toBe('Choose file')
expect(statusElementText).toBe('No file chosen')
expect(buttonAriaText).toBe(
'Upload a file, Choose file or drop file, No file chosen'
)
})
})
})
Expand Down Expand Up @@ -348,6 +344,61 @@ describe('/components/file-upload', () => {
})
})

describe('accessible name', () => {
beforeEach(async () => {})

it('includes the label, the status, the pseudo button and instruction', async () => {
await render(page, 'file-upload', examples.enhanced)

const $element = await page.$('.govuk-file-upload__button')

const accessibleName = await getAccessibleName(page, $element)
await expect(accessibleName.replaceAll(/\s+/g, ' ')).toBe(
'Upload a file , No file chosen , Choose file or drop file'
)
})

it('includes the label, file name, pseudo button and instruction once a file is selected', async () => {
await render(page, 'file-upload', examples.enhanced)

const $element = await page.$('.govuk-file-upload__button')

const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click(buttonSelector)
])
await fileChooser.accept(['fakefile.txt'])

const accessibleName = await getAccessibleName(page, $element)
await expect(accessibleName.replaceAll(/\s+/g, ' ')).toBe(
'Upload a file , fakefile.txt , Choose file or drop file'
)
})

it('includes the label, file name, pseudo button and instruction once a file is selected', async () => {
await render(page, 'file-upload', examples.enhanced, {
beforeInitialisation() {
document
.querySelector('[type="file"]')
.setAttribute('multiple', '')
}
})

const $element = await page.$('.govuk-file-upload__button')

const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click(buttonSelector)
])
await fileChooser.accept(['fakefile1.txt', 'fakefile2.txt'])

const accessibleName = await getAccessibleName(page, $element)
await expect(accessibleName.replaceAll(/\s+/g, ' ')).toBe(
'Upload a file , 2 files chosen , Choose file or drop file'
)
})
})

describe('i18n', () => {
beforeEach(async () => {
await render(page, 'file-upload', examples.translated)
Expand All @@ -363,15 +414,8 @@ describe('/components/file-upload', () => {
el.innerHTML.trim()
)

const buttonAriaText = await page.$eval(buttonSelector, (el) =>
el.getAttribute('aria-label')
)

expect(buttonElementText).toBe('Dewiswch ffeil')
expect(statusElementText).toBe("Dim ffeiliau wedi'u dewis")
expect(buttonAriaText).toBe(
"Llwythwch ffeil i fyny, Dewiswch ffeil neu ollwng ffeil, Dim ffeiliau wedi'u dewis"
)
})

describe('status element', () => {
Expand Down

0 comments on commit e09c607

Please sign in to comment.