From 02ffa5d783860fff8703028d32a76b45db61525f Mon Sep 17 00:00:00 2001 From: Nick Colley Date: Mon, 19 Mar 2018 13:52:35 +0000 Subject: [PATCH 1/7] Add checkbox conditional reveal --- src/all/all.js | 8 ++ src/checkboxes/_checkboxes.scss | 42 ++++++++-- src/checkboxes/checkboxes.js | 65 ++++++++++++++ src/checkboxes/checkboxes.test.js | 135 ++++++++++++++++++++++++++++++ src/checkboxes/checkboxes.yaml | 65 ++++++++++++++ src/checkboxes/template.njk | 19 ++++- 6 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 src/checkboxes/checkboxes.js create mode 100644 src/checkboxes/checkboxes.test.js diff --git a/src/all/all.js b/src/all/all.js index 90300c42d8..15dcbaf8e9 100644 --- a/src/all/all.js +++ b/src/all/all.js @@ -1,9 +1,17 @@ +import { nodeListForEach } from '../globals/common' + import Button from '../button/button' import Details from '../details/details' +import Checkboxes from '../checkboxes/checkboxes' export function initAll () { new Button().init() new Details().init() + + var $checkboxes = document.querySelectorAll('[data-module="checkboxes"]') + nodeListForEach($checkboxes, function ($checkbox) { + new Checkboxes($checkbox).init() + }) } (initAll()) diff --git a/src/checkboxes/_checkboxes.scss b/src/checkboxes/_checkboxes.scss index 0be5d70d38..1e09c8f178 100644 --- a/src/checkboxes/_checkboxes.scss +++ b/src/checkboxes/_checkboxes.scss @@ -5,16 +5,19 @@ @import "../label/label"; @include govuk-exports("checkboxes") { + $govuk-checkboxes-size: $govuk-spacing-scale-7; + $govuk-checkboxes-label-padding-left-right: $govuk-spacing-scale-3; + .govuk-checkboxes__item { @include govuk-font-regular-19; display: block; position: relative; - min-height: $govuk-spacing-scale-7; + min-height: $govuk-checkboxes-size; margin-bottom: $govuk-spacing-scale-2; - padding: 0 0 0 40px; + padding: 0 0 0 $govuk-checkboxes-size; clear: left; } @@ -31,8 +34,8 @@ top: 0; left: 0; - width: $govuk-spacing-scale-7; - height: $govuk-spacing-scale-7; + width: $govuk-checkboxes-size; + height: $govuk-checkboxes-size; cursor: pointer; @@ -44,8 +47,8 @@ } .govuk-checkboxes__label { - display: inline-block; - padding: 8px $govuk-spacing-scale-3 $govuk-spacing-scale-1; + display: block; + padding: 8px $govuk-checkboxes-label-padding-left-right $govuk-spacing-scale-1; cursor: pointer; // remove 300ms pause on mobile -ms-touch-action: manipulation; @@ -58,8 +61,8 @@ position: absolute; top: 0; left: 0; - width: $govuk-spacing-scale-7; - height: $govuk-spacing-scale-7; + width: $govuk-checkboxes-size; + height: $govuk-checkboxes-size; border: $govuk-border-width-form-element solid currentColor; background: transparent; @@ -106,4 +109,27 @@ .govuk-checkboxes__input:disabled + .govuk-checkboxes__label { opacity: .5; } + + $conditional-border-width: $govuk-border-width-mobile; + // Calculate the amount of padding needed to keep the border centered against the checkbox. + $conditional-border-padding: ($govuk-checkboxes-size / 2) - ($conditional-border-width / 2); + // Move the border centered with the checkbox + $conditional-margin-left: $conditional-border-padding; + // Move the contents of the conditional inline with the label + $conditional-padding-left: $conditional-border-padding + $govuk-checkboxes-label-padding-left-right; + + .govuk-checkboxes__conditional { + @include govuk-responsive-margin($govuk-spacing-responsive-4, "bottom"); + margin-left: $conditional-margin-left; + padding-left: $conditional-padding-left; + border-left: $conditional-border-width solid $govuk-border-colour; + + &[aria-hidden="true"] { + display: none; + } + + & > :last-child { + margin-bottom: 0; + } + } } diff --git a/src/checkboxes/checkboxes.js b/src/checkboxes/checkboxes.js new file mode 100644 index 0000000000..c21f72f0e9 --- /dev/null +++ b/src/checkboxes/checkboxes.js @@ -0,0 +1,65 @@ +// TODO: Ideally this would be a NodeList.prototype.forEach polyfill +// This seems to fail in IE8, requires more investigation. +// See: https://github.com/imagitama/nodelist-foreach-polyfill +var NodeListForEach = function (nodes, callback) { + if (window.NodeList.prototype.forEach) { + return nodes.forEach(callback) + } + for (var i = 0; i < nodes.length; i++) { + callback.call(window, nodes[i], i, nodes) + } +} + +function Checkboxes ($module) { + this.$module = $module + this.$inputs = $module.querySelectorAll('input[type="checkbox"]') +} + +Checkboxes.prototype.init = function () { + var $module = this.$module + var $inputs = this.$inputs + + /** + * Loop over all items with [data-controls] + * Check if they have a matching conditional reveal + * If they do, assign attributes. + **/ + NodeListForEach($inputs, function ($input) { + var controls = $input.getAttribute('data-aria-controls') + + // Check if input controls anything + // Check if content exists, before setting attributes. + if (!controls || !$module.querySelector('#' + controls)) { + return + } + + // If we have content that is controlled, set attributes. + $input.setAttribute('aria-controls', controls) + $input.removeAttribute('data-aria-controls') + this.setAttributes($input) + }.bind(this)) + + // Handle events + $module.addEventListener('click', this.handleClick.bind(this)) +} + +Checkboxes.prototype.setAttributes = function ($input) { + var inputIsChecked = $input.checked + $input.setAttribute('aria-expanded', inputIsChecked) + + var $content = document.querySelector('#' + $input.getAttribute('aria-controls')) + $content.setAttribute('aria-hidden', !inputIsChecked) +} + +Checkboxes.prototype.handleClick = function (event) { + var $target = event.target + + // If a checkbox with aria-controls, handle click + var isCheckbox = $target.getAttribute('type') === 'checkbox' + var hasAriaControls = $target.getAttribute('aria-controls') + if (isCheckbox && hasAriaControls) { + this.setAttributes($target) + } +} + +export default Checkboxes diff --git a/src/checkboxes/checkboxes.test.js b/src/checkboxes/checkboxes.test.js new file mode 100644 index 0000000000..3075fc46df --- /dev/null +++ b/src/checkboxes/checkboxes.test.js @@ -0,0 +1,135 @@ +/** + * @jest-environment ./lib/puppeteer/environment.js + */ +/* eslint-env jest */ + +const cheerio = require('cheerio') + +const configPaths = require('../../config/paths.json') +const PORT = configPaths.ports.test + +let browser +let page +let baseUrl = 'http://localhost:' + PORT + +const goToAndGetComponent = async (name, example) => { + const componentPath = `${baseUrl}/components/${name}/${example}/preview` + await page.goto(componentPath, { waitUntil: 'load' }) + const html = await page.evaluate(() => document.body.innerHTML) + const $ = cheerio.load(html) + return $ +} + +const waitForHiddenSelector = async (selector) => { + return page.waitForSelector(selector, { + hidden: true, + timeout: 1000 + }) +} + +const waitForVisibleSelector = async (selector) => { + return page.waitForSelector(selector, { + visible: true, + timeout: 1000 + }) +} + +beforeEach(async () => { + browser = global.__BROWSER__ + page = await browser.newPage() +}) + +afterEach(async () => { + await page.close() +}) + +describe('Checkboxes with conditional reveals', () => { + describe('when JavaScript is unavailable or fails', () => { + it('has no ARIA attributes applied', async () => { + await page.setJavaScriptEnabled(false) + + const $ = await goToAndGetComponent('checkboxes', 'with-conditional') + const $component = $('.govuk-checkboxes') + + const hasAriaHidden = $component.find('.govuk-checkboxes__conditional[aria-hidden]').length + const hasAriaExpanded = $component.find('.govuk-checkboxes__input[aria-expanded]').length + const hasAriaControls = $component.find('.govuk-checkboxes__input[aria-controls]').length + + expect(hasAriaHidden).toBeFalsy() + expect(hasAriaExpanded).toBeFalsy() + expect(hasAriaControls).toBeFalsy() + }) + it('falls back to making all conditional content visible', async () => { + await page.setJavaScriptEnabled(false) + + await goToAndGetComponent('checkboxes', 'with-conditional') + + const isContentVisible = await waitForVisibleSelector('.govuk-checkboxes__conditional') + expect(isContentVisible).toBeTruthy() + }) + }) + describe('when JavaScript is available', () => { + it('has conditional content revealed that is associated with a checked input', async () => { + const $ = await goToAndGetComponent('checkboxes', 'with-conditional-checked') + const $component = $('.govuk-checkboxes') + const $checkedInput = $component.find('.govuk-checkboxes__input:checked') + const inputAriaControls = $checkedInput.attr('aria-controls') + + const isContentVisible = await waitForVisibleSelector(`[aria-hidden=false][id="${inputAriaControls}"]`) + expect(isContentVisible).toBeTruthy() + }) + it('has no conditional content revealed that is associated with an unchecked input', async () => { + const $ = await goToAndGetComponent('checkboxes', 'with-conditional-checked') + const $component = $('.govuk-checkboxes') + const $firstInput = $component.find('.govuk-checkboxes__item:first-child .govuk-checkboxes__input') + const firstInputAriaControls = $firstInput.attr('aria-controls') + + const isContentHidden = await waitForHiddenSelector(`[aria-hidden=true][id="${firstInputAriaControls}"]`) + expect(isContentHidden).toBeTruthy() + }) + it('indicates when conditional content is collapsed or revealed', async () => { + await goToAndGetComponent('checkboxes', 'with-conditional') + + const isNotExpanded = await waitForVisibleSelector('.govuk-checkboxes__item:first-child .govuk-checkboxes__input[aria-expanded=false]') + expect(isNotExpanded).toBeTruthy() + + await page.click('.govuk-checkboxes__item:first-child .govuk-checkboxes__input') + + const isExpanded = await waitForVisibleSelector('.govuk-checkboxes__item:first-child .govuk-checkboxes__input[aria-expanded=true]') + expect(isExpanded).toBeTruthy() + }) + it('toggles the conditional content when clicking an input', async () => { + const $ = await goToAndGetComponent('checkboxes', 'with-conditional') + const $component = $('.govuk-checkboxes') + const $firstInput = $component.find('.govuk-checkboxes__item:first-child .govuk-checkboxes__input') + const firstInputAriaControls = $firstInput.attr('aria-controls') + + await page.click('.govuk-checkboxes__item:first-child .govuk-checkboxes__input') + + const isContentVisible = await waitForVisibleSelector(`[id="${firstInputAriaControls}"]`) + expect(isContentVisible).toBeTruthy() + + await page.click('.govuk-checkboxes__item:first-child .govuk-checkboxes__input') + + const isContentHidden = await waitForHiddenSelector(`[id="${firstInputAriaControls}"]`) + expect(isContentHidden).toBeTruthy() + }) + it('toggles the conditional content when using an input with a keyboard', async () => { + const $ = await goToAndGetComponent('checkboxes', 'with-conditional') + const $component = $('.govuk-checkboxes') + const $firstInput = $component.find('.govuk-checkboxes__item:first-child .govuk-checkboxes__input') + const firstInputAriaControls = $firstInput.attr('aria-controls') + + await page.focus('.govuk-checkboxes__item:first-child .govuk-checkboxes__input') + await page.keyboard.press('Space') + + const isContentVisible = await waitForVisibleSelector(`[id="${firstInputAriaControls}"]`) + expect(isContentVisible).toBeTruthy() + + await page.keyboard.press('Space') + + const isContentHidden = await waitForHiddenSelector(`[id="${firstInputAriaControls}"]`) + expect(isContentHidden).toBeTruthy() + }) + }) +}) diff --git a/src/checkboxes/checkboxes.yaml b/src/checkboxes/checkboxes.yaml index 4635e45e4b..7cc6775325 100644 --- a/src/checkboxes/checkboxes.yaml +++ b/src/checkboxes/checkboxes.yaml @@ -1,3 +1,13 @@ +accessibilityCriteria: | + ## Conditional reveals + Must: + - be visible as static content if JavaScript is unavailable or fails + - be hidden if JavaScript is available and is collapsed + - indicate if content is expanded or collapsed + - indicate that there is collapsed content to interact with + + Note that we have known issues against this criteria: https://github.com/alphagov/govuk_elements/issues/575 + examples: - name: default data: @@ -87,3 +97,58 @@ examples: text: Waste from mines or quarries - value: farm text: Farm or agricultural waste + +- name: with-conditional + readme: false + data: + idPrefix: 'how-contacted' + fieldset: + legendHtml: +

How do you want to be contacted?

+ items: + - value: email + text: Email + conditional: + html: | + + + - value: phone + text: Phone + conditional: + html: | + + + - value: text + text: Text message + conditional: + html: | + + + +- name: with-conditional-checked + readme: false + data: + idPrefix: 'how-contacted-checked' + fieldset: + legendHtml: +

How do you want to be contacted?

+ items: + - value: email + text: Email + conditional: + html: | + + + - value: phone + text: Phone + checked: true + conditional: + html: | + + + - value: text + text: Text message + conditional: + html: | + + diff --git a/src/checkboxes/template.njk b/src/checkboxes/template.njk index a967a8a6bc..6663517cff 100644 --- a/src/checkboxes/template.njk +++ b/src/checkboxes/template.njk @@ -1,17 +1,27 @@ {% from "fieldset/macro.njk" import govukFieldset %} {% from "label/macro.njk" import govukLabel %} +{% set isConditional = false %} +{% for item in params.items %} + {% if item.conditional %} + {% set isConditional = true %} + {% endif %} +{% endfor %} + {#- Capture the HTML so we can optionally nest it in a fieldset -#} {% set innerHtml %}
+ {%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %} + {%- if isConditional %} data-module="checkboxes"{% endif -%}> {% for item in params.items %} {% set idPrefix = params.idPrefix if params.idPrefix else params.name %} {% set id = item.id if item.id else idPrefix + "-" + loop.index %} + {% set conditionalId = "conditional-" + id %}
+ {{-" disabled" if item.disabled }} + {%- if item.conditional %} data-aria-controls="{{ conditionalId }}"{% endif -%}> {{ govukLabel({ html: item.html, text: item.text, @@ -20,6 +30,11 @@ for: id }) | indent(4) | trim }}
+ {% if item.conditional %} +
+ {{ item.conditional.html | safe }} +
+ {% endif %} {% endfor %}
{% endset %} From f294097636c6fe83c55c36684a049ac2b8465030 Mon Sep 17 00:00:00 2001 From: Nick Colley Date: Thu, 22 Mar 2018 14:47:48 +0000 Subject: [PATCH 2/7] Add radio conditional reveal --- src/all/all.js | 6 ++ src/radios/_radios.scss | 42 +++++++++--- src/radios/radios.js | 65 ++++++++++++++++++ src/radios/radios.test.js | 135 ++++++++++++++++++++++++++++++++++++++ src/radios/radios.yaml | 67 +++++++++++++++++++ src/radios/template.njk | 19 +++++- 6 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 src/radios/radios.js create mode 100644 src/radios/radios.test.js diff --git a/src/all/all.js b/src/all/all.js index 15dcbaf8e9..2a1cc3e556 100644 --- a/src/all/all.js +++ b/src/all/all.js @@ -3,6 +3,7 @@ import { nodeListForEach } from '../globals/common' import Button from '../button/button' import Details from '../details/details' import Checkboxes from '../checkboxes/checkboxes' +import Radios from '../radios/radios' export function initAll () { new Button().init() @@ -12,6 +13,11 @@ export function initAll () { nodeListForEach($checkboxes, function ($checkbox) { new Checkboxes($checkbox).init() }) + + var $radios = document.querySelectorAll('[data-module="radios"]') + nodeListForEach($radios, function ($radio) { + new Radios($radio).init() + }) } (initAll()) diff --git a/src/radios/_radios.scss b/src/radios/_radios.scss index 7d6ff766ec..e528e578ff 100644 --- a/src/radios/_radios.scss +++ b/src/radios/_radios.scss @@ -5,6 +5,9 @@ @import "../label/label"; @include govuk-exports("radios") { + $govuk-radios-size: $govuk-spacing-scale-7; + $govuk-radios-label-padding-left-right: $govuk-spacing-scale-3; + .govuk-radios__item { @include govuk-font-regular-19; @@ -12,10 +15,10 @@ position: relative; - min-height: $govuk-spacing-scale-7; + min-height: $govuk-radios-size; margin-bottom: $govuk-spacing-scale-2; - padding: 0 0 0 $govuk-spacing-scale-7; + padding: 0 0 0 $govuk-radios-size; clear: left; } @@ -32,8 +35,8 @@ top: 0; left: 0; - width: $govuk-spacing-scale-7; - height: $govuk-spacing-scale-7; + width: $govuk-radios-size; + height: $govuk-radios-size; cursor: pointer; @@ -45,8 +48,8 @@ } .govuk-radios__label { - display: inline-block; - padding: 8px $govuk-spacing-scale-3 $govuk-spacing-scale-1; + display: block; + padding: 8px $govuk-radios-label-padding-left-right $govuk-spacing-scale-1; cursor: pointer; // remove 300ms pause on mobile -ms-touch-action: manipulation; @@ -60,8 +63,8 @@ top: 0; left: 0; - width: $govuk-spacing-scale-7; - height: $govuk-spacing-scale-7; + width: $govuk-radios-size; + height: $govuk-radios-size; border: $govuk-border-width-form-element solid currentColor; border-radius: 50%; @@ -116,4 +119,27 @@ } } } + + $conditional-border-width: $govuk-border-width-mobile; + // Calculate the amount of padding needed to keep the border centered against the radios. + $conditional-border-padding: ($govuk-radios-size / 2) - ($conditional-border-width / 2); + // Move the border centered with the radios + $conditional-margin-left: $conditional-border-padding; + // Move the contents of the conditional inline with the label + $conditional-padding-left: $conditional-border-padding + $govuk-radios-label-padding-left-right; + + .govuk-radios__conditional { + @include govuk-responsive-margin($govuk-spacing-responsive-4, "bottom"); + margin-left: $conditional-margin-left; + padding-left: $conditional-padding-left; + border-left: $conditional-border-width solid $govuk-border-colour; + + &[aria-hidden="true"] { + display: none; + } + + & > :last-child { + margin-bottom: 0; + } + } } diff --git a/src/radios/radios.js b/src/radios/radios.js new file mode 100644 index 0000000000..622def3f48 --- /dev/null +++ b/src/radios/radios.js @@ -0,0 +1,65 @@ +// TODO: Ideally this would be a NodeList.prototype.forEach polyfill +// This seems to fail in IE8, requires more investigation. +// See: https://github.com/imagitama/nodelist-foreach-polyfill +var NodeListForEach = function (nodes, callback) { + if (window.NodeList.prototype.forEach) { + return nodes.forEach(callback) + } + for (var i = 0; i < nodes.length; i++) { + callback.call(window, nodes[i], i, nodes) + } +} + +function Radios ($module) { + this.$module = $module + this.$inputs = $module.querySelectorAll('input[type="radio"]') +} + +Radios.prototype.init = function () { + var $module = this.$module + var $inputs = this.$inputs + + /** + * Loop over all items with [data-controls] + * Check if they have a matching conditional reveal + * If they do, assign attributes. + **/ + NodeListForEach($inputs, function ($input) { + var controls = $input.getAttribute('data-aria-controls') + + // Check if input controls anything + // Check if content exists, before setting attributes. + if (!controls || !$module.querySelector('#' + controls)) { + return + } + + // If we have content that is controlled, set attributes. + $input.setAttribute('aria-controls', controls) + $input.removeAttribute('data-aria-controls') + this.setAttributes($input) + }.bind(this)) + + // Handle events + $module.addEventListener('click', this.handleClick.bind(this)) +} + +Radios.prototype.setAttributes = function ($input) { + var inputIsChecked = $input.checked + $input.setAttribute('aria-expanded', inputIsChecked) + + var $content = document.querySelector('#' + $input.getAttribute('aria-controls')) + $content.setAttribute('aria-hidden', !inputIsChecked) +} + +Radios.prototype.handleClick = function (event) { + NodeListForEach(this.$inputs, function ($input) { + // If a radio with aria-controls, handle click + var isRadio = $input.getAttribute('type') === 'radio' + var hasAriaControls = $input.getAttribute('aria-controls') + if (isRadio && hasAriaControls) { + this.setAttributes($input) + } + }.bind(this)) +} + +export default Radios diff --git a/src/radios/radios.test.js b/src/radios/radios.test.js new file mode 100644 index 0000000000..32e1de0bb2 --- /dev/null +++ b/src/radios/radios.test.js @@ -0,0 +1,135 @@ +/** + * @jest-environment ./lib/puppeteer/environment.js + */ +/* eslint-env jest */ + +const cheerio = require('cheerio') + +const configPaths = require('../../config/paths.json') +const PORT = configPaths.ports.test + +let browser +let page +let baseUrl = 'http://localhost:' + PORT + +const goToAndGetComponent = async (name, example) => { + const componentPath = `${baseUrl}/components/${name}/${example}/preview` + await page.goto(componentPath, { waitUntil: 'load' }) + const html = await page.evaluate(() => document.body.innerHTML) + const $ = cheerio.load(html) + return $ +} + +const waitForHiddenSelector = async (selector) => { + return page.waitForSelector(selector, { + hidden: true, + timeout: 1000 + }) +} + +const waitForVisibleSelector = async (selector) => { + return page.waitForSelector(selector, { + visible: true, + timeout: 1000 + }) +} + +beforeEach(async () => { + browser = global.__BROWSER__ + page = await browser.newPage() +}) + +afterEach(async () => { + await page.close() +}) + +describe('Radios with conditional reveals', () => { + describe('when JavaScript is unavailable or fails', () => { + it('has no ARIA attributes applied', async () => { + await page.setJavaScriptEnabled(false) + + const $ = await goToAndGetComponent('radios', 'with-conditional') + const $component = $('.govuk-radios') + + const hasAriaHidden = $component.find('.govuk-radios__conditional[aria-hidden]').length + const hasAriaExpanded = $component.find('.govuk-radios__input[aria-expanded]').length + const hasAriaControls = $component.find('.govuk-radios__input[aria-controls]').length + + expect(hasAriaHidden).toBeFalsy() + expect(hasAriaExpanded).toBeFalsy() + expect(hasAriaControls).toBeFalsy() + }) + it('falls back to making all conditional content visible', async () => { + await page.setJavaScriptEnabled(false) + + await goToAndGetComponent('radios', 'with-conditional') + + const isContentVisible = await waitForVisibleSelector('.govuk-radios__conditional') + expect(isContentVisible).toBeTruthy() + }) + }) + describe('when JavaScript is available', () => { + it('has conditional content revealed that is associated with a checked input', async () => { + const $ = await goToAndGetComponent('radios', 'with-conditional-checked') + const $component = $('.govuk-radios') + const $checkedInput = $component.find('.govuk-radios__input:checked') + const inputAriaControls = $checkedInput.attr('aria-controls') + + const isContentVisible = await waitForVisibleSelector(`[id="${inputAriaControls}"]`) + expect(isContentVisible).toBeTruthy() + }) + it('has no conditional content revealed that is associated with an unchecked input', async () => { + const $ = await goToAndGetComponent('radios', 'with-conditional-checked') + const $component = $('.govuk-radios') + const $firstInput = $component.find('.govuk-radios__item:first-child .govuk-radios__input') + const firstInputAriaControls = $firstInput.attr('aria-controls') + + const isContentHidden = await waitForHiddenSelector(`[aria-hidden=true][id="${firstInputAriaControls}"]`) + expect(isContentHidden).toBeTruthy() + }) + it('indicates when conditional content is collapsed or revealed', async () => { + await goToAndGetComponent('radios', 'with-conditional') + + const isNotExpanded = await waitForVisibleSelector('.govuk-radios__item:first-child .govuk-radios__input[aria-expanded=false]') + expect(isNotExpanded).toBeTruthy() + + await page.click('.govuk-radios__item:first-child .govuk-radios__input') + + const isExpanded = await waitForVisibleSelector('.govuk-radios__item:first-child .govuk-radios__input[aria-expanded=true]') + expect(isExpanded).toBeTruthy() + }) + it('toggles the conditional content when clicking an input', async () => { + const $ = await goToAndGetComponent('radios', 'with-conditional') + const $component = $('.govuk-radios') + const $firstInput = $component.find('.govuk-radios__item:first-child .govuk-radios__input') + const firstInputAriaControls = $firstInput.attr('aria-controls') + + await page.click('.govuk-radios__item:first-child .govuk-radios__input') + + const isContentVisible = await waitForVisibleSelector(`[id="${firstInputAriaControls}"]`) + expect(isContentVisible).toBeTruthy() + + await page.click('.govuk-radios__item:nth-child(3) .govuk-radios__input') + + const isContentHidden = await waitForHiddenSelector(`[id="${firstInputAriaControls}"]`) + expect(isContentHidden).toBeTruthy() + }) + it('toggles the conditional content when using an input with a keyboard', async () => { + const $ = await goToAndGetComponent('radios', 'with-conditional') + const $component = $('.govuk-radios') + const $firstInput = $component.find('.govuk-radios__item:first-child .govuk-radios__input') + const firstInputAriaControls = $firstInput.attr('aria-controls') + + await page.focus('.govuk-radios__item:first-child .govuk-radios__input') + await page.keyboard.press('Space') + + const isContentVisible = await waitForVisibleSelector(`[id="${firstInputAriaControls}"]`) + expect(isContentVisible).toBeTruthy() + + await page.keyboard.press('ArrowRight') + + const isContentHidden = await waitForHiddenSelector(`[id="${firstInputAriaControls}"]`) + expect(isContentHidden).toBeTruthy() + }) + }) +}) diff --git a/src/radios/radios.yaml b/src/radios/radios.yaml index c3446ba56f..fa9231e913 100644 --- a/src/radios/radios.yaml +++ b/src/radios/radios.yaml @@ -1,3 +1,13 @@ +accessibilityCriteria: | + ## Conditional reveals + Must: + - be visible as static content if JavaScript is unavailable or fails + - be hidden if JavaScript is available and is collapsed + - indicate if content is expanded or collapsed + - indicate that there is collapsed content to interact with + + Note that we have known issues against this criteria: https://github.com/alphagov/govuk_elements/issues/575 + examples: - name: default data: @@ -94,3 +104,60 @@ examples: - value: no text: No checked: true + +- name: with-conditional + readme: false + data: + idPrefix: 'how-contacted' + name: 'how-contacted' + fieldset: + legendHtml: +

How do you want to be contacted?

+ items: + - value: email + text: Email + conditional: + html: | + + + - value: phone + text: Phone + conditional: + html: | + + + - value: text + text: Text message + conditional: + html: | + + + +- name: with-conditional-checked + readme: false + data: + idPrefix: 'how-contacted-checked' + name: 'how-contacted-checked' + fieldset: + legendHtml: +

How do you want to be contacted?

+ items: + - value: email + text: Email + conditional: + html: | + + + - value: phone + text: Phone + checked: true + conditional: + html: | + + + - value: text + text: Text message + conditional: + html: | + + diff --git a/src/radios/template.njk b/src/radios/template.njk index c892e472e4..ca6035abb5 100644 --- a/src/radios/template.njk +++ b/src/radios/template.njk @@ -1,17 +1,27 @@ {% from "fieldset/macro.njk" import govukFieldset %} {% from "label/macro.njk" import govukLabel %} +{% set isConditional = false %} +{% for item in params.items %} + {% if item.conditional %} + {% set isConditional = true %} + {% endif %} +{% endfor %} + {#- Capture the HTML so we can optionally nest it in a fieldset -#} {% set innerHtml %}
+ {%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %} + {%- if isConditional %} data-module="radios"{% endif -%}> {% for item in params.items %} {% set idPrefix = params.idPrefix if params.idPrefix else params.name %} {% set id = item.id if item.id else idPrefix + "-" + loop.index %} + {% set conditionalId = "conditional-" + id %}
+ {{-" disabled" if item.disabled }} + {%- if item.conditional %} data-aria-controls="{{ conditionalId }}"{% endif -%}> {{ govukLabel({ html: item.html, text: item.text, @@ -20,6 +30,11 @@ for: id }) | indent(4) | trim }}
+ {% if item.conditional %} +
+ {{ item.conditional.html | safe }} +
+ {% endif %} {% endfor %}
{% endset %} From 4445060ae2837881ef9193c47286701acd222694 Mon Sep 17 00:00:00 2001 From: Nick Colley Date: Tue, 17 Apr 2018 15:14:37 +0100 Subject: [PATCH 3/7] Add polyfills used in Checkbox and Radios --- src/checkboxes/checkboxes.js | 3 + src/globals/polyfills/Document.js | 26 ++ src/globals/polyfills/Element.js | 114 ++++++++ src/globals/polyfills/Event.js | 252 ++++++++++++++++++ .../polyfills/Object/defineProperty.js | 2 +- src/globals/polyfills/Window.js | 20 ++ src/radios/radios.js | 3 + 7 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 src/globals/polyfills/Document.js create mode 100644 src/globals/polyfills/Element.js create mode 100644 src/globals/polyfills/Event.js create mode 100644 src/globals/polyfills/Window.js diff --git a/src/checkboxes/checkboxes.js b/src/checkboxes/checkboxes.js index c21f72f0e9..1ec166325f 100644 --- a/src/checkboxes/checkboxes.js +++ b/src/checkboxes/checkboxes.js @@ -1,3 +1,6 @@ +import '../globals/polyfills/Function/prototype/bind' +import '../globals/polyfills/Event' // addEventListener and event.target normaliziation + // TODO: Ideally this would be a NodeList.prototype.forEach polyfill // This seems to fail in IE8, requires more investigation. // See: https://github.com/imagitama/nodelist-foreach-polyfill diff --git a/src/globals/polyfills/Document.js b/src/globals/polyfills/Document.js new file mode 100644 index 0000000000..ffbe9e8fb0 --- /dev/null +++ b/src/globals/polyfills/Document.js @@ -0,0 +1,26 @@ +(function(undefined) { + +// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Document/detect.js +var detect = ("Document" in this) + +if (detect) return + +// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Document&flags=always +if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { + + if (this.HTMLDocument) { // IE8 + + // HTMLDocument is an extension of Document. If the browser has HTMLDocument but not Document, the former will suffice as an alias for the latter. + this.Document = this.HTMLDocument; + + } else { + + // Create an empty function to act as the missing constructor for the document object, attach the document object as its prototype. The function needs to be anonymous else it is hoisted and causes the feature detect to prematurely pass, preventing the assignments below being made. + this.Document = this.HTMLDocument = document.constructor = (new Function('return function Document() {}')()); + this.Document.prototype = document; + } +} + + +}) +.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); diff --git a/src/globals/polyfills/Element.js b/src/globals/polyfills/Element.js new file mode 100644 index 0000000000..d10712b9a9 --- /dev/null +++ b/src/globals/polyfills/Element.js @@ -0,0 +1,114 @@ +import './Document' + +(function(undefined) { + +// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Element/detect.js +var detect = ('Element' in this && 'HTMLElement' in this) + +if (detect) return + +// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Element&flags=always +(function () { + + // IE8 + if (window.Element && !window.HTMLElement) { + window.HTMLElement = window.Element; + return; + } + + // create Element constructor + window.Element = window.HTMLElement = new Function('return function Element() {}')(); + + // generate sandboxed iframe + var vbody = document.appendChild(document.createElement('body')); + var frame = vbody.appendChild(document.createElement('iframe')); + + // use sandboxed iframe to replicate Element functionality + var frameDocument = frame.contentWindow.document; + var prototype = Element.prototype = frameDocument.appendChild(frameDocument.createElement('*')); + var cache = {}; + + // polyfill Element.prototype on an element + var shiv = function (element, deep) { + var + childNodes = element.childNodes || [], + index = -1, + key, value, childNode; + + if (element.nodeType === 1 && element.constructor !== Element) { + element.constructor = Element; + + for (key in cache) { + value = cache[key]; + element[key] = value; + } + } + + while (childNode = deep && childNodes[++index]) { + shiv(childNode, deep); + } + + return element; + }; + + var elements = document.getElementsByTagName('*'); + var nativeCreateElement = document.createElement; + var interval; + var loopLimit = 100; + + prototype.attachEvent('onpropertychange', function (event) { + var + propertyName = event.propertyName, + nonValue = !cache.hasOwnProperty(propertyName), + newValue = prototype[propertyName], + oldValue = cache[propertyName], + index = -1, + element; + + while (element = elements[++index]) { + if (element.nodeType === 1) { + if (nonValue || element[propertyName] === oldValue) { + element[propertyName] = newValue; + } + } + } + + cache[propertyName] = newValue; + }); + + prototype.constructor = Element; + + if (!prototype.hasAttribute) { + // .hasAttribute + prototype.hasAttribute = function hasAttribute(name) { + return this.getAttribute(name) !== null; + }; + } + + // Apply Element prototype to the pre-existing DOM as soon as the body element appears. + function bodyCheck() { + if (!(loopLimit--)) clearTimeout(interval); + if (document.body && !document.body.prototype && /(complete|interactive)/.test(document.readyState)) { + shiv(document, true); + if (interval && document.body.prototype) clearTimeout(interval); + return (!!document.body.prototype); + } + return false; + } + if (!bodyCheck()) { + document.onreadystatechange = bodyCheck; + interval = setInterval(bodyCheck, 25); + } + + // Apply to any new elements created after load + document.createElement = function createElement(nodeName) { + var element = nativeCreateElement(String(nodeName).toLowerCase()); + return shiv(element); + }; + + // remove sandboxed iframe + document.removeChild(vbody); +}()); + +}) +.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); diff --git a/src/globals/polyfills/Event.js b/src/globals/polyfills/Event.js new file mode 100644 index 0000000000..2e7aa4c9bb --- /dev/null +++ b/src/globals/polyfills/Event.js @@ -0,0 +1,252 @@ +import './Window' +import './Element' +import './Object/defineProperty' + +(function(undefined) { + +// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Event/detect.js +var detect = ( + (function(global) { + + if (!('Event' in global)) return false; + if (typeof global.Event === 'function') return true; + + try { + + // In IE 9-11, the Event object exists but cannot be instantiated + new Event('click'); + return true; + } catch(e) { + return false; + } + }(this)) +) + +if (detect) return + +// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Event&flags=always +(function () { + var unlistenableWindowEvents = { + click: 1, + dblclick: 1, + keyup: 1, + keypress: 1, + keydown: 1, + mousedown: 1, + mouseup: 1, + mousemove: 1, + mouseover: 1, + mouseenter: 1, + mouseleave: 1, + mouseout: 1, + storage: 1, + storagecommit: 1, + textinput: 1 + }; + + // This polyfill depends on availability of `document` so will not run in a worker + // However, we asssume there are no browsers with worker support that lack proper + // support for `Event` within the worker + if (typeof document === 'undefined' || typeof window === 'undefined') return; + + function indexOf(array, element) { + var + index = -1, + length = array.length; + + while (++index < length) { + if (index in array && array[index] === element) { + return index; + } + } + + return -1; + } + + var existingProto = (window.Event && window.Event.prototype) || null; + window.Event = Window.prototype.Event = function Event(type, eventInitDict) { + if (!type) { + throw new Error('Not enough arguments'); + } + + var event; + // Shortcut if browser supports createEvent + if ('createEvent' in document) { + event = document.createEvent('Event'); + var bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; + var cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; + + event.initEvent(type, bubbles, cancelable); + + return event; + } + + event = document.createEventObject(); + + event.type = type; + event.bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; + event.cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; + + return event; + }; + if (existingProto) { + Object.defineProperty(window.Event, 'prototype', { + configurable: false, + enumerable: false, + writable: true, + value: existingProto + }); + } + + if (!('createEvent' in document)) { + window.addEventListener = Window.prototype.addEventListener = Document.prototype.addEventListener = Element.prototype.addEventListener = function addEventListener() { + var + element = this, + type = arguments[0], + listener = arguments[1]; + + if (element === window && type in unlistenableWindowEvents) { + throw new Error('In IE8 the event: ' + type + ' is not available on the window object. Please see https://github.com/Financial-Times/polyfill-service/issues/317 for more information.'); + } + + if (!element._events) { + element._events = {}; + } + + if (!element._events[type]) { + element._events[type] = function (event) { + var + list = element._events[event.type].list, + events = list.slice(), + index = -1, + length = events.length, + eventElement; + + event.preventDefault = function preventDefault() { + if (event.cancelable !== false) { + event.returnValue = false; + } + }; + + event.stopPropagation = function stopPropagation() { + event.cancelBubble = true; + }; + + event.stopImmediatePropagation = function stopImmediatePropagation() { + event.cancelBubble = true; + event.cancelImmediate = true; + }; + + event.currentTarget = element; + event.relatedTarget = event.fromElement || null; + event.target = event.target || event.srcElement || element; + event.timeStamp = new Date().getTime(); + + if (event.clientX) { + event.pageX = event.clientX + document.documentElement.scrollLeft; + event.pageY = event.clientY + document.documentElement.scrollTop; + } + + while (++index < length && !event.cancelImmediate) { + if (index in events) { + eventElement = events[index]; + + if (indexOf(list, eventElement) !== -1 && typeof eventElement === 'function') { + eventElement.call(element, event); + } + } + } + }; + + element._events[type].list = []; + + if (element.attachEvent) { + element.attachEvent('on' + type, element._events[type]); + } + } + + element._events[type].list.push(listener); + }; + + window.removeEventListener = Window.prototype.removeEventListener = Document.prototype.removeEventListener = Element.prototype.removeEventListener = function removeEventListener() { + var + element = this, + type = arguments[0], + listener = arguments[1], + index; + + if (element._events && element._events[type] && element._events[type].list) { + index = indexOf(element._events[type].list, listener); + + if (index !== -1) { + element._events[type].list.splice(index, 1); + + if (!element._events[type].list.length) { + if (element.detachEvent) { + element.detachEvent('on' + type, element._events[type]); + } + delete element._events[type]; + } + } + } + }; + + window.dispatchEvent = Window.prototype.dispatchEvent = Document.prototype.dispatchEvent = Element.prototype.dispatchEvent = function dispatchEvent(event) { + if (!arguments.length) { + throw new Error('Not enough arguments'); + } + + if (!event || typeof event.type !== 'string') { + throw new Error('DOM Events Exception 0'); + } + + var element = this, type = event.type; + + try { + if (!event.bubbles) { + event.cancelBubble = true; + + var cancelBubbleEvent = function (event) { + event.cancelBubble = true; + + (element || window).detachEvent('on' + type, cancelBubbleEvent); + }; + + this.attachEvent('on' + type, cancelBubbleEvent); + } + + this.fireEvent('on' + type, event); + } catch (error) { + event.target = element; + + do { + event.currentTarget = element; + + if ('_events' in element && typeof element._events[type] === 'function') { + element._events[type].call(element, event); + } + + if (typeof element['on' + type] === 'function') { + element['on' + type].call(element, event); + } + + element = element.nodeType === 9 ? element.parentWindow : element.parentNode; + } while (element && !event.cancelBubble); + } + + return true; + }; + + // Add the DOMContentLoaded Event + document.attachEvent('onreadystatechange', function() { + if (document.readyState === 'complete') { + document.dispatchEvent(new Event('DOMContentLoaded', { + bubbles: true + })); + } + }); + } +}()); + +}) +.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); diff --git a/src/globals/polyfills/Object/defineProperty.js b/src/globals/polyfills/Object/defineProperty.js index f34e65d471..e11ee62842 100644 --- a/src/globals/polyfills/Object/defineProperty.js +++ b/src/globals/polyfills/Object/defineProperty.js @@ -1,6 +1,6 @@ (function(undefined) { -// Detection from https://cdn.polyfill.io/v2/polyfill.js?features=Object.defineProperty&flags=always +// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js var detect = ( // In IE8, defineProperty could only act on DOM elements, so full support // for the feature requires the ability to set a property on an arbitrary object diff --git a/src/globals/polyfills/Window.js b/src/globals/polyfills/Window.js new file mode 100644 index 0000000000..695169e9ce --- /dev/null +++ b/src/globals/polyfills/Window.js @@ -0,0 +1,20 @@ +(function(undefined) { + +// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Window/detect.js +var detect = ('Window' in this) + +if (detect) return + +// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Window&flags=always +if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { + (function (global) { + if (global.constructor) { + global.Window = global.constructor; + } else { + (global.Window = global.constructor = new Function('return function Window() {}')()).prototype = this; + } + }(this)); +} + +}) +.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); diff --git a/src/radios/radios.js b/src/radios/radios.js index 622def3f48..af1edda2e6 100644 --- a/src/radios/radios.js +++ b/src/radios/radios.js @@ -1,3 +1,6 @@ +import '../globals/polyfills/Function/prototype/bind' +import '../globals/polyfills/Event' // addEventListener and event.target normaliziation + // TODO: Ideally this would be a NodeList.prototype.forEach polyfill // This seems to fail in IE8, requires more investigation. // See: https://github.com/imagitama/nodelist-foreach-polyfill From 90cf86f93af7bd8404417723214ea5c5eac18233 Mon Sep 17 00:00:00 2001 From: Nick Colley Date: Tue, 17 Apr 2018 16:38:42 +0100 Subject: [PATCH 4/7] Move nodeListForEach into shared common --- src/checkboxes/checkboxes.js | 15 ++------------- src/globals/common.js | 14 ++++++++++++++ src/radios/radios.js | 17 +++-------------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/checkboxes/checkboxes.js b/src/checkboxes/checkboxes.js index 1ec166325f..18a191d1b8 100644 --- a/src/checkboxes/checkboxes.js +++ b/src/checkboxes/checkboxes.js @@ -1,17 +1,6 @@ import '../globals/polyfills/Function/prototype/bind' import '../globals/polyfills/Event' // addEventListener and event.target normaliziation - -// TODO: Ideally this would be a NodeList.prototype.forEach polyfill -// This seems to fail in IE8, requires more investigation. -// See: https://github.com/imagitama/nodelist-foreach-polyfill -var NodeListForEach = function (nodes, callback) { - if (window.NodeList.prototype.forEach) { - return nodes.forEach(callback) - } - for (var i = 0; i < nodes.length; i++) { - callback.call(window, nodes[i], i, nodes) - } -} +import { nodeListForEach } from '../globals/common' function Checkboxes ($module) { this.$module = $module @@ -27,7 +16,7 @@ Checkboxes.prototype.init = function () { * Check if they have a matching conditional reveal * If they do, assign attributes. **/ - NodeListForEach($inputs, function ($input) { + nodeListForEach($inputs, function ($input) { var controls = $input.getAttribute('data-aria-controls') // Check if input controls anything diff --git a/src/globals/common.js b/src/globals/common.js index 8b72661049..a9db2d8df2 100644 --- a/src/globals/common.js +++ b/src/globals/common.js @@ -61,3 +61,17 @@ export function preventDefault (event) { event.returnValue = false } } + +/** + * TODO: Ideally this would be a NodeList.prototype.forEach polyfill + * This seems to fail in IE8, requires more investigation. + * See: https://github.com/imagitama/nodelist-foreach-polyfill + */ +export function nodeListForEach (nodes, callback) { + if (window.NodeList.prototype.forEach) { + return nodes.forEach(callback) + } + for (var i = 0; i < nodes.length; i++) { + callback.call(window, nodes[i], i, nodes) + } +} diff --git a/src/radios/radios.js b/src/radios/radios.js index af1edda2e6..6aa2a10ca6 100644 --- a/src/radios/radios.js +++ b/src/radios/radios.js @@ -1,17 +1,6 @@ import '../globals/polyfills/Function/prototype/bind' import '../globals/polyfills/Event' // addEventListener and event.target normaliziation - -// TODO: Ideally this would be a NodeList.prototype.forEach polyfill -// This seems to fail in IE8, requires more investigation. -// See: https://github.com/imagitama/nodelist-foreach-polyfill -var NodeListForEach = function (nodes, callback) { - if (window.NodeList.prototype.forEach) { - return nodes.forEach(callback) - } - for (var i = 0; i < nodes.length; i++) { - callback.call(window, nodes[i], i, nodes) - } -} +import { nodeListForEach } from '../globals/common' function Radios ($module) { this.$module = $module @@ -27,7 +16,7 @@ Radios.prototype.init = function () { * Check if they have a matching conditional reveal * If they do, assign attributes. **/ - NodeListForEach($inputs, function ($input) { + nodeListForEach($inputs, function ($input) { var controls = $input.getAttribute('data-aria-controls') // Check if input controls anything @@ -55,7 +44,7 @@ Radios.prototype.setAttributes = function ($input) { } Radios.prototype.handleClick = function (event) { - NodeListForEach(this.$inputs, function ($input) { + nodeListForEach(this.$inputs, function ($input) { // If a radio with aria-controls, handle click var isRadio = $input.getAttribute('type') === 'radio' var hasAriaControls = $input.getAttribute('aria-controls') From efb2a807611224b940e7e9cb038b292ed8577eb6 Mon Sep 17 00:00:00 2001 From: Nick Colley Date: Mon, 23 Apr 2018 10:41:20 +0100 Subject: [PATCH 5/7] Add radio template tests for conditional feature --- src/radios/template.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/radios/template.test.js b/src/radios/template.test.js index 8dfc0bc66e..fb207edf03 100644 --- a/src/radios/template.test.js +++ b/src/radios/template.test.js @@ -194,6 +194,33 @@ describe('Radios', () => { const $lastInput = $component.find('.govuk-radios__item:last-child input') expect($lastInput.attr('checked')).toEqual('checked') }) + + it('render conditional', () => { + const $ = render('radios', { + name: 'example-conditional', + items: [ + { + value: 'yes', + text: 'Yes' + }, + { + value: 'no', + text: 'No', + checked: true, + conditional: { + html: 'Conditional content' + } + } + ] + }) + + const $component = $('.govuk-radios') + const $lastInput = $component.find('.govuk-radios__input').last() + expect($lastInput.attr('data-aria-controls')).toBe('conditional-example-conditional-2') + const $lastConditional = $component.find('.govuk-radios__conditional').last() + expect($lastConditional.attr('id')).toBe('conditional-example-conditional-2') + expect($lastConditional.html()).toContain('Conditional content') + }) }) describe('nested dependant components', () => { From ef068135e7dbe8ace6f42593013a3706ae7cce29 Mon Sep 17 00:00:00 2001 From: Nick Colley Date: Mon, 23 Apr 2018 10:43:31 +0100 Subject: [PATCH 6/7] Add checkboxes template test for conditional feature --- src/checkboxes/template.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/checkboxes/template.test.js b/src/checkboxes/template.test.js index e8ab7dcbdd..253c406de4 100644 --- a/src/checkboxes/template.test.js +++ b/src/checkboxes/template.test.js @@ -199,6 +199,33 @@ describe('Checkboxes', () => { }) }) + it('render conditional', () => { + const $ = render('checkboxes', { + name: 'example-conditional', + items: [ + { + value: 'yes', + text: 'Yes' + }, + { + value: 'no', + text: 'No', + checked: true, + conditional: { + html: 'Conditional content' + } + } + ] + }) + + const $component = $('.govuk-checkboxes') + const $lastInput = $component.find('.govuk-checkboxes__input').last() + expect($lastInput.attr('data-aria-controls')).toBe('conditional-example-conditional-2') + const $lastConditional = $component.find('.govuk-checkboxes__conditional').last() + expect($lastConditional.attr('id')).toBe('conditional-example-conditional-2') + expect($lastConditional.html()).toContain('Conditional content') + }) + describe('nested dependant components', () => { it('have correct nesting order', () => { const $ = render('checkboxes', examples['with-extreme-fieldset']) From 0a0ce3c78478d832b75b7b40ddbcba7bf753f838 Mon Sep 17 00:00:00 2001 From: Nick Colley Date: Tue, 24 Apr 2018 16:48:27 +0100 Subject: [PATCH 7/7] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2d58663d5..89691d06fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ Fixes: New features: - We're now using ES6 Modules and [rollup](https://rollupjs.org/guide/en) to distribute our JavaScript. (PR [#652](https://github.com/alphagov/govuk-frontend/pull/652)) +- Checkboxes and Radios conditional reveal +(PR [#616](https://github.com/alphagov/govuk-frontend/pull/616)) Internal: