mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-17 04:59:37 -04:00
Revert "Merge pull request #2969 from alphagov/revert-2956-progressively-enhance-folder-permissions"
This reverts commit8266f3d65c, reversing changes made tob2a38fe222.
This commit is contained in:
BIN
app/assets/images/folder-black.png
Normal file
BIN
app/assets/images/folder-black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 321 B |
1
app/assets/images/folder-black.svg
Normal file
1
app/assets/images/folder-black.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 20" width="26" height="20"><polygon fill="#DEE0E2" points="1,19.1 1,1 9.8,1 11.3,5 25,5 25,19.1"/><path fill="0B0C0C" d="M9.2,1.9l1,2.8l0.5,1.2H12h12.1v12.2H1.9v-11V5.2V1.9H9.2 M10.5,0h-0.2H0v5.2v1.9V20h26V4H12L10.5,0L10.5,0z"/></svg>
|
||||||
|
After Width: | Height: | Size: 291 B |
170
app/assets/javascripts/collapsibleCheckboxes.js
Normal file
170
app/assets/javascripts/collapsibleCheckboxes.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const GOVUK = global.GOVUK;
|
||||||
|
|
||||||
|
function Summary (module) {
|
||||||
|
this.module = module;
|
||||||
|
this.$el = module.$formGroup.find('.selection-summary');
|
||||||
|
this.fieldLabel = module.fieldLabel;
|
||||||
|
this.total = module.total;
|
||||||
|
this.addContent();
|
||||||
|
this.update(module.getSelection());
|
||||||
|
}
|
||||||
|
Summary.prototype.templates = {
|
||||||
|
all: (selection, total, field) => `All ${field}s`,
|
||||||
|
some: (selection, total, field) => `${selection} of ${total} ${field}s`,
|
||||||
|
none: (selection, total, field) => ({
|
||||||
|
"folder": "No folders (only templates outside a folder)",
|
||||||
|
"team member": "No team members (only you)"
|
||||||
|
}[field] || `No ${field}s`)
|
||||||
|
};
|
||||||
|
Summary.prototype.addContent = function() {
|
||||||
|
this.$text = $(`<p class="selection-summary__text" />`);
|
||||||
|
|
||||||
|
if (this.fieldLabel === 'folder') { this.$text.addClass('selection-summary__text--folders'); }
|
||||||
|
|
||||||
|
this.$el.append(this.$text);
|
||||||
|
};
|
||||||
|
Summary.prototype.update = function(selection) {
|
||||||
|
let template;
|
||||||
|
|
||||||
|
if (selection === this.total) {
|
||||||
|
template = 'all';
|
||||||
|
} else if (selection > 0) {
|
||||||
|
template = 'some';
|
||||||
|
} else {
|
||||||
|
template = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$text.html(this.templates[template](selection, this.total, this.fieldLabel));
|
||||||
|
};
|
||||||
|
Summary.prototype.bindEvents = function () {
|
||||||
|
// take summary out of tab order when focus moves
|
||||||
|
this.$el.on('blur', (e) => $(this).attr('tabindex', '-1'));
|
||||||
|
};
|
||||||
|
|
||||||
|
function Footer (module) {
|
||||||
|
this.module = module;
|
||||||
|
this.fieldLabel = module.fieldLabel;
|
||||||
|
this.fieldsetId = module.$fieldset.attr('id');
|
||||||
|
this.$el = this.getEl(this.module.expanded);
|
||||||
|
this.module.$formGroup.append(this.$el);
|
||||||
|
}
|
||||||
|
Footer.prototype.buttonContent = {
|
||||||
|
change: (fieldLabel) => `Choose ${fieldLabel}s`,
|
||||||
|
done: (fieldLabel) => `Done<span class="visuallyhidden"> choosing ${fieldLabel}s</span>`
|
||||||
|
};
|
||||||
|
Footer.prototype.getEl = function (expanded) {
|
||||||
|
const buttonState = expanded ? 'done' : 'change';
|
||||||
|
const buttonContent = this.buttonContent[buttonState](this.fieldLabel);
|
||||||
|
const stickyClass = expanded ? ' js-stick-at-bottom-when-scrolling' : '';
|
||||||
|
|
||||||
|
return $(`<div class="selection-footer${stickyClass}">
|
||||||
|
<button
|
||||||
|
class="button button-secondary"
|
||||||
|
aria-expanded="${expanded ? 'true' : 'false'}"
|
||||||
|
aria-controls="${this.fieldsetId}">
|
||||||
|
${buttonContent}
|
||||||
|
</button>
|
||||||
|
</div>`);
|
||||||
|
};
|
||||||
|
Footer.prototype.update = function (expanded) {
|
||||||
|
this.$el.remove();
|
||||||
|
this.$el = this.getEl(expanded);
|
||||||
|
|
||||||
|
this.module.$formGroup.append(this.$el);
|
||||||
|
|
||||||
|
// make footer sticky if expanded, clear up from it being sticky if not
|
||||||
|
GOVUK.stickAtBottomWhenScrolling.recalculate();
|
||||||
|
};
|
||||||
|
|
||||||
|
function CollapsibleCheckboxes () {}
|
||||||
|
CollapsibleCheckboxes.prototype._focusTextElement = ($el) => {
|
||||||
|
$el
|
||||||
|
.attr('tabindex', '-1')
|
||||||
|
.focus();
|
||||||
|
};
|
||||||
|
CollapsibleCheckboxes.prototype.start = function(component) {
|
||||||
|
this.$formGroup = $(component);
|
||||||
|
this.$fieldset = this.$formGroup.find('fieldset');
|
||||||
|
this.$checkboxes = this.$fieldset.find('input[type=checkbox]');
|
||||||
|
this.fieldLabel = this.$formGroup.data('fieldLabel');
|
||||||
|
this.total = this.$checkboxes.length;
|
||||||
|
this.legendText = this.$fieldset.find('legend').text().trim();
|
||||||
|
this.expanded = false;
|
||||||
|
|
||||||
|
this.addHeadingHideLegend();
|
||||||
|
|
||||||
|
// generate summary and footer
|
||||||
|
this.footer = new Footer(this);
|
||||||
|
this.summary = new Summary(this);
|
||||||
|
|
||||||
|
this.$fieldset.before(this.summary.$el);
|
||||||
|
|
||||||
|
// add custom classes
|
||||||
|
this.$formGroup.addClass('selection-wrapper');
|
||||||
|
this.$fieldset.addClass('selection-content');
|
||||||
|
|
||||||
|
// hide checkboxes
|
||||||
|
this.$fieldset.hide();
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
};
|
||||||
|
CollapsibleCheckboxes.prototype.getSelection = function() { return this.$checkboxes.filter(':checked').length; };
|
||||||
|
CollapsibleCheckboxes.prototype.addHeadingHideLegend = function() {
|
||||||
|
const headingLevel = this.$formGroup.data('heading-level') || '2';
|
||||||
|
|
||||||
|
this.$heading = $(`<h${headingLevel} class="heading-small">${this.legendText}</h${headingLevel}>`);
|
||||||
|
this.$fieldset.before(this.$heading);
|
||||||
|
|
||||||
|
this.$fieldset.find('legend').addClass('visuallyhidden');
|
||||||
|
};
|
||||||
|
CollapsibleCheckboxes.prototype.expand = function(e) {
|
||||||
|
if (e !== undefined) { e.preventDefault(); }
|
||||||
|
|
||||||
|
if (!this.expanded) {
|
||||||
|
this.$fieldset.show();
|
||||||
|
this.expanded = true;
|
||||||
|
this.summary.update(this.getSelection());
|
||||||
|
this.footer.update(this.expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
// shift focus whether expanded or not
|
||||||
|
this._focusTextElement(this.$fieldset);
|
||||||
|
};
|
||||||
|
CollapsibleCheckboxes.prototype.collapse = function(e) {
|
||||||
|
if (e !== undefined) { e.preventDefault(); }
|
||||||
|
|
||||||
|
if (this.expanded) {
|
||||||
|
this.$fieldset.hide();
|
||||||
|
this.expanded = false;
|
||||||
|
this.summary.update(this.getSelection());
|
||||||
|
this.footer.update(this.expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
// shift focus whether expanded or not
|
||||||
|
this._focusTextElement(this.summary.$text);
|
||||||
|
};
|
||||||
|
CollapsibleCheckboxes.prototype.handleClick = function(e) {
|
||||||
|
if (this.expanded) {
|
||||||
|
this.collapse(e);
|
||||||
|
} else {
|
||||||
|
this.expand(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
CollapsibleCheckboxes.prototype.handleSelection = function(e) {
|
||||||
|
this.summary.update(this.getSelection(), this.total, this.fieldLabel);
|
||||||
|
};
|
||||||
|
CollapsibleCheckboxes.prototype.bindEvents = function() {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
this.$formGroup.on('click', '.button', this.handleClick.bind(this));
|
||||||
|
this.$checkboxes.on('click', this.handleSelection.bind(this));
|
||||||
|
|
||||||
|
this.summary.bindEvents(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
GOVUK.Modules.CollapsibleCheckboxes = CollapsibleCheckboxes;
|
||||||
|
|
||||||
|
}(window));
|
||||||
@@ -11,12 +11,11 @@
|
|||||||
var $scrollArea = $el.closest('.sticky-scroll-area');
|
var $scrollArea = $el.closest('.sticky-scroll-area');
|
||||||
|
|
||||||
$scrollArea = $scrollArea.length ? $scrollArea : $el.parent();
|
$scrollArea = $scrollArea.length ? $scrollArea : $el.parent();
|
||||||
scrollArea = $scrollArea.get(0);
|
|
||||||
|
|
||||||
this._els = [el];
|
this._els = [el];
|
||||||
this.edge = edge;
|
this.edge = edge;
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
this.node = scrollArea;
|
this.node = $scrollArea.get(0);
|
||||||
this.setEvents();
|
this.setEvents();
|
||||||
};
|
};
|
||||||
ScrollArea.prototype.addEl = function (el) {
|
ScrollArea.prototype.addEl = function (el) {
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
};
|
};
|
||||||
ScrollArea.prototype.getFocusedDetails = {
|
ScrollArea.prototype.getFocusedDetails = {
|
||||||
forElement: function ($focusedElement) {
|
forElement: function ($focusedElement) {
|
||||||
focused = {
|
var focused = {
|
||||||
'top': $focusedElement.offset().top,
|
'top': $focusedElement.offset().top,
|
||||||
'height': $focusedElement.outerHeight(),
|
'height': $focusedElement.outerHeight(),
|
||||||
'type': 'element'
|
'type': 'element'
|
||||||
|
|||||||
@@ -124,10 +124,20 @@ td {
|
|||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
&.heading-small {
|
||||||
|
@include bold-19();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
color: $secondary-text-colour;
|
color: $secondary-text-colour;
|
||||||
|
|
||||||
|
.form-label + & {
|
||||||
|
display: block;
|
||||||
|
margin-top: -5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-bullet {
|
.list-bullet {
|
||||||
|
|||||||
@@ -1,3 +1,37 @@
|
|||||||
|
.selection-summary {
|
||||||
|
|
||||||
|
.selection-summary__text {
|
||||||
|
@include core-19($tabular-numbers: true);
|
||||||
|
padding: 5px 0 0 0;
|
||||||
|
margin-bottom: $gutter / 2;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-summary__text--folders {
|
||||||
|
padding: 10px 15px 5px 51px;
|
||||||
|
background-image: file-url('folder-black.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 39px auto;
|
||||||
|
background-position: 0px 4px;
|
||||||
|
|
||||||
|
@include ie-lte(8) {
|
||||||
|
background-image: file-url('folder-black.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert full-width button for smaller screens
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.checkboxes-nested {
|
.checkboxes-nested {
|
||||||
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -42,3 +76,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selection-content {
|
||||||
|
margin-bottom: ($gutter / 3) * 2;
|
||||||
|
|
||||||
|
.checkboxes-nested {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-footer {
|
||||||
|
clear: both;
|
||||||
|
|
||||||
|
.button-secondary {
|
||||||
|
// revert full-width button for smaller screens
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// styles specific to the collapsible checkboxes module
|
||||||
|
.selection-wrapper {
|
||||||
|
fieldset:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro checkboxes_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False) %}
|
{% 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, input="checkbox") }}
|
{{ select_nested(field, child_map, hint, disable, option_hints, hide_legend, collapsible_opts, legend_style, input="checkbox") }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro checkboxes(field, hint=None, disable=[], option_hints={}, hide_legend=False) %}
|
{% macro checkboxes(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}) %}
|
||||||
{{ select(field, hint, disable, option_hints, hide_legend, input="checkbox") }}
|
{{ select(field, hint, disable, option_hints, hide_legend, collapsible_opts, input="checkbox") }}
|
||||||
{% endmacro %}
|
{% 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(
|
{% 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 %}
|
{% for option in field %}
|
||||||
{{ select_input(option, disable, option_hints, input=input) }}
|
{{ select_input(option, disable, option_hints, input=input) }}
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
{% endmacro %}
|
{% 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(
|
{% 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">
|
<div class="{{ "radios" if input == "radio" else "checkboxes" }}-nested">
|
||||||
{{ select_list(child_map[None], child_map, disable, option_hints, input=input) }}
|
{{ select_list(child_map[None], child_map, disable, option_hints, input=input) }}
|
||||||
@@ -35,10 +35,14 @@
|
|||||||
{% endmacro %}
|
{% 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") %}
|
||||||
<div class="form-group {% if field.errors %} form-group-error{% endif %}">
|
{% set is_collapsible = collapsible_opts|length %}
|
||||||
<fieldset>
|
<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 %}>
|
||||||
<legend class="{{ 'form-label' if not hide_legend else '' }}">
|
{% if is_collapsible %}
|
||||||
|
<div class="selection-summary" role="region" aria-live="polite"></div>
|
||||||
|
{% 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 %}
|
{% if hide_legend %}<span class="visually-hidden">{% endif %}
|
||||||
{{ field.label.text|safe }}
|
{{ field.label.text|safe }}
|
||||||
{% if hide_legend %}</span>{% endif %}
|
{% if hide_legend %}</span>{% endif %}
|
||||||
|
|||||||
@@ -2,20 +2,19 @@
|
|||||||
{% from "components/radios.html" import radio, radios, conditional_radio_panel %}
|
{% from "components/radios.html" import radio, radios, conditional_radio_panel %}
|
||||||
|
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<legend class="form-label">
|
<legend class="form-label heading-small">
|
||||||
Permissions
|
Permissions
|
||||||
</legend>
|
</legend>
|
||||||
|
<span class="hint">
|
||||||
|
All team members can see sent messages.
|
||||||
|
</span>
|
||||||
{% for field in form.permissions_fields %}
|
{% for field in form.permissions_fields %}
|
||||||
{{ checkbox(field) }}
|
{{ checkbox(field) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<p class="bottom-gutter">
|
|
||||||
All team members can see sent messages.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% if current_service.has_permission("edit_folder_permissions") and form.folder_permissions.all_template_folders %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if service_has_email_auth %}
|
{% if service_has_email_auth %}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
{{ textbox(form.name, width='1-1') }}
|
{{ textbox(form.name, width='1-1') }}
|
||||||
{% if current_service.has_permission("edit_folder_permissions") %}
|
{% if current_service.has_permission("edit_folder_permissions") %}
|
||||||
{% if current_user.has_permissions("manage_service") and form.users_with_permission.all_service_users %}
|
{% 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 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ const javascripts = () => {
|
|||||||
paths.src + 'javascripts/previewPane.js',
|
paths.src + 'javascripts/previewPane.js',
|
||||||
paths.src + 'javascripts/colourPreview.js',
|
paths.src + 'javascripts/colourPreview.js',
|
||||||
paths.src + 'javascripts/templateFolderForm.js',
|
paths.src + 'javascripts/templateFolderForm.js',
|
||||||
|
paths.src + 'javascripts/collapsibleCheckboxes.js',
|
||||||
paths.src + 'javascripts/main.js'
|
paths.src + 'javascripts/main.js'
|
||||||
])
|
])
|
||||||
.pipe(plugins.prettyerror())
|
.pipe(plugins.prettyerror())
|
||||||
|
|||||||
500
tests/javascripts/collapsibleCheckboxes.test.js
Normal file
500
tests/javascripts/collapsibleCheckboxes.test.js
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
const helpers = require('./support/helpers');
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// set up jQuery
|
||||||
|
window.jQuery = require('jquery');
|
||||||
|
$ = window.jQuery;
|
||||||
|
|
||||||
|
// load module code
|
||||||
|
require('govuk_frontend_toolkit/javascripts/govuk/modules.js');
|
||||||
|
|
||||||
|
// TODO: remove this when tests for sticky JS are written
|
||||||
|
require('../../app/assets/javascripts/stick-to-window-when-scrolling.js');
|
||||||
|
|
||||||
|
require('../../app/assets/javascripts/collapsibleCheckboxes.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
window.jQuery = null;
|
||||||
|
$ = null;
|
||||||
|
|
||||||
|
delete window.GOVUK;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Collapsible fieldset', () => {
|
||||||
|
|
||||||
|
let fieldset;
|
||||||
|
let checkboxes;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const _checkboxes = (start, end, descendents) => {
|
||||||
|
result = '';
|
||||||
|
|
||||||
|
for (let num = start; num <= end; num++) {
|
||||||
|
let id = `folder-permissions-${num}`;
|
||||||
|
|
||||||
|
if (!descendents) { descendents = ''; }
|
||||||
|
|
||||||
|
result += `<li class="multiple-choice">
|
||||||
|
<input id="${id}" name="folder_permissions" type="checkbox" value="${id}">
|
||||||
|
<label class="block-label" for="{id}">
|
||||||
|
Folder 18
|
||||||
|
</label>
|
||||||
|
${descendents}
|
||||||
|
</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// set up DOM
|
||||||
|
document.body.innerHTML =
|
||||||
|
`<div class="form-group" data-module="collapsible-checkboxes" data-field-label="folder">
|
||||||
|
<div class="selection-summary"></div>
|
||||||
|
<fieldset id="folder_permissions">
|
||||||
|
<legend class="form-label heading-small">
|
||||||
|
Folders this team member can see
|
||||||
|
</legend>
|
||||||
|
<div class="checkboxes-nested">
|
||||||
|
<ul>
|
||||||
|
${_checkboxes(1, 10)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
formGroup = document.querySelector('.form-group');
|
||||||
|
fieldset = formGroup.querySelector('fieldset');
|
||||||
|
checkboxesContainer = fieldset.querySelector('.checkboxes-nested');
|
||||||
|
checkboxes = checkboxesContainer.querySelectorAll('input[type=checkbox]');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when started', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
|
||||||
|
// reset checkboxes to default state
|
||||||
|
checkboxes.forEach(el => el.removeAttribute('checked'));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adds the right classes to the group and fieldset", () => {
|
||||||
|
|
||||||
|
expect(formGroup.classList.contains('selection-wrapper')).toBe(true);
|
||||||
|
expect(fieldset.classList.contains('selection-content')).toBe(true);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adds a heading before the selected fieldset", () => {
|
||||||
|
|
||||||
|
const heading = helpers.element(fieldset).getPreviousSibling(
|
||||||
|
el => (el.nodeName === 'h2') && (el.hasClass('heading-small'))
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(heading).not.toBeNull();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adds the right content and classes to the summary", () => {
|
||||||
|
|
||||||
|
const summary = formGroup.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
expect(summary).not.toBeNull();
|
||||||
|
expect(summary.classList.contains('selection-summary__text--folders')).toBe(true);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the legend of the fieldset is visually hidden", () => {
|
||||||
|
|
||||||
|
const legend = helpers.element(fieldset.querySelector('legend'));
|
||||||
|
|
||||||
|
expect(legend.hasClass('visuallyhidden')).toBe(true);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has a button to expand the fieldset", () => {
|
||||||
|
|
||||||
|
const button = formGroup.querySelector('.button');
|
||||||
|
|
||||||
|
expect(button).not.toBeNull();
|
||||||
|
expect(button.textContent.trim()).toEqual('Choose folders');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has the correct aria attributes on the button", () => {
|
||||||
|
|
||||||
|
expect(helpers.element(formGroup.querySelector('.button')).hasAttributesSetTo({
|
||||||
|
'aria-controls': fieldset.getAttribute('id'),
|
||||||
|
'aria-expanded': 'false'
|
||||||
|
})).toBe(true);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hides the checkboxes", () => {
|
||||||
|
|
||||||
|
expect(helpers.element(fieldset).is('hidden')).toEqual(true);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('has the right summary text when started with no checkboxes selected', () => {
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
// default state is for none to be selected
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("No folders (only templates outside a folder)");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('has the right summary text when started with some checkboxes selected', () => {
|
||||||
|
|
||||||
|
// select the first 3 checkboxes
|
||||||
|
checkboxes.forEach((el, idx) => {
|
||||||
|
if ([0,1,2].includes(idx)) { el.setAttribute('checked', ''); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("3 of 10 folders");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('has the right summary text when started with all checkboxes selected', () => {
|
||||||
|
|
||||||
|
// select all the checkboxes
|
||||||
|
checkboxes.forEach(el => el.setAttribute('checked', ''));
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("All folders");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the summary doesn't have a folder icon if fields aren't called 'folder'", () => {
|
||||||
|
|
||||||
|
formGroup.dataset.fieldLabel = 'team member';
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
expect(summaryText.classList.contains('.selection-summary__text-label')).toBe(false);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when button is clicked while the fieldset is collapsed", () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
helpers.triggerEvent(formGroup.querySelector('.button'), 'click');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it shows the checkboxes", () => {
|
||||||
|
|
||||||
|
expect(helpers.element(fieldset).is('hidden')).toBe(false);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it focuses the fieldset", () => {
|
||||||
|
|
||||||
|
expect(document.activeElement).toBe(fieldset);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it uses ARIA to mark the checkboxes as expanded", () => {
|
||||||
|
|
||||||
|
expect(formGroup.querySelector('.button').getAttribute('aria-expanded')).toEqual('true');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it changes it's text to indicate it's new action", () => {
|
||||||
|
|
||||||
|
expect(formGroup.querySelector('.button').textContent.trim()).toEqual("Done choosing folders");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when button is clicked when the fieldset is expanded", () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
// show the checkboxes
|
||||||
|
helpers.triggerEvent(formGroup.querySelector('.button'), 'click');
|
||||||
|
|
||||||
|
// click the button
|
||||||
|
helpers.triggerEvent(formGroup.querySelector('.button'), 'click');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it hides the checkboxes", () => {
|
||||||
|
|
||||||
|
expect(helpers.element(fieldset).is('hidden')).toBe(true);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it focuses the summary text", () => {
|
||||||
|
|
||||||
|
expect(document.activeElement).toBe(document.querySelector('.selection-summary__text'));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it uses ARIA to mark the checkboxes as collapsed", () => {
|
||||||
|
|
||||||
|
expect(formGroup.querySelector('.button').getAttribute('aria-expanded')).toEqual('false');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it changes it's text to indicate it's new action", () => {
|
||||||
|
|
||||||
|
expect(formGroup.querySelector('.button').textContent.trim()).toEqual("Choose folders");
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the footer (that wraps the button)", () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
// track calls to sticky JS
|
||||||
|
window.GOVUK.stickAtBottomWhenScrolling.recalculate = jest.fn(() => {});
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
// show the checkboxes
|
||||||
|
helpers.triggerEvent(formGroup.querySelector('.button'), 'click');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("is made sticky when the fieldset is expanded", () => {
|
||||||
|
|
||||||
|
expect(formGroup.querySelector('.selection-footer').classList.contains('js-stick-at-bottom-when-scrolling')).toBe(true);
|
||||||
|
expect(window.GOVUK.stickAtBottomWhenScrolling.recalculate.mock.calls.length).toBe(1);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has its stickiness removed when the fieldset is collapsed", () => {
|
||||||
|
|
||||||
|
// click the button to collapse the fieldset
|
||||||
|
helpers.triggerEvent(formGroup.querySelector('.button'), 'click');
|
||||||
|
|
||||||
|
expect(formGroup.querySelector('.selection-footer').classList.contains('js-stick-at-bottom-when-scrolling')).toBe(false);
|
||||||
|
expect(window.GOVUK.stickAtBottomWhenScrolling.recalculate.mock.calls.length).toBe(2);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the selection changes", () => {
|
||||||
|
|
||||||
|
const showCheckboxes = () => {
|
||||||
|
helpers.triggerEvent(formGroup.querySelector('.button'), 'click');
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkFirstCheckbox = () => {
|
||||||
|
checkboxes[0].setAttribute('checked', '');
|
||||||
|
checkboxes[0].checked = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkAllCheckboxes = () => {
|
||||||
|
Array.from(checkboxes).forEach(checkbox => {
|
||||||
|
checkbox.setAttribute('checked', '');
|
||||||
|
checkbox.checked = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkAllCheckboxesButTheLast = () => {
|
||||||
|
Array.from(checkboxes).forEach((checkbox, idx) => {
|
||||||
|
if (idx > 0) {
|
||||||
|
checkbox.setAttribute('checked', '');
|
||||||
|
checkbox.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("from some to none the summary updates to reflect that", () => {
|
||||||
|
|
||||||
|
test("if fields are called 'folders'", () => {
|
||||||
|
|
||||||
|
formGroup.dataset.fieldLabel = 'folder';
|
||||||
|
|
||||||
|
checkFirstCheckbox();
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
showCheckboxes();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
// click the first checkbox
|
||||||
|
helpers.triggerEvent(checkboxes[0], 'click');
|
||||||
|
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("No folders (only templates outside a folder)");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("if fields are called 'team member'", () => {
|
||||||
|
|
||||||
|
formGroup.dataset.fieldLabel = 'team member';
|
||||||
|
|
||||||
|
checkFirstCheckbox();
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
showCheckboxes();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
// click the first checkbox
|
||||||
|
helpers.triggerEvent(checkboxes[0], 'click');
|
||||||
|
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("No team members (only you)");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("if fields are called 'arbitrary thing'", () => {
|
||||||
|
|
||||||
|
formGroup.dataset.fieldLabel = 'arbitrary thing';
|
||||||
|
|
||||||
|
checkFirstCheckbox();
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
showCheckboxes();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
// click the first checkbox
|
||||||
|
helpers.triggerEvent(checkboxes[0], 'click');
|
||||||
|
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("No arbitrary things");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("from all to some the summary updates to reflect that", () => {
|
||||||
|
|
||||||
|
test("if fields are called 'folder'", () => {
|
||||||
|
|
||||||
|
formGroup.dataset.fieldLabel = 'folder';
|
||||||
|
|
||||||
|
checkAllCheckboxes();
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
showCheckboxes();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
// click the first checkbox
|
||||||
|
helpers.triggerEvent(checkboxes[1], 'click');
|
||||||
|
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("9 of 10 folders");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("if fields are called 'team member'", () => {
|
||||||
|
|
||||||
|
formGroup.dataset.fieldLabel = 'team member';
|
||||||
|
|
||||||
|
checkAllCheckboxes();
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
showCheckboxes();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
// click the first checkbox
|
||||||
|
helpers.triggerEvent(checkboxes[1], 'click');
|
||||||
|
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("9 of 10 team members");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("from some to all the summary updates to reflect that", () => {
|
||||||
|
|
||||||
|
test("if fields are called 'folder'", () => {
|
||||||
|
|
||||||
|
formGroup.dataset.fieldLabel = 'folder';
|
||||||
|
|
||||||
|
checkAllCheckboxesButTheLast();
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
showCheckboxes();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
helpers.triggerEvent(checkboxes[0], 'click');
|
||||||
|
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("All folders");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("if fields are called 'team member'", () => {
|
||||||
|
|
||||||
|
formGroup.dataset.fieldLabel = 'team member';
|
||||||
|
|
||||||
|
checkAllCheckboxesButTheLast();
|
||||||
|
|
||||||
|
// start module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
|
||||||
|
showCheckboxes();
|
||||||
|
|
||||||
|
const summaryText = document.querySelector('.selection-summary__text');
|
||||||
|
|
||||||
|
helpers.triggerEvent(checkboxes[0], 'click');
|
||||||
|
|
||||||
|
expect(summaryText.textContent.trim()).toEqual("All team members");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
84
tests/javascripts/support/helpers.js
Normal file
84
tests/javascripts/support/helpers.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
const triggerEvent = (el, evtType) => {
|
||||||
|
const evt = new Event(evtType, {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
el.dispatchEvent(evt);
|
||||||
|
};
|
||||||
|
|
||||||
|
class ElementQuery {
|
||||||
|
constructor (el) {
|
||||||
|
this.el = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
get nodeName () {
|
||||||
|
return this.el.nodeName.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
get firstTextNodeValue () {
|
||||||
|
const textNodes = Array.from(this.el.childNodes).filter(el => el.nodeType === 3);
|
||||||
|
|
||||||
|
return textNodes.length ? textNodes[0].nodeValue : undefined;
|
||||||
|
};
|
||||||
|
// returns the elements attributes as an object
|
||||||
|
hasAttributesSetTo (mappings) {
|
||||||
|
if (!this.el.hasAttributes()) { return false; }
|
||||||
|
|
||||||
|
const keys = Object.keys(mappings);
|
||||||
|
let matches = 0;
|
||||||
|
|
||||||
|
keys.forEach(key => {
|
||||||
|
if (this.el.hasAttribute(key) && (this.el.attributes[key].value === mappings[key])) {
|
||||||
|
matches++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return matches === keys.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasClass (classToken) {
|
||||||
|
return Array.from(this.el.classList).includes(classToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
is (state) {
|
||||||
|
const test = `_is${state.charAt(0).toUpperCase()}${state.slice(1)}`;
|
||||||
|
|
||||||
|
if (ElementQuery.prototype.hasOwnProperty(test)) {
|
||||||
|
return this[test]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// looks for a sibling before the el that matches the supplied test function
|
||||||
|
// the test function gets sent each sibling, wrapped in an Element instance
|
||||||
|
getPreviousSibling (test) {
|
||||||
|
let node = this.el.previousElementSibling;
|
||||||
|
let el;
|
||||||
|
|
||||||
|
while(node) {
|
||||||
|
el = element(node);
|
||||||
|
|
||||||
|
if (test(el)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = node.previousElementSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isHidden () {
|
||||||
|
const display = window.getComputedStyle(this.el).getPropertyValue('display');
|
||||||
|
|
||||||
|
return display === 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// function to ask certain questions of a DOM Element
|
||||||
|
const element = function (el) {
|
||||||
|
return new ElementQuery(el);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.triggerEvent = triggerEvent;
|
||||||
|
exports.element = element;
|
||||||
Reference in New Issue
Block a user