import { expect, fixture, fixtureSync, html } from '@open-wc/testing'; // @ts-ignore import Popper from 'popper.js/dist/esm/popper.min.js'; import { OverlayController } from '../src/OverlayController.js'; import { normalizeTransformStyle } from './utils-tests/local-positioning-helpers.js'; /** * @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig * @typedef {import('../types/OverlayConfig').ViewportPlacement} ViewportPlacement */ 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(/** @type {Popper} */ (ctrl._popper)).to.be.an.instanceof(Popper); expect(/** @type {Popper} */ (ctrl._popper).modifiers).to.exist; await ctrl.hide(); expect(/** @type {Popper} */ (ctrl._popper)).to.be.an.instanceof(Popper); expect(/** @type {Popper} */ (ctrl._popper).modifiers).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.content}
`); await ctrl.show(); expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal( 'translate3d(-30px, -38px, 0px)', 'translate3d should be -30px [to center = (80 - 20)/2*-1] -38px [to place above = 30 + 8 default padding]', ); }); 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.content}
`); await ctrl.show(); expect(ctrl.content.getAttribute('x-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.content}
`); await ctrl.show(); expect(ctrl.content.getAttribute('x-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: 'top-start', }, }); await fixture(html`
${ctrl.invokerNode}${ctrl.content}
`); await ctrl.show(); expect(ctrl.content.getAttribute('x-placement')).to.equal('bottom-start'); }); 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: { keepTogether: { enabled: false, }, offset: { enabled: true, offset: `0, 16px`, }, }, }, }); await fixture(html`
${ctrl.invokerNode}${ctrl.content}
`); await ctrl.show(); const keepTogether = /** @type {Popper} */ (ctrl._popper).modifiers.find( (/** @type {{ name: string }} */ item) => item.name === 'keepTogether', ); const offset = /** @type {Popper} */ (ctrl._popper).modifiers.find( (/** @type {{ name: string }} */ item) => item.name === 'offset', ); expect(keepTogether.enabled).to.be.false; expect(offset.enabled).to.be.true; expect(offset.offset).to.equal('0, 16px'); }); it('positions the Popper element correctly on show', async () => { const ctrl = new OverlayController({ ...withLocalTestConfig(), contentNode: /** @type {HTMLElement} */ (fixtureSync( html`
`, )), invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`
ctrl.show()}>
`)), popperConfig: { placement: 'top', }, }); await fixture(html`
${ctrl.invokerNode}${ctrl.content}
`); await ctrl.show(); expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal( 'translate3d(10px, -28px, 0px)', 'Popper positioning values', ); await ctrl.hide(); await ctrl.show(); expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal( 'translate3d(10px, -28px, 0px)', '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: { offset: { enabled: true, offset: '0, 10px', }, }, }, }); await fixture(html`
${ctrl.invokerNode} ${ctrl.content}
`); await ctrl.show(); expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal( 'translate3d(10px, -30px, 0px)', 'Popper positioning values', ); await ctrl.hide(); await ctrl.updateConfig({ popperConfig: { modifiers: { offset: { enabled: true, offset: '0, 20px', }, }, }, }); await ctrl.show(); expect(/** @type {Popper} */ (ctrl._popper).options.modifiers.offset.offset).to.equal( '0, 20px', ); expect(normalizeTransformStyle(ctrl.content.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: { offset: { enabled: true, offset: '0, 10px', }, }, }, }); await fixture(html`
${ctrl.invokerNode} ${ctrl.content}
`); await ctrl.show(); expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal( 'translate3d(10px, -30px, 0px)', 'Popper positioning values', ); await ctrl.updateConfig({ popperConfig: { modifiers: { offset: { enabled: true, offset: '0, 20px', }, }, }, }); expect(normalizeTransformStyle(ctrl.content.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.content.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.content.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.content.style.width).to.equal('60px'); }); }); });