import { fixture, oneEvent, elementUpdated, nextFrame, fixtureCleanup, } from '@open-wc/testing-helpers'; import { describe, it, expect, afterEach } from 'vitest'; import { TemplateResult, html } from 'lit'; import '../src/modal-manager'; import { ModalConfig } from '../src/modal-config'; import { ModalManager } from '../src/modal-manager'; import { ModalManagerMode } from '../src/modal-manager-mode'; import { ModalManagerInterface } from '../src/modal-manager-interface'; import { getTabbableElements } from '../src/shoelace/tabbable'; describe('Modal Manager', () => { afterEach(() => { fixtureCleanup(); }); it('defaults to closed', async () => { const el = (await fixture(html` `)) as ModalManager; expect(el.mode).to.equal('closed'); }); it('can be closed by calling closeModal', async () => { const el = (await fixture(html` `)) as ModalManager; el.customModalContent = 'foo' as unknown as TemplateResult; await elementUpdated(el); expect(el.customModalContent).to.equal('foo'); el.closeModal(); await elementUpdated(el); expect(el.mode).to.equal('closed'); expect(el.customModalContent).to.equal(undefined); }); it('can be closed by clicking on the backdrop', async () => { const el = (await fixture(html` `)) as ModalManager; const backdrop = el.shadowRoot?.querySelector('.backdrop'); const clickEvent = new MouseEvent('click'); backdrop?.dispatchEvent(clickEvent); await elementUpdated(el); expect(el.mode).to.equal('closed'); }); it('emits a modeChanged event when opening', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); setTimeout(() => { el.showModal({ config }); }); const response = await oneEvent(el, 'modeChanged'); expect(response.detail.mode).to.equal(ModalManagerMode.Open); }); it('emits a modeChanged event when closing', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); await el.showModal({ config }); await elementUpdated(el); await nextFrame(); setTimeout(() => { el.closeModal(); }); const response = await oneEvent(el, 'modeChanged'); expect(response.detail.mode).to.equal(ModalManagerMode.Closed); }); it('can show a modal', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); el.showModal({ config }); await elementUpdated(el); expect(el.mode).to.equal(ModalManagerMode.Open); }); it('sets the --containerHeight CSS property when the window resizes', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); el.showModal({ config }); await elementUpdated(el); const event = new Event('resize'); const propBefore = el.style.getPropertyValue('--containerHeight'); expect(propBefore).to.equal(''); window.dispatchEvent(event); await elementUpdated(el); const propAfter = el.style.getPropertyValue('--containerHeight'); expect(propAfter).to.not.equal(''); }); it('calls the userClosedModalCallback when the user taps the backdrop', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); let callbackCalled = false; const callback = (): void => { callbackCalled = true; }; el.showModal({ config, userClosedModalCallback: callback, }); await elementUpdated(el); const backdrop = el.shadowRoot?.querySelector('.backdrop'); const clickEvent = new MouseEvent('click'); backdrop?.dispatchEvent(clickEvent); await elementUpdated(el); expect(callbackCalled).to.equal(true); }); it('does not call the userClosedModalCallback when the modal just closes', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); let callbackCalled = false; const callback = (): void => { callbackCalled = true; }; el.showModal({ config, userClosedModalCallback: callback, }); await elementUpdated(el); el.closeModal(); await elementUpdated(el); expect(callbackCalled).to.equal(false); }); it('calls the userPressedLeftNavButtonCallback when the user clicks the left nav button', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); config.showLeftNavButton = true; let callbackCalled = false; const callback = (): void => { callbackCalled = true; }; el.showModal({ config, userPressedLeftNavButtonCallback: callback, }); await elementUpdated(el); const modalTemplate = el.shadowRoot?.querySelector('modal-template'); expect(modalTemplate).to.exist; modalTemplate?.dispatchEvent(new Event('leftNavButtonPressed')); await elementUpdated(el); expect(callbackCalled).to.equal(true); }); it('mode is set to closed when close button is pressed', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); el.showModal({ config }); await elementUpdated(el); expect(el.mode).to.equal('open'); const modal = el.shadowRoot?.querySelector('modal-template'); const closeButton = modal?.shadowRoot?.querySelector('.close-button'); const clickEvent = new MouseEvent('click'); closeButton?.dispatchEvent(clickEvent); await elementUpdated(el); expect(el.mode).to.equal('closed'); }); it('mode is set to closed when close button gets spacebar pressed', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); el.showModal({ config }); await elementUpdated(el); expect(el.mode).to.equal('open'); const modal = el.shadowRoot?.querySelector('modal-template'); const closeButton = modal?.shadowRoot?.querySelector('.close-button'); // Close with keyboard const spacebarEvent = new KeyboardEvent('keydown', { key: ' ' }); closeButton?.dispatchEvent(spacebarEvent); await elementUpdated(el); expect(el.mode).to.equal('closed'); }); it('mode remains open when close button gets non-button keypress', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); el.showModal({ config }); await elementUpdated(el); expect(el.mode).to.equal('open'); const modal = el.shadowRoot?.querySelector('modal-template'); const closeButton = modal?.shadowRoot?.querySelector('.close-button'); // Close with keyboard const keyboardEvent = new KeyboardEvent('keydown', { key: '.' }); closeButton?.dispatchEvent(keyboardEvent); await elementUpdated(el); expect(el.mode).to.equal('open'); }); it('allows the user to close by clicking on the backdrop if configured to', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); config.closeOnBackdropClick = true; el.showModal({ config }); await elementUpdated(el); const backdrop = el.shadowRoot?.querySelector('.backdrop'); const clickEvent = new MouseEvent('click'); backdrop?.dispatchEvent(clickEvent); await elementUpdated(el); expect(el.mode).to.equal('closed'); }); it("doesn't allow the user to close by clicking on the backdrop if configured to", async () => { const el = (await fixture(html` `)) as ModalManagerInterface; const config = new ModalConfig(); config.closeOnBackdropClick = false; el.showModal({ config }); await elementUpdated(el); const backdrop = el.shadowRoot?.querySelector('.backdrop'); const clickEvent = new MouseEvent('click'); backdrop?.dispatchEvent(clickEvent); await elementUpdated(el); expect(el.getMode()).to.equal('open'); }); it('ia logo should not visible on modal', async () => { const el = (await fixture(html` `)) as ModalManagerInterface; const config = new ModalConfig(); config.showHeaderLogo = false; el.showModal({ config }); await elementUpdated(el); const logoIcon = el.shadowRoot?.querySelector('.logo-icon'); expect(logoIcon).to.not.exist; }); it('should trap Tab key', async () => { const el = (await fixture(html` `)) as ModalManager; const config = new ModalConfig(); el.showModal({ config }); await elementUpdated(el); expect(el.mode).to.equal('open'); // Tab once to focus const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' }); document.dispatchEvent(tabEvent); await elementUpdated(el); await nextFrame(); // Should be only one tabbable element const modal = el.shadowRoot?.querySelector('modal-template') as HTMLElement; const tabbableElements = getTabbableElements(modal); expect(tabbableElements?.length).to.equal(1); const closeButton = modal?.shadowRoot?.querySelector( '.close-button', ) as HTMLElement; expect(modal?.shadowRoot?.activeElement).to.equal(closeButton); // Tab again el.dispatchEvent(tabEvent); await elementUpdated(el); await nextFrame(); // Should be only one tabbable element expect(modal?.shadowRoot?.activeElement).to.equal(closeButton); // Shift + Tab const shiftTabEvent = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, }); document.dispatchEvent(shiftTabEvent); await elementUpdated(el); await nextFrame(); // Should be only one tabbable element expect(modal?.shadowRoot?.activeElement).to.equal(closeButton); }); it('returns keyboard focus to the triggering element on close', async () => { const config = new ModalConfig(); const el = (await fixture(html`
`)) as HTMLDivElement; const openBtn = el.querySelector('#open-modal-btn') as HTMLButtonElement; const modal = el.querySelector('modal-manager') as ModalManager; // Focus is initially on the Open button openBtn.focus(); expect(document.activeElement).to.equal(openBtn); // Focus enters the modal when it is opened openBtn.click(); await nextFrame(); expect(document.activeElement).to.equal(modal); // With the modal already open, simulate showing different content. // This step is to ensure that even if showModal is called multiple times, we still // maintain the originally-focused element (subsequent calls do not overwrite it). modal.showModal({ config: new ModalConfig() }); await nextFrame(); // Focus returns to the Open button when the modal closes modal.closeModal(); await modal.updateComplete; expect(document.activeElement).to.equal(openBtn); }); });