MDL-81702 core_message: Add focustrap and backdrop

* Add focustrap and backdrop to message drawer.
This commit is contained in:
Laurent David
2025-07-07 07:09:16 +02:00
parent e41473d9ff
commit 067138aab4
8 changed files with 98 additions and 10 deletions
+3 -2
View File
@@ -36,8 +36,9 @@
class="{{$drawerclasses}}{{/drawerclasses}} drawer bg-white {{^show}}hidden{{/show}}"
{{^show}}aria-hidden="true"{{/show}}
data-region="right-hand-drawer"
role="region"
tabindex="0"
role="{{$role}}region{{/role}}"
tabindex="{{$tabindex}}0{{/tabindex}}"
{{$drawerattributes}}{{/drawerattributes}}
>
{{$drawercontent}}{{/drawercontent}}
</div>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,6 +5,6 @@
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("core_message/message_popover",["jquery","core/custom_interaction_events","core/pubsub","core_message/message_drawer_events"],(function($,CustomEvents,PubSub,MessageDrawerEvents){var SELECTORS_COUNT_CONTAINER='[data-region="count-container"]',handleDecrementConversationCount=function(button){return function(){var countContainer=button.find(SELECTORS_COUNT_CONTAINER),count=parseInt(countContainer.text(),10);isNaN(count)||!count||count<2?countContainer.addClass("hidden"):(count-=1,countContainer.text(count))}},registerEventListeners=function(button){CustomEvents.define(button,[CustomEvents.events.activate]),button.on(CustomEvents.events.activate,(function(e,data){var buttonid;buttonid=button.attr("id"),PubSub.publish(MessageDrawerEvents.TOGGLE_VISIBILITY,buttonid),button.focus(),data.originalEvent.preventDefault()})),PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ,handleDecrementConversationCount(button)),PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED,handleDecrementConversationCount(button)),PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED,handleDecrementConversationCount(button))};return{init:function(button){button=$(button),registerEventListeners(button)}}}));
define("core_message/message_popover",["jquery","core/custom_interaction_events","core/pubsub","core_message/message_drawer_events"],(function($,CustomEvents,PubSub,MessageDrawerEvents){var SELECTORS_COUNT_CONTAINER='[data-region="count-container"]',handleDecrementConversationCount=function(button){return function(){var countContainer=button.find(SELECTORS_COUNT_CONTAINER),count=parseInt(countContainer.text(),10);isNaN(count)||!count||count<2?countContainer.addClass("hidden"):(count-=1,countContainer.text(count))}},registerEventListeners=function(button){CustomEvents.define(button,[CustomEvents.events.activate]),button.on(CustomEvents.events.activate,(function(e,data){var buttonid;buttonid=button.attr("id"),PubSub.publish(MessageDrawerEvents.TOGGLE_VISIBILITY,buttonid),data.originalEvent.preventDefault()})),PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ,handleDecrementConversationCount(button)),PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED,handleDecrementConversationCount(button)),PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED,handleDecrementConversationCount(button))};return{init:function(button){button=$(button),registerEventListeners(button)}}}));
//# sourceMappingURL=message_popover.min.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"message_popover.min.js","sources":["../src/message_popover.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Controls the message popover in the nav bar.\n *\n * @module core_message/message_popover\n * @copyright 2018 Ryan Wyllie <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n 'jquery',\n 'core/custom_interaction_events',\n 'core/pubsub',\n 'core_message/message_drawer_events'\n],\nfunction(\n $,\n CustomEvents,\n PubSub,\n MessageDrawerEvents\n) {\n var SELECTORS = {\n COUNT_CONTAINER: '[data-region=\"count-container\"]'\n };\n\n /**\n * Toggle the message drawer visibility.\n *\n * @param {String} buttonid The button id for the popover.\n */\n var toggleMessageDrawerVisibility = function(buttonid) {\n PubSub.publish(MessageDrawerEvents.TOGGLE_VISIBILITY, buttonid);\n };\n\n /**\n * Decrement the unread conversation count in the nav bar if a conversation\n * is read. When there are no unread conversations then hide the counter.\n *\n * @param {Object} button The button element for the popover.\n * @return {Function}\n */\n var handleDecrementConversationCount = function(button) {\n return function() {\n var countContainer = button.find(SELECTORS.COUNT_CONTAINER);\n var count = parseInt(countContainer.text(), 10);\n\n if (isNaN(count)) {\n countContainer.addClass('hidden');\n } else if (!count || count < 2) {\n countContainer.addClass('hidden');\n } else {\n count = count - 1;\n countContainer.text(count);\n }\n };\n };\n\n /**\n * Add events listeners for when the popover icon is clicked and when conversations\n * are read.\n *\n * @param {Object} button The button element for the popover.\n */\n var registerEventListeners = function(button) {\n CustomEvents.define(button, [CustomEvents.events.activate]);\n\n button.on(CustomEvents.events.activate, function(e, data) {\n toggleMessageDrawerVisibility(button.attr('id'));\n button.focus();\n data.originalEvent.preventDefault();\n });\n\n PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, handleDecrementConversationCount(button));\n PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, handleDecrementConversationCount(button));\n PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, handleDecrementConversationCount(button));\n };\n\n /**\n * Initialise the message popover.\n *\n * @param {Object} button The button element for the popover.\n */\n var init = function(button) {\n button = $(button);\n registerEventListeners(button);\n };\n\n return {\n init: init,\n };\n});\n"],"names":["define","$","CustomEvents","PubSub","MessageDrawerEvents","SELECTORS","handleDecrementConversationCount","button","countContainer","find","count","parseInt","text","isNaN","addClass","registerEventListeners","events","activate","on","e","data","buttonid","attr","publish","TOGGLE_VISIBILITY","focus","originalEvent","preventDefault","subscribe","CONVERSATION_READ","CONTACT_REQUEST_ACCEPTED","CONTACT_REQUEST_DECLINED","init"],"mappings":";;;;;;;AAsBAA,sCACA,CACI,SACA,iCACA,cACA,uCAEJ,SACIC,EACAC,aACAC,OACAC,yBAEIC,0BACiB,kCAmBjBC,iCAAmC,SAASC,eACrC,eACCC,eAAiBD,OAAOE,KAAKJ,2BAC7BK,MAAQC,SAASH,eAAeI,OAAQ,IAExCC,MAAMH,SAEEA,OAASA,MAAQ,EADzBF,eAAeM,SAAS,WAIxBJ,OAAgB,EAChBF,eAAeI,KAAKF,UAW5BK,uBAAyB,SAASR,QAClCL,aAAaF,OAAOO,OAAQ,CAACL,aAAac,OAAOC,WAEjDV,OAAOW,GAAGhB,aAAac,OAAOC,UAAU,SAASE,EAAGC,MApCpB,IAASC,SAAAA,SAqCPd,OAAOe,KAAK,MApC9CnB,OAAOoB,QAAQnB,oBAAoBoB,kBAAmBH,UAqClDd,OAAOkB,QACPL,KAAKM,cAAcC,oBAGvBxB,OAAOyB,UAAUxB,oBAAoByB,kBAAmBvB,iCAAiCC,SACzFJ,OAAOyB,UAAUxB,oBAAoB0B,yBAA0BxB,iCAAiCC,SAChGJ,OAAOyB,UAAUxB,oBAAoB2B,yBAA0BzB,iCAAiCC,gBAa7F,CACHyB,KANO,SAASzB,QAChBA,OAASN,EAAEM,QACXQ,uBAAuBR"}
{"version":3,"file":"message_popover.min.js","sources":["../src/message_popover.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Controls the message popover in the nav bar.\n *\n * @module core_message/message_popover\n * @copyright 2018 Ryan Wyllie <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n 'jquery',\n 'core/custom_interaction_events',\n 'core/pubsub',\n 'core_message/message_drawer_events'\n],\nfunction(\n $,\n CustomEvents,\n PubSub,\n MessageDrawerEvents\n) {\n var SELECTORS = {\n COUNT_CONTAINER: '[data-region=\"count-container\"]'\n };\n\n /**\n * Toggle the message drawer visibility.\n *\n * @param {String} buttonid The button id for the popover.\n */\n var toggleMessageDrawerVisibility = function(buttonid) {\n PubSub.publish(MessageDrawerEvents.TOGGLE_VISIBILITY, buttonid);\n };\n\n /**\n * Decrement the unread conversation count in the nav bar if a conversation\n * is read. When there are no unread conversations then hide the counter.\n *\n * @param {Object} button The button element for the popover.\n * @return {Function}\n */\n var handleDecrementConversationCount = function(button) {\n return function() {\n var countContainer = button.find(SELECTORS.COUNT_CONTAINER);\n var count = parseInt(countContainer.text(), 10);\n\n if (isNaN(count)) {\n countContainer.addClass('hidden');\n } else if (!count || count < 2) {\n countContainer.addClass('hidden');\n } else {\n count = count - 1;\n countContainer.text(count);\n }\n };\n };\n\n /**\n * Add events listeners for when the popover icon is clicked and when conversations\n * are read.\n *\n * @param {Object} button The button element for the popover.\n */\n var registerEventListeners = function(button) {\n CustomEvents.define(button, [CustomEvents.events.activate]);\n\n button.on(CustomEvents.events.activate, function(e, data) {\n toggleMessageDrawerVisibility(button.attr('id'));\n data.originalEvent.preventDefault();\n });\n\n PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, handleDecrementConversationCount(button));\n PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, handleDecrementConversationCount(button));\n PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, handleDecrementConversationCount(button));\n };\n\n /**\n * Initialise the message popover.\n *\n * @param {Object} button The button element for the popover.\n */\n var init = function(button) {\n button = $(button);\n registerEventListeners(button);\n };\n\n return {\n init: init,\n };\n});\n"],"names":["define","$","CustomEvents","PubSub","MessageDrawerEvents","SELECTORS","handleDecrementConversationCount","button","countContainer","find","count","parseInt","text","isNaN","addClass","registerEventListeners","events","activate","on","e","data","buttonid","attr","publish","TOGGLE_VISIBILITY","originalEvent","preventDefault","subscribe","CONVERSATION_READ","CONTACT_REQUEST_ACCEPTED","CONTACT_REQUEST_DECLINED","init"],"mappings":";;;;;;;AAsBAA,sCACA,CACI,SACA,iCACA,cACA,uCAEJ,SACIC,EACAC,aACAC,OACAC,yBAEIC,0BACiB,kCAmBjBC,iCAAmC,SAASC,eACrC,eACCC,eAAiBD,OAAOE,KAAKJ,2BAC7BK,MAAQC,SAASH,eAAeI,OAAQ,IAExCC,MAAMH,SAEEA,OAASA,MAAQ,EADzBF,eAAeM,SAAS,WAIxBJ,OAAgB,EAChBF,eAAeI,KAAKF,UAW5BK,uBAAyB,SAASR,QAClCL,aAAaF,OAAOO,OAAQ,CAACL,aAAac,OAAOC,WAEjDV,OAAOW,GAAGhB,aAAac,OAAOC,UAAU,SAASE,EAAGC,MApCpB,IAASC,SAAAA,SAqCPd,OAAOe,KAAK,MApC9CnB,OAAOoB,QAAQnB,oBAAoBoB,kBAAmBH,UAqClDD,KAAKK,cAAcC,oBAGvBvB,OAAOwB,UAAUvB,oBAAoBwB,kBAAmBtB,iCAAiCC,SACzFJ,OAAOwB,UAAUvB,oBAAoByB,yBAA0BvB,iCAAiCC,SAChGJ,OAAOwB,UAAUvB,oBAAoB0B,yBAA0BxB,iCAAiCC,gBAa7F,CACHwB,KANO,SAASxB,QAChBA,OAASN,EAAEM,QACXQ,uBAAuBR"}
+85 -3
View File
@@ -38,6 +38,13 @@ define(
'core_message/message_drawer_helper',
'core/pending',
'core/drawer',
'core/toast',
'core/str',
'core/config',
'core/ajax',
'core/local/aria/focuslock',
'core/modal_backdrop',
'core/templates',
],
function(
$,
@@ -55,7 +62,14 @@ function(
Events,
Helper,
Pending,
Drawer
Drawer,
Toast,
Str,
Config,
Ajax,
FocusLock,
ModalBackdrop,
Templates,
) {
var SELECTORS = {
@@ -129,6 +143,69 @@ function(
});
};
let backdropPromise = null;
/**
* Set the focus on the drawer.
*
* This method also creates or destroy any necessary backdrop zone and focus trap.
*
* @param {Object} root The message drawer container.
* @param {Boolean} hasFocus Whether the drawer has focus or not.
*/
var setFocus = function(root, hasFocus) {
var drawerRoot = Drawer.getDrawerRoot(root);
if (!drawerRoot.length) {
return;
}
if (!backdropPromise) {
backdropPromise = Templates.render('core/modal_backdrop', {})
.then(html => new ModalBackdrop(html));
}
const backdropWithAdjustments = backdropPromise.then(modalBackdrop => {
const messageDrawerZIndex = window.getComputedStyle(drawerRoot[0]).zIndex;
if (messageDrawerZIndex) {
modalBackdrop.setZIndex(messageDrawerZIndex - 1);
}
modalBackdrop.getAttachmentPoint().get(0).addEventListener('click', e => {
PubSub.publish(Events.HIDE, {});
e.preventDefault();
});
return modalBackdrop;
});
if (hasFocus) {
FocusLock.trapFocus(root[0]);
// eslint-disable-next-line promise/catch-or-return
backdropWithAdjustments.then(modalBackdrop => {
if (modalBackdrop) {
modalBackdrop.show();
const pageWrapper = document.getElementById('page');
pageWrapper.style.overflow = 'hidden';
// Set the focus on the close button so when we press enter, it closes the drawer as it did before
var closeButton = root.find(SELECTORS.CLOSE_BUTTON);
if (closeButton.length) {
closeButton.focus();
}
}
return modalBackdrop;
});
} else {
// eslint-disable-next-line promise/catch-or-return
backdropWithAdjustments.then(modalBackdrop => {
if (modalBackdrop) {
FocusLock.untrapFocus();
var button = $(SELECTORS.DRAWER).attr('data-origin');
if (button) {
$('#' + button).focus();
}
modalBackdrop.hide();
const pageWrapper = document.getElementById('page');
pageWrapper.style.overflow = 'visible';
}
return modalBackdrop;
});
}
};
/**
* Show the message drawer.
*
@@ -143,6 +220,7 @@ function(
var drawerRoot = Drawer.getDrawerRoot(root);
if (drawerRoot.length) {
setFocus(root, true);
Drawer.show(drawerRoot);
}
};
@@ -155,6 +233,7 @@ function(
var hide = function(root) {
var drawerRoot = Drawer.getDrawerRoot(root);
if (drawerRoot.length) {
setFocus(root, false);
Drawer.hide(drawerRoot);
}
};
@@ -190,7 +269,7 @@ function(
* @param {bool} alwaysVisible Is this messaging app always shown?
*/
var registerEventListeners = function(namespace, root, alwaysVisible) {
CustomEvents.define(root, [CustomEvents.events.activate]);
CustomEvents.define(root, [CustomEvents.events.activate, CustomEvents.events.escape]);
var paramRegex = /^data-route-param-?(\d*)$/;
root.on(CustomEvents.events.activate, SELECTORS.ROUTES, function(e, data) {
@@ -293,6 +372,9 @@ function(
$(SELECTORS.JUMPTO).attr('tabindex', 0);
}
});
root.on(CustomEvents.events.escape, function() {
PubSub.publish(Events.HIDE, {});
});
}
PubSub.subscribe(Events.SHOW_CONVERSATION, function(args) {
@@ -304,7 +386,7 @@ function(
var closebutton = root.find(SELECTORS.CLOSE_BUTTON);
closebutton.on(CustomEvents.events.activate, function(e, data) {
data.originalEvent.preventDefault();
setFocus(root, false);
var button = $(SELECTORS.DRAWER).attr('data-origin');
if (button) {
$('#' + button).focus();
-1
View File
@@ -80,7 +80,6 @@ function(
button.on(CustomEvents.events.activate, function(e, data) {
toggleMessageDrawerVisibility(button.attr('id'));
button.focus();
data.originalEvent.preventDefault();
});
@@ -34,6 +34,12 @@
}}
{{< core/drawer}}
{{$tabindex}}-1{{/tabindex}}
{{$role}}dialog{{/role}}
{{$drawerattributes}}
aria-modal="true"
aria-labelledby="message-drawer-header-{{uniqid}}"
{{/drawerattributes}}
{{$drawercontent}}
<div id="message-drawer-{{uniqid}}" class="message-app" data-region="message-drawer" role="region">
<h2 class="sr-only">{{#str}} messagingcategory,admin {{/str}}</h2>