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 @@
+
+
+
+ {title}
+
+
+
\ 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 @@
+
+
+
+
+ Upper slot forwarded content
+ Lower slot forwarded content
+ Text being forwarded
+
+ Static vnode default slot forwarded
+
+
+
+
+
+ Upper slot forwarded content
+ Lower slot forwarded content
+ Text being forwarded
+
+ Static vnode default slot forwarded
+
+
+
+
+
+ Upper slot forwarded content
+ Lower slot forwarded content
+ Text being forwarded
+
+ Static vnode default slot forwarded
+
+
+
+
+
+ Upper slot forwarded content
+ Lower slot forwarded content
+ Text being forwarded
+
+ Static vnode default slot forwarded
+
+
\ 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 @@
+
+ Light DOM custom element content
+
\ 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 @@
+
+
+
+
+
+
+
+ Default slot not yet forwarded
+
+
\ 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 @@
+
+
+
+
+
+
+ Default slot not yet forwarded
+
+
\ 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 @@
+
+ Shadow DOM custom element content
+
\ 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 @@
+
+
+
+
+
+
+ Default slot not yet forwarded
+
+
\ 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 @@
+
+
+
+
+
+
+
+ Default slot not yet forwarded
+
+
\ 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 @@
+
+
+ Upper slot content
+ Lower slot content
+ Default slot content
+
+ Conditional slot content
+
+
+
+ Upper slot content
+ Lower slot content
+ Default slot content
+
+ Conditional slot content
+
+
+
+ Upper slot content
+ Lower slot content
+ Default slot content
+
+ Conditional slot content
+
+
\ 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 @@
+ Default Slot No Slot Attribute
+ Slot Empty String Attribute
+ Slot Boolean Attribute
+ Dynamic Slot Content
+ Variable As Slot Assignment
Header Slot Content
Default Content
+ Undefined Slot Content
+ Null Slot Content
+ Empty slot value
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);
}