/* eslint-disable lit-a11y/no-autofocus */ import { expect, fixture, nextFrame } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { renderLitAsNode } from '@lion/ui/helpers.js'; import { getDeepActiveElement, getFocusableElements } from '@lion/ui/overlays.js'; import { keyCodes } from '../../src/utils/key-codes.js'; import { containFocus } from '../../src/utils/contain-focus.js'; function simulateTabWithinContainFocus() { const event = new CustomEvent('keydown', { detail: 0, bubbles: true }); // @ts-ignore override keyCode event.keyCode = keyCodes.tab; window.dispatchEvent(event); } /** * @param {HTMLElement} elToRecieveFocus */ function simulateTabInWindow(elToRecieveFocus) { window.dispatchEvent(new Event('blur')); elToRecieveFocus.focus(); window.dispatchEvent(new Event('focusin')); } const interactionElementsNode = renderLitAsNode(html`
foo
`); const lightDomTemplate = html`
${interactionElementsNode}
`; const lightDomAutofocusTemplate = html`
foo
`; function createShadowDomNode() { const shadowDomNode = renderLitAsNode(html`
`); const rootElementShadow = shadowDomNode?.querySelector('#rootElementShadow'); rootElementShadow?.attachShadow({ mode: 'open' }); if (interactionElementsNode) { rootElementShadow?.shadowRoot?.appendChild(interactionElementsNode); } return shadowDomNode; } describe('containFocus()', () => { it('starts focus at the root element when there is no element with [autofocus]', async () => { await fixture(lightDomTemplate); const root = /** @type {HTMLElement} */ (document.getElementById('rootElement')); const { disconnect } = containFocus(root); expect(getDeepActiveElement()).to.equal(root); expect(root.getAttribute('tabindex')).to.equal('-1'); expect(root.style.getPropertyValue('outline-style')).to.equal('none'); disconnect(); }); it('starts focus at the element with [autofocus] attribute', async () => { await fixture(lightDomAutofocusTemplate); const el = /** @type {HTMLElement} */ (document.querySelector('input[autofocus]')); const { disconnect } = containFocus(el); expect(getDeepActiveElement()).to.equal(el); disconnect(); }); it('on tab, focuses first focusable element if focus was on element outside root element', async () => { await fixture(lightDomTemplate); const root = /** @type {HTMLElement} */ (document.getElementById('rootElement')); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); /** @type {HTMLElement} */ (document.getElementById('outside-1')).focus(); simulateTabWithinContainFocus(); expect(getDeepActiveElement()).to.equal(focusableElements[0]); disconnect(); }); it('on tab, focuses first focusable element if focus was on the last focusable element', async () => { await fixture(lightDomTemplate); const root = /** @type {HTMLElement} */ (document.getElementById('rootElement')); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); focusableElements[focusableElements.length - 1].focus(); simulateTabWithinContainFocus(); expect(getDeepActiveElement()).to.equal(focusableElements[0]); disconnect(); }); it('on tab, does not interfere if focus remains within the root element', async () => { await fixture(lightDomTemplate); const root = /** @type {HTMLElement} */ (document.getElementById('rootElement')); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); focusableElements[2].focus(); simulateTabWithinContainFocus(); /** * We test if focus remained on the same element because we cannot simulate * actual tab key press. So the best we can do is if we didn't redirect focus * to the first element. */ expect(getDeepActiveElement()).to.equal(focusableElements[2]); disconnect(); }); it.skip('restores focus to root element if focusout event happens where activeElement goes outside', async () => { await fixture(lightDomTemplate); const root = /** @type {HTMLElement} */ (document.getElementById('rootElement')); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); focusableElements[2].focus(); expect(getDeepActiveElement()).to.equal(focusableElements[2]); document.body.click(); // this does not cause focusout event :( doesn't seem possible to mock expect(getDeepActiveElement()).to.equal(root); disconnect(); }); 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')); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); // Simulate tab in window simulateTabInWindow(/** @type {HTMLElement} */ (document.getElementById('outside-1'))); expect(getDeepActiveElement()).to.equal(focusableElements[0]); // Simulate shift+tab in window simulateTabInWindow(/** @type {HTMLElement} */ (document.getElementById('outside-2'))); expect(getDeepActiveElement()).to.equal(focusableElements[focusableElements.length - 1]); disconnect(); }); it('restores focus within root element with shadow dom', async () => { const el = await fixture(html`${createShadowDomNode()}`); const root = /** @type {HTMLElement} */ (el.querySelector('#rootElementShadow')); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); // Simulate tab in window simulateTabInWindow(/** @type {HTMLElement} */ (document.getElementById('outside-1'))); expect(getDeepActiveElement()).to.equal(focusableElements[0]); // Simulate shift+tab in window simulateTabInWindow(/** @type {HTMLElement} */ (document.getElementById('outside-2'))); expect(getDeepActiveElement()).to.equal(focusableElements[focusableElements.length - 1]); disconnect(); }); it('keeps focus if already in rootElement', async () => { const el = await fixture(html`${createShadowDomNode()}`); const root = /** @type {HTMLElement} */ (el.querySelector('#rootElementShadow')); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); // Simulate tab in window simulateTabInWindow(focusableElements[0]); expect(getDeepActiveElement()).to.equal(focusableElements[0]); // Simulate shift+tab in window simulateTabInWindow(focusableElements[focusableElements.length - 1]); expect(getDeepActiveElement()).to.equal(focusableElements[focusableElements.length - 1]); disconnect(); }); }); });