From d9f091be239432331be9ebbba434b1b87d8790c3 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:34:02 +0200 Subject: [PATCH 01/35] Toggle the inert attribute when adding or removing the first/last editor --- .../directives/components/editor/umbeditors.directive.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 672b3fa286db..5e2f3713d1a8 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) { + function EditorsDirective($timeout, eventsService, focusLockService) { function link(scope, el, attr, ctrl) { @@ -27,6 +27,9 @@ if(isLeftColumnAbove){ $(sectionId).removeClass(aboveBackDropCssClass); } + + // Add the inert attribute + focusLockService.addInertAttribute(); } $timeout(() => { @@ -54,6 +57,9 @@ } isLeftColumnAbove = false; + + // Remove the inert attribute + focusLockService.removeInertAttribute(); } } From c44821cd3eaa60dc1c2426309d8971031b7f5077 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:34:47 +0200 Subject: [PATCH 02/35] Add focus lock directive for the editor --- .../src/views/components/editor/umb-editors.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html index 2f1286b09056..0e8debbd1ec7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html @@ -2,6 +2,7 @@
Date: Sat, 1 Aug 2020 13:15:43 +0200 Subject: [PATCH 06/35] Conditionally add umb-focus-lock and inert attributes --- .../src/views/components/editor/umb-editors.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html index 0e8debbd1ec7..c2152f80d35a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html @@ -2,7 +2,8 @@
  • -
    +
    From 4d16703d58b99107a1089c5421fb82f1fc4567fc Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sun, 2 Aug 2020 17:39:47 +0200 Subject: [PATCH 17/35] Narrow attributes to look for down to the bare minimum of the umb-focus-lock --- .../directives/components/forms/umbfocuslock.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 7b5445cc4feb..bce58e3f603d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -66,7 +66,7 @@ var observer = new MutationObserver(domChange); // Options for the observer (which mutations to observe) - var config = { attributes: true, attributeOldValue: true, childList: true, subtree: true}; + var config = { attributes: true, attributeFilter: ['umb-focus-lock'], childList: true, subtree: true}; function domChange(mutationsList) { if(!init){ From 051c8f9031c22aae49a526d87187e02a91898458 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sun, 2 Aug 2020 17:45:03 +0200 Subject: [PATCH 18/35] Refactor to using good ol' for loop (Fastest) --- .../directives/components/forms/umbfocuslock.directive.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index bce58e3f603d..3ebd8d89218e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -74,8 +74,9 @@ return; } - for (var mutation of mutationsList) { - + for (let index = 0; index < mutationsList.length; index++) { + const mutation = mutationsList[index]; + // Look at the attributes - If the disabled attribute changes we call the getFocusableElements method // ensuring the enabled element can be tabbed into if (mutation.type === 'attributes') { From 64ffc49934045573f25837923573b305d36e4fa0 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sun, 2 Aug 2020 18:39:26 +0200 Subject: [PATCH 19/35] Disconnect the observer once the init function has been called - Massive performance improvement --- .../directives/components/forms/umbfocuslock.directive.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 3ebd8d89218e..54a68efdcc7e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -83,6 +83,9 @@ if(mutation.attributeName === 'umb-focus-lock') { onInit(mutation.target); + + // Disconnect the observer once onInit has been called again + observer.disconnect(); } } } @@ -149,3 +152,7 @@ angular.module('umbraco.directives').directive('umbFocusLock', FocusLock); })(); + + +// TODO: observer.disconnect() on destroy - Ensure it does not conflict with the eventlistener for the closed event! +// TODO: removeEventHandlers on destroy - Be ware of the same as weith observer.disconnect(); From 1108a676663602bc16aec0712a402ac68930f1e0 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Mon, 3 Aug 2020 21:10:12 +0200 Subject: [PATCH 20/35] Event handler cleanup --- .../forms/umbfocuslock.directive.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 54a68efdcc7e..3a0b65cb2ca8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -95,8 +95,31 @@ observer.observe(targetToObserve, config); } + function cleanupEventHandlers() { + const infiniteEditorsWrapper = document.querySelector('.umb-editors'); + const infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor')); + const activeEditor = infiniteEditors[infiniteEditors.length - 1]; + const inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); + + if(inactiveEditors.length > 0) { + for (let index = 0; index < inactiveEditors.length; index++) { + const inactiveEditor = inactiveEditors[index]; + + // Remove event handlers from inactive editors + inactiveEditor.removeEventListener('keydown', handleKeydown); + } + } + else { + // Remove event handlers from the active editor + activeEditor.removeEventListener('keydown', handleKeydown); + } + } + function onInit(targetElm) { $timeout(function() { + + cleanupEventHandlers(); + getFocusableElements(targetElm); if(focusableElements.length > 0) { From f0239aabc6489daf001955bb70db1cda50db0553 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Tue, 4 Aug 2020 21:47:43 +0200 Subject: [PATCH 21/35] Refactor the code to re-initialize the init method on destroy in case infinite editors still exists in the DOM --- .../forms/umbfocuslock.directive.js | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 3a0b65cb2ca8..110b5dbcf4c9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -20,11 +20,19 @@ var focusableElements; var firstFocusableElement; var lastFocusableElement; + var infiniteEditorsWrapper; + var infiniteEditors; + var disconnectObserver = false; // List of elements that can be focusable within the focus lock var focusableElementsSelector = 'a[href]:not([disabled]):not(.ng-hide), button:not([disabled]):not(.ng-hide), textarea:not([disabled]):not(.ng-hide), input:not([disabled]):not(.ng-hide), select:not([disabled]):not(.ng-hide)'; var bodyElement = document.querySelector('body'); + function getDomNodes(){ + infiniteEditorsWrapper = document.querySelector('.umb-editors'); + infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor')); + } + function getFocusableElements(targetElm) { var elm = targetElm ? targetElm : target; focusableElements = elm.querySelectorAll(focusableElementsSelector); @@ -66,38 +74,22 @@ var observer = new MutationObserver(domChange); // Options for the observer (which mutations to observe) - var config = { attributes: true, attributeFilter: ['umb-focus-lock'], childList: true, subtree: true}; - - function domChange(mutationsList) { - if(!init){ - getFocusableElements(); - return; - } - - for (let index = 0; index < mutationsList.length; index++) { - const mutation = mutationsList[index]; - - // Look at the attributes - If the disabled attribute changes we call the getFocusableElements method - // ensuring the enabled element can be tabbed into - if (mutation.type === 'attributes') { + var config = { attributes: true, childList: true, subtree: true}; - if(mutation.attributeName === 'umb-focus-lock') { - onInit(mutation.target); - - // Disconnect the observer once onInit has been called again - observer.disconnect(); - } - } - } + function domChange() { + getFocusableElements(); } // Start observing the target node for configured mutations observer.observe(targetToObserve, config); + + // Disconnect observer + if(disconnectObserver){ + observer.disconnect(); + } } function cleanupEventHandlers() { - const infiniteEditorsWrapper = document.querySelector('.umb-editors'); - const infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor')); const activeEditor = infiniteEditors[infiniteEditors.length - 1]; const inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); @@ -115,10 +107,17 @@ } } - function onInit(targetElm) { - $timeout(function() { + function onInit(targetElm, delay) { + const timeout = delay ? delay : 500; + + $timeout(() => { - cleanupEventHandlers(); + getDomNodes(); + + // Only do the cleanup if we're in infinite editing mode + if(infiniteEditors.length > 0){ + cleanupEventHandlers(); + } getFocusableElements(targetElm); @@ -144,22 +143,25 @@ target.addEventListener('keydown', handleKeydown); } - }, 500); + }, timeout); } onInit(); - // Reinitialize the onInit() method if it was not the last editor that was closed - eventsService.on('appState.editors.close', (event, args) => { - if(args && args.editors.length !== 0) { - if(target.hasAttribute('umb-focus-lock')) { - observeDomChanges(true); - } - } - }); - - // Remove the event listener + // If more than one editor is still open then re-initialize otherwise remove the event listener scope.$on('$destroy', function () { + // Make sure to disconnect the observer so we potentially don't end up with having many active ones + disconnectObserver = true; + + // Pass the correct editor in order to find the focusable elements + const newTarget = infiniteEditors[infiniteEditors.length - 2]; + + if(infiniteEditors.length > 1){ + // Passing the timeout parameter as a string on purpose to bypass the falsy value that a number would give + onInit(newTarget, '0'); + return; + } + target.removeEventListener('keydown', handleKeydown); }); } @@ -175,7 +177,3 @@ angular.module('umbraco.directives').directive('umbFocusLock', FocusLock); })(); - - -// TODO: observer.disconnect() on destroy - Ensure it does not conflict with the eventlistener for the closed event! -// TODO: removeEventHandlers on destroy - Be ware of the same as weith observer.disconnect(); From 538600d67fa0340b0adc54ca78b1b1fb1b69377a Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Tue, 4 Aug 2020 22:07:23 +0200 Subject: [PATCH 22/35] Align codestyle --- .../components/forms/umbfocuslock.directive.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 110b5dbcf4c9..c6458ef81d90 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -90,12 +90,12 @@ } function cleanupEventHandlers() { - const activeEditor = infiniteEditors[infiniteEditors.length - 1]; - const inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); + var activeEditor = infiniteEditors[infiniteEditors.length - 1]; + var inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); if(inactiveEditors.length > 0) { - for (let index = 0; index < inactiveEditors.length; index++) { - const inactiveEditor = inactiveEditors[index]; + for (var index = 0; index < inactiveEditors.length; index++) { + var inactiveEditor = inactiveEditors[index]; // Remove event handlers from inactive editors inactiveEditor.removeEventListener('keydown', handleKeydown); @@ -108,7 +108,7 @@ } function onInit(targetElm, delay) { - const timeout = delay ? delay : 500; + var timeout = delay ? delay : 500; $timeout(() => { @@ -154,7 +154,7 @@ disconnectObserver = true; // Pass the correct editor in order to find the focusable elements - const newTarget = infiniteEditors[infiniteEditors.length - 2]; + var newTarget = infiniteEditors[infiniteEditors.length - 2]; if(infiniteEditors.length > 1){ // Passing the timeout parameter as a string on purpose to bypass the falsy value that a number would give From 4e7a5774a547e5fadaa1b3d29415ae1d20509d0e Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Thu, 6 Aug 2020 20:37:26 +0200 Subject: [PATCH 23/35] Add logic to deal with "lastKnowFocused" elements in infinite editing mode --- .../forms/umbfocuslock.directive.js | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index c6458ef81d90..af40e2204d69 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function FocusLock($timeout, eventsService) { + function FocusLock($timeout, $rootScope) { function getAutoFocusElement (elements) { var elmentWithAutoFocus = null; @@ -23,6 +23,13 @@ var infiniteEditorsWrapper; var infiniteEditors; var disconnectObserver = false; + var closingEditor = false; + + if(!$rootScope.lastKnowFocusedElements){ + $rootScope.lastKnowFocusedElements = []; + } + + $rootScope.lastKnowFocusedElements.push(document.activeElement); // List of elements that can be focusable within the focus lock var focusableElementsSelector = 'a[href]:not([disabled]):not(.ng-hide), button:not([disabled]):not(.ng-hide), textarea:not([disabled]):not(.ng-hide), input:not([disabled]):not(.ng-hide), select:not([disabled]):not(.ng-hide)'; @@ -66,6 +73,55 @@ } } } + + function clearLastKnownFocusedElements() { + $rootScope.lastKnowFocusedElements = []; + } + + function setElementFocus() { + var defaultFocusedElement = getAutoFocusElement(focusableElements); + var lastknownElement; + + if(closingEditor){ + var lastItemIndex = $rootScope.lastKnowFocusedElements.length - 1; + var editorInfo = infiniteEditors[0].querySelector('.editor-info'); + + // If there is only one editor open, search for the "editor-info" inside it and set focus on it + // This is relevant when a property editor has been selected and the editor where we selected it from + // is closed taking us back to the first layer + // Otherwise set it to the last element in the lastKnownFocusedElements array + if(infiniteEditors.length === 1 && editorInfo !== null){ + lastknownElement = editorInfo; + + // Clear the array + clearLastKnownFocusedElements(); + } + else { + lastknownElement = $rootScope.lastKnowFocusedElements[lastItemIndex]; + + // Remove the last item from the array so we always set the correct lastKnowFocus for each layer + $rootScope.lastKnowFocusedElements.splice(lastItemIndex, 1); + } + + // Update the lastknowelement variable here + closingEditor = false; + } + + // 1st - we check for any last known element - Usually the element the trigger the opening of a new layer + // If it exists it will receive fous + // 2nd - We check to see if a default focus has been set using the umb-auto-focus directive. If not we set focus on + // the first focusable element + // 3rd - Otherwise put the focus on the default focused element + if(lastknownElement){ + lastknownElement.focus(); + } + else if(defaultFocusedElement === null ){ + firstFocusableElement.focus(); + } + else { + defaultFocusedElement.focus(); + } + } function observeDomChanges(init) { var targetToObserve = init ? document.querySelector('.umb-editors') : target; @@ -124,20 +180,12 @@ if(focusableElements.length > 0) { observeDomChanges(); - - var defaultFocusedElement = getAutoFocusElement(focusableElements); // We need to add the tabbing-active class in order to highlight the focused button since the default style is // outline: none; set in the stylesheet specifically bodyElement.classList.add('tabbing-active'); - // If there is no default focused element put focus on the first focusable element in the nodelist - if(defaultFocusedElement === null ){ - firstFocusableElement.focus(); - } - else { - defaultFocusedElement.focus(); - } + setElementFocus(); // Handle keydown target.addEventListener('keydown', handleKeydown); @@ -157,11 +205,20 @@ var newTarget = infiniteEditors[infiniteEditors.length - 2]; if(infiniteEditors.length > 1){ + // Setting closing till true will let us re-apply the last known focus to then opened layer that then becomes + // active + closingEditor = true; + // Passing the timeout parameter as a string on purpose to bypass the falsy value that a number would give onInit(newTarget, '0'); + return; } + // Clear lastKnowFocusedElements + clearLastKnownFocusedElements(); + + // Cleanup event handler target.removeEventListener('keydown', handleKeydown); }); } From 18bbe3489577681e9d0ecced0f682e1c6c5d43a6 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sun, 23 Aug 2020 00:19:10 +0200 Subject: [PATCH 24/35] Re-add attributes after merge with contrib branch --- .../src/views/components/editor/umb-editors.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html index 9bb5a6161d78..4d6ceb2ffca1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html @@ -1,7 +1,9 @@
    - 0) { for (var index = 0; index < inactiveEditors.length; index++) { var inactiveEditor = inactiveEditors[index]; - + // Remove event handlers from inactive editors inactiveEditor.removeEventListener('keydown', handleKeydown); } @@ -169,7 +169,7 @@ $timeout(() => { getDomNodes(); - + // Only do the cleanup if we're in infinite editing mode if(infiniteEditors.length > 0){ cleanupEventHandlers(); @@ -180,13 +180,13 @@ if(focusableElements.length > 0) { observeDomChanges(); - + // We need to add the tabbing-active class in order to highlight the focused button since the default style is // outline: none; set in the stylesheet specifically bodyElement.classList.add('tabbing-active'); setElementFocus(); - + // Handle keydown target.addEventListener('keydown', handleKeydown); } @@ -214,8 +214,8 @@ return; } - - // Clear lastKnowFocusedElements + + // Clear lastKnownFocusableElements clearLastKnownFocusedElements(); // Cleanup event handler From b69af2a5827f5f411fc456c7e984d980f0c8673e Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sat, 26 Sep 2020 16:28:29 +0200 Subject: [PATCH 26/35] Move onInit into the $includeContentLoaded event and set the timeout to 0 --- .../components/forms/umbfocuslock.directive.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 2c2f2fc6bec5..30edc6058c31 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -191,10 +191,16 @@ target.addEventListener('keydown', handleKeydown); } - }, timeout); + }, 0); } - onInit(); + scope.$on('$includeContentLoaded', () => { + scope.$evalAsync(); + + onInit(); + }); + + // onInit(); // If more than one editor is still open then re-initialize otherwise remove the event listener scope.$on('$destroy', function () { From e8c7156107a73c88e8729623ab0ad3de37415987 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sun, 27 Sep 2020 21:01:18 +0200 Subject: [PATCH 27/35] Make sure to add focus to elements with role="button" as well --- .../directives/components/forms/umbfocuslock.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 30edc6058c31..37a43efc7002 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -32,7 +32,7 @@ $rootScope.lastKnownFocusableElements.push(document.activeElement); // List of elements that can be focusable within the focus lock - var focusableElementsSelector = 'a[href]:not([disabled]):not(.ng-hide), button:not([disabled]):not(.ng-hide), textarea:not([disabled]):not(.ng-hide), input:not([disabled]):not(.ng-hide), select:not([disabled]):not(.ng-hide)'; + var focusableElementsSelector = '[role="button"], a[href]:not([disabled]):not(.ng-hide), button:not([disabled]):not(.ng-hide), textarea:not([disabled]):not(.ng-hide), input:not([disabled]):not(.ng-hide), select:not([disabled]):not(.ng-hide)'; var bodyElement = document.querySelector('body'); function getDomNodes(){ From 664a586b426f648879a3c54824ad968109dc87cf Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sun, 27 Sep 2020 22:17:09 +0200 Subject: [PATCH 28/35] Add comments and remove timeout / delay settings --- .../components/forms/umbfocuslock.directive.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 37a43efc7002..9a03928e6e6b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -82,6 +82,7 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var lastknownElement; + // If an inifite editor is being closed then we reset the focus to the element that triggered the the overlay if(closingEditor){ var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; var editorInfo = infiniteEditors[0].querySelector('.editor-info'); @@ -163,8 +164,7 @@ } } - function onInit(targetElm, delay) { - var timeout = delay ? delay : 500; + function onInit(targetElm) { $timeout(() => { @@ -191,7 +191,7 @@ target.addEventListener('keydown', handleKeydown); } - }, 0); + }); } scope.$on('$includeContentLoaded', () => { @@ -200,8 +200,6 @@ onInit(); }); - // onInit(); - // If more than one editor is still open then re-initialize otherwise remove the event listener scope.$on('$destroy', function () { // Make sure to disconnect the observer so we potentially don't end up with having many active ones @@ -215,8 +213,7 @@ // active closingEditor = true; - // Passing the timeout parameter as a string on purpose to bypass the falsy value that a number would give - onInit(newTarget, '0'); + onInit(newTarget); return; } From 44c34e211c1289d32c090d7d054d40c8c1c88b61 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sun, 27 Sep 2020 23:07:05 +0200 Subject: [PATCH 29/35] Debouce domObserver --- .../directives/components/forms/umbfocuslock.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 9a03928e6e6b..bc877f3e1967 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -128,7 +128,7 @@ var targetToObserve = init ? document.querySelector('.umb-editors') : target; // Watch for DOM changes - so we can refresh the focusable elements if an element // changes from being disabled to being enabled for instance - var observer = new MutationObserver(domChange); + var observer = new MutationObserver(_.debounce(domChange, 200)); // Options for the observer (which mutations to observe) var config = { attributes: true, childList: true, subtree: true}; From 5c02d18760868eb5b4f235f22622e42f3865e7af Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sun, 27 Sep 2020 23:29:20 +0200 Subject: [PATCH 30/35] Wrap init function in safeApply --- .../directives/components/forms/umbfocuslock.directive.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index bc877f3e1967..2355478a237f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function FocusLock($timeout, $rootScope) { + function FocusLock($timeout, $rootScope, angularHelper) { function getAutoFocusElement (elements) { var elmentWithAutoFocus = null; @@ -195,9 +195,9 @@ } scope.$on('$includeContentLoaded', () => { - scope.$evalAsync(); - - onInit(); + angularHelper.safeApply(scope, () => { + onInit(); + }); }); // If more than one editor is still open then re-initialize otherwise remove the event listener From 7fe763742b8b27bd178e1221f9224083a719388c Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sat, 17 Oct 2020 14:31:27 +0200 Subject: [PATCH 31/35] Add comments to help remember / understand what things are intended to be doing and add missing event param as well as getting rid of some unused code --- .../components/forms/umbfocuslock.directive.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 2355478a237f..7310abd8bc4f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -3,6 +3,7 @@ function FocusLock($timeout, $rootScope, angularHelper) { + // If the umb-auto-focus directive is in use we respect that by leaving the default focus on it instead of choosing the first focusable element using this function function getAutoFocusElement (elements) { var elmentWithAutoFocus = null; @@ -33,6 +34,8 @@ // List of elements that can be focusable within the focus lock var focusableElementsSelector = '[role="button"], a[href]:not([disabled]):not(.ng-hide), button:not([disabled]):not(.ng-hide), textarea:not([disabled]):not(.ng-hide), input:not([disabled]):not(.ng-hide), select:not([disabled]):not(.ng-hide)'; + + // Grab the body element so we can add the tabbing class on it when needed var bodyElement = document.querySelector('body'); function getDomNodes(){ @@ -49,7 +52,7 @@ return focusableElements; } - function handleKeydown() { + function handleKeydown(event) { var isTabPressed = (event.key === 'Tab' || event.keyCode === 9); if (!isTabPressed){ @@ -124,8 +127,7 @@ } } - function observeDomChanges(init) { - var targetToObserve = init ? document.querySelector('.umb-editors') : target; + function observeDomChanges() { // Watch for DOM changes - so we can refresh the focusable elements if an element // changes from being disabled to being enabled for instance var observer = new MutationObserver(_.debounce(domChange, 200)); @@ -138,7 +140,7 @@ } // Start observing the target node for configured mutations - observer.observe(targetToObserve, config); + observer.observe(target, config); // Disconnect observer if(disconnectObserver){ @@ -200,6 +202,7 @@ }); }); + // Only used for resetting the focus lock on infinite editors whenever a layer is closed and there is still one open // If more than one editor is still open then re-initialize otherwise remove the event listener scope.$on('$destroy', function () { // Make sure to disconnect the observer so we potentially don't end up with having many active ones @@ -237,3 +240,6 @@ angular.module('umbraco.directives').directive('umbFocusLock', FocusLock); })(); + + +// TODO: Ensure the domObserver is NOT started when there is only one infinite overlay and it's being destroyed! From 863fdff6fdc57149aea35c508d82b5764d283487 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sat, 17 Oct 2020 14:54:17 +0200 Subject: [PATCH 32/35] Adding more comments --- .../components/forms/umbfocuslock.directive.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 7310abd8bc4f..18f5881a11d9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -46,6 +46,8 @@ function getFocusableElements(targetElm) { var elm = targetElm ? targetElm : target; focusableElements = elm.querySelectorAll(focusableElementsSelector); + + // Maybe don't set these here but wait doing it until the observer has run? firstFocusableElement = focusableElements[0]; lastFocusableElement = focusableElements[focusableElements.length -1]; @@ -83,7 +85,7 @@ function setElementFocus() { var defaultFocusedElement = getAutoFocusElement(focusableElements); - var lastknownElement; + var lastKnownElement; // If an inifite editor is being closed then we reset the focus to the element that triggered the the overlay if(closingEditor){ @@ -95,13 +97,13 @@ // is closed taking us back to the first layer // Otherwise set it to the last element in the lastKnownFocusedElements array if(infiniteEditors.length === 1 && editorInfo !== null){ - lastknownElement = editorInfo; + lastKnownElement = editorInfo; // Clear the array clearLastKnownFocusedElements(); } else { - lastknownElement = $rootScope.lastKnownFocusableElements[lastItemIndex]; + lastKnownElement = $rootScope.lastKnownFocusableElements[lastItemIndex]; // Remove the last item from the array so we always set the correct lastKnowFocus for each layer $rootScope.lastKnownFocusableElements.splice(lastItemIndex, 1); @@ -116,8 +118,8 @@ // 2nd - We check to see if a default focus has been set using the umb-auto-focus directive. If not we set focus on // the first focusable element // 3rd - Otherwise put the focus on the default focused element - if(lastknownElement){ - lastknownElement.focus(); + if(lastKnownElement){ + lastKnownElement.focus(); } else if(defaultFocusedElement === null ){ firstFocusableElement.focus(); @@ -135,6 +137,8 @@ // Options for the observer (which mutations to observe) var config = { attributes: true, childList: true, subtree: true}; + // Whenever the DOM changes ensure the list of focused elements is updated + // TODO: Maybe track the first call for this function ensuring first and last focusable elements are detected initially and then just disover focusable elements function domChange() { getFocusableElements(); } @@ -170,9 +174,10 @@ $timeout(() => { + // Fetch the DOM nodes we need getDomNodes(); - // Only do the cleanup if we're in infinite editing mode + // Cleanup event handlers if we're in infinite editing mode if(infiniteEditors.length > 0){ cleanupEventHandlers(); } @@ -202,7 +207,6 @@ }); }); - // Only used for resetting the focus lock on infinite editors whenever a layer is closed and there is still one open // If more than one editor is still open then re-initialize otherwise remove the event listener scope.$on('$destroy', function () { // Make sure to disconnect the observer so we potentially don't end up with having many active ones From ce41333ec73f76c977adff8887737e46ae469f26 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sat, 17 Oct 2020 17:07:54 +0200 Subject: [PATCH 33/35] Move setting of first and last focusable elements into the setElement function --- .../components/forms/umbfocuslock.directive.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 18f5881a11d9..b0138d3b95f8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -46,12 +46,6 @@ function getFocusableElements(targetElm) { var elm = targetElm ? targetElm : target; focusableElements = elm.querySelectorAll(focusableElementsSelector); - - // Maybe don't set these here but wait doing it until the observer has run? - firstFocusableElement = focusableElements[0]; - lastFocusableElement = focusableElements[focusableElements.length -1]; - - return focusableElements; } function handleKeydown(event) { @@ -87,6 +81,10 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var lastKnownElement; + // Set first and last focusable elements + firstFocusableElement = focusableElements[0]; + lastFocusableElement = focusableElements[focusableElements.length - 1]; + // If an inifite editor is being closed then we reset the focus to the element that triggered the the overlay if(closingEditor){ var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; From b75ab71eb4fdf4c9203ccc042faca466f3a9fdf1 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sat, 17 Oct 2020 19:21:19 +0200 Subject: [PATCH 34/35] Remove todo --- .../common/directives/components/forms/umbfocuslock.directive.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index b0138d3b95f8..9ed336390e91 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -136,7 +136,6 @@ var config = { attributes: true, childList: true, subtree: true}; // Whenever the DOM changes ensure the list of focused elements is updated - // TODO: Maybe track the first call for this function ensuring first and last focusable elements are detected initially and then just disover focusable elements function domChange() { getFocusableElements(); } From a408a387a2acefdea38fcd850b222cddddab23a1 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Sat, 17 Oct 2020 20:03:12 +0200 Subject: [PATCH 35/35] Move the setup of first and last focusable elements back to where they were... --- .../directives/components/forms/umbfocuslock.directive.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 9ed336390e91..448f3fc8b1cf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -46,6 +46,9 @@ function getFocusableElements(targetElm) { var elm = targetElm ? targetElm : target; focusableElements = elm.querySelectorAll(focusableElementsSelector); + // Set first and last focusable elements + firstFocusableElement = focusableElements[0]; + lastFocusableElement = focusableElements[focusableElements.length - 1]; } function handleKeydown(event) { @@ -81,10 +84,6 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var lastKnownElement; - // Set first and last focusable elements - firstFocusableElement = focusableElements[0]; - lastFocusableElement = focusableElements[focusableElements.length - 1]; - // If an inifite editor is being closed then we reset the focus to the element that triggered the the overlay if(closingEditor){ var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1;