Merge branch 'MDL-87993-405' of https://github.com/junpataleta/moodle into MOODLE_405_STABLE

This commit is contained in:
Huong Nguyen
2026-03-03 08:36:30 +07:00
8 changed files with 29 additions and 22 deletions
@@ -0,0 +1,8 @@
issueNumber: MDL-87993
notes:
core:
- message: >-
The `core/toast` JS module now accepts a `visuallyHidden` configuration
parameter to render visually hidden toast messages for screen reader
users.
type: improved
+1 -1
View File
@@ -1,3 +1,3 @@
define("core/local/reactive/srlogger",["exports","core/local/reactive/logger"],(function(_exports,_logger){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_logger=(obj=_logger)&&obj.__esModule?obj:{default:obj};class SRLogger extends _logger.default{add(entry){if(entry.feedbackMessage){let loggerFeedback=document.getElementById(SRLogger.liveRegionId);loggerFeedback||(loggerFeedback=document.createElement("div"),loggerFeedback.id=SRLogger.liveRegionId,loggerFeedback.classList.add("sr-only"),loggerFeedback.setAttribute("aria-live","polite"),document.body.append(loggerFeedback)),loggerFeedback.innerHTML=entry.feedbackMessage,setTimeout((()=>{loggerFeedback.innerHTML=""}),4e3)}}}return _exports.default=SRLogger,function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(SRLogger,"liveRegionId","sr-logger-feedback-container"),_exports.default}));
define("core/local/reactive/srlogger",["exports","core/toast","core/local/reactive/logger"],(function(_exports,_toast,_logger){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_logger=(obj=_logger)&&obj.__esModule?obj:{default:obj};class SRLogger extends _logger.default{add(entry){entry.feedbackMessage&&(0,_toast.add)(entry.feedbackMessage,{visuallyHidden:!0})}}return _exports.default=SRLogger,function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(SRLogger,"liveRegionId","sr-logger-feedback-container"),_exports.default}));
//# sourceMappingURL=srlogger.min.js.map
@@ -1 +1 @@
{"version":3,"file":"srlogger.min.js","sources":["../../../src/local/reactive/srlogger.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 * Screen reader-only (sr-only) reactive mutations logger class.\n *\n * This logger can be used by the StateManager to log mutation feedbacks and actions.\n * The feedback messages logged by this logger will be rendered in a sr-only, ARIA live region.\n *\n * @module core/local/reactive/srlogger\n * @class SRLogger\n * @copyright 2023 Jun Pataleta <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Logger from 'core/local/reactive/logger';\n\n/**\n * Logger entry structure.\n *\n * @typedef {object} LoggerEntry\n * @property {string} feedbackMessage Feedback message.\n */\n\n/**\n * Screen reader-only (sr-only) reactive mutations logger class.\n *\n * @class SRLogger\n */\nexport default class SRLogger extends Logger {\n /**\n * The element ID of the ARIA live region where the logger feedback will be rendered.\n *\n * @type {string}\n */\n static liveRegionId = 'sr-logger-feedback-container';\n\n /**\n * Add a log entry.\n * @param {LoggerEntry} entry Log entry.\n */\n add(entry) {\n if (entry.feedbackMessage) {\n // Fetch or create an ARIA live region that will serve as the container for the logger feedback.\n let loggerFeedback = document.getElementById(SRLogger.liveRegionId);\n if (!loggerFeedback) {\n loggerFeedback = document.createElement('div');\n loggerFeedback.id = SRLogger.liveRegionId;\n loggerFeedback.classList.add('sr-only');\n loggerFeedback.setAttribute('aria-live', 'polite');\n document.body.append(loggerFeedback);\n }\n // Set the ARIA live region's contents with the feedback.\n loggerFeedback.innerHTML = entry.feedbackMessage;\n\n // Clear the feedback message after 4 seconds to avoid the contents from being read out in case the user navigates\n // to this region. This is similar to the default timeout of toast messages before disappearing from view.\n setTimeout(() => {\n loggerFeedback.innerHTML = '';\n }, 4000);\n }\n }\n}\n"],"names":["SRLogger","Logger","add","entry","feedbackMessage","loggerFeedback","document","getElementById","liveRegionId","createElement","id","classList","setAttribute","body","append","innerHTML","setTimeout"],"mappings":"iQAyCqBA,iBAAiBC,gBAYlCC,IAAIC,UACIA,MAAMC,gBAAiB,KAEnBC,eAAiBC,SAASC,eAAeP,SAASQ,cACjDH,iBACDA,eAAiBC,SAASG,cAAc,OACxCJ,eAAeK,GAAKV,SAASQ,aAC7BH,eAAeM,UAAUT,IAAI,WAC7BG,eAAeO,aAAa,YAAa,UACzCN,SAASO,KAAKC,OAAOT,iBAGzBA,eAAeU,UAAYZ,MAAMC,gBAIjCY,YAAW,KACPX,eAAeU,UAAY,KAC5B,kLA9BMf,wBAMK"}
{"version":3,"file":"srlogger.min.js","sources":["../../../src/local/reactive/srlogger.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 * Screen reader-only (sr-only) reactive mutations logger class.\n *\n * This logger can be used by the StateManager to log mutation feedbacks and actions.\n * The feedback messages logged by this logger will be rendered in a sr-only, ARIA live region.\n *\n * @module core/local/reactive/srlogger\n * @class SRLogger\n * @copyright 2023 Jun Pataleta <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {add as addToast} from 'core/toast';\nimport Logger from 'core/local/reactive/logger';\n\n/**\n * Logger entry structure.\n *\n * @typedef {object} LoggerEntry\n * @property {string} feedbackMessage Feedback message.\n */\n\n/**\n * Screen reader-only (sr-only) reactive mutations logger class.\n *\n * @class SRLogger\n */\nexport default class SRLogger extends Logger {\n /**\n * The element ID of the ARIA live region where the logger feedback will be rendered.\n *\n * @type {string}\n */\n static liveRegionId = 'sr-logger-feedback-container';\n\n /**\n * Add a log entry.\n * @param {LoggerEntry} entry Log entry.\n */\n add(entry) {\n if (entry.feedbackMessage) {\n addToast(entry.feedbackMessage, {visuallyHidden: true});\n }\n }\n}\n"],"names":["SRLogger","Logger","add","entry","feedbackMessage","visuallyHidden"],"mappings":"qRA0CqBA,iBAAiBC,gBAYlCC,IAAIC,OACIA,MAAMC,gCACGD,MAAMC,gBAAiB,CAACC,gBAAgB,gLAdxCL,wBAMK"}
+1 -1
View File
@@ -5,6 +5,6 @@ define("core/toast",["exports","core/templates","core/notification","core/pendin
* @module core/toast
* @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.removeToastRegion=_exports.addToastRegion=_exports.add=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const addToastRegion=async parent=>{const pendingPromise=new _pending.default("addToastRegion");try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/toast/wrapper",{});_templates.default.prependNodeContents(parent,html,js)}catch(e){_notification.default.exception(e)}pendingPromise.resolve()};_exports.addToastRegion=addToastRegion;_exports.add=async(message,configuration)=>{const pendingPromise=new _pending.default("addToastRegion");configuration={type:"info",closeButton:!1,autohide:!0,delay:4e3,...configuration};try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/toast/message",{message:await message,...configuration}),targetNode=await getTargetNode();_templates.default.prependNodeContents(targetNode,html,js)}catch(e){_notification.default.exception(e)}pendingPromise.resolve()};const getTargetNode=async()=>{const regions=document.querySelectorAll(".toast-wrapper");return regions.length?regions[regions.length-1]:(await addToastRegion(document.body),getTargetNode())};_exports.removeToastRegion=async function(parent){let newParent=arguments.length>1&&void 0!==arguments[1]?arguments[1]:document;const pendingPromise=new _pending.default("core/toast:removeToastRegion"),getRegionFromParent=thisParent=>thisParent.querySelector(".toast-wrapper"),regionToRemove=getRegionFromParent(parent);if(regionToRemove){const targetRegion=getRegionFromParent(newParent);regionToRemove.children.forEach((node=>{targetRegion.insertBefore(node,targetRegion.firstChild)})),regionToRemove.remove()}pendingPromise.resolve()}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.removeToastRegion=_exports.addToastRegion=_exports.add=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);const addToastRegion=async parent=>{const pendingPromise=new _pending.default("addToastRegion");try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/toast/wrapper",{});_templates.default.prependNodeContents(parent,html,js)}catch(e){_notification.default.exception(e)}pendingPromise.resolve()};_exports.addToastRegion=addToastRegion;_exports.add=async(message,configuration)=>{const pendingPromise=new _pending.default("addToastRegion");(configuration={type:"info",closeButton:!1,autohide:!0,delay:4e3,visuallyHidden:!1,...configuration}).visuallyHidden&&(configuration.autohide=!0,configuration.closeButton=!1);try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/toast/message",{message:await message,...configuration}),targetNode=await getTargetNode();_templates.default.prependNodeContents(targetNode,html,js)}catch(e){_notification.default.exception(e)}pendingPromise.resolve()};const getTargetNode=async()=>{const regions=document.querySelectorAll(".toast-wrapper");return regions.length?regions[regions.length-1]:(await addToastRegion(document.body),getTargetNode())};_exports.removeToastRegion=async function(parent){let newParent=arguments.length>1&&void 0!==arguments[1]?arguments[1]:document;const pendingPromise=new _pending.default("core/toast:removeToastRegion"),getRegionFromParent=thisParent=>thisParent.querySelector(".toast-wrapper"),regionToRemove=getRegionFromParent(parent);if(regionToRemove){const targetRegion=getRegionFromParent(newParent);regionToRemove.children.forEach((node=>{targetRegion.insertBefore(node,targetRegion.firstChild)})),regionToRemove.remove()}pendingPromise.resolve()}}));
//# sourceMappingURL=toast.min.js.map
File diff suppressed because one or more lines are too long
+2 -17
View File
@@ -25,6 +25,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {add as addToast} from 'core/toast';
import Logger from 'core/local/reactive/logger';
/**
@@ -53,23 +54,7 @@ export default class SRLogger extends Logger {
*/
add(entry) {
if (entry.feedbackMessage) {
// Fetch or create an ARIA live region that will serve as the container for the logger feedback.
let loggerFeedback = document.getElementById(SRLogger.liveRegionId);
if (!loggerFeedback) {
loggerFeedback = document.createElement('div');
loggerFeedback.id = SRLogger.liveRegionId;
loggerFeedback.classList.add('sr-only');
loggerFeedback.setAttribute('aria-live', 'polite');
document.body.append(loggerFeedback);
}
// Set the ARIA live region's contents with the feedback.
loggerFeedback.innerHTML = entry.feedbackMessage;
// Clear the feedback message after 4 seconds to avoid the contents from being read out in case the user navigates
// to this region. This is similar to the default timeout of toast messages before disappearing from view.
setTimeout(() => {
loggerFeedback.innerHTML = '';
}, 4000);
addToast(entry.feedbackMessage, {visuallyHidden: true});
}
}
}
+14
View File
@@ -57,6 +57,8 @@ export const addToastRegion = async(parent) => {
* @param {Boolean} [configuration.autohide=true]
* @param {Boolean} [configuration.closeButton=false]
* @param {Number} [configuration.delay=4000]
* @param {Boolean} [configuration.visuallyHidden=false] Whether the toast should be visually hidden. This is intended for cases
* where we want to announce ARIA live updates for screen reader users only.
*
* @example
* import {add as addToast} from 'core/toast';
@@ -73,6 +75,10 @@ export const addToastRegion = async(parent) => {
* autohide: false,
* closeButton: true,
* });
*
* addToast('A string to be rendered visually hidden', {
* visuallyHidden: true,
* });
*/
export const add = async(message, configuration) => {
const pendingPromise = new Pending('addToastRegion');
@@ -81,15 +87,23 @@ export const add = async(message, configuration) => {
closeButton: false,
autohide: true,
delay: 4000,
visuallyHidden: false,
...configuration,
};
// Ensure that visually hidden toasts are always autohidden and do not have a close button that can receive keyboard focus.
if (configuration.visuallyHidden) {
configuration.autohide = true;
configuration.closeButton = false;
}
const templateName = `core/local/toast/message`;
try {
const {html, js} = await Templates.renderForPromise(templateName, {
message: await message,
...configuration
});
const targetNode = await getTargetNode();
Templates.prependNodeContents(targetNode, html, js);
} catch (e) {
+1 -1
View File
@@ -36,7 +36,7 @@
"closebutton": true
}
}}
<div id="toast-{{uniqid}}" class="toast mx-auto {{#type}}toast-{{type}}{{/type}}" data-delay="{{delay}}" {{^autohide}}data-autohide="false"{{/autohide}}>
<div id="toast-{{uniqid}}" class="toast mx-auto {{#type}}toast-{{type}}{{/type}} {{#visuallyHidden}}sr-only{{/visuallyHidden}}" data-delay="{{delay}}" {{^autohide}}data-autohide="false"{{/autohide}}>
{{#title}}
<div class="toast-header">
<span class="toast-title">{{{title}}}</span>