diff --git a/dist/inert.js b/dist/inert.js index b631d5b..558477e 100644 --- a/dist/inert.js +++ b/dist/inert.js @@ -94,537 +94,134 @@ 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. - * - * - 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. + * - 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 {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 {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); + 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; } - } 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; + // Keep searching in the shadowRoot. + active = active.shadowRoot ? active.shadowRoot.activeElement : null; } - 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); + 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 + if (!index(rootElement, acceptsShadowRootSelector)) { + var potentialCustomElement = rootElement.tagName.indexOf('-') !== -1; + if (!potentialCustomElement) return; } - - /** - * 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); + // We already failed inerting this shadow root. + if (rootElement.__failedAttachShadow) return; + + // 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; }); - } - - /** - * 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; - } - } - } - } - - /** - * 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; - + } else { + // Detect if this is a closed shadowRoot with try/catch (sigh). + var shadowRoot = null; 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; - } - } + shadowRoot = rootElement.attachShadow({ + mode: 'open' + }); + } catch (e) { + // Most likely a closed shadowRoot was already attached. + rootElement.__failedAttachShadow = true; + console.warn('Could not inert element shadowRoot', rootElement, e); + return; } + 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; + }; } - }, { - key: 'managedNodes', - get: function get$$1() { - return new Set(this._managedNodes); + if (onShadowRootMutation !== null) { + // Give visibility on changing nodes in the shadowRoot. + this._observer = new MutationObserver(onShadowRootMutation); + this._observer.observe(rootElement.shadowRoot, { + attributes: true, + subtree: true, + childList: true + }); } - }]); - 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. + * 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'); + if (this._observer) this._observer.disconnect(); - // Use `delete` to restore native focus method. - if (this._overrodeFocusMethod) delete this._node.focus; - } - this._node = null; - this._inertRoots = null; - - this._destroyed = true; - } + this._rootElement.removeAttribute('aria-hidden'); + if (this._rootTabindex) this._rootElement.setAttribute('tabindex', this._rootTabindex);else this._rootElement.removeAttribute('tabindex'); - /** - * @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', - - - /** - * 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'); - } - } - - /** - * 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 +230,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 +240,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 +249,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._boundWatchForInert = 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._boundWatchForInert); // 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 +288,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); + 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. - // 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 +313,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 +322,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 +363,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 +434,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 +453,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 +479,21 @@ var createClass = function () { inertManager.setInert(this, inert); } }); + + 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 37158e6..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()}(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} - * All managed focusable nodes in this InertRoot's subtree. + * @param {!Element} rootElement The Element at the root of the inert subtree. + * @param {?Function} onShadowRootMutation Callback invoked on shadow root mutations. */ - 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, onShadowRootMutation) { + /** @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; + + // 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) { + let 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; } - 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 (matches(node, _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); - } - - 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); + 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 + if (!matches(rootElement, acceptsShadowRootSelector)) { + const potentialCustomElement = rootElement.tagName.indexOf('-') !== -1; + if (!potentialCustomElement) + return; + } + // We already failed inerting this shadow root. + if (rootElement.__failedAttachShadow) + return; - // 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); - } + // 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. + const inertChildren = Array.from(rootElement.shadowRoot.querySelectorAll('[inert]')); + inertChildren.forEach((child) => child.inert = true); + } else { + // Detect if this is a closed shadowRoot with try/catch (sigh). + let shadowRoot = null; + try { + shadowRoot = rootElement.attachShadow({ + mode: 'open', + }); + } catch (e) { + // Most likely a closed shadowRoot was already attached. + rootElement.__failedAttachShadow = true; + console.warn('Could not inert element shadowRoot', rootElement, e); + return; } + 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 (onShadowRootMutation !== null) { + // Give visibility on changing nodes in the shadowRoot. + this._observer = new MutationObserver(onShadowRootMutation); + this._observer.observe(rootElement.shadowRoot, { + attributes: true, + subtree: true, + childList: true, + }); } } - } -} - -/** - * `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. - */ - constructor(node, inertRoot) { - /** @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. + * 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. */ - this._inertRoots = new Set([inertRoot]); - - /** @type {boolean} */ - this._destroyed = false; - - // Save any prior tabindex info and make this node untabbable - this.ensureUntabbable(); - } + destructor() { + if (this._observer) + this._observer.disconnect(); - /** - * 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); + this._rootElement.removeAttribute('aria-hidden'); + if (this._rootTabindex) + this._rootElement.setAttribute('tabindex', this._rootTabindex); else - this._node.removeAttribute('tabindex'); + this._rootElement.removeAttribute('tabindex'); - // Use `delete` to restore native focus method. - if (this._overrodeFocusMethod) - delete this._node.focus; + this._observer = null; + this._rootElement = null; + this._rootTabindex = null; } - 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. + * 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. */ - get destroyed() { - return this._destroyed; - } - - /** - * Throw if user tries to access destroyed InertNode. - */ - _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 (matches(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; + 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 inert roots known to this InertManager. In a map to allow looking up by Node. + * @type {Map} + */ + this._inertRoots = new Map(); + + this._boundWatchForInert = this._watchForInert.bind(this); + + /** + * Observer for mutations on `document.body`. + * @type {MutationObserver} + */ + this._observer = new MutationObserver(this._boundWatchForInert); + + // 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(); } - } 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} + * Set whether the given element should be an inert root or not. + * @param {Element} root + * @param {boolean} inert */ - this._managedNodes = new Map(); + setInert(root, inert) { + if (this._inertRoots.has(root) === inert) // element is already inert + return; + if (inert) { + 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. + if (!this._document.body.contains(root)) { + let parent = root.parentNode; + while (parent) { + if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + addInertStyle(parent); + break; + } + parent = parent.parentNode; + } + } + } else { + const inertRoot = this._inertRoots.get(root); + inertRoot.destructor(); + this._inertRoots.delete(root); + root.removeAttribute('inert'); + } + } /** - * 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 (matches(node, '[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. + * Adds a style element to the node containing the inert specific styles * @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. - * @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 (matches(node, '[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; - } - } - } -} - - /** - * 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); - - // 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); + function addInertStyle(node) { + if (node.querySelector('style#inert-style')) { return; } + const style = document.createElement('style'); + style.setAttribute('id', 'inert-style'); + style.textContent = ` + [inert], [inert] * { + pointer-events: none; + cursor: default; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + }`; + node.appendChild(style); + } - // 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; + 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); + }, + }); + + const nativeFocus = HTMLElement.prototype.focus; + HTMLElement.prototype.focus = function() { + // If it is inert or into an inert node, no focus! + let 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 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); - } + if (target && target.inert) return; - } - } - - // 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); - }, - }); + return nativeFocus.call(this); + }; })(document); diff --git a/test/index.js b/test/index.js index 58ecfd0..732d0e8 100644 --- a/test/index.js +++ b/test/index.js @@ -27,8 +27,6 @@ function isUnfocusable(el) { return false; if (document.activeElement === el) return false; - if (el.tabIndex !== -1) - return false; return true; } @@ -63,7 +61,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); }); @@ -127,9 +124,9 @@ describe('Basic', function() { }); }); - describe('ShadowDOM v0', function() { - if (!Element.prototype.createShadowRoot) { - console.log('ShadowDOM v0 is not supported by the browser.'); + describe('ShadowDOM v1', function() { + if (!Element.prototype.attachShadow) { + console.log('ShadowDOM v1 is not supported by the browser.'); return; } @@ -141,7 +138,9 @@ describe('Basic', function() { fixture.inert = false; host = document.createElement('div'); fixture.appendChild(host); - host.createShadowRoot(); + host.attachShadow({ + mode: 'open', + }); }); it('should apply inside shadow trees', function() { @@ -157,13 +156,14 @@ describe('Basic', 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'); + shadowButton.inert = false; + expect(getComputedStyle(shadowButton).pointerEvents).to.equal('auto'); }); it('should apply inside shadow trees distributed content', function() { - host.shadowRoot.appendChild(document.createElement('content')); + host.shadowRoot.appendChild(document.createElement('slot')); const distributedButton = document.createElement('button'); distributedButton.textContent = 'Distributed button'; host.appendChild(distributedButton); @@ -171,56 +171,28 @@ describe('Basic', function() { 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.'); - return; - } - - let fixture; - let host; - - beforeEach(function() { - fixture = document.querySelector('#fixture'); - fixture.inert = false; - host = document.createElement('div'); - fixture.appendChild(host); - host.attachShadow({ - mode: 'open', - }); - }); - - it('should apply inside shadow trees', function() { - const shadowButton = document.createElement('button'); - shadowButton.textContent = 'Shadow button'; - host.shadowRoot.appendChild(shadowButton); - shadowButton.focus(); + it('should allow to call attachShadow', function() { fixture.inert = true; - expect(isUnfocusable(shadowButton)).to.equal(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 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; - Promise.resolve().then(() => { - expect(getComputedStyle(shadowButton).pointerEvents).to.equal('none'); - done(); - }); + it('should throw error when calling attachShadow twice', function() { + fixture.inert = true; + fixture.attachShadow({mode:'open'}); + expect(() => fixture.attachShadow({mode:'open'})).to.throw(); }); - it('should apply inside shadow trees distributed content', function() { - host.shadowRoot.appendChild(document.createElement('slot')); - const distributedButton = document.createElement('button'); - distributedButton.textContent = 'Distributed button'; - host.appendChild(distributedButton); - distributedButton.focus(); - fixture.inert = true; - expect(isUnfocusable(distributedButton)).to.equal(true); + 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); }); }); });