From 8e23ce86235c93a77d54b7ccccb0edad923ef7e5 Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Tue, 14 May 2019 20:24:58 +0100 Subject: [PATCH] Support initial aria-describedby on all form fields --- CHANGELOG.md | 8 ++ src/components/checkboxes/checkboxes.yaml | 20 +++++ src/components/checkboxes/template.njk | 5 +- src/components/checkboxes/template.test.js | 93 ++++++++++++++++++++- src/components/date-input/date-input.yaml | 20 ++++- src/components/date-input/template.njk | 2 +- src/components/date-input/template.test.js | 82 +++++++++++++++--- src/components/fieldset/fieldset.yaml | 2 +- src/components/fieldset/template.test.js | 9 ++ src/components/file-upload/file-upload.yaml | 4 + src/components/file-upload/template.njk | 2 +- src/components/file-upload/template.test.js | 87 ++++++++++++++++++- src/components/input/input.yaml | 4 + src/components/input/template.njk | 2 +- src/components/input/template.test.js | 86 ++++++++++++++++++- src/components/radios/radios.yaml | 16 ++++ src/components/radios/template.njk | 2 +- src/components/radios/template.test.js | 93 ++++++++++++++++++++- src/components/select/select.yaml | 4 + src/components/select/template.njk | 2 +- src/components/select/template.test.js | 86 ++++++++++++++++++- src/components/textarea/template.njk | 2 +- src/components/textarea/template.test.js | 86 ++++++++++++++++++- src/components/textarea/textarea.yaml | 4 + 24 files changed, 682 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e3577787..13f1b33208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ πŸ†• New features: +- Support aria-describedby on all form fields + + All form fields now support an initial `aria-describedby` value, populated before the optional hint and error message IDs are appended. + + Useful when fields are described by errors or hints on parent fieldsets. + + ([PR #1347](https://github.com/alphagov/govuk-frontend/pull/1347)) + - Pull Request Title goes here Description goes here (optional) diff --git a/src/components/checkboxes/checkboxes.yaml b/src/components/checkboxes/checkboxes.yaml index 5908ad7f07..aff1facee9 100644 --- a/src/components/checkboxes/checkboxes.yaml +++ b/src/components/checkboxes/checkboxes.yaml @@ -1,4 +1,8 @@ params: +- name: describedBy + type: string + required: false + description: One or more element IDs to add to the input `aria-describedby` attribute without a fieldset, used to provide additional descriptive information for screenreader users. - name: fieldset type: object required: false @@ -241,6 +245,22 @@ examples: hint: text: Go on, you know you want to! +- name: with fieldset and error message + data: + name: colours + errorMessage: + text: Please accept the terms and conditions + fieldset: + legend: + text: What is your nationality? + items: + - value: british + text: British + - value: irish + text: Irish + - value: other + text: Citizen of another country + - name: with all fieldset attributes data: idPrefix: example diff --git a/src/components/checkboxes/template.njk b/src/components/checkboxes/template.njk index e8cdc8c9f6..9a12be65cb 100644 --- a/src/components/checkboxes/template.njk +++ b/src/components/checkboxes/template.njk @@ -9,7 +9,10 @@ {#- a record of other elements that we need to associate with the input using aria-describedby – for example hints or error messages -#} -{% set describedBy = "" %} +{% set describedBy = params.describedBy if params.describedBy else "" %} +{% if params.fieldset.describedBy %} + {% set describedBy = params.fieldset.describedBy %} +{% endif %} {% set isConditional = false %} {% for item in params.items %} diff --git a/src/components/checkboxes/template.test.js b/src/components/checkboxes/template.test.js index a32b3a0fc2..29e0aeb851 100644 --- a/src/components/checkboxes/template.test.js +++ b/src/components/checkboxes/template.test.js @@ -71,6 +71,30 @@ describe('Checkboxes', () => { expect($component.hasClass('app-checkboxes--custom-modifier')).toBeTruthy() }) + it('renders initial aria-describedby on fieldset', () => { + const describedById = 'some-id' + + const $ = render('checkboxes', { + name: 'example-name', + fieldset: { + describedBy: describedById + }, + items: [ + { + value: '1', + text: 'Option 1' + }, + { + value: '2', + text: 'Option 2' + } + ] + }) + + const $fieldset = $('.govuk-fieldset') + expect($fieldset.attr('aria-describedby')).toMatch(describedById) + }) + it('render attributes', () => { const $ = render('checkboxes', { name: 'example-name', @@ -521,7 +545,7 @@ describe('Checkboxes', () => { }) it('associates the fieldset as "described by" the error message', () => { - const $ = render('checkboxes', examples['with all fieldset attributes']) + const $ = render('checkboxes', examples['with fieldset and error message']) const $fieldset = $('.govuk-fieldset') const $errorMessage = $('.govuk-error-message') @@ -534,6 +558,27 @@ describe('Checkboxes', () => { .toMatch(errorMessageId) }) + it('associates the fieldset as "described by" the error message and parent fieldset', () => { + const describedById = 'some-id' + const params = examples['with fieldset and error message'] + + params.fieldset.describedBy = describedById + + const $ = render('checkboxes', params) + + const $fieldset = $('.govuk-fieldset') + const $errorMessage = $('.govuk-error-message') + + const errorMessageId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $errorMessage.attr('id') + WORD_BOUNDARY + ) + + expect($fieldset.attr('aria-describedby')) + .toMatch(errorMessageId) + + delete params.fieldset.describedBy + }) + it('does not associate each input as "described by" the error message', () => { const $ = render('checkboxes', examples['with error message and hints on items']) @@ -576,6 +621,24 @@ describe('Checkboxes', () => { expect($fieldset.attr('aria-describedby')).toMatch(hintId) }) + + it('associates the fieldset as "described by" the hint and parent fieldset', () => { + const describedById = 'some-id' + const params = examples['with all fieldset attributes'] + + params.fieldset.describedBy = describedById + + const $ = render('checkboxes', params) + const $fieldset = $('.govuk-fieldset') + const $hint = $('.govuk-hint') + + const hintId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $hint.attr('id') + WORD_BOUNDARY + ) + + expect($fieldset.attr('aria-describedby')).toMatch(hintId) + delete params.fieldset.describedBy + }) }) describe('when they include both a hint and an error message', () => { @@ -583,15 +646,37 @@ describe('Checkboxes', () => { const $ = render('checkboxes', examples['with all fieldset attributes']) const $fieldset = $('.govuk-fieldset') - const $errorMessageId = $('.govuk-error-message').attr('id') - const $hintId = $('.govuk-hint').attr('id') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') + + const combinedIds = new RegExp( + WORD_BOUNDARY + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY + ) + + expect($fieldset.attr('aria-describedby')) + .toMatch(combinedIds) + }) + + it('associates the fieldset as described by the hint, error message and parent fieldset', () => { + const describedById = 'some-id' + const params = examples['with all fieldset attributes'] + + params.fieldset.describedBy = describedById + + const $ = render('checkboxes', params) + + const $fieldset = $('.govuk-fieldset') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') const combinedIds = new RegExp( - WORD_BOUNDARY + $hintId + WHITESPACE + $errorMessageId + WORD_BOUNDARY + WORD_BOUNDARY + describedById + WHITESPACE + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY ) expect($fieldset.attr('aria-describedby')) .toMatch(combinedIds) + + delete params.fieldset.describedBy }) }) diff --git a/src/components/date-input/date-input.yaml b/src/components/date-input/date-input.yaml index f37b4f12c9..68d8dcadd3 100644 --- a/src/components/date-input/date-input.yaml +++ b/src/components/date-input/date-input.yaml @@ -97,7 +97,25 @@ examples: - name: year classes: govuk-input--width-4 -- name: with errors +- name: with errors only + data: + id: dob-errors + fieldset: + legend: + text: What is your date of birth? + errorMessage: + text: Error message goes here + items: + - + name: day + classes: govuk-input--width-2 govuk-input--error + - + name: month + classes: govuk-input--width-2 govuk-input--error + - + name: year + classes: govuk-input--width-4 govuk-input--error +- name: with errors and hint data: id: dob-errors fieldset: diff --git a/src/components/date-input/template.njk b/src/components/date-input/template.njk index b7d4cbacea..c72b9bce88 100644 --- a/src/components/date-input/template.njk +++ b/src/components/date-input/template.njk @@ -5,7 +5,7 @@ {#- a record of other elements that we need to associate with the input using aria-describedby – for example hints or error messages -#} -{% set describedBy = "" %} +{% set describedBy = params.fieldset.describedBy if params.fieldset.describedBy else "" %} {% if params.items %} {% set dateInputItems = params.items %} diff --git a/src/components/date-input/template.test.js b/src/components/date-input/template.test.js index 09a8ac99b8..f14b196cbd 100644 --- a/src/components/date-input/template.test.js +++ b/src/components/date-input/template.test.js @@ -308,7 +308,7 @@ describe('Date input', () => { }) it('sets the `group` role on the fieldset to force JAWS18 to announce the hint and error message', () => { - const $ = render('date-input', examples['with errors']) + const $ = render('date-input', examples['with errors and hint']) const $fieldset = $('.govuk-fieldset') @@ -336,12 +336,12 @@ describe('Date input', () => { describe('when it includes a hint', () => { it('renders the hint', () => { - const $ = render('date-input', examples['with errors']) + const $ = render('date-input', examples['with errors and hint']) expect(htmlWithClassName($, '.govuk-hint')).toMatchSnapshot() }) it('associates the fieldset as "described by" the hint', () => { - const $ = render('date-input', examples['with errors']) + const $ = render('date-input', examples['with errors and hint']) const $fieldset = $('.govuk-fieldset') const $hint = $('.govuk-hint') @@ -353,16 +353,37 @@ describe('Date input', () => { expect($fieldset.attr('aria-describedby')) .toMatch(hintId) }) + + it('associates the fieldset as "described by" the hint and parent fieldset', () => { + const describedById = 'some-id' + const params = examples['with errors and hint'] + + params.fieldset.describedBy = describedById + + const $ = render('date-input', params) + + const $fieldset = $('.govuk-fieldset') + const $hint = $('.govuk-hint') + + const hintId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $hint.attr('id') + WORD_BOUNDARY + ) + + expect($fieldset.attr('aria-describedby')) + .toMatch(hintId) + + delete params.fieldset.describedBy + }) }) describe('when it includes an error message', () => { it('renders the error message', () => { - const $ = render('date-input', examples['with errors']) + const $ = render('date-input', examples['with errors only']) expect(htmlWithClassName($, '.govuk-error-message')).toMatchSnapshot() }) it('uses the id as a prefix for the error message id', () => { - const $ = render('date-input', examples['with errors']) + const $ = render('date-input', examples['with errors only']) const $errorMessage = $('.govuk-error-message') @@ -370,7 +391,7 @@ describe('Date input', () => { }) it('associates the fieldset as "described by" the error message', () => { - const $ = render('date-input', examples['with errors']) + const $ = render('date-input', examples['with errors only']) const $fieldset = $('.govuk-fieldset') const $errorMessage = $('.govuk-error-message') @@ -383,6 +404,27 @@ describe('Date input', () => { .toMatch(errorMessageId) }) + it('associates the fieldset as "described by" the error message and parent fieldset', () => { + const describedById = 'some-id' + const params = examples['with errors only'] + + params.fieldset.describedBy = describedById + + const $ = render('date-input', params) + + const $fieldset = $('.govuk-fieldset') + const $errorMessage = $('.govuk-error-message') + + const errorMessageId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $errorMessage.attr('id') + WORD_BOUNDARY + ) + + expect($fieldset.attr('aria-describedby')) + .toMatch(errorMessageId) + + delete params.fieldset.describedBy + }) + it('renders with a form group wrapper that has an error state', () => { const $ = render('date-input', { errorMessage: { @@ -397,14 +439,34 @@ describe('Date input', () => { describe('when they include both a hint and an error message', () => { it('associates the fieldset as described by both the hint and the error message', () => { - const $ = render('date-input', examples['with errors']) + const $ = render('date-input', examples['with errors and hint']) + + const $fieldset = $('.govuk-fieldset') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') + + const combinedIds = new RegExp( + WORD_BOUNDARY + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY + ) + + expect($fieldset.attr('aria-describedby')) + .toMatch(combinedIds) + }) + + it('associates the fieldset as described by the hint, error message and parent fieldset', () => { + const describedById = 'some-id' + const params = examples['with errors and hint'] + + params.fieldset.describedBy = describedById + + const $ = render('date-input', params) const $fieldset = $('.govuk-fieldset') - const $errorMessageId = $('.govuk-error-message').attr('id') - const $hintId = $('.govuk-hint').attr('id') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') const combinedIds = new RegExp( - WORD_BOUNDARY + $hintId + WHITESPACE + $errorMessageId + WORD_BOUNDARY + WORD_BOUNDARY + describedById + WHITESPACE + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY ) expect($fieldset.attr('aria-describedby')) diff --git a/src/components/fieldset/fieldset.yaml b/src/components/fieldset/fieldset.yaml index 22f28a722d..fa4fa249c1 100644 --- a/src/components/fieldset/fieldset.yaml +++ b/src/components/fieldset/fieldset.yaml @@ -2,7 +2,7 @@ params: - name: describedBy type: string required: false - description: Text or element id to add to the `aria-describedby` attribute to provide description of the group of fields for screenreader users. + description: One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users. - name: legend type: object required: false diff --git a/src/components/fieldset/template.test.js b/src/components/fieldset/template.test.js index 7c6ee6096d..1c647abf38 100644 --- a/src/components/fieldset/template.test.js +++ b/src/components/fieldset/template.test.js @@ -61,6 +61,15 @@ describe('fieldset', () => { expect($legend.text().trim()).toEqual('What is your address?') }) + it('allows you to set the aria-describedby attribute', () => { + const $ = render('fieldset', { + describedBy: 'some-id' + }) + + const $component = $('.govuk-fieldset') + expect($component.attr('aria-describedby')).toEqual('some-id') + }) + it('escapes HTML in the text argument', () => { const $ = render('fieldset', { legend: { diff --git a/src/components/file-upload/file-upload.yaml b/src/components/file-upload/file-upload.yaml index 51d2d346f0..713db75d34 100644 --- a/src/components/file-upload/file-upload.yaml +++ b/src/components/file-upload/file-upload.yaml @@ -11,6 +11,10 @@ params: type: string required: false description: Optional initial value of the input +- name: describedBy + type: string + required: false + description: One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users. - name: label type: object required: true diff --git a/src/components/file-upload/template.njk b/src/components/file-upload/template.njk index 9dbd9bf9be..afc0fa9e12 100644 --- a/src/components/file-upload/template.njk +++ b/src/components/file-upload/template.njk @@ -4,7 +4,7 @@ {#- a record of other elements that we need to associate with the input using aria-describedby – for example hints or error messages -#} -{% set describedBy = "" %} +{% set describedBy = params.describedBy if params.describedBy else "" %}
{{ govukLabel({ html: params.label.html, diff --git a/src/components/file-upload/template.test.js b/src/components/file-upload/template.test.js index ac3b2c0546..337da46a49 100644 --- a/src/components/file-upload/template.test.js +++ b/src/components/file-upload/template.test.js @@ -57,6 +57,17 @@ describe('File upload', () => { expect($component.val()).toEqual('C:/fakepath') }) + it('renders with aria-describedby', () => { + const describedById = 'some-id' + + const $ = render('file-upload', { + describedBy: describedById + }) + + const $component = $('.govuk-file-upload') + expect($component.attr('aria-describedby')).toMatch(describedById) + }) + it('renders with attributes', () => { const $ = render('file-upload', { attributes: { @@ -117,6 +128,28 @@ describe('File upload', () => { expect($component.attr('aria-describedby')) .toMatch(hintId) }) + + it('associates the input as "described by" the hint and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('file-upload', { + id: 'file-upload-with-hint', + describedBy: describedById, + hint: { + text: 'Your photo may be in your Pictures, Photos, Downloads or Desktop folder. Or in an app like iPhoto.' + } + }) + + const $component = $('.govuk-file-upload') + const $hint = $('.govuk-hint') + + const hintId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $hint.attr('id') + WORD_BOUNDARY + ) + + expect($component.attr('aria-describedby')) + .toMatch(hintId) + }) }) describe('when it includes an error message', () => { @@ -150,6 +183,28 @@ describe('File upload', () => { .toMatch(errorMessageId) }) + it('associates the input as "described by" the error message and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('file-upload', { + id: 'input-with-error', + describedBy: describedById, + errorMessage: { + 'text': 'Error message' + } + }) + + const $component = $('.govuk-file-upload') + const $errorMessage = $('.govuk-error-message') + + const errorMessageId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $errorMessage.attr('id') + WORD_BOUNDARY + ) + + expect($component.attr('aria-describedby')) + .toMatch(errorMessageId) + }) + it('includes the error class on the component', () => { const $ = render('file-upload', { errorMessage: { @@ -186,11 +241,37 @@ describe('File upload', () => { }) const $component = $('.govuk-file-upload') - const $errorMessageId = $('.govuk-error-message').attr('id') - const $hintId = $('.govuk-hint').attr('id') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') + + const combinedIds = new RegExp( + WORD_BOUNDARY + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY + ) + + expect($component.attr('aria-describedby')) + .toMatch(combinedIds) + }) + + it('associates the input as described by the hint, error message and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('file-upload', { + id: 'input-with-error', + describedBy: describedById, + errorMessage: { + 'text': 'Error message' + }, + hint: { + 'text': 'Hint' + } + }) + + const $component = $('.govuk-file-upload') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') const combinedIds = new RegExp( - WORD_BOUNDARY + $hintId + WHITESPACE + $errorMessageId + WORD_BOUNDARY + WORD_BOUNDARY + describedById + WHITESPACE + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY ) expect($component.attr('aria-describedby')) diff --git a/src/components/input/input.yaml b/src/components/input/input.yaml index 41f9f1e9f4..a9b15f151c 100644 --- a/src/components/input/input.yaml +++ b/src/components/input/input.yaml @@ -15,6 +15,10 @@ params: type: string required: false description: Optional initial value of the input. +- name: describedBy + type: string + required: false + description: One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users. - name: label type: object required: true diff --git a/src/components/input/template.njk b/src/components/input/template.njk index 9245ffab40..95cbd9aa87 100644 --- a/src/components/input/template.njk +++ b/src/components/input/template.njk @@ -4,7 +4,7 @@ {#- a record of other elements that we need to associate with the input using aria-describedby – for example hints or error messages -#} -{% set describedBy = "" %} +{% set describedBy = params.describedBy if params.describedBy else "" %}
{{ govukLabel({ html: params.label.html, diff --git a/src/components/input/template.test.js b/src/components/input/template.test.js index 1a0469f687..8382cfa3d5 100644 --- a/src/components/input/template.test.js +++ b/src/components/input/template.test.js @@ -73,6 +73,17 @@ describe('Input', () => { expect($component.val()).toEqual('QQ 12 34 56 C') }) + it('renders with aria-describedby', () => { + const describedById = 'some-id' + + const $ = render('input', { + describedBy: describedById + }) + + const $component = $('.govuk-input') + expect($component.attr('aria-describedby')).toMatch(describedById) + }) + it('renders with attributes', () => { const $ = render('input', { attributes: { @@ -133,6 +144,28 @@ describe('Input', () => { expect($input.attr('aria-describedby')) .toMatch(hintId) }) + + it('associates the input as "described by" the hint and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('input', { + id: 'input-with-hint', + describedBy: describedById, + hint: { + text: 'It’s on your National Insurance card, benefit letter, payslip or P60. For example, β€˜QQ 12 34 56 C’.' + } + }) + + const $input = $('.govuk-input') + const $hint = $('.govuk-hint') + + const hintId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $hint.attr('id') + WORD_BOUNDARY + ) + + expect($input.attr('aria-describedby')) + .toMatch(hintId) + }) }) describe('when it includes an error message', () => { @@ -166,6 +199,28 @@ describe('Input', () => { .toMatch(errorMessageId) }) + it('associates the input as "described by" the error message and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('input', { + id: 'input-with-error', + describedBy: describedById, + errorMessage: { + text: 'Error message' + } + }) + + const $input = $('.govuk-input') + const $errorMessage = $('.govuk-error-message') + + const errorMessageId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $errorMessage.attr('id') + WORD_BOUNDARY + ) + + expect($input.attr('aria-describedby')) + .toMatch(errorMessageId) + }) + it('includes the error class on the input', () => { const $ = render('input', { errorMessage: { @@ -201,11 +256,36 @@ describe('Input', () => { }) const $component = $('.govuk-input') - const $errorMessageId = $('.govuk-error-message').attr('id') - const $hintId = $('.govuk-hint').attr('id') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') + + const combinedIds = new RegExp( + WORD_BOUNDARY + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY + ) + + expect($component.attr('aria-describedby')) + .toMatch(combinedIds) + }) + + it('associates the input as described by the hint, error message and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('input', { + describedBy: describedById, + errorMessage: { + text: 'Error message' + }, + hint: { + text: 'Hint' + } + }) + + const $component = $('.govuk-input') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') const combinedIds = new RegExp( - WORD_BOUNDARY + $hintId + WHITESPACE + $errorMessageId + WORD_BOUNDARY + WORD_BOUNDARY + describedById + WHITESPACE + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY ) expect($component.attr('aria-describedby')) diff --git a/src/components/radios/radios.yaml b/src/components/radios/radios.yaml index f001ef2830..02ea3765ee 100644 --- a/src/components/radios/radios.yaml +++ b/src/components/radios/radios.yaml @@ -242,6 +242,22 @@ examples: - value: blue text: Blue +- name: with fieldset and error message + data: + idPrefix: example + name: example + errorMessage: + text: Please select an option + fieldset: + legend: + text: Have you changed your name? + items: + - value: yes + text: Yes + - value: no + text: No + checked: true + - name: with all fieldset attributes data: idPrefix: example diff --git a/src/components/radios/template.njk b/src/components/radios/template.njk index 176a61bc4e..5ef8b832c1 100644 --- a/src/components/radios/template.njk +++ b/src/components/radios/template.njk @@ -9,7 +9,7 @@ {#- a record of other elements that we need to associate with the input using aria-describedby – for example hints or error messages -#} -{% set describedBy = "" %} +{% set describedBy = params.fieldset.describedBy if params.fieldset.describedBy else "" %} {% set isConditional = false %} {% for item in params.items %} diff --git a/src/components/radios/template.test.js b/src/components/radios/template.test.js index 8cf313475d..45962c9921 100644 --- a/src/components/radios/template.test.js +++ b/src/components/radios/template.test.js @@ -71,6 +71,30 @@ describe('Radios', () => { expect($component.hasClass('app-radios--custom-modifier')).toBeTruthy() }) + it('renders initial aria-describedby on fieldset', () => { + const describedById = 'some-id' + + const $ = render('radios', { + name: 'example-name', + fieldset: { + describedBy: describedById + }, + items: [ + { + value: 'yes', + text: 'Yes' + }, + { + value: 'no', + text: 'No' + } + ] + }) + + const $fieldset = $('.govuk-fieldset') + expect($fieldset.attr('aria-describedby')).toMatch(describedById) + }) + it('render attributes', () => { const $ = render('radios', { name: 'example-name', @@ -462,6 +486,24 @@ describe('Radios', () => { expect($fieldset.attr('aria-describedby')).toMatch(hintId) }) + + it('associates the fieldset as "described by" the hint and parent fieldset', () => { + const describedById = 'some-id' + const params = examples['with all fieldset attributes'] + + params.fieldset.describedBy = describedById + + const $ = render('radios', params) + const $fieldset = $('.govuk-fieldset') + const $hint = $('.govuk-hint') + + const hintId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $hint.attr('id') + WORD_BOUNDARY + ) + + expect($fieldset.attr('aria-describedby')).toMatch(hintId) + delete params.fieldset.describedBy + }) }) describe('when they include an error message', () => { @@ -510,7 +552,7 @@ describe('Radios', () => { }) it('associates the fieldset as "described by" the error message', () => { - const $ = render('radios', examples['with all fieldset attributes']) + const $ = render('radios', examples['with fieldset and error message']) const $fieldset = $('.govuk-fieldset') const $errorMessage = $('.govuk-error-message') @@ -523,6 +565,27 @@ describe('Radios', () => { .toMatch(errorMessageId) }) + it('associates the fieldset as "described by" the error message and parent fieldset', () => { + const describedById = 'some-id' + const params = examples['with fieldset and error message'] + + params.fieldset.describedBy = describedById + + const $ = render('radios', params) + + const $fieldset = $('.govuk-fieldset') + const $errorMessage = $('.govuk-error-message') + + const errorMessageId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $errorMessage.attr('id') + WORD_BOUNDARY + ) + + expect($fieldset.attr('aria-describedby')) + .toMatch(errorMessageId) + + delete params.fieldset.describedBy + }) + it('renders with a form group wrapper that has an error state', () => { const $ = render('radios', { errorMessage: { @@ -540,15 +603,37 @@ describe('Radios', () => { const $ = render('radios', examples['with all fieldset attributes']) const $fieldset = $('.govuk-fieldset') - const $errorMessageId = $('.govuk-error-message').attr('id') - const $hintId = $('.govuk-hint').attr('id') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') + + const combinedIds = new RegExp( + WORD_BOUNDARY + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY + ) + + expect($fieldset.attr('aria-describedby')) + .toMatch(combinedIds) + }) + + it('associates the fieldset as described by the hint, error message and parent fieldset', () => { + const describedById = 'some-id' + const params = examples['with all fieldset attributes'] + + params.fieldset.describedBy = describedById + + const $ = render('radios', params) + + const $fieldset = $('.govuk-fieldset') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') const combinedIds = new RegExp( - WORD_BOUNDARY + $hintId + WHITESPACE + $errorMessageId + WORD_BOUNDARY + WORD_BOUNDARY + describedById + WHITESPACE + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY ) expect($fieldset.attr('aria-describedby')) .toMatch(combinedIds) + + delete params.fieldset.describedBy }) }) diff --git a/src/components/select/select.yaml b/src/components/select/select.yaml index d539cc2a6d..d4298a0a02 100644 --- a/src/components/select/select.yaml +++ b/src/components/select/select.yaml @@ -32,6 +32,10 @@ params: type: object required: false description: HTML attributes (for example data attributes) to add to the option. +- name: describedBy + type: string + required: false + description: One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users. - name: label type: object required: false diff --git a/src/components/select/template.njk b/src/components/select/template.njk index bffc5ec137..10c919278f 100644 --- a/src/components/select/template.njk +++ b/src/components/select/template.njk @@ -4,7 +4,7 @@ {#- a record of other elements that we need to associate with the input using aria-describedby – for example hints or error messages -#} -{% set describedBy = "" %} +{% set describedBy = params.describedBy if params.describedBy else "" %}
{{ govukLabel({ html: params.label.html, diff --git a/src/components/select/template.test.js b/src/components/select/template.test.js index 7ac482722a..97baff4f12 100644 --- a/src/components/select/template.test.js +++ b/src/components/select/template.test.js @@ -135,6 +135,17 @@ describe('Select', () => { expect($firstItem.attr('disabled')).toBeTruthy() }) + it('renders with aria-describedby', () => { + const describedById = 'some-id' + + const $ = render('select', { + describedBy: describedById + }) + + const $component = $('.govuk-select') + expect($component.attr('aria-describedby')).toMatch(describedById) + }) + it('renders with attributes', () => { const $ = render('select', { attributes: { @@ -229,6 +240,28 @@ describe('Select', () => { expect($select.attr('aria-describedby')) .toMatch(hintId) }) + + it('associates the select as "described by" the hint and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('select', { + id: 'select-with-hint', + describedBy: describedById, + hint: { + 'text': 'Hint text goes here' + } + }) + + const $select = $('.govuk-select') + const $hint = $('.govuk-hint') + + const hintId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $hint.attr('id') + WORD_BOUNDARY + ) + + expect($select.attr('aria-describedby')) + .toMatch(hintId) + }) }) describe('when it includes an error message', () => { @@ -262,6 +295,28 @@ describe('Select', () => { .toMatch(errorMessageId) }) + it('associates the select as "described by" the error message and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('select', { + id: 'select-with-error', + describedBy: describedById, + errorMessage: { + 'text': 'Error message' + } + }) + + const $input = $('.govuk-select') + const $errorMessage = $('.govuk-error-message') + + const errorMessageId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $errorMessage.attr('id') + WORD_BOUNDARY + ) + + expect($input.attr('aria-describedby')) + .toMatch(errorMessageId) + }) + it('adds the error class to the select', () => { const $ = render('select', { errorMessage: { @@ -297,11 +352,36 @@ describe('Select', () => { }) const $component = $('.govuk-select') - const $errorMessageId = $('.govuk-error-message').attr('id') - const $hintId = $('.govuk-hint').attr('id') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') + + const combinedIds = new RegExp( + WORD_BOUNDARY + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY + ) + + expect($component.attr('aria-describedby')) + .toMatch(combinedIds) + }) + + it('associates the select as described by the hint, error message and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('select', { + describedBy: describedById, + errorMessage: { + text: 'Error message' + }, + hint: { + text: 'Hint' + } + }) + + const $component = $('.govuk-select') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') const combinedIds = new RegExp( - WORD_BOUNDARY + $hintId + WHITESPACE + $errorMessageId + WORD_BOUNDARY + WORD_BOUNDARY + describedById + WHITESPACE + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY ) expect($component.attr('aria-describedby')) diff --git a/src/components/textarea/template.njk b/src/components/textarea/template.njk index 47cd4d45bb..56d9cc3333 100644 --- a/src/components/textarea/template.njk +++ b/src/components/textarea/template.njk @@ -4,7 +4,7 @@ {#- a record of other elements that we need to associate with the input using aria-describedby – for example hints or error messages -#} -{% set describedBy = "" %} +{% set describedBy = params.describedBy if params.describedBy else "" %}
{{ govukLabel({ html: params.label.html, diff --git a/src/components/textarea/template.test.js b/src/components/textarea/template.test.js index 2f2747b574..7fe58fd4d5 100644 --- a/src/components/textarea/template.test.js +++ b/src/components/textarea/template.test.js @@ -48,6 +48,17 @@ describe('Textarea', () => { expect($component.attr('name')).toEqual('my-textarea-name') }) + it('renders with aria-describedby', () => { + const describedById = 'some-id' + + const $ = render('textarea', { + describedBy: describedById + }) + + const $component = $('.govuk-textarea') + expect($component.attr('aria-describedby')).toMatch(describedById) + }) + it('renders with rows', () => { const $ = render('textarea', { rows: '4' @@ -122,6 +133,28 @@ describe('Textarea', () => { expect($textarea.attr('aria-describedby')) .toMatch(hintId) }) + + it('associates the textarea as "described by" the hint and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('textarea', { + id: 'textarea-with-error', + describedBy: describedById, + hint: { + 'text': 'It’s on your National Insurance card, benefit letter, payslip or P60. For example, β€˜QQ 12 34 56 C’.' + } + }) + + const $textarea = $('.govuk-textarea') + const $hint = $('.govuk-hint') + + const hintId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $hint.attr('id') + WORD_BOUNDARY + ) + + expect($textarea.attr('aria-describedby')) + .toMatch(hintId) + }) }) describe('when it includes an error message', () => { @@ -155,6 +188,28 @@ describe('Textarea', () => { .toMatch(errorMessageId) }) + it('associates the textarea as "described by" the error message and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('textarea', { + id: 'textarea-with-error', + describedBy: describedById, + errorMessage: { + 'text': 'Error message' + } + }) + + const $component = $('.govuk-textarea') + const $errorMessage = $('.govuk-error-message') + + const errorMessageId = new RegExp( + WORD_BOUNDARY + describedById + WHITESPACE + $errorMessage.attr('id') + WORD_BOUNDARY + ) + + expect($component.attr('aria-describedby')) + .toMatch(errorMessageId) + }) + it('adds the error class to the textarea', () => { const $ = render('textarea', { errorMessage: { @@ -201,11 +256,36 @@ describe('Textarea', () => { }) const $component = $('.govuk-textarea') - const $errorMessageId = $('.govuk-error-message').attr('id') - const $hintId = $('.govuk-hint').attr('id') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') + + const combinedIds = new RegExp( + WORD_BOUNDARY + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY + ) + + expect($component.attr('aria-describedby')) + .toMatch(combinedIds) + }) + + it('associates the textarea as described by the hint, error message and parent fieldset', () => { + const describedById = 'some-id' + + const $ = render('textarea', { + describedBy: describedById, + errorMessage: { + 'text': 'Error message' + }, + hint: { + 'text': 'Hint' + } + }) + + const $component = $('.govuk-textarea') + const errorMessageId = $('.govuk-error-message').attr('id') + const hintId = $('.govuk-hint').attr('id') const combinedIds = new RegExp( - WORD_BOUNDARY + $hintId + WHITESPACE + $errorMessageId + WORD_BOUNDARY + WORD_BOUNDARY + describedById + WHITESPACE + hintId + WHITESPACE + errorMessageId + WORD_BOUNDARY ) expect($component.attr('aria-describedby')) diff --git a/src/components/textarea/textarea.yaml b/src/components/textarea/textarea.yaml index a90aa3dcbd..6f3c950a7f 100644 --- a/src/components/textarea/textarea.yaml +++ b/src/components/textarea/textarea.yaml @@ -15,6 +15,10 @@ params: type: string required: false description: Optional initial value of the textarea. +- name: describedBy + type: string + required: false + description: One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users. - name: label type: object required: true