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')); + + }); });