MDL-84324 core: Fix Bootstrap dropdown key listeners

Bootstrap EventHandler is managing the keydown event at document
level in the capture phase. That is conflicting with dropdown elements
with form autocomplete inputs inside, because when suggestions panel is
open, pressing the "Esc" key will close the whole dropdown.
This commit fixes that behaviour by realocating the Bootsrap dropdown
event listener to the body instead of the document.
This commit is contained in:
Mikel Martín
2025-02-25 13:31:41 +01:00
parent de8e8806da
commit ac146ac14e
9 changed files with 39 additions and 25 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+13 -1
View File
@@ -31,6 +31,7 @@ define([
'core/aria',
'core_form/changechecker',
'core/popper2',
'theme_boost/bootstrap/dom/event-handler',
], function(
$,
log,
@@ -40,7 +41,8 @@ define([
LoadingIcon,
Aria,
FormChangeChecker,
Popper
Popper,
EventHandler,
) {
// Private functions and variables.
/** @var {Object} KEYS - List of keycode constants. */
@@ -1083,6 +1085,16 @@ define([
});
}
}
// Add a Bootstrap keydown handler to close the suggestions list preventing the whole Dropdown close.
EventHandler.on(document, 'keydown.bs.dropdown.data-api', '.dropdown-menu', (event) => {
const pendingPromise = addPendingJSPromise('addNavigation-' + state.inputId + '-' + event.key);
if (event.key === "Escape" && inputElement.attr('aria-expanded') === "true") {
event.stopImmediatePropagation();
return pendingPromise.resolve(closeSuggestions(state));
}
return pendingPromise.resolve();
});
};
/**
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,4 +1,4 @@
define("theme_boost/loader",["exports","./aria","./index","core/pending","./bootstrap/util/sanitizer","./pending"],(function(_exports,Aria,Bootstrap,_pending,_sanitizer,_pending2){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
define("theme_boost/loader",["exports","./aria","./index","core/pending","./bootstrap/util/sanitizer","./pending","./bootstrap/dom/event-handler"],(function(_exports,Aria,Bootstrap,_pending,_sanitizer,_pending2,_eventHandler){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* Template renderer for Moodle. Load and render Moodle templates with Mustache.
*
@@ -6,6 +6,6 @@ define("theme_boost/loader",["exports","./aria","./index","core/pending","./boot
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Bootstrap=void 0,Aria=_interopRequireWildcard(Aria),Bootstrap=_interopRequireWildcard(Bootstrap),_exports.Bootstrap=Bootstrap,_pending=_interopRequireDefault(_pending),_pending2=_interopRequireDefault(_pending2);const pendingPromise=new _pending.default("theme_boost/loader:init");(0,_pending2.default)(),Aria.init(),(()=>{[...document.querySelectorAll('a[data-bs-toggle="tab"]')].map((tabTriggerEl=>tabTriggerEl.addEventListener("shown.bs.tab",(e=>{var hash=e.target.getAttribute("href");history.replaceState?history.replaceState(null,null,hash):location.hash=hash}))));const hash=window.location.hash;if(hash){const tab=document.querySelector('[role="tablist"] [href="'+hash+'"]');tab&&tab.click()}})(),(()=>{const popoverTriggerList=document.querySelectorAll('[data-bs-toggle="popover"]'),popoverConfig={container:"body",trigger:"focus",allowList:Object.assign(_sanitizer.DefaultAllowlist,{table:[],thead:[],tbody:[],tr:[],th:[],td:[]})};[...popoverTriggerList].map((popoverTriggerEl=>new Bootstrap.Popover(popoverTriggerEl,popoverConfig))),document.addEventListener("core/modal:bodyRendered",(e=>{[...e.target.querySelectorAll('[data-bs-toggle="popover"]')].map((popoverTriggerEl=>new Bootstrap.Popover(popoverTriggerEl,popoverConfig)))})),document.addEventListener("keydown",(e=>{const popoverTrigger=e.target.closest('[data-bs-toggle="popover"]');"Escape"===e.key&&popoverTrigger&&Bootstrap.Popover.getInstance(popoverTrigger).hide(),"Enter"===e.key&&popoverTrigger&&Bootstrap.Popover.getInstance(popoverTrigger).show()})),document.addEventListener("click",(e=>{const popoverTrigger=e.target.closest('[data-bs-toggle="popover"]');if(!popoverTrigger)return;const popover=Bootstrap.Popover.getInstance(popoverTrigger);popover._isShown()||popover.show()}))})(),[...document.querySelectorAll('[data-bs-toggle="tooltip"]')].map((tooltipTriggerEl=>new Bootstrap.Tooltip(tooltipTriggerEl))),pendingPromise.resolve()}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.Bootstrap=void 0,Aria=_interopRequireWildcard(Aria),Bootstrap=_interopRequireWildcard(Bootstrap),_exports.Bootstrap=Bootstrap,_pending=_interopRequireDefault(_pending),_pending2=_interopRequireDefault(_pending2),_eventHandler=_interopRequireDefault(_eventHandler);const pendingPromise=new _pending.default("theme_boost/loader:init");(0,_pending2.default)(),Aria.init(),(()=>{[...document.querySelectorAll('a[data-bs-toggle="tab"]')].map((tabTriggerEl=>tabTriggerEl.addEventListener("shown.bs.tab",(e=>{var hash=e.target.getAttribute("href");history.replaceState?history.replaceState(null,null,hash):location.hash=hash}))));const hash=window.location.hash;if(hash){const tab=document.querySelector('[role="tablist"] [href="'+hash+'"]');tab&&tab.click()}})(),(()=>{const popoverTriggerList=document.querySelectorAll('[data-bs-toggle="popover"]'),popoverConfig={container:"body",trigger:"focus",allowList:Object.assign(_sanitizer.DefaultAllowlist,{table:[],thead:[],tbody:[],tr:[],th:[],td:[]})};[...popoverTriggerList].map((popoverTriggerEl=>new Bootstrap.Popover(popoverTriggerEl,popoverConfig))),document.addEventListener("core/modal:bodyRendered",(e=>{[...e.target.querySelectorAll('[data-bs-toggle="popover"]')].map((popoverTriggerEl=>new Bootstrap.Popover(popoverTriggerEl,popoverConfig)))})),document.addEventListener("keydown",(e=>{const popoverTrigger=e.target.closest('[data-bs-toggle="popover"]');"Escape"===e.key&&popoverTrigger&&Bootstrap.Popover.getInstance(popoverTrigger).hide(),"Enter"===e.key&&popoverTrigger&&Bootstrap.Popover.getInstance(popoverTrigger).show()})),document.addEventListener("click",(e=>{const popoverTrigger=e.target.closest('[data-bs-toggle="popover"]');if(!popoverTrigger)return;const popover=Bootstrap.Popover.getInstance(popoverTrigger);popover._isShown()||popover.show()}))})(),[...document.querySelectorAll('[data-bs-toggle="tooltip"]')].map((tooltipTriggerEl=>new Bootstrap.Tooltip(tooltipTriggerEl))),_eventHandler.default.off(document,"keydown.bs.dropdown.data-api",".dropdown-menu",Bootstrap.Dropdown.dataApiKeydownHandler),_eventHandler.default.on(document.body,"keydown.bs.dropdown.data-api",".dropdown-menu",Bootstrap.Dropdown.dataApiKeydownHandler),pendingPromise.resolve()}));
//# sourceMappingURL=loader.min.js.map
File diff suppressed because one or more lines are too long
-17
View File
@@ -227,23 +227,6 @@ const dropdownFix = () => {
});
}
});
// Prevent closing the dropdown when the autocomplete suggestions are visible.
document.addEventListener('hide.bs.dropdown', (e) => {
const dropdown = e.target.closest('.dropdown');
if (!dropdown) {
return;
}
const autocompleteSuggestions = document.querySelector('.form-autocomplete-suggestions:not([aria-hidden="true"])');
if (autocompleteSuggestions) {
// Dropdown should not be hidden while the autocomplete suggestions are visible inside.
e.preventDefault();
// Return the focus to the autocomplete input.
const autocompleteElement = autocompleteSuggestions.closest('[data-fieldtype="autocomplete"]');
const autocompleteInput = autocompleteElement.querySelector('.form-autocomplete-input input');
setTimeout(()=> autocompleteInput.focus(), 1000);
}
});
};
/**
+19
View File
@@ -27,6 +27,7 @@ import * as Bootstrap from './index';
import Pending from 'core/pending';
import {DefaultAllowlist} from './bootstrap/util/sanitizer';
import setupBootstrapPendingChecks from './pending';
import EventHandler from './bootstrap/dom/event-handler';
/**
* Rember the last visited tabs.
@@ -100,6 +101,21 @@ const enableTooltips = () => {
[...tooltipTriggerList].map(tooltipTriggerEl => new Bootstrap.Tooltip(tooltipTriggerEl));
};
/**
* Realocate Bootstrap events to the body element.
*
* Bootstrap 5 has a unique event handling mechanism that attaches all event handlers at the document level
* during the capture phase, rather than the usual bubbling phase. As a result, original Bootstrap events
* cannot be stopped or prevented, since the document is the first node executed in the capture phase.
* For certain advanced UI elements, such as form autocomplete, it is important to capture key-down events before
* Bootstrap's handlers to prevent unintended closures of elements. Therefore, we need to change the Bootstrap handler
* so that it operates one level lower, specifically at the body level.
*/
const realocateBootstrapEvents = () => {
EventHandler.off(document, 'keydown.bs.dropdown.data-api', '.dropdown-menu', Bootstrap.Dropdown.dataApiKeydownHandler);
EventHandler.on(document.body, 'keydown.bs.dropdown.data-api', '.dropdown-menu', Bootstrap.Dropdown.dataApiKeydownHandler);
};
const pendingPromise = new Pending('theme_boost/loader:init');
// Add pending promise event listeners to relevant Bootstrap custom events.
@@ -117,6 +133,9 @@ enablePopovers();
// Enable all tooltips.
enableTooltips();
// Realocate Bootstrap events to the body element.
realocateBootstrapEvents();
pendingPromise.resolve();
export {