From 4abfcd8657ac19eca5e3ca4045046a2fe23330d5 Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Mon, 22 Feb 2021 11:23:42 +0000 Subject: [PATCH 1/5] Add divider to checkboxes --- src/govuk/components/checkboxes/_index.scss | 13 ++++ .../components/checkboxes/checkboxes.yaml | 4 ++ src/govuk/components/checkboxes/template.njk | 66 ++++++++++--------- 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/govuk/components/checkboxes/_index.scss b/src/govuk/components/checkboxes/_index.scss index 99a1fd097b..fe72b91a4f 100644 --- a/src/govuk/components/checkboxes/_index.scss +++ b/src/govuk/components/checkboxes/_index.scss @@ -138,6 +138,19 @@ opacity: .5; } + // ========================================================= + // Dividers ('or') + // ========================================================= + + .govuk-checkboxes__divider { + $govuk-divider-size: $govuk-checkboxes-size !default; + @include govuk-font($size: 19); + @include govuk-text-colour; + width: $govuk-divider-size; + margin-bottom: govuk-spacing(2); + text-align: center; + } + // ========================================================= // Conditional reveals // ========================================================= diff --git a/src/govuk/components/checkboxes/checkboxes.yaml b/src/govuk/components/checkboxes/checkboxes.yaml index 602f36a09d..56cb6f918b 100644 --- a/src/govuk/components/checkboxes/checkboxes.yaml +++ b/src/govuk/components/checkboxes/checkboxes.yaml @@ -70,6 +70,10 @@ params: required: false description: Provide hint to each checkbox item. isComponent: true + - name: divider + type: string + required: false + description: Divider text to separate checkbox items, for example the text "or". - name: checked type: boolean required: false diff --git a/src/govuk/components/checkboxes/template.njk b/src/govuk/components/checkboxes/template.njk index 6337207509..181b1b0059 100644 --- a/src/govuk/components/checkboxes/template.njk +++ b/src/govuk/components/checkboxes/template.njk @@ -67,38 +67,42 @@ {%- endif -%} {% set name = item.name if item.name else params.name %} {% set conditionalId = "conditional-" + id %} - {% set hasHint = true if item.hint.text or item.hint.html %} - {% set itemHintId = id + "-item-hint" if hasHint else "" %} - {% set itemDescribedBy = describedBy if not hasFieldset else "" %} - {% set itemDescribedBy = (itemDescribedBy + " " + itemHintId) | trim %} -
- - {{ govukLabel({ - html: item.html, - text: item.text, - classes: 'govuk-checkboxes__label' + (' ' + item.label.classes if item.label.classes), - attributes: item.label.attributes, - for: id - }) | indent(6) | trim }} - {% if hasHint %} - {{ govukHint({ - id: itemHintId, - classes: 'govuk-checkboxes__hint' + (' ' + item.hint.classes if item.hint.classes), - attributes: item.hint.attributes, - html: item.hint.html, - text: item.hint.text - }) | indent(6) | trim }} - {% endif %} -
- {% if item.conditional.html %} -
- {{ item.conditional.html | safe }} + {%- if item.divider %} +
{{ item.divider }}
+ {%- else %} + {% set hasHint = true if item.hint.text or item.hint.html %} + {% set itemHintId = id + "-item-hint" if hasHint else "" %} + {% set itemDescribedBy = describedBy if not hasFieldset else "" %} + {% set itemDescribedBy = (itemDescribedBy + " " + itemHintId) | trim %} +
+ + {{ govukLabel({ + html: item.html, + text: item.text, + classes: 'govuk-checkboxes__label' + (' ' + item.label.classes if item.label.classes), + attributes: item.label.attributes, + for: id + }) | indent(6) | trim }} + {% if hasHint %} + {{ govukHint({ + id: itemHintId, + classes: 'govuk-checkboxes__hint' + (' ' + item.hint.classes if item.hint.classes), + attributes: item.hint.attributes, + html: item.hint.html, + text: item.hint.text + }) | indent(6) | trim }} + {% endif %}
+ {% if item.conditional.html %} +
+ {{ item.conditional.html | safe }} +
+ {% endif %} {% endif %} {% endif %} {% endfor %} From 7e746b4670e10349a3e678f4bbd6e7d85e8b771d Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Tue, 30 Mar 2021 21:05:51 +0100 Subject: [PATCH 2/5] Add data attribute to HTML for behaviour, and always insert data-module attribute. This fixes a bug/edge-case where if you use two separate calls to the macro within the same `
` and specify the same `name` attribute value for both sets of checkboxes, but only one of them has the `exclusive` behaviour or the conditional content, then checking a checkbox in one list would not uncheck the "None" checkbox in the other list, as there was no `eventListener` set up. Always initialising the javascript for every set of checkboxes solves this. This does introduce a small performance penalty of potentially initialising javascript when it's not needed, but this should be negligible and non-blocking. --- src/govuk/components/checkboxes/template.njk | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/govuk/components/checkboxes/template.njk b/src/govuk/components/checkboxes/template.njk index 181b1b0059..d87bbb9ffb 100644 --- a/src/govuk/components/checkboxes/template.njk +++ b/src/govuk/components/checkboxes/template.njk @@ -14,13 +14,6 @@ {% set describedBy = params.fieldset.describedBy %} {% endif %} -{% set isConditional = false %} -{% for item in params.items %} - {% if item.conditional.html %} - {% set isConditional = true %} - {% endif %} -{% endfor %} - {#- fieldset is false by default -#} {% set hasFieldset = true if params.fieldset else false %} @@ -51,7 +44,7 @@ {% endif %}
+ data-module="govuk-checkboxes"> {% for item in params.items %} {% if item %} {#- If the user explicitly sets an id, use this instead of the regular idPrefix -#} @@ -79,6 +72,7 @@ {{-" checked" if item.checked }} {{-" disabled" if item.disabled }} {%- if item.conditional.html %} data-aria-controls="{{ conditionalId }}"{% endif -%} + {%- if item.behaviour %} data-behaviour="{{ item.behaviour }}"{% endif -%} {%- if itemDescribedBy %} aria-describedby="{{ itemDescribedBy }}"{% endif -%} {%- for attribute, value in item.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}> {{ govukLabel({ From fad5263cc54941acc708e495d6cb72006c3c2640 Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Tue, 30 Mar 2021 21:06:09 +0100 Subject: [PATCH 3/5] Add documentation for new feature --- .../components/checkboxes/checkboxes.yaml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/govuk/components/checkboxes/checkboxes.yaml b/src/govuk/components/checkboxes/checkboxes.yaml index 56cb6f918b..e6291a6679 100644 --- a/src/govuk/components/checkboxes/checkboxes.yaml +++ b/src/govuk/components/checkboxes/checkboxes.yaml @@ -86,6 +86,10 @@ params: type: string required: false description: Provide content for the conditional reveal. + - name: behaviour + type: string + required: false + description: If set to `exclusive`, implements a "None of these" type behaviour via javascript when checkboxes are clicked - name: disabled type: boolean required: false @@ -125,6 +129,26 @@ examples: - value: other text: Citizen of another country +- name: with divider and None + data: + name: with-divider-and-none + fieldset: + legend: + text: Which types of waste do you transport regularly? + classes: govuk-fieldset__legend--l + isPageHeading: true + items: + - value: animal + text: Waste from animal carcasses + - value: mines + text: Waste from mines or quarries + - value: farm + text: Farm or agricultural waste + - divider: or + - value: none + text: None of these + behaviour: exclusive + - name: with id and name data: name: with-id-and-name From 90d7f266b9648869949167383d5769c10ea95bee Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Tue, 30 Mar 2021 21:06:40 +0100 Subject: [PATCH 4/5] Add javascript behaviour and tests --- src/govuk/components/checkboxes/checkboxes.js | 64 ++++++++++++++++- .../components/checkboxes/checkboxes.test.js | 69 +++++++++++++++++++ .../components/checkboxes/checkboxes.yaml | 28 +++++++- 3 files changed, 157 insertions(+), 4 deletions(-) diff --git a/src/govuk/components/checkboxes/checkboxes.js b/src/govuk/components/checkboxes/checkboxes.js index 4c9858d388..ebe086334d 100644 --- a/src/govuk/components/checkboxes/checkboxes.js +++ b/src/govuk/components/checkboxes/checkboxes.js @@ -86,6 +86,47 @@ Checkboxes.prototype.syncConditionalRevealWithInputState = function ($input) { } } +/** + * Uncheck other checkboxes + * + * Find any other checkbox inputs with the same name value, and uncheck them. + * This is useful for when a “None of these" checkbox is checked. + */ +Checkboxes.prototype.unCheckAllInputsExcept = function ($input) { + var allInputsWithSameName = document.querySelectorAll('input[type="checkbox"][name="' + $input.name + '"]') + + nodeListForEach(allInputsWithSameName, function ($inputWithSameName) { + var hasSameFormOwner = ($input.form === $inputWithSameName.form) + if (hasSameFormOwner && $inputWithSameName !== $input) { + $inputWithSameName.checked = false + } + }) + + this.syncAllConditionalReveals() +} + +/** + * Uncheck exclusive inputs + * + * Find any checkbox inputs with the same name value and the 'exclusive' behaviour, + * and uncheck them. This helps prevent someone checking both a regular checkbox and a + * "None of these" checkbox in the same fieldset. + */ +Checkboxes.prototype.unCheckExclusiveInputs = function ($input) { + var allInputsWithSameNameAndExclusiveBehaviour = document.querySelectorAll( + 'input[data-behaviour="exclusive"][type="checkbox"][name="' + $input.name + '"]' + ) + + nodeListForEach(allInputsWithSameNameAndExclusiveBehaviour, function ($exclusiveInput) { + var hasSameFormOwner = ($input.form === $exclusiveInput.form) + if (hasSameFormOwner) { + $exclusiveInput.checked = false + } + }) + + this.syncAllConditionalReveals() +} + /** * Click event handler * @@ -97,12 +138,29 @@ Checkboxes.prototype.syncConditionalRevealWithInputState = function ($input) { Checkboxes.prototype.handleClick = function (event) { var $target = event.target - // If a checkbox with aria-controls, handle click - var isCheckbox = $target.getAttribute('type') === 'checkbox' + // Ignore clicks on things that aren't checkbox inputs + if ($target.type !== 'checkbox') { + return + } + + // If the checkbox conditionally-reveals some content, sync the state var hasAriaControls = $target.getAttribute('aria-controls') - if (isCheckbox && hasAriaControls) { + if (hasAriaControls) { this.syncConditionalRevealWithInputState($target) } + + // No further behaviour needed for unchecking + if (!$target.checked) { + return + } + + // Handle 'exclusive' checkbox behaviour (ie "None of these") + var hasBehaviourExclusive = ($target.getAttribute('data-behaviour') === 'exclusive') + if (hasBehaviourExclusive) { + this.unCheckAllInputsExcept($target) + } else { + this.unCheckExclusiveInputs($target) + } } export default Checkboxes diff --git a/src/govuk/components/checkboxes/checkboxes.test.js b/src/govuk/components/checkboxes/checkboxes.test.js index 853b6ccace..3d5f9d2c8a 100644 --- a/src/govuk/components/checkboxes/checkboxes.test.js +++ b/src/govuk/components/checkboxes/checkboxes.test.js @@ -121,3 +121,72 @@ describe('Checkboxes with conditional reveals', () => { }) }) }) + +describe('Checkboxes with a None checkbox', () => { + describe('when JavaScript is available', () => { + it('unchecks other checkboxes when the None checkbox is checked', async () => { + await goToAndGetComponent('checkboxes', 'with-divider-and-None') + + // Check the first 3 checkboxes + await page.click('#with-divider-and-none') + await page.click('#with-divider-and-none-2') + await page.click('#with-divider-and-none-3') + + // Check the None checkbox + await page.click('#with-divider-and-none-5') + + // Expect first 3 checkboxes to have been unchecked + const firstCheckboxIsUnchecked = await waitForVisibleSelector('[id="with-divider-and-none"]:not(:checked)') + expect(firstCheckboxIsUnchecked).toBeTruthy() + + const secondCheckboxIsUnchecked = await waitForVisibleSelector('[id="with-divider-and-none-2"]:not(:checked)') + expect(secondCheckboxIsUnchecked).toBeTruthy() + + const thirdCheckboxIsUnchecked = await waitForVisibleSelector('[id="with-divider-and-none-3"]:not(:checked)') + expect(thirdCheckboxIsUnchecked).toBeTruthy() + }) + + it('unchecks the None checkbox when any other checkbox is checked', async () => { + await goToAndGetComponent('checkboxes', 'with-divider-and-None') + + // Check the None checkbox + await page.click('#with-divider-and-none-5') + + // Check the first checkbox + await page.click('#with-divider-and-none') + + // Expect the None checkbox to have been unchecked + const noneCheckboxIsUnchecked = await waitForVisibleSelector('[id="with-divider-and-none-5"]:not(:checked)') + expect(noneCheckboxIsUnchecked).toBeTruthy() + }) + }) +}) + +describe('Checkboxes with a None checkbox and conditional reveals', () => { + describe('when JavaScript is available', () => { + it('unchecks other checkboxes and hides conditional reveals when the None checkbox is checked', async () => { + const $ = await goToAndGetComponent('checkboxes', 'with-divider,-None-and-conditional-items') + + // Check the 4th checkbox, which reveals an additional field + await page.click('#with-divider-and-none-and-conditional-items-4') + + const $checkedInput = $('#with-divider-and-none-and-conditional-items-4') + const conditionalContentId = $checkedInput.attr('aria-controls') + + // Expect conditional content to have been revealed + const isConditionalContentVisible = await waitForVisibleSelector(`[id="${conditionalContentId}"]`) + expect(isConditionalContentVisible).toBeTruthy() + + // Check the None checkbox + await page.click('#with-divider-and-none-and-conditional-items-6') + + // Expect the 4th checkbox to have been unchecked + const forthCheckboxIsUnchecked = await waitForVisibleSelector('[id="with-divider-and-none-and-conditional-items-4"]:not(:checked)') + expect(forthCheckboxIsUnchecked).toBeTruthy() + + // Expect conditional content to have been hidden + const isConditionalContentHidden = await waitForHiddenSelector(`[id="${conditionalContentId}"]`) + expect(isConditionalContentHidden).toBeTruthy() + }) + }) +}) diff --git a/src/govuk/components/checkboxes/checkboxes.yaml b/src/govuk/components/checkboxes/checkboxes.yaml index e6291a6679..05a9d8e53f 100644 --- a/src/govuk/components/checkboxes/checkboxes.yaml +++ b/src/govuk/components/checkboxes/checkboxes.yaml @@ -73,7 +73,7 @@ params: - name: divider type: string required: false - description: Divider text to separate checkbox items, for example the text "or". + description: Divider text to separate checkbox items, for example the text "or". - name: checked type: boolean required: false @@ -149,6 +149,32 @@ examples: text: None of these behaviour: exclusive +- name: with divider, None and conditional items + data: + name: with-divider-and-none-and-conditional-items + fieldset: + legend: + text: Do you have any access needs? + classes: govuk-fieldset__legend--l + isPageHeading: true + items: + - value: accessible-toilets + text: Accessible toilets available + - value: braille + text: Braille translation service available + - value: disabled-car-parking + text: Disabled car parking available + - value: another-access-need + text: Another access need + conditional: + html: | + + + - divider: or + - value: none + text: None of these + behaviour: exclusive + - name: with id and name data: name: with-id-and-name From 1042a8e182a87a9616c366c1caefa9f7bd7af233 Mon Sep 17 00:00:00 2001 From: Frankie Roberto Date: Wed, 7 Apr 2021 12:26:41 +0100 Subject: [PATCH 5/5] Add template tests --- src/govuk/components/checkboxes/template.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/govuk/components/checkboxes/template.test.js b/src/govuk/components/checkboxes/template.test.js index 77af6c47df..786b5b30bf 100644 --- a/src/govuk/components/checkboxes/template.test.js +++ b/src/govuk/components/checkboxes/template.test.js @@ -47,6 +47,21 @@ describe('Checkboxes', () => { expect($items.length).toEqual(2) }) + it('render example with a divider and ‘None’ checkbox with exclusive behaviour', () => { + const $ = render('checkboxes', examples['with divider and None']) + + const $component = $('.govuk-checkboxes') + + const $divider = $component.find('.govuk-checkboxes__divider').first() + expect($divider.text().trim()).toEqual('or') + + const $items = $component.find('.govuk-checkboxes__item') + expect($items.length).toEqual(4) + + const $orItemInput = $items.last().find('input').first() + expect($orItemInput.attr('data-behaviour')).toEqual('exclusive') + }) + it('render additional label classes', () => { const $ = render('checkboxes', examples['with label classes'])