From 87dcea779ede3ecf48e68a6a1091ef78aac7d868 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Tue, 22 Sep 2020 11:45:55 +0100 Subject: [PATCH] Make footer popup forms groups & add labelling The existing behaviour focused the form control for each popup (radios or textbox) when opened. This gives no indication the submit button or cancel link have been added to the page. These changes: - make the parent element a region to group all the new content - label the region to link it to the button that opened it - add a description to the region so users know how to use it and that all the controls have been added to the page --- app/assets/javascripts/templateFolderForm.js | 87 ++++-- .../stylesheets/components/message.scss | 11 + app/templates/views/templates/_move_to.html | 18 +- tests/javascripts/templateFolderForm.test.js | 254 ++++++++++++------ 4 files changed, 251 insertions(+), 119 deletions(-) diff --git a/app/assets/javascripts/templateFolderForm.js b/app/assets/javascripts/templateFolderForm.js index aa2a1d4c3..943d136d0 100644 --- a/app/assets/javascripts/templateFolderForm.js +++ b/app/assets/javascripts/templateFolderForm.js @@ -17,18 +17,59 @@ // all the diff states that we want to show or hide this.states = [ - {key: 'nothing-selected-buttons', $el: this.$form.find('#nothing_selected'), cancellable: false}, - {key: 'items-selected-buttons', $el: this.$form.find('#items_selected'), cancellable: false}, - {key: 'move-to-existing-folder', $el: this.$form.find('#move_to_folder_radios'), cancellable: true, setFocus: this.getFocusRoutine('#move_to_folder_radios fieldset', true), action: 'move to folder'}, - {key: 'move-to-new-folder', $el: this.$form.find('#move_to_new_folder_form'), cancellable: true, setFocus: this.getFocusRoutine('#move_to_new_folder_name', false), action: 'move to new folder'}, - {key: 'add-new-folder', $el: this.$form.find('#add_new_folder_form'), cancellable: true, setFocus: this.getFocusRoutine('#add_new_folder_name', false), action: 'new folder'}, - {key: 'add-new-template', $el: this.$form.find('#add_new_template_form'), cancellable: true, setFocus: this.getFocusRoutine('#add_new_template_form fieldset', true), action: 'new template'} + { + key: 'nothing-selected-buttons', + $el: this.$form.find('#nothing_selected'), + cancellable: false + }, + { + key: 'items-selected-buttons', + $el: this.$form.find('#items_selected'), + cancellable: false + }, + { + key: 'move-to-existing-folder', + $el: this.$form.find('#move_to_folder_radios'), + cancellable: true, + setFocus: () => $('#move_to_folder_radios').focus(), + action: 'move to folder', + description: 'Press move to confirm or cancel to close' + }, + { + key: 'move-to-new-folder', + $el: this.$form.find('#move_to_new_folder_form'), + cancellable: true, + setFocus: () => $('#move_to_new_folder_form').focus(), + action: 'move to new folder', + description: 'Press add to new folder to confirm name or cancel to close' + }, + { + key: 'add-new-folder', + $el: this.$form.find('#add_new_folder_form'), + cancellable: true, + setFocus: () => $('#add_new_folder_form').focus(), + action: 'new folder', + description: 'Press add new folder to confirm name or cancel to close' + }, + { + key: 'add-new-template', + $el: this.$form.find('#add_new_template_form'), + cancellable: true, + setFocus: () => $('#add_new_template_form').focus(), + action: 'new template', + description: 'Press continue to confirm selection or cancel to close' + } ]; // cancel/clear buttons only relevant if JS enabled, so this.states.filter(state => state.cancellable).forEach((x) => this.addCancelButton(x)); this.states.filter(state => state.key === 'items-selected-buttons').forEach(x => this.addClearButton(x)); + // make elements focusabled + this.states.filter(state => state.setFocus).forEach(x => x.$el.attr('tabindex', '0')); + + this.addDescriptionsToStates(); + // activate stickiness of elements in each state this.activateStickyElements(); @@ -45,22 +86,16 @@ this.$form.on('change', 'input[type=checkbox]', () => this.templateFolderCheckboxChanged()); }; - this.getFocusRoutine = function (selector, setTabindex) { - return function () { - let $el = $(selector); - let removeTabindex = (e) => { - $(e.target) - .removeAttr('tabindex') - .off('blur', removeTabindex); - }; + this.addDescriptionsToStates = function () { + let id, description; - if (setTabindex) { - $el.attr('tabindex', '-1'); - $el.on('blur', removeTabindex); - } - - $el.focus(); - }; + $.each(this.states.filter(state => 'description' in state), (idx, state) => { + id = `${state.key}__description`; + description = `

${state.description}

`; + state.$el + .prepend(description) + .attr('aria-describedby', id); + }); }; this.activateStickyElements = function() { @@ -140,8 +175,7 @@ this.currentState = 'nothing-selected-buttons'; this.templateFolderCheckboxChanged(); if (targetSelector) { - let setFocus = this.getFocusRoutine(targetSelector, false); - setFocus(); + $(targetSelector).focus(); } }; @@ -240,6 +274,7 @@ this.render = function() { let mode = 'default'; let currentStateObj = this.states.filter(state => { return (state.key === this.currentState); })[0]; + let scrollTop; // detach everything, unless they are the currentState this.states.forEach( @@ -254,7 +289,11 @@ // make sticky JS recalculate its cache of the element's position GOVUK.stickAtBottomWhenScrolling.recalculate(); - if (currentStateObj && ('setFocus' in currentStateObj)) { currentStateObj.setFocus(); } + if (currentStateObj && ('setFocus' in currentStateObj)) { + scrollTop = $(window).scrollTop(); + currentStateObj.setFocus(); + $(window).scrollTop(scrollTop); + } }; this.nothingSelectedButtons = $(` diff --git a/app/assets/stylesheets/components/message.scss b/app/assets/stylesheets/components/message.scss index 2cd63fdbf..58341ba31 100644 --- a/app/assets/stylesheets/components/message.scss +++ b/app/assets/stylesheets/components/message.scss @@ -176,6 +176,17 @@ $message-type-bottom-spacing: govuk-spacing(4); } +.sticky-template-form { + + padding: govuk-spacing(3); + margin: govuk-spacing(3) * -1; + + &:focus { + outline: none; + } + +} + .folder-heading { .govuk-grid-row & { diff --git a/app/templates/views/templates/_move_to.html b/app/templates/views/templates/_move_to.html index e07939738..5392abd87 100644 --- a/app/templates/views/templates/_move_to.html +++ b/app/templates/views/templates/_move_to.html @@ -5,7 +5,7 @@ {% if templates_and_folders_form.move_to.choices and template_list.templates_to_show %} -
+
{{ radios_nested(templates_and_folders_form.move_to, move_to_children, option_hints=option_hints) }}
@@ -13,22 +13,20 @@ {{ page_footer('Move', button_name='operation', button_value='move-to-existing-folder', button_text_hidden_suffix=' selection to folder') }}
-
-
- Add to new folder +
+
{{ templates_and_folders_form.move_to_new_folder_name(param_extensions={"classes": "govuk-!-width-full"}) }} {{ page_footer('Add to new folder', button_name='operation', button_value='move-to-new-folder') }} -
+
{% endif %} -
-
- Add new folder +
+
{{ templates_and_folders_form.add_new_folder_name(param_extensions={"classes": "govuk-!-width-full"}) }} {{ page_footer('Add new folder', button_name='operation', button_value='add-new-folder') }} -
+
-
+
{{ radios(templates_and_folders_form.add_template_by_template_type) }}
diff --git a/tests/javascripts/templateFolderForm.test.js b/tests/javascripts/templateFolderForm.test.js index 06b498f72..014aae86f 100644 --- a/tests/javascripts/templateFolderForm.test.js +++ b/tests/javascripts/templateFolderForm.test.js @@ -31,7 +31,7 @@ function setFixtures (hierarchy, newTemplateDataModules = "") { return `
-
+
@@ -50,9 +50,8 @@ function setFixtures (hierarchy, newTemplateDataModules = "") {
-
-
- Add to new folder +
+
+
-
-
- Add new folder +
+
+
-
+
@@ -141,10 +139,16 @@ function resetStickyMocks () { beforeAll(() => { require('../../app/assets/javascripts/templateFolderForm.js'); + + // plug JSDOM's lack of support for window.scrollTo + window.scrollTo = () => {}; }); afterAll(() => { require('./support/teardown.js'); + + // tidy up + delete window.scrollTo; }); describe('TemplateFolderForm', () => { @@ -227,7 +231,7 @@ describe('TemplateFolderForm', () => { return formControls.querySelector('[role=status]'); }; - describe("Before the page loads", () => { + describe("Before the module starts", () => { // We need parts of the module to be made sticky, but by the module code, // not the sticky JS code that operates on the HTML at page load. @@ -243,7 +247,7 @@ describe('TemplateFolderForm', () => { }); - describe("When the page loads", () => { + describe("When the module starts", () => { beforeEach(() => { @@ -360,40 +364,60 @@ describe('TemplateFolderForm', () => { afterEach(() => resetStickyMocks()); - test("should show options for all the types of template", () => { + describe("Should show a region", () => { - const options = [ - 'Email', 'Text message', 'Letter', 'Copy an existing template' - ]; + test("with options for all the types of template", () => { - const labels = Array.from(formControls.querySelectorAll('label')); - const radios = Array.from(formControls.querySelectorAll('input[type=radio]')); + const options = [ + 'Email', 'Text message', 'Letter', 'Copy an existing template' + ]; - options.forEach(option => { - let matchingLabels = labels.filter(label => label.textContent.trim() === option); + const labels = Array.from(formControls.querySelectorAll('label')); + const radios = Array.from(formControls.querySelectorAll('input[type=radio]')); - expect(matchingLabels.length > 0).toBe(true); + options.forEach(option => { + let matchingLabels = labels.filter(label => label.textContent.trim() === option); - let matchingRadio = formControls.querySelector(`#${matchingLabels[0].getAttribute('for')}`) + expect(matchingLabels.length > 0).toBe(true); + + let matchingRadio = formControls.querySelector(`#${matchingLabels[0].getAttribute('for')}`) + + expect(matchingRadio).not.toBeNull(); + }); - expect(matchingRadio).not.toBeNull(); }); - }); + test("with a 'Cancel' link", () => { - test("should show a 'Cancel' link", () => { + const cancelLink = formControls.querySelector('.js-cancel'); - const cancelLink = formControls.querySelector('.js-cancel'); + expect(cancelLink).not.toBeNull(); + expect(cancelLink.querySelector('.govuk-visually-hidden')).not.toBeNull(); + expect(cancelLink.querySelector('.govuk-visually-hidden').textContent.trim()).toEqual('new template'); - expect(cancelLink).not.toBeNull(); - expect(cancelLink.querySelector('.govuk-visually-hidden')).not.toBeNull(); - expect(cancelLink.querySelector('.govuk-visually-hidden').textContent.trim()).toEqual('new template'); + }); - }); + test("with an accessible role, name and description", () => { - test("should focus the fieldset", () => { + let id, description; + const region = document.querySelector('#add_new_template_form'); + expect(region.hasAttribute('role')).toBe(true); + expect(region.getAttribute('role')).toEqual('region'); + expect(region.hasAttribute('aria-label')).toBe(true); + expect(region.getAttribute('aria-label')).toEqual('Choose template type'); + expect(region.hasAttribute('aria-describedby')).toBe(true); + id = region.getAttribute('aria-describedby'); + description = document.getElementById(id); + expect(description).not.toBeNull(); + expect(description.textContent.trim()).toEqual('Press continue to confirm selection or cancel to close'); - expect(document.activeElement).toBe(formControls.querySelector('fieldset')); + }); + + test("and focus it", () => { + + expect(document.activeElement).toBe(formControls.querySelector('#add_new_template_form')); + + }); }); @@ -464,28 +488,48 @@ describe('TemplateFolderForm', () => { }); - test("should show a textbox for the folder name", () => { + describe("should show a parent region", () => { - expect(textbox).not.toBeNull(); + test("with an accessible role, name and description", () => { - // check textbox has a label - expect(formControls.querySelector(`label[for=${textbox.getAttribute('id')}]`)).not.toBeNull(); + let id, description; + const region = document.querySelector('#add_new_folder_form'); + expect(region.hasAttribute('role')).toBe(true); + expect(region.getAttribute('role')).toEqual('region'); + expect(region.hasAttribute('aria-label')).toBe(true); + expect(region.getAttribute('aria-label')).toEqual('Enter name of the new folder'); + expect(region.hasAttribute('aria-describedby')).toBe(true); + id = region.getAttribute('aria-describedby'); + description = document.getElementById(id); + expect(description).not.toBeNull(); + expect(description.textContent.trim()).toEqual('Press add new folder to confirm name or cancel to close'); - }); + }); - test("should show a 'Cancel' link", () => { + test("with a textbox for the folder name", () => { - const cancelLink = formControls.querySelector('.js-cancel'); + expect(textbox).not.toBeNull(); - expect(cancelLink).not.toBeNull(); - expect(cancelLink.querySelector('.govuk-visually-hidden')).not.toBeNull(); - expect(cancelLink.querySelector('.govuk-visually-hidden').textContent.trim()).toEqual('new folder'); + // check textbox has a label + expect(formControls.querySelector(`label[for=${textbox.getAttribute('id')}]`)).not.toBeNull(); - }); + }); - test("should focus the textbox", () => { + test("with a 'Cancel' link", () => { - expect(document.activeElement).toBe(textbox); + const cancelLink = formControls.querySelector('.js-cancel'); + + expect(cancelLink).not.toBeNull(); + expect(cancelLink.querySelector('.govuk-visually-hidden')).not.toBeNull(); + expect(cancelLink.querySelector('.govuk-visually-hidden').textContent.trim()).toEqual('new folder'); + + }); + + test("and focus it", () => { + + expect(document.activeElement).toBe(formControls.querySelector('#add_new_folder_form')); + + }); }); @@ -643,51 +687,71 @@ describe('TemplateFolderForm', () => { }); - test("should show radios for all the folders in the hierarchy", () => { + describe("Should show a region", () => { - const foldersInHierarchy = []; + test("with an accessible role, name and description", () => { - function getFolders (nodes) { + let id, description; + const region = document.querySelector('#move_to_folder_radios'); + expect(region.hasAttribute('role')).toBe(true); + expect(region.getAttribute('role')).toEqual('region'); + expect(region.hasAttribute('aria-label')).toBe(true); + expect(region.getAttribute('aria-label')).toEqual('Choose the folder to move selected items to'); + expect(region.hasAttribute('aria-describedby')).toBe(true); + id = region.getAttribute('aria-describedby'); + description = document.getElementById(id); + expect(description).not.toBeNull(); + expect(description.textContent.trim()).toEqual('Press move to confirm or cancel to close'); - nodes.forEach(node => { - if (node.type === 'folder') { + }); - foldersInHierarchy.push(node.label); - if (node.children.length) { getFolders(node.children) } + test("with radios for all the folders in the hierarchy", () => { - } - }); + const foldersInHierarchy = []; - }; + function getFolders (nodes) { - getFolders(hierarchy); + nodes.forEach(node => { + if (node.type === 'folder') { - const folderLabels = Array.from(formControls.querySelectorAll('#move_to label')) - .filter(label => label.textContent.trim() !== 'Templates'); + foldersInHierarchy.push(node.label); + if (node.children.length) { getFolders(node.children) } - expect(folderLabels.map(label => label.textContent.trim())).toEqual(foldersInHierarchy); + } + }); - const radiosForLabels = folderLabels - .map(label => formControls.querySelector(`#${label.getAttribute('for')}`)) - .filter(radio => radio !== null); + }; - expect(radiosForLabels.length).toEqual(foldersInHierarchy.length); + getFolders(hierarchy); - }); + const folderLabels = Array.from(formControls.querySelectorAll('#move_to label')) + .filter(label => label.textContent.trim() !== 'Templates'); - test("should show a 'Cancel' link", () => { + expect(folderLabels.map(label => label.textContent.trim())).toEqual(foldersInHierarchy); - const cancelLink = formControls.querySelector('.js-cancel'); + const radiosForLabels = folderLabels + .map(label => formControls.querySelector(`#${label.getAttribute('for')}`)) + .filter(radio => radio !== null); - expect(cancelLink).not.toBeNull(); - expect(cancelLink.querySelector('.govuk-visually-hidden')).not.toBeNull(); - expect(cancelLink.querySelector('.govuk-visually-hidden').textContent.trim()).toEqual('move to folder'); + expect(radiosForLabels.length).toEqual(foldersInHierarchy.length); - }); + }); - test("focus the 'Choose a folder' fieldset", () => { + test("with a 'Cancel' link", () => { - expect(document.activeElement).toBe(formControls.querySelector('#move_to')); + const cancelLink = formControls.querySelector('.js-cancel'); + + expect(cancelLink).not.toBeNull(); + expect(cancelLink.querySelector('.govuk-visually-hidden')).not.toBeNull(); + expect(cancelLink.querySelector('.govuk-visually-hidden').textContent.trim()).toEqual('move to folder'); + + }); + + test("and focus it", () => { + + expect(document.activeElement).toBe(formControls.querySelector('#move_to_folder_radios')); + + }); }); @@ -748,28 +812,48 @@ describe('TemplateFolderForm', () => { }); - test("should show a textbox for the folder name", () => { + describe("Should show a region", () => { - expect(textbox).not.toBeNull(); + test("with an accessible role, name and description", () => { - // check textbox has a label - expect(formControls.querySelector(`label[for=${textbox.getAttribute('id')}]`)).not.toBeNull(); + let id, description; + const region = document.querySelector('#move_to_new_folder_form'); + expect(region.hasAttribute('role')).toBe(true); + expect(region.getAttribute('role')).toEqual('region'); + expect(region.hasAttribute('aria-label')).toBe(true); + expect(region.getAttribute('aria-label')).toEqual('Enter name of the new folder to move selected items to'); + expect(region.hasAttribute('aria-describedby')).toBe(true); + id = region.getAttribute('aria-describedby'); + description = document.getElementById(id); + expect(description).not.toBeNull(); + expect(description.textContent.trim()).toEqual('Press add to new folder to confirm name or cancel to close'); - }); + }); - test("should show a 'Cancel' link", () => { + test("with a textbox for the folder name", () => { - const cancelLink = formControls.querySelector('.js-cancel'); + expect(textbox).not.toBeNull(); - expect(cancelLink).not.toBeNull(); - expect(cancelLink.querySelector('.govuk-visually-hidden')).not.toBeNull(); - expect(cancelLink.querySelector('.govuk-visually-hidden').textContent.trim()).toEqual('move to new folder'); + // check textbox has a label + expect(formControls.querySelector(`label[for=${textbox.getAttribute('id')}]`)).not.toBeNull(); - }); + }); - test("should focus the textbox", () => { + test("with a 'Cancel' link", () => { - expect(document.activeElement).toBe(textbox); + const cancelLink = formControls.querySelector('.js-cancel'); + + expect(cancelLink).not.toBeNull(); + expect(cancelLink.querySelector('.govuk-visually-hidden')).not.toBeNull(); + expect(cancelLink.querySelector('.govuk-visually-hidden').textContent.trim()).toEqual('move to new folder'); + + }); + + test("and focus it", () => { + + expect(document.activeElement).toBe(formControls.querySelector('#move_to_new_folder_form')); + + }); });