Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Checkboxes and Radios conditional reveal #616

Merged
merged 7 commits into from
Apr 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
14 changes: 14 additions & 0 deletions src/all/all.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
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()
new Details().init()

var $checkboxes = document.querySelectorAll('[data-module="checkboxes"]')
nodeListForEach($checkboxes, function ($checkbox) {
new Checkboxes($checkbox).init()
})

var $radios = document.querySelectorAll('[data-module="radios"]')
nodeListForEach($radios, function ($radio) {
new Radios($radio).init()
})
}

(initAll())
42 changes: 34 additions & 8 deletions src/checkboxes/_checkboxes.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
}
}
}
57 changes: 57 additions & 0 deletions src/checkboxes/checkboxes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import '../globals/polyfills/Function/prototype/bind'
import '../globals/polyfills/Event' // addEventListener and event.target normaliziation
import { nodeListForEach } from '../globals/common'

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
135 changes: 135 additions & 0 deletions src/checkboxes/checkboxes.test.js
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
65 changes: 65 additions & 0 deletions src/checkboxes/checkboxes.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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:
<h3 class="govuk-heading-m">How do you want to be contacted?</h3>
items:
- value: email
text: Email
conditional:
html: |
<label class="govuk-label" for="context-email">Mobile phone number</label>
<input class="govuk-input govuk-!-width-one-third" name="context-email" type="text" id="context-email">
- value: phone
text: Phone
conditional:
html: |
<label class="govuk-label" for="contact-phone">Phone number</label>
<input class="govuk-input govuk-!-width-one-third" name="contact-phone" type="text" id="contact-phone">
- value: text
text: Text message
conditional:
html: |
<label class="govuk-label" for="contact-text-message">Mobile phone number</label>
<input class="govuk-input govuk-!-width-one-third" name="contact-text-message" type="text" id="contact-text-message">

- name: with-conditional-checked
readme: false
data:
idPrefix: 'how-contacted-checked'
fieldset:
legendHtml:
<h3 class="govuk-heading-m">How do you want to be contacted?</h3>
items:
- value: email
text: Email
conditional:
html: |
<label class="govuk-label" for="context-email">Mobile phone number</label>
<input class="govuk-input govuk-!-width-one-third" name="context-email" type="text" id="context-email">
- value: phone
text: Phone
checked: true
conditional:
html: |
<label class="govuk-label" for="contact-phone">Phone number</label>
<input class="govuk-input govuk-!-width-one-third" name="contact-phone" type="text" id="contact-phone">
- value: text
text: Text message
conditional:
html: |
<label class="govuk-label" for="contact-text-message">Mobile phone number</label>
<input class="govuk-input govuk-!-width-one-third" name="contact-text-message" type="text" id="contact-text-message">
Loading