From 4f91d117a10c31ad66e024e52331be51b8136591 Mon Sep 17 00:00:00 2001 From: Andy Mantell <134642+andymantell@users.noreply.github.com> Date: Fri, 23 Jul 2021 13:34:06 +0100 Subject: [PATCH] Add "None of these" JavaScript behaviour to checkboxes See original at https://github.com/alphagov/govuk-frontend/pull/2151 --- .../checkboxes/none-of-the-above.njk | 98 +++++++++++ app/pages/examples.njk | 1 + packages/common.js | 9 +- packages/components/checkboxes/README.md | 161 ++++++++++++++++++ packages/components/checkboxes/checkboxes.js | 57 +++++++ packages/components/checkboxes/template.njk | 1 + 6 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 app/components/checkboxes/none-of-the-above.njk diff --git a/app/components/checkboxes/none-of-the-above.njk b/app/components/checkboxes/none-of-the-above.njk new file mode 100644 index 000000000..c9ca22bc6 --- /dev/null +++ b/app/components/checkboxes/none-of-the-above.njk @@ -0,0 +1,98 @@ +{% set html_style = 'background-color: #f0f4f5;' %} +{% set title = 'Checkboxes with conditional content' %} +{% from 'components/checkboxes/macro.njk' import checkboxes %} +{% from 'components/input/macro.njk' import input %} +{% extends 'layout.njk' %} + +{% set emailHtml %} + {{ input({ + id: "email", + name: "email", + classes: "nhsuk-u-width-two-thirds", + label: { + text: "Email address" + } + }) }} +{% endset -%} + +{% set phoneHtml %} + {{ input({ + id: "phone", + name: "phone", + classes: "nhsuk-u-width-two-thirds", + label: { + text: "Phone number" + } + }) }} +{% endset -%} + +{% set mobileHtml %} + {{ input({ + id: "mobile", + name: "mobile", + classes: "nhsuk-u-width-two-thirds", + label: { + text: "Mobile phone number" + } + }) }} +{% endset -%} + +{% block body %} + +
+
+ +
+
+ {{ checkboxes({ + "idPrefix": "contact", + "name": "contact", + "fieldset": { + "legend": { + "text": "How would you prefer to be contacted?", + "classes": "nhsuk-fieldset__legend--l", + "isPageHeading": "true" + } + }, + "hint": { + "text": "Select all options that are relevant to you." + }, + "items": [ + { + "value": "email", + "text": "Email", + "conditional": { + "html": emailHtml + } + }, + { + "value": "phone", + "text": "Phone", + "conditional": { + "html": phoneHtml + } + }, + { + "value": "text", + "text": "Text message", + "conditional": { + "html": mobileHtml + } + }, + { + divider: "or" + }, + { + value: "none", + text: "None of the above", + behaviour: "exclusive" + } + ] + }) }} +
+
+ +
+
+ +{% endblock %} diff --git a/app/pages/examples.njk b/app/pages/examples.njk index 3b1e4d30c..9f5757238 100644 --- a/app/pages/examples.njk +++ b/app/pages/examples.njk @@ -42,6 +42,7 @@
  • Checkboxes with legend as page heading
  • Checkboxes with error message
  • Checkboxes with conditional content
  • +
  • Checkboxes with "none of the above" option
  • Contents list
  • Date input
  • Date input with autocomplete attribute
  • diff --git a/packages/common.js b/packages/common.js index 0da099e3d..0fe6a6691 100644 --- a/packages/common.js +++ b/packages/common.js @@ -25,8 +25,13 @@ export const toggleConditionalInput = (input, className) => { // Get the conditional element from the input data-aria-controls attribute const conditionalElement = document.getElementById(conditionalId); if (conditionalElement) { - conditionalElement.classList.toggle(className); - toggleAttribute(input, 'aria-expanded'); + if (input.checked) { + conditionalElement.classList.remove(className); + input.setAttribute('aria-expanded', false); + } else { + conditionalElement.classList.add(className); + input.setAttribute('aria-expanded', true); + } } } }; diff --git a/packages/components/checkboxes/README.md b/packages/components/checkboxes/README.md index 606344447..27e6fb1a1 100644 --- a/packages/components/checkboxes/README.md +++ b/packages/components/checkboxes/README.md @@ -508,6 +508,166 @@ Find out more about the checkboxes component and when to use it in the [NHS digi --- +### Checkboxes with "none of the above" option + +[Preview the checkboxes with "none of the above" option](https://nhsuk.github.io/nhsuk-frontend/components/checkboxes/conditional.html) + +#### HTML markup + +```html +
    +
    + +

    + How would you prefer to be contacted? +

    +
    +
    + Select all options that are relevant to you. +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    or
    +
    + + +
    +
    +
    +
    +``` + +#### Nunjucks macro + +``` +{% from 'components/checkboxes/macro.njk' import checkboxes %} +{% from 'components/input/macro.njk' import input %} + +{% set emailHtml %} + {{ input({ + id: "email", + name: "email", + classes: "nhsuk-u-width-two-thirds", + label: { + text: "Email address" + } + }) }} +{% endset -%} + +{% set phoneHtml %} + {{ input({ + id: "phone", + name: "phone", + classes: "nhsuk-u-width-two-thirds", + label: { + text: "Phone number" + } + }) }} +{% endset -%} + +{% set mobileHtml %} + {{ input({ + id: "mobile", + name: "mobile", + classes: "nhsuk-u-width-two-thirds", + label: { + text: "Mobile phone number" + } + }) }} +{% endset -%} + +{{ checkboxes({ + "idPrefix": "contact", + "name": "contact", + "fieldset": { + "legend": { + "text": "How would you prefer to be contacted?", + "classes": "nhsuk-fieldset__legend--l", + "isPageHeading": "true" + } + }, + "hint": { + "text": "Select all options that are relevant to you." + }, + "items": [ + { + "value": "email", + "text": "Email", + "conditional": { + "html": emailHtml + } + }, + { + "value": "phone", + "text": "Phone", + "conditional": { + "html": phoneHtml + } + }, + { + "value": "text", + "text": "Text message", + "conditional": { + "html": mobileHtml + } + }, + { + divider: "or" + }, + { + value: "none", + text: "None of the above", + behaviour: "exclusive" + } + ] +}) }} +``` + +--- + ### Nunjucks arguments The checkboxes Nunjucks macro takes the following arguments: @@ -534,6 +694,7 @@ The checkboxes Nunjucks macro takes the following arguments: | **items[].conditional.html** | string | No | HTML to be displayed when the checkbox is checked | | **classes** | string | No | Optional additional classes to add to the checkboxes container. Separate each class with a space. | | **attributes** | object | No | Any extra HTML attributes (for example data attributes) to add to the checkboxes container. | +| **behaviour** | string | No | If set to `exclusive`, implements a "None of these" type behaviour via javascript when checkboxes are clicked | If you are using Nunjucks macros in production be aware that using `html` arguments, or ones ending with `html` can be a [security risk](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting). Read more about this in the [Nunjucks documentation](https://mozilla.github.io/nunjucks/api.html#user-defined-templates-warning). diff --git a/packages/components/checkboxes/checkboxes.js b/packages/components/checkboxes/checkboxes.js index ece7acc18..6df3ebce3 100644 --- a/packages/components/checkboxes/checkboxes.js +++ b/packages/components/checkboxes/checkboxes.js @@ -5,6 +5,52 @@ import { toggleConditionalInput } from '../../common'; * Test at http://0.0.0.0:3000/components/checkboxes/conditional.html */ +/** + * 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. + */ + const unCheckAllInputsExcept = function (input) { + const allInputsWithSameName = document.querySelectorAll('input[type="checkbox"][name="' + input.name + '"]') + + allInputsWithSameName.forEach((inputWithSameName) => { + const hasSameFormOwner = input.form === inputWithSameName.form + if (hasSameFormOwner && inputWithSameName !== input) { + inputWithSameName.checked = false + } + }) + + syncAllConditionalReveals(input) +} + +/** + * 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. + */ +const unCheckExclusiveInputs = function (input) { + const allInputsWithSameNameAndExclusiveBehaviour = document.querySelectorAll( + 'input[data-behaviour="exclusive"][type="checkbox"][name="' + input.name + '"]' + ) + + allInputsWithSameNameAndExclusiveBehaviour.forEach((exclusiveInput) => { + const hasSameFormOwner = input.form === exclusiveInput.form + if (hasSameFormOwner) { + exclusiveInput.checked = false + } + }) + + syncAllConditionalReveals(input) +} + +const syncAllConditionalReveals = function(input) { + const allInputsWithSameName = document.querySelectorAll('input[type="checkbox"][name="' + input.name + '"]'); + Array.from(allInputsWithSameName).map((input) => toggleConditionalInput(input, 'nhsuk-checkboxes__conditional--hidden')); +} + export default () => { // Checkbox input DOMElements inside a conditional form group const checkboxInputs = document.querySelectorAll('.nhsuk-checkboxes--conditional .nhsuk-checkboxes__input'); @@ -16,6 +62,17 @@ export default () => { const handleClick = (event) => { // Toggle conditional content based on checked state toggleConditionalInput(event.target, 'nhsuk-checkboxes__conditional--hidden'); + + if (!event.target.checked) { + return + } + + // Handle 'exclusive' checkbox behaviour (ie "None of these") + if (event.target.getAttribute('data-behaviour') === 'exclusive') { + unCheckAllInputsExcept(event.target) + } else { + unCheckExclusiveInputs(event.target) + } }; // Attach handleClick as click to checkboxInputs diff --git a/packages/components/checkboxes/template.njk b/packages/components/checkboxes/template.njk index 65e0ec13e..d0968332d 100644 --- a/packages/components/checkboxes/template.njk +++ b/packages/components/checkboxes/template.njk @@ -56,6 +56,7 @@