/* eslint-disable no-new */ import { expect, html, fixture, aTimeout, defineCE, unsafeStatic, nextFrame, } from '@open-wc/testing'; import { fixtureSync } from '@open-wc/testing-helpers'; import '@lion/core/test-helpers/keyboardEventShimIE.js'; import sinon from 'sinon'; import { keyCodes } from '../src/utils/key-codes.js'; import { simulateTab } from '../src/utils/simulate-tab.js'; import { OverlayController } from '../src/OverlayController.js'; import { overlays } from '../src/overlays.js'; const withGlobalTestConfig = () => ({ placementMode: 'global', contentNode: fixtureSync(html`
my content
`), }); const withLocalTestConfig = () => ({ placementMode: 'local', contentNode: fixtureSync(html`
my content
`), invokerNode: fixtureSync(html`
Invoker
`), }); afterEach(() => { overlays.teardown(); }); describe('OverlayController', () => { describe('Init', () => { it('adds OverlayController instance to OverlayManager', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); expect(ctrl.manager).to.equal(overlays); expect(overlays.list).to.include(ctrl); }); it('prepares a content node wrapper', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); expect(ctrl.content).not.to.be.undefined; expect(ctrl.contentNode.parentElement).to.equal(ctrl.content); }); describe('Z-index on local overlays', () => { let contentNode; async function createZNode(zIndexVal, { mode } = {}) { if (mode === 'global') { contentNode = await fixture(html`
I should be on top
`); } if (mode === 'inline') { contentNode = await fixture(html`
I should be on top
`); } return contentNode; } it('sets a z-index to make sure overlay is painted on top of siblings', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: await createZNode('auto', { mode: 'global' }), }); await ctrl.show(); expect(ctrl.content.style.zIndex).to.equal('1'); ctrl.updateConfig({ contentNode: await createZNode('auto', { mode: 'inline' }) }); await ctrl.show(); expect(ctrl.content.style.zIndex).to.equal('1'); ctrl.updateConfig({ contentNode: await createZNode('0', { mode: 'global' }) }); await ctrl.show(); expect(ctrl.content.style.zIndex).to.equal('1'); ctrl.updateConfig({ contentNode: await createZNode('0', { mode: 'inline' }) }); await ctrl.show(); expect(ctrl.content.style.zIndex).to.equal('1'); }); it.skip("doesn't set a z-index when contentNode already has >= 1", async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: await createZNode('1', { mode: 'global' }), }); await ctrl.show(); expect(ctrl.content.style.zIndex).to.equal(''); ctrl.updateConfig({ contentNode: await createZNode('auto', { mode: 'inline' }) }); await ctrl.show(); expect(ctrl.content.style.zIndex).to.equal(''); ctrl.updateConfig({ contentNode: await createZNode('2', { mode: 'global' }) }); await ctrl.show(); expect(ctrl.content.style.zIndex).to.equal(''); ctrl.updateConfig({ contentNode: await createZNode('2', { mode: 'inline' }) }); await ctrl.show(); expect(ctrl.content.style.zIndex).to.equal(''); }); it("doesn't touch the value of .contentNode", async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: await createZNode('auto', { mode: 'global' }), }); expect(ctrl.contentNode.style.zIndex).to.equal(''); }); }); describe('Render target', () => { it('creates global target for placement mode "global"', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); expect(ctrl._renderTarget).to.equal(overlays.globalRootNode); }); it.skip('creates local target next to sibling for placement mode "local"', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), invokerNode: await fixture(html` `), }); expect(ctrl._renderTarget).to.be.undefined; expect(ctrl.content).to.equal(ctrl.invokerNode.nextElementSibling); }); it('keeps local target for placement mode "local" when already connected', async () => { const parentNode = await fixture(html`
Content
`); const contentNode = parentNode.querySelector('#content'); const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode, }); expect(ctrl._renderTarget).to.equal(parentNode); }); }); }); // TODO: Add teardown feature tests describe('Teardown', () => { it('removes the contentNodeWrapper from global rootnode upon teardown', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); expect(ctrl.manager.globalRootNode.children.length).to.equal(1); ctrl.teardown(); expect(ctrl.manager.globalRootNode.children.length).to.equal(0); }); }); describe('Node Configuration', () => { it('accepts an .contentNode to directly set content', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), contentNode: await fixture('

direct node

'), }); expect(ctrl.contentNode).to.have.trimmed.text('direct node'); }); it('accepts an .invokerNode to directly set invoker', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), invokerNode: await fixture(''), }); expect(ctrl.invokerNode).to.have.trimmed.text('invoke'); }); }); describe('Feature Configuration', () => { describe('trapsKeyboardFocus', () => { it('offers an hasActiveTrapsKeyboardFocus flag', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), trapsKeyboardFocus: true, }); expect(ctrl.hasActiveTrapsKeyboardFocus).to.be.false; await ctrl.show(); expect(ctrl.hasActiveTrapsKeyboardFocus).to.be.true; }); it('focuses the overlay on show', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), trapsKeyboardFocus: true, }); await ctrl.show(); expect(ctrl.contentNode).to.equal(document.activeElement); }); it('keeps focus within the overlay e.g. you can not tab out by accident', async () => { const contentNode = await fixture(html`
`); const ctrl = new OverlayController({ ...withGlobalTestConfig(), trapsKeyboardFocus: true, contentNode, }); await ctrl.show(); const elOutside = await fixture(html` `); const input1 = ctrl.contentNode.querySelectorAll('input')[0]; const input2 = ctrl.contentNode.querySelectorAll('input')[1]; input2.focus(); // this mimics a tab within the contain-focus system used const event = new CustomEvent('keydown', { detail: 0, bubbles: true }); event.keyCode = keyCodes.tab; window.dispatchEvent(event); expect(elOutside).to.not.equal(document.activeElement); expect(input1).to.equal(document.activeElement); }); it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => { const contentNode = await fixture(html`
`); const ctrl = new OverlayController({ ...withGlobalTestConfig(), contentNode, trapsKeyboardFocus: true, }); // add element to dom to allow focus await fixture(html` ${ctrl.content} `); await ctrl.show(); const elOutside = await fixture(html` `); const input = ctrl.contentNode.querySelector('input'); input.focus(); simulateTab(); expect(elOutside).to.equal(document.activeElement); }); it('keeps focus within overlay with multiple overlays with all traps on true', async () => { const ctrl0 = new OverlayController({ ...withGlobalTestConfig(), trapsKeyboardFocus: true, }); const ctrl1 = new OverlayController({ ...withGlobalTestConfig(), trapsKeyboardFocus: true, }); await ctrl0.show(); await ctrl1.show(); expect(ctrl0.hasActiveTrapsKeyboardFocus).to.be.false; expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.true; await ctrl1.hide(); expect(ctrl0.hasActiveTrapsKeyboardFocus).to.be.true; expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.false; }); }); describe('hidesOnEsc', () => { it('hides when [escape] is pressed', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), hidesOnEsc: true, }); await ctrl.show(); ctrl.contentNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' })); await aTimeout(); expect(ctrl.isShown).to.be.false; }); it('stays shown when [escape] is pressed on outside element', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), hidesOnEsc: true, }); await ctrl.show(); document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' })); expect(ctrl.isShown).to.be.true; }); }); describe('hidesOnHideEventInContentNode', () => { it('hides content on hide event within the content ', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), hidesOnHideEventInContentNode: true, contentNode: fixtureSync(html`
my content
`), }); await ctrl.show(); const closeBtn = ctrl.contentNode.querySelector('button'); closeBtn.click(); expect(ctrl.isShown).to.be.false; }); it('does stop propagation of the "hide" event to not pollute the event stack and to prevent side effects', () => { // TODO: how to test this? }); }); describe('hidesOnOutsideClick', () => { it('hides on outside click', async () => { const contentNode = await fixture('
Content
'); const ctrl = new OverlayController({ ...withGlobalTestConfig(), hidesOnOutsideClick: true, contentNode, }); await ctrl.show(); document.body.click(); await aTimeout(); expect(ctrl.isShown).to.be.false; }); it('doesn\'t hide on "inside" click', async () => { const invokerNode = await fixture(''); const contentNode = await fixture('
Content
'); const ctrl = new OverlayController({ ...withGlobalTestConfig(), hidesOnOutsideClick: true, contentNode, invokerNode, }); await ctrl.show(); // Don't hide on invoker click ctrl.invokerNode.click(); await aTimeout(); expect(ctrl.isShown).to.be.true; // Don't hide on inside (content) click ctrl.contentNode.click(); await aTimeout(); expect(ctrl.isShown).to.be.true; // Important to check if it can be still shown after, because we do some hacks inside await ctrl.hide(); expect(ctrl.isShown).to.be.false; await ctrl.show(); expect(ctrl.isShown).to.be.true; }); it('doesn\'t hide on "inside sub shadow dom" click', async () => { const invokerNode = await fixture(''); const contentNode = await fixture('
Content
'); const ctrl = new OverlayController({ ...withGlobalTestConfig(), hidesOnOutsideClick: true, contentNode, invokerNode, }); await ctrl.show(); // Works as well when clicked content element lives in shadow dom const tagString = defineCE( class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.shadowRoot.innerHTML = '
'; } }, ); const tag = unsafeStatic(tagString); ctrl.updateConfig({ contentNode: await fixture(html`
Content
<${tag}>
`), }); await ctrl.show(); // Don't hide on inside shadowDom click ctrl.contentNode .querySelector(tagString) .shadowRoot.querySelector('button') .click(); await aTimeout(); expect(ctrl.isShown).to.be.true; // Important to check if it can be still shown after, because we do some hacks inside await ctrl.hide(); expect(ctrl.isShown).to.be.false; await ctrl.show(); expect(ctrl.isShown).to.be.true; }); it('works with 3rd party code using "event.stopPropagation()" on bubble phase', async () => { const invokerNode = await fixture('
Invoker
'); const contentNode = await fixture('
Content
'); const ctrl = new OverlayController({ ...withLocalTestConfig(), hidesOnOutsideClick: true, contentNode, invokerNode, }); const dom = await fixture(`
This element prevents our handlers from reaching the document click handler.
`); await ctrl.show(); expect(ctrl.isShown).to.equal(true); dom.querySelector('third-party-noise').click(); await aTimeout(); expect(ctrl.isShown).to.equal(false); // Important to check if it can be still shown after, because we do some hacks inside await ctrl.show(); expect(ctrl.isShown).to.equal(true); }); it('works with 3rd party code using "event.stopPropagation()" on capture phase', async () => { const invokerNode = await fixture(html`
Invoker
`); const contentNode = await fixture('
Content
'); const ctrl = new OverlayController({ ...withLocalTestConfig(), hidesOnOutsideClick: true, contentNode, invokerNode, }); const dom = await fixture(`
This element prevents our handlers from reaching the document click handler.
`); dom.querySelector('third-party-noise').addEventListener( 'click', event => { event.stopPropagation(); }, true, ); await ctrl.show(); expect(ctrl.isShown).to.equal(true); dom.querySelector('third-party-noise').click(); await aTimeout(); expect(ctrl.isShown).to.equal(false); // Important to check if it can be still shown after, because we do some hacks inside await ctrl.show(); expect(ctrl.isShown).to.equal(true); }); }); describe('elementToFocusAfterHide', () => { it('focuses body when hiding by default', async () => { const contentNode = await fixture('
'); const ctrl = new OverlayController({ ...withGlobalTestConfig(), viewportConfig: { placement: 'top-left', }, contentNode, }); await ctrl.show(); const input = contentNode.querySelector('input'); input.focus(); expect(document.activeElement).to.equal(input); await ctrl.hide(); await nextFrame(); // moving focus to body takes time? expect(document.activeElement).to.equal(document.body); }); it('supports elementToFocusAfterHide option to focus it when hiding', async () => { const input = await fixture(''); const contentNode = await fixture('
'); const ctrl = new OverlayController({ ...withGlobalTestConfig(), elementToFocusAfterHide: input, contentNode, }); await ctrl.show(); const textarea = contentNode.querySelector('textarea'); textarea.focus(); expect(document.activeElement).to.equal(textarea); await ctrl.hide(); expect(document.activeElement).to.equal(input); }); it('allows to set elementToFocusAfterHide on show', async () => { const input = await fixture(''); const contentNode = await fixture('
'); const ctrl = new OverlayController({ ...withGlobalTestConfig(), viewportConfig: { placement: 'top-left', }, contentNode, }); await ctrl.show(input); const textarea = contentNode.querySelector('textarea'); textarea.focus(); expect(document.activeElement).to.equal(textarea); await ctrl.hide(); expect(document.activeElement).to.equal(input); }); }); describe('preventsScroll', () => { it('prevent scrolling the background', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), preventsScroll: true, }); await ctrl.show(); expect(getComputedStyle(document.body).overflow).to.equal('hidden'); await ctrl.hide(); expect(getComputedStyle(document.body).overflow).to.equal('visible'); }); it('keeps preventing of scrolling when multiple overlays are opened and closed', async () => { const ctrl0 = new OverlayController({ ...withGlobalTestConfig(), preventsScroll: true, }); const ctrl1 = new OverlayController({ ...withGlobalTestConfig(), preventsScroll: true, }); await ctrl0.show(); await ctrl1.show(); await ctrl1.hide(); expect(getComputedStyle(document.body).overflow).to.equal('hidden'); }); }); describe('hasBackdrop', () => { it('has no backdrop by default', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); await ctrl.show(); expect(ctrl.backdropNode).to.be.undefined; }); it('supports a backdrop option', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), hasBackdrop: false, }); await ctrl.show(); expect(ctrl.backdropNode).to.be.undefined; await ctrl.hide(); const controllerWithBackdrop = new OverlayController({ ...withGlobalTestConfig(), hasBackdrop: true, }); await controllerWithBackdrop.show(); expect(controllerWithBackdrop.backdropNode).to.have.class('global-overlays__backdrop'); }); it('reenables the backdrop when shown/hidden/shown', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), hasBackdrop: true, }); await ctrl.show(); expect(ctrl.backdropNode).to.have.class('global-overlays__backdrop'); await ctrl.hide(); await ctrl.show(); expect(ctrl.backdropNode).to.have.class('global-overlays__backdrop'); }); it('adds and stacks backdrops if .hasBackdrop is enabled', async () => { const ctrl0 = new OverlayController({ ...withGlobalTestConfig(), hasBackdrop: true, }); await ctrl0.show(); expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop'); const ctrl1 = new OverlayController({ ...withGlobalTestConfig(), hasBackdrop: false, }); await ctrl1.show(); expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop'); expect(ctrl1.backdropNode).to.be.undefined; const ctrl2 = new OverlayController({ ...withGlobalTestConfig(), hasBackdrop: true, }); await ctrl2.show(); expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop'); expect(ctrl1.backdropNode).to.be.undefined; expect(ctrl2.backdropNode).to.have.class('global-overlays__backdrop'); }); }); describe('isBlocking', () => { it('prevents showing of other overlays', async () => { const ctrl0 = new OverlayController({ ...withGlobalTestConfig(), isBlocking: false, }); const ctrl1 = new OverlayController({ ...withGlobalTestConfig(), isBlocking: false, }); const ctrl2 = new OverlayController({ ...withGlobalTestConfig(), isBlocking: true, }); const ctrl3 = new OverlayController({ ...withGlobalTestConfig(), isBlocking: false, }); await ctrl0.show(); await ctrl1.show(); await ctrl2.show(); // blocking expect(ctrl0.content).to.not.be.displayed; expect(ctrl1.content).to.not.be.displayed; expect(ctrl2.content).to.be.displayed; await ctrl3.show(); expect(ctrl3.content).to.be.displayed; await ctrl2.hide(); expect(ctrl0.content).to.be.displayed; expect(ctrl1.content).to.be.displayed; await ctrl2.show(); // blocking expect(ctrl0.content).to.not.be.displayed; expect(ctrl1.content).to.not.be.displayed; expect(ctrl2.content).to.be.displayed; expect(ctrl3.content).to.not.be.displayed; }); it('keeps backdrop status when used in combination with blocking', async () => { const ctrl0 = new OverlayController({ ...withGlobalTestConfig(), isBlocking: false, hasBackdrop: true, }); await ctrl0.show(); const ctrl1 = new OverlayController({ ...withGlobalTestConfig(), isBlocking: false, hasBackdrop: true, }); await ctrl1.show(); await ctrl1.hide(); expect(ctrl0.hasActiveBackdrop).to.be.true; expect(ctrl1.hasActiveBackdrop).to.be.false; await ctrl1.show(); expect(ctrl0.hasActiveBackdrop).to.be.true; expect(ctrl1.hasActiveBackdrop).to.be.true; }); }); }); describe('Show / Hide / Toggle', () => { it('has .isShown which defaults to false', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); expect(ctrl.isShown).to.be.false; }); it('has async show() which shows the overlay', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); await ctrl.show(); expect(ctrl.isShown).to.be.true; expect(ctrl.show()).to.be.instanceOf(Promise); }); it('has async hide() which hides the overlay', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); await ctrl.hide(); expect(ctrl.isShown).to.be.false; expect(ctrl.hide()).to.be.instanceOf(Promise); }); it('fires "show" event once overlay becomes shown', async () => { const showSpy = sinon.spy(); const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); ctrl.addEventListener('show', showSpy); await ctrl.show(); expect(showSpy.callCount).to.equal(1); await ctrl.show(); expect(showSpy.callCount).to.equal(1); }); it('fires "before-show" event right before overlay becomes shown', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); const eventSpy = sinon.spy(); ctrl.addEventListener('before-show', eventSpy); ctrl.addEventListener('show', eventSpy); await ctrl.show(); expect(eventSpy.getCall(0).args[0].type).to.equal('before-show'); expect(eventSpy.getCall(1).args[0].type).to.equal('show'); expect(eventSpy.callCount).to.equal(2); await ctrl.show(); expect(eventSpy.callCount).to.equal(2); }); it('fires "hide" event once overlay becomes hidden', async () => { const hideSpy = sinon.spy(); const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); ctrl.addEventListener('hide', hideSpy); await ctrl.show(); await ctrl.hide(); expect(hideSpy.callCount).to.equal(1); await ctrl.hide(); expect(hideSpy.callCount).to.equal(1); }); it('fires "before-hide" event right before overlay becomes hidden', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); const eventSpy = sinon.spy(); ctrl.addEventListener('before-hide', eventSpy); ctrl.addEventListener('hide', eventSpy); await ctrl.show(); await ctrl.hide(); expect(eventSpy.getCall(0).args[0].type).to.equal('before-hide'); expect(eventSpy.getCall(1).args[0].type).to.equal('hide'); expect(eventSpy.callCount).to.equal(2); await ctrl.hide(); expect(eventSpy.callCount).to.equal(2); }); it('can be toggled', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), }); await ctrl.toggle(); expect(ctrl.isShown).to.be.true; await ctrl.toggle(); expect(ctrl.isShown).to.be.false; await ctrl.toggle(); expect(ctrl.isShown).to.be.true; // check for hide expect(ctrl.toggle()).to.be.instanceOf(Promise); // check for show expect(ctrl.toggle()).to.be.instanceOf(Promise); }); it('makes sure the latest shown overlay is visible', async () => { const ctrl0 = new OverlayController({ ...withGlobalTestConfig(), }); const ctrl1 = new OverlayController({ ...withGlobalTestConfig(), }); await ctrl0.show(); const rect = ctrl0.contentNode.getBoundingClientRect(); const getTopEl = () => document.elementFromPoint(Math.ceil(rect.left), Math.ceil(rect.top)); await ctrl0.show(); expect(getTopEl()).to.equal(ctrl0.contentNode); await ctrl1.show(); expect(getTopEl()).to.equal(ctrl1.contentNode); await ctrl0.show(); expect(getTopEl()).to.equal(ctrl0.contentNode); }); }); describe('Update Configuration', () => { it('reinitializes content', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: await fixture( html`
content1
`, ), }); await ctrl.show(); // Popper adds inline styles expect(ctrl.content.style.transform).not.to.be.undefined; expect(ctrl.contentNode.textContent).to.include('content1'); ctrl.updateConfig({ placementMode: 'local', contentNode: await fixture( html`
content2
`, ), }); expect(ctrl.contentNode.textContent).to.include('content2'); }); it('respects the initial config provided to new OverlayController(initialConfig)', async () => { const contentNode = fixtureSync(html`
my content
`); const ctrl = new OverlayController({ // This is the shared config placementMode: 'global', handlesAccesibility: true, contentNode, }); ctrl.updateConfig({ // This is the added config placementMode: 'local', hidesOnEsc: true, }); expect(ctrl.placementMode).to.equal('local'); expect(ctrl.handlesAccesibility).to.equal(true); expect(ctrl.contentNode).to.equal(contentNode); }); // TODO: Currently not working, enable again when we fix updateConfig it.skip('allows for updating viewport config placement only, while keeping the content shown', async () => { const contentNode = fixtureSync(html`
my content
`); const ctrl = new OverlayController({ // This is the shared config placementMode: 'global', handlesAccesibility: true, contentNode, }); ctrl.show(); expect( ctrl._contentNodeWrapper.classList.contains('global-overlays__overlay-container--center'), ); expect(ctrl.isShown).to.be.true; ctrl.updateConfig({ viewportConfig: { placement: 'top-right' } }); expect( ctrl._contentNodeWrapper.classList.contains( 'global-overlays__overlay-container--top-right', ), ); expect(ctrl.isShown).to.be.true; }); }); describe('Accessibility', () => { it('adds and removes [aria-expanded] on invoker', async () => { const invokerNode = await fixture('
invoker
'); const ctrl = new OverlayController({ ...withLocalTestConfig(), handlesAccessibility: true, invokerNode, }); expect(ctrl.invokerNode.getAttribute('aria-expanded')).to.equal('false'); await ctrl.show(); expect(ctrl.invokerNode.getAttribute('aria-expanded')).to.equal('true'); await ctrl.hide(); expect(ctrl.invokerNode.getAttribute('aria-expanded')).to.equal('false'); }); it('creates unique id for content', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), handlesAccessibility: true, }); expect(ctrl.contentNode.id).to.contain(ctrl._contentId); }); it('preserves content id when present', async () => { const contentNode = await fixture('
content
'); const ctrl = new OverlayController({ ...withLocalTestConfig(), handlesAccessibility: true, contentNode, }); expect(ctrl.contentNode.id).to.contain('preserved'); }); it('adds [role=dialog] on content', async () => { const invokerNode = await fixture('
invoker
'); const ctrl = new OverlayController({ ...withLocalTestConfig(), handlesAccessibility: true, invokerNode, }); expect(ctrl.contentNode.getAttribute('role')).to.equal('dialog'); }); it('adds attributes inert and aria-hidden="true" on all siblings of rootNode if an overlay is shown', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), trapsKeyboardFocus: true, }); const sibling1 = document.createElement('div'); const sibling2 = document.createElement('div'); document.body.insertBefore(sibling1, ctrl.manager.globalRootNode); document.body.appendChild(sibling2); await ctrl.show(); [sibling1, sibling2].forEach(sibling => { expect(sibling).to.have.attribute('aria-hidden', 'true'); expect(sibling).to.have.attribute('inert'); }); expect(ctrl.content.hasAttribute('aria-hidden')).to.be.false; expect(ctrl.content.hasAttribute('inert')).to.be.false; await ctrl.hide(); [sibling1, sibling2].forEach(sibling => { expect(sibling).to.not.have.attribute('aria-hidden'); expect(sibling).to.not.have.attribute('inert'); }); // cleanup document.body.removeChild(sibling1); document.body.removeChild(sibling2); }); /** * style.userSelect: * - chrome: 'none' * - rest: undefined * * style.pointerEvents: * - chrome: auto * - IE11: visiblePainted */ it('disables pointer events and selection on inert elements', async () => { const ctrl = new OverlayController({ ...withGlobalTestConfig(), trapsKeyboardFocus: true, }); // show+hide are needed to create a root node await ctrl.show(); await ctrl.hide(); const sibling1 = document.createElement('div'); const sibling2 = document.createElement('div'); document.body.insertBefore(sibling1, ctrl.manager.globalRootNode); document.body.appendChild(sibling2); await ctrl.show(); [sibling1, sibling2].forEach(sibling => { expect(window.getComputedStyle(sibling).userSelect).to.be.oneOf(['none', undefined]); expect(window.getComputedStyle(sibling).pointerEvents).to.equal('none'); }); expect(window.getComputedStyle(ctrl.contentNode).userSelect).to.be.oneOf(['auto', undefined]); expect(window.getComputedStyle(ctrl.contentNode).pointerEvents).to.be.oneOf([ 'auto', 'visiblePainted', ]); await ctrl.hide(); [sibling1, sibling2].forEach(sibling => { expect(window.getComputedStyle(sibling).userSelect).to.be.oneOf(['auto', undefined]); expect(window.getComputedStyle(sibling).pointerEvents).to.be.oneOf([ 'auto', 'visiblePainted', ]); }); // cleanup document.body.removeChild(sibling1); document.body.removeChild(sibling2); }); describe('Tooltip', () => { it('adds [aria-describedby] on invoker', async () => { const invokerNode = await fixture('
invoker
'); const ctrl = new OverlayController({ ...withLocalTestConfig(), handlesAccessibility: true, isTooltip: true, invokerNode, }); expect(ctrl.invokerNode.getAttribute('aria-describedby')).to.equal(ctrl._contentId); }); it('adds [role=tooltip] on content', async () => { const invokerNode = await fixture('
invoker
'); const ctrl = new OverlayController({ ...withLocalTestConfig(), handlesAccessibility: true, isTooltip: true, invokerNode, }); expect(ctrl.contentNode.getAttribute('role')).to.equal('tooltip'); }); }); }); describe('Exception handling', () => { it('throws if no .placementMode gets passed on', async () => { expect(() => { new OverlayController({ contentNode: {}, }); }).to.throw('You need to provide a .placementMode ("global"|"local")'); }); it('throws if invalid .placementMode gets passed on', async () => { expect(() => { new OverlayController({ placementMode: 'invalid', }); }).to.throw('"invalid" is not a valid .placementMode, use ("global"|"local")'); }); it('throws if no .contentNode gets passed on', async () => { expect(() => { new OverlayController({ placementMode: 'global', }); }).to.throw('You need to provide a .contentNode'); }); }); });