mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-23 11:51:05 -05:00
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.
This commit is contained in:
142
app/assets/javascripts/collapsibleCheckboxes.js
Normal file
142
app/assets/javascripts/collapsibleCheckboxes.js
Normal file
@@ -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 = $(`<h${headingLevel} class="heading-small">${$legend.text().trim()}</h${headingLevel}>`);
|
||||
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 = $(`<p class="selection-summary folder-selection">
|
||||
<span class="selection-summary__text"></span> <button class="button button-secondary">Change</button>
|
||||
</p>`);
|
||||
|
||||
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 = $(`<div class="selection-footer">
|
||||
<button class="button button-secondary">
|
||||
Done
|
||||
</button>
|
||||
</div>`);
|
||||
|
||||
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));
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
) %}
|
||||
<div class="{{ "radios" if input == "radio" else "checkboxes" }}-nested">
|
||||
{{ 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) %}
|
||||
<div class="form-group {% if field.errors %} form-group-error{% endif %}">
|
||||
<fieldset>
|
||||
<legend class="{{ 'form-label' if not hide_legend else '' }}">
|
||||
{% macro select_wrapper(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text") %}
|
||||
{% set is_collapsible = collapsible_opts|length %}
|
||||
<div class="form-group {% if field.errors %} form-group-error{% endif %}"{% if is_collapsible %} data-module="collapsible-checkboxes"{% if collapsible_opts.field %} data-field-label="{{ collapsible_opts.field }}"{% endif %}{% endif %}>
|
||||
<fieldset id="{{ field.id }}">
|
||||
<legend class="{{ 'form-label' if not hide_legend else '' }}{% if legend_style != 'text' %} {{ legend_style }}{% endif %}">
|
||||
{% if hide_legend %}<span class="visually-hidden">{% endif %}
|
||||
{{ field.label.text|safe }}
|
||||
{% if hide_legend %}</span>{% endif %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</fieldset>
|
||||
|
||||
{% 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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user