fix(overlays): reinsert missing tab detect el, don't error on disconnect

This commit is contained in:
Joren Broekema 2021-01-28 11:24:00 +01:00
parent 37f975ea48
commit a77452b0ac
3 changed files with 46 additions and 2 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/overlays': patch
---
Use MutationObserver to watch child changes of the contentNode, and re-insert tab detection element when necessary.

View file

@ -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');
}

View file

@ -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'));