`)
);
contentNode.style.zIndex = zIndexVal;
}
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(),
});
const { renderTarget } = getProtectedMembers(ctrl);
expect(renderTarget).to.equal(overlays.globalRootNode);
});
it.skip('creates local target next to sibling for placement mode "local"', async () => {
const ctrl = new OverlayController({
...withLocalTestConfig(),
invokerNode: /** @type {HTMLElement} */ (await fixture(html``)),
});
const { renderTarget } = getProtectedMembers(ctrl);
expect(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 = /** @type {HTMLElement} */ (
await fixture(html`
Content
`)
);
const contentNode = /** @type {HTMLElement} */ (parentNode.querySelector('#content'));
const ctrl = new OverlayController({
...withLocalTestConfig(),
contentNode,
});
const { renderTarget } = getProtectedMembers(ctrl);
expect(renderTarget).to.equal(parentNode);
});
it('throws when passing a content node that was created "offline"', async () => {
const contentNode = document.createElement('div');
const createOverlayController = () => {
new OverlayController({
...withLocalTestConfig(),
contentNode,
});
};
expect(createOverlayController).to.throw(
'[OverlayController] Could not find a render target, since the provided contentNode is not connected to the DOM. Make sure that it is connected, e.g. by doing "document.body.appendChild(contentNode)", before passing it on.',
);
});
it('succeeds when passing a content node that was created "online"', async () => {
const contentNode = document.createElement('div');
document.body.appendChild(contentNode);
const overlay = new OverlayController({
...withLocalTestConfig(),
contentNode,
});
const { renderTarget } = getProtectedMembers(overlay);
expect(overlay.contentNode.isConnected).to.be.true;
expect(renderTarget).to.not.be.undefined;
});
});
});
// TODO: Add teardown feature tests
describe('Teardown', () => {
it('removes the contentWrapperNode 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);
});
it('[global] restores contentNode if it was/is a projected node', async () => {
const shadowHost = document.createElement('div');
shadowHost.id = 'shadowHost';
shadowHost.attachShadow({ mode: 'open' });
/** @type {ShadowRoot} */
(shadowHost.shadowRoot).innerHTML = `
`;
const contentNode = document.createElement('div');
contentNode.slot = 'contentNode';
shadowHost.appendChild(contentNode);
const wrapper = /** @type {HTMLElement} */ (await fixture(''));
// Ensure the contentNode is connected to DOM
wrapper.appendChild(shadowHost);
// has one child =
expect(shadowHost.children.length).to.equal(1);
const ctrl = new OverlayController({
...withLocalTestConfig(),
placementMode: 'global',
contentNode,
contentWrapperNode: shadowHost,
});
// has no children as content gets moved to the body
expect(shadowHost.children.length).to.equal(0);
ctrl.teardown();
// restores original light dom in teardown
expect(shadowHost.children.length).to.equal(1);
});
});
describe('Node Configuration', () => {
it('accepts an .contentNode to directly set content', async () => {
const ctrl = new OverlayController({
...withGlobalTestConfig(),
contentNode: /** @type {HTMLElement} */ (await fixture('
'));
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() {
/** @type {ShadowRoot} */
(this.shadowRoot).innerHTML = '';
}
},
);
const tag = unsafeStatic(tagString);
ctrl.updateConfig({
contentNode: /** @type {HTMLElement} */ (
await fixture(html`
Content
<${tag}>${tag}>
`)
),
});
await ctrl.show();
// Don't hide on inside shadowDom click
/** @type {ShadowRoot} */
// @ts-expect-error
(ctrl.contentNode.querySelector(tagString).shadowRoot).querySelector('button').click();
await aTimeout(0);
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 = /** @type {HTMLElement} */ (
await fixture('
This element prevents our handlers from reaching the document click handler.
`,
);
await ctrl.show();
expect(ctrl.isShown).to.equal(true);
const noiseEl = /** @type {HTMLElement} */ (dom.querySelector('#third-party-noise'));
mimicClick(noiseEl);
await aTimeout(0);
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 = /** @type {HTMLElement} */ (
await fixture(html`
`)),
});
expect(ctrl.contentNode.textContent).to.include('content2');
});
it('respects the initial config provided to new OverlayController(initialConfig)', async () => {
const contentNode = /** @type {HTMLElement} */ (fixtureSync(html`
my content
`));
const ctrl = new OverlayController({
// This is the shared config
placementMode: 'global',
handlesAccessibility: true,
contentNode,
});
ctrl.updateConfig({
// This is the added config
placementMode: 'local',
hidesOnEsc: true,
});
expect(ctrl.placementMode).to.equal('local');
expect(ctrl.handlesAccessibility).to.equal(true);
expect(ctrl.contentNode).to.equal(contentNode);
});
// 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 = /** @type {HTMLElement} */ (fixtureSync(html`
')
);
const ctrl = new OverlayController({
...withLocalTestConfig(),
handlesAccessibility: true,
isTooltip: true,
invokerNode,
contentNode,
invokerRelation: 'label',
});
expect(invokerNode.getAttribute('aria-labelledby')).to.equal(contentNode.id);
ctrl.teardown();
expect(invokerNode.getAttribute('aria-labelledby')).to.equal(null);
});
});
});
});
describe('Exception handling', () => {
it('throws if no .placementMode gets passed on', async () => {
const contentNode = document.createElement('div');
// Ensure the contentNode is connected to DOM
document.body.appendChild(contentNode);
expect(() => {
new OverlayController({
contentNode,
});
}).to.throw('[OverlayController] You need to provide a .placementMode ("global"|"local")');
});
it('throws if invalid .placementMode gets passed on', async () => {
expect(() => {
new OverlayController({
// @ts-ignore
placementMode: 'invalid',
});
}).to.throw(
'[OverlayController] "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('[OverlayController] You need to provide a .contentNode');
});
it('throws if contentNodeWrapper is not provided for projected contentNode', async () => {
const shadowHost = document.createElement('div');
shadowHost.attachShadow({ mode: 'open' });
/** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = `
`;
const contentNode = document.createElement('div');
contentNode.slot = 'contentNode';
shadowHost.appendChild(contentNode);
// Ensure the contentNode is connected to DOM
document.body.appendChild(shadowHost);
expect(() => {
new OverlayController({
...withLocalTestConfig(),
contentNode,
});
}).to.throw(
'[OverlayController] You need to provide a .contentWrapperNode when .contentNode is projected',
);
});
it('throws if placementMode is global for a tooltip', async () => {
const contentNode = document.createElement('div');
document.body.appendChild(contentNode);
expect(() => {
new OverlayController({
placementMode: 'global',
contentNode,
isTooltip: true,
handlesAccessibility: true,
});
}).to.throw(
'[OverlayController] .isTooltip should be configured with .placementMode "local"',
);
});
it('throws if handlesAccessibility is false for a tooltip', async () => {
const contentNode = document.createElement('div');
document.body.appendChild(contentNode);
expect(() => {
new OverlayController({
placementMode: 'local',
contentNode,
isTooltip: true,
handlesAccessibility: false,
});
}).to.throw(
'[OverlayController] .isTooltip only takes effect when .handlesAccessibility is enabled',
);
});
});
});