Skip to content

Commit

Permalink
Add "None of these" JavaScript behaviour to checkboxes
Browse files Browse the repository at this point in the history
  • Loading branch information
andymantell committed Jul 23, 2021
1 parent 7dc973d commit 4f91d11
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 2 deletions.
98 changes: 98 additions & 0 deletions app/components/checkboxes/none-of-the-above.njk
Original file line number Diff line number Diff line change
@@ -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 %}

<div class="nhsuk-width-container">
<main class="nhsuk-main-wrapper" id="maincontent">

<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-two-thirds">
{{ 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"
}
]
}) }}
</div>
</div>

</main>
</div>

{% endblock %}
1 change: 1 addition & 0 deletions app/pages/examples.njk
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<li><a href="../components/checkboxes/page-heading.html">Checkboxes with legend as page heading</a></li>
<li><a href="../components/checkboxes/error.html">Checkboxes with error message</a></li>
<li><a href="../components/checkboxes/conditional.html">Checkboxes with conditional content</a></li>
<li><a href="../components/checkboxes/none-of-the-above.html">Checkboxes with "none of the above" option</a></li>
<li><a href="../components/contents-list/index.html">Contents list</a></li>
<li><a href="../components/date-input/index.html">Date input</a></li>
<li><a href="../components/date-input/autocomplete.html">Date input with autocomplete attribute</a></li>
Expand Down
9 changes: 7 additions & 2 deletions packages/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
};
161 changes: 161 additions & 0 deletions packages/components/checkboxes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<div class="nhsuk-form-group">
<fieldset class="nhsuk-fieldset" aria-describedby="contact-hint">
<legend class="nhsuk-fieldset__legend nhsuk-fieldset__legend--l">
<h1 class="nhsuk-fieldset__heading">
How would you prefer to be contacted?
</h1>
</legend>
<div class="nhsuk-hint" id="contact-hint">
Select all options that are relevant to you.
</div>
<div class="nhsuk-checkboxes nhsuk-checkboxes--conditional">
<div class="nhsuk-checkboxes__item">
<input class="nhsuk-checkboxes__input" id="contact-1" name="contact" type="checkbox" value="email" aria-controls="conditional-contact-1" aria-expanded="false">
<label class="nhsuk-label nhsuk-checkboxes__label" for="contact-1">
Email
</label>
</div>
<div class="nhsuk-checkboxes__conditional nhsuk-checkboxes__conditional--hidden" id="conditional-contact-1">
<div class="nhsuk-form-group">
<label class="nhsuk-label" for="email">
Email address
</label>
<input class="nhsuk-input nhsuk-u-width-two-thirds" id="email" name="email" type="text">
</div>
</div>
<div class="nhsuk-checkboxes__item">
<input class="nhsuk-checkboxes__input" id="contact-2" name="contact" type="checkbox" value="phone" aria-controls="conditional-contact-2" aria-expanded="false">
<label class="nhsuk-label nhsuk-checkboxes__label" for="contact-2">
Phone
</label>
</div>
<div class="nhsuk-checkboxes__conditional nhsuk-checkboxes__conditional--hidden" id="conditional-contact-2">
<div class="nhsuk-form-group">
<label class="nhsuk-label" for="phone">
Phone number
</label>
<input class="nhsuk-input nhsuk-u-width-two-thirds" id="phone" name="phone" type="text">
</div>
</div>
<div class="nhsuk-checkboxes__item">
<input class="nhsuk-checkboxes__input" id="contact-3" name="contact" type="checkbox" value="text" aria-controls="conditional-contact-3" aria-expanded="false">
<label class="nhsuk-label nhsuk-checkboxes__label" for="contact-3">
Text message
</label>
</div>
<div class="nhsuk-checkboxes__conditional nhsuk-checkboxes__conditional--hidden" id="conditional-contact-3">
<div class="nhsuk-form-group">
<label class="nhsuk-label" for="mobile">
Mobile phone number
</label>
<input class="nhsuk-input nhsuk-u-width-two-thirds" id="mobile" name="mobile" type="text">
</div>
</div>
<div class="nhsuk-checkboxes__divider">or</div>
<div class="nhsuk-checkboxes__item">
<input class="nhsuk-checkboxes__input" id="contact-5" name="contact" type="checkbox" value="none" data-behaviour="exclusive">
<label class="nhsuk-label nhsuk-checkboxes__label" for="contact-5">
None of the above
</label>
</div>
</div>
</fieldset>
</div>
```

#### 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:
Expand All @@ -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).

Expand Down
57 changes: 57 additions & 0 deletions packages/components/checkboxes/checkboxes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/components/checkboxes/template.njk
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<input class="nhsuk-checkboxes__input" id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ item.value }}"
{{-" checked" if item.checked }}
{{-" disabled" if item.disabled }}
{%- if item.behaviour %} data-behaviour="{{ item.behaviour }}"{% endif -%}
{%- if item.conditional %} aria-controls="{{ conditionalId }}" aria-expanded="{{"true" if item.checked else "false"}}"{% endif -%}
{%- if hasHint %} aria-describedby="{{ itemHintId }}"{% endif -%}
{%- for attribute, value in item.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
Expand Down

0 comments on commit 4f91d11

Please sign in to comment.