MDL-68390 aria: Add new core_aria module

This commit is contained in:
Andrew Nicols 2020-04-07 14:39:37 +08:00
parent ee23a8cf25
commit 4f1c8ce764
15 changed files with 401 additions and 41 deletions

2
lib/amd/build/aria.min.js vendored Normal file
View file

@ -0,0 +1,2 @@
define ("core/aria",["exports","./local/aria/aria-hidden"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"hide",{enumerable:!0,get:function get(){return b.hide}});Object.defineProperty(a,"unhide",{enumerable:!0,get:function get(){return b.unhide}});Object.defineProperty(a,"hideSiblings",{enumerable:!0,get:function get(){return b.hideSiblings}});Object.defineProperty(a,"unhideSiblings",{enumerable:!0,get:function get(){return b.unhideSiblings}})});
//# sourceMappingURL=aria.min.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","sourcesContent":[],"file":"aria.min.js"}

View file

@ -0,0 +1,2 @@
define ("core/local/aria/aria-hidden",["exports","core/normalise","./selectors"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.unhideSiblings=a.hideSiblings=a.unhide=a.hide=void 0;c=function(a){return a&&a.__esModule?a:{default:a}}(c);var d=new Map,e=new Map,f=function(){return MutationObserver&&"function"==typeof MutationObserver},g=function(a){if(!(a instanceof HTMLElement)){return}if(a.matches(c.default.elements.focusable)){h(a)}a.querySelectorAll(c.default.elements.focusable).forEach(h)},h=function(a){if("undefined"!=typeof a.dataset.ariaHiddenTabIndex){return}if(a.getAttribute("tabindex")){a.dataset.ariaHiddenTabIndex=a.getAttribute("tabindex")}else{a.dataset.ariaHiddenTabIndex=""}a.setAttribute("tabindex",-1)},i=function(a){if(!(a instanceof HTMLElement)){return}if(a.matches(c.default.elements.focusableToUnhide)){j(a)}a.querySelectorAll(c.default.elements.focusableToUnhide).forEach(j)},j=function(a){if(a.closest(c.default.aria.hidden)){return}var b=a.dataset.ariaHiddenTabIndex;if(""===b){a.removeAttribute("tabindex")}else{a.setAttribute("tabindex",b)}delete a.dataset.ariaHiddenTabIndex},k=function(a){return(0,b.getList)(a).forEach(l)};a.hide=k;var l=function(a){if(!(a instanceof HTMLElement)){return}if(a.closest(c.default.aria.hidden)){return}a.setAttribute("aria-hidden",!0);g(a);if(f()){var b=new MutationObserver(function(a){a.forEach(function(a){a.addedNodes.forEach(g)})});b.observe(a,{childList:!0,subtree:!0});d.set(a,b)}},m=function(a){return(0,b.getList)(a).forEach(n)};a.unhide=m;var n=function(a){if(!(a instanceof HTMLElement)){return}a.removeAttribute("aria-hidden");i(a);if(d.has(a)){d.get(a).disconnect();d.delete(a)}};a.hideSiblings=function hideSiblings(a){return(0,b.getList)(a).forEach(o)};var o=function(a){if(!(a instanceof HTMLElement)){return}if(!a.parentElement){return}a.parentElement.childNodes.forEach(function(b){if(b===a){return}k(b)});if(f()){var b=new MutationObserver(function(b){b.forEach(function(b){b.addedNodes.forEach(function(b){if(a.contains(b)){return}k(b)})})});b.observe(a.parentElement,{childList:!0,subtree:!0});e.set(a.parentElement,b)}};a.unhideSiblings=function unhideSiblings(a){return(0,b.getList)(a).forEach(p)};var p=function(a){if(!(a instanceof HTMLElement)){return}if(!a.parentElement){return}a.parentElement.childNodes.forEach(function(b){if(b===a){return}m(b)});if(e.has(a.parentElement)){e.get(a.parentElement).disconnect();e.delete(a.parentElement)}}});
//# sourceMappingURL=aria-hidden.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
define ("core/local/aria/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={aria:{hidden:"[aria-hidden]"},elements:{focusable:"input:not([type=\"hidden\"]), a[href], button, textarea, select, [tabindex]",focusableToUnhide:"[data-aria-hidden-tab-index]"}};return a.default});
//# sourceMappingURL=selectors.min.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["../../../src/local/aria/selectors.js"],"names":["aria","hidden","elements","focusable","focusableToUnhide"],"mappings":"qJAwBe,CACXA,IAAI,CAAE,CACFC,MAAM,CAAE,eADN,CADK,CAIXC,QAAQ,CAAE,CACNC,SAAS,CAAE,6EADL,CAENC,iBAAiB,CAAE,8BAFb,CAJC,C","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 * Selectors used for ARIA.\n *\n * @module core/local/aria/selectors\n * @class selectors\n * @package core\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n aria: {\n hidden: '[aria-hidden]',\n },\n elements: {\n focusable: 'input:not([type=\"hidden\"]), a[href], button, textarea, select, [tabindex]',\n focusableToUnhide: '[data-aria-hidden-tab-index]',\n },\n};\n"],"file":"selectors.min.js"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
lib/amd/build/normalise.min.js vendored Normal file
View file

@ -0,0 +1,2 @@
define ("core/normalise",["exports","jquery"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.getList=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c=function(a){if(a instanceof HTMLElement){return[a]}if(a instanceof Array){return a}if(a instanceof NodeList){return Array.from(a)}if(a instanceof b.default){return a.get()}return Array.from(a)};a.getList=c});
//# sourceMappingURL=normalise.min.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["../src/normalise.js"],"names":["getList","nodes","HTMLElement","Array","NodeList","from","jQuery","get"],"mappings":"2IAyBA,uDAEO,GAAMA,CAAAA,CAAO,CAAG,SAAAC,CAAK,CAAI,CAC5B,GAAIA,CAAK,WAAYC,CAAAA,WAArB,CAAkC,CAE9B,MAAO,CAACD,CAAD,CACV,CAED,GAAIA,CAAK,WAAYE,CAAAA,KAArB,CAA4B,CAExB,MAAOF,CAAAA,CACV,CAED,GAAIA,CAAK,WAAYG,CAAAA,QAArB,CAA+B,CAE3B,MAAOD,CAAAA,KAAK,CAACE,IAAN,CAAWJ,CAAX,CACV,CAED,GAAIA,CAAK,WAAYK,UAArB,CAA6B,CAEzB,MAAOL,CAAAA,CAAK,CAACM,GAAN,EACV,CAGD,MAAOJ,CAAAA,KAAK,CAACE,IAAN,CAAWJ,CAAX,CACV,CAvBM,C","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 * Normalisation helpers.\n *\n * @module core/normalise\n * @class normalise\n * @package core\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\n\nexport const getList = nodes => {\n if (nodes instanceof HTMLElement) {\n // A single record to conver to a NodeList.\n return [nodes];\n }\n\n if (nodes instanceof Array) {\n // A single record to conver to a NodeList.\n return nodes;\n }\n\n if (nodes instanceof NodeList) {\n // Already a NodeList.\n return Array.from(nodes);\n }\n\n if (nodes instanceof jQuery) {\n // A jQuery object to a NodeList.\n return nodes.get();\n }\n\n // Fallback to just having a go.\n return Array.from(nodes);\n};\n"],"file":"normalise.min.js"}

31
lib/amd/src/aria.js Normal file
View file

@ -0,0 +1,31 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Helpers to perform ARIA compliance changes to the DOM.
*
* @module core/aria
* @class aria
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export {
hide,
unhide,
hideSiblings,
unhideSiblings,
} from './local/aria/aria-hidden';

View file

@ -0,0 +1,267 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* ARIA helpers related to the aria-hidden attribute.
*
* @module core/local/aria/aria-hidden.
* @class aria
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getList} from 'core/normalise';
import Selectors from './selectors';
// The map of MutationObserver objects for an object.
const childObserverMap = new Map();
const siblingObserverMap = new Map();
/**
* Determine whether the browser supports the MutationObserver system.
*
* @returns {Bool}
*/
const supportsMutationObservers = () => (MutationObserver && typeof MutationObserver === 'function');
/**
* Disable element focusability, disabling the tabindex for child elements which are normally focusable.
*
* @param {HTMLElement} target
*/
const disableElementFocusability = target => {
if (!(target instanceof HTMLElement)) {
// This element is not an HTMLElement.
// This can happen for Text Nodes.
return;
}
if (target.matches(Selectors.elements.focusable)) {
disableAndStoreTabIndex(target);
}
target.querySelectorAll(Selectors.elements.focusable).forEach(disableAndStoreTabIndex);
};
/**
* Remove the current tab-index and store it for later restoration.
*
* @param {HTMLElement} element
*/
const disableAndStoreTabIndex = element => {
if (typeof element.dataset.ariaHiddenTabIndex !== 'undefined') {
// This child already has a hidden attribute.
// Do not modify it as the original value will be lost.
return;
}
// Store the old tabindex in a data attribute.
if (element.getAttribute('tabindex')) {
element.dataset.ariaHiddenTabIndex = element.getAttribute('tabindex');
} else {
element.dataset.ariaHiddenTabIndex = '';
}
element.setAttribute('tabindex', -1);
};
/**
* Re-enable element focusability, restoring any tabindex.
*
* @param {HTMLElement} target
*/
const enableElementFocusability = target => {
if (!(target instanceof HTMLElement)) {
// This element is not an HTMLElement.
// This can happen for Text Nodes.
return;
}
if (target.matches(Selectors.elements.focusableToUnhide)) {
restoreTabIndex(target);
}
target.querySelectorAll(Selectors.elements.focusableToUnhide).forEach(restoreTabIndex);
};
/**
* Restore the tab-index of the supplied element.
*
* When disabling focusability the current tab-index is stored in the ariaHiddenTabIndex data attribute.
* This is used to restore the tab-index, but only whilst the parent nodes remain unhidden.
*
* @param {HTMLElement} element
*/
const restoreTabIndex = element => {
if (element.closest(Selectors.aria.hidden)) {
// This item still has a hidden parent, or is hidden itself. Do not unhide it.
return;
}
const oldTabIndex = element.dataset.ariaHiddenTabIndex;
if (oldTabIndex === '') {
element.removeAttribute('tabindex');
} else {
element.setAttribute('tabindex', oldTabIndex);
}
delete element.dataset.ariaHiddenTabIndex;
};
/**
* Update the supplied DOM Module to be hidden.
*
* @param {HTMLElement} target
* @returns {Array}
*/
export const hide = target => getList(target).forEach(_hide);
const _hide = target => {
if (!(target instanceof HTMLElement)) {
// This element is not an HTMLElement.
// This can happen for Text Nodes.
return;
}
if (target.closest(Selectors.aria.hidden)) {
// This Element, or a parent Element, is already hidden.
// Stop processing.
return;
}
// Set the aria-hidden attribute to true.
target.setAttribute('aria-hidden', true);
// Based on advice from https://dequeuniversity.com/rules/axe/3.3/aria-hidden-focus, upon setting the aria-hidden
// attribute, all focusable elements underneath that element should be modified such that they are not focusable.
disableElementFocusability(target);
if (supportsMutationObservers()) {
// Add a MutationObserver to check for new children to the tree.
const newNodeObserver = new MutationObserver(mutationList => {
mutationList.forEach(mutation => {
mutation.addedNodes.forEach(disableElementFocusability);
});
});
newNodeObserver.observe(target, {childList: true, subtree: true});
childObserverMap.set(target, newNodeObserver);
}
};
/**
* Reverse the effect of the hide action.
*
* @param {HTMLElement} target
* @returns {Array}
*/
export const unhide = target => getList(target).forEach(_unhide);
const _unhide = target => {
if (!(target instanceof HTMLElement)) {
return;
}
// Note: The aria-hidden attribute should be removed, and not set to false.
// The presence of the attribute is sufficient for some browsers to treat it as being true, regardless of its value.
target.removeAttribute('aria-hidden');
// Restore the tabindex across all child nodes of the target.
enableElementFocusability(target);
// Remove the focusability MutationObserver watching this tree.
if (childObserverMap.has(target)) {
childObserverMap.get(target).disconnect();
childObserverMap.delete(target);
}
};
/**
* Correctly mark all siblings of the supplied target Element as hidden.
*
* @param {HTMLElement} target
* @returns {Array}
*/
export const hideSiblings = target => getList(target).forEach(_hideSiblings);
const _hideSiblings = target => {
if (!(target instanceof HTMLElement)) {
return;
}
if (!target.parentElement) {
return;
}
target.parentElement.childNodes.forEach(node => {
if (node === target) {
// Skip self;
return;
}
hide(node);
});
if (supportsMutationObservers()) {
// Add a MutationObserver to check for new children to the tree.
const newNodeObserver = new MutationObserver(mutationList => {
mutationList.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (target.contains(node)) {
// Skip self, and children of self.
return;
}
hide(node);
});
});
});
newNodeObserver.observe(target.parentElement, {childList: true, subtree: true});
siblingObserverMap.set(target.parentElement, newNodeObserver);
}
};
/**
* Correctly reverse the hide action of all children of the supplied target Element.
*
* @param {HTMLElement} target
* @returns {Array}
*/
export const unhideSiblings = target => getList(target).forEach(_unhideSiblings);
const _unhideSiblings = target => {
if (!(target instanceof HTMLElement)) {
return;
}
if (!target.parentElement) {
return;
}
target.parentElement.childNodes.forEach(node => {
if (node === target) {
// Skip self;
return;
}
unhide(node);
});
// Remove the sibling MutationObserver watching this tree.
if (siblingObserverMap.has(target.parentElement)) {
siblingObserverMap.get(target.parentElement).disconnect();
siblingObserverMap.delete(target.parentElement);
}
};

View file

@ -0,0 +1,33 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Selectors used for ARIA.
*
* @module core/local/aria/selectors
* @class selectors
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
aria: {
hidden: '[aria-hidden]',
},
elements: {
focusable: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
focusableToUnhide: '[data-aria-hidden-tab-index]',
},
};

View file

@ -33,7 +33,8 @@ define([
'core/modal_events',
'core/local/aria/focuslock',
'core/pending',
], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending) {
'core/aria',
], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending, Aria) {
var SELECTORS = {
CONTAINER: '[data-region="modal-container"]',
@ -732,30 +733,10 @@ define([
* @method accessibilityShow
*/
Modal.prototype.accessibilityShow = function() {
// We need to get a list containing each sibling element and the shallowest
// non-ancestral nodes in the DOM. We can shortcut this a little by leveraging
// the fact that this dialogue is always appended to the document body therefore
// it's siblings are the shallowest non-ancestral nodes. If that changes then
// this code should also be updated.
$('body').children().each(function(index, child) {
// Skip the current modal.
if (!this.root.is(child)) {
child = $(child);
var hidden = child.attr('aria-hidden');
// If they are already hidden we can ignore them.
if (hidden !== 'true') {
// Save their current state.
child.data('previous-aria-hidden', hidden);
this.hiddenSiblings.push(child);
// Hide this node from screen readers.
child.attr('aria-hidden', 'true');
}
}
}.bind(this));
Aria.hideSiblings(this.root.get()[0]);
// Make us visible to screen readers.
this.root.attr('aria-hidden', 'false');
this.root.removeAttr('aria-hidden');
};
/**
@ -768,22 +749,7 @@ define([
Modal.prototype.accessibilityHide = function() {
this.root.attr('aria-hidden', 'true');
// Restore the sibling nodes back to their original values.
$.each(this.hiddenSiblings, function(index, sibling) {
sibling = $(sibling);
var previousValue = sibling.data('previous-aria-hidden');
// If the element didn't previously have an aria-hidden attribute
// then we can just remove the one we set.
if (typeof previousValue == 'undefined') {
sibling.removeAttr('aria-hidden');
} else {
// Otherwise set it back to the old value (which will be false).
sibling.attr('aria-hidden', previousValue);
}
});
// Clear the cache. No longer need to store these.
this.hiddenSiblings = [];
Aria.unhideSiblings(this.root.get()[0]);
};
/**

51
lib/amd/src/normalise.js Normal file
View file

@ -0,0 +1,51 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Normalisation helpers.
*
* @module core/normalise
* @class normalise
* @package core
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import jQuery from 'jquery';
export const getList = nodes => {
if (nodes instanceof HTMLElement) {
// A single record to conver to a NodeList.
return [nodes];
}
if (nodes instanceof Array) {
// A single record to conver to a NodeList.
return nodes;
}
if (nodes instanceof NodeList) {
// Already a NodeList.
return Array.from(nodes);
}
if (nodes instanceof jQuery) {
// A jQuery object to a NodeList.
return nodes.get();
}
// Fallback to just having a go.
return Array.from(nodes);
};