From 6ffd2af8c628b490d7642c99c14b3e6006d93215 Mon Sep 17 00:00:00 2001 From: BatJan Date: Mon, 11 Feb 2019 22:05:27 +0100 Subject: [PATCH 01/54] Add a TODO in the relevant file - Still need to figure out the proper way of doing this for easy reuse in ordinary overlays too --- .../components/editor/umbeditors.directive.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index 4104a663d34d..c7defbbab8fe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -1,3 +1,9 @@ +/** TODO + * Make some reusable code that can set all other relevant areas to have the "inert" attribute apart from the infinite overlay + * Keep in mind that the code setting the inert attribute is also highly relevant for being used in overlay's that appear when deleting / browsing away from something + * However that might be for another PR if/once this current PR is accepted + */ + (function () { 'use strict'; @@ -11,7 +17,7 @@ scope.editors = []; function addEditor(editor) { - + editor.inFront = true; editor.moveRight = true; editor.level = 0; @@ -68,9 +74,8 @@ /** update layer positions. With ability to offset positions, needed for when an item is moving out, then we dont want it to influence positions */ function updateEditors(offset) { - offset = offset || 0;// fallback value. - + var len = scope.editors.length; var calcLen = len + offset; var ceiling = Math.min(calcLen, allowedNumberOfVisibleEditors); From fd7221f6a8d9adebde984205f6b80228b75cd30d Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 24 Mar 2019 17:21:11 +0100 Subject: [PATCH 02/54] Install the wicg inert polyfill --- src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js | 8 ++++++++ src/Umbraco.Web.UI.Client/package-lock.json | 5 +++++ src/Umbraco.Web.UI.Client/package.json | 3 ++- src/Umbraco.Web.UI.Client/src/install.loader.js | 2 +- src/Umbraco.Web.UI.Client/test/config/karma.conf.js | 1 + src/Umbraco.Web/JavaScript/JsInitialize.js | 2 ++ 6 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js index 819c804a4fcd..62454e20cfe6 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js @@ -232,6 +232,14 @@ gulp.task('dependencies', function () { "name": "underscore", "src": ["node_modules/underscore/underscore-min.js"], "base": "./node_modules/underscore" + }, + { + "name": "wicg-inert", + "src": [ + "./node_modules/wicg-inert/dist/inert.min.js", + "./node_modules/wicg-inert/dist/inert.min.js.map" + ], + "base": "./node_modules/wicg-inert" } ]; diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index b996db214ccb..322521d2fa7f 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -16157,6 +16157,11 @@ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, + "wicg-inert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-2.1.0.tgz", + "integrity": "sha512-7QFAisbzgMY8/+ftQI+LUpCICAAOah0MBJj0GNLsy7YFZo4UxpDsdf7qgixdA5mml4zmyFA7d7zUefsRbbFXTA==" + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 3a5ff22f04ad..993e27cb2c32 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -41,7 +41,8 @@ "spectrum-colorpicker": "1.8.0", "tinymce": "4.9.2", "typeahead.js": "0.11.1", - "underscore": "1.9.1" + "underscore": "1.9.1", + "wicg-inert": "^2.1.0" }, "devDependencies": { "@babel/core": "7.1.6", diff --git a/src/Umbraco.Web.UI.Client/src/install.loader.js b/src/Umbraco.Web.UI.Client/src/install.loader.js index 99be6db9f19d..e442dacb1cd1 100644 --- a/src/Umbraco.Web.UI.Client/src/install.loader.js +++ b/src/Umbraco.Web.UI.Client/src/install.loader.js @@ -1,11 +1,11 @@ LazyLoad.js([ 'lib/jquery/jquery.min.js', - 'lib/angular/angular.js', 'lib/angular-cookies/angular-cookies.js', 'lib/angular-touch/angular-touch.js', 'lib/angular-sanitize/angular-sanitize.js', 'lib/angular-messages/angular-messages.js', + 'lib/wicg-inert/dist/inert.min.js', 'lib/underscore/underscore-min.js', 'lib/angular-ui-sortable/sortable.js', 'js/installer.app.js', diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index 08223de092de..8067f131e10e 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -21,6 +21,7 @@ module.exports = function (config) { 'node_modules/angular-mocks/angular-mocks.js', 'node_modules/angular-ui-sortable/dist/sortable.js', 'node_modules/underscore/underscore-min.js', + 'node_modules/wicg-inert/dist/inert.min.js', 'node_modules/moment/min/moment-with-locales.js', 'lib/umbraco/Extensions.js', 'node_modules/lazyload-js/lazyload.min.js', diff --git a/src/Umbraco.Web/JavaScript/JsInitialize.js b/src/Umbraco.Web/JavaScript/JsInitialize.js index 1ef6e74dfefa..129c47e062a6 100644 --- a/src/Umbraco.Web/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/JavaScript/JsInitialize.js @@ -27,6 +27,8 @@ 'lib/chart.js/chart.min.js', 'lib/angular-chart.js/angular-chart.min.js', + 'lib/wicg-inert/dist/inert.min.js', + 'lib/umbraco/Extensions.js', 'lib/umbraco/NamespaceManager.js', From b3f1b4c07cc225adf4f9ba5b9f2b82de2f49a31e Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 24 Mar 2019 17:21:35 +0100 Subject: [PATCH 03/54] Add a focustrap service --- .../src/common/services/focustrap.service.js | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js diff --git a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js new file mode 100644 index 000000000000..5f61aab97de6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js @@ -0,0 +1,62 @@ +/** Used to improve accesibility ensuring that the focus is trapped inside the active overlay and also ensuring aria-hidden is set accordingly so potential screen readers don't get confused about the context either */ + +function focusTrapService() { + + console.log('trap da focus mayn!'); + + return { + addFocusTrap: function(mode){ + + if(mode === 'overlay'){ + // Call overlay helper method + addFocusTrapOverlayMode(); + } + + if(mode === 'infinite'){ + // Call infinite helper method + addFocusTrapInfiniteMode(); + } + + //TODO: + // addFocusTrap + // Check if there is at least 1 item (infinite or modal) and then add the trap + + // removeFocusTrap + // If there are 0 items then by all means remove the focus trap + + // updateFocusTrap + // If we're in infinite editing mode then whenever a there is more than 1 item we need to update the focus trap to target the correct infinite modal + // Also ensuring that we don't touch the DOM for those fixed elements where the inert and aria-hidden attributes have already been added + + // Maybe add a "infinte editor check" and a "modal" check. Maybe just add a "type" param that needs to be either "modal" or "infinite editor" in order for calling the correct method to deal with the DOM manipulation? + }, + removeFocusTrap: function () { + console.log('Remove that trap!'); + + //TODO: Simply find all inert and aria-hidden attributes and remove them?.... + } + }; +} + +function addFocusTrapOverlayMode () { + var mainWrapper = $('#mainwrapper'); + + // TODO: Add inert and aria-hidden attributes to the mainWrapper and remove it again once the modal is removed + + console.log('add the focus trap for the OVERLAY mode, hehehehe'); +} + +function addFocusTrapInfiniteMode () { + var appHeader = $('.umb-app-header'); + var leftColumn = $('#leftcolumn'); + var contentColumn = $('#contentcolumn > div:first-child'); + + appHeader.attr('inert',''); + leftColumn.attr('inert',''); + contentColumn.attr('inert',''); + var editors = $('.umb-editors'); // Keep an eye on the added / removed editors... not sure if watch is needed? But always add inert/aria-hidden to the prev siblings... + + console.log('add the focus trap for the INFINITE mode, hehehehe'); +} + +angular.module('umbraco.services').factory('focusTrapService', focusTrapService); From cf69f3b9e909d3d81c6710ef5a20cbc062b34da6 Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 24 Mar 2019 17:22:17 +0100 Subject: [PATCH 04/54] Inject the focus trap service into the umbeditors directive --- .../components/editor/umbeditors.directive.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index c7defbbab8fe..a404e1a2e547 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -7,7 +7,7 @@ (function () { 'use strict'; - function EditorsDirective($timeout, eventsService) { + function EditorsDirective($timeout, eventsService, focusTrapService) { function link(scope, el, attr, ctrl) { @@ -17,7 +17,6 @@ scope.editors = []; function addEditor(editor) { - editor.inFront = true; editor.moveRight = true; editor.level = 0; @@ -36,11 +35,9 @@ setTimeout(revealEditorContent.bind(this, editor), 400); updateEditors(); - } function removeEditor(editor) { - editor.moveRight = true; editor.animating = true; @@ -81,6 +78,7 @@ var ceiling = Math.min(calcLen, allowedNumberOfVisibleEditors); var origin = Math.max(calcLen-1, 0)-ceiling; var i = 0; + while(i 0){ + focusTrapService.addFocusTrap('infinite'); + } + else{ + focusTrapService.removeFocusTrap(); + } } evts.push(eventsService.on("appState.editors.open", function (name, args) { From 4e43fa414ea6f78a64b6b3388f0135cca87a48c4 Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 24 Mar 2019 20:51:21 +0100 Subject: [PATCH 05/54] Oops - The polyfill is of course not needed during installation :-) --- src/Umbraco.Web.UI.Client/src/install.loader.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/install.loader.js b/src/Umbraco.Web.UI.Client/src/install.loader.js index e442dacb1cd1..69d150dde5b5 100644 --- a/src/Umbraco.Web.UI.Client/src/install.loader.js +++ b/src/Umbraco.Web.UI.Client/src/install.loader.js @@ -5,7 +5,6 @@ LazyLoad.js([ 'lib/angular-touch/angular-touch.js', 'lib/angular-sanitize/angular-sanitize.js', 'lib/angular-messages/angular-messages.js', - 'lib/wicg-inert/dist/inert.min.js', 'lib/underscore/underscore-min.js', 'lib/angular-ui-sortable/sortable.js', 'js/installer.app.js', From 01a3df3eec3bac4ad568e8ca6d18f5268325ed6a Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 24 Mar 2019 22:18:44 +0100 Subject: [PATCH 06/54] Add some comments to remember the progress and what needs to be done --- .../src/common/services/focustrap.service.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js index 5f61aab97de6..daf8c4b00087 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js @@ -50,13 +50,24 @@ function addFocusTrapInfiniteMode () { var appHeader = $('.umb-app-header'); var leftColumn = $('#leftcolumn'); var contentColumn = $('#contentcolumn > div:first-child'); + var editors = $('.umb-editors'); + // Remove focus from any interactive elements in the appHeader, leftColumn and the first child in the contentColumn appHeader.attr('inert',''); leftColumn.attr('inert',''); contentColumn.attr('inert',''); - var editors = $('.umb-editors'); // Keep an eye on the added / removed editors... not sure if watch is needed? But always add inert/aria-hidden to the prev siblings... console.log('add the focus trap for the INFINITE mode, hehehehe'); + + // Make sure the DOM has been updated before dealing with how many children there are... + // TODO: Make sure that all children, if there are more than one, get the inert attribute - Except that last one! + // This means we'll need to check in the "removeEditor" method whether or not the array of editors is greater than one... and then add/remove the inert attribute accordingly + // This scenario might need it's own set of methods... + + // Currently just seeing if children are available with each call to the "addEditor" method + setTimeout(function(){ + console.log(editors.children()); + }, 100); } angular.module('umbraco.services').factory('focusTrapService', focusTrapService); From 28e548a6f41e708593d805bca4c008d919245d17 Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 31 Mar 2019 17:10:03 +0200 Subject: [PATCH 07/54] Add a toggleInert method to ensure that when more than one infinite overlay is triggered then we figure out how to "hide" those that should not be tabable and ensure that the current one is --- .../src/common/services/focustrap.service.js | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js index daf8c4b00087..067b116c1e46 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js @@ -52,22 +52,28 @@ function addFocusTrapInfiniteMode () { var contentColumn = $('#contentcolumn > div:first-child'); var editors = $('.umb-editors'); - // Remove focus from any interactive elements in the appHeader, leftColumn and the first child in the contentColumn + // Remove focus from any interactive elements in the "appHeader", "leftColumn" and the first child in the "contentColumn" appHeader.attr('inert',''); leftColumn.attr('inert',''); contentColumn.attr('inert',''); - console.log('add the focus trap for the INFINITE mode, hehehehe'); - - // Make sure the DOM has been updated before dealing with how many children there are... - // TODO: Make sure that all children, if there are more than one, get the inert attribute - Except that last one! - // This means we'll need to check in the "removeEditor" method whether or not the array of editors is greater than one... and then add/remove the inert attribute accordingly - // This scenario might need it's own set of methods... - - // Currently just seeing if children are available with each call to the "addEditor" method + // TODO: Maybe this can be refactored into being a watch thingy detecting when add/remove of an infinite overlay is done so we can skip the timeout function - But it works for now setTimeout(function(){ - console.log(editors.children()); + toggleInert(editors); }, 100); } +// TODO: Make sure to set focus on the first focusable element in the focusable overlay (There is a method for that somewhere :-)) +// TODO: Consider adding a tablock method to avoid the possibility of escaping to the browser address bar - But maybe have a discussion about this in the PR instead?... + +function toggleInert (editors) { + const editorChildren = editors.children(); + const lastEditorChildIndex = editorChildren.length - 1; + const lastEditorChild = $(editorChildren[lastEditorChildIndex]); + + editorChildren.attr('inert',''); + lastEditorChild.removeAttr('inert'); + +} + angular.module('umbraco.services').factory('focusTrapService', focusTrapService); From 63c16f6281f7929688f2e0e86801b97c99ad2663 Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 31 Mar 2019 17:51:44 +0200 Subject: [PATCH 08/54] Add method for removing the inert attribute --- .../components/editor/umbeditors.directive.js | 4 +- .../src/common/services/focustrap.service.js | 43 ++++++++++++++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index a404e1a2e547..689aa13efb4d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -87,11 +87,13 @@ i++; } + // If there are any active editors we call the addFocusTrap method if(len > 0){ focusTrapService.addFocusTrap('infinite'); } + // Otherwise we make sure to remove the focus trap else{ - focusTrapService.removeFocusTrap(); + focusTrapService.removeFocusTrap('infinite'); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js index 067b116c1e46..30169473e94a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js @@ -1,19 +1,19 @@ /** Used to improve accesibility ensuring that the focus is trapped inside the active overlay and also ensuring aria-hidden is set accordingly so potential screen readers don't get confused about the context either */ -function focusTrapService() { +function focusTrapService(eventsService) { + // TODO: If possible I would like to store all the needed DOM references in an elements object, wich could then be passed to the methods so we don't need to reference and store them in each method + // But some of the elements are not available untill the app.authenticated event happens... Not sure about the best practice around this... console.log('trap da focus mayn!'); return { addFocusTrap: function(mode){ if(mode === 'overlay'){ - // Call overlay helper method addFocusTrapOverlayMode(); } if(mode === 'infinite'){ - // Call infinite helper method addFocusTrapInfiniteMode(); } @@ -30,9 +30,16 @@ function focusTrapService() { // Maybe add a "infinte editor check" and a "modal" check. Maybe just add a "type" param that needs to be either "modal" or "infinite editor" in order for calling the correct method to deal with the DOM manipulation? }, - removeFocusTrap: function () { + removeFocusTrap: function (mode) { console.log('Remove that trap!'); + if (mode === 'overlay') { + removeFocusTrapOverlayMode(); + } + + if (mode === 'infinite') { + removeFocusTrapInfiniteMode(); + } //TODO: Simply find all inert and aria-hidden attributes and remove them?.... } }; @@ -43,7 +50,15 @@ function addFocusTrapOverlayMode () { // TODO: Add inert and aria-hidden attributes to the mainWrapper and remove it again once the modal is removed - console.log('add the focus trap for the OVERLAY mode, hehehehe'); + console.log('ADD the focus trap for the OVERLAY mode, hehehehe'); +} + +function removeFocusTrapOverlayMode () { + var mainWrapper = $('#mainwrapper'); + + // TODO: Add inert and aria-hidden attributes to the mainWrapper and remove it again once the modal is removed + + console.log('REMOVE the focus trap for the OVERLAY mode, hehehehe'); } function addFocusTrapInfiniteMode () { @@ -63,6 +78,23 @@ function addFocusTrapInfiniteMode () { }, 100); } +function removeFocusTrapInfiniteMode () { + var appHeader = $('.umb-app-header'); + var leftColumn = $('#leftcolumn'); + var contentColumn = $('#contentcolumn > div:first-child'); + + console.log(appHeader); + console.log(leftColumn); + console.log(contentColumn); + + // Remove the inert attribute from the key elements so they're tabable once the infinite editing mode has been deactivated + appHeader.removeAttr('inert'); + leftColumn.removeAttr('inert'); + contentColumn.removeAttr('inert'); + + console.log('inert removed and everything is back to normal....as you were!'); +} + // TODO: Make sure to set focus on the first focusable element in the focusable overlay (There is a method for that somewhere :-)) // TODO: Consider adding a tablock method to avoid the possibility of escaping to the browser address bar - But maybe have a discussion about this in the PR instead?... @@ -73,7 +105,6 @@ function toggleInert (editors) { editorChildren.attr('inert',''); lastEditorChild.removeAttr('inert'); - } angular.module('umbraco.services').factory('focusTrapService', focusTrapService); From 80e783d0de93e715d04cfabec0b6c2b8bab27c17 Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 31 Mar 2019 18:48:12 +0200 Subject: [PATCH 09/54] Remove comments, change mode from "overlay" to "modal" to make a more clear difference between infinite overlays and modal overlays. Also adding the start of method trying to add focus to the "previous" infinte overlay in case we have multiple open and close the active one --- .../src/common/services/focustrap.service.js | 53 +++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js index 30169473e94a..81c446147757 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js @@ -1,6 +1,6 @@ /** Used to improve accesibility ensuring that the focus is trapped inside the active overlay and also ensuring aria-hidden is set accordingly so potential screen readers don't get confused about the context either */ -function focusTrapService(eventsService) { +function focusTrapService() { // TODO: If possible I would like to store all the needed DOM references in an elements object, wich could then be passed to the methods so we don't need to reference and store them in each method // But some of the elements are not available untill the app.authenticated event happens... Not sure about the best practice around this... @@ -8,39 +8,22 @@ function focusTrapService(eventsService) { return { addFocusTrap: function(mode){ - - if(mode === 'overlay'){ + if(mode === 'modal'){ addFocusTrapOverlayMode(); } if(mode === 'infinite'){ addFocusTrapInfiniteMode(); } - - //TODO: - // addFocusTrap - // Check if there is at least 1 item (infinite or modal) and then add the trap - - // removeFocusTrap - // If there are 0 items then by all means remove the focus trap - - // updateFocusTrap - // If we're in infinite editing mode then whenever a there is more than 1 item we need to update the focus trap to target the correct infinite modal - // Also ensuring that we don't touch the DOM for those fixed elements where the inert and aria-hidden attributes have already been added - - // Maybe add a "infinte editor check" and a "modal" check. Maybe just add a "type" param that needs to be either "modal" or "infinite editor" in order for calling the correct method to deal with the DOM manipulation? }, removeFocusTrap: function (mode) { - console.log('Remove that trap!'); - - if (mode === 'overlay') { + if (mode === 'modal') { removeFocusTrapOverlayMode(); } if (mode === 'infinite') { removeFocusTrapInfiniteMode(); } - //TODO: Simply find all inert and aria-hidden attributes and remove them?.... } }; } @@ -48,17 +31,13 @@ function focusTrapService(eventsService) { function addFocusTrapOverlayMode () { var mainWrapper = $('#mainwrapper'); - // TODO: Add inert and aria-hidden attributes to the mainWrapper and remove it again once the modal is removed - - console.log('ADD the focus trap for the OVERLAY mode, hehehehe'); + mainWrapper.attr('inert',''); } function removeFocusTrapOverlayMode () { var mainWrapper = $('#mainwrapper'); - // TODO: Add inert and aria-hidden attributes to the mainWrapper and remove it again once the modal is removed - - console.log('REMOVE the focus trap for the OVERLAY mode, hehehehe'); + mainWrapper.removeAttr('inert'); } function addFocusTrapInfiniteMode () { @@ -83,19 +62,12 @@ function removeFocusTrapInfiniteMode () { var leftColumn = $('#leftcolumn'); var contentColumn = $('#contentcolumn > div:first-child'); - console.log(appHeader); - console.log(leftColumn); - console.log(contentColumn); - // Remove the inert attribute from the key elements so they're tabable once the infinite editing mode has been deactivated appHeader.removeAttr('inert'); leftColumn.removeAttr('inert'); contentColumn.removeAttr('inert'); - - console.log('inert removed and everything is back to normal....as you were!'); } -// TODO: Make sure to set focus on the first focusable element in the focusable overlay (There is a method for that somewhere :-)) // TODO: Consider adding a tablock method to avoid the possibility of escaping to the browser address bar - But maybe have a discussion about this in the PR instead?... function toggleInert (editors) { @@ -105,6 +77,21 @@ function toggleInert (editors) { editorChildren.attr('inert',''); lastEditorChild.removeAttr('inert'); + + // Add focus to the first element in the potential infinite overlay that lays behind the one that we just closed + setFocusableElement(lastEditorChild); +} + +function setFocusableElement(element) { + var focusableElementsString = 'a[href], area[href], input:not([disabled]):not(.ng-hide), select:not([disabled]), textarea:not([disabled]), button:not([disabled]):not([tabindex="-1"]), iframe, object, embed, [tabindex="0"], [contenteditable]'; + var focusableElements = element.find(focusableElementsString); + + if(focusableElements.length){ + var firstFocusableElement = $(focusableElements[0]); + console.log(firstFocusableElement); + + // TODO: Figure out how to make sure that the first element receives focus... + } } angular.module('umbraco.services').factory('focusTrapService', focusTrapService); From d86d5b2e8c91d364dc5ea9210cc73c44168ca3e2 Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 31 Mar 2019 20:14:38 +0200 Subject: [PATCH 10/54] Set focus on the new refocused element - Probably needs to be refactored but it will do for now --- .../src/common/services/focustrap.service.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js index 81c446147757..c1713ee61ca2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js @@ -87,10 +87,13 @@ function setFocusableElement(element) { var focusableElements = element.find(focusableElementsString); if(focusableElements.length){ - var firstFocusableElement = $(focusableElements[0]); - console.log(firstFocusableElement); + var firstFocusableElement = focusableElements[0]; - // TODO: Figure out how to make sure that the first element receives focus... + // TODO: Figure out why it's necessary to make use of setTimeout and why it's necessary to remove the focus attributes + setTimeout(function(){ + $(firstFocusableElement).removeAttr('auto-umb-focus').removeAttr('focus-on-filled').focus(); + $(firstFocusableElement).attr('auto-umb-focus'); + },100); } } From aaca68bc4885628ab78c265357a49ba5b36c35ad Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 31 Mar 2019 21:58:26 +0200 Subject: [PATCH 11/54] Refactor code to resemble the setup in other services where a "service" object is returned --- .../src/common/services/focustrap.service.js | 182 +++++++++--------- 1 file changed, 96 insertions(+), 86 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js index c1713ee61ca2..c8be9a261a81 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/focustrap.service.js @@ -1,100 +1,110 @@ -/** Used to improve accesibility ensuring that the focus is trapped inside the active overlay and also ensuring aria-hidden is set accordingly so potential screen readers don't get confused about the context either */ +/** + @ngdoc service + * @name umbraco.services.focusTrapService + * + * @description + * Added in Umbraco 8.0.2. Service for adding focus traps to overlays. Intended for internal use only to improve accesibility of overlays. + */ function focusTrapService() { - // TODO: If possible I would like to store all the needed DOM references in an elements object, wich could then be passed to the methods so we don't need to reference and store them in each method - // But some of the elements are not available untill the app.authenticated event happens... Not sure about the best practice around this... - console.log('trap da focus mayn!'); - - return { - addFocusTrap: function(mode){ - if(mode === 'modal'){ - addFocusTrapOverlayMode(); - } - - if(mode === 'infinite'){ - addFocusTrapInfiniteMode(); - } - }, - removeFocusTrap: function (mode) { - if (mode === 'modal') { - removeFocusTrapOverlayMode(); - } + // TODO: Figure out how the needed DOM references can be put into an object that can be passed to the method so we don't need to get them from the DOM in each of the methods + // TODO: Figure out how to avoid the setTimeout methods + // TODO: Consider if some of the DOM manipulation should be done using a directive somehow... + // TODO: Consider adding a tablock method to avoid the possibility of escaping to the browser address bar - But maybe have a discussion about this in the PR instead?... - if (mode === 'infinite') { - removeFocusTrapInfiniteMode(); - } + function addFocusTrapOverlayMode () { + var mainWrapper = $('#mainwrapper'); + + mainWrapper.attr('inert',''); + } + + function removeFocusTrapOverlayMode () { + var mainWrapper = $('#mainwrapper'); + mainWrapper.removeAttr('inert'); + } + + function addFocusTrapInfiniteMode () { + var appHeader = $('.umb-app-header'); + var leftColumn = $('#leftcolumn'); + var contentColumn = $('#contentcolumn > div:first-child'); + var editors = $('.umb-editors'); + + // Remove focus from any interactive elements in the "appHeader", "leftColumn" and the first child in the "contentColumn" + appHeader.attr('inert',''); + leftColumn.attr('inert',''); + contentColumn.attr('inert',''); + + setTimeout(function(){ + toggleInert(editors); + }, 100); + } + + function removeFocusTrapInfiniteMode () { + var appHeader = $('.umb-app-header'); + var leftColumn = $('#leftcolumn'); + var contentColumn = $('#contentcolumn > div:first-child'); + + // Remove the inert attribute from the key elements so they're tabable once the infinite editing mode has been deactivated + appHeader.removeAttr('inert'); + leftColumn.removeAttr('inert'); + contentColumn.removeAttr('inert'); + } + + function toggleInert (editors) { + const editorChildren = editors.children(); + const lastEditorChildIndex = editorChildren.length - 1; + const lastEditorChild = $(editorChildren[lastEditorChildIndex]); + + editorChildren.attr('inert',''); + lastEditorChild.removeAttr('inert'); + + // Add focus to the first element in the potential infinite overlay that lays behind the one that we just closed + setFocusableElement(lastEditorChild); + } + + function setFocusableElement(element) { + var focusableElementsString = 'a[href], area[href], input:not([disabled]):not(.ng-hide), select:not([disabled]), textarea:not([disabled]), button:not([disabled]):not([tabindex="-1"]), iframe, object, embed, [tabindex="0"], [contenteditable]'; + var focusableElements = element.find(focusableElementsString); + + if(focusableElements.length){ + var firstFocusableElement = focusableElements[0]; + + setTimeout(function(){ + $(firstFocusableElement).removeAttr('auto-umb-focus').removeAttr('focus-on-filled').focus(); + $(firstFocusableElement).attr('auto-umb-focus'); + },100); } - }; -} - -function addFocusTrapOverlayMode () { - var mainWrapper = $('#mainwrapper'); - - mainWrapper.attr('inert',''); -} - -function removeFocusTrapOverlayMode () { - var mainWrapper = $('#mainwrapper'); - - mainWrapper.removeAttr('inert'); -} - -function addFocusTrapInfiniteMode () { - var appHeader = $('.umb-app-header'); - var leftColumn = $('#leftcolumn'); - var contentColumn = $('#contentcolumn > div:first-child'); - var editors = $('.umb-editors'); - - // Remove focus from any interactive elements in the "appHeader", "leftColumn" and the first child in the "contentColumn" - appHeader.attr('inert',''); - leftColumn.attr('inert',''); - contentColumn.attr('inert',''); - - // TODO: Maybe this can be refactored into being a watch thingy detecting when add/remove of an infinite overlay is done so we can skip the timeout function - But it works for now - setTimeout(function(){ - toggleInert(editors); - }, 100); -} - -function removeFocusTrapInfiniteMode () { - var appHeader = $('.umb-app-header'); - var leftColumn = $('#leftcolumn'); - var contentColumn = $('#contentcolumn > div:first-child'); - - // Remove the inert attribute from the key elements so they're tabable once the infinite editing mode has been deactivated - appHeader.removeAttr('inert'); - leftColumn.removeAttr('inert'); - contentColumn.removeAttr('inert'); -} - -// TODO: Consider adding a tablock method to avoid the possibility of escaping to the browser address bar - But maybe have a discussion about this in the PR instead?... - -function toggleInert (editors) { - const editorChildren = editors.children(); - const lastEditorChildIndex = editorChildren.length - 1; - const lastEditorChild = $(editorChildren[lastEditorChildIndex]); + } - editorChildren.attr('inert',''); - lastEditorChild.removeAttr('inert'); + function addFocusTrap(mode){ + if(mode === 'modal'){ + addFocusTrapOverlayMode(); + } - // Add focus to the first element in the potential infinite overlay that lays behind the one that we just closed - setFocusableElement(lastEditorChild); -} + if(mode === 'infinite'){ + addFocusTrapInfiniteMode(); + } + } -function setFocusableElement(element) { - var focusableElementsString = 'a[href], area[href], input:not([disabled]):not(.ng-hide), select:not([disabled]), textarea:not([disabled]), button:not([disabled]):not([tabindex="-1"]), iframe, object, embed, [tabindex="0"], [contenteditable]'; - var focusableElements = element.find(focusableElementsString); + function removeFocusTrap (mode) { + if (mode === 'modal') { + removeFocusTrapOverlayMode(); + } - if(focusableElements.length){ - var firstFocusableElement = focusableElements[0]; + if (mode === 'infinite') { + removeFocusTrapInfiniteMode(); + } + } - // TODO: Figure out why it's necessary to make use of setTimeout and why it's necessary to remove the focus attributes - setTimeout(function(){ - $(firstFocusableElement).removeAttr('auto-umb-focus').removeAttr('focus-on-filled').focus(); - $(firstFocusableElement).attr('auto-umb-focus'); - },100); + // Define the service object + var service = { + addFocusTrap: addFocusTrap, + removeFocusTrap: removeFocusTrap } + + // Return the service object + return service; } angular.module('umbraco.services').factory('focusTrapService', focusTrapService); From f284da7a7371a1143e4d2391371f8a1290a51a44 Mon Sep 17 00:00:00 2001 From: BatJan Date: Sun, 31 Mar 2019 21:59:19 +0200 Subject: [PATCH 12/54] add the addFocusTrap and removeFocusTrap functions to the overlay service --- .../src/common/services/overlay.service.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js index 6de0b4170b2a..54f6623a571d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js @@ -8,7 +8,7 @@ (function () { "use strict"; - function overlayService(eventsService, backdropService) { + function overlayService(eventsService, backdropService, focusTrapService) { var currentOverlay = null; @@ -37,6 +37,9 @@ backdropOptions.disableEventsOnClick = true; } + // Add focus trap + focusTrapService.addFocusTrap('modal'); + overlay.show = true; backdropService.open(backdropOptions); currentOverlay = overlay; @@ -46,6 +49,10 @@ function close() { backdropService.close(); currentOverlay = null; + + // Remove focus trap + focusTrapService.removeFocusTrap('modal'); + eventsService.emit("appState.overlay", null); } @@ -72,4 +79,4 @@ angular.module("umbraco.services").factory("overlayService", overlayService); -})(); \ No newline at end of file +})(); From c4afcf761edd866253c9205a50e1d75198cbb68d Mon Sep 17 00:00:00 2001 From: BatJan Date: Mon, 1 Apr 2019 20:11:37 +0200 Subject: [PATCH 13/54] Remove initial TODO comment --- .../directives/components/editor/umbeditors.directive.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index 689aa13efb4d..933f8b76e912 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -1,9 +1,3 @@ -/** TODO - * Make some reusable code that can set all other relevant areas to have the "inert" attribute apart from the infinite overlay - * Keep in mind that the code setting the inert attribute is also highly relevant for being used in overlay's that appear when deleting / browsing away from something - * However that might be for another PR if/once this current PR is accepted - */ - (function () { 'use strict'; From 6d717113fe81b46c06b5b07f95a591ef41fb17f2 Mon Sep 17 00:00:00 2001 From: BatJan Date: Tue, 2 Apr 2019 18:03:51 +0200 Subject: [PATCH 14/54] Started refactoring the focusTrap service into an inert directive instead, which seems to be the more correct solution. Therefore removing calls to the focusTrap service while leaving the service for refernece while doing the directive --- .../components/editor/umbeditors.directive.js | 11 +---------- .../src/common/services/overlay.service.js | 8 +------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index 933f8b76e912..a4d18f1df7cd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function EditorsDirective($timeout, eventsService, focusTrapService) { + function EditorsDirective($timeout, eventsService) { function link(scope, el, attr, ctrl) { @@ -80,15 +80,6 @@ iEditor.inFront = iEditor.level >= ceiling; i++; } - - // If there are any active editors we call the addFocusTrap method - if(len > 0){ - focusTrapService.addFocusTrap('infinite'); - } - // Otherwise we make sure to remove the focus trap - else{ - focusTrapService.removeFocusTrap('infinite'); - } } evts.push(eventsService.on("appState.editors.open", function (name, args) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js index 54f6623a571d..2e63baabc23b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js @@ -8,7 +8,7 @@ (function () { "use strict"; - function overlayService(eventsService, backdropService, focusTrapService) { + function overlayService(eventsService, backdropService) { var currentOverlay = null; @@ -37,9 +37,6 @@ backdropOptions.disableEventsOnClick = true; } - // Add focus trap - focusTrapService.addFocusTrap('modal'); - overlay.show = true; backdropService.open(backdropOptions); currentOverlay = overlay; @@ -50,9 +47,6 @@ backdropService.close(); currentOverlay = null; - // Remove focus trap - focusTrapService.removeFocusTrap('modal'); - eventsService.emit("appState.overlay", null); } From a7faddedde613baaeafd43dc37a9b6640e0cb421 Mon Sep 17 00:00:00 2001 From: BatJan Date: Tue, 2 Apr 2019 20:40:22 +0200 Subject: [PATCH 15/54] add umbInertAttribute directive - Still WIP --- .../util/umbInertAttribute.directive.js | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/util/umbInertAttribute.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/umbInertAttribute.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/umbInertAttribute.directive.js new file mode 100644 index 000000000000..50fd066ebe8b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/umbInertAttribute.directive.js @@ -0,0 +1,70 @@ +(function () { + 'use strict'; + + function umbInertAttributeDirective(eventsService) { + var directive = { + restrict: "A", // Can only be used as an attribute + scope: {"umbInertAttribute":"@"}, + link: function (scope, element, attrs) { + // If the value passed to the "umbInertAttribute" is "overlay" we will add/remove the inert attribute depending on what is emitted + if (scope.umbInertAttribute === 'infinite-overlay') { + eventsService.on('appState.editors.open', function (name, args) { + console.log('an infinite editor just opened'); + + // The umb-editor needs a special touch :) + if (element.hasClass('umb-editor--infinityMode')) { + console.log(element,'what element are we dealing with?'); + console.log(args,'what are the passed args?'); + } + // Set the inert attribute if it's missing on the element + else { + if (!element.attr('inert')) { + element.attr('inert',''); + } + } + + // TODO: Perhaps a mutation observer is needed for the "umb-editor" stuff... + }); + + eventsService.on('appState.editors.close', function (name, args) { + console.log('an infinite editor just closed'); + console.log(element,'what element are we dealing with?'); + console.log(args,'what are the passed args?'); + + if (element.hasClass('umb-editor--infinityMode')) { + console.log('do something special'); + } + // Figure out if any editors are still en view before removing the inert attribute + else { + if (args.editors.length === 0) { + element.removeAttr('inert'); + } + } + + // element.removeAttr('inert',''); // TODO: do something special if it's the "editors" element we're on AND make sure to check the args and see if the inert attribute needs to be removed + }); + } + + // If the value passed to the "umbInertAttribute" is "overlay" we will add/remove the inert attribute when the overlay is toggled + if (scope.umbInertAttribute === 'overlay'){ + eventsService.on('appState.overlay', function (name, args) { + // Add the inert attribute + if (args !== null) { + element.attr('inert',''); + } + // Otherwise we will remove it again + else { + element.removeAttr('inert',''); + } + }); + } + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbInertAttribute', umbInertAttributeDirective); + +})(); From 43ad1ef844901753cd88c0b57ac18227f48f5095 Mon Sep 17 00:00:00 2001 From: BatJan Date: Tue, 2 Apr 2019 20:41:08 +0200 Subject: [PATCH 16/54] Add inert directive where it's needed in order to help us achieve the tab lock --- .../src/views/components/application/umb-app-header.html | 4 ++-- .../src/views/components/application/umb-navigation.html | 2 +- .../src/views/components/editor/umb-editors.html | 1 + src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index 07d25e7465d5..0ed077a6c90d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -1,7 +1,7 @@
-
- +
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index 829582329fd8..2693970773cd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -2,7 +2,7 @@