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 } }} -
+
{{#title}}
{{{title}}}