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 GOVUK.ShowHideContent JavaScript #315

Merged
merged 12 commits into from
Sep 5, 2016
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ In production:
* [Stick at top when scrolling](/docs/javascript.md#stick-at-top-when-scrolling)
* [Selection buttons](/docs/javascript.md#selection-buttons)
* [Shim links with button role](/docs/javascript.md#shim-links-with-button-role)
* [Show/Hide content](/docs/javascript.md#show-hide-content)
* [Analytics](/docs/analytics.md)
* [Create an analytics tracker](/docs/analytics.md#create-an-analytics-tracker)
* [Virtual pageviews](/docs/analytics.md#virtual-pageviews)
Expand Down
37 changes: 37 additions & 0 deletions docs/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,41 @@ It’s also possible to define more or different keycodes to activate against:
GOVUK.shimLinksWithButtonRole.init({
keycodes: [32, 114]
});

## Show/Hide content

Script to support show/hide content, toggled by radio buttons and checkboxes. This allows for progressive disclosure of question and answer forms based on selected values:

<label class="block-label" data-target="show-me">
<input type="radio" name="enabled" value="yes" /> Yes
</label>

<label class="block-label">
<input type="radio" name="enabled" value="no" /> No
</label>

<div id="show-me" class="panel js-hidden">
<p>Show/Hide content to be toggled</p>
</div>

When the input's `checked` attribute is set, the show/hide content's `.js-hidden` class is removed and ARIA attributes are added to enable it. Note the sample `show-me` id attribute used to link the label to show/hide content.

### Usage

#### GOVUK.ShowHideContent

To apply this behaviour to elements with the above HTML pattern, call the `GOVUK.ShowHideContent` constructor:

```
var showHideContent = new GOVUK.ShowHideContent();
showHideContent.init();
```

This will bind two event handlers to $(document.body), one for radio inputs and one for checkboxes. By listening for events bubbling up to the `body` tag, additional show/hide content added to the page will still be picked up after `.init()` is called.

Alternatively, pass in your own selector. In the example below, event handlers are bound to the form instead.

```
var showHideContent = new GOVUK.ShowHideContent();
showHideContent.init($('form.example'));
```
171 changes: 171 additions & 0 deletions javascripts/govuk/show-hide-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
;(function (global) {
'use strict'

var $ = global.jQuery
var GOVUK = global.GOVUK || {}

function ShowHideContent () {
var self = this

// Radio and Checkbox selectors
var selectors = {
namespace: 'ShowHideContent',
radio: '.block-label[data-target] input[type="radio"]',
checkbox: '.block-label[data-target] input[type="checkbox"]'
}

// Escape name attribute for use in DOM selector
function escapeElementName (str) {
var result = str.replace('[', '\\[').replace(']', '\\]')
return result
}

// Adds ARIA attributes to control + associated content
function initToggledContent () {
var $control = $(this)
var $content = getToggledContent($control)

// Set aria-controls and defaults
if ($content.length) {
$control.attr('aria-controls', $content.attr('id'))
$control.attr('aria-expanded', 'false')
$content.attr('aria-hidden', 'true')
}
}

// Return toggled content for control
function getToggledContent ($control) {
var id = $control.attr('aria-controls')

// ARIA attributes aren't set before init
if (!id) {
id = $control.closest('label').data('target')
}

// Find show/hide content by id
return $('#' + id)
}

// Show toggled content for control
function showToggledContent ($control, $content) {
// Show content
if ($content.hasClass('js-hidden')) {
$content.removeClass('js-hidden')
$content.attr('aria-hidden', 'false')

// If the controlling input, update aria-expanded
if ($control.attr('aria-controls')) {
$control.attr('aria-expanded', 'true')
}
}
}

// Hide toggled content for control
function hideToggledContent ($control, $content) {
$content = $content || getToggledContent($control)

// Hide content
if (!$content.hasClass('js-hidden')) {
$content.addClass('js-hidden')
$content.attr('aria-hidden', 'true')

// If the controlling input, update aria-expanded
if ($control.attr('aria-controls')) {
$control.attr('aria-expanded', 'false')
}
}
}

// Handle radio show/hide
function handleRadioContent ($control, $content) {
// All radios in this group which control content
var selector = selectors.radio + '[name=' + escapeElementName($control.attr('name')) + '][aria-controls]'
Copy link
Contributor

@colinrotherham colinrotherham Aug 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tombye, @gemmaleigh.

This has a side effect unfortunately… Now only controlling radios can be used to show/hide content.

Imagine a yes or no question and answer form, for example "Do you have a partner"?

Previously the "Yes" radio (controlling) could toggle the content open with further content, and the "No" radio in the same name group (not controlling) could toggle the content closed again.

With this latest change, how do you close it once it's open?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure the method you show/hide content for radio groups works with the change in that scenario:

If you click 'yes', all content controlled by the group would be hidden by the loop on line 87. The condition on line 92 will return true and so show the content controlled by the event target.

If you click 'no', all content controlled by the group would still be hidden by the loop on line 87. The condition on line 92 will return false so no content is shown.

I ran the tests after making this change and they were green so figured it was ok. I made a jsbin to play around with it which seems to work as expected: https://jsbin.com/nademaxaxo/edit?html,js,output

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @tombye, that's great.

Forgot I wrote a test to cover that very scenario!
https://github.com/alphagov/govuk_frontend_toolkit/pull/315/files#diff-1cb888219674ec4080d3a7d2bdca5117R183

Still works as intended then 👍

var $radios = $control.closest('form').find(selector)

// Hide content for radios in group
$radios.each(function () {
hideToggledContent($(this))
})

// Select content for this control
if ($control.is('[aria-controls]')) {
showToggledContent($control, $content)
}
}

// Handle checkbox show/hide
function handleCheckboxContent ($control, $content) {
// Show checkbox content
if ($control.is(':checked')) {
showToggledContent($control, $content)
} else { // Hide checkbox content
hideToggledContent($control, $content)
}
}

// Set up event handlers etc
function init ($container, elementSelector, eventSelectors, handler) {
$container = $container || $(document.body)

// Handle control clicks
function deferred () {
var $control = $(this)
handler($control, getToggledContent($control))
}

// Prepare ARIA attributes
var $controls = $(elementSelector)
$controls.each(initToggledContent)

// Handle events
$.each(eventSelectors, function (idx, eventSelector) {
$container.on('click.' + selectors.namespace, eventSelector, deferred)
})

// Any already :checked on init?
if ($controls.is(':checked')) {
$controls.filter(':checked').each(deferred)
}
}

// Get event selectors for all radio groups
function getEventSelectorsForRadioGroups () {
var radioGroups = []

// Build an array of radio group selectors
return $(selectors.radio).map(function () {
var groupName = $(this).attr('name')

if ($.inArray(groupName, radioGroups) === -1) {
radioGroups.push(groupName)
return 'input[type="radio"][name="' + $(this).attr('name') + '"]'
}
return null
})
}

// Set up radio show/hide content for container
self.showHideRadioToggledContent = function ($container) {
init($container, selectors.radio, getEventSelectorsForRadioGroups(), handleRadioContent)
}

// Set up checkbox show/hide content for container
self.showHideCheckboxToggledContent = function ($container) {
init($container, selectors.checkbox, [selectors.checkbox], handleCheckboxContent)
}

// Remove event handlers
self.destroy = function ($container) {
$container = $container || $(document.body)
$container.off('.' + selectors.namespace)
}
}

ShowHideContent.prototype.init = function ($container) {
this.showHideRadioToggledContent($container)
this.showHideCheckboxToggledContent($container)
}

GOVUK.ShowHideContent = ShowHideContent
global.GOVUK = GOVUK
})(window)
8 changes: 5 additions & 3 deletions spec/manifest.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// Paths are relative to the /spec/support folder
var manifest = {
support : [
support: [
'../../node_modules/jquery/dist/jquery.js',
'../../javascripts/govuk/modules.js',
'../../javascripts/govuk/modules/auto-track-event.js',
'../../javascripts/govuk/multivariate-test.js',
'../../javascripts/govuk/primary-links.js',
'../../javascripts/govuk/shim-links-with-button-role.js',
'../../javascripts/govuk/show-hide-content.js',
'../../javascripts/govuk/stick-at-top-when-scrolling.js',
'../../javascripts/govuk/stop-scrolling-at-footer.js',
'../../javascripts/govuk/selection-buttons.js',
Expand All @@ -17,12 +18,13 @@ var manifest = {
'../../javascripts/govuk/analytics/download-link-tracker.js',
'../../javascripts/govuk/analytics/mailto-link-tracker.js'
],
test : [
test: [
'../unit/modules.spec.js',
'../unit/Modules/auto-track-event.spec.js',
'../unit/multivariate-test.spec.js',
'../unit/primary-links.spec.js',
'../unit/shim-links-with-button-role.spec.js',
'../unit/show-hide-content.spec.js',
'../unit/stick-at-top-when-scrolling.spec.js',
'../unit/selection-button.spec.js',
'../unit/analytics/google-analytics-universal-tracker.spec.js',
Expand All @@ -32,4 +34,4 @@ var manifest = {
'../unit/analytics/download-link-tracker.spec.js',
'../unit/analytics/mailto-link-tracker.spec.js'
]
};
}
8 changes: 4 additions & 4 deletions spec/support/LocalTestRunner.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
<head>
<title>Jasmine Test Runner</title>

<link rel="stylesheet" type="text/css" href="../../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
<link rel="stylesheet" type="text/css" href="../../node_modules/grunt-contrib-jasmine/node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
<style>
#wrapper { display: none; }
</style>

<!-- JASMINE FILES -->
<script type="text/javascript" src="../../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script type="text/javascript" src="../../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script type="text/javascript" src="../../node_modules/grunt-contrib-jasmine/node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script type="text/javascript" src="../../node_modules/grunt-contrib-jasmine/node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>

<script type="text/javascript" src="../../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script type="text/javascript" src="../../node_modules/grunt-contrib-jasmine/node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<script type="text/javascript" src="./load.js"></script>
</head>
<body>
Expand Down
Loading