From 9144374545c1ec62385ba002f31c5fd2ab233178 Mon Sep 17 00:00:00 2001 From: valdrinkoshi Date: Thu, 23 Mar 2017 13:02:53 -0700 Subject: [PATCH 01/12] use ShadowDOM v1 --- dist/inert.js | 804 ++++++++------------------------------------ dist/inert.min.js | 2 +- src/inert.js | 832 ++++++++++++++-------------------------------- test/index.js | 56 +--- 4 files changed, 397 insertions(+), 1297 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index f4e9f8b..a9952ab 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -24,23 +24,17 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons (function (document) { /** @type {string} */ - var _focusableElementsString = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]'].join(','); + var acceptsShadowSel = ['article', 'aside', 'blockquote', 'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'main', 'nav', 'p', 'section', 'span'].join(','); /** - * `InertRoot` manages a single inert subtree, i.e. a DOM subtree whose root element has an `inert` + * `InertRoot` manages a single inert subtree, i.e. a ShadowDOM subtree whose root element has an `inert` * attribute. * * Its main functions are: * - * - to create and maintain a set of managed `InertNode`s, including when mutations occur in the - * subtree. The `makeSubtreeUnfocusable()` method handles collecting `InertNode`s via registering - * each focusable node in the subtree with the singleton `InertManager` which manages all known - * focusable nodes within inert subtrees. `InertManager` ensures that a single `InertNode` - * instance exists for each focusable node which has at least one inert root as an ancestor. + * - make the rootElement untabbable. * - * - to notify all managed `InertNode`s when this subtree stops being inert (i.e. when the `inert` - * attribute is removed from the root node). This is handled in the destructor, which calls the - * `deregister` method on `InertManager` for each managed inert node. + * - notify the manager of inerted nodes in the rootElement's shadowRoot. */ var InertRoot = function () { @@ -49,33 +43,60 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons * @param {InertManager} inertManager The global singleton InertManager object. */ function InertRoot(rootElement, inertManager) { - _classCallCheck(this, InertRoot); + var _this = this; - /** @type {InertManager} */ - this._inertManager = inertManager; + _classCallCheck(this, InertRoot); /** @type {Element} */ this._rootElement = rootElement; - /** - * @type {Set} - * All managed focusable nodes in this InertRoot's subtree. - */ - this._managedNodes = new Set([]); + /** @type {string} */ + this._rootTabindex = rootElement.getAttribute('tabindex') || null; // Make the subtree hidden from assistive technology - this._rootElement.setAttribute('aria-hidden', 'true'); - - // Make all focusable elements in the subtree unfocusable and add them to _managedNodes - this._makeSubtreeUnfocusable(this._rootElement); - - // Watch for: - // - any additions in the subtree: make them unfocusable too - // - any removals from the subtree: remove them from this inert root's managed nodes - // - attribute changes: if `tabindex` is added, or removed from an intrinsically focusable element, - // make that node a managed node. - this._observer = new MutationObserver(this._onMutation.bind(this)); - this._observer.observe(this._rootElement, { attributes: true, childList: true, subtree: true }); + rootElement.setAttribute('aria-hidden', 'true'); + + // Make it untabbable. + rootElement.tabIndex = -1; + + // We can attach a shadowRoot if element has potential custom element name + // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name + // or is a native element that accepts shadow roots + // https://dom.spec.whatwg.org/#dom-element-attachshadow + var canAttachShadow = rootElement.localName.indexOf('-') !== -1 || rootElement.matches(acceptsShadowSel); + if (!canAttachShadow) return; + + // Force shadowRoot. + if (!rootElement.shadowRoot) { + rootElement.attachShadow({ + mode: 'open' + }).appendChild(document.createElement('slot')); + var nativeAttachShadow = rootElement.attachShadow; + rootElement.attachShadow = function () { + // Clear the slot we added. + _this.shadowRoot.removeChild(rootElement.shadowRoot.firstChild); + _this.attachShadow = nativeAttachShadow; + return _this.shadowRoot; + }; + } else { + // Manager might have not "seen" these children since they're in a shadowRoot. + var inertChildren = rootElement.shadowRoot.querySelectorAll('[inert]'); + for (var i = 0, l = inertChildren.length; i < l; i++) { + inertManager.setInert(inertChildren[i], true); + } + } + + // Ensure we move the focus outside of rootElement. + var active = rootElement.activeElement || document.activeElement; + if (active && rootElement.contains(active)) active.blur(); + + // Give the manager visibility on changing nodes in the shadowRoot. + this._observer = new MutationObserver(inertManager.watchForInert); + this._observer.observe(rootElement.shadowRoot, { + attributes: true, + subtree: true, + childList: true + }); } /** @@ -87,472 +108,24 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons _createClass(InertRoot, [{ key: 'destructor', value: function destructor() { - this._observer.disconnect(); - this._observer = null; - - if (this._rootElement) this._rootElement.removeAttribute('aria-hidden'); - this._rootElement = null; - - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = this._managedNodes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var inertNode = _step.value; - - this._unmanageNode(inertNode.node); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - this._managedNodes = null; - - this._inertManager = null; - } - - /** - * @return {Set} A copy of this InertRoot's managed nodes set. - */ - - }, { - key: '_makeSubtreeUnfocusable', - - - /** - * @param {Node} startNode - */ - value: function _makeSubtreeUnfocusable(startNode) { - var _this = this; - - composedTreeWalk(startNode, function (node) { - _this._visitNode(node); - }); - - var activeElement = document.activeElement; - if (!document.body.contains(startNode)) { - // startNode may be in shadow DOM, so find its nearest shadowRoot to get the activeElement. - var node = startNode; - var root = undefined; - while (node) { - if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - root = node; - break; - } - node = node.parentNode; - } - if (root) activeElement = root.activeElement; - } - if (startNode.contains(activeElement)) activeElement.blur(); - } - - /** - * @param {Node} node - */ - - }, { - key: '_visitNode', - value: function _visitNode(node) { - if (node.nodeType !== Node.ELEMENT_NODE) return; - - // If a descendant inert root becomes un-inert, its descendants will still be inert because of this - // inert root, so all of its managed nodes need to be adopted by this InertRoot. - if (node !== this._rootElement && node.hasAttribute('inert')) this._adoptInertRoot(node); - - if (node.matches(_focusableElementsString) || node.hasAttribute('tabindex')) this._manageNode(node); - } - - /** - * Register the given node with this InertRoot and with InertManager. - * @param {Node} node - */ - - }, { - key: '_manageNode', - value: function _manageNode(node) { - var inertNode = this._inertManager.register(node, this); - this._managedNodes.add(inertNode); - } - - /** - * Unregister the given node with this InertRoot and with InertManager. - * @param {Node} node - */ - - }, { - key: '_unmanageNode', - value: function _unmanageNode(node) { - var inertNode = this._inertManager.deregister(node, this); - if (inertNode) this._managedNodes.delete(inertNode); - } - - /** - * Unregister the entire subtree starting at `startNode`. - * @param {Node} startNode - */ + if (this._observer) this._observer.disconnect(); - }, { - key: '_unmanageSubtree', - value: function _unmanageSubtree(startNode) { - var _this2 = this; - - composedTreeWalk(startNode, function (node) { - _this2._unmanageNode(node); - }); - } - - /** - * If a descendant node is found with an `inert` attribute, adopt its managed nodes. - * @param {Node} node - */ - - }, { - key: '_adoptInertRoot', - value: function _adoptInertRoot(node) { - var inertSubroot = this._inertManager.getInertRoot(node); - - // During initialisation this inert root may not have been registered yet, - // so register it now if need be. - if (!inertSubroot) { - this._inertManager.setInert(node, true); - inertSubroot = this._inertManager.getInertRoot(node); - } - - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = inertSubroot.managedNodes[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var savedInertNode = _step2.value; - - this._manageNode(savedInertNode.node); - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } + this._rootElement.removeAttribute('aria-hidden'); + if (this._rootTabindex) { + this._rootElement.setAttribute('tabindex', this._rootTabindex); + } else { + this._rootElement.removeAttribute('tabindex'); } - } - - /** - * Callback used when mutation observer detects subtree additions, removals, or attribute changes. - * @param {MutationRecord} records - * @param {MutationObserver} self - */ - - }, { - key: '_onMutation', - value: function _onMutation(records, self) { - var _iteratorNormalCompletion3 = true; - var _didIteratorError3 = false; - var _iteratorError3 = undefined; - try { - for (var _iterator3 = records[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { - var record = _step3.value; - - var target = record.target; - if (record.type === 'childList') { - // Manage added nodes - var _iteratorNormalCompletion4 = true; - var _didIteratorError4 = false; - var _iteratorError4 = undefined; - - try { - for (var _iterator4 = Array.from(record.addedNodes)[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { - var node = _step4.value; - - this._makeSubtreeUnfocusable(node); - } // Un-manage removed nodes - } catch (err) { - _didIteratorError4 = true; - _iteratorError4 = err; - } finally { - try { - if (!_iteratorNormalCompletion4 && _iterator4.return) { - _iterator4.return(); - } - } finally { - if (_didIteratorError4) { - throw _iteratorError4; - } - } - } - - var _iteratorNormalCompletion5 = true; - var _didIteratorError5 = false; - var _iteratorError5 = undefined; - - try { - for (var _iterator5 = Array.from(record.removedNodes)[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { - var _node = _step5.value; - - this._unmanageSubtree(_node); - } - } catch (err) { - _didIteratorError5 = true; - _iteratorError5 = err; - } finally { - try { - if (!_iteratorNormalCompletion5 && _iterator5.return) { - _iterator5.return(); - } - } finally { - if (_didIteratorError5) { - throw _iteratorError5; - } - } - } - } else if (record.type === 'attributes') { - if (record.attributeName === 'tabindex') { - // Re-initialise inert node if tabindex changes - this._manageNode(target); - } else if (target !== this._rootElement && record.attributeName === 'inert' && target.hasAttribute('inert')) { - // If a new inert root is added, adopt its managed nodes and make sure it knows about the - // already managed nodes from this inert subroot. - this._adoptInertRoot(target); - var inertSubroot = this._inertManager.getInertRoot(target); - var _iteratorNormalCompletion6 = true; - var _didIteratorError6 = false; - var _iteratorError6 = undefined; - - try { - for (var _iterator6 = this._managedNodes[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { - var managedNode = _step6.value; - - if (target.contains(managedNode.node)) inertSubroot._manageNode(managedNode.node); - } - } catch (err) { - _didIteratorError6 = true; - _iteratorError6 = err; - } finally { - try { - if (!_iteratorNormalCompletion6 && _iterator6.return) { - _iterator6.return(); - } - } finally { - if (_didIteratorError6) { - throw _iteratorError6; - } - } - } - } - } - } - } catch (err) { - _didIteratorError3 = true; - _iteratorError3 = err; - } finally { - try { - if (!_iteratorNormalCompletion3 && _iterator3.return) { - _iterator3.return(); - } - } finally { - if (_didIteratorError3) { - throw _iteratorError3; - } - } - } - } - }, { - key: 'managedNodes', - get: function get() { - return new Set(this._managedNodes); + this._observer = null; + this._rootElement = null; + this._rootTabindex = null; } }]); return InertRoot; }(); - /** - * `InertNode` initialises and manages a single inert node. - * A node is inert if it is a descendant of one or more inert root elements. - * - * On construction, `InertNode` saves the existing `tabindex` value for the node, if any, and - * either removes the `tabindex` attribute or sets it to `-1`, depending on whether the element - * is intrinsically focusable or not. - * - * `InertNode` maintains a set of `InertRoot`s which are descendants of this `InertNode`. When an - * `InertRoot` is destroyed, and calls `InertManager.deregister()`, the `InertManager` notifies the - * `InertNode` via `removeInertRoot()`, which in turn destroys the `InertNode` if no `InertRoot`s - * remain in the set. On destruction, `InertNode` reinstates the stored `tabindex` if one exists, - * or removes the `tabindex` attribute if the element is intrinsically focusable. - */ - - - var InertNode = function () { - /** - * @param {Node} node A focusable element to be made inert. - * @param {InertRoot} inertRoot The inert root element associated with this inert node. - */ - function InertNode(node, inertRoot) { - _classCallCheck(this, InertNode); - - /** @type {Node} */ - this._node = node; - - /** @type {boolean} */ - this._overrodeFocusMethod = false; - - /** - * @type {Set} The set of descendant inert roots. - * If and only if this set becomes empty, this node is no longer inert. - */ - this._inertRoots = new Set([inertRoot]); - - /** @type {boolean} */ - this._destroyed = false; - - // Save any prior tabindex info and make this node untabbable - this.ensureUntabbable(); - } - - /** - * Call this whenever this object is about to become obsolete. - * This makes the managed node focusable again and deletes all of the previously stored state. - */ - - - _createClass(InertNode, [{ - key: 'destructor', - value: function destructor() { - this._throwIfDestroyed(); - - if (this._node) { - if (this.hasSavedTabIndex) this._node.setAttribute('tabindex', this.savedTabIndex);else this._node.removeAttribute('tabindex'); - - // Use `delete` to restore native focus method. - if (this._overrodeFocusMethod) delete this._node.focus; - } - this._node = null; - this._inertRoots = null; - - this._destroyed = true; - } - - /** - * @type {boolean} Whether this object is obsolete because the managed node is no longer inert. - * If the object has been destroyed, any attempt to access it will cause an exception. - */ - - }, { - key: '_throwIfDestroyed', - value: function _throwIfDestroyed() { - if (this.destroyed) throw new Error("Trying to access destroyed InertNode"); - } - - /** @return {boolean} */ - - }, { - key: 'ensureUntabbable', - - - /** Save the existing tabindex value and make the node untabbable and unfocusable */ - value: function ensureUntabbable() { - var node = this.node; - if (node.matches(_focusableElementsString)) { - if (node.tabIndex === -1 && this.hasSavedTabIndex) return; - - if (node.hasAttribute('tabindex')) this._savedTabIndex = node.tabIndex; - node.setAttribute('tabindex', '-1'); - if (node.nodeType === Node.ELEMENT_NODE) { - node.focus = function () {}; - this._overrodeFocusMethod = true; - } - } else if (node.hasAttribute('tabindex')) { - this._savedTabIndex = node.tabIndex; - node.removeAttribute('tabindex'); - } - } - - /** - * Add another inert root to this inert node's set of managing inert roots. - * @param {InertRoot} inertRoot - */ - - }, { - key: 'addInertRoot', - value: function addInertRoot(inertRoot) { - this._throwIfDestroyed(); - this._inertRoots.add(inertRoot); - } - - /** - * Remove the given inert root from this inert node's set of managing inert roots. - * If the set of managing inert roots becomes empty, this node is no longer inert, - * so the object should be destroyed. - * @param {InertRoot} inertRoot - */ - - }, { - key: 'removeInertRoot', - value: function removeInertRoot(inertRoot) { - this._throwIfDestroyed(); - this._inertRoots.delete(inertRoot); - if (this._inertRoots.size === 0) this.destructor(); - } - }, { - key: 'destroyed', - get: function get() { - return this._destroyed; - } - }, { - key: 'hasSavedTabIndex', - get: function get() { - return '_savedTabIndex' in this; - } - - /** @return {Node} */ - - }, { - key: 'node', - get: function get() { - this._throwIfDestroyed(); - return this._node; - } - - /** @param {number} tabIndex */ - - }, { - key: 'savedTabIndex', - set: function set(tabIndex) { - this._throwIfDestroyed(); - this._savedTabIndex = tabIndex; - } - - /** @return {number} */ - , - get: function get() { - this._throwIfDestroyed(); - return this._savedTabIndex; - } - }]); - - return InertNode; - }(); - /** * InertManager is a per-document singleton object which manages all inert roots and nodes. * @@ -569,6 +142,8 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons * @param {Document} document */ function InertManager(document) { + var _this2 = this; + _classCallCheck(this, InertManager); if (!document) throw new Error('Missing required argument; InertManager needs to wrap a document.'); @@ -576,30 +151,30 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons /** @type {Document} */ this._document = document; - /** - * All managed nodes known to this InertManager. In a map to allow looking up by Node. - * @type {Map} - */ - this._managedNodes = new Map(); - /** * All inert roots known to this InertManager. In a map to allow looking up by Node. * @type {Map} */ this._inertRoots = new Map(); + this.watchForInert = this._watchForInert.bind(this); + /** * Observer for mutations on `document.body`. * @type {MutationObserver} */ - this._observer = new MutationObserver(this._watchForInert.bind(this)); + this._observer = new MutationObserver(this.watchForInert); // Add inert style. addInertStyle(document.head || document.body || document.documentElement); - // Wait for document to be loaded. + // Wait for document to be interactive. if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', this._onDocumentLoaded.bind(this)); + var onchanged = function onchanged() { + document.removeEventListener('readystatechange', onchanged); + _this2._onDocumentLoaded(); + }; + document.addEventListener('readystatechange', onchanged); } else { this._onDocumentLoaded(); } @@ -615,28 +190,26 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons _createClass(InertManager, [{ key: 'setInert', value: function setInert(root, inert) { + if (this._inertRoots.has(root) === inert) // element is already inert + return; if (inert) { - if (this._inertRoots.has(root)) // element is already inert - return; - var inertRoot = new InertRoot(root, this); root.setAttribute('inert', ''); this._inertRoots.set(root, inertRoot); - // If not contained in the document, it must be in a shadowRoot. - // Ensure inert styles are added there. - if (!this._document.body.contains(root)) { + // Ensure inert styles are added in shadowRoots. + if (root.shadowRoot) { + addInertStyle(root.shadowRoot); + } else if (!this._document.body.contains(root)) { + // If not contained in the document, it must be in a shadowRoot. var parent = root.parentNode; while (parent) { - if (parent.nodeType === 11) { + if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { addInertStyle(parent); } parent = parent.parentNode; } } } else { - if (!this._inertRoots.has(root)) // element is already non-inert - return; - var _inertRoot = this._inertRoots.get(root); _inertRoot.destructor(); this._inertRoots.delete(root); @@ -656,55 +229,6 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons return this._inertRoots.get(element); } - /** - * Register the given InertRoot as managing the given node. - * In the case where the node has a previously existing inert root, this inert root will - * be added to its set of inert roots. - * @param {Node} node - * @param {InertRoot} inertRoot - * @return {InertNode} inertNode - */ - - }, { - key: 'register', - value: function register(node, inertRoot) { - var inertNode = this._managedNodes.get(node); - if (inertNode !== undefined) { - // node was already in an inert subtree - inertNode.addInertRoot(inertRoot); - // Update saved tabindex value if necessary - inertNode.ensureUntabbable(); - } else { - inertNode = new InertNode(node, inertRoot); - } - - this._managedNodes.set(node, inertNode); - - return inertNode; - } - - /** - * De-register the given InertRoot as managing the given inert node. - * Removes the inert root from the InertNode's set of managing inert roots, and remove the inert - * node from the InertManager's set of managed nodes if it is destroyed. - * If the node is not currently managed, this is essentially a no-op. - * @param {Node} node - * @param {InertRoot} inertRoot - * @return {InertNode?} The potentially destroyed InertNode associated with this node, if any. - */ - - }, { - key: 'deregister', - value: function deregister(node, inertRoot) { - var inertNode = this._managedNodes.get(node); - if (!inertNode) return null; - - inertNode.removeInertRoot(inertRoot); - if (inertNode.destroyed) this._managedNodes.delete(node); - - return inertNode; - } - /** * Callback used when document has finished loading. */ @@ -714,32 +238,36 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons value: function _onDocumentLoaded() { // Find all inert roots in document and make them actually inert. var inertElements = Array.from(this._document.querySelectorAll('[inert]')); - var _iteratorNormalCompletion7 = true; - var _didIteratorError7 = false; - var _iteratorError7 = undefined; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; try { - for (var _iterator7 = inertElements[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { - var inertElement = _step7.value; + for (var _iterator = inertElements[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var inertElement = _step.value; this.setInert(inertElement, true); } // Comment this out to use programmatic API only. } catch (err) { - _didIteratorError7 = true; - _iteratorError7 = err; + _didIteratorError = true; + _iteratorError = err; } finally { try { - if (!_iteratorNormalCompletion7 && _iterator7.return) { - _iterator7.return(); + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); } } finally { - if (_didIteratorError7) { - throw _iteratorError7; + if (_didIteratorError) { + throw _iteratorError; } } } - this._observer.observe(this._document.body, { attributes: true, subtree: true, childList: true }); + this._observer.observe(this._document.body, { + attributes: true, + subtree: true, + childList: true + }); } /** @@ -751,63 +279,63 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons }, { key: '_watchForInert', value: function _watchForInert(records, self) { - var _iteratorNormalCompletion8 = true; - var _didIteratorError8 = false; - var _iteratorError8 = undefined; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; try { - for (var _iterator8 = records[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { - var record = _step8.value; + for (var _iterator2 = records[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var record = _step2.value; switch (record.type) { case 'childList': - var _iteratorNormalCompletion9 = true; - var _didIteratorError9 = false; - var _iteratorError9 = undefined; + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; try { - for (var _iterator9 = Array.from(record.addedNodes)[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { - var node = _step9.value; + for (var _iterator3 = Array.from(record.addedNodes)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var node = _step3.value; if (node.nodeType !== Node.ELEMENT_NODE) continue; var inertElements = Array.from(node.querySelectorAll('[inert]')); if (node.matches('[inert]')) inertElements.unshift(node); - var _iteratorNormalCompletion10 = true; - var _didIteratorError10 = false; - var _iteratorError10 = undefined; + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; try { - for (var _iterator10 = inertElements[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { - var inertElement = _step10.value; + for (var _iterator4 = inertElements[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var inertElement = _step4.value; this.setInert(inertElement, true); } } catch (err) { - _didIteratorError10 = true; - _iteratorError10 = err; + _didIteratorError4 = true; + _iteratorError4 = err; } finally { try { - if (!_iteratorNormalCompletion10 && _iterator10.return) { - _iterator10.return(); + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); } } finally { - if (_didIteratorError10) { - throw _iteratorError10; + if (_didIteratorError4) { + throw _iteratorError4; } } } } } catch (err) { - _didIteratorError9 = true; - _iteratorError9 = err; + _didIteratorError3 = true; + _iteratorError3 = err; } finally { try { - if (!_iteratorNormalCompletion9 && _iterator9.return) { - _iterator9.return(); + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); } } finally { - if (_didIteratorError9) { - throw _iteratorError9; + if (_didIteratorError3) { + throw _iteratorError3; } } } @@ -822,16 +350,16 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons } } } catch (err) { - _didIteratorError8 = true; - _iteratorError8 = err; + _didIteratorError2 = true; + _iteratorError2 = err; } finally { try { - if (!_iteratorNormalCompletion8 && _iterator8.return) { - _iterator8.return(); + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); } } finally { - if (_didIteratorError8) { - throw _iteratorError8; + if (_didIteratorError2) { + throw _iteratorError2; } } } @@ -842,76 +370,18 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons }(); /** - * Recursively walk the composed tree from |node|. + * Adds a style element to the node containing the inert specific styles * @param {Node} node - * @param {(function (Element))=} callback Callback to be called for each element traversed, - * before descending into child nodes. - * @param {ShadowRoot=} shadowRootAncestor The nearest ShadowRoot ancestor, if any. */ - function composedTreeWalk(node, callback, shadowRootAncestor) { - if (node.nodeType == Node.ELEMENT_NODE) { - var element = /** @type {Element} */node; - if (callback) callback(element); - - // Descend into node: - // If it has a ShadowRoot, ignore all child elements - these will be picked - // up by the or elements. Descend straight into the - // ShadowRoot. - var shadowRoot = element.shadowRoot || element.webkitShadowRoot; - if (shadowRoot) { - composedTreeWalk(shadowRoot, callback, shadowRoot); - return; - } - - // If it is a element, descend into distributed elements - these - // are elements from outside the shadow root which are rendered inside the - // shadow DOM. - if (element.localName == 'content') { - var content = /** @type {HTMLContentElement} */element; - // Verifies if ShadowDom v0 is supported. - var distributedNodes = content.getDistributedNodes ? content.getDistributedNodes() : []; - for (var i = 0; i < distributedNodes.length; i++) { - composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); - } - return; - } - - // If it is a element, descend into assigned nodes - these - // are elements from outside the shadow root which are rendered inside the - // shadow DOM. - if (element.localName == 'slot') { - var slot = /** @type {HTMLSlotElement} */element; - // Verify if ShadowDom v1 is supported. - var _distributedNodes = slot.assignedNodes ? slot.assignedNodes({ flatten: true }) : []; - for (var _i = 0; _i < _distributedNodes.length; _i++) { - composedTreeWalk(_distributedNodes[_i], callback, shadowRootAncestor); - } - return; - } - } - - // If it is neither the parent of a ShadowRoot, a element, a - // element, nor a element recurse normally. - var child = node.firstChild; - while (child != null) { - composedTreeWalk(child, callback, shadowRootAncestor); - child = child.nextSibling; - } - } - - /** - * Adds a style element to the node containing the inert specific styles - * @param {Node} node - */ function addInertStyle(node) { if (node.querySelector('style#inert-style')) { return; } var style = document.createElement('style'); style.setAttribute('id', 'inert-style'); - style.textContent = "\n" + "[inert] {\n" + " pointer-events: none;\n" + " cursor: default;\n" + "}\n" + "\n" + "[inert], [inert] * {\n" + " user-select: none;\n" + " -webkit-user-select: none;\n" + " -moz-user-select: none;\n" + " -ms-user-select: none;\n" + "}\n"; + style.textContent = "\n" + "[inert], [inert] * {\n" + " pointer-events: none;\n" + " cursor: default;\n" + " user-select: none;\n" + " -webkit-user-select: none;\n" + " -moz-user-select: none;\n" + " -ms-user-select: none;\n" + "}\n"; node.appendChild(style); } @@ -926,4 +396,18 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons inertManager.setInert(this, inert); } }); + + var nativeFocus = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'focus'); + Object.defineProperty(HTMLElement.prototype, 'focus', { + enumerable: true, + configurable: true, + writable: true, + value: function value() { + // If it is inert or into an inert node, no focus! + var target = this; + while (!target.inert && (target = target.parentNode || target.host)) {} + if (target && target.inert) return; + return nativeFocus.value.call(this); + } + }); })(document); \ No newline at end of file diff --git a/dist/inert.min.js b/dist/inert.min.js index 77defd0..b4fa27a 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var r=0;r} - * All managed focusable nodes in this InertRoot's subtree. + * @param {Element} rootElement The Element at the root of the inert subtree. + * @param {InertManager} inertManager The global singleton InertManager object. */ - this._managedNodes = new Set([]); - - // Make the subtree hidden from assistive technology - this._rootElement.setAttribute('aria-hidden', 'true'); - - // Make all focusable elements in the subtree unfocusable and add them to _managedNodes - this._makeSubtreeUnfocusable(this._rootElement); - - // Watch for: - // - any additions in the subtree: make them unfocusable too - // - any removals from the subtree: remove them from this inert root's managed nodes - // - attribute changes: if `tabindex` is added, or removed from an intrinsically focusable element, - // make that node a managed node. - this._observer = new MutationObserver(this._onMutation.bind(this)); - this._observer.observe(this._rootElement, { attributes: true, childList: true, subtree: true }); - } - - /** - * Call this whenever this object is about to become obsolete. This unwinds all of the state - * stored in this object and updates the state of all of the managed nodes. - */ - destructor() { - this._observer.disconnect(); - this._observer = null; - - if (this._rootElement) - this._rootElement.removeAttribute('aria-hidden'); - this._rootElement = null; - - for (let inertNode of this._managedNodes) - this._unmanageNode(inertNode.node); - - this._managedNodes = null; - - this._inertManager = null; - } - - /** - * @return {Set} A copy of this InertRoot's managed nodes set. - */ - get managedNodes() { - return new Set(this._managedNodes); - } - - /** - * @param {Node} startNode - */ - _makeSubtreeUnfocusable(startNode) { - composedTreeWalk(startNode, (node) => { this._visitNode(node); }); - - let activeElement = document.activeElement; - if (!document.body.contains(startNode)) { - // startNode may be in shadow DOM, so find its nearest shadowRoot to get the activeElement. - let node = startNode; - let root = undefined; - while (node) { - if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - root = node; - break; + constructor(rootElement, inertManager) { + /** @type {Element} */ + this._rootElement = rootElement; + + /** @type {string} */ + this._rootTabindex = rootElement.getAttribute('tabindex') || null; + + // Make the subtree hidden from assistive technology + rootElement.setAttribute('aria-hidden', 'true'); + + // Make it untabbable. + rootElement.tabIndex = -1; + + // We can attach a shadowRoot if element has potential custom element name + // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name + // or is a native element that accepts shadow roots + // https://dom.spec.whatwg.org/#dom-element-attachshadow + const canAttachShadow = rootElement.localName.indexOf('-') !== -1 || rootElement.matches(acceptsShadowSel); + if (!canAttachShadow) return; + + // Force shadowRoot. + if (!rootElement.shadowRoot) { + rootElement.attachShadow({ + mode: 'open' + }).appendChild(document.createElement('slot')); + const nativeAttachShadow = rootElement.attachShadow; + rootElement.attachShadow = () => { + // Clear the slot we added. + this.shadowRoot.removeChild(rootElement.shadowRoot.firstChild); + this.attachShadow = nativeAttachShadow; + return this.shadowRoot; + }; + } else { + // Manager might have not "seen" these children since they're in a shadowRoot. + const inertChildren = rootElement.shadowRoot.querySelectorAll('[inert]'); + for (let i = 0, l = inertChildren.length; i < l; i++) { + inertManager.setInert(inertChildren[i], true); } - node = node.parentNode; } - if (root) - activeElement = root.activeElement - } - if (startNode.contains(activeElement)) - activeElement.blur(); - } - - /** - * @param {Node} node - */ - _visitNode(node) { - if (node.nodeType !== Node.ELEMENT_NODE) - return; - - // If a descendant inert root becomes un-inert, its descendants will still be inert because of this - // inert root, so all of its managed nodes need to be adopted by this InertRoot. - if (node !== this._rootElement && node.hasAttribute('inert')) - this._adoptInertRoot(node); - - if (node.matches(_focusableElementsString) || node.hasAttribute('tabindex')) - this._manageNode(node); - } - - /** - * Register the given node with this InertRoot and with InertManager. - * @param {Node} node - */ - _manageNode(node) { - const inertNode = this._inertManager.register(node, this); - this._managedNodes.add(inertNode); - } - - /** - * Unregister the given node with this InertRoot and with InertManager. - * @param {Node} node - */ - _unmanageNode(node) { - const inertNode = this._inertManager.deregister(node, this); - if (inertNode) - this._managedNodes.delete(inertNode); - } - - /** - * Unregister the entire subtree starting at `startNode`. - * @param {Node} startNode - */ - _unmanageSubtree(startNode) { - composedTreeWalk(startNode, (node) => { this._unmanageNode(node); }); - } - - /** - * If a descendant node is found with an `inert` attribute, adopt its managed nodes. - * @param {Node} node - */ - _adoptInertRoot(node) { - let inertSubroot = this._inertManager.getInertRoot(node); - // During initialisation this inert root may not have been registered yet, - // so register it now if need be. - if (!inertSubroot) { - this._inertManager.setInert(node, true); - inertSubroot = this._inertManager.getInertRoot(node); + // Ensure we move the focus outside of rootElement. + const active = rootElement.activeElement || document.activeElement; + if (active && rootElement.contains(active)) active.blur(); + + // Give the manager visibility on changing nodes in the shadowRoot. + this._observer = new MutationObserver(inertManager.watchForInert); + this._observer.observe(rootElement.shadowRoot, { + attributes: true, + subtree: true, + childList: true + }); } - for (let savedInertNode of inertSubroot.managedNodes) - this._manageNode(savedInertNode.node); - } - - /** - * Callback used when mutation observer detects subtree additions, removals, or attribute changes. - * @param {MutationRecord} records - * @param {MutationObserver} self - */ - _onMutation(records, self) { - for (let record of records) { - const target = record.target; - if (record.type === 'childList') { - // Manage added nodes - for (let node of Array.from(record.addedNodes)) - this._makeSubtreeUnfocusable(node); + /** + * Call this whenever this object is about to become obsolete. This unwinds all of the state + * stored in this object and updates the state of all of the managed nodes. + */ + destructor() { + if (this._observer) this._observer.disconnect(); - // Un-manage removed nodes - for (let node of Array.from(record.removedNodes)) - this._unmanageSubtree(node); - } else if (record.type === 'attributes') { - if (record.attributeName === 'tabindex') { - // Re-initialise inert node if tabindex changes - this._manageNode(target); - } else if (target !== this._rootElement && - record.attributeName === 'inert' && - target.hasAttribute('inert')) { - // If a new inert root is added, adopt its managed nodes and make sure it knows about the - // already managed nodes from this inert subroot. - this._adoptInertRoot(target); - const inertSubroot = this._inertManager.getInertRoot(target); - for (let managedNode of this._managedNodes) { - if (target.contains(managedNode.node)) - inertSubroot._manageNode(managedNode.node); - } - } + this._rootElement.removeAttribute('aria-hidden'); + if (this._rootTabindex) { + this._rootElement.setAttribute('tabindex', this._rootTabindex); + } else { + this._rootElement.removeAttribute('tabindex'); } + + this._observer = null; + this._rootElement = null; + this._rootTabindex = null; } } -} -/** - * `InertNode` initialises and manages a single inert node. - * A node is inert if it is a descendant of one or more inert root elements. - * - * On construction, `InertNode` saves the existing `tabindex` value for the node, if any, and - * either removes the `tabindex` attribute or sets it to `-1`, depending on whether the element - * is intrinsically focusable or not. - * - * `InertNode` maintains a set of `InertRoot`s which are descendants of this `InertNode`. When an - * `InertRoot` is destroyed, and calls `InertManager.deregister()`, the `InertManager` notifies the - * `InertNode` via `removeInertRoot()`, which in turn destroys the `InertNode` if no `InertRoot`s - * remain in the set. On destruction, `InertNode` reinstates the stored `tabindex` if one exists, - * or removes the `tabindex` attribute if the element is intrinsically focusable. - */ -class InertNode { /** - * @param {Node} node A focusable element to be made inert. - * @param {InertRoot} inertRoot The inert root element associated with this inert node. + * InertManager is a per-document singleton object which manages all inert roots and nodes. + * + * When an element becomes an inert root by having an `inert` attribute set and/or its `inert` + * property set to `true`, the `setInert` method creates an `InertRoot` object for the element. + * The `InertRoot` in turn registers itself as managing all of the element's focusable descendant + * nodes via the `register()` method. The `InertManager` ensures that a single `InertNode` instance + * is created for each such node, via the `_managedNodes` map. */ - constructor(node, inertRoot) { - /** @type {Node} */ - this._node = node; - - /** @type {boolean} */ - this._overrodeFocusMethod = false; - + class InertManager { /** - * @type {Set} The set of descendant inert roots. - * If and only if this set becomes empty, this node is no longer inert. + * @param {Document} document */ - this._inertRoots = new Set([inertRoot]); - - /** @type {boolean} */ - this._destroyed = false; - - // Save any prior tabindex info and make this node untabbable - this.ensureUntabbable(); - } - - /** - * Call this whenever this object is about to become obsolete. - * This makes the managed node focusable again and deletes all of the previously stored state. - */ - destructor() { - this._throwIfDestroyed(); - - if (this._node) { - if (this.hasSavedTabIndex) - this._node.setAttribute('tabindex', this.savedTabIndex); - else - this._node.removeAttribute('tabindex'); - - // Use `delete` to restore native focus method. - if (this._overrodeFocusMethod) - delete this._node.focus; + constructor(document) { + if (!document) + throw new Error('Missing required argument; InertManager needs to wrap a document.'); + + /** @type {Document} */ + this._document = document; + + /** + * All inert roots known to this InertManager. In a map to allow looking up by Node. + * @type {Map} + */ + this._inertRoots = new Map(); + + this.watchForInert = this._watchForInert.bind(this); + + /** + * Observer for mutations on `document.body`. + * @type {MutationObserver} + */ + this._observer = new MutationObserver(this.watchForInert); + + + // Add inert style. + addInertStyle(document.head || document.body || document.documentElement); + + // Wait for document to be interactive. + if (document.readyState === 'loading') { + const onchanged = () => { + document.removeEventListener('readystatechange', onchanged); + this._onDocumentLoaded(); + }; + document.addEventListener('readystatechange', onchanged); + } else { + this._onDocumentLoaded(); + } } - this._node = null; - this._inertRoots = null; - - this._destroyed = true; - } - - /** - * @type {boolean} Whether this object is obsolete because the managed node is no longer inert. - * If the object has been destroyed, any attempt to access it will cause an exception. - */ - get destroyed() { - return this._destroyed; - } - - _throwIfDestroyed() { - if (this.destroyed) - throw new Error("Trying to access destroyed InertNode"); - } - - /** @return {boolean} */ - get hasSavedTabIndex() { - return '_savedTabIndex' in this; - } - /** @return {Node} */ - get node() { - this._throwIfDestroyed(); - return this._node; - } - - /** @param {number} tabIndex */ - set savedTabIndex(tabIndex) { - this._throwIfDestroyed(); - this._savedTabIndex = tabIndex; - } - - /** @return {number} */ - get savedTabIndex() { - this._throwIfDestroyed(); - return this._savedTabIndex; - } - - /** Save the existing tabindex value and make the node untabbable and unfocusable */ - ensureUntabbable() { - const node = this.node; - if (node.matches(_focusableElementsString)) { - if (node.tabIndex === -1 && this.hasSavedTabIndex) + /** + * Set whether the given element should be an inert root or not. + * @param {Element} root + * @param {boolean} inert + */ + setInert(root, inert) { + if (this._inertRoots.has(root) === inert) // element is already inert return; - - if (node.hasAttribute('tabindex')) - this._savedTabIndex = node.tabIndex; - node.setAttribute('tabindex', '-1'); - if (node.nodeType === Node.ELEMENT_NODE) { - node.focus = function() {}; - this._overrodeFocusMethod = true; + if (inert) { + const inertRoot = new InertRoot(root, this); + root.setAttribute('inert', ''); + this._inertRoots.set(root, inertRoot); + // Ensure inert styles are added in shadowRoots. + if (root.shadowRoot) { + addInertStyle(root.shadowRoot); + } else if (!this._document.body.contains(root)) { + // If not contained in the document, it must be in a shadowRoot. + let parent = root.parentNode; + while (parent) { + if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + addInertStyle(parent); + } + parent = parent.parentNode; + } + } + } else { + const inertRoot = this._inertRoots.get(root); + inertRoot.destructor(); + this._inertRoots.delete(root); + root.removeAttribute('inert'); } - } else if (node.hasAttribute('tabindex')) { - this._savedTabIndex = node.tabIndex; - node.removeAttribute('tabindex'); } - } - - /** - * Add another inert root to this inert node's set of managing inert roots. - * @param {InertRoot} inertRoot - */ - addInertRoot(inertRoot) { - this._throwIfDestroyed(); - this._inertRoots.add(inertRoot); - } - - /** - * Remove the given inert root from this inert node's set of managing inert roots. - * If the set of managing inert roots becomes empty, this node is no longer inert, - * so the object should be destroyed. - * @param {InertRoot} inertRoot - */ - removeInertRoot(inertRoot) { - this._throwIfDestroyed(); - this._inertRoots.delete(inertRoot); - if (this._inertRoots.size === 0) - this.destructor(); - } -} - -/** - * InertManager is a per-document singleton object which manages all inert roots and nodes. - * - * When an element becomes an inert root by having an `inert` attribute set and/or its `inert` - * property set to `true`, the `setInert` method creates an `InertRoot` object for the element. - * The `InertRoot` in turn registers itself as managing all of the element's focusable descendant - * nodes via the `register()` method. The `InertManager` ensures that a single `InertNode` instance - * is created for each such node, via the `_managedNodes` map. - */ -class InertManager { - /** - * @param {Document} document - */ - constructor(document) { - if (!document) - throw new Error('Missing required argument; InertManager needs to wrap a document.'); - - /** @type {Document} */ - this._document = document; /** - * All managed nodes known to this InertManager. In a map to allow looking up by Node. - * @type {Map} + * Get the InertRoot object corresponding to the given inert root element, if any. + * @param {Element} element + * @return {InertRoot?} */ - this._managedNodes = new Map(); + getInertRoot(element) { + return this._inertRoots.get(element); + } /** - * All inert roots known to this InertManager. In a map to allow looking up by Node. - * @type {Map} + * Callback used when document has finished loading. */ - this._inertRoots = new Map(); + _onDocumentLoaded() { + // Find all inert roots in document and make them actually inert. + const inertElements = Array.from(this._document.querySelectorAll('[inert]')); + for (let inertElement of inertElements) + this.setInert(inertElement, true); + + // Comment this out to use programmatic API only. + this._observer.observe(this._document.body, { + attributes: true, + subtree: true, + childList: true + }); + } /** - * Observer for mutations on `document.body`. - * @type {MutationObserver} + * Callback used when mutation observer detects attribute changes. + * @param {MutationRecord} records + * @param {MutationObserver} self */ - this._observer = new MutationObserver(this._watchForInert.bind(this)); - - - // Add inert style. - addInertStyle(document.head || document.body || document.documentElement); - - // Wait for document to be loaded. - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', this._onDocumentLoaded.bind(this)); - } else { - this._onDocumentLoaded(); - } - } - - /** - * Set whether the given element should be an inert root or not. - * @param {Element} root - * @param {boolean} inert - */ - setInert(root, inert) { - if (inert) { - if (this._inertRoots.has(root)) // element is already inert - return; - - const inertRoot = new InertRoot(root, this); - root.setAttribute('inert', ''); - this._inertRoots.set(root, inertRoot); - // If not contained in the document, it must be in a shadowRoot. - // Ensure inert styles are added there. - if (!this._document.body.contains(root)) { - let parent = root.parentNode; - while (parent) { - if (parent.nodeType === 11) { - addInertStyle(parent); - } - parent = parent.parentNode; + _watchForInert(records, self) { + for (let record of records) { + switch (record.type) { + case 'childList': + for (let node of Array.from(record.addedNodes)) { + if (node.nodeType !== Node.ELEMENT_NODE) + continue; + const inertElements = Array.from(node.querySelectorAll('[inert]')); + if (node.matches('[inert]')) + inertElements.unshift(node); + for (let inertElement of inertElements) + this.setInert(inertElement, true); + } + break; + case 'attributes': + if (record.attributeName !== 'inert') + continue; + const target = record.target; + const inert = target.hasAttribute('inert'); + this.setInert(target, inert); + break; } } - } else { - if (!this._inertRoots.has(root)) // element is already non-inert - return; - - const inertRoot = this._inertRoots.get(root); - inertRoot.destructor(); - this._inertRoots.delete(root); - root.removeAttribute('inert'); - } - } - - /** - * Get the InertRoot object corresponding to the given inert root element, if any. - * @param {Element} element - * @return {InertRoot?} - */ - getInertRoot(element) { - return this._inertRoots.get(element); - } - - /** - * Register the given InertRoot as managing the given node. - * In the case where the node has a previously existing inert root, this inert root will - * be added to its set of inert roots. - * @param {Node} node - * @param {InertRoot} inertRoot - * @return {InertNode} inertNode - */ - register(node, inertRoot) { - let inertNode = this._managedNodes.get(node); - if (inertNode !== undefined) { // node was already in an inert subtree - inertNode.addInertRoot(inertRoot); - // Update saved tabindex value if necessary - inertNode.ensureUntabbable(); - } else { - inertNode = new InertNode(node, inertRoot); } - - this._managedNodes.set(node, inertNode); - - return inertNode; } /** - * De-register the given InertRoot as managing the given inert node. - * Removes the inert root from the InertNode's set of managing inert roots, and remove the inert - * node from the InertManager's set of managed nodes if it is destroyed. - * If the node is not currently managed, this is essentially a no-op. + * Adds a style element to the node containing the inert specific styles * @param {Node} node - * @param {InertRoot} inertRoot - * @return {InertNode?} The potentially destroyed InertNode associated with this node, if any. - */ - deregister(node, inertRoot) { - const inertNode = this._managedNodes.get(node); - if (!inertNode) - return null; - - inertNode.removeInertRoot(inertRoot); - if (inertNode.destroyed) - this._managedNodes.delete(node); - - return inertNode; - } - - /** - * Callback used when document has finished loading. - */ - _onDocumentLoaded() { - // Find all inert roots in document and make them actually inert. - const inertElements = Array.from(this._document.querySelectorAll('[inert]')); - for (let inertElement of inertElements) - this.setInert(inertElement, true); - - // Comment this out to use programmatic API only. - this._observer.observe(this._document.body, { attributes: true, subtree: true, childList: true }); - } - - /** - * Callback used when mutation observer detects attribute changes. - * @param {MutationRecord} records - * @param {MutationObserver} self */ - _watchForInert(records, self) { - for (let record of records) { - switch (record.type) { - case 'childList': - for (let node of Array.from(record.addedNodes)) { - if (node.nodeType !== Node.ELEMENT_NODE) - continue; - const inertElements = Array.from(node.querySelectorAll('[inert]')); - if (node.matches('[inert]')) - inertElements.unshift(node); - for (let inertElement of inertElements) - this.setInert(inertElement, true); - } - break; - case 'attributes': - if (record.attributeName !== 'inert') - continue; - const target = record.target; - const inert = target.hasAttribute('inert'); - this.setInert(target, inert); - break; - } + function addInertStyle(node) { + if (node.querySelector('style#inert-style')) { + return; } + const style = document.createElement('style'); + style.setAttribute('id', 'inert-style'); + style.textContent = "\n" + + "[inert], [inert] * {\n" + + " pointer-events: none;\n" + + " cursor: default;\n" + + " user-select: none;\n" + + " -webkit-user-select: none;\n" + + " -moz-user-select: none;\n" + + " -ms-user-select: none;\n" + + "}\n"; + node.appendChild(style); } -} - /** - * Recursively walk the composed tree from |node|. - * @param {Node} node - * @param {(function (Element))=} callback Callback to be called for each element traversed, - * before descending into child nodes. - * @param {ShadowRoot=} shadowRootAncestor The nearest ShadowRoot ancestor, if any. - */ -function composedTreeWalk(node, callback, shadowRootAncestor) { - if (node.nodeType == Node.ELEMENT_NODE) { - const element = /** @type {Element} */ (node); - if (callback) - callback(element) + const inertManager = new InertManager(document); - // Descend into node: - // If it has a ShadowRoot, ignore all child elements - these will be picked - // up by the or elements. Descend straight into the - // ShadowRoot. - const shadowRoot = element.shadowRoot || element.webkitShadowRoot; - if (shadowRoot) { - composedTreeWalk(shadowRoot, callback, shadowRoot); - return; + Object.defineProperty(Element.prototype, 'inert', { + enumerable: true, + get: function() { + return this.hasAttribute('inert'); + }, + set: function(inert) { + inertManager.setInert(this, inert) } - - // If it is a element, descend into distributed elements - these - // are elements from outside the shadow root which are rendered inside the - // shadow DOM. - if (element.localName == 'content') { - const content = /** @type {HTMLContentElement} */ (element); - // Verifies if ShadowDom v0 is supported. - const distributedNodes = content.getDistributedNodes ? - content.getDistributedNodes() : []; - for (let i = 0; i < distributedNodes.length; i++) { - composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); - } - return; - } - - // If it is a element, descend into assigned nodes - these - // are elements from outside the shadow root which are rendered inside the - // shadow DOM. - if (element.localName == 'slot') { - const slot = /** @type {HTMLSlotElement} */ (element); - // Verify if ShadowDom v1 is supported. - const distributedNodes = slot.assignedNodes ? - slot.assignedNodes({ flatten: true }) : []; - for (let i = 0; i < distributedNodes.length; i++) { - composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); - } - return; + }); + + const nativeFocus = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'focus'); + Object.defineProperty(HTMLElement.prototype, 'focus', { + enumerable: true, + configurable: true, + writable: true, + value: function() { + // If it is inert or into an inert node, no focus! + let target = this; + while (!target.inert && (target = target.parentNode || target.host)) {} + if (target && target.inert) return; + return nativeFocus.value.call(this); } - } - - // If it is neither the parent of a ShadowRoot, a element, a - // element, nor a element recurse normally. - let child = node.firstChild; - while (child != null) { - composedTreeWalk(child, callback, shadowRootAncestor); - child = child.nextSibling; - } -} - -/** - * Adds a style element to the node containing the inert specific styles - * @param {Node} node - */ -function addInertStyle(node) { - if (node.querySelector('style#inert-style')) { - return; - } - const style = document.createElement('style'); - style.setAttribute('id', 'inert-style'); - style.textContent = "\n"+ - "[inert] {\n" + - " pointer-events: none;\n" + - " cursor: default;\n" + - "}\n" + - "\n" + - "[inert], [inert] * {\n" + - " user-select: none;\n" + - " -webkit-user-select: none;\n" + - " -moz-user-select: none;\n" + - " -ms-user-select: none;\n" + - "}\n"; - node.appendChild(style); -} - -const inertManager = new InertManager(document); - -Object.defineProperty(Element.prototype, 'inert', { - enumerable: true, - get: function() { return this.hasAttribute('inert'); }, - set: function(inert) { inertManager.setInert(this, inert) } - }); + }); -})(document); +})(document); \ No newline at end of file diff --git a/test/index.js b/test/index.js index 1fc12da..0b6d49e 100644 --- a/test/index.js +++ b/test/index.js @@ -25,8 +25,6 @@ function isUnfocusable(el) { return false; if (document.activeElement === el) return false; - if (el.tabIndex !== -1) - return false; return true; } @@ -61,7 +59,6 @@ describe('Basic', function() { it('should make explicitly focusable child not focusable', function() { const div = document.querySelector('#fake-button'); - expect(div.hasAttribute('tabindex')).to.equal(false); expect(isUnfocusable(div)).to.equal(true); }); @@ -124,54 +121,6 @@ describe('Basic', function() { }); }); - describe('ShadowDOM v0', function() { - if (!Element.prototype.createShadowRoot) { - console.log('ShadowDOM v0 is not supported by the browser.'); - return; - } - let fixture, host; - - beforeEach(function() { - fixture = document.querySelector('#fixture'); - fixture.inert = false; - host = document.createElement('div'); - fixture.appendChild(host); - host.createShadowRoot(); - }); - - afterEach(function() { - fixture.removeChild(host); - }); - - it('should apply inside shadow trees', function() { - const shadowButton = document.createElement('button'); - shadowButton.textContent = 'Shadow button'; - host.shadowRoot.appendChild(shadowButton); - shadowButton.focus(); - fixture.inert = true; - expect(isUnfocusable(shadowButton)).to.equal(true); - }); - - it('should apply inert styles inside shadow trees', function() { - const shadowButton = document.createElement('button'); - shadowButton.textContent = 'Shadow button'; - host.shadowRoot.appendChild(shadowButton); - shadowButton.focus(); - shadowButton.inert = true; - expect(getComputedStyle(shadowButton).pointerEvents).to.equal('none'); - }); - - it('should apply inside shadow trees distributed content', function() { - host.shadowRoot.appendChild(document.createElement('content')); - const distributedButton = document.createElement('button'); - distributedButton.textContent = 'Distributed button'; - host.appendChild(distributedButton); - distributedButton.focus(); - fixture.inert = true; - expect(isUnfocusable(distributedButton)).to.equal(true); - }); - }); - describe('ShadowDOM v1', function() { if (!Element.prototype.attachShadow) { console.log('ShadowDOM v1 is not supported by the browser.'); @@ -208,10 +157,7 @@ describe('Basic', function() { host.shadowRoot.appendChild(shadowButton); shadowButton.focus(); shadowButton.inert = true; - Promise.resolve().then(() => { - expect(getComputedStyle(shadowButton).pointerEvents).to.equal('none'); - done(); - }); + expect(getComputedStyle(shadowButton).pointerEvents).to.equal('none'); }); it('should apply inside shadow trees distributed content', function() { From fe5f4562e57f3134e3d134c6bbb10ff3c27f279c Mon Sep 17 00:00:00 2001 From: valdrinkoshi Date: Thu, 23 Mar 2017 13:24:41 -0700 Subject: [PATCH 02/12] add tests for attachShadow called by user --- dist/inert.js | 24 +++++++++++------------- dist/inert.min.js | 2 +- src/inert.js | 18 ++++++++++-------- test/index.js | 15 +++++++++++++++ 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index a9952ab..33b7c1e 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -23,8 +23,9 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons (function (document) { + // https://dom.spec.whatwg.org/#dom-element-attachshadow /** @type {string} */ - var acceptsShadowSel = ['article', 'aside', 'blockquote', 'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'main', 'nav', 'p', 'section', 'span'].join(','); + var acceptsShadowRootSelector = ['article', 'aside', 'blockquote', 'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'main', 'nav', 'p', 'section', 'span'].join(','); /** * `InertRoot` manages a single inert subtree, i.e. a ShadowDOM subtree whose root element has an `inert` @@ -43,8 +44,6 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons * @param {InertManager} inertManager The global singleton InertManager object. */ function InertRoot(rootElement, inertManager) { - var _this = this; - _classCallCheck(this, InertRoot); /** @type {Element} */ @@ -59,12 +58,10 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons // Make it untabbable. rootElement.tabIndex = -1; - // We can attach a shadowRoot if element has potential custom element name + // We can attach a shadowRoot if supported, is a native element that accepts shadowRoot + // or if element has potential custom element name. // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name - // or is a native element that accepts shadow roots - // https://dom.spec.whatwg.org/#dom-element-attachshadow - var canAttachShadow = rootElement.localName.indexOf('-') !== -1 || rootElement.matches(acceptsShadowSel); - if (!canAttachShadow) return; + if (!rootElement.attachShadow || rootElement.matches(acceptsShadowRootSelector) || rootElement.localName.indexOf('-') !== -1) return; // Force shadowRoot. if (!rootElement.shadowRoot) { @@ -74,9 +71,10 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons var nativeAttachShadow = rootElement.attachShadow; rootElement.attachShadow = function () { // Clear the slot we added. - _this.shadowRoot.removeChild(rootElement.shadowRoot.firstChild); - _this.attachShadow = nativeAttachShadow; - return _this.shadowRoot; + var slot = this.shadowRoot.querySelector('slot'); + slot && this.shadowRoot.removeChild(slot); + this.attachShadow = nativeAttachShadow; + return this.shadowRoot; }; } else { // Manager might have not "seen" these children since they're in a shadowRoot. @@ -142,7 +140,7 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons * @param {Document} document */ function InertManager(document) { - var _this2 = this; + var _this = this; _classCallCheck(this, InertManager); @@ -172,7 +170,7 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons if (document.readyState === 'loading') { var onchanged = function onchanged() { document.removeEventListener('readystatechange', onchanged); - _this2._onDocumentLoaded(); + _this._onDocumentLoaded(); }; document.addEventListener('readystatechange', onchanged); } else { diff --git a/dist/inert.min.js b/dist/inert.min.js index b4fa27a..ab2b0f1 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var r=0;r { + rootElement.attachShadow = function() { // Clear the slot we added. - this.shadowRoot.removeChild(rootElement.shadowRoot.firstChild); + let slot = this.shadowRoot.querySelector('slot'); + slot && this.shadowRoot.removeChild(slot); this.attachShadow = nativeAttachShadow; return this.shadowRoot; }; diff --git a/test/index.js b/test/index.js index 0b6d49e..381f56c 100644 --- a/test/index.js +++ b/test/index.js @@ -169,6 +169,21 @@ describe('Basic', function() { fixture.inert = true; expect(isUnfocusable(distributedButton)).to.equal(true); }); + + it('should allow to call attachShadow', function() { + fixture.inert = true; + const input = document.createElement('button'); + fixture.attachShadow({mode:'open'}).appendChild(input); + expect(fixture.shadowRoot.querySelector('slot')).to.equal(null); + expect(input.parentNode).to.equal(fixture.shadowRoot); + expect(isUnfocusable(input)).to.equal(true); + }); + + it('should throw error when calling attachShadow twice', function() { + fixture.inert = true; + fixture.attachShadow({mode:'open'}); + expect(() => fixture.attachShadow({mode:'open'})).to.throw(); + }); }); }); From 47de05823943f50c146fe15a64bb87e0ce24b6cf Mon Sep 17 00:00:00 2001 From: valdrinkoshi Date: Thu, 23 Mar 2017 13:46:32 -0700 Subject: [PATCH 03/12] correct check --- dist/inert.js | 3 ++- dist/inert.min.js | 2 +- src/inert.js | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index 33b7c1e..f15b304 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -61,7 +61,8 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons // We can attach a shadowRoot if supported, is a native element that accepts shadowRoot // or if element has potential custom element name. // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name - if (!rootElement.attachShadow || rootElement.matches(acceptsShadowRootSelector) || rootElement.localName.indexOf('-') !== -1) return; + var canAttachShadow = rootElement.attachShadow && rootElement.matches(acceptsShadowRootSelector) || rootElement.localName.indexOf('-') !== -1; + if (!canAttachShadow) return; // Force shadowRoot. if (!rootElement.shadowRoot) { diff --git a/dist/inert.min.js b/dist/inert.min.js index ab2b0f1..17b08c1 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var r=0;r Date: Thu, 23 Mar 2017 14:05:36 -0700 Subject: [PATCH 04/12] break at first found shadowRoot --- dist/inert.js | 1 + dist/inert.min.js | 2 +- src/inert.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dist/inert.js b/dist/inert.js index f15b304..b237d7c 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -204,6 +204,7 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons while (parent) { if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { addInertStyle(parent); + break; } parent = parent.parentNode; } diff --git a/dist/inert.min.js b/dist/inert.min.js index 17b08c1..e16694d 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var r=0;r Date: Thu, 23 Mar 2017 14:13:31 -0700 Subject: [PATCH 05/12] global bool for nativeShadowDOM --- dist/inert.js | 4 +++- dist/inert.min.js | 2 +- src/inert.js | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index b237d7c..d848f9c 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -22,6 +22,8 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons */ (function (document) { + /** @type {boolean} */ + var nativeShadowDOM = 'attachShadow' in Element.prototype; // https://dom.spec.whatwg.org/#dom-element-attachshadow /** @type {string} */ @@ -61,7 +63,7 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons // We can attach a shadowRoot if supported, is a native element that accepts shadowRoot // or if element has potential custom element name. // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name - var canAttachShadow = rootElement.attachShadow && rootElement.matches(acceptsShadowRootSelector) || rootElement.localName.indexOf('-') !== -1; + var canAttachShadow = nativeShadowDOM && rootElement.matches(acceptsShadowRootSelector) || rootElement.localName.indexOf('-') !== -1; if (!canAttachShadow) return; // Force shadowRoot. diff --git a/dist/inert.min.js b/dist/inert.min.js index e16694d..815105a 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var r=0;r Date: Fri, 24 Mar 2017 15:08:07 -0700 Subject: [PATCH 06/12] blur activeElement contained in shadowRoot --- dist/inert.js | 32 +++++++++++++++++++++++++++----- dist/inert.min.js | 2 +- src/inert.js | 31 ++++++++++++++++++++++++++----- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index d848f9c..20dbcf5 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -60,6 +60,26 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons // Make it untabbable. rootElement.tabIndex = -1; + // Ensure we move the focus away from rootElement. + // This will blur also focused elements contained + // in the rootElement's shadowRoot. + rootElement.blur(); + // If rootElement has distributed content, it might + // be that the active element is contained into it. + // We must blur it. + if (rootElement.firstElementChild) { + var active = document.activeElement; + if (active === document.body) active = null; + while (active) { + if (rootElement.contains(active)) { + active.blur(); + break; + } + // Keep searching in the shadowRoot. + active = active.shadowRoot ? active.shadowRoot.activeElement : null; + } + } + // We can attach a shadowRoot if supported, is a native element that accepts shadowRoot // or if element has potential custom element name. // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name @@ -87,10 +107,6 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons } } - // Ensure we move the focus outside of rootElement. - var active = rootElement.activeElement || document.activeElement; - if (active && rootElement.contains(active)) active.blur(); - // Give the manager visibility on changing nodes in the shadowRoot. this._observer = new MutationObserver(inertManager.watchForInert); this._observer.observe(rootElement.shadowRoot, { @@ -407,7 +423,13 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons value: function value() { // If it is inert or into an inert node, no focus! var target = this; - while (!target.inert && (target = target.parentNode || target.host)) {} + while (target && !target.inert) { + // Target might be distributed, so go to the deepest assignedSlot + // and walk up the tree from there. + while (target.assignedSlot) { + target = target.assignedSlot; + }target = target.parentNode || target.host; + } if (target && target.inert) return; return nativeFocus.value.call(this); } diff --git a/dist/inert.min.js b/dist/inert.min.js index 815105a..a510dce 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var r=0;r Date: Fri, 24 Mar 2017 15:50:49 -0700 Subject: [PATCH 07/12] add styles to shadowRoots. Fix tests --- dist/inert.js | 7 ++----- dist/inert.min.js | 2 +- src/inert.js | 7 ++----- test/index.js | 3 ++- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index 20dbcf5..e4f7c11 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -213,11 +213,8 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons var inertRoot = new InertRoot(root, this); root.setAttribute('inert', ''); this._inertRoots.set(root, inertRoot); - // Ensure inert styles are added in shadowRoots. - if (root.shadowRoot) { - addInertStyle(root.shadowRoot); - } else if (!this._document.body.contains(root)) { - // If not contained in the document, it must be in a shadowRoot. + // If not contained in the document, it must be in a shadowRoot. + if (!this._document.body.contains(root)) { var parent = root.parentNode; while (parent) { if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { diff --git a/dist/inert.min.js b/dist/inert.min.js index a510dce..ae44a25 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var r=0;r Date: Tue, 28 Mar 2017 13:42:51 -0700 Subject: [PATCH 08/12] remove getInertRoot --- dist/inert.js | 12 ------------ dist/inert.min.js | 2 +- src/inert.js | 11 +---------- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index e4f7c11..69c5a87 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -232,18 +232,6 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons } } - /** - * Get the InertRoot object corresponding to the given inert root element, if any. - * @param {Element} element - * @return {InertRoot?} - */ - - }, { - key: 'getInertRoot', - value: function getInertRoot(element) { - return this._inertRoots.get(element); - } - /** * Callback used when document has finished loading. */ diff --git a/dist/inert.min.js b/dist/inert.min.js index ae44a25..bca6ce6 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var r=0;r Date: Fri, 14 Apr 2017 16:06:59 -0700 Subject: [PATCH 09/12] lint and build --- dist/inert.js | 838 +++++++++------------------------------------- dist/inert.min.js | 2 +- src/inert.js | 51 ++- 3 files changed, 190 insertions(+), 701 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index b631d5b..6541ec9 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -94,24 +94,22 @@ var createClass = function () { */ (function (document) { + /** @type {boolean} */ + var nativeShadowDOM = 'attachShadow' in Element.prototype; + + // https://dom.spec.whatwg.org/#dom-element-attachshadow /** @type {string} */ - var _focusableElementsString = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]'].join(','); + var acceptsShadowRootSelector = ['article', 'aside', 'blockquote', 'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'main', 'nav', 'p', 'section', 'span'].join(','); /** - * `InertRoot` manages a single inert subtree, i.e. a DOM subtree whose root element has an `inert` - * attribute. + * `InertRoot` manages a single inert subtree, i.e. a DOM subtree whose root element + * has an `inert` attribute. * * Its main functions are: * - * - to create and maintain a set of managed `InertNode`s, including when mutations occur in the - * subtree. The `makeSubtreeUnfocusable()` method handles collecting `InertNode`s via registering - * each focusable node in the subtree with the singleton `InertManager` which manages all known - * focusable nodes within inert subtrees. `InertManager` ensures that a single `InertNode` - * instance exists for each focusable node which has at least one inert root as an ancestor. + * - make the rootElement untabbable. * - * - to notify all managed `InertNode`s when this subtree stops being inert (i.e. when the `inert` - * attribute is removed from the root node). This is handled in the destructor, which calls the - * `deregister` method on `InertManager` for each managed inert node. + * - notify the manager of inerted nodes in the rootElement's shadowRoot. */ var InertRoot = function () { @@ -122,509 +120,98 @@ var createClass = function () { function InertRoot(rootElement, inertManager) { classCallCheck(this, InertRoot); - /** @type {InertManager} */ - this._inertManager = inertManager; - /** @type {Element} */ this._rootElement = rootElement; - /** - * @type {Set} - * All managed focusable nodes in this InertRoot's subtree. - */ - this._managedNodes = new Set([]); + /** @type {string} */ + this._rootTabindex = rootElement.getAttribute('tabindex') || null; // Make the subtree hidden from assistive technology - this._rootElement.setAttribute('aria-hidden', 'true'); - - // Make all focusable elements in the subtree unfocusable and add them to _managedNodes - this._makeSubtreeUnfocusable(this._rootElement); - - // Watch for: - // - any additions in the subtree: make them unfocusable too - // - any removals from the subtree: remove them from this inert root's managed nodes - // - attribute changes: if `tabindex` is added, or removed from an intrinsically focusable - // element, make that node a managed node. - this._observer = new MutationObserver(this._onMutation.bind(this)); - this._observer.observe(this._rootElement, { attributes: true, childList: true, subtree: true }); - } - - /** - * Call this whenever this object is about to become obsolete. This unwinds all of the state - * stored in this object and updates the state of all of the managed nodes. - */ - - - createClass(InertRoot, [{ - key: 'destructor', - value: function destructor() { - this._observer.disconnect(); - this._observer = null; - - if (this._rootElement) this._rootElement.removeAttribute('aria-hidden'); - this._rootElement = null; - - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = this._managedNodes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var inertNode = _step.value; - - this._unmanageNode(inertNode.node); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - this._managedNodes = null; - - this._inertManager = null; - } - - /** - * @return {Set} A copy of this InertRoot's managed nodes set. - */ - - }, { - key: '_makeSubtreeUnfocusable', - - - /** - * @param {Node} startNode - */ - value: function _makeSubtreeUnfocusable(startNode) { - var _this = this; - - composedTreeWalk(startNode, function (node) { - return _this._visitNode(node); - }); - - var activeElement = document.activeElement; - if (!document.body.contains(startNode)) { - // startNode may be in shadow DOM, so find its nearest shadowRoot to get the activeElement. - var node = startNode; - var root = undefined; - while (node) { - if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - root = node; - break; - } - node = node.parentNode; - } - if (root) activeElement = root.activeElement; - } - if (startNode.contains(activeElement)) activeElement.blur(); - } - - /** - * @param {Node} node - */ - - }, { - key: '_visitNode', - value: function _visitNode(node) { - if (node.nodeType !== Node.ELEMENT_NODE) return; - - // If a descendant inert root becomes un-inert, its descendants will still be inert because of - // this inert root, so all of its managed nodes need to be adopted by this InertRoot. - if (node !== this._rootElement && node.hasAttribute('inert')) this._adoptInertRoot(node); - - if (index(node, _focusableElementsString) || node.hasAttribute('tabindex')) this._manageNode(node); - } - - /** - * Register the given node with this InertRoot and with InertManager. - * @param {Node} node - */ - - }, { - key: '_manageNode', - value: function _manageNode(node) { - var inertNode = this._inertManager.register(node, this); - this._managedNodes.add(inertNode); - } - - /** - * Unregister the given node with this InertRoot and with InertManager. - * @param {Node} node - */ - - }, { - key: '_unmanageNode', - value: function _unmanageNode(node) { - var inertNode = this._inertManager.deregister(node, this); - if (inertNode) this._managedNodes.delete(inertNode); - } - - /** - * Unregister the entire subtree starting at `startNode`. - * @param {Node} startNode - */ - - }, { - key: '_unmanageSubtree', - value: function _unmanageSubtree(startNode) { - var _this2 = this; - - composedTreeWalk(startNode, function (node) { - return _this2._unmanageNode(node); - }); - } - - /** - * If a descendant node is found with an `inert` attribute, adopt its managed nodes. - * @param {Node} node - */ - - }, { - key: '_adoptInertRoot', - value: function _adoptInertRoot(node) { - var inertSubroot = this._inertManager.getInertRoot(node); - - // During initialisation this inert root may not have been registered yet, - // so register it now if need be. - if (!inertSubroot) { - this._inertManager.setInert(node, true); - inertSubroot = this._inertManager.getInertRoot(node); - } - - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = inertSubroot.managedNodes[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var savedInertNode = _step2.value; - - this._manageNode(savedInertNode.node); - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } + rootElement.setAttribute('aria-hidden', 'true'); + + // Make it untabbable. + rootElement.tabIndex = -1; + + // Ensure we move the focus away from rootElement. + // This will blur also focused elements contained + // in the rootElement's shadowRoot. + rootElement.blur(); + // If rootElement has distributed content, it might + // be that the active element is contained into it. + // We must blur it. + if (rootElement.firstElementChild) { + var active = document.activeElement; + if (active === document.body) active = null; + while (active) { + if (rootElement.contains(active)) { + active.blur(); + break; } + // Keep searching in the shadowRoot. + active = active.shadowRoot ? active.shadowRoot.activeElement : null; } } - /** - * Callback used when mutation observer detects subtree additions, removals, or attribute changes. - * @param {MutationRecord} records - * @param {MutationObserver} self - */ - - }, { - key: '_onMutation', - value: function _onMutation(records, self) { - var _iteratorNormalCompletion3 = true; - var _didIteratorError3 = false; - var _iteratorError3 = undefined; - - try { - for (var _iterator3 = records[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { - var record = _step3.value; - - var target = record.target; - if (record.type === 'childList') { - // Manage added nodes - var _iteratorNormalCompletion4 = true; - var _didIteratorError4 = false; - var _iteratorError4 = undefined; - - try { - for (var _iterator4 = Array.from(record.addedNodes)[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { - var node = _step4.value; - - this._makeSubtreeUnfocusable(node); - } // Un-manage removed nodes - } catch (err) { - _didIteratorError4 = true; - _iteratorError4 = err; - } finally { - try { - if (!_iteratorNormalCompletion4 && _iterator4.return) { - _iterator4.return(); - } - } finally { - if (_didIteratorError4) { - throw _iteratorError4; - } - } - } - - var _iteratorNormalCompletion5 = true; - var _didIteratorError5 = false; - var _iteratorError5 = undefined; - - try { - for (var _iterator5 = Array.from(record.removedNodes)[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { - var _node = _step5.value; - - this._unmanageSubtree(_node); - } - } catch (err) { - _didIteratorError5 = true; - _iteratorError5 = err; - } finally { - try { - if (!_iteratorNormalCompletion5 && _iterator5.return) { - _iterator5.return(); - } - } finally { - if (_didIteratorError5) { - throw _iteratorError5; - } - } - } - } else if (record.type === 'attributes') { - if (record.attributeName === 'tabindex') { - // Re-initialise inert node if tabindex changes - this._manageNode(target); - } else if (target !== this._rootElement && record.attributeName === 'inert' && target.hasAttribute('inert')) { - // If a new inert root is added, adopt its managed nodes and make sure it knows about the - // already managed nodes from this inert subroot. - this._adoptInertRoot(target); - var inertSubroot = this._inertManager.getInertRoot(target); - var _iteratorNormalCompletion6 = true; - var _didIteratorError6 = false; - var _iteratorError6 = undefined; - - try { - for (var _iterator6 = this._managedNodes[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { - var managedNode = _step6.value; - - if (target.contains(managedNode.node)) inertSubroot._manageNode(managedNode.node); - } - } catch (err) { - _didIteratorError6 = true; - _iteratorError6 = err; - } finally { - try { - if (!_iteratorNormalCompletion6 && _iterator6.return) { - _iterator6.return(); - } - } finally { - if (_didIteratorError6) { - throw _iteratorError6; - } - } - } - } - } - } - } catch (err) { - _didIteratorError3 = true; - _iteratorError3 = err; - } finally { - try { - if (!_iteratorNormalCompletion3 && _iterator3.return) { - _iterator3.return(); - } - } finally { - if (_didIteratorError3) { - throw _iteratorError3; - } - } + // We can attach a shadowRoot if supported, is a native element that accepts shadowRoot + // or if element has potential custom element name. + // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name + var canAttachShadow = nativeShadowDOM && index(rootElement, acceptsShadowRootSelector) || rootElement.localName.indexOf('-') !== -1; + if (!canAttachShadow) return; + + // Force shadowRoot. + if (!rootElement.shadowRoot) { + rootElement.attachShadow({ + mode: 'open' + }).appendChild(document.createElement('slot')); + var nativeAttachShadow = rootElement.attachShadow; + rootElement.attachShadow = function () { + // Clear the slot we added. + var slot = this.shadowRoot.querySelector('slot'); + slot && this.shadowRoot.removeChild(slot); + this.attachShadow = nativeAttachShadow; + return this.shadowRoot; + }; + } else { + // Manager might have not "seen" these children since they're in a shadowRoot. + var inertChildren = rootElement.shadowRoot.querySelectorAll('[inert]'); + for (var i = 0, l = inertChildren.length; i < l; i++) { + inertManager.setInert(inertChildren[i], true); } } - }, { - key: 'managedNodes', - get: function get$$1() { - return new Set(this._managedNodes); - } - }]); - return InertRoot; - }(); - - /** - * `InertNode` initialises and manages a single inert node. - * A node is inert if it is a descendant of one or more inert root elements. - * - * On construction, `InertNode` saves the existing `tabindex` value for the node, if any, and - * either removes the `tabindex` attribute or sets it to `-1`, depending on whether the element - * is intrinsically focusable or not. - * - * `InertNode` maintains a set of `InertRoot`s which are descendants of this `InertNode`. When an - * `InertRoot` is destroyed, and calls `InertManager.deregister()`, the `InertManager` notifies the - * `InertNode` via `removeInertRoot()`, which in turn destroys the `InertNode` if no `InertRoot`s - * remain in the set. On destruction, `InertNode` reinstates the stored `tabindex` if one exists, - * or removes the `tabindex` attribute if the element is intrinsically focusable. - */ - - - var InertNode = function () { - /** - * @param {Node} node A focusable element to be made inert. - * @param {InertRoot} inertRoot The inert root element associated with this inert node. - */ - function InertNode(node, inertRoot) { - classCallCheck(this, InertNode); - /** @type {Node} */ - this._node = node; - - /** @type {boolean} */ - this._overrodeFocusMethod = false; - - /** - * @type {Set} The set of descendant inert roots. - * If and only if this set becomes empty, this node is no longer inert. - */ - this._inertRoots = new Set([inertRoot]); - - /** @type {boolean} */ - this._destroyed = false; - - // Save any prior tabindex info and make this node untabbable - this.ensureUntabbable(); + // Give the manager visibility on changing nodes in the shadowRoot. + this._observer = new MutationObserver(inertManager.watchForInert); + this._observer.observe(rootElement.shadowRoot, { + attributes: true, + subtree: true, + childList: true + }); } /** - * Call this whenever this object is about to become obsolete. - * This makes the managed node focusable again and deletes all of the previously stored state. + * Call this whenever this object is about to become obsolete. This unwinds all of the state + * stored in this object and updates the state of all of the managed nodes. */ - createClass(InertNode, [{ + createClass(InertRoot, [{ key: 'destructor', value: function destructor() { - this._throwIfDestroyed(); - - if (this._node) { - if (this.hasSavedTabIndex) this._node.setAttribute('tabindex', this.savedTabIndex);else this._node.removeAttribute('tabindex'); - - // Use `delete` to restore native focus method. - if (this._overrodeFocusMethod) delete this._node.focus; - } - this._node = null; - this._inertRoots = null; - - this._destroyed = true; - } - - /** - * @type {boolean} Whether this object is obsolete because the managed node is no longer inert. - * If the object has been destroyed, any attempt to access it will cause an exception. - */ - - }, { - key: '_throwIfDestroyed', + if (this._observer) this._observer.disconnect(); - - /** - * Throw if user tries to access destroyed InertNode. - */ - value: function _throwIfDestroyed() { - if (this.destroyed) throw new Error('Trying to access destroyed InertNode'); - } - - /** @return {boolean} */ - - }, { - key: 'ensureUntabbable', - - - /** Save the existing tabindex value and make the node untabbable and unfocusable */ - value: function ensureUntabbable() { - var node = this.node; - if (index(node, _focusableElementsString)) { - if (node.tabIndex === -1 && this.hasSavedTabIndex) return; - - if (node.hasAttribute('tabindex')) this._savedTabIndex = node.tabIndex; - node.setAttribute('tabindex', '-1'); - if (node.nodeType === Node.ELEMENT_NODE) { - node.focus = function () {}; - this._overrodeFocusMethod = true; - } - } else if (node.hasAttribute('tabindex')) { - this._savedTabIndex = node.tabIndex; - node.removeAttribute('tabindex'); + this._rootElement.removeAttribute('aria-hidden'); + if (this._rootTabindex) { + this._rootElement.setAttribute('tabindex', this._rootTabindex); + } else { + this._rootElement.removeAttribute('tabindex'); } - } - - /** - * Add another inert root to this inert node's set of managing inert roots. - * @param {InertRoot} inertRoot - */ - - }, { - key: 'addInertRoot', - value: function addInertRoot(inertRoot) { - this._throwIfDestroyed(); - this._inertRoots.add(inertRoot); - } - /** - * Remove the given inert root from this inert node's set of managing inert roots. - * If the set of managing inert roots becomes empty, this node is no longer inert, - * so the object should be destroyed. - * @param {InertRoot} inertRoot - */ - - }, { - key: 'removeInertRoot', - value: function removeInertRoot(inertRoot) { - this._throwIfDestroyed(); - this._inertRoots.delete(inertRoot); - if (this._inertRoots.size === 0) this.destructor(); - } - }, { - key: 'destroyed', - get: function get$$1() { - return this._destroyed; - } - }, { - key: 'hasSavedTabIndex', - get: function get$$1() { - return '_savedTabIndex' in this; - } - - /** @return {Node} */ - - }, { - key: 'node', - get: function get$$1() { - this._throwIfDestroyed(); - return this._node; - } - - /** @param {number} tabIndex */ - - }, { - key: 'savedTabIndex', - set: function set$$1(tabIndex) { - this._throwIfDestroyed(); - this._savedTabIndex = tabIndex; - } - - /** @return {number} */ - , - get: function get$$1() { - this._throwIfDestroyed(); - return this._savedTabIndex; + this._observer = null; + this._rootElement = null; + this._rootTabindex = null; } }]); - return InertNode; + return InertRoot; }(); /** @@ -633,8 +220,8 @@ var createClass = function () { * When an element becomes an inert root by having an `inert` attribute set and/or its `inert` * property set to `true`, the `setInert` method creates an `InertRoot` object for the element. * The `InertRoot` in turn registers itself as managing all of the element's focusable descendant - * nodes via the `register()` method. The `InertManager` ensures that a single `InertNode` instance - * is created for each such node, via the `_managedNodes` map. + * nodes via the `register()` method. The `InertManager` ensures that a single `InertNode` + * instance is created for each such node, via the `_managedNodes` map. */ @@ -643,6 +230,8 @@ var createClass = function () { * @param {Document} document */ function InertManager(document) { + var _this = this; + classCallCheck(this, InertManager); if (!document) throw new Error('Missing required argument; InertManager needs to wrap a document.'); @@ -650,30 +239,30 @@ var createClass = function () { /** @type {Document} */ this._document = document; - /** - * All managed nodes known to this InertManager. In a map to allow looking up by Node. - * @type {Map} - */ - this._managedNodes = new Map(); - /** * All inert roots known to this InertManager. In a map to allow looking up by Node. * @type {Map} */ this._inertRoots = new Map(); + this.watchForInert = this._watchForInert.bind(this); + /** * Observer for mutations on `document.body`. * @type {MutationObserver} */ - this._observer = new MutationObserver(this._watchForInert.bind(this)); + this._observer = new MutationObserver(this.watchForInert); // Add inert style. addInertStyle(document.head || document.body || document.documentElement); - // Wait for document to be loaded. + // Wait for document to be interactive. if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', this._onDocumentLoaded.bind(this)); + var onchanged = function onchanged() { + document.removeEventListener('readystatechange', onchanged); + _this._onDocumentLoaded(); + }; + document.addEventListener('readystatechange', onchanged); } else { this._onDocumentLoaded(); } @@ -689,28 +278,24 @@ var createClass = function () { createClass(InertManager, [{ key: 'setInert', value: function setInert(root, inert) { + if (this._inertRoots.has(root) === inert) // element is already inert + return; if (inert) { - if (this._inertRoots.has(root)) // element is already inert - return; - var inertRoot = new InertRoot(root, this); root.setAttribute('inert', ''); this._inertRoots.set(root, inertRoot); // If not contained in the document, it must be in a shadowRoot. - // Ensure inert styles are added there. if (!this._document.body.contains(root)) { var parent = root.parentNode; while (parent) { - if (parent.nodeType === 11) { + if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { addInertStyle(parent); + break; } parent = parent.parentNode; } } } else { - if (!this._inertRoots.has(root)) // element is already non-inert - return; - var _inertRoot = this._inertRoots.get(root); _inertRoot.destructor(); this._inertRoots.delete(root); @@ -718,67 +303,6 @@ var createClass = function () { } } - /** - * Get the InertRoot object corresponding to the given inert root element, if any. - * @param {Element} element - * @return {InertRoot?} - */ - - }, { - key: 'getInertRoot', - value: function getInertRoot(element) { - return this._inertRoots.get(element); - } - - /** - * Register the given InertRoot as managing the given node. - * In the case where the node has a previously existing inert root, this inert root will - * be added to its set of inert roots. - * @param {Node} node - * @param {InertRoot} inertRoot - * @return {InertNode} inertNode - */ - - }, { - key: 'register', - value: function register(node, inertRoot) { - var inertNode = this._managedNodes.get(node); - if (inertNode !== undefined) { - // node was already in an inert subtree - inertNode.addInertRoot(inertRoot); - // Update saved tabindex value if necessary - inertNode.ensureUntabbable(); - } else { - inertNode = new InertNode(node, inertRoot); - } - - this._managedNodes.set(node, inertNode); - - return inertNode; - } - - /** - * De-register the given InertRoot as managing the given inert node. - * Removes the inert root from the InertNode's set of managing inert roots, and remove the inert - * node from the InertManager's set of managed nodes if it is destroyed. - * If the node is not currently managed, this is essentially a no-op. - * @param {Node} node - * @param {InertRoot} inertRoot - * @return {InertNode?} The potentially destroyed InertNode associated with this node, if any. - */ - - }, { - key: 'deregister', - value: function deregister(node, inertRoot) { - var inertNode = this._managedNodes.get(node); - if (!inertNode) return null; - - inertNode.removeInertRoot(inertRoot); - if (inertNode.destroyed) this._managedNodes.delete(node); - - return inertNode; - } - /** * Callback used when document has finished loading. */ @@ -788,32 +312,36 @@ var createClass = function () { value: function _onDocumentLoaded() { // Find all inert roots in document and make them actually inert. var inertElements = Array.from(this._document.querySelectorAll('[inert]')); - var _iteratorNormalCompletion7 = true; - var _didIteratorError7 = false; - var _iteratorError7 = undefined; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; try { - for (var _iterator7 = inertElements[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { - var inertElement = _step7.value; + for (var _iterator = inertElements[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var inertElement = _step.value; this.setInert(inertElement, true); } // Comment this out to use programmatic API only. } catch (err) { - _didIteratorError7 = true; - _iteratorError7 = err; + _didIteratorError = true; + _iteratorError = err; } finally { try { - if (!_iteratorNormalCompletion7 && _iterator7.return) { - _iterator7.return(); + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); } } finally { - if (_didIteratorError7) { - throw _iteratorError7; + if (_didIteratorError) { + throw _iteratorError; } } } - this._observer.observe(this._document.body, { attributes: true, subtree: true, childList: true }); + this._observer.observe(this._document.body, { + attributes: true, + subtree: true, + childList: true + }); } /** @@ -825,63 +353,63 @@ var createClass = function () { }, { key: '_watchForInert', value: function _watchForInert(records, self) { - var _iteratorNormalCompletion8 = true; - var _didIteratorError8 = false; - var _iteratorError8 = undefined; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; try { - for (var _iterator8 = records[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { - var record = _step8.value; + for (var _iterator2 = records[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var record = _step2.value; switch (record.type) { case 'childList': - var _iteratorNormalCompletion9 = true; - var _didIteratorError9 = false; - var _iteratorError9 = undefined; + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; try { - for (var _iterator9 = Array.from(record.addedNodes)[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { - var node = _step9.value; + for (var _iterator3 = Array.from(record.addedNodes)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var node = _step3.value; if (node.nodeType !== Node.ELEMENT_NODE) continue; var inertElements = Array.from(node.querySelectorAll('[inert]')); if (index(node, '[inert]')) inertElements.unshift(node); - var _iteratorNormalCompletion10 = true; - var _didIteratorError10 = false; - var _iteratorError10 = undefined; + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; try { - for (var _iterator10 = inertElements[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { - var inertElement = _step10.value; + for (var _iterator4 = inertElements[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var inertElement = _step4.value; this.setInert(inertElement, true); } } catch (err) { - _didIteratorError10 = true; - _iteratorError10 = err; + _didIteratorError4 = true; + _iteratorError4 = err; } finally { try { - if (!_iteratorNormalCompletion10 && _iterator10.return) { - _iterator10.return(); + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); } } finally { - if (_didIteratorError10) { - throw _iteratorError10; + if (_didIteratorError4) { + throw _iteratorError4; } } } } } catch (err) { - _didIteratorError9 = true; - _iteratorError9 = err; + _didIteratorError3 = true; + _iteratorError3 = err; } finally { try { - if (!_iteratorNormalCompletion9 && _iterator9.return) { - _iterator9.return(); + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); } } finally { - if (_didIteratorError9) { - throw _iteratorError9; + if (_didIteratorError3) { + throw _iteratorError3; } } } @@ -896,16 +424,16 @@ var createClass = function () { } } } catch (err) { - _didIteratorError8 = true; - _iteratorError8 = err; + _didIteratorError2 = true; + _iteratorError2 = err; } finally { try { - if (!_iteratorNormalCompletion8 && _iterator8.return) { - _iterator8.return(); + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); } } finally { - if (_didIteratorError8) { - throw _iteratorError8; + if (_didIteratorError2) { + throw _iteratorError2; } } } @@ -915,76 +443,18 @@ var createClass = function () { }(); /** - * Recursively walk the composed tree from |node|. + * Adds a style element to the node containing the inert specific styles * @param {Node} node - * @param {(function (Element))=} callback Callback to be called for each element traversed, - * before descending into child nodes. - * @param {ShadowRoot=} shadowRootAncestor The nearest ShadowRoot ancestor, if any. */ - function composedTreeWalk(node, callback, shadowRootAncestor) { - if (node.nodeType == Node.ELEMENT_NODE) { - var element = /** @type {Element} */node; - if (callback) callback(element); - - // Descend into node: - // If it has a ShadowRoot, ignore all child elements - these will be picked - // up by the or elements. Descend straight into the - // ShadowRoot. - var shadowRoot = element.shadowRoot || element.webkitShadowRoot; - if (shadowRoot) { - composedTreeWalk(shadowRoot, callback, shadowRoot); - return; - } - - // If it is a element, descend into distributed elements - these - // are elements from outside the shadow root which are rendered inside the - // shadow DOM. - if (element.localName == 'content') { - var content = /** @type {HTMLContentElement} */element; - // Verifies if ShadowDom v0 is supported. - var distributedNodes = content.getDistributedNodes ? content.getDistributedNodes() : []; - for (var i = 0; i < distributedNodes.length; i++) { - composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); - } - return; - } - - // If it is a element, descend into assigned nodes - these - // are elements from outside the shadow root which are rendered inside the - // shadow DOM. - if (element.localName == 'slot') { - var slot = /** @type {HTMLSlotElement} */element; - // Verify if ShadowDom v1 is supported. - var _distributedNodes = slot.assignedNodes ? slot.assignedNodes({ flatten: true }) : []; - for (var _i = 0; _i < _distributedNodes.length; _i++) { - composedTreeWalk(_distributedNodes[_i], callback, shadowRootAncestor); - } - return; - } - } - - // If it is neither the parent of a ShadowRoot, a element, a - // element, nor a element recurse normally. - var child = node.firstChild; - while (child != null) { - composedTreeWalk(child, callback, shadowRootAncestor); - child = child.nextSibling; - } - } - - /** - * Adds a style element to the node containing the inert specific styles - * @param {Node} node - */ function addInertStyle(node) { if (node.querySelector('style#inert-style')) { return; } var style = document.createElement('style'); style.setAttribute('id', 'inert-style'); - style.textContent = '\n' + '[inert] {\n' + ' pointer-events: none;\n' + ' cursor: default;\n' + '}\n' + '\n' + '[inert], [inert] * {\n' + ' user-select: none;\n' + ' -webkit-user-select: none;\n' + ' -moz-user-select: none;\n' + ' -ms-user-select: none;\n' + '}\n'; + style.textContent = '\n [inert], [inert] * {\n pointer-events: none;\n cursor: default;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n }'; node.appendChild(style); } @@ -999,6 +469,26 @@ var createClass = function () { inertManager.setInert(this, inert); } }); + + var nativeFocus = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'focus'); + Object.defineProperty(HTMLElement.prototype, 'focus', { + enumerable: true, + configurable: true, + writable: true, + value: function value() { + // If it is inert or into an inert node, no focus! + var target = this; + while (target && !target.inert) { + // Target might be distributed, so go to the deepest assignedSlot + // and walk up the tree from there. + while (target.assignedSlot) { + target = target.assignedSlot; + }target = target.parentNode || target.host; + } + if (target && target.inert) return; + return nativeFocus.value.call(this); + } + }); })(document); }))); diff --git a/dist/inert.min.js b/dist/inert.min.js index 37158e6..a81638e 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(this,function(){"use strict";function e(e,t){var r=window.Element.prototype,n=r.matches||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector;if(!e||1!==e.nodeType)return!1;var o=e.parentNode;if(n)return n.call(e,t);for(var i=o.querySelectorAll(t),a=i.length,s=0;s Date: Mon, 17 Apr 2017 10:57:59 -0700 Subject: [PATCH 10/12] simpler override of focus --- dist/inert.js | 31 +++++++++++++------------------ dist/inert.min.js | 2 +- src/inert.js | 31 +++++++++++++------------------ 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index 6541ec9..c61d8a0 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -470,25 +470,20 @@ var createClass = function () { } }); - var nativeFocus = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'focus'); - Object.defineProperty(HTMLElement.prototype, 'focus', { - enumerable: true, - configurable: true, - writable: true, - value: function value() { - // If it is inert or into an inert node, no focus! - var target = this; - while (target && !target.inert) { - // Target might be distributed, so go to the deepest assignedSlot - // and walk up the tree from there. - while (target.assignedSlot) { - target = target.assignedSlot; - }target = target.parentNode || target.host; - } - if (target && target.inert) return; - return nativeFocus.value.call(this); + var nativeFocus = HTMLElement.prototype.focus; + HTMLElement.prototype.focus = function () { + // If it is inert or into an inert node, no focus! + var target = this; + while (target && !target.inert) { + // Target might be distributed, so go to the deepest assignedSlot + // and walk up the tree from there. + while (target.assignedSlot) { + target = target.assignedSlot; + }target = target.parentNode || target.host; } - }); + if (target && target.inert) return; + return nativeFocus.call(this); + }; })(document); }))); diff --git a/dist/inert.min.js b/dist/inert.min.js index a81638e..f14fc31 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";function e(e,t){var r=window.Element.prototype,n=r.matches||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector;if(!e||1!==e.nodeType)return!1;var o=e.parentNode;if(n)return n.call(e,t);for(var i=o.querySelectorAll(t),a=i.length,s=0;s Date: Mon, 17 Apr 2017 12:01:23 -0700 Subject: [PATCH 11/12] InertRoot exposes only callback. Handle closed shadowRoots. --- dist/inert.js | 70 ++++++++++++++++++++++++++------------------- dist/inert.min.js | 2 +- src/inert.js | 72 +++++++++++++++++++++++++++-------------------- test/index.js | 8 ++++++ 4 files changed, 91 insertions(+), 61 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index c61d8a0..5af7098 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -114,10 +114,10 @@ var createClass = function () { var InertRoot = function () { /** - * @param {Element} rootElement The Element at the root of the inert subtree. - * @param {InertManager} inertManager The global singleton InertManager object. + * @param {!Element} rootElement The Element at the root of the inert subtree. + * @param {Function} onShadowRootMutation Callback invoked on shadow root mutations. */ - function InertRoot(rootElement, inertManager) { + function InertRoot(rootElement, onShadowRootMutation) { classCallCheck(this, InertRoot); /** @type {Element} */ @@ -152,40 +152,52 @@ var createClass = function () { } } - // We can attach a shadowRoot if supported, is a native element that accepts shadowRoot - // or if element has potential custom element name. + if (!nativeShadowDOM) return; + // If element doesn't accept shadowRoot, check if it is a potential custom element // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name - var canAttachShadow = nativeShadowDOM && index(rootElement, acceptsShadowRootSelector) || rootElement.localName.indexOf('-') !== -1; - if (!canAttachShadow) return; + if (!index(rootElement, acceptsShadowRootSelector)) { + var potentialCustomElement = rootElement.tagName.indexOf('-') !== -1; + if (!potentialCustomElement) return; + } + // We already failed inerting this shadow root. + if (rootElement.__failedAttachShadow) return; // Force shadowRoot. if (!rootElement.shadowRoot) { - rootElement.attachShadow({ - mode: 'open' - }).appendChild(document.createElement('slot')); - var nativeAttachShadow = rootElement.attachShadow; - rootElement.attachShadow = function () { - // Clear the slot we added. - var slot = this.shadowRoot.querySelector('slot'); - slot && this.shadowRoot.removeChild(slot); - this.attachShadow = nativeAttachShadow; - return this.shadowRoot; - }; + // Detect if this is a closed shadowRoot with try/catch (sigh). + try { + rootElement.attachShadow({ + mode: 'open' + }).appendChild(document.createElement('slot')); + // NOTE: we allow attachShadow to be called again since we're using it + // for polyfilling inert. We ensure the shadowRoot is empty and return it. + rootElement.attachShadow = function () { + var slot = this.shadowRoot.querySelector('slot'); + slot && this.shadowRoot.removeChild(slot); + delete this.attachShadow; + return this.shadowRoot; + }; + } catch (e) { + rootElement.__failedAttachShadow = true; + console.warn('Could not inert element shadowRoot', rootElement, e); + return; + } } else { - // Manager might have not "seen" these children since they're in a shadowRoot. + // Ensure inerted elements in the shadowRoot have the property updated. var inertChildren = rootElement.shadowRoot.querySelectorAll('[inert]'); for (var i = 0, l = inertChildren.length; i < l; i++) { - inertManager.setInert(inertChildren[i], true); + inertChildren[i].inert = true; } } - - // Give the manager visibility on changing nodes in the shadowRoot. - this._observer = new MutationObserver(inertManager.watchForInert); - this._observer.observe(rootElement.shadowRoot, { - attributes: true, - subtree: true, - childList: true - }); + if (typeof onShadowRootMutation === 'function') { + // Give visibility on changing nodes in the shadowRoot. + this._observer = new MutationObserver(onShadowRootMutation); + this._observer.observe(rootElement.shadowRoot, { + attributes: true, + subtree: true, + childList: true + }); + } } /** @@ -281,7 +293,7 @@ var createClass = function () { if (this._inertRoots.has(root) === inert) // element is already inert return; if (inert) { - var inertRoot = new InertRoot(root, this); + var inertRoot = new InertRoot(root, this.watchForInert); root.setAttribute('inert', ''); this._inertRoots.set(root, inertRoot); // If not contained in the document, it must be in a shadowRoot. diff --git a/dist/inert.min.js b/dist/inert.min.js index f14fc31..cd5c876 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";function e(e,t){var r=window.Element.prototype,n=r.matches||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector;if(!e||1!==e.nodeType)return!1;var o=e.parentNode;if(n)return n.call(e,t);for(var i=o.querySelectorAll(t),a=i.length,s=0;s fixture.attachShadow({mode:'open'})).to.throw(); }); + + it('handles closed shadowRoots', function() { + host = document.createElement('div'); + fixture.appendChild(host); + host.attachShadow({mode: 'closed'}); + host.inert = true; + expect(isUnfocusable(host)).to.equal(true); + }); }); }); From ae3302b9532cc9eeefd7b194f3fbf69d4b18f5fa Mon Sep 17 00:00:00 2001 From: valdrinkoshi Date: Tue, 23 May 2017 17:26:31 -0700 Subject: [PATCH 12/12] address feedback --- dist/inert.js | 60 +++++++++++++++++++------------------- dist/inert.min.js | 2 +- src/inert.js | 73 ++++++++++++++++++++++++----------------------- 3 files changed, 68 insertions(+), 67 deletions(-) diff --git a/dist/inert.js b/dist/inert.js index 5af7098..558477e 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -104,18 +104,15 @@ var createClass = function () { /** * `InertRoot` manages a single inert subtree, i.e. a DOM subtree whose root element * has an `inert` attribute. - * * Its main functions are: - * * - make the rootElement untabbable. - * * - notify the manager of inerted nodes in the rootElement's shadowRoot. */ var InertRoot = function () { /** * @param {!Element} rootElement The Element at the root of the inert subtree. - * @param {Function} onShadowRootMutation Callback invoked on shadow root mutations. + * @param {?Function} onShadowRootMutation Callback invoked on shadow root mutations. */ function InertRoot(rootElement, onShadowRootMutation) { classCallCheck(this, InertRoot); @@ -162,34 +159,39 @@ var createClass = function () { // We already failed inerting this shadow root. if (rootElement.__failedAttachShadow) return; - // Force shadowRoot. - if (!rootElement.shadowRoot) { + // Ensure the rootElement has a shadowRoot in order to leverage the behavior of tabindex = -1, + // which will remove the rootElement and its contents from the navigation order. + // See Step 3 https://www.w3.org/TR/shadow-dom/#dfn-document-sequential-focus-navigation-order + if (rootElement.shadowRoot) { + // It might be that rootElement had inert children in its shadowRoot and this is the first + // time we see them, hence we have to update their `inert` property. + var inertChildren = Array.from(rootElement.shadowRoot.querySelectorAll('[inert]')); + inertChildren.forEach(function (child) { + return child.inert = true; + }); + } else { // Detect if this is a closed shadowRoot with try/catch (sigh). + var shadowRoot = null; try { - rootElement.attachShadow({ + shadowRoot = rootElement.attachShadow({ mode: 'open' - }).appendChild(document.createElement('slot')); - // NOTE: we allow attachShadow to be called again since we're using it - // for polyfilling inert. We ensure the shadowRoot is empty and return it. - rootElement.attachShadow = function () { - var slot = this.shadowRoot.querySelector('slot'); - slot && this.shadowRoot.removeChild(slot); - delete this.attachShadow; - return this.shadowRoot; - }; + }); } catch (e) { + // Most likely a closed shadowRoot was already attached. rootElement.__failedAttachShadow = true; console.warn('Could not inert element shadowRoot', rootElement, e); return; } - } else { - // Ensure inerted elements in the shadowRoot have the property updated. - var inertChildren = rootElement.shadowRoot.querySelectorAll('[inert]'); - for (var i = 0, l = inertChildren.length; i < l; i++) { - inertChildren[i].inert = true; - } + shadowRoot.appendChild(document.createElement('slot')); + // NOTE: we allow attachShadow to be called again since we're using it + // for polyfilling inert. We ensure the shadowRoot is empty and return it. + rootElement.attachShadow = function () { + shadowRoot.innerHTML = ''; + delete rootElement.attachShadow; + return shadowRoot; + }; } - if (typeof onShadowRootMutation === 'function') { + if (onShadowRootMutation !== null) { // Give visibility on changing nodes in the shadowRoot. this._observer = new MutationObserver(onShadowRootMutation); this._observer.observe(rootElement.shadowRoot, { @@ -212,11 +214,7 @@ var createClass = function () { if (this._observer) this._observer.disconnect(); this._rootElement.removeAttribute('aria-hidden'); - if (this._rootTabindex) { - this._rootElement.setAttribute('tabindex', this._rootTabindex); - } else { - this._rootElement.removeAttribute('tabindex'); - } + if (this._rootTabindex) this._rootElement.setAttribute('tabindex', this._rootTabindex);else this._rootElement.removeAttribute('tabindex'); this._observer = null; this._rootElement = null; @@ -257,13 +255,13 @@ var createClass = function () { */ this._inertRoots = new Map(); - this.watchForInert = this._watchForInert.bind(this); + this._boundWatchForInert = this._watchForInert.bind(this); /** * Observer for mutations on `document.body`. * @type {MutationObserver} */ - this._observer = new MutationObserver(this.watchForInert); + this._observer = new MutationObserver(this._boundWatchForInert); // Add inert style. addInertStyle(document.head || document.body || document.documentElement); @@ -293,7 +291,7 @@ var createClass = function () { if (this._inertRoots.has(root) === inert) // element is already inert return; if (inert) { - var inertRoot = new InertRoot(root, this.watchForInert); + var inertRoot = new InertRoot(root, this._boundWatchForInert); root.setAttribute('inert', ''); this._inertRoots.set(root, inertRoot); // If not contained in the document, it must be in a shadowRoot. diff --git a/dist/inert.min.js b/dist/inert.min.js index cd5c876..94ea5c8 100644 --- a/dist/inert.min.js +++ b/dist/inert.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";function e(e,t){var r=window.Element.prototype,n=r.matches||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector;if(!e||1!==e.nodeType)return!1;var o=e.parentNode;if(n)return n.call(e,t);for(var i=o.querySelectorAll(t),a=i.length,s=0;s child.inert = true); + } else { // Detect if this is a closed shadowRoot with try/catch (sigh). + let shadowRoot = null; try { - rootElement.attachShadow({ + shadowRoot = rootElement.attachShadow({ mode: 'open', - }).appendChild(document.createElement('slot')); - // NOTE: we allow attachShadow to be called again since we're using it - // for polyfilling inert. We ensure the shadowRoot is empty and return it. - rootElement.attachShadow = function() { - const slot = this.shadowRoot.querySelector('slot'); - slot && this.shadowRoot.removeChild(slot); - delete this.attachShadow; - return this.shadowRoot; - }; + }); } catch (e) { + // Most likely a closed shadowRoot was already attached. rootElement.__failedAttachShadow = true; console.warn('Could not inert element shadowRoot', rootElement, e); return; } - } else { - // Ensure inerted elements in the shadowRoot have the property updated. - const inertChildren = rootElement.shadowRoot.querySelectorAll('[inert]'); - for (let i = 0, l = inertChildren.length; i < l; i++) { - inertChildren[i].inert = true; - } + shadowRoot.appendChild(document.createElement('slot')); + // NOTE: we allow attachShadow to be called again since we're using it + // for polyfilling inert. We ensure the shadowRoot is empty and return it. + rootElement.attachShadow = () => { + shadowRoot.innerHTML = ''; + delete rootElement.attachShadow; + return shadowRoot; + }; } - if (typeof onShadowRootMutation === 'function') { + if (onShadowRootMutation !== null) { // Give visibility on changing nodes in the shadowRoot. this._observer = new MutationObserver(onShadowRootMutation); this._observer.observe(rootElement.shadowRoot, { @@ -145,14 +148,14 @@ import matches from 'dom-matches'; * stored in this object and updates the state of all of the managed nodes. */ destructor() { - if (this._observer) this._observer.disconnect(); + if (this._observer) + this._observer.disconnect(); this._rootElement.removeAttribute('aria-hidden'); - if (this._rootTabindex) { + if (this._rootTabindex) this._rootElement.setAttribute('tabindex', this._rootTabindex); - } else { + else this._rootElement.removeAttribute('tabindex'); - } this._observer = null; this._rootElement = null; @@ -186,14 +189,13 @@ import matches from 'dom-matches'; */ this._inertRoots = new Map(); - this.watchForInert = this._watchForInert.bind(this); + this._boundWatchForInert = this._watchForInert.bind(this); /** * Observer for mutations on `document.body`. * @type {MutationObserver} */ - this._observer = new MutationObserver(this.watchForInert); - + this._observer = new MutationObserver(this._boundWatchForInert); // Add inert style. addInertStyle(document.head || document.body || document.documentElement); @@ -219,7 +221,7 @@ import matches from 'dom-matches'; if (this._inertRoots.has(root) === inert) // element is already inert return; if (inert) { - const inertRoot = new InertRoot(root, this.watchForInert); + const inertRoot = new InertRoot(root, this._boundWatchForInert); root.setAttribute('inert', ''); this._inertRoots.set(root, inertRoot); // If not contained in the document, it must be in a shadowRoot. @@ -333,7 +335,8 @@ import matches from 'dom-matches'; while (target.assignedSlot) target = target.assignedSlot; target = target.parentNode || target.host; } - if (target && target.inert) return; + if (target && target.inert) + return; return nativeFocus.call(this); }; })(document);