From f6bbc2f26ee886d0c81d552ee537f20a466aac65 Mon Sep 17 00:00:00 2001 From: valdrinkoshi Date: Fri, 5 Aug 2016 17:25:18 -0700 Subject: [PATCH] observe shadow root child nodes --- inert.js | 36 +++++++++++++++++++++++++++++++- test/index.js | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/inert.js b/inert.js index bf62765..70227d6 100644 --- a/inert.js +++ b/inert.js @@ -69,13 +69,44 @@ class InertRoot { // Make all focusable elements in the subtree unfocusable and add them to _managedNodes this._makeSubtreeUnfocusable(this._rootElement); + const boundOnMutation = this._onMutation.bind(this); // 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 = new MutationObserver(boundOnMutation); this._observer.observe(this._rootElement, { attributes: true, childList: true, subtree: true }); + + // Watch for: + // - any additions in the shadowRoot subtree: make them unfocusable too + // - any removals from the shadowRoot subtree: remove them from this inert root's managed nodes + const rootObserver = new MutationObserver(boundOnMutation); + const shadowRoot = this._rootElement.shadowRoot || this._rootElement.webkitShadowRoot; + if (shadowRoot) { + rootObserver.observe(shadowRoot, { childList: true, subtree: true }); + } else { + // Might create a shadowRoot in the future, either by attachShadow + // (ShadowDOM v1) or by createShadowRoot (ShadowDOM v0), so we patch both + // of them. + const attachShadowFn = this._rootElement.attachShadow; + if (attachShadowFn) { + this._rootElement.attachShadow = function patchedAttachShadow() { + const shadowRoot = attachShadowFn.apply(this, arguments); + rootObserver.observe(shadowRoot, { childList: true, subtree: true }); + return shadowRoot; + }; + } + const createShadowRootFn = this._rootElement.createShadowRoot; + if (createShadowRootFn) { + this._rootElement.createShadowRoot = function patchedCreateShadowRoot() { + const shadowRoot = createShadowRootFn.apply(this, arguments); + rootObserver.observe(shadowRoot, { childList: true, subtree: true }); + return shadowRoot; + }; + } + } + this._rootObserver = rootObserver; } /** @@ -86,6 +117,9 @@ class InertRoot { this._observer.disconnect(); this._observer = null; + this._rootObserver.disconnect(); + delete this._rootObserver; + if (this._rootElement) this._rootElement.removeAttribute('aria-hidden'); this._rootElement = null; diff --git a/test/index.js b/test/index.js index efc2f48..67abc6f 100644 --- a/test/index.js +++ b/test/index.js @@ -150,6 +150,34 @@ describe('Basic', function() { expect(isUnfocusable(shadowButton)).to.equal(true); }); + it('should apply to elements added to shadow trees later in time', function(done) { + host.inert = true; + const shadowButton = document.createElement('button'); + shadowButton.textContent = 'Shadow button'; + host.shadowRoot.appendChild(shadowButton); + // Give time to mutation observers. + setTimeout(function() { + expect(isUnfocusable(shadowButton)).to.equal(true); + done(); + }); + }); + + it('should apply to elements that create shadow tree later in time', function(done) { + fixture.removeChild(host); + host = document.createElement('div'); + fixture.appendChild(host); + host.inert = true; + const shadowRoot = host.createShadowRoot(); + const shadowButton = document.createElement('button'); + shadowButton.textContent = 'Shadow button'; + shadowRoot.appendChild(shadowButton); + // Give time to mutation observers. + setTimeout(function() { + expect(isUnfocusable(shadowButton)).to.equal(true); + done(); + }); + }); + it('should apply inert styles inside shadow trees', function() { const shadowButton = document.createElement('button'); shadowButton.textContent = 'Shadow button'; @@ -200,6 +228,36 @@ describe('Basic', function() { expect(isUnfocusable(shadowButton)).to.equal(true); }); + it('should apply to elements added to shadow trees later in time', function(done) { + host.inert = true; + const shadowButton = document.createElement('button'); + shadowButton.textContent = 'Shadow button'; + host.shadowRoot.appendChild(shadowButton); + // Give time to mutation observers. + setTimeout(function() { + expect(isUnfocusable(shadowButton)).to.equal(true); + done(); + }); + }); + + it('should apply to elements that create shadow tree later in time', function(done) { + fixture.removeChild(host); + host = document.createElement('div'); + fixture.appendChild(host); + host.inert = true; + const shadowRoot = host.attachShadow({ + mode: 'open' + }); + const shadowButton = document.createElement('button'); + shadowButton.textContent = 'Shadow button'; + shadowRoot.appendChild(shadowButton); + // Give time to mutation observers. + setTimeout(function() { + expect(isUnfocusable(shadowButton)).to.equal(true); + done(); + }); + }); + it('should apply inert styles inside shadow trees', function() { const shadowButton = document.createElement('button'); shadowButton.textContent = 'Shadow button';