diff --git a/packages/@lwc/engine-core/src/framework/api.ts b/packages/@lwc/engine-core/src/framework/api.ts index 5f453bec45..bc83e15a6a 100644 --- a/packages/@lwc/engine-core/src/framework/api.ts +++ b/packages/@lwc/engine-core/src/framework/api.ts @@ -50,6 +50,8 @@ import { VText, VStaticPart, VStaticPartData, + isVBaseElement, + isVStatic, } from './vnodes'; import { getComponentRegisteredName } from './component'; @@ -92,6 +94,7 @@ function st(fragment: Element, key: Key, parts?: VStaticPart[]): VStatic { fragment, owner, parts, + slotAssignment: undefined, }; return vnode; @@ -160,7 +163,7 @@ function h(sel: string, data: VElementData, children: VNodes = EmptyArray): VEle }); } - const { key } = data; + const { key, slotAssignment } = data; const vnode: VElement = { type: VNodeType.Element, @@ -170,6 +173,7 @@ function h(sel: string, data: VElementData, children: VNodes = EmptyArray): VEle elm: undefined, key, owner: vmBeingRendered, + slotAssignment, }; return vnode; @@ -207,6 +211,10 @@ function s( assert.isTrue(isObject(data), `s() 2nd argument data must be an object.`); assert.isTrue(isArray(children), `h() 3rd argument children must be an array.`); } + + const vmBeingRendered = getVMBeingRendered()!; + const { renderMode, apiVersion } = vmBeingRendered; + if ( !isUndefined(slotset) && !isUndefined(slotset.slotAssignments) && @@ -236,7 +244,6 @@ function s( } // If the passed slot content is factory, evaluate it and add the produced vnodes if (assignedNodeIsScopedSlot) { - const vmBeingRenderedInception = getVMBeingRendered(); // Evaluate in the scope of the slot content's owner // if a slotset is provided, there will always be an owner. The only case where owner is // undefined is for root components, but root components cannot accept slotted content @@ -249,19 +256,34 @@ function s( ArrayPush.call(newChildren, vnode.factory(data.slotData, data.key)); }); } finally { - setVMBeingRendered(vmBeingRenderedInception); + setVMBeingRendered(vmBeingRendered); } } else { + // This block is for standard slots (non-scoped slots) + let clonedVNode; + if ( + renderMode === RenderMode.Light && + isAPIFeatureEnabled(APIFeature.USE_LIGHT_DOM_SLOT_FORWARDING, apiVersion) && + (isVBaseElement(vnode) || isVStatic(vnode)) && + // We only need to copy the vnodes when the slot assignment changes, copying every time causes issues with + // disconnected/connected callback firing. + vnode.slotAssignment !== data.slotAssignment + ) { + // When the light DOM slot assignment (slot attribute) changes we can't use the same reference + // to the vnode because the current way the diffing algo works, it will replace the original reference + // to the host element with a new one. This means the new element will be mounted and immediately unmounted. + // Creating a copy of the vnode to preserve a reference to the previous host element. + clonedVNode = { ...vnode, slotAssignment: data.slotAssignment }; + } // If the slot content is standard type, the content is static, no additional // processing needed on the vnode - ArrayPush.call(newChildren, vnode); + ArrayPush.call(newChildren, clonedVNode ?? vnode); } } } children = newChildren; } - const vmBeingRendered = getVMBeingRendered()!; - const { renderMode, shadowMode, apiVersion } = vmBeingRendered; + const { shadowMode } = vmBeingRendered; if (renderMode === RenderMode.Light) { // light DOM slots - backwards-compatible behavior uses flattening, new behavior uses fragments @@ -324,7 +346,7 @@ function c( }); } } - const { key } = data; + const { key, slotAssignment } = data; let elm, aChildren, vm; const vnode: VCustomElement = { type: VNodeType.CustomElement, @@ -333,6 +355,7 @@ function c( children, elm, key, + slotAssignment, ctor: Ctor, owner: vmBeingRendered, diff --git a/packages/@lwc/engine-core/src/framework/modules/attrs.ts b/packages/@lwc/engine-core/src/framework/modules/attrs.ts index 3aa20cbc17..16d6f88f3f 100644 --- a/packages/@lwc/engine-core/src/framework/modules/attrs.ts +++ b/packages/@lwc/engine-core/src/framework/modules/attrs.ts @@ -15,7 +15,7 @@ import { import { RendererAPI } from '../renderer'; import { EmptyObject } from '../utils'; -import { VBaseElement } from '../vnodes'; +import { VBaseElement, VStatic } from '../vnodes'; const ColonCharCode = 58; @@ -63,3 +63,24 @@ export function patchAttributes( } } } + +export function patchSlotAssignment( + oldVnode: VBaseElement | VStatic | null, + vnode: VBaseElement | VStatic, + renderer: RendererAPI +) { + const { slotAssignment } = vnode; + + if (oldVnode?.slotAssignment === slotAssignment) { + return; + } + + const { elm } = vnode; + const { setAttribute, removeAttribute } = renderer; + + if (isUndefined(slotAssignment) || isNull(slotAssignment)) { + removeAttribute(elm, 'slot'); + } else { + setAttribute(elm, 'slot', slotAssignment); + } +} diff --git a/packages/@lwc/engine-core/src/framework/rendering.ts b/packages/@lwc/engine-core/src/framework/rendering.ts index 586f2a784f..d487f9c04b 100644 --- a/packages/@lwc/engine-core/src/framework/rendering.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -47,6 +47,7 @@ import { isVCustomElement, isVFragment, isVScopedSlotFragment, + isVStatic, Key, VBaseElement, VComment, @@ -60,7 +61,7 @@ import { VText, } from './vnodes'; -import { patchAttributes } from './modules/attrs'; +import { patchAttributes, patchSlotAssignment } from './modules/attrs'; import { patchProps } from './modules/props'; import { patchClassAttribute } from './modules/computed-class-attr'; import { patchStyleAttribute } from './modules/computed-style-attr'; @@ -266,6 +267,8 @@ function mountElement( function patchStatic(n1: VStatic, n2: VStatic, renderer: RendererAPI) { const elm = (n2.elm = n1.elm!); + // slotAssignments can only apply to the top level element, never to a static part. + patchSlotAssignment(n1, n2, renderer); // The `refs` object is blown away in every re-render, so we always need to re-apply them applyStaticParts(elm, n2, renderer, false); } @@ -299,6 +302,8 @@ function mountStatic( } } + // slotAssignments can only apply to the top level element, never to a static part. + patchSlotAssignment(null, vnode, renderer); insertNode(elm, parent, anchor, renderer); applyStaticParts(elm, vnode, renderer, true); } @@ -601,6 +606,7 @@ function patchElementPropsAndAttrsAndRefs( patchAttributes(oldVnode, vnode, renderer); patchProps(oldVnode, vnode, renderer); + patchSlotAssignment(oldVnode, vnode, renderer); // The `refs` object is blown away in every re-render, so we always need to re-apply them applyRefs(vnode, vnode.owner); @@ -799,8 +805,8 @@ function allocateInSlot(vm: VM, children: VNodes, owner: VM) { } let slotName: unknown = ''; - if (isVBaseElement(vnode)) { - slotName = vnode.data.attrs?.slot ?? ''; + if (isVBaseElement(vnode) || isVStatic(vnode)) { + slotName = vnode.slotAssignment ?? ''; } else if (isVScopedSlotFragment(vnode)) { slotName = vnode.slotName; } diff --git a/packages/@lwc/engine-core/src/framework/vnodes.ts b/packages/@lwc/engine-core/src/framework/vnodes.ts index c663a9c67b..ea040890c9 100644 --- a/packages/@lwc/engine-core/src/framework/vnodes.ts +++ b/packages/@lwc/engine-core/src/framework/vnodes.ts @@ -63,6 +63,8 @@ export interface VStatic extends BaseVNode { readonly fragment: Element; readonly parts: VStaticPart[] | undefined; elm: Element | undefined; + // Corresponds to the slot attribute of the element and indicates which `slot` element it should be assigned to + slotAssignment: string | undefined; } export interface VFragment extends BaseVNode, BaseVParent { @@ -99,6 +101,8 @@ export interface VBaseElement extends BaseVNode, BaseVParent { data: VElementData; elm: Element | undefined; key: Key; + // Corresponds to the slot attribute of the element and indicates which `slot` element it should be assigned to + slotAssignment: string | undefined; } export interface VElement extends VBaseElement { @@ -134,6 +138,8 @@ export interface VElementData extends VNodeData { readonly external?: boolean; readonly ref?: string; readonly slotData?: any; + // Corresponds to the slot attribute of the element and indicates which `slot` element it should be assigned to + readonly slotAssignment?: string; } export function isVBaseElement(vnode: VNode): vnode is VElement | VCustomElement { @@ -156,3 +162,7 @@ export function isVFragment(vnode: VNode): vnode is VFragment { export function isVScopedSlotFragment(vnode: VNode): vnode is VScopedSlotFragment { return vnode.type === VNodeType.ScopedSlotFragment; } + +export function isVStatic(vnode: VNode): vnode is VStatic { + return vnode.type === VNodeType.Static; +} diff --git a/packages/@lwc/integration-karma/helpers/test-utils.js b/packages/@lwc/integration-karma/helpers/test-utils.js index cdc4636372..3bea9d7ba0 100644 --- a/packages/@lwc/integration-karma/helpers/test-utils.js +++ b/packages/@lwc/integration-karma/helpers/test-utils.js @@ -22,9 +22,9 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { // TODO [#869]: Replace this custom spy with standard spyOn jasmine spy when logWarning doesn't use console.group // anymore. On IE11 console.group has a different behavior when the F12 inspector is attached to the page. function spyConsole() { - var originalConsole = window.console; + const originalConsole = window.console; - var calls = { + const calls = { log: [], warn: [], error: [], @@ -67,15 +67,15 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { return function consoleDevMatcher() { return { negativeCompare: function negativeCompare(actual) { - var spy = spyConsole(); + const spy = spyConsole(); try { actual(); } finally { spy.reset(); } - var callsArgs = spy.calls[methodName]; - var formattedCalls = callsArgs + const callsArgs = spy.calls[methodName]; + const formattedCalls = callsArgs .map(function (arg) { return '"' + formatConsoleCall(arg) + '"'; }) @@ -118,7 +118,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { ); } - var spy = spyConsole(); + const spy = spyConsole(); try { actual(); @@ -126,8 +126,8 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { spy.reset(); } - var callsArgs = spy.calls[methodName]; - var formattedCalls = callsArgs + const callsArgs = spy.calls[methodName]; + const formattedCalls = callsArgs .map(function (callArgs) { return '"' + formatConsoleCall(callArgs) + '"'; }) @@ -168,10 +168,10 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { ' time(s).' ); } - for (var i = 0; i < callsArgs.length; i++) { - var callsArg = callsArgs[i]; - var expectedMessage = expectedMessages[i]; - var actualMessage = formatConsoleCall(callsArg); + for (let i = 0; i < callsArgs.length; i++) { + const callsArg = callsArgs[i]; + const expectedMessage = expectedMessages[i]; + const actualMessage = formatConsoleCall(callsArg); if (!matchMessage(actualMessage, expectedMessage)) { return fail( 'Expected console.' + @@ -243,7 +243,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { ); } - var thrown = errorListener(actual); + const thrown = errorListener(actual); if (!expectInProd && process.env.NODE_ENV === 'production') { if (thrown !== undefined) { @@ -294,7 +294,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { // Listen for errors using window.addEventListener('error') function windowErrorListener(callback) { - var error; + let error; function onError(event) { event.preventDefault(); // don't log the error error = event.error; @@ -302,7 +302,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { // Prevent jasmine from handling the global error. There doesn't seem to be another // way to disable this behavior: https://github.com/jasmine/jasmine/pull/1860 - var originalOnError = window.onerror; + const originalOnError = window.onerror; window.onerror = null; window.addEventListener('error', onError); @@ -329,7 +329,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { : directErrorListener(callback); } - var customMatchers = { + const customMatchers = { toLogErrorDev: consoleDevMatcherFactory('error'), toLogError: consoleDevMatcherFactory('error', true), toLogWarningDev: consoleDevMatcherFactory('warn'), @@ -365,7 +365,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { } function extractDataIds(root) { - var nodes = {}; + const nodes = {}; function processElement(elm) { if (elm.hasAttribute('data-id')) { @@ -383,14 +383,14 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { // Work around Internet Explorer wanting a function instead of an object. IE also *requires* this argument where // other browsers don't. - var safeFilter = acceptNode; + const safeFilter = acceptNode; safeFilter.acceptNode = acceptNode; - var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, safeFilter, false); + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, safeFilter, false); processElement(root); - var elm; + let elm; while ((elm = walker.nextNode())) { processElement(elm); } @@ -399,26 +399,26 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { } function extractShadowDataIds(shadowRoot) { - var nodes = {}; + const nodes = {}; // Add the shadow root here even if they don't have [data-id] attributes. This reference is // subsequently used to add event listeners. - var dataId = shadowRoot.host.getAttribute('data-id'); + const dataId = shadowRoot.host.getAttribute('data-id'); if (dataId) { nodes[dataId + '.shadowRoot'] = shadowRoot; } // We can't use a TreeWalker directly on the ShadowRoot since with synthetic shadow the ShadowRoot is not an // actual DOM nodes. So we need to iterate over the children manually and run the tree walker on each child. - for (var i = 0; i < shadowRoot.childNodes.length; i++) { - var child = shadowRoot.childNodes[i]; + for (let i = 0; i < shadowRoot.childNodes.length; i++) { + const child = shadowRoot.childNodes[i]; Object.assign(nodes, extractDataIds(child)); } return nodes; } - var register = {}; + let register = {}; function load(id) { return Promise.resolve(register[id]); } @@ -447,7 +447,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { } // Providing overridable hooks for tests - var sanitizeHtmlContentHook = function () { + let sanitizeHtmlContentHook = function () { throw new Error('sanitizeHtmlContent hook must be implemented.'); }; @@ -470,7 +470,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { } // This mapping should be kept up-to-date with the mapping in @lwc/shared -> aria.ts - var ariaPropertiesMapping = { + const ariaPropertiesMapping = { ariaAutoComplete: 'aria-autocomplete', ariaChecked: 'aria-checked', ariaCurrent: 'aria-current', @@ -526,7 +526,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { }; // See the README for @lwc/aria-reflection - var nonStandardAriaProperties = [ + const nonStandardAriaProperties = [ 'ariaActiveDescendant', 'ariaControls', 'ariaDescribedBy', @@ -539,7 +539,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { // These properties are not included in the global polyfill, but were added to LightningElement/BridgeElement // prototypes in https://github.com/salesforce/lwc/pull/3702 - var nonPolyfilledAriaProperties = [ + const nonPolyfilledAriaProperties = [ 'ariaColIndexText', 'ariaBrailleLabel', 'ariaBrailleRoleDescription', @@ -547,10 +547,10 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { 'ariaRowIndexText', ]; - var ariaProperties = Object.keys(ariaPropertiesMapping); + const ariaProperties = Object.keys(ariaPropertiesMapping); // Can't use Object.values because we need to support IE11 - var ariaAttributes = []; + const ariaAttributes = []; for (let i = 0; i < ariaProperties.length; i++) { ariaAttributes.push(ariaPropertiesMapping[ariaProperties[i]]); } @@ -558,7 +558,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { // Keep traversing up the prototype chain until a property descriptor is found function getPropertyDescriptor(object, prop) { do { - var descriptor = Object.getOwnPropertyDescriptor(object, prop); + const descriptor = Object.getOwnPropertyDescriptor(object, prop); if (descriptor) { return descriptor; } @@ -566,6 +566,10 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { } while (object); } + // These values are based on the API versions in @lwc/shared/api-version + const lightDomSlotForwardingEnabled = process.env.API_VERSION > 60; + const vFragBookEndEnabled = process.env.API_VERSION > 59; + return { clearRegister: clearRegister, extractDataIds: extractDataIds, @@ -588,5 +592,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { attachReportingControlDispatcher: attachReportingControlDispatcher, detachReportingControlDispatcher: detachReportingControlDispatcher, nativeCustomElementLifecycleEnabled: nativeCustomElementLifecycleEnabled, + lightDomSlotForwardingEnabled: lightDomSlotForwardingEnabled, + vFragBookEndEnabled: vFragBookEndEnabled, }; })(LWC, jasmine, beforeAll); diff --git a/packages/@lwc/integration-karma/test/act/act-components/test-multiple-slots.js b/packages/@lwc/integration-karma/test/act/act-components/test-multiple-slots.js index d2056cc029..2b022e5de1 100644 --- a/packages/@lwc/integration-karma/test/act/act-components/test-multiple-slots.js +++ b/packages/@lwc/integration-karma/test/act/act-components/test-multiple-slots.js @@ -27,9 +27,7 @@ export default function (define) { 'ui-something', _uiSomething__default['default'], { - attrs: { - slot: 'first', - }, + slotAssignment: 'first', props: { text: 'Hello', }, @@ -41,9 +39,7 @@ export default function (define) { 'ui-another', _uiAnother__default['default'], { - attrs: { - slot: 'second', - }, + slotAssignment: 'second', props: { value: 'Foo', }, diff --git a/packages/@lwc/integration-karma/test/act/act-components/test-slot-adjacent-to-named-slot.js b/packages/@lwc/integration-karma/test/act/act-components/test-slot-adjacent-to-named-slot.js index 5ff313343d..a10b99998f 100644 --- a/packages/@lwc/integration-karma/test/act/act-components/test-slot-adjacent-to-named-slot.js +++ b/packages/@lwc/integration-karma/test/act/act-components/test-slot-adjacent-to-named-slot.js @@ -27,9 +27,7 @@ export default function (define) { 'ui-another', _uiAnother__default['default'], { - attrs: { - slot: 'adjacent', - }, + slotAssignment: 'adjacent', props: { value: 'Foo', }, diff --git a/packages/@lwc/integration-karma/test/act/act-components/test-slot-in-grandchild.js b/packages/@lwc/integration-karma/test/act/act-components/test-slot-in-grandchild.js index 310b62c3ca..04e4c4fd8a 100644 --- a/packages/@lwc/integration-karma/test/act/act-components/test-slot-in-grandchild.js +++ b/packages/@lwc/integration-karma/test/act/act-components/test-slot-in-grandchild.js @@ -27,9 +27,7 @@ export default function (define) { 'ui-something', _uiSomething__default['default'], { - attrs: { - slot: 'first', - }, + slotAssignment: 'first', props: { text: 'Hello', }, @@ -40,9 +38,7 @@ export default function (define) { 'ui-another', _uiAnother__default['default'], { - attrs: { - slot: 'second', - }, + slotAssignment: 'second', props: { value: 'Foo', }, diff --git a/packages/@lwc/integration-karma/test/api/CustomElementConstructor-getter/index.spec.js b/packages/@lwc/integration-karma/test/api/CustomElementConstructor-getter/index.spec.js index a589e99f46..5ccc20934b 100644 --- a/packages/@lwc/integration-karma/test/api/CustomElementConstructor-getter/index.spec.js +++ b/packages/@lwc/integration-karma/test/api/CustomElementConstructor-getter/index.spec.js @@ -1,4 +1,5 @@ import { LightningElement } from 'lwc'; +import { vFragBookEndEnabled } from 'test-utils'; import ReflectElement from 'x/reflect'; import LifecycleParent from 'x/lifecycleParent'; @@ -10,7 +11,7 @@ import ReflectCamel from 'x/reflectCamel'; import WithChildElmsHasSlot from 'x/withChildElmsHasSlot'; import WithChildElmsHasSlotLight from 'x/withChildElmsHasSlotLight'; -const vFragBookend = process.env.API_VERSION > 59 ? '' : ''; +const vFragBookend = vFragBookEndEnabled ? '' : ''; it('should throw when trying to claim abstract LightningElement as custom element', () => { expect(() => LightningElement.CustomElementConstructor).toThrowError( diff --git a/packages/@lwc/integration-karma/test/light-dom/scoped-slot/if-block/index.spec.js b/packages/@lwc/integration-karma/test/light-dom/scoped-slot/if-block/index.spec.js index 3976a57dd8..ed3d52def6 100644 --- a/packages/@lwc/integration-karma/test/light-dom/scoped-slot/if-block/index.spec.js +++ b/packages/@lwc/integration-karma/test/light-dom/scoped-slot/if-block/index.spec.js @@ -1,8 +1,9 @@ import { createElement } from 'lwc'; +import { vFragBookEndEnabled } from 'test-utils'; import MixedSlotParent from 'x/mixedSlotParent'; -const vFragBookend = process.env.API_VERSION > 59 ? '' : ''; +const vFragBookend = vFragBookEndEnabled ? '' : ''; describe('if-block', () => { it('should work when parent and child have matching slot types', () => { diff --git a/packages/@lwc/integration-karma/test/light-dom/scoped-slot/index.spec.js b/packages/@lwc/integration-karma/test/light-dom/scoped-slot/index.spec.js index 1751554f0a..447ff25540 100644 --- a/packages/@lwc/integration-karma/test/light-dom/scoped-slot/index.spec.js +++ b/packages/@lwc/integration-karma/test/light-dom/scoped-slot/index.spec.js @@ -1,4 +1,5 @@ import { createElement } from 'lwc'; +import { lightDomSlotForwardingEnabled, vFragBookEndEnabled } from 'test-utils'; import BasicParent from 'x/basicParent'; import ParentOfChildWithForEach from 'x/parentOfChildWithForEach'; @@ -6,7 +7,7 @@ import ParentWNoSlotContent from 'x/parentWNoSlotContent'; import ParentOfChildWithNamedSlots from 'x/parentOfChildWithNamedSlots'; import NestedSlots from 'x/nestedSlots'; -const vFragBookend = process.env.API_VERSION > 59 ? '' : ''; +const vFragBookend = vFragBookEndEnabled ? '' : ''; describe('scoped slots', () => { it('scoped slots work with default slots', () => { @@ -61,7 +62,9 @@ describe('scoped slots', () => { // For standard slot content, "slot" attribute goes directly on the element unlike scoped // slots where the attribute goes on the template tag expect(child.querySelector('.slotname3').innerHTML).toBe( - `${vFragBookend}

MLB

${vFragBookend}` + lightDomSlotForwardingEnabled + ? `${vFragBookend}

MLB

${vFragBookend}` + : `${vFragBookend}

MLB

${vFragBookend}` ); }); diff --git a/packages/@lwc/integration-karma/test/light-dom/scoped-slot/runtime-checks/index.spec.js b/packages/@lwc/integration-karma/test/light-dom/scoped-slot/runtime-checks/index.spec.js index fc1a8afb33..7a38b8016e 100644 --- a/packages/@lwc/integration-karma/test/light-dom/scoped-slot/runtime-checks/index.spec.js +++ b/packages/@lwc/integration-karma/test/light-dom/scoped-slot/runtime-checks/index.spec.js @@ -1,9 +1,10 @@ import { createElement } from 'lwc'; +import { vFragBookEndEnabled } from 'test-utils'; import ParentWithScopedSlotContent from 'x/parentWithScopedSlotContent'; import ParentWithStandardSlotContent from 'x/parentWithStandardSlotContent'; -const vFragBookend = process.env.API_VERSION > 59 ? '' : ''; +const vFragBookend = vFragBookEndEnabled ? '' : ''; describe('runtime validation of slot content and slot', () => { it('Ignores content when parent uses scoped slot and child has standard slot', () => { diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/index.spec.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/index.spec.js new file mode 100644 index 0000000000..f195526561 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/index.spec.js @@ -0,0 +1,31 @@ +import { createElement } from 'lwc'; + +import LightContainer from './x/lightContainer/lightContainer'; + +describe('scoped slots, slot forwarding', () => { + let lightContainer; + beforeAll(() => { + lightContainer = createElement('x-light-container', { is: LightContainer }); + document.body.appendChild(lightContainer); + }); + + afterAll(() => { + document.body.removeChild(lightContainer); + }); + + // TODO [#3889]: This test should be updated once a fix is ready. + it('does not reassign slot content', () => { + const leaf = lightContainer.querySelector('x-leaf'); + expect(leaf.shadowRoot.children.length).toEqual(2); + + const defaultSlot = leaf.shadowRoot.querySelector('slot'); + const defaultSlotContent = defaultSlot.assignedNodes(); + expect(defaultSlotContent.length).toEqual(1); + expect(defaultSlotContent[0].innerText).toEqual('Hello world!'); + + const fooSlot = leaf.shadowRoot.querySelector('[name="foo"]'); + const fooSlotContent = fooSlot.assignedNodes(); + // Note the slot reassignment does not work on scoped slots + expect(fooSlotContent.length).toEqual(0); + }); +}); diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/leaf/leaf.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/leaf/leaf.html new file mode 100644 index 0000000000..68f2c67ef3 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/leaf/leaf.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/leaf/leaf.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/leaf/leaf.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/leaf/leaf.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/lightContainer/lightContainer.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/lightContainer/lightContainer.html new file mode 100644 index 0000000000..4511f208bb --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/lightContainer/lightContainer.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/lightContainer/lightContainer.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/lightContainer/lightContainer.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/lightContainer/lightContainer.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotChild/scopedSlotChild.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotChild/scopedSlotChild.html new file mode 100644 index 0000000000..4a385ec61a --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotChild/scopedSlotChild.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotChild/scopedSlotChild.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotChild/scopedSlotChild.js new file mode 100644 index 0000000000..ba52afd521 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotChild/scopedSlotChild.js @@ -0,0 +1,7 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; + + title = 'Hello world!'; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotParent/scopedSlotParent.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotParent/scopedSlotParent.html new file mode 100644 index 0000000000..ab8f526ad6 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotParent/scopedSlotParent.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotParent/scopedSlotParent.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotParent/scopedSlotParent.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/scoped-slots/x/scopedSlotParent/scopedSlotParent.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/index.spec.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/index.spec.js new file mode 100644 index 0000000000..81dd3b706c --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/index.spec.js @@ -0,0 +1,271 @@ +import { createElement } from 'lwc'; +import { extractDataIds, lightDomSlotForwardingEnabled, vFragBookEndEnabled } from 'test-utils'; + +import LightContainer from './x/lightContainer/lightContainer'; + +const commentNode = document.createComment(' comments being forwarded '); + +const slotAssignmentWithForwarding = { + expectedUpperSlot: { + tagName: 'x-shadow-dom-element', + content: 'Lower slot forwarded content', + slotAttr: null, + }, + expectedLowerSlot: { + tagName: 'x-light-dom-element', + content: 'Upper slot forwarded content', + slotAttr: null, + }, + expectedDefaultSlot: { + innerText: 'Static vnode default slot forwarded', + textContent: 'Text being forwarded', + }, +}; + +const slotAssignmentWithoutForwarding = { + expectedUpperSlot: { + tagName: 'x-light-dom-element', + content: 'Upper slot forwarded content', + slotAttr: 'upper', + }, + expectedLowerSlot: { + tagName: 'x-shadow-dom-element', + content: 'Lower slot forwarded content', + slotAttr: 'lower', + }, + expectedDefaultSlot: { + innerText: 'Static vnode default slot forwarded', + textContent: 'Text being forwarded', + }, +}; + +const expectedSlotAssignment = lightDomSlotForwardingEnabled + ? slotAssignmentWithForwarding + : slotAssignmentWithoutForwarding; + +describe('slot forwarding', () => { + let nodes; + let lightContainer; + beforeAll(() => { + lightContainer = createElement('x-light-container', { is: LightContainer }); + document.body.appendChild(lightContainer); + nodes = extractDataIds(lightContainer); + }); + + afterAll(() => { + document.body.removeChild(lightContainer); + }); + + it('should correctly forward slot assignments - light > light slot', () => { + const lightLightLeaf = nodes['light-light-leaf']; + // Note these include the vFragmentBookend elements + expect(lightLightLeaf.children.length).toBe(5); + + const { expectedUpperSlot, expectedLowerSlot, expectedDefaultSlot } = + expectedSlotAssignment; + + const slotContent = Array.from(lightLightLeaf.children); + const upperSlotContent = slotContent.slice(0, 2); + // Lower content is now mapped to upper slot + expect(upperSlotContent[0].getAttribute('slot')).toBe(expectedUpperSlot.slotAttr); + expect(upperSlotContent[0].tagName.toLowerCase()).toBe(expectedUpperSlot.tagName); + expect(upperSlotContent[1].getAttribute('slot')).toBe(expectedUpperSlot.slotAttr); + expect(upperSlotContent[1].innerText).toEqual(expectedUpperSlot.content); + + // Upper content is now mapped to lower slot + const lowerSlotContent = slotContent.slice(2, 4); + expect(lowerSlotContent[0].getAttribute('slot')).toBe(expectedLowerSlot.slotAttr); + expect(lowerSlotContent[0].tagName.toLowerCase()).toEqual(expectedLowerSlot.tagName); + expect(lowerSlotContent[1].getAttribute('slot')).toBe(expectedLowerSlot.slotAttr); + expect(lowerSlotContent[1].innerText).toEqual(expectedLowerSlot.content); + + // Default slot content is mapped to `defaultSlotReassigned` slot + const remappedDefaultSlotContent = slotContent.slice(4, 5); + expect(remappedDefaultSlotContent[0].innerText).toEqual(expectedDefaultSlot.innerText); + + // These are to cover API versions 60, 59 and below + const defaultSlotTextIndex = lightDomSlotForwardingEnabled + ? 12 + : vFragBookEndEnabled + ? 11 + : 4; + const defaultSlotCommentIndex = lightDomSlotForwardingEnabled + ? 13 + : vFragBookEndEnabled + ? 12 + : 5; + + expect(lightLightLeaf.childNodes[defaultSlotTextIndex].textContent).toEqual( + expectedDefaultSlot.textContent + ); + expect(lightLightLeaf.childNodes[defaultSlotCommentIndex]).toEqual(commentNode); + }); + + it('should correctly forward slot assignments - light > shadow slot', () => { + const lightShadowLeaf = nodes['light-shadow-leaf']; + expect(lightShadowLeaf.shadowRoot.children.length).toBe(4); + + const { expectedUpperSlot, expectedLowerSlot, expectedDefaultSlot } = + expectedSlotAssignment; + + const slots = extractDataIds(lightShadowLeaf); + const upperSlotContent = slots['upper-slot'].assignedNodes(); + expect(upperSlotContent[0].getAttribute('slot')).toBe('upper'); + expect(upperSlotContent[0].tagName.toLowerCase()).toBe(expectedUpperSlot.tagName); + expect(upperSlotContent[1].getAttribute('slot')).toBe('upper'); + expect(upperSlotContent[1].innerText).toEqual(expectedUpperSlot.content); + + const lowerSlotContent = slots['lower-slot'].assignedNodes(); + expect(lowerSlotContent[0].getAttribute('slot')).toBe('lower'); + expect(lowerSlotContent[0].tagName.toLowerCase()).toEqual(expectedLowerSlot.tagName); + expect(lowerSlotContent[1].getAttribute('slot')).toBe('lower'); + expect(lowerSlotContent[1].innerText).toEqual(expectedLowerSlot.content); + + const defaultSlotContent = slots['default-slot'].assignedNodes(); + expect(defaultSlotContent[0].textContent).toEqual(expectedDefaultSlot.textContent); + if (!process.env.NATIVE_SHADOW) { + // Native shadow doesn't slot comments + expect(defaultSlotContent[1]).toEqual(commentNode); + } + + if (lightDomSlotForwardingEnabled) { + // With slot forwarding + const reassginedDefaultSlot = slots['default-slot-reassigned'].assignedNodes(); + // Verify static vnode `slot` attribute is reassigned + expect(reassginedDefaultSlot[0].getAttribute('slot')).toBe('defaultSlotReassigned'); + expect(reassginedDefaultSlot[0].innerText).toEqual(expectedDefaultSlot.innerText); + } else { + // Without slot forwarding + expect(defaultSlotContent[process.env.NATIVE_SHADOW ? 1 : 2].innerText).toEqual( + expectedDefaultSlot.innerText + ); + } + }); + + it('should correctly forward slot assignments - shadow > light slot', () => { + const shadowLightLeaf = nodes['shadow-light-leaf']; + expect(shadowLightLeaf.children.length).toBe(3); + + const { expectedUpperSlot, expectedLowerSlot, expectedDefaultSlot } = + slotAssignmentWithForwarding; + + const slots = extractDataIds(shadowLightLeaf); + const upperSlot = slots['upper-slot']; + // In this test, the shadow slot is passed directly to the light DOM slot which will cause the + // slot attribute to be remapped to the slot attribute on the light DOM slot. + // Verify the slot attribute was correctly updated. + // Api versions < 61 slot forwarding is not enabled, so the slot attribute is untouched + expect(upperSlot.hasAttribute('slot')).toBe(!lightDomSlotForwardingEnabled); + + const upperSlotContent = upperSlot.assignedNodes(); + // Note that because the shadow slot is passed, the slot element is what's updated. + // Verify that the children have not been modified. + expect(upperSlotContent[0].getAttribute('slot')).toBe('lower'); + expect(upperSlotContent[0].tagName.toLowerCase()).toBe(expectedUpperSlot.tagName); + expect(upperSlotContent[1].getAttribute('slot')).toBe('lower'); + expect(upperSlotContent[1].innerText).toEqual(expectedUpperSlot.content); + + const lowerSlot = slots['lower-slot']; + expect(lowerSlot.hasAttribute('slot')).toBe(!lightDomSlotForwardingEnabled); + + const lowerSlotContent = lowerSlot.assignedNodes(); + expect(lowerSlotContent[0].getAttribute('slot')).toBe('upper'); + expect(lowerSlotContent[0].tagName.toLowerCase()).toEqual(expectedLowerSlot.tagName); + expect(lowerSlotContent[1].getAttribute('slot')).toBe('upper'); + expect(lowerSlotContent[1].innerText).toEqual(expectedLowerSlot.content); + + const defaultSlot = slots['default-slot']; + expect(defaultSlot.hasAttribute('slot')).toBe(!lightDomSlotForwardingEnabled); + + // Note since the content forwarded to the default shadow slot are wrapped in an actual slot element, + // all content inside of it is forwarded together. + // This is in contrast to the above test where the static element is re-parented to reassginedDefaultSlot + const defaultSlotContent = defaultSlot.assignedNodes(); + expect(defaultSlotContent[0].textContent).toEqual(expectedDefaultSlot.textContent); + if (!process.env.NATIVE_SHADOW) { + // Native shadow doesn't slot comments + expect(defaultSlotContent[1]).toEqual(commentNode); + } + expect(defaultSlotContent[process.env.NATIVE_SHADOW ? 1 : 2].innerText).toEqual( + expectedDefaultSlot.innerText + ); + }); + + it('should correctly forward slot assignments - shadow > shadow slot', () => { + const shadowShadowLeaf = nodes['shadow-shadow-leaf']; + expect(shadowShadowLeaf.children.length).toBe(3); + + const slots = extractDataIds(shadowShadowLeaf); + const upperLeafSlot = slots['upper-slot']; + // Verify the slot element is not replaced with children at the leaf + expect(upperLeafSlot.tagName.toLowerCase()).toEqual('slot'); + expect(upperLeafSlot.getAttribute('name')).toEqual('upper'); + + const upperForwardedSlot = upperLeafSlot.assignedNodes(); + expect(upperForwardedSlot.length).toEqual(1); + // Verify slot was not modified, ie: + // + expect(upperForwardedSlot[0].tagName.toLowerCase()).toEqual('slot'); + expect(upperForwardedSlot[0].getAttribute('slot')).toEqual('upper'); + expect(upperForwardedSlot[0].getAttribute('name')).toEqual('lower'); + + const upperSlotContent = upperForwardedSlot[0].assignedNodes(); + expect(upperSlotContent.length).toEqual(2); + expect(upperSlotContent[0].getAttribute('slot')).toBe('lower'); + expect(upperSlotContent[0].tagName.toLowerCase()).toBe('x-shadow-dom-element'); + expect(upperSlotContent[1].getAttribute('slot')).toBe('lower'); + expect(upperSlotContent[1].innerText).toEqual('Lower slot forwarded content'); + + const lowerLeafSlot = slots['lower-slot']; + // Verify the slot element is not replaced with children at the leaf + expect(lowerLeafSlot.tagName.toLowerCase()).toEqual('slot'); + expect(lowerLeafSlot.getAttribute('name')).toEqual('lower'); + + const lowerForwardedSlot = lowerLeafSlot.assignedNodes(); + expect(lowerForwardedSlot.length).toEqual(1); + // Verify slot was not modified, ie: + // + expect(lowerForwardedSlot[0].tagName.toLowerCase()).toEqual('slot'); + expect(lowerForwardedSlot[0].getAttribute('slot')).toEqual('lower'); + expect(lowerForwardedSlot[0].getAttribute('name')).toEqual('upper'); + + const lowerSlotContent = lowerForwardedSlot[0].assignedNodes(); + expect(lowerSlotContent.length).toEqual(2); + expect(lowerSlotContent[0].getAttribute('slot')).toBe('upper'); + expect(lowerSlotContent[0].tagName.toLowerCase()).toEqual('x-light-dom-element'); + expect(lowerSlotContent[1].getAttribute('slot')).toBe('upper'); + expect(lowerSlotContent[1].innerText).toEqual('Upper slot forwarded content'); + + const defaultLeafSlot = slots['default-slot']; + expect(defaultLeafSlot.hasAttribute('slot')).toBe(false); + + const defaultForwardedSlot = defaultLeafSlot.assignedNodes(); + // No slots were reassigned to the default leaf slot because they are remapped at the forwarded slot + expect(defaultForwardedSlot.length).toEqual(0); + + const defaultRemappedLeafSlot = slots['default-slot-reassigned']; + expect(defaultRemappedLeafSlot.tagName.toLowerCase()).toEqual('slot'); + expect(defaultRemappedLeafSlot.getAttribute('name')).toEqual('defaultSlotReassigned'); + + const defaultRemappedForwardedSlot = defaultRemappedLeafSlot.assignedNodes(); + expect(defaultRemappedForwardedSlot.length).toEqual(1); + + // Verify slot was not modified, ie: + // Default slot not yet forwarded + expect(defaultRemappedForwardedSlot[0].tagName.toLowerCase()).toEqual('slot'); + expect(defaultRemappedForwardedSlot[0].getAttribute('slot')).toEqual( + 'defaultSlotReassigned' + ); + + const defaultRemappedSlotContent = defaultRemappedForwardedSlot[0].assignedNodes(); + expect(defaultRemappedSlotContent.length).toEqual(process.env.NATIVE_SHADOW ? 2 : 3); + expect(defaultRemappedSlotContent[0].textContent).toEqual('Text being forwarded'); + if (!process.env.NATIVE_SHADOW) { + // Native shadow doesn't slot comments + expect(defaultRemappedSlotContent[1]).toEqual(commentNode); + } + expect(defaultRemappedSlotContent[process.env.NATIVE_SHADOW ? 1 : 2].innerText).toEqual( + 'Static vnode default slot forwarded' + ); + }); +}); diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightContainer/lightContainer.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightContainer/lightContainer.html new file mode 100644 index 0000000000..a87a857acc --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightContainer/lightContainer.html @@ -0,0 +1,41 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightContainer/lightContainer.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightContainer/lightContainer.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightContainer/lightContainer.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightDomElement/lightDomElement.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightDomElement/lightDomElement.html new file mode 100644 index 0000000000..49246e22f2 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightDomElement/lightDomElement.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightDomElement/lightDomElement.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightDomElement/lightDomElement.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightDomElement/lightDomElement.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLeaf/lightLeaf.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLeaf/lightLeaf.html new file mode 100644 index 0000000000..de49d4feda --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLeaf/lightLeaf.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLeaf/lightLeaf.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLeaf/lightLeaf.js new file mode 100644 index 0000000000..4f98e8c1bc --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLeaf/lightLeaf.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class LightContainer extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLightSlot/lightLightSlot.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLightSlot/lightLightSlot.html new file mode 100644 index 0000000000..7e54c84012 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLightSlot/lightLightSlot.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLightSlot/lightLightSlot.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLightSlot/lightLightSlot.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightLightSlot/lightLightSlot.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightShadowSlot/lightShadowSlot.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightShadowSlot/lightShadowSlot.html new file mode 100644 index 0000000000..2ef5abd89a --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightShadowSlot/lightShadowSlot.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightShadowSlot/lightShadowSlot.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightShadowSlot/lightShadowSlot.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/lightShadowSlot/lightShadowSlot.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowDomElement/shadowDomElement.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowDomElement/shadowDomElement.html new file mode 100644 index 0000000000..15804f5198 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowDomElement/shadowDomElement.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowDomElement/shadowDomElement.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowDomElement/shadowDomElement.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowDomElement/shadowDomElement.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLeaf/shadowLeaf.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLeaf/shadowLeaf.html new file mode 100644 index 0000000000..ce609204be --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLeaf/shadowLeaf.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLeaf/shadowLeaf.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLeaf/shadowLeaf.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLeaf/shadowLeaf.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLightSlot/shadowLightSlot.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLightSlot/shadowLightSlot.html new file mode 100644 index 0000000000..77f3a43370 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLightSlot/shadowLightSlot.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLightSlot/shadowLightSlot.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLightSlot/shadowLightSlot.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowLightSlot/shadowLightSlot.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowShadowSlot/shadowShadowSlot.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowShadowSlot/shadowShadowSlot.html new file mode 100644 index 0000000000..c4815fd97d --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowShadowSlot/shadowShadowSlot.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowShadowSlot/shadowShadowSlot.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowShadowSlot/shadowShadowSlot.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/forwarding/x/shadowShadowSlot/shadowShadowSlot.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/index.spec.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/index.spec.js new file mode 100644 index 0000000000..a1735fa91b --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/index.spec.js @@ -0,0 +1,269 @@ +import { createElement } from 'lwc'; +import { extractDataIds, lightDomSlotForwardingEnabled } from 'test-utils'; + +import LightContainer from './x/lightContainer/lightContainer'; + +describe('light DOM slot forwarding reactivity', () => { + let nodes; + let lightContainer; + + beforeAll(() => { + lightContainer = createElement('x-light-container', { is: LightContainer }); + document.body.appendChild(lightContainer); + nodes = extractDataIds(lightContainer); + }); + + afterAll(() => { + document.body.removeChild(lightContainer); + }); + + const verifySlotContent = (leaf, expected) => { + const children = Array.from(leaf.shadowRoot?.children ?? leaf.children); + let expectedIndex = 0; + children.forEach((child) => { + const actualSlotContent = + child.tagName.toLowerCase() === 'slot' ? child.assignedNodes() : [child]; + + actualSlotContent.forEach((slotContent) => { + expect(child.getAttribute('slot')).toEqual(expected[expectedIndex].slotAssignment); + expect(slotContent.innerText).toEqual(expected[expectedIndex++].slotContent); + }); + }); + }; + + const expectedDefaultSlotContent = (shadowMode) => [ + { + slotAssignment: 'upper', + slotContent: 'Upper slot content', + }, + { + slotAssignment: 'lower', + slotContent: 'Lower slot content', + }, + { + slotAssignment: + shadowMode.includes('shadow') || lightDomSlotForwardingEnabled ? '' : null, + slotContent: 'Default slot content', + }, + ]; + + const expectedSlotContentAfterParentMutation = (shadowMode) => [ + { + slotAssignment: 'upper', + slotContent: 'Lower slot content', + }, + { + slotAssignment: 'lower', + slotContent: 'Upper slot content', + }, + { + slotAssignment: + shadowMode.includes('shadow') || lightDomSlotForwardingEnabled ? '' : null, + slotContent: 'Default slot content', + }, + ]; + + const expectedSlotContentAfterForwardedSlotMutation = [ + { + slotAssignment: 'upper', + slotContent: 'Upper slot content', + }, + { + slotAssignment: 'lower', + slotContent: 'Default slot content', + }, + { + slotAssignment: '', + slotContent: 'Lower slot content', + }, + ]; + + const expectedSlotContentAfterLeafMutation = (shadowMode) => [ + { + slotAssignment: 'lower', + slotContent: + shadowMode.includes('shadow') && !lightDomSlotForwardingEnabled + ? 'Lower slot content' + : 'Upper slot content', + }, + { + slotAssignment: '', + slotContent: + shadowMode.includes('shadow') && !lightDomSlotForwardingEnabled + ? 'Upper slot content' + : 'Default slot content', + }, + { + slotAssignment: 'upper', + slotContent: + shadowMode.includes('shadow') && !lightDomSlotForwardingEnabled + ? 'Default slot content' + : 'Lower slot content', + }, + ]; + + const expectedSlotContentAfterConditionalMutation = [ + { + slotAssignment: 'lower', + slotContent: 'Upper slot content', + }, + { + slotAssignment: '', + slotContent: 'Default slot content', + }, + { + slotAssignment: 'upper', + slotContent: 'Lower slot content', + }, + { + slotAssignment: 'upper', + slotContent: 'Conditional slot content', + }, + ]; + + const testCases = [ + { + testName: 'lightLight', + expectedDefaultSlotContent: expectedDefaultSlotContent('light'), + expectedSlotContentAfterParentMutation: expectedSlotContentAfterParentMutation('light'), + expectedSlotContentAfterForwardedSlotMutation: lightDomSlotForwardingEnabled + ? expectedSlotContentAfterForwardedSlotMutation + : expectedSlotContentAfterParentMutation('light'), + expectedSlotContentAfterLeafMutation: lightDomSlotForwardingEnabled + ? expectedSlotContentAfterLeafMutation('light') + : expectedSlotContentAfterParentMutation('light'), + expectedSlotContentAfterConditionalMutation: lightDomSlotForwardingEnabled + ? expectedSlotContentAfterConditionalMutation + : [ + { + slotAssignment: 'upper', + slotContent: 'Lower slot content', + }, + { + slotAssignment: 'upper', + slotContent: 'Conditional slot content', + }, + { + slotAssignment: 'lower', + slotContent: 'Upper slot content', + }, + { + slotAssignment: null, + slotContent: 'Default slot content', + }, + ], + }, + { + testName: 'lightShadow', + expectedDefaultSlotContent: expectedDefaultSlotContent('shadow'), + expectedSlotContentAfterParentMutation: + expectedSlotContentAfterParentMutation('shadow'), + expectedSlotContentAfterForwardedSlotMutation: lightDomSlotForwardingEnabled + ? expectedSlotContentAfterForwardedSlotMutation + : expectedSlotContentAfterParentMutation('shadow'), + expectedSlotContentAfterLeafMutation: expectedSlotContentAfterLeafMutation('shadow'), + expectedSlotContentAfterConditionalMutation: lightDomSlotForwardingEnabled + ? expectedSlotContentAfterConditionalMutation + : [ + { + slotAssignment: 'lower', + slotContent: 'Lower slot content', + }, + { + slotAssignment: 'lower', + slotContent: 'Conditional slot content', + }, + { + slotAssignment: '', + slotContent: 'Upper slot content', + }, + { + slotAssignment: 'upper', + slotContent: 'Default slot content', + }, + ], + }, + ]; + + if (process.env.NATIVE_SHADOW) { + // TODO [#3885]: Using expressions on forwarded synthetic shadow DOM slots throws an error, only test in native for now + testCases.push({ + testName: 'shadowLight', + expectedDefaultSlotContent: expectedDefaultSlotContent('shadow'), + expectedSlotContentAfterParentMutation: + expectedSlotContentAfterParentMutation('shadow'), + expectedSlotContentAfterForwardedSlotMutation, + expectedSlotContentAfterLeafMutation: lightDomSlotForwardingEnabled + ? expectedSlotContentAfterLeafMutation('shadow') + : expectedSlotContentAfterForwardedSlotMutation, + expectedSlotContentAfterConditionalMutation: lightDomSlotForwardingEnabled + ? expectedSlotContentAfterConditionalMutation + : [ + { + slotAssignment: 'upper', + slotContent: 'Upper slot content', + }, + { + slotAssignment: 'lower', + slotContent: 'Default slot content', + }, + { + slotAssignment: '', + slotContent: 'Lower slot content', + }, + { + slotAssignment: '', + slotContent: 'Conditional slot content', + }, + ], + }); + } + + testCases.forEach( + ({ + testName, + expectedDefaultSlotContent, + expectedSlotContentAfterParentMutation, + expectedSlotContentAfterForwardedSlotMutation, + expectedSlotContentAfterLeafMutation, + expectedSlotContentAfterConditionalMutation, + }) => { + it(`should update correctly for ${testName} slots`, async () => { + const parent = nodes[testName]; + const leaf = parent.leaf; + expect((leaf.shadowRoot?.children ?? leaf.children).length).toBe(3); + + verifySlotContent(leaf, expectedDefaultSlotContent); + + lightContainer[`${testName}Upper`] = 'lower'; + lightContainer[`${testName}Lower`] = 'upper'; + + await Promise.resolve(); + + verifySlotContent(leaf, expectedSlotContentAfterParentMutation); + + parent.upperSlot = ''; + parent.lowerSlot = 'upper'; + parent.defaultSlot = 'lower'; + + await Promise.resolve(); + + verifySlotContent(leaf, expectedSlotContentAfterForwardedSlotMutation); + + leaf.upperSlot = 'lower'; + leaf.lowerSlot = ''; + leaf.defaultSlot = 'upper'; + + await Promise.resolve(); + + verifySlotContent(leaf, expectedSlotContentAfterLeafMutation); + + lightContainer[`${testName}ConditionalSlot`] = true; + + await Promise.resolve(); + + verifySlotContent(leaf, expectedSlotContentAfterConditionalMutation); + }); + } + ); +}); diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightContainer/lightContainer.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightContainer/lightContainer.html new file mode 100644 index 0000000000..8cb20994c3 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightContainer/lightContainer.html @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightContainer/lightContainer.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightContainer/lightContainer.js new file mode 100644 index 0000000000..56de5c3645 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightContainer/lightContainer.js @@ -0,0 +1,24 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; + + @api + lightLightUpper = 'upper'; + @api + lightLightLower = 'lower'; + @api + lightShadowUpper = 'upper'; + @api + lightShadowLower = 'lower'; + @api + shadowLightUpper = 'upper'; + @api + shadowLightLower = 'lower'; + @api + lightLightConditionalSlot = false; + @api + lightShadowConditionalSlot = false; + @api + shadowLightConditionalSlot = false; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLeaf/lightLeaf.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLeaf/lightLeaf.html new file mode 100644 index 0000000000..6d057b78cb --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLeaf/lightLeaf.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLeaf/lightLeaf.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLeaf/lightLeaf.js new file mode 100644 index 0000000000..734b6bc1cc --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLeaf/lightLeaf.js @@ -0,0 +1,14 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; + + @api + upperSlot = 'upper'; + + @api + lowerSlot = 'lower'; + + @api + defaultSlot = ''; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLightSlot/lightLightSlot.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLightSlot/lightLightSlot.html new file mode 100644 index 0000000000..2b7d823a6a --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLightSlot/lightLightSlot.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLightSlot/lightLightSlot.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLightSlot/lightLightSlot.js new file mode 100644 index 0000000000..c4fd897542 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightLightSlot/lightLightSlot.js @@ -0,0 +1,19 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; + + @api + upperSlot = 'upper'; + + @api + lowerSlot = 'lower'; + + @api + defaultSlot = ''; + + @api + get leaf() { + return this.querySelector('x-light-leaf'); + } +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightShadowSlot/lightShadowSlot.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightShadowSlot/lightShadowSlot.html new file mode 100644 index 0000000000..390b647b7f --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightShadowSlot/lightShadowSlot.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightShadowSlot/lightShadowSlot.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightShadowSlot/lightShadowSlot.js new file mode 100644 index 0000000000..444666838d --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/lightShadowSlot/lightShadowSlot.js @@ -0,0 +1,19 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; + + @api + upperSlot = 'upper'; + + @api + lowerSlot = 'lower'; + + @api + defaultSlot = ''; + + @api + get leaf() { + return this.querySelector('x-shadow-leaf'); + } +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLeaf/shadowLeaf.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLeaf/shadowLeaf.html new file mode 100644 index 0000000000..219614dce5 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLeaf/shadowLeaf.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLeaf/shadowLeaf.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLeaf/shadowLeaf.js new file mode 100644 index 0000000000..5a76401672 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLeaf/shadowLeaf.js @@ -0,0 +1,12 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api + upperSlot = 'upper'; + + @api + lowerSlot = 'lower'; + + @api + defaultSlot = ''; +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLightSlot/shadowLightSlot.html b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLightSlot/shadowLightSlot.html new file mode 100644 index 0000000000..0d7a512339 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLightSlot/shadowLightSlot.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLightSlot/shadowLightSlot.js b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLightSlot/shadowLightSlot.js new file mode 100644 index 0000000000..724c307373 --- /dev/null +++ b/packages/@lwc/integration-karma/test/light-dom/slot-fowarding/slots/reactivity/x/shadowLightSlot/shadowLightSlot.js @@ -0,0 +1,17 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + @api + upperSlot = 'upper'; + + @api + lowerSlot = 'lower'; + + @api + defaultSlot = ''; + + @api + get leaf() { + return this.template.querySelector('x-light-leaf'); + } +} diff --git a/packages/@lwc/integration-karma/test/light-dom/slotting/index.spec.js b/packages/@lwc/integration-karma/test/light-dom/slotting/index.spec.js index 99e4665444..0406c4c5ac 100644 --- a/packages/@lwc/integration-karma/test/light-dom/slotting/index.spec.js +++ b/packages/@lwc/integration-karma/test/light-dom/slotting/index.spec.js @@ -1,6 +1,8 @@ import { createElement } from 'lwc'; import { extractDataIds } from 'test-utils'; +import { vFragBookEndEnabled, lightDomSlotForwardingEnabled } from 'test-utils'; + import BasicSlot from 'x/basicSlot'; import DynamicChildren from 'x/dynamicChildren'; import LightConsumer from 'x/lightConsumer'; @@ -9,7 +11,7 @@ import ConditionalSlot from 'x/conditionalSlot'; import ConditionalSlotted from 'x/conditionalSlotted'; import ForwardedSlotConsumer from 'x/forwardedSlotConsumer'; -const vFragBookend = process.env.API_VERSION > 59 ? '' : ''; +const vFragBookend = vFragBookEndEnabled ? '' : ''; function createTestElement(tag, component) { const elm = createElement(tag, { is: component }); @@ -105,7 +107,9 @@ describe('Slotting', () => { const nodes = createTestElement('x-forwarded-slot-consumer', ForwardedSlotConsumer); const elm = nodes['x-forwarded-slot-consumer']; expect(elm.innerHTML).toEqual( - `${vFragBookend}

Upper slot content forwarded

${vFragBookend}${vFragBookend}

Default slot forwarded

${vFragBookend}${vFragBookend}

Lower slot content forwarded

${vFragBookend}
` + lightDomSlotForwardingEnabled + ? `${vFragBookend}

Upper slot content forwarded

${vFragBookend}${vFragBookend}

Default slot forwarded

${vFragBookend}${vFragBookend}

Lower slot content forwarded

${vFragBookend}
` + : `${vFragBookend}

Upper slot content forwarded

${vFragBookend}${vFragBookend}

Default slot forwarded

${vFragBookend}${vFragBookend}

Lower slot content forwarded

${vFragBookend}
` ); }); it('should render default content in forwarded slots', async () => { diff --git a/packages/@lwc/shared/src/api-version.ts b/packages/@lwc/shared/src/api-version.ts index 58c257a144..833ed2fbae 100644 --- a/packages/@lwc/shared/src/api-version.ts +++ b/packages/@lwc/shared/src/api-version.ts @@ -80,6 +80,11 @@ export const enum APIFeature { * instead of comment nodes. */ USE_COMMENTS_FOR_FRAGMENT_BOOKENDS, + /** + * If enabled, allows slot forwarding for light DOM slots. This will cause the slot attribute of the slotted + * content to be updated to match the slot attribute of the light DOM slot it slotted into. + */ + USE_LIGHT_DOM_SLOT_FORWARDING, /** * If enabled, we use the native custom element lifecycle events: connectedCallback, disconnectedCallback * rather than synthetic events. @@ -100,6 +105,7 @@ export function isAPIFeatureEnabled( case APIFeature.SKIP_UNNECESSARY_REGISTER_DECORATORS: case APIFeature.USE_COMMENTS_FOR_FRAGMENT_BOOKENDS: return apiVersion >= APIVersion.V60_248_SPRING_24; + case APIFeature.USE_LIGHT_DOM_SLOT_FORWARDING: case APIFeature.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE: return apiVersion >= APIVersion.V61_250_SUMMER_24; } diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-lwc-if-else/slots/named-slots/simple/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-lwc-if-else/slots/named-slots/simple/expected.js index 805ecf841d..c8b2952df9 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-lwc-if-else/slots/named-slots/simple/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-lwc-if-else/slots/named-slots/simple/expected.js @@ -4,9 +4,7 @@ const stc0 = { key: 0, }; const stc1 = { - attrs: { - slot: "slotname1", - }, + slotAssignment: "slotname1", key: 2, }; function tmpl($api, $cmp, $slotset, $ctx) { diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-render-mode/template/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-render-mode/template/expected.js index 6d45ef492a..9bd15e0836 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-render-mode/template/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/directive-render-mode/template/expected.js @@ -10,9 +10,9 @@ const stc1 = { key: 3, }; const stc2 = { + slotAssignment: "forward", attrs: { name: "forwarded", - slot: "forward", }, key: 4, }; diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/dynamic-components/valid/attributes/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/dynamic-components/valid/attributes/expected.js index 89b3f8d583..37ecf7fad3 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/dynamic-components/valid/attributes/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/dynamic-components/valid/attributes/expected.js @@ -2,16 +2,13 @@ import { registerTemplate } from "lwc"; const stc0 = { "slds-style": true, }; -const stc1 = { - slot: "slotName", -}; function tmpl($api, $cmp, $slotset, $ctx) { const { b: api_bind, dc: api_dynamic_component } = $api; const { _m0 } = $ctx; return [ api_dynamic_component($cmp.ctor, { classMap: stc0, - attrs: stc1, + slotAssignment: "slotName", key: 0, on: { click: _m0 || ($ctx._m0 = api_bind($cmp.handleClick)), diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/scoped-slots/parent-mixed-slot-content/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/scoped-slots/parent-mixed-slot-content/expected.js index 6c0bbfe860..049e3cd3b6 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/scoped-slots/parent-mixed-slot-content/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/scoped-slots/parent-mixed-slot-content/expected.js @@ -8,9 +8,7 @@ const stc1 = { key: 1, }; const stc2 = { - attrs: { - slot: "slotname2", - }, + slotAssignment: "slotname2", key: 2, }; function tmpl($api, $cmp, $slotset, $ctx) { diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/scoped-slots/slot-content-with-directives/valid/dynamic-components/parent/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/scoped-slots/slot-content-with-directives/valid/dynamic-components/parent/expected.js index 0342eeb461..db2d4ba154 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/scoped-slots/slot-content-with-directives/valid/dynamic-components/parent/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/scoped-slots/slot-content-with-directives/valid/dynamic-components/parent/expected.js @@ -4,21 +4,15 @@ const stc0 = { key: 0, }; const stc1 = { - attrs: { - slot: "header", - }, + slotAssignment: "header", key: 1, }; const stc2 = { - attrs: { - slot: "body", - }, + slotAssignment: "body", key: 2, }; const stc3 = { - attrs: { - slot: "footer", - }, + slotAssignment: "footer", key: 3, }; function tmpl($api, $cmp, $slotset, $ctx) { diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/dynamic-components/usage/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/dynamic-components/usage/expected.js index 139076ca7b..6e4b610910 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/dynamic-components/usage/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/dynamic-components/usage/expected.js @@ -3,15 +3,11 @@ const stc0 = { key: 0, }; const stc1 = { - attrs: { - slot: "header", - }, + slotAssignment: "header", key: 1, }; const stc2 = { - attrs: { - slot: "", - }, + slotAssignment: "", key: 2, }; function tmpl($api, $cmp, $slotset, $ctx) { diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/dynamic-name/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/dynamic-name/expected.js index 1082e59275..4eda155e48 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/dynamic-name/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/dynamic-name/expected.js @@ -8,9 +8,7 @@ function tmpl($api, $cmp, $slotset, $ctx) { return [ api_custom_element("ns-cmp", _nsCmp, stc0, [ api_element("p", { - attrs: { - slot: $cmp.mySlot, - }, + slotAssignment: $cmp.mySlot, key: 1, }), ]), diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage-if/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage-if/expected.js index 7780905e00..bac675a407 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage-if/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage-if/expected.js @@ -7,15 +7,11 @@ const stc1 = { key: 1, }; const stc2 = { - attrs: { - slot: "", - }, + slotAssignment: "", key: 2, }; const stc3 = { - attrs: { - slot: "", - }, + slotAssignment: "", key: 3, }; function tmpl($api, $cmp, $slotset, $ctx) { diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/actual.html b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/actual.html index 6f3d2eff26..5132cfbcaf 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/actual.html +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/actual.html @@ -1,8 +1,16 @@ diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/ast.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/ast.json index 46a94615e7..af0bb45928 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/ast.json +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/ast.json @@ -4,10 +4,10 @@ "location": { "startLine": 1, "startColumn": 1, - "endLine": 8, + "endLine": 16, "endColumn": 12, "start": 0, - "end": 179, + "end": 605, "startTag": { "startLine": 1, "startColumn": 1, @@ -17,12 +17,12 @@ "end": 10 }, "endTag": { - "startLine": 8, + "startLine": 16, "startColumn": 1, - "endLine": 8, + "endLine": 16, "endColumn": 12, - "start": 168, - "end": 179 + "start": 594, + "end": 605 } }, "directives": [], @@ -34,10 +34,10 @@ "location": { "startLine": 2, "startColumn": 5, - "endLine": 7, + "endLine": 15, "endColumn": 15, "start": 15, - "end": 167, + "end": 593, "startTag": { "startLine": 2, "startColumn": 5, @@ -47,12 +47,12 @@ "end": 24 }, "endTag": { - "startLine": 7, + "startLine": 15, "startColumn": 5, - "endLine": 7, + "endLine": 15, "endColumn": 15, - "start": 157, - "end": 167 + "start": 583, + "end": 593 } }, "attributes": [], @@ -67,10 +67,10 @@ "location": { "startLine": 3, "startColumn": 9, - "endLine": 6, + "endLine": 14, "endColumn": 18, "start": 33, - "end": 152, + "end": 578, "startTag": { "startLine": 3, "startColumn": 9, @@ -80,12 +80,12 @@ "end": 41 }, "endTag": { - "startLine": 6, + "startLine": 14, "startColumn": 9, - "endLine": 6, + "endLine": 14, "endColumn": 18, - "start": 143, - "end": 152 + "start": 569, + "end": 578 } }, "attributes": [], @@ -101,24 +101,380 @@ "startLine": 4, "startColumn": 13, "endLine": 4, - "endColumn": 53, + "endColumn": 50, "start": 54, - "end": 94, + "end": 91, "startTag": { "startLine": 4, "startColumn": 13, "endLine": 4, - "endColumn": 30, + "endColumn": 16, "start": 54, - "end": 71 + "end": 57 }, "endTag": { "startLine": 4, - "startColumn": 49, + "startColumn": 46, "endLine": 4, + "endColumn": 50, + "start": 87, + "end": 91 + } + }, + "attributes": [], + "properties": [], + "directives": [], + "listeners": [], + "children": [ + { + "type": "Text", + "raw": "Default Slot No Slot Attribute", + "value": { + "type": "Literal", + "value": "Default Slot No Slot Attribute" + }, + "location": { + "startLine": 4, + "startColumn": 16, + "endLine": 4, + "endColumn": 46, + "start": 57, + "end": 87 + } + } + ] + }, + { + "type": "Element", + "name": "p", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 5, + "startColumn": 13, + "endLine": 5, + "endColumn": 55, + "start": 104, + "end": 146, + "startTag": { + "startLine": 5, + "startColumn": 13, + "endLine": 5, + "endColumn": 24, + "start": 104, + "end": 115 + }, + "endTag": { + "startLine": 5, + "startColumn": 51, + "endLine": 5, + "endColumn": 55, + "start": 142, + "end": 146 + } + }, + "attributes": [ + { + "type": "Attribute", + "name": "slot", + "value": { + "type": "Literal", + "value": "" + }, + "location": { + "startLine": 5, + "startColumn": 16, + "endLine": 5, + "endColumn": 23, + "start": 107, + "end": 114 + } + } + ], + "properties": [], + "directives": [], + "listeners": [], + "children": [ + { + "type": "Text", + "raw": "Slot Empty String Attribute", + "value": { + "type": "Literal", + "value": "Slot Empty String Attribute" + }, + "location": { + "startLine": 5, + "startColumn": 24, + "endLine": 5, + "endColumn": 51, + "start": 115, + "end": 142 + } + } + ] + }, + { + "type": "Element", + "name": "p", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 6, + "startColumn": 13, + "endLine": 6, + "endColumn": 52, + "start": 159, + "end": 198, + "startTag": { + "startLine": 6, + "startColumn": 13, + "endLine": 6, + "endColumn": 26, + "start": 159, + "end": 172 + }, + "endTag": { + "startLine": 6, + "startColumn": 48, + "endLine": 6, + "endColumn": 52, + "start": 194, + "end": 198 + } + }, + "attributes": [ + { + "type": "Attribute", + "name": "slot", + "value": { + "type": "Literal", + "value": "true" + }, + "location": { + "startLine": 6, + "startColumn": 16, + "endLine": 6, + "endColumn": 25, + "start": 162, + "end": 171 + } + } + ], + "properties": [], + "directives": [], + "listeners": [], + "children": [ + { + "type": "Text", + "raw": "Slot Boolean Attribute", + "value": { + "type": "Literal", + "value": "Slot Boolean Attribute" + }, + "location": { + "startLine": 6, + "startColumn": 26, + "endLine": 6, + "endColumn": 48, + "start": 172, + "end": 194 + } + } + ] + }, + { + "type": "Element", + "name": "p", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 7, + "startColumn": 13, + "endLine": 7, + "endColumn": 57, + "start": 211, + "end": 255, + "startTag": { + "startLine": 7, + "startColumn": 13, + "endLine": 7, + "endColumn": 33, + "start": 211, + "end": 231 + }, + "endTag": { + "startLine": 7, + "startColumn": 53, + "endLine": 7, + "endColumn": 57, + "start": 251, + "end": 255 + } + }, + "attributes": [ + { + "type": "Attribute", + "name": "slot", + "value": { + "type": "MemberExpression", + "start": 1, + "end": 10, + "object": { + "type": "Identifier", + "start": 1, + "end": 5, + "name": "slot" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 10, + "name": "name" + }, + "computed": false, + "optional": false, + "location": { + "startLine": 7, + "startColumn": 16, + "endLine": 7, + "endColumn": 32, + "start": 214, + "end": 230 + } + }, + "location": { + "startLine": 7, + "startColumn": 16, + "endLine": 7, + "endColumn": 32, + "start": 214, + "end": 230 + } + } + ], + "properties": [], + "directives": [], + "listeners": [], + "children": [ + { + "type": "Text", + "raw": "Dynamic Slot Content", + "value": { + "type": "Literal", + "value": "Dynamic Slot Content" + }, + "location": { + "startLine": 7, + "startColumn": 33, + "endLine": 7, + "endColumn": 53, + "start": 231, + "end": 251 + } + } + ] + }, + { + "type": "Element", + "name": "p", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 8, + "startColumn": 13, + "endLine": 8, + "endColumn": 67, + "start": 268, + "end": 322, + "startTag": { + "startLine": 8, + "startColumn": 13, + "endLine": 8, + "endColumn": 36, + "start": 268, + "end": 291 + }, + "endTag": { + "startLine": 8, + "startColumn": 63, + "endLine": 8, + "endColumn": 67, + "start": 318, + "end": 322 + } + }, + "attributes": [ + { + "type": "Attribute", + "name": "slot", + "value": { + "type": "Identifier", + "start": 1, + "end": 13, + "name": "slotVariable", + "location": { + "startLine": 8, + "startColumn": 16, + "endLine": 8, + "endColumn": 35, + "start": 271, + "end": 290 + } + }, + "location": { + "startLine": 8, + "startColumn": 16, + "endLine": 8, + "endColumn": 35, + "start": 271, + "end": 290 + } + } + ], + "properties": [], + "directives": [], + "listeners": [], + "children": [ + { + "type": "Text", + "raw": "Variable As Slot Assignment", + "value": { + "type": "Literal", + "value": "Variable As Slot Assignment" + }, + "location": { + "startLine": 8, + "startColumn": 36, + "endLine": 8, + "endColumn": 63, + "start": 291, + "end": 318 + } + } + ] + }, + { + "type": "Element", + "name": "p", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 9, + "startColumn": 13, + "endLine": 9, + "endColumn": 53, + "start": 335, + "end": 375, + "startTag": { + "startLine": 9, + "startColumn": 13, + "endLine": 9, + "endColumn": 30, + "start": 335, + "end": 352 + }, + "endTag": { + "startLine": 9, + "startColumn": 49, + "endLine": 9, "endColumn": 53, - "start": 90, - "end": 94 + "start": 371, + "end": 375 } }, "attributes": [ @@ -130,12 +486,12 @@ "value": "header" }, "location": { - "startLine": 4, + "startLine": 9, "startColumn": 16, - "endLine": 4, + "endLine": 9, "endColumn": 29, - "start": 57, - "end": 70 + "start": 338, + "end": 351 } } ], @@ -151,12 +507,12 @@ "value": "Header Slot Content" }, "location": { - "startLine": 4, + "startLine": 9, "startColumn": 30, - "endLine": 4, + "endLine": 9, "endColumn": 49, - "start": 71, - "end": 90 + "start": 352, + "end": 371 } } ] @@ -166,27 +522,27 @@ "name": "p", "namespace": "http://www.w3.org/1999/xhtml", "location": { - "startLine": 5, + "startLine": 10, "startColumn": 13, - "endLine": 5, + "endLine": 10, "endColumn": 40, - "start": 107, - "end": 134, + "start": 388, + "end": 415, "startTag": { - "startLine": 5, + "startLine": 10, "startColumn": 13, - "endLine": 5, + "endLine": 10, "endColumn": 21, - "start": 107, - "end": 115 + "start": 388, + "end": 396 }, "endTag": { - "startLine": 5, + "startLine": 10, "startColumn": 36, - "endLine": 5, + "endLine": 10, "endColumn": 40, - "start": 130, - "end": 134 + "start": 411, + "end": 415 } }, "attributes": [ @@ -198,12 +554,12 @@ "value": true }, "location": { - "startLine": 5, + "startLine": 10, "startColumn": 16, - "endLine": 5, + "endLine": 10, "endColumn": 20, - "start": 110, - "end": 114 + "start": 391, + "end": 395 } } ], @@ -219,12 +575,216 @@ "value": "Default Content" }, "location": { - "startLine": 5, + "startLine": 10, "startColumn": 21, - "endLine": 5, + "endLine": 10, "endColumn": 36, - "start": 115, - "end": 130 + "start": 396, + "end": 411 + } + } + ] + }, + { + "type": "Element", + "name": "p", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 11, + "startColumn": 13, + "endLine": 11, + "endColumn": 57, + "start": 428, + "end": 472, + "startTag": { + "startLine": 11, + "startColumn": 13, + "endLine": 11, + "endColumn": 31, + "start": 428, + "end": 446 + }, + "endTag": { + "startLine": 11, + "startColumn": 53, + "endLine": 11, + "endColumn": 57, + "start": 468, + "end": 472 + } + }, + "attributes": [ + { + "type": "Attribute", + "name": "slot", + "value": { + "type": "Literal", + "value": "undefined" + }, + "location": { + "startLine": 11, + "startColumn": 16, + "endLine": 11, + "endColumn": 30, + "start": 431, + "end": 445 + } + } + ], + "properties": [], + "directives": [], + "listeners": [], + "children": [ + { + "type": "Text", + "raw": "Undefined Slot Content", + "value": { + "type": "Literal", + "value": "Undefined Slot Content" + }, + "location": { + "startLine": 11, + "startColumn": 31, + "endLine": 11, + "endColumn": 53, + "start": 446, + "end": 468 + } + } + ] + }, + { + "type": "Element", + "name": "p", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 12, + "startColumn": 13, + "endLine": 12, + "endColumn": 47, + "start": 485, + "end": 519, + "startTag": { + "startLine": 12, + "startColumn": 13, + "endLine": 12, + "endColumn": 26, + "start": 485, + "end": 498 + }, + "endTag": { + "startLine": 12, + "startColumn": 43, + "endLine": 12, + "endColumn": 47, + "start": 515, + "end": 519 + } + }, + "attributes": [ + { + "type": "Attribute", + "name": "slot", + "value": { + "type": "Literal", + "value": "null" + }, + "location": { + "startLine": 12, + "startColumn": 16, + "endLine": 12, + "endColumn": 25, + "start": 488, + "end": 497 + } + } + ], + "properties": [], + "directives": [], + "listeners": [], + "children": [ + { + "type": "Text", + "raw": "Null Slot Content", + "value": { + "type": "Literal", + "value": "Null Slot Content" + }, + "location": { + "startLine": 12, + "startColumn": 26, + "endLine": 12, + "endColumn": 43, + "start": 498, + "end": 515 + } + } + ] + }, + { + "type": "Element", + "name": "p", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 13, + "startColumn": 13, + "endLine": 13, + "endColumn": 41, + "start": 532, + "end": 560, + "startTag": { + "startLine": 13, + "startColumn": 13, + "endLine": 13, + "endColumn": 21, + "start": 532, + "end": 540 + }, + "endTag": { + "startLine": 13, + "startColumn": 37, + "endLine": 13, + "endColumn": 41, + "start": 556, + "end": 560 + } + }, + "attributes": [ + { + "type": "Attribute", + "name": "slot", + "value": { + "type": "Literal", + "value": true + }, + "location": { + "startLine": 13, + "startColumn": 16, + "endLine": 13, + "endColumn": 20, + "start": 535, + "end": 539 + } + } + ], + "properties": [], + "directives": [], + "listeners": [], + "children": [ + { + "type": "Text", + "raw": "Empty slot value", + "value": { + "type": "Literal", + "value": "Empty slot value" + }, + "location": { + "startLine": 13, + "startColumn": 21, + "endLine": 13, + "endColumn": 37, + "start": 540, + "end": 556 } } ] diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/expected.js index a990a56eca..999e159ae0 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/slots/usage/expected.js @@ -1,5 +1,6 @@ import _nsCmp from "ns/cmp"; -import { registerTemplate } from "lwc"; +import { parseFragment, registerTemplate } from "lwc"; +const $fragment1 = parseFragment`Default Slot No Slot Attribute

`; const stc0 = { key: 0, }; @@ -7,24 +8,67 @@ const stc1 = { key: 1, }; const stc2 = { - attrs: { - slot: "header", - }, - key: 2, + slotAssignment: "", + key: 4, }; const stc3 = { - attrs: { - slot: "", - }, - key: 3, + slotAssignment: "true", + key: 5, +}; +const stc4 = { + slotAssignment: "header", + key: 8, +}; +const stc5 = { + slotAssignment: "", + key: 9, +}; +const stc6 = { + slotAssignment: "undefined", + key: 10, +}; +const stc7 = { + slotAssignment: "null", + key: 11, +}; +const stc8 = { + slotAssignment: "", + key: 12, }; function tmpl($api, $cmp, $slotset, $ctx) { - const { t: api_text, h: api_element, c: api_custom_element } = $api; + const { + st: api_static_fragment, + t: api_text, + h: api_element, + c: api_custom_element, + } = $api; return [ api_element("section", stc0, [ api_custom_element("ns-cmp", _nsCmp, stc1, [ - api_element("p", stc2, [api_text("Header Slot Content")]), - api_element("p", stc3, [api_text("Default Content")]), + api_static_fragment($fragment1(), 3), + api_element("p", stc2, [api_text("Slot Empty String Attribute")]), + api_element("p", stc3, [api_text("Slot Boolean Attribute")]), + api_element( + "p", + { + slotAssignment: $cmp.slot.name, + key: 6, + }, + [api_text("Dynamic Slot Content")] + ), + api_element( + "p", + { + slotAssignment: $cmp.slotVariable, + key: 7, + }, + [api_text("Variable As Slot Assignment")] + ), + api_element("p", stc4, [api_text("Header Slot Content")]), + api_element("p", stc5, [api_text("Default Content")]), + api_element("p", stc6, [api_text("Undefined Slot Content")]), + api_element("p", stc7, [api_text("Null Slot Content")]), + api_element("p", stc8, [api_text("Empty slot value")]), ]), ]), ]; diff --git a/packages/@lwc/template-compiler/src/codegen/index.ts b/packages/@lwc/template-compiler/src/codegen/index.ts index b6a10d2194..90273fa7f1 100644 --- a/packages/@lwc/template-compiler/src/codegen/index.ts +++ b/packages/@lwc/template-compiler/src/codegen/index.ts @@ -547,6 +547,14 @@ function transform(codeGen: CodeGen): t.Expression { const styleAST = styleMapToStyleDeclsAST(styleMap); data.push(t.property(t.identifier('styleDecls'), styleAST)); } + } else if (name === 'slot') { + let slotValue; + if (isExpression(value)) { + slotValue = codeGen.bindExpression(value); + } else { + slotValue = isStringLiteral(value) ? t.literal(value.value) : t.literal(''); + } + data.push(t.property(t.identifier('slotAssignment'), slotValue)); } else { rest[name] = computeAttrValue(attr, element, !addSanitizationHook); }