From 12ee085a578baf376746e8bd4aed038d9b477633 Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Fri, 16 Oct 2015 11:30:33 +0100 Subject: [PATCH 1/3] Add a checkbox toggle based on GOV.UK elements Create a checkbox toggle so that content can be conditionally revealed and forms simplified. The JS is based on GOV.UK elements patterns, setting aria controls and aria hidden attributes dynamically. A checkbox and a toggle target are linked with an ID. The module checks the current state of the form and then hides or shows content based on that. --- .../modules/checkbox_toggle.js.js | 27 +++++++ .../style_guide/index.html.erb | 38 +++++++++- spec/javascripts/spec/checkbox_toggle.spec.js | 73 +++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js.js create mode 100644 spec/javascripts/spec/checkbox_toggle.spec.js diff --git a/app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js.js b/app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js.js new file mode 100644 index 0000000..ed9061c --- /dev/null +++ b/app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js.js @@ -0,0 +1,27 @@ +(function(Modules) { + "use strict"; + + Modules.CheckboxToggle = function() { + this.start = function(element) { + var $checkbox = element.find('input[type="checkbox"]'), + target = element.data('target'), + $target = $('#' + target); + + $checkbox.attr('aria-controls', target); + toggle(); + $checkbox.on('click', toggle); + + function toggle() { + var state = $checkbox.is(':checked'); + $target.toggle(state); + setAriaAttr(state) + } + + function setAriaAttr(state) { + $checkbox.attr('aria-expanded', state); + $target.attr('aria-hidden', !state); + } + }; + }; + +})(window.GOVUKAdmin.Modules); diff --git a/app/views/govuk_admin_template/style_guide/index.html.erb b/app/views/govuk_admin_template/style_guide/index.html.erb index 7663d05..9764c35 100644 --- a/app/views/govuk_admin_template/style_guide/index.html.erb +++ b/app/views/govuk_admin_template/style_guide/index.html.erb @@ -20,7 +20,41 @@
-

Input widths

+

Forms

+ +

Conditionally revealing content

+
+
+

+ Based on the GOV.UK elements pattern, ticking a checkbox or selecting a radio element can toggle the display of further content or fields. +

+
<div class="checkbox"
+     data-module="toggle-checkbox"
+     data-target="target-id">
+  <label>
+    <input type="checkbox"> Check me
+  </label>
+  <div id="target-id">Content</div>
+</div>
+
+
+
+ +
Content that appears
+
+
+
+ +
When the checkbox is checked, the content will display.
+
+
+
+ +

Input widths

Bootstrap 3 inputs always extend to the size of their container. The recommendation from Bootstrap is to wrap form elements with grid classes. Sometimes this isn’t suitable, for instance if a page isn’t using a grid.

@@ -57,7 +91,7 @@
-

Inputs in a grid

+

Inputs in a grid

diff --git a/spec/javascripts/spec/checkbox_toggle.spec.js b/spec/javascripts/spec/checkbox_toggle.spec.js new file mode 100644 index 0000000..82f6974 --- /dev/null +++ b/spec/javascripts/spec/checkbox_toggle.spec.js @@ -0,0 +1,73 @@ +describe('A checkbox toggle module', function() { + "use strict"; + + var root = window, + toggle, + toggleElement; + + beforeEach(function() { + toggleElement = $('
\ + \ +
Content
\ + '); + + toggle = new GOVUKAdmin.Modules.CheckboxToggle(); + $('body').append(toggleElement); + }); + + afterEach(function() { + toggleElement.remove(); + }); + + describe('when starting', function() { + it('hides content if unchecked', function() { + toggle.start(toggleElement); + + expectToggleToBeHidden(); + toggleCheckbox(); + expectToggleToBeVisible(); + }); + + it('shows content if checked', function() { + toggleCheckbox(); + toggle.start(toggleElement); + + expectToggleToBeVisible(); + toggleCheckbox(); + expectToggleToBeHidden(); + }); + + it('adds an `aria-controls` attribute linking checkbox to target', function() { + toggle.start(toggleElement); + var ariaControls = toggleElement.find('input[type="checkbox"]').attr('aria-controls'); + expect(ariaControls).toBe('target-id'); + }); + }); + + it('toggles content when checkbox changes', function() { + toggle.start(toggleElement); + + expectToggleToBeHidden(); + toggleCheckbox(); + expectToggleToBeVisible(); + toggleCheckbox(); + expectToggleToBeHidden(); + }); + + function toggleCheckbox() { + toggleElement.find('input[type="checkbox"]').click(); + } + + function expectToggleToBeVisible() { + expect(toggleElement.find('#target-id').is(':visible')).toBe(true); + expect(toggleElement.find('#target-id').attr('aria-hidden')).toBe('false'); + } + + function expectToggleToBeHidden() { + expect(toggleElement.find('#target-id').is(':visible')).toBe(false); + expect(toggleElement.find('#target-id').attr('aria-hidden')).toBe('true'); + } + +}); From 484d42b978c27eceacb9798ab8918b04a7741212 Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Fri, 16 Oct 2015 12:01:23 +0100 Subject: [PATCH 2/3] Modify checkbox module to allow multiples Use a single module to handle the toggling and targets of multiple checkboxes. Each checkbox should define a target. --- .../modules/checkbox_toggle.js | 31 ++++++++++ .../modules/checkbox_toggle.js.js | 27 -------- .../style_guide/index.html.erb | 16 ++--- spec/javascripts/spec/checkbox_toggle.spec.js | 61 +++++++++++++++---- 4 files changed, 86 insertions(+), 49 deletions(-) create mode 100644 app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js delete mode 100644 app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js.js diff --git a/app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js b/app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js new file mode 100644 index 0000000..3f171e7 --- /dev/null +++ b/app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js @@ -0,0 +1,31 @@ +(function(Modules) { + "use strict"; + + Modules.CheckboxToggle = function() { + this.start = function(element) { + var $checkboxes = element.find('input[type="checkbox"][data-target]'); + + $checkboxes.each(function() { + var $checkbox = $(this), + target = $checkbox.data('target'), + $target = $('#' + target); + + $checkbox.attr('aria-controls', target); + toggle(); + $checkbox.on('click', toggle); + + function toggle() { + var state = $checkbox.is(':checked'); + $target.toggle(state); + setAriaAttr(state) + } + + function setAriaAttr(state) { + $checkbox.attr('aria-expanded', state); + $target.attr('aria-hidden', !state); + } + }); + }; + }; + +})(window.GOVUKAdmin.Modules); diff --git a/app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js.js b/app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js.js deleted file mode 100644 index ed9061c..0000000 --- a/app/assets/javascripts/govuk-admin-template/modules/checkbox_toggle.js.js +++ /dev/null @@ -1,27 +0,0 @@ -(function(Modules) { - "use strict"; - - Modules.CheckboxToggle = function() { - this.start = function(element) { - var $checkbox = element.find('input[type="checkbox"]'), - target = element.data('target'), - $target = $('#' + target); - - $checkbox.attr('aria-controls', target); - toggle(); - $checkbox.on('click', toggle); - - function toggle() { - var state = $checkbox.is(':checked'); - $target.toggle(state); - setAriaAttr(state) - } - - function setAriaAttr(state) { - $checkbox.attr('aria-expanded', state); - $target.attr('aria-hidden', !state); - } - }; - }; - -})(window.GOVUKAdmin.Modules); diff --git a/app/views/govuk_admin_template/style_guide/index.html.erb b/app/views/govuk_admin_template/style_guide/index.html.erb index 9764c35..c921484 100644 --- a/app/views/govuk_admin_template/style_guide/index.html.erb +++ b/app/views/govuk_admin_template/style_guide/index.html.erb @@ -28,26 +28,22 @@

Based on the GOV.UK elements pattern, ticking a checkbox or selecting a radio element can toggle the display of further content or fields.

-
<div class="checkbox"
-     data-module="toggle-checkbox"
-     data-target="target-id">
+    
<div class="checkbox" data-module="toggle-checkbox">
   <label>
-    <input type="checkbox"> Check me
+    <input type="checkbox" data-target="target-id"> Check me
   </label>
   <div id="target-id">Content</div>
 </div>
-
+
Content that appears
-
-
-
+
When the checkbox is checked, the content will display.
diff --git a/spec/javascripts/spec/checkbox_toggle.spec.js b/spec/javascripts/spec/checkbox_toggle.spec.js index 82f6974..0698f36 100644 --- a/spec/javascripts/spec/checkbox_toggle.spec.js +++ b/spec/javascripts/spec/checkbox_toggle.spec.js @@ -6,9 +6,12 @@ describe('A checkbox toggle module', function() { toggleElement; beforeEach(function() { - toggleElement = $('
\ + toggleElement = $('
\ \ + \
Content
\ '); @@ -41,12 +44,12 @@ describe('A checkbox toggle module', function() { it('adds an `aria-controls` attribute linking checkbox to target', function() { toggle.start(toggleElement); - var ariaControls = toggleElement.find('input[type="checkbox"]').attr('aria-controls'); + var ariaControls = toggleElement.find('input[type="checkbox"][data-target]').attr('aria-controls'); expect(ariaControls).toBe('target-id'); }); }); - it('toggles content when checkbox changes', function() { + it('toggles content when checkbox with a target changes', function() { toggle.start(toggleElement); expectToggleToBeHidden(); @@ -56,18 +59,52 @@ describe('A checkbox toggle module', function() { expectToggleToBeHidden(); }); - function toggleCheckbox() { - toggleElement.find('input[type="checkbox"]').click(); + it('does nothing when a checkbox without a target changes', function() { + toggle.start(toggleElement); + + expectToggleToBeHidden(); + toggleElement.find('.without-target').click(); + expectToggleToBeHidden(); + toggleElement.find('.without-target').click(); + expectToggleToBeHidden(); + }); + + it('can handle multiple checkboxes with toggles', function() { + toggleElement.find('div').append('\ + \ +
Content
\ + '); + toggle.start(toggleElement); + + expectToggleToBeHidden(); + expectToggleToBeHidden('#second-target-id'); + toggleCheckbox(); + expectToggleToBeVisible(); + expectToggleToBeHidden('#second-target-id'); + toggleCheckbox('.second-with-target'); + expectToggleToBeVisible(); + expectToggleToBeVisible('#second-target-id'); + }); + + function toggleCheckbox(selector) { + selector = selector || '.with-target'; + toggleElement.find(selector).click(); } - function expectToggleToBeVisible() { - expect(toggleElement.find('#target-id').is(':visible')).toBe(true); - expect(toggleElement.find('#target-id').attr('aria-hidden')).toBe('false'); + function expectToggleToBeVisible(id) { + id = id || '#target-id'; + + expect(toggleElement.find(id).is(':visible')).toBe(true); + expect(toggleElement.find(id).attr('aria-hidden')).toBe('false'); } - function expectToggleToBeHidden() { - expect(toggleElement.find('#target-id').is(':visible')).toBe(false); - expect(toggleElement.find('#target-id').attr('aria-hidden')).toBe('true'); + function expectToggleToBeHidden(id) { + id = id || '#target-id'; + + expect(toggleElement.find(id).is(':visible')).toBe(false); + expect(toggleElement.find(id).attr('aria-hidden')).toBe('true'); } }); From 71e7f6110b37c2b3537ace063a79fe5c5f01eb4f Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Fri, 16 Oct 2015 15:46:53 +0100 Subject: [PATCH 3/3] Add a radio toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a radio toggle so that content can be conditionally revealed and forms simplified. The JS is based on GOV.UK elements patterns, setting aria controls and aria hidden attributes dynamically. A radio and a toggle target are linked with an ID. The module checks the current state of the form and then hides or shows content based on that. The radio example is a little more convoluted than the checkbox equivalent. It needs to listen for changes to all radio elements with the same name – when one radio is selected there is no corresponding change event on the others. --- .../modules/radio_toggle.js | 46 +++++++ .../style_guide/index.html.erb | 20 +++ spec/javascripts/spec/radio_toggle.spec.js | 116 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 app/assets/javascripts/govuk-admin-template/modules/radio_toggle.js create mode 100644 spec/javascripts/spec/radio_toggle.spec.js diff --git a/app/assets/javascripts/govuk-admin-template/modules/radio_toggle.js b/app/assets/javascripts/govuk-admin-template/modules/radio_toggle.js new file mode 100644 index 0000000..768f5e9 --- /dev/null +++ b/app/assets/javascripts/govuk-admin-template/modules/radio_toggle.js @@ -0,0 +1,46 @@ +(function(Modules) { + "use strict"; + + Modules.RadioToggle = function() { + this.start = function(element) { + var $radios = element.find('input[type="radio"][data-target]'), + radioGroups = {}; + + $radios.each(function() { + var $radio = $(this), + target = $radio.data('target'), + $target = $('#' + target); + + radioGroups[$radio.attr('name')] = true; + + $radio.attr('aria-controls', target); + $radio.on('radioValueChanged', toggle); + + function toggle() { + var state = $radio.is(':checked'); + $target.toggle(state); + setAriaAttr(state); + } + + function setAriaAttr(state) { + $radio.attr('aria-expanded', state); + $target.attr('aria-hidden', !state); + } + }); + + listenToChangesOnRadioGroups(); + triggerToggles(); + + function listenToChangesOnRadioGroups() { + $.map(radioGroups, function(v, radioGroupName) { + element.on('change', 'input[type="radio"][name="'+radioGroupName+'"]', triggerToggles); + }); + } + + function triggerToggles() { + $radios.trigger('radioValueChanged'); + } + }; + }; + +})(window.GOVUKAdmin.Modules); diff --git a/app/views/govuk_admin_template/style_guide/index.html.erb b/app/views/govuk_admin_template/style_guide/index.html.erb index c921484..0e55aa1 100644 --- a/app/views/govuk_admin_template/style_guide/index.html.erb +++ b/app/views/govuk_admin_template/style_guide/index.html.erb @@ -47,6 +47,26 @@
When the checkbox is checked, the content will display.
+
+
+
+ +
From Radio 1
+
+
+ +
From Radio 2
+
+
+ +
+
diff --git a/spec/javascripts/spec/radio_toggle.spec.js b/spec/javascripts/spec/radio_toggle.spec.js new file mode 100644 index 0000000..4c52af9 --- /dev/null +++ b/spec/javascripts/spec/radio_toggle.spec.js @@ -0,0 +1,116 @@ +describe('A radio toggle module', function() { + "use strict"; + + var root = window, + toggle, + element; + + beforeEach(function() { + element = $('
\ + \ +
\ + \ +
\ + \ +
'); + + toggle = new GOVUKAdmin.Modules.RadioToggle(); + $('body').append(element); + }); + + afterEach(function() { + element.remove(); + }); + + describe('when starting', function() { + it('hides content if not selected', function() { + toggle.start(element); + expectToggleToBeHidden('#radio-1-target'); + }); + + it('shows content if selected', function() { + toggleRadio('.radio-1'); + toggle.start(element); + expectToggleToBeVisible('#radio-1-target'); + }); + + it('adds an `aria-controls` attribute linking radio to each target', function() { + toggle.start(element); + var $ariaControls = element.find('input[type="radio"][data-target]'); + expect($ariaControls.eq(0).attr('aria-controls')).toBe('radio-1-target'); + expect($ariaControls.eq(1).attr('aria-controls')).toBe('radio-2-target'); + }); + }); + + it('toggles content when radio values change', function() { + toggle.start(element); + + expectToggleToBeHidden('#radio-1-target'); + expectToggleToBeHidden('#radio-2-target'); + + toggleRadio('.radio-1'); + expectToggleToBeVisible('#radio-1-target'); + expectToggleToBeHidden('#radio-2-target'); + + toggleRadio('.radio-2'); + expectToggleToBeHidden('#radio-1-target'); + expectToggleToBeVisible('#radio-2-target'); + + toggleRadio('.radio-3'); + expectToggleToBeHidden('#radio-1-target'); + expectToggleToBeHidden('#radio-2-target'); + }); + + it('can handle multiple radio groups with toggles', function() { + element.append('
\ + \ +
\ + \ +
'); + + toggle.start(element); + + expectToggleToBeHidden('#radio-1-target'); + expectToggleToBeHidden('#radio-2-target'); + expectToggleToBeHidden('#radio-4-target'); + + toggleRadio('.radio-1'); + toggleRadio('.radio-4'); + expectToggleToBeVisible('#radio-1-target'); + expectToggleToBeVisible('#radio-4-target'); + + toggleRadio('.radio-2'); + expectToggleToBeHidden('#radio-1-target'); + expectToggleToBeVisible('#radio-2-target'); + expectToggleToBeVisible('#radio-4-target'); + + toggleRadio('.radio-3'); + expectToggleToBeHidden('#radio-1-target'); + expectToggleToBeHidden('#radio-2-target'); + expectToggleToBeVisible('#radio-4-target'); + }); + + function toggleRadio(selector) { + element.find(selector).click(); + } + + function expectToggleToBeVisible(id) { + expect(element.find(id).is(':visible')).toBe(true); + expect(element.find(id).attr('aria-hidden')).toBe('false'); + } + + function expectToggleToBeHidden(id) { + expect(element.find(id).is(':visible')).toBe(false); + expect(element.find(id).attr('aria-hidden')).toBe('true'); + } +});