diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs index 169cb664db..2413e6bc13 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -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 @@ -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') @@ -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' @@ -106,10 +113,12 @@ 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)) + + // Add a space so the button and instruction read correctly + // when CSS is disabled + containerSpan.insertAdjacentText('beforeend', ' ') const instructionSpan = document.createElement('span') instructionSpan.className = 'govuk-body govuk-file-upload__instruction' @@ -119,8 +128,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)) @@ -144,7 +153,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() @@ -271,11 +280,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}` - ) } /** diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js index bbad4c7e5d..126e7320d7 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js @@ -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' @@ -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' - ) }) }) }) @@ -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) @@ -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', () => {