Skip to content

Commit

Permalink
Merge pull request #315 from alphagov/add-govuk-show-hide-content-js
Browse files Browse the repository at this point in the history
Add GOVUK.ShowHideContent JavaScript
  • Loading branch information
gemmaleigh authored Sep 5, 2016
2 parents 5997c64 + a8ae9ee commit 157f5a3
Show file tree
Hide file tree
Showing 6 changed files with 463 additions and 7 deletions.
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]'
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

0 comments on commit 157f5a3

Please sign in to comment.