/* eslint-disable lit-a11y/click-events-have-key-events */ import { expect, fixture, fixtureSync } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { OverlayController } from '@lion/ui/overlays.js'; import { browserDetection } from '@lion/ui/core.js'; import { normalizeTransformStyle } from '../test-helpers/normalizeTransformStyle.js'; /** * @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig * @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement */ /** * Make sure we never use a native button element, since its dimensions * are not cross browser consistent * For debugging purposes, add colors... * @param {{clickHandler?: function; width?: number; height?: number}} opts */ function createInvokerSync({ clickHandler = () => {}, width = 100, height = 20 }) { return /** @type {HTMLDivElement} */ ( fixtureSync(html`
Invoker
`) ); } /** * @param {{ width?: number; height?: number }} opts */ function createContentSync({ width = 80, height = 20 }) { return /** @type {HTMLDivElement} */ ( fixtureSync(html`
Content
`) ); } const withLocalTestConfig = () => /** @type {OverlayConfig} */ ({ placementMode: 'local', contentNode: /** @type {HTMLElement} */ (fixtureSync(html`
my content
`)), invokerNode: /** @type {HTMLElement} */ ( fixtureSync(html`
Invoker
`) ), }); describe('Local Positioning', () => { // Please use absolute positions in the tests below to prevent the HTML generated by // the test runner from interfering. describe('Positioning', () => { it('creates a Popper instance on the controller when shown, keeps it when hidden', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), }); await ctrl.show(); expect(ctrl._popper.state.modifiersData).to.exist; await ctrl.hide(); expect(ctrl._popper.state.modifiersData).to.exist; }); it('positions correctly', async () => { // smoke test for integration of popper const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: /** @type {HTMLElement} */ ( fixtureSync(html`
`) ), invokerNode: /** @type {HTMLElement} */ ( fixtureSync(html`
`) ), }); await fixture(html`
${ctrl.invokerNode}${ctrl.__wrappingDialogNode}
`); await ctrl.show(); // TODO: test fails on Firefox, but looks fine in browser => try again in a later version and investigate when persists (or move to anchor positioning when available in all browsers) if (browserDetection.isFirefox) { return; } expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal( 'translate(70px, -508px)', ); }); it('uses top as the default placement', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: /** @type {HTMLElement} */ ( fixtureSync(html`
`) ), invokerNode: /** @type {HTMLElement} */ ( fixtureSync(html`
ctrl.show()} >
`) ), }); await fixture(html`
${ctrl.invokerNode}${ctrl.contentWrapperNode}
`); await ctrl.show(); expect(ctrl.contentWrapperNode.getAttribute('data-popper-placement')).to.equal('top'); }); it('positions to preferred place if placement is set and space is available', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: /** @type {HTMLElement} */ ( fixtureSync(html`
`) ), invokerNode: /** @type {HTMLElement} */ ( fixtureSync(html`
ctrl.show()} >
`) ), popperConfig: { placement: 'left-start', }, }); await fixture(html`
${ctrl.invokerNode}${ctrl.contentWrapperNode}
`); await ctrl.show(); expect(ctrl.contentWrapperNode.getAttribute('data-popper-placement')).to.equal('left-start'); }); it('positions to different place if placement is set and no space is available', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: /** @type {HTMLElement} */ ( fixtureSync(html`
invoker
`) ), invokerNode: /** @type {HTMLElement} */ ( fixtureSync(html`
ctrl.show()}> content
`) ), popperConfig: { placement: 'left', }, }); await fixture(html`
${ctrl.invokerNode}${ctrl.contentWrapperNode}
`); await ctrl.show(); expect(ctrl.contentWrapperNode.getAttribute('data-popper-placement')).to.equal('right'); }); it('allows the user to override default Popper modifiers', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: /** @type {HTMLElement} */ ( fixtureSync(html`
`) ), invokerNode: /** @type {HTMLElement} */ ( fixtureSync(html`
ctrl.show()} >
`) ), popperConfig: { modifiers: [ { name: 'keepTogether', enabled: false, }, { name: 'offset', enabled: true, options: { offset: [0, 16] } }, ], }, }); await fixture(html`
${ctrl.invokerNode}${ctrl.contentWrapperNode}
`); await ctrl.show(); expect(ctrl._popper.state.modifiersData.offset.auto).to.eql({ x: 0, y: 16 }); }); it('positions the Popper element correctly on show', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: createContentSync({ width: 80, height: 20 }), invokerNode: createInvokerSync({ clickHandler: () => ctrl.show(), width: 100, height: 20 }), popperConfig: { placement: 'top', }, }); await fixture(html`
${ctrl.invokerNode}${ctrl.__wrappingDialogNode}
`); await ctrl.show(); // TODO: test fails on Firefox, but looks fine in browser => try again in a later version and investigate when persists (or move to anchor positioning when available in all browsers) if (browserDetection.isFirefox) { return; } // N.B. margin between invoker and content = 8px expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal( 'translate(110px, -308px)', '110 = (100 + (100-80)/2); -308= 300 + 8', ); await ctrl.hide(); await ctrl.show(); expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal( 'translate(110px, -308px)', 'Popper positioning values should be identical after hiding and showing', ); }); // TODO: Reenable test and make sure it passes it.skip('updates placement properly even during hidden state', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: /** @type {HTMLElement} */ ( fixtureSync(html`
`) ), invokerNode: /** @type {HTMLElement} */ ( fixtureSync(html`
ctrl.show()} >
`) ), popperConfig: { placement: 'top', modifiers: [ { name: 'offset', enabled: true, options: { offset: [0, 10], }, }, ], }, }); await fixture(html`
${ctrl.invokerNode} ${ctrl.contentWrapperNode}
`); await ctrl.show(); expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal( 'translate3d(10px, -30px, 0px)', 'Popper positioning values', ); await ctrl.hide(); await ctrl.updateConfig({ popperConfig: { modifiers: [ { name: 'offset', enabled: true, options: { offset: [0, 20], }, }, ], }, }); await ctrl.show(); expect(ctrl._popper.options.modifiers.offset.offset).to.equal('0, 20px'); expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal( 'translate3d(10px, -40px, 0px)', 'Popper positioning Y value should be 10 less than previous, due to the added extra 10px offset', ); }); // TODO: Not yet implemented it.skip('updates positioning correctly during shown state when config gets updated', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: /** @type {HTMLElement} */ ( fixtureSync(html`
`) ), invokerNode: /** @type {HTMLElement} */ ( fixtureSync(html`
ctrl.show()}> Invoker
`) ), popperConfig: { placement: 'top', modifiers: [ { name: 'offset', enabled: true, options: { offset: [0, 10], }, }, ], }, }); await fixture(html`
${ctrl.invokerNode} ${ctrl.contentWrapperNode}
`); await ctrl.show(); expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal( 'translate3d(10px, -30px, 0px)', 'Popper positioning values', ); await ctrl.updateConfig({ popperConfig: { modifiers: [{ name: 'offset', enabled: true, options: { offset: [0, 20] } }], }, }); expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal( 'translate3d(10px, -40px, 0px)', 'Popper positioning Y value should be 10 less than previous, due to the added extra 10px offset', ); }); it('can set the contentNode minWidth as the invokerNode width', async () => { const invokerNode = /** @type {HTMLElement} */ ( await fixture(html`
invoker
`) ); const ctrl = new OverlayController({ ...withLocalTestConfig(), inheritsReferenceWidth: 'min', invokerNode, }); await ctrl.show(); expect(ctrl.contentWrapperNode.style.minWidth).to.equal('60px'); }); it('can set the contentNode maxWidth as the invokerNode width', async () => { const invokerNode = /** @type {HTMLElement} */ ( await fixture(html`
invoker
`) ); const ctrl = new OverlayController({ ...withLocalTestConfig(), inheritsReferenceWidth: 'max', invokerNode, }); await ctrl.show(); expect(ctrl.contentWrapperNode.style.maxWidth).to.equal('60px'); }); it('can set the contentNode width as the invokerNode width', async () => { const invokerNode = /** @type {HTMLElement} */ ( await fixture(html`
invoker
`) ); const ctrl = new OverlayController({ ...withLocalTestConfig(), inheritsReferenceWidth: 'full', invokerNode, }); await ctrl.show(); expect(ctrl.contentWrapperNode.style.width).to.equal('60px'); }); }); });