From a77452b0ac6fada637f1342e1b6334f670ad22fb Mon Sep 17 00:00:00 2001 From: Joren Broekema Date: Thu, 28 Jan 2021 11:24:00 +0100 Subject: [PATCH] fix(overlays): reinsert missing tab detect el, don't error on disconnect --- .changeset/long-fans-flash.md | 5 ++++ packages/overlays/src/utils/contain-focus.js | 30 ++++++++++++++++++- .../test/utils-tests/contain-focus.test.js | 13 +++++++- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 .changeset/long-fans-flash.md diff --git a/.changeset/long-fans-flash.md b/.changeset/long-fans-flash.md new file mode 100644 index 000000000..df6b255f0 --- /dev/null +++ b/.changeset/long-fans-flash.md @@ -0,0 +1,5 @@ +--- +'@lion/overlays': patch +--- + +Use MutationObserver to watch child changes of the contentNode, and re-insert tab detection element when necessary. diff --git a/packages/overlays/src/utils/contain-focus.js b/packages/overlays/src/utils/contain-focus.js index f100d1bf7..df28bce5b 100644 --- a/packages/overlays/src/utils/contain-focus.js +++ b/packages/overlays/src/utils/contain-focus.js @@ -77,6 +77,8 @@ export function containFocus(rootElement) { const initialFocus = focusableElements.find(e => e.hasAttribute('autofocus')) || rootElement; /** @type {HTMLElement} */ let tabDetectionElement; + /** @type {MutationObserver} */ + let rootElementMutationObserver; // If root element will receive focus, it should have a tabindex of -1. // This makes it focusable through js, but it won't appear in the tab order @@ -101,7 +103,28 @@ export function containFocus(rootElement) { function createHelpersDetectingTabDirection() { tabDetectionElement = document.createElement('div'); tabDetectionElement.style.display = 'none'; + tabDetectionElement.setAttribute('data-is-tab-detection-element', ''); rootElement.insertBefore(tabDetectionElement, rootElement.children[0]); + + rootElementMutationObserver = new MutationObserver(mutationsList => { + for (const mutation of mutationsList) { + if (mutation.type === 'childList') { + const tabDetectionElIsMissing = !Array.from(rootElement.children).find(el => + el.hasAttribute('data-is-tab-detection-element'), + ); + const foundTabDetectionElInMutations = Array.from(mutation.addedNodes).find( + /** @param {Node} el */ el => + el instanceof HTMLElement && el.hasAttribute('data-is-tab-detection-element'), + ); + // Prevent infinite loop by detecting that mutation event is not from adding the tab detection el + if (tabDetectionElIsMissing && !foundTabDetectionElInMutations) { + rootElementMutationObserver.disconnect(); + createHelpersDetectingTabDirection(); + } + } + } + }); + rootElementMutationObserver.observe(rootElement, { childList: true }); } function isForwardTabInWindow() { @@ -162,7 +185,12 @@ export function containFocus(rootElement) { window.removeEventListener('keydown', handleKeydown); window.removeEventListener('focusin', handleFocusin); window.removeEventListener('focusout', handleFocusout); - rootElement.removeChild(tabDetectionElement); + // Guard this, since we also disconnect if we notice a missing tab + // detection element. We reinsert it, so it's okay to not fail here. + rootElementMutationObserver.disconnect(); + if (Array.from(rootElement.children).includes(tabDetectionElement)) { + rootElement.removeChild(tabDetectionElement); + } rootElement.style.removeProperty('outline'); } diff --git a/packages/overlays/test/utils-tests/contain-focus.test.js b/packages/overlays/test/utils-tests/contain-focus.test.js index b7fcb3552..6063262ba 100644 --- a/packages/overlays/test/utils-tests/contain-focus.test.js +++ b/packages/overlays/test/utils-tests/contain-focus.test.js @@ -1,4 +1,4 @@ -import { expect, fixture, html } from '@open-wc/testing'; +import { expect, fixture, html, nextFrame } from '@open-wc/testing'; import { renderLitAsNode } from '@lion/helpers'; import { getDeepActiveElement } from '../../src/utils/get-deep-active-element.js'; import { getFocusableElements } from '../../src/utils/get-focusable-elements.js'; @@ -164,6 +164,17 @@ describe('containFocus()', () => { }); describe('Tabbing into window', () => { + it('reinserts tab detection element when contentNode changes inner content', async () => { + await fixture(lightDomTemplate); + const root = /** @type {HTMLElement} */ (document.getElementById('rootElement')); + const { disconnect } = containFocus(root); + expect(root.querySelector('[data-is-tab-detection-element]')).to.exist; + root.innerHTML = `my content`; + await nextFrame(); + expect(root.querySelector('[data-is-tab-detection-element]')).to.exist; + disconnect(); + }); + it('restores focus within root element', async () => { await fixture(lightDomTemplate); const root = /** @type {HTMLElement} */ (document.getElementById('rootElement'));