import { expect, fixture, html } 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'; 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 }); event.keyCode = keyCodes.tab; window.dispatchEvent(event); } function simulateTabInWindow(elToRecieveFocus) { window.dispatchEvent(new Event('blur')); elToRecieveFocus.focus(); window.dispatchEvent(new Event('focusin')); } const interactionElementsNode = renderLitAsNode(html`
`); const lightDomTemplate = html`
${interactionElementsNode}
`; const lightDomAutofocusTemplate = html`
`; function createShadowDomNode() { const shadowDomNode = renderLitAsNode(html`
`); const rootElementShadow = shadowDomNode.querySelector('#rootElementShadow'); rootElementShadow.attachShadow({ mode: 'open' }); 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 = 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 = 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 = document.getElementById('rootElement'); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); 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 = 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 = 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(); }); describe('Tabbing into window', () => { it('restores focus within root element', async () => { await fixture(lightDomTemplate); const root = document.getElementById('rootElement'); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); // Simulate tab in window simulateTabInWindow(document.getElementById('outside-1')); expect(getDeepActiveElement()).to.equal(focusableElements[0]); // Simulate shift+tab in window simulateTabInWindow(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 = el.querySelector('#rootElementShadow'); const focusableElements = getFocusableElements(root); const { disconnect } = containFocus(root); // Simulate tab in window simulateTabInWindow(document.getElementById('outside-1')); expect(getDeepActiveElement()).to.equal(focusableElements[0]); // Simulate shift+tab in window simulateTabInWindow(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 = 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(); }); }); });