*
*
* And the rest of content is added via `slots` getter by SlotMixin
* The function should move only content projection and ignore the rest
* */
source.innerHTML = `
${variableNothing}
${defaultSlottableProvidedViaSlotsGetter}
${namedSlottable}
`;
moveUserProvidedDefaultSlottablesToTarget(source, target);
expect(target.children.length).to.equal(3);
const test1Element = target.querySelector('.test1');
const test2Element = target.querySelector('.test2');
const test3Element = target.querySelector('.test3');
expect(test1Element?.parentElement === target).to.equal(true);
expect(test2Element?.parentElement === target).to.equal(true);
expect(test3Element?.parentElement === target).to.equal(true);
});
describe('Rerender', () => {
it('supports rerender when SlotRerenderObject provided', async () => {
const tag = defineCE(
// @ts-expect-error
class extends SlotMixin(LitElement) {
static properties = { currentValue: Number };
constructor() {
super();
this.currentValue = 0;
}
get slots() {
return {
...super.slots,
template: () => ({ template: html`
${this.currentValue} ` }),
};
}
render() {
return html`
`;
}
get _templateNode() {
return /** @type HTMLSpanElement */ (
Array.from(this.children).find(elm => elm.slot === 'template')
);
}
},
);
const el = /** @type {* & SlotHost} */ (await fixture(`<${tag}>${tag}>`));
await el.updateComplete;
expect(el._templateNode.slot).to.equal('template');
expect(el._templateNode.textContent?.trim()).to.equal('0');
el.currentValue = 1;
await el.updateComplete;
expect(el._templateNode.textContent?.trim()).to.equal('1');
});
it('keeps focus after rerender', async () => {
const tag = defineCE(
// @ts-expect-error
class extends SlotMixin(LitElement) {
static properties = { currentValue: Number };
constructor() {
super();
this.currentValue = 0;
}
get slots() {
return {
...super.slots,
'focusable-node': () => ({
template: html`
`,
renderAsDirectHostChild: false,
}),
};
}
render() {
return html`
`;
}
get _focusableNode() {
return /** @type HTMLSpanElement */ (
Array.from(this.children).find(elm => elm.slot === 'focusable-node')
?.firstElementChild
);
}
},
);
const el = /** @type {* & SlotHost} */ (await fixture(`<${tag}>${tag}>`));
el._focusableNode.focus();
expect(isActiveElement(el._focusableNode)).to.be.true;
el.currentValue = 1;
await el.updateComplete;
expect(isActiveElement(el._focusableNode)).to.be.true;
});
it('keeps focus after rerendering complex shadow root into slot', async () => {
const complexSlotTagName = defineCE(
class extends LitElement {
render() {
return html`
`;
}
get _buttonNode() {
// @ts-expect-error
return this.shadowRoot.querySelector('button');
}
},
);
const complexSlotTag = unsafeStatic(complexSlotTagName);
const tagName = defineCE(
// @ts-expect-error
class extends SlotMixin(LitElement) {
static properties = { currentValue: Number };
constructor() {
super();
this.currentValue = 0;
}
get slots() {
return {
...super.slots,
'focusable-node': () => ({
template: html`<${complexSlotTag}> ${complexSlotTag}> `,
}),
};
}
render() {
return html`
`;
}
get _focusableNode() {
return /** @type HTMLSpanElement */ (
Array.from(this.children).find(elm => elm.slot === 'focusable-node')
?.firstElementChild
);
}
},
);
const el = /** @type {* & SlotHost} */ (await fixture(`<${tagName}>${tagName}>`));
el._focusableNode._buttonNode.focus();
expect(isActiveElement(el._focusableNode._buttonNode, { deep: true })).to.be.true;
el.currentValue = 1;
await el.updateComplete;
expect(isActiveElement(el._focusableNode)).to.be.true;
expect(isActiveElement(el._focusableNode._buttonNode, { deep: true })).to.be.true;
});
it('allows for rerendering complex shadow root into slot as a direct child', async () => {
const complexSlotTagName = defineCE(
class extends LitElement {
render() {
return html`
`;
}
get _buttonNode() {
// @ts-expect-error
return this.shadowRoot.querySelector('button');
}
},
);
const complexSlotTag = unsafeStatic(complexSlotTagName);
const tagName = defineCE(
// @ts-expect-error
class extends SlotMixin(LitElement) {
static properties = { currentValue: Number };
constructor() {
super();
this.currentValue = 0;
}
get slots() {
return {
...super.slots,
'focusable-node': () => ({
template: html`<${complexSlotTag}> ${complexSlotTag}> `,
renderAsDirectHostChild: true,
}),
};
}
render() {
return html`
`;
}
get _focusableNode() {
return /** @type HTMLSpanElement */ (
Array.from(this.children).find(elm => elm.slot === 'focusable-node')
);
}
},
);
const el = /** @type {* & SlotHost} */ (await fixture(`<${tagName}>${tagName}>`));
el._focusableNode._buttonNode.focus();
expect(isActiveElement(el._focusableNode._buttonNode, { deep: true })).to.be.true;
el.currentValue = 1;
await el.updateComplete;
expect(isActiveElement(el._focusableNode)).to.be.true;
expect(isActiveElement(el._focusableNode._buttonNode, { deep: true })).to.be.true;
});
it('allows for switching template root in slot as a direct child', async () => {
const tagName = defineCE(
// @ts-expect-error
class extends SlotMixin(LitElement) {
static properties = { isSwitched: Boolean };
constructor() {
super();
this.isSwitched = false;
}
get slots() {
return {
...super.slots,
'my-root-switcher-node': () => ({
template: this.isSwitched
? html`
`
: html`
`,
renderAsDirectHostChild: true,
}),
};
}
render() {
return html`
`;
}
get _myRootSwitcherNode() {
return /** @type HTMLSpanElement */ (
Array.from(this.children).find(elm => elm.slot === 'my-root-switcher-node')
);
}
},
);
const el = /** @type {* & SlotHost} */ (await fixture(`<${tagName}>${tagName}>`));
expect(el._myRootSwitcherNode.id).to.equal('is-not-switched');
expect(el.innerHTML).to.equal(
`
`,
);
el.isSwitched = true;
await el.updateComplete;
expect(el._myRootSwitcherNode.id).to.equal('is-switched');
expect(el.innerHTML).to.equal(
`
`,
);
});
describe('firstRenderOnConnected (for backwards compatibility)', () => {
it('does render on connected when firstRenderOnConnected:true', async () => {
// Start with elem that does not render on connectedCallback
const tag = defineCE(
// @ts-expect-error
class extends SlotMixin(LitElement) {
static properties = { currentValue: Number };
constructor() {
super();
this.currentValue = 0;
}
get slots() {
return {
...super.slots,
template: () => ({
firstRenderOnConnected: true,
template: html`
${this.currentValue} `,
}),
};
}
render() {
return html`
`;
}
get _templateNode() {
return /** @type HTMLSpanElement */ (
Array.from(this.children).find(elm => elm.slot === 'template')
);
}
},
);
const el = /** @type {* & SlotHost} */ (fixtureSync(`<${tag}>${tag}>`));
expect(el._templateNode.slot).to.equal('template');
expect(el._templateNode.textContent?.trim()).to.equal('0');
});
it('does not render on connected when firstRenderOnConnected:false', async () => {
// Start with elem that does not render on connectedCallback
const tag = defineCE(
// @ts-expect-error
class extends SlotMixin(LitElement) {
static properties = { currentValue: Number };
constructor() {
super();
this.currentValue = 0;
}
get slots() {
return {
...super.slots,
template: () => ({ template: html`
${this.currentValue} ` }),
};
}
render() {
return html`
`;
}
get _templateNode() {
return /** @type HTMLSpanElement */ (
Array.from(this.children).find(elm => elm.slot === 'template')
);
}
},
);
const el = /** @type {* & SlotHost} */ (fixtureSync(`<${tag}>${tag}>`));
expect(el._templateNode).to.be.undefined;
await el.updateComplete;
expect(el._templateNode.slot).to.equal('template');
expect(el._templateNode.textContent?.trim()).to.equal('0');
});
});
});
describe('SlotFunctionResult types', () => {
it('supports complex dom trees as element (type "Element")', async () => {
const tag = defineCE(
class extends SlotMixin(LitElement) {
constructor() {
super();
this.foo = 'bar';
}
get slots() {
return {
...super.slots,
feedback: () => {
const el = document.createElement('div');
el.setAttribute('foo', this.foo);
const subEl = document.createElement('p');
subEl.innerText = 'cat';
el.appendChild(subEl);
return el;
},
};
}
},
);
const el = await fixture(`<${tag}>${tag}>`);
expect(el.children[0].slot).to.equal('feedback');
expect(el.children[0].getAttribute('foo')).to.equal('bar');
expect(/** @type HTMLParagraphElement */ (el.children[0].children[0]).innerText).to.equal(
'cat',
);
});
it('supports conditional slots (type "undefined")', async () => {
let renderSlot = true;
const tag = defineCE(
class extends SlotMixin(LitElement) {
get slots() {
return {
...super.slots,
conditional: () => {
if (renderSlot) {
const el = document.createElement('div');
el.id = 'someSlot';
return el;
}
return undefined;
},
};
}
},
);
const elSlot = await fixture(`<${tag}><${tag}>`);
expect(elSlot.querySelector('#someSlot')).to.exist;
renderSlot = false;
const elNoSlot = await fixture(`<${tag}><${tag}>`);
expect(elNoSlot.querySelector('#someSlot')).to.not.exist;
});
it('supports templates (type "TemplateResult")', async () => {
const tag = defineCE(
class extends SlotMixin(LitElement) {
get slots() {
return {
...super.slots,
template: () => html`
text`,
};
}
render() {
return html`
`;
}
},
);
const el = await fixture(`<${tag}>${tag}>`);
const slot = /** @type HTMLSpanElement */ (
Array.from(el.children).find(elm => elm.slot === 'template')
);
expect(slot.slot).to.equal('template');
expect(slot.tagName).to.equal('SPAN');
});
it('supports (deprecated) afterRender logic (type "{ template:TemplateResults; afterRender: Function}" )', async () => {
let varThatProvesAfterRenderIsCalled = 'not called';
const tag = defineCE(
// @ts-expect-error
class extends SlotMixin(LitElement) {
static properties = { currentValue: Number };
constructor() {
super();
this.currentValue = 0;
}
get slots() {
return {
...super.slots,
template: () => ({
template: html`
${this.currentValue}, `,
afterRender: () => {
varThatProvesAfterRenderIsCalled = 'called';
},
}),
};
}
render() {
return html`
`;
}
get _templateNode() {
return /** @type HTMLSpanElement */ (
Array.from(this.children).find(elm => elm.slot === 'template')?.firstElementChild
);
}
},
);
const el = /** @type {* & SlotHost} */ (await fixture(`<${tag}>${tag}>`));
expect(el._templateNode.textContent).to.equal('0');
el.currentValue = 1;
await el.updateComplete;
expect(varThatProvesAfterRenderIsCalled).to.equal('called');
expect(el._templateNode.textContent).to.equal('1');
});
});
describe('Scoped Registries', () => {
it('supports scoped elements when scoped registries supported (or polyfill loaded)', async () => {
if (!supportsScopedRegistry()) return;
// @ts-expect-error
const createElSpy = sinon.spy(ShadowRoot.prototype, 'createElement');
class ScopedEl extends LitElement {}
const tagName = defineCE(
// @ts-ignore
class extends ScopedElementsMixin(SlotMixin(LitElement)) {
static get scopedElements() {
return {
// @ts-expect-error
...super.scopedElements,
'scoped-elm': ScopedEl,
};
}
get slots() {
return {
...super.slots,
template: () => html`
`,
};
}
render() {
return html`
`;
}
},
);
const tag = unsafeStatic(tagName);
await fixture(html`<${tag}>${tag}>`);
expect(createElSpy.callCount).to.equal(1);
createElSpy.restore();
});
it('does not scope elements when scoped registries not supported (or polyfill not loaded)', async () => {
if (supportsScopedRegistry()) return;
class ScopedEl extends LitElement {}
const tagName = defineCE(
class extends ScopedElementsMixin(SlotMixin(LitElement)) {
static get scopedElements() {
return {
// @ts-expect-error
...super.scopedElements,
'scoped-el': ScopedEl,
};
}
get slots() {
return {
...super.slots,
template: () => html`
`,
};
}
render() {
return html`
`;
}
},
);
const renderTarget = document.createElement('div');
const el = document.createElement(tagName);
// We don't use fixture, so we limit the amount of calls to document.createElement
const docSpy = sinon.spy(document, 'createElement');
document.body.appendChild(renderTarget);
renderTarget.appendChild(el);
expect(docSpy.callCount).to.equal(2);
document.body.removeChild(renderTarget);
docSpy.restore();
});
});
});