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..ea900614e4 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
})
})
@@ -664,6 +749,21 @@ describe('Checkboxes', () => {
const $input = $('input')
expect($input.attr('aria-describedby')).toMatch('t-and-c-error')
})
+
+ it('adds aria-describedby to input if there is an error and parent fieldset', () => {
+ const describedById = 'some-id'
+ const params = examples["with single option set 'aria-describedby' on input"]
+
+ params.describedBy = describedById
+
+ const $ = render('checkboxes', params)
+ const $input = $('input')
+
+ expect($input.attr('aria-describedby'))
+ .toMatch(`${describedById} t-and-c-error`)
+
+ delete params.describedBy
+ })
})
describe('single checkbox (with hint) without a fieldset', () => {
@@ -672,5 +772,18 @@ describe('Checkboxes', () => {
const $input = $('input')
expect($input.attr('aria-describedby')).toMatch('t-and-c-with-hint-error t-and-c-with-hint-1-item-hint')
})
+
+ it('adds aria-describedby to input if there is an error, hint and parent fieldset', () => {
+ const describedById = 'some-id'
+ const params = examples["with single option (and hint) set 'aria-describedby' on input"]
+
+ params.describedBy = describedById
+
+ const $ = render('checkboxes', params)
+ const $input = $('input')
+
+ expect($input.attr('aria-describedby'))
+ .toMatch(`${describedById} t-and-c-with-hint-error t-and-c-with-hint-1-item-hint`)
+ })
})
})
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