This commit is contained in:
Jun Pataleta
2025-02-05 18:05:39 +01:00
committed by Sara Arjona
8 changed files with 95 additions and 6 deletions
+1 -1
View File
@@ -8,6 +8,6 @@ define("core/local/aria/focuslock",["exports","./selectors"],(function(_exports,
* @module core/local/aria/focuslock
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.untrapFocus=_exports.trapFocus=void 0,_selectors=(obj=_selectors)&&obj.__esModule?obj:{default:obj};const lockRegionStack=[],initialFocusElementStack=[],finalFocusElementStack=[];let lastFocus=null,ignoreFocusChanges=!1,isLocked=!1;const lockHandler=event=>{if(ignoreFocusChanges)return;let lockRegion=getCurrentLockRegion();for(;lockRegion&&!document.contains(lockRegion);)untrapFocus(),lockRegion=getCurrentLockRegion();lockRegion&&(lockRegion.contains(event.target)?lastFocus=event.target:(focusFirstDescendant(),lastFocus==document.activeElement&&focusLastDescendant(),lastFocus=document.activeElement))},focusFirstDescendant=()=>{const lockRegion=getCurrentLockRegion(),focusableElements=Array.from(lockRegion.querySelectorAll(_selectors.default.elements.focusable));return focusableElements.unshift(lockRegion),focusableElements.some((focusableElement=>attemptFocus(focusableElement)))},focusLastDescendant=()=>{const lockRegion=getCurrentLockRegion(),focusableElements=Array.from(lockRegion.querySelectorAll(_selectors.default.elements.focusable)).reverse();return focusableElements.push(lockRegion),focusableElements.some((focusableElement=>attemptFocus(focusableElement)))},attemptFocus=focusTarget=>{if(!(focusTarget=>{if(focusTarget.tabIndex>0||0===focusTarget.tabIndex&&null!==focusTarget.getAttribute("tabIndex"))return!0;if(focusTarget.disabled)return!1;switch(focusTarget.nodeName){case"A":return!!focusTarget.href&&"ignore"!=focusTarget.rel;case"INPUT":return"hidden"!=focusTarget.type&&"file"!=focusTarget.type;case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}})(focusTarget))return!1;ignoreFocusChanges=!0;try{focusTarget.focus()}catch(e){}return ignoreFocusChanges=!1,document.activeElement===focusTarget},getCurrentLockRegion=()=>lockRegionStack[lockRegionStack.length-1];_exports.trapFocus=newLockRegion=>{if((newLockRegion=>{if(newLockRegion===getCurrentLockRegion())return;lockRegionStack.push(newLockRegion);const currentLockRegion=getCurrentLockRegion(),element=document.createElement("div");element.tabIndex=0,element.style.position="fixed",element.style.top=0,element.style.left=0;const initialNode=element.cloneNode();currentLockRegion.parentNode.insertBefore(initialNode,currentLockRegion),initialFocusElementStack.push(initialNode);const finalNode=element.cloneNode();currentLockRegion.parentNode.insertBefore(finalNode,currentLockRegion.nextSibling),finalFocusElementStack.push(finalNode)})(newLockRegion),isLocked||document.addEventListener("focus",lockHandler,!0),!focusFirstDescendant()){const currentLockRegion=getCurrentLockRegion(),originalRegionTabIndex=currentLockRegion.tabIndex;currentLockRegion.tabIndex=0,attemptFocus(currentLockRegion),currentLockRegion.tabIndex=originalRegionTabIndex}lastFocus=document.activeElement,isLocked=!0};const untrapFocus=()=>{(()=>{lockRegionStack.pop();const finalNode=finalFocusElementStack.pop();finalNode&&finalNode.remove();const initialNode=initialFocusElementStack.pop();initialNode&&initialNode.remove()})(),lockRegionStack.length||(document.removeEventListener("focus",lockHandler,!0),lastFocus=null,ignoreFocusChanges=!1,isLocked=!1)};_exports.untrapFocus=untrapFocus}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.untrapFocus=_exports.trapFocus=void 0,_selectors=(obj=_selectors)&&obj.__esModule?obj:{default:obj};const lockRegionStack=[],initialFocusElementStack=[],finalFocusElementStack=[];let lastFocus=null,ignoreFocusChanges=!1,isLocked=!1;const lockHandler=event=>{if(ignoreFocusChanges)return;let lockRegion=getCurrentLockRegion();for(;lockRegion&&!document.contains(lockRegion);)untrapFocus(),lockRegion=getCurrentLockRegion();lockRegion&&(event&&lockRegion.contains(event.target)?lastFocus=event.target:(focusFirstDescendant(),lastFocus==document.activeElement&&focusLastDescendant(),lastFocus=document.activeElement))},keyDownHandler=event=>{if("Tab"===event.key&&getCurrentLockRegion())if(event.shiftKey){const firstFocusable=getCurrentLockRegion().querySelector(_selectors.default.elements.focusable);document.activeElement===firstFocusable&&(lockHandler(),event.preventDefault())}else{const allFocusable=document.querySelectorAll(_selectors.default.elements.focusable);document.activeElement===allFocusable[allFocusable.length-1]&&(lockHandler(),event.preventDefault())}},focusFirstDescendant=()=>{const lockRegion=getCurrentLockRegion(),focusableElements=Array.from(lockRegion.querySelectorAll(_selectors.default.elements.focusable));return focusableElements.unshift(lockRegion),focusableElements.some((focusableElement=>attemptFocus(focusableElement)))},focusLastDescendant=()=>{const lockRegion=getCurrentLockRegion(),focusableElements=Array.from(lockRegion.querySelectorAll(_selectors.default.elements.focusable)).reverse();return focusableElements.push(lockRegion),focusableElements.some((focusableElement=>attemptFocus(focusableElement)))},attemptFocus=focusTarget=>{if(!(focusTarget=>{if(focusTarget.tabIndex>0||0===focusTarget.tabIndex&&null!==focusTarget.getAttribute("tabIndex"))return!0;if(focusTarget.disabled)return!1;switch(focusTarget.nodeName){case"A":return!!focusTarget.href&&"ignore"!=focusTarget.rel;case"INPUT":return"hidden"!=focusTarget.type&&"file"!=focusTarget.type;case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}})(focusTarget))return!1;ignoreFocusChanges=!0;try{focusTarget.focus()}catch(e){}return ignoreFocusChanges=!1,document.activeElement===focusTarget},getCurrentLockRegion=()=>lockRegionStack[lockRegionStack.length-1];_exports.trapFocus=newLockRegion=>{if((newLockRegion=>{if(newLockRegion===getCurrentLockRegion())return;lockRegionStack.push(newLockRegion);const currentLockRegion=getCurrentLockRegion(),element=document.createElement("div");element.tabIndex=0,element.style.position="fixed",element.style.top=0,element.style.left=0;const initialNode=element.cloneNode();currentLockRegion.parentNode.insertBefore(initialNode,currentLockRegion),initialFocusElementStack.push(initialNode);const finalNode=element.cloneNode();currentLockRegion.parentNode.insertBefore(finalNode,currentLockRegion.nextSibling),finalFocusElementStack.push(finalNode)})(newLockRegion),isLocked||(document.addEventListener("focus",lockHandler,!0),document.addEventListener("keydown",keyDownHandler,!0)),!focusFirstDescendant()){const currentLockRegion=getCurrentLockRegion(),originalRegionTabIndex=currentLockRegion.tabIndex;currentLockRegion.tabIndex=0,attemptFocus(currentLockRegion),currentLockRegion.tabIndex=originalRegionTabIndex}lastFocus=document.activeElement,isLocked=!0};const untrapFocus=()=>{(()=>{lockRegionStack.pop();const finalNode=finalFocusElementStack.pop();finalNode&&finalNode.remove();const initialNode=initialFocusElementStack.pop();initialNode&&initialNode.remove()})(),lockRegionStack.length||(document.removeEventListener("focus",lockHandler,!0),document.removeEventListener("keydown",keyDownHandler,!0),lastFocus=null,ignoreFocusChanges=!1,isLocked=!1)};_exports.untrapFocus=untrapFocus}));
//# sourceMappingURL=focuslock.min.js.map
File diff suppressed because one or more lines are too long
+47 -2
View File
@@ -46,8 +46,11 @@ let isLocked = false;
* This gives us a solution which supports focus locking of any kind, which loops in both directions, and which
* prevents the lock from escaping the modal entirely.
*
* If no event is supplied then this function can be used to focus the first element in the lock region, or the
* last element if the first element is already focused.
*
* @method
* @param {Event} event The event from the focus change
* @param {Event} [event] The event from the focus change
*/
const lockHandler = event => {
if (ignoreFocusChanges) {
@@ -71,7 +74,7 @@ const lockHandler = event => {
return;
}
if (lockRegion.contains(event.target)) {
if (event && lockRegion.contains(event.target)) {
lastFocus = event.target;
} else {
focusFirstDescendant();
@@ -82,6 +85,46 @@ const lockHandler = event => {
}
};
/**
* Catch event for any keydown during focus lock.
*
* This is used to detect situations when the user would be tabbing out to the browser UI. In that
* case, no 'focus' event is generated, so we need to trap it before it happens via the keydown
* event.
*
* @param {KeyboardEvent} event
*/
const keyDownHandler = event => {
// We only care about Tab keypresses and only if there is a current lock region.
if (event.key !== 'Tab' || !getCurrentLockRegion()) {
return;
}
if (!event.shiftKey) {
// Have they already focused the last focusable element in the document?
const allFocusable = document.querySelectorAll(Selectors.elements.focusable);
if (document.activeElement === allFocusable[allFocusable.length - 1]) {
// When the last thing is focused, focus would go to browser UI next, instead use
// lockHandler to put focus back on the first element in lock region.
lockHandler();
event.preventDefault();
}
} else {
// Have they already focused the first focusable element in the lock region?
const lockRegion = getCurrentLockRegion();
const firstFocusable = lockRegion.querySelector(Selectors.elements.focusable);
if (document.activeElement === firstFocusable) {
// When the first thing is focused, use lockHandler which will focus the last element
// in lock region. We do this here rather than using lockHandler to get the focus event
// because (a) there would be no focus event if the current element is the first in
// document, and (b) temporarily focusing outside the region could result in unexpected
// scrolling.
lockHandler();
event.preventDefault();
}
}
};
/**
* Focus the first descendant of the current lock region.
*
@@ -276,6 +319,7 @@ export const trapFocus = newLockRegion => {
if (!isLocked) {
// Add the focus handler.
document.addEventListener('focus', lockHandler, true);
document.addEventListener('keydown', keyDownHandler, true);
}
// Attempt to focus on the first item in the lock region.
@@ -312,6 +356,7 @@ export const untrapFocus = () => {
}
document.removeEventListener('focus', lockHandler, true);
document.removeEventListener('keydown', keyDownHandler, true);
lastFocus = null;
ignoreFocusChanges = false;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -45,6 +45,7 @@ export const displayFilepicker = (editor, filetype) => new Promise((resolve, rej
const options = {
...configuration,
formcallback: resolve,
previousActiveElement: document.activeElement,
};
M.core_filepicker.show(Y, options);
return;
@@ -17,6 +17,14 @@ Feature: Use the TinyMCE editor to upload an image
And I click on "Browse repositories" "button" in the "Insert image" "dialogue"
Then "File picker" "dialogue" should exist
Scenario: Focus returns to the correct location after closing a nested FilePicker
Given I log in as "admin"
And I open my profile in edit mode
When I click on the "Image" button for the "Description" TinyMCE editor
And I press "Browse repositories"
When I press the escape key
Then the focused element is "Browse repositories" "button"
@_file_upload @test_tiny
Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker and upload url image
Given I log in as "admin"
+35
View File
@@ -0,0 +1,35 @@
@core
Feature: Focus lock in modal popups
In order to navigate a modal popup with keyboard
As a user
The tab key should cycle through elements in the form and not go outside it
@javascript
Scenario: Tab cycles through elements in modal, using image popup in Tiny as an example
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "activities" exist:
| activity | name | course | idnumber |
| forum | Test forum | C1 | forum1 |
And I am on the "forum1" "Activity" page logged in as "admin"
And I follow "Add discussion topic"
And I click on "Image" "button"
# Repeated tabs just to get to the last element. This may need changing if controls are added
# or removed to the form.
And I press the tab key
And I press the tab key
And I press the tab key
And I press the tab key
And I press the tab key
And the focused element is "Browse repositories" "button"
# Tab past last element should go back to the first one, which is the modal itself, then the close button.
And I press the tab key
And I press the tab key
Then the focused element is "Close" "button" in the "Insert image" "dialogue"
And I press the shift tab key
And I press the shift tab key
And the focused element is "Browse repositories" "button"