From 33d074c00aaf330b105a81a1bb39c3a6df62e7d1 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Tue, 7 May 2019 16:08:17 +0100 Subject: [PATCH] Allow nested checkboxes to be collapsible Expands the API of the macro to allow nested checkboxes to have a summary tracking the current selection, the fieldset to expand/collapse and buttons to be added to allow jumping between states. Includes making 'Done' button inline on mobile. Helps differentiate it form the form submit. --- .../javascripts/collapsibleCheckboxes.js | 142 ++++++++++++++++++ .../stylesheets/components/checkboxes.scss | 52 +++++++ app/templates/components/checkbox.html | 8 +- app/templates/components/select-input.html | 17 ++- .../views/manage-users/permissions.html | 2 +- .../templates/manage-template-folder.html | 2 +- gulpfile.js | 1 + 7 files changed, 210 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/collapsibleCheckboxes.js diff --git a/app/assets/javascripts/collapsibleCheckboxes.js b/app/assets/javascripts/collapsibleCheckboxes.js new file mode 100644 index 000000000..97957b3d1 --- /dev/null +++ b/app/assets/javascripts/collapsibleCheckboxes.js @@ -0,0 +1,142 @@ +(function (Modules) { + "use strict"; + + Modules.CollapsibleCheckboxes = function() { + const _focusTextElement = ($el) => { + $el + .attr('tabindex', '-1') + .focus(); + }; + + this.start = function(component) { + this.$formGroup = $(component); + this.$fieldset = this.$formGroup.find('fieldset'); + this.$checkboxes = this.$fieldset.find('input[type=checkbox]'); + this.summary.$el = this.$formGroup.find('.selection-summary'); + this.fieldLabel = this.$formGroup.data('fieldLabel'); + this.total = this.$checkboxes.length; + + this.addHeadingHideLegend(); + this.addFooterAndDoneButton(); + + // create summary from component pieces and match text to current selection + this.summary.$el = this.summary.create(this.getSelection(), this.total); + this.summary.update(this.getSelection(), this.total, this.fieldLabel); + this.$fieldset.before(this.summary.$el); + + // hide checkboxes + this.$fieldset.hide(); + this.expanded = false; + + // set semantic relationships with aria attributes + this.addARIAToButtons(); + + this.bindEvents(); + }; + this.getSelection = function() { return this.$checkboxes.filter(':checked').length; }; + this.addHeadingHideLegend = function() { + const headingLevel = this.$formGroup.data('heading-level') || '2'; + const $legend = this.$fieldset.find('legend'); + + this.$heading = $(`${$legend.text().trim()}`); + this.$fieldset.before(this.$heading); + + $legend.addClass('visuallyhidden'); + }; + this.summary = { + templates: { + all: (selection, total, field) => `All ${field}s`, + some: (selection, total, field) => `${selection} of ${total} ${field}s`, + none: (selection, total, field) => { + if (field === 'folder') { + return "No folders (only templates outside a folder)"; + } else { + return `No ${field}s`; + } + } + }, + create: function() { + const $el = $(`

+ +

`); + + this.$text = $el.find('span'); + this.$changeButton = $el.find('button'); + + return $el; + }, + update: function(selection, total, field) { + let template; + + if (selection === total) { + template = 'all'; + } else if (selection > 0) { + template = 'some'; + } else { + template = 'none'; + } + + this.$text.html(this.templates[template](selection, total, field)); + } + }; + this.addFooterAndDoneButton = function () { + this.$footer = $(``); + + this.$doneButton = this.$footer.find('.button'); + this.$fieldset.append(this.$footer); + }; + this.addARIAToButtons = function () { + const aria = { + 'aria-expanded': this.expanded, + 'aria-controls': this.$fieldset.attr('id') + }; + + this.summary.$changeButton.attr(aria); + this.$doneButton.attr(aria); + }; + this.expand = function(e) { + if (e !== undefined) { e.preventDefault(); } + + if (!this.expanded) { + this.$fieldset.show(); + this.summary.$changeButton.attr('aria-expanded', true); + this.$doneButton.attr('aria-expanded', true); + this.expanded = true; + } + + // shift focus whether expanded or not + _focusTextElement(this.$fieldset); + }; + this.collapse = function(e) { + if (e !== undefined) { e.preventDefault(); } + + if (this.expanded) { + this.$fieldset.hide(); + this.summary.$changeButton.attr('aria-expanded', false); + this.$doneButton.attr('aria-expanded', false); + this.expanded = false; + } + + // shift focus whether expanded or not + _focusTextElement(this.summary.$text); + }; + this.handleSelection = function(e) { + this.summary.update(this.getSelection(), this.total, this.fieldLabel); + }; + this.bindEvents = function() { + const self = this; + + this.summary.$changeButton.on('click', this.expand.bind(this)); + this.$doneButton.on('click', this.collapse.bind(this)); + this.$checkboxes.on('click', this.handleSelection.bind(this)); + + // take summary out of tab order when focus moves + this.summary.$el.on('blur', (e) => $(this).attr('tabindex', '-1')); + }; + }; + +}(window.GOVUK.Modules)); diff --git a/app/assets/stylesheets/components/checkboxes.scss b/app/assets/stylesheets/components/checkboxes.scss index 40ca600d4..6e6b5414c 100644 --- a/app/assets/stylesheets/components/checkboxes.scss +++ b/app/assets/stylesheets/components/checkboxes.scss @@ -1,3 +1,31 @@ +.selection-summary { + + .selection-summary__text { + display: inline-block; + padding: .526315em .789473em .263157em 40px; + padding-left: 40px; + background-image: file-url('folder-black-bold.svg'); + background-repeat: no-repeat; + background-size: auto 20px; + background-position: 0px 8px; + + &:focus { + outline: none; + } + } + + @include ie-lte(8) { + background-image: file-url('folder-blue-bold.png'); + } + + // revert full-width button for smaller screens + .button { + display: inline-block; + width: auto; + } + +} + .checkboxes-nested { margin-bottom: 10px; @@ -42,3 +70,27 @@ } } + +.selection-footer { + clear: both; + padding-top: $gutter / 6; + + @include media(tablet) { + padding-top: $gutter / 3; + } + + .button-secondary { + // revert full-width button for smaller screens + display: inline-block; + width: auto; + } +} + +// styles specific to the collapsible checkboxes module +[data-module=collapsible-checkboxes] { + + fieldset:focus { + outline: none; + } + +} diff --git a/app/templates/components/checkbox.html b/app/templates/components/checkbox.html index 51594eea6..3647c4f1c 100644 --- a/app/templates/components/checkbox.html +++ b/app/templates/components/checkbox.html @@ -19,13 +19,13 @@ {% endmacro %} -{% macro checkboxes_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False) %} - {{ select_nested(field, child_map, hint, disable, option_hints, hide_legend, input="checkbox") }} +{% macro checkboxes_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text") %} + {{ select_nested(field, child_map, hint, disable, option_hints, hide_legend, collapsible_opts, legend_style, input="checkbox") }} {% endmacro %} -{% macro checkboxes(field, hint=None, disable=[], option_hints={}, hide_legend=False) %} - {{ select(field, hint, disable, option_hints, hide_legend, input="checkbox") }} +{% macro checkboxes(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}) %} + {{ select(field, hint, disable, option_hints, hide_legend, collapsible_opts, input="checkbox") }} {% endmacro %} diff --git a/app/templates/components/select-input.html b/app/templates/components/select-input.html index b3cfbb9e4..a42f00a9f 100644 --- a/app/templates/components/select-input.html +++ b/app/templates/components/select-input.html @@ -1,6 +1,6 @@ -{% macro select(field, hint=None, disable=[], option_hints={}, hide_legend=False, input="radio") %} +{% macro select(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text", input="radio") %} {% call select_wrapper( - field, hint, disable, option_hints, hide_legend + field, hint, disable, option_hints, hide_legend, collapsible_opts, legend_style ) %} {% for option in field %} {{ select_input(option, disable, option_hints, input=input) }} @@ -24,9 +24,9 @@ {% endmacro %} -{% macro select_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, input="radio") %} +{% macro select_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text", input="radio") %} {% call select_wrapper( - field, hint, disable, option_hints, hide_legend + field, hint, disable, option_hints, hide_legend, collapsible_opts, legend_style ) %}
{{ select_list(child_map[None], child_map, disable, option_hints, input=input) }} @@ -35,10 +35,11 @@ {% endmacro %} -{% macro select_wrapper(field, hint=None, disable=[], option_hints={}, hide_legend=False) %} -
-
- +{% macro select_wrapper(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text") %} + {% set is_collapsible = collapsible_opts|length %} +
+
+ {% if hide_legend %}{% endif %} {{ field.label.text|safe }} {% if hide_legend %}{% endif %} diff --git a/app/templates/views/manage-users/permissions.html b/app/templates/views/manage-users/permissions.html index 801424c89..8d839b9aa 100644 --- a/app/templates/views/manage-users/permissions.html +++ b/app/templates/views/manage-users/permissions.html @@ -14,7 +14,7 @@
{% if current_service.has_permission("edit_folder_permissions") and form.folder_permissions.all_template_folders %} - {{ checkboxes_nested(form.folder_permissions, form.folder_permissions.children()) }} + {{ checkboxes_nested(form.folder_permissions, form.folder_permissions.children(), hide_legend=True, collapsible_opts={ 'field': 'folder' }) }} {% endif %} {% if service_has_email_auth %} diff --git a/app/templates/views/templates/manage-template-folder.html b/app/templates/views/templates/manage-template-folder.html index f7bbb37a0..56367fac6 100644 --- a/app/templates/views/templates/manage-template-folder.html +++ b/app/templates/views/templates/manage-template-folder.html @@ -27,7 +27,7 @@ {{ textbox(form.name) }} {% if current_service.has_permission("edit_folder_permissions") %} {% if current_user.has_permissions("manage_service") and form.users_with_permission.all_service_users %} - {{ checkboxes(form.users_with_permission) }} + {{ checkboxes(form.users_with_permission, collapsible_opts={ 'field': 'team member' }) }} {% endif %} {% endif %} diff --git a/gulpfile.js b/gulpfile.js index 648bf86eb..305323158 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -95,6 +95,7 @@ const javascripts = () => { paths.src + 'javascripts/previewPane.js', paths.src + 'javascripts/colourPreview.js', paths.src + 'javascripts/templateFolderForm.js', + paths.src + 'javascripts/collapsibleCheckboxes.js', paths.src + 'javascripts/main.js' ]) .pipe(plugins.prettyerror())