');
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}>${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(`
${invokerNode}${contentNode}
{
/* propagates */
}}"
>
e.stopPropagation()}">
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(`
${invokerNode}${ctrl.content}
{
/* propagates */
}}"
>
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`
`,
),
});
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('
');
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');
});
});
});