diff --git a/.upgradenotes/MDL-87993-2026022007035360.yml b/.upgradenotes/MDL-87993-2026022007035360.yml
new file mode 100644
index 00000000000..bb0f82433a2
--- /dev/null
+++ b/.upgradenotes/MDL-87993-2026022007035360.yml
@@ -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
diff --git a/lib/amd/build/local/reactive/srlogger.min.js b/lib/amd/build/local/reactive/srlogger.min.js
index bffbee830e1..64070df5ff1 100644
--- a/lib/amd/build/local/reactive/srlogger.min.js
+++ b/lib/amd/build/local/reactive/srlogger.min.js
@@ -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
\ No newline at end of file
diff --git a/lib/amd/build/local/reactive/srlogger.min.js.map b/lib/amd/build/local/reactive/srlogger.min.js.map
index c309c3b6dd6..3d8db8a48d8 100644
--- a/lib/amd/build/local/reactive/srlogger.min.js.map
+++ b/lib/amd/build/local/reactive/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 .\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 \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"}
\ No newline at end of file
+{"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 .\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 \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"}
\ No newline at end of file
diff --git a/lib/amd/build/toast.min.js b/lib/amd/build/toast.min.js
index 999a00d8816..b4f7203ffaa 100644
--- a/lib/amd/build/toast.min.js
+++ b/lib/amd/build/toast.min.js
@@ -5,6 +5,6 @@ define("core/toast",["exports","core/templates","core/notification","core/pendin
* @module core/toast
* @copyright 2019 Andrew Nicols
* @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
\ No newline at end of file
diff --git a/lib/amd/build/toast.min.js.map b/lib/amd/build/toast.min.js.map
index 261af9bf12f..55bfa9b30d1 100644
--- a/lib/amd/build/toast.min.js.map
+++ b/lib/amd/build/toast.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"toast.min.js","sources":["../src/toast.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 .\n\n/**\n * A system for displaying small snackbar notifications to users which disappear shortly after they are shown.\n *\n * @module core/toast\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\nconst regionSelector = '.toast-wrapper';\n\n/**\n * Add a new region to place toasts in, taking in a parent element.\n *\n * @method\n * @param {HTMLElement} parent\n */\nexport const addToastRegion = async(parent) => {\n const pendingPromise = new Pending('addToastRegion');\n\n try {\n const {html, js} = await Templates.renderForPromise('core/local/toast/wrapper', {});\n Templates.prependNodeContents(parent, html, js);\n } catch (e) {\n Notification.exception(e);\n }\n\n pendingPromise.resolve();\n};\n\n/**\n * Add a new toast or snackbar notification to the page.\n *\n * @method\n * @param {String|Promise} message\n * @param {Object} configuration\n * @param {String} [configuration.title]\n * @param {String} [configuration.subtitle]\n * @param {String} [configuration.type=info] Optional type of the toast notification ('success', 'info', 'warning' or 'danger')\n * @param {Boolean} [configuration.autohide=true]\n * @param {Boolean} [configuration.closeButton=false]\n * @param {Number} [configuration.delay=4000]\n *\n * @example\n * import {add as addToast} from 'core/toast';\n * import {getString} from 'core/str';\n *\n * addToast('Example string', {\n * type: 'warning',\n * autohide: false,\n * closeButton: true,\n * });\n *\n * addToast(getString('example', 'mod_myexample'), {\n * type: 'warning',\n * autohide: false,\n * closeButton: true,\n * });\n */\nexport const add = async(message, configuration) => {\n const pendingPromise = new Pending('addToastRegion');\n configuration = {\n type: 'info',\n closeButton: false,\n autohide: true,\n delay: 4000,\n ...configuration,\n };\n\n const templateName = `core/local/toast/message`;\n try {\n const {html, js} = await Templates.renderForPromise(templateName, {\n message: await message,\n ...configuration\n });\n const targetNode = await getTargetNode();\n Templates.prependNodeContents(targetNode, html, js);\n } catch (e) {\n Notification.exception(e);\n }\n\n pendingPromise.resolve();\n};\n\nconst getTargetNode = async() => {\n const regions = document.querySelectorAll(regionSelector);\n\n if (regions.length) {\n return regions[regions.length - 1];\n }\n\n await addToastRegion(document.body, 'fixed-bottom');\n return getTargetNode();\n};\n\n/**\n * Remove a parent region.\n *\n * This is useful in cases such as where a dialog is to be removed and the toast region should be moved back to the body.\n *\n * @param {HTMLElement} parent The region that the toast region is currently a child of.\n * @param {HTMLElement} newParent The parent element to move the toast region content to.\n */\nexport const removeToastRegion = async(parent, newParent = document) => {\n const pendingPromise = new Pending('core/toast:removeToastRegion');\n const getRegionFromParent = (thisParent) => thisParent.querySelector(regionSelector);\n\n const regionToRemove = getRegionFromParent(parent);\n if (regionToRemove) {\n const targetRegion = getRegionFromParent(newParent);\n\n regionToRemove.children.forEach((node) => {\n targetRegion.insertBefore(node, targetRegion.firstChild);\n });\n\n regionToRemove.remove();\n }\n pendingPromise.resolve();\n};\n"],"names":["addToastRegion","async","pendingPromise","Pending","html","js","Templates","renderForPromise","prependNodeContents","parent","e","exception","resolve","message","configuration","type","closeButton","autohide","delay","targetNode","getTargetNode","regions","document","querySelectorAll","length","body","newParent","getRegionFromParent","thisParent","querySelector","regionToRemove","targetRegion","children","forEach","node","insertBefore","firstChild","remove"],"mappings":";;;;;;;sRAkCaA,eAAiBC,MAAAA,eACpBC,eAAiB,IAAIC,iBAAQ,4BAGzBC,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiB,2BAA4B,uBACtEC,oBAAoBC,OAAQL,KAAMC,IAC9C,MAAOK,yBACQC,UAAUD,GAG3BR,eAAeU,+DAgCAX,MAAMY,QAASC,uBACxBZ,eAAiB,IAAIC,iBAAQ,kBACnCW,cAAgB,CACZC,KAAM,OACNC,aAAa,EACbC,UAAU,EACVC,MAAO,OACJJ,yBAKGV,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,4CAA+B,CAC9DM,cAAeA,WACZC,gBAEDK,iBAAmBC,mCACfZ,oBAAoBW,WAAYf,KAAMC,IAClD,MAAOK,yBACQC,UAAUD,GAG3BR,eAAeU,iBAGbQ,cAAgBnB,gBACZoB,QAAUC,SAASC,iBA5EN,yBA8EfF,QAAQG,OACDH,QAAQA,QAAQG,OAAS,UAG9BxB,eAAesB,SAASG,MACvBL,6CAWsBnB,eAAMQ,YAAQiB,iEAAYJ,eACjDpB,eAAiB,IAAIC,iBAAQ,gCAC7BwB,oBAAuBC,YAAeA,WAAWC,cAhGpC,kBAkGbC,eAAiBH,oBAAoBlB,WACvCqB,eAAgB,OACVC,aAAeJ,oBAAoBD,WAEzCI,eAAeE,SAASC,SAASC,OAC7BH,aAAaI,aAAaD,KAAMH,aAAaK,eAGjDN,eAAeO,SAEnBnC,eAAeU"}
\ No newline at end of file
+{"version":3,"file":"toast.min.js","sources":["../src/toast.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 .\n\n/**\n * A system for displaying small snackbar notifications to users which disappear shortly after they are shown.\n *\n * @module core/toast\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\n\nconst regionSelector = '.toast-wrapper';\n\n/**\n * Add a new region to place toasts in, taking in a parent element.\n *\n * @method\n * @param {HTMLElement} parent\n */\nexport const addToastRegion = async(parent) => {\n const pendingPromise = new Pending('addToastRegion');\n\n try {\n const {html, js} = await Templates.renderForPromise('core/local/toast/wrapper', {});\n Templates.prependNodeContents(parent, html, js);\n } catch (e) {\n Notification.exception(e);\n }\n\n pendingPromise.resolve();\n};\n\n/**\n * Add a new toast or snackbar notification to the page.\n *\n * @method\n * @param {String|Promise} message\n * @param {Object} configuration\n * @param {String} [configuration.title]\n * @param {String} [configuration.subtitle]\n * @param {String} [configuration.type=info] Optional type of the toast notification ('success', 'info', 'warning' or 'danger')\n * @param {Boolean} [configuration.autohide=true]\n * @param {Boolean} [configuration.closeButton=false]\n * @param {Number} [configuration.delay=4000]\n * @param {Boolean} [configuration.visuallyHidden=false] Whether the toast should be visually hidden. This is intended for cases\n * where we want to announce ARIA live updates for screen reader users only.\n *\n * @example\n * import {add as addToast} from 'core/toast';\n * import {getString} from 'core/str';\n *\n * addToast('Example string', {\n * type: 'warning',\n * autohide: false,\n * closeButton: true,\n * });\n *\n * addToast(getString('example', 'mod_myexample'), {\n * type: 'warning',\n * autohide: false,\n * closeButton: true,\n * });\n *\n * addToast('A string to be rendered visually hidden', {\n * visuallyHidden: true,\n * });\n */\nexport const add = async(message, configuration) => {\n const pendingPromise = new Pending('addToastRegion');\n configuration = {\n type: 'info',\n closeButton: false,\n autohide: true,\n delay: 4000,\n visuallyHidden: false,\n ...configuration,\n };\n\n // Ensure that visually hidden toasts are always autohidden and do not have a close button that can receive keyboard focus.\n if (configuration.visuallyHidden) {\n configuration.autohide = true;\n configuration.closeButton = false;\n }\n\n const templateName = `core/local/toast/message`;\n try {\n const {html, js} = await Templates.renderForPromise(templateName, {\n message: await message,\n ...configuration\n });\n\n const targetNode = await getTargetNode();\n Templates.prependNodeContents(targetNode, html, js);\n } catch (e) {\n Notification.exception(e);\n }\n\n pendingPromise.resolve();\n};\n\nconst getTargetNode = async() => {\n const regions = document.querySelectorAll(regionSelector);\n\n if (regions.length) {\n return regions[regions.length - 1];\n }\n\n await addToastRegion(document.body, 'fixed-bottom');\n return getTargetNode();\n};\n\n/**\n * Remove a parent region.\n *\n * This is useful in cases such as where a dialog is to be removed and the toast region should be moved back to the body.\n *\n * @param {HTMLElement} parent The region that the toast region is currently a child of.\n * @param {HTMLElement} newParent The parent element to move the toast region content to.\n */\nexport const removeToastRegion = async(parent, newParent = document) => {\n const pendingPromise = new Pending('core/toast:removeToastRegion');\n const getRegionFromParent = (thisParent) => thisParent.querySelector(regionSelector);\n\n const regionToRemove = getRegionFromParent(parent);\n if (regionToRemove) {\n const targetRegion = getRegionFromParent(newParent);\n\n regionToRemove.children.forEach((node) => {\n targetRegion.insertBefore(node, targetRegion.firstChild);\n });\n\n regionToRemove.remove();\n }\n pendingPromise.resolve();\n};\n"],"names":["addToastRegion","async","pendingPromise","Pending","html","js","Templates","renderForPromise","prependNodeContents","parent","e","exception","resolve","message","configuration","type","closeButton","autohide","delay","visuallyHidden","targetNode","getTargetNode","regions","document","querySelectorAll","length","body","newParent","getRegionFromParent","thisParent","querySelector","regionToRemove","targetRegion","children","forEach","node","insertBefore","firstChild","remove"],"mappings":";;;;;;;sRAkCaA,eAAiBC,MAAAA,eACpBC,eAAiB,IAAIC,iBAAQ,4BAGzBC,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,iBAAiB,2BAA4B,uBACtEC,oBAAoBC,OAAQL,KAAMC,IAC9C,MAAOK,yBACQC,UAAUD,GAG3BR,eAAeU,+DAsCAX,MAAMY,QAASC,uBACxBZ,eAAiB,IAAIC,iBAAQ,mBACnCW,cAAgB,CACZC,KAAM,OACNC,aAAa,EACbC,UAAU,EACVC,MAAO,IACPC,gBAAgB,KACbL,gBAIWK,iBACdL,cAAcG,UAAW,EACzBH,cAAcE,aAAc,aAKtBZ,KAACA,KAADC,GAAOA,UAAYC,mBAAUC,4CAA+B,CAC9DM,cAAeA,WACZC,gBAGDM,iBAAmBC,mCACfb,oBAAoBY,WAAYhB,KAAMC,IAClD,MAAOK,yBACQC,UAAUD,GAG3BR,eAAeU,iBAGbS,cAAgBpB,gBACZqB,QAAUC,SAASC,iBA1FN,yBA4FfF,QAAQG,OACDH,QAAQA,QAAQG,OAAS,UAG9BzB,eAAeuB,SAASG,MACvBL,6CAWsBpB,eAAMQ,YAAQkB,iEAAYJ,eACjDrB,eAAiB,IAAIC,iBAAQ,gCAC7ByB,oBAAuBC,YAAeA,WAAWC,cA9GpC,kBAgHbC,eAAiBH,oBAAoBnB,WACvCsB,eAAgB,OACVC,aAAeJ,oBAAoBD,WAEzCI,eAAeE,SAASC,SAASC,OAC7BH,aAAaI,aAAaD,KAAMH,aAAaK,eAGjDN,eAAeO,SAEnBpC,eAAeU"}
\ No newline at end of file
diff --git a/lib/amd/src/local/reactive/srlogger.js b/lib/amd/src/local/reactive/srlogger.js
index f5dcc4fba3c..58cef8113ae 100644
--- a/lib/amd/src/local/reactive/srlogger.js
+++ b/lib/amd/src/local/reactive/srlogger.js
@@ -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});
}
}
}
diff --git a/lib/amd/src/toast.js b/lib/amd/src/toast.js
index 4d27a9933a7..105dfa48dc8 100644
--- a/lib/amd/src/toast.js
+++ b/lib/amd/src/toast.js
@@ -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) {
diff --git a/lib/templates/local/toast/message.mustache b/lib/templates/local/toast/message.mustache
index deabd6de80d..b90c180df8b 100644
--- a/lib/templates/local/toast/message.mustache
+++ b/lib/templates/local/toast/message.mustache
@@ -36,7 +36,7 @@
"closebutton": true
}
}}
-