Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(runtime): nested multiple default slot relocation #5403

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions src/runtime/dom-extras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => {
HostElementPrototype.__appendChild = HostElementPrototype.appendChild;
HostElementPrototype.appendChild = function (this: d.RenderNode, newChild: d.RenderNode) {
const slotName = (newChild['s-sn'] = getSlotName(newChild));
const slotNode = getHostSlotNode(this.childNodes, slotName);
const slotNode = getHostSlotNode(this.childNodes, slotName, this.tagName);
if (slotNode) {
const slotChildNodes = getHostSlotChildNodes(slotNode, slotName);
const appendAfter = slotChildNodes[slotChildNodes.length - 1];
Expand All @@ -107,7 +107,7 @@ const patchSlotRemoveChild = (ElementPrototype: any) => {
ElementPrototype.__removeChild = ElementPrototype.removeChild;
ElementPrototype.removeChild = function (this: d.RenderNode, toRemove: d.RenderNode) {
if (toRemove && typeof toRemove['s-sn'] !== 'undefined') {
const slotNode = getHostSlotNode(this.childNodes, toRemove['s-sn']);
const slotNode = getHostSlotNode(this.childNodes, toRemove['s-sn'], this.tagName);
if (slotNode) {
// Get all slot content
const slotChildNodes = getHostSlotChildNodes(slotNode, toRemove['s-sn']);
Expand Down Expand Up @@ -141,7 +141,7 @@ export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => {
newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode;
}
const slotName = (newChild['s-sn'] = getSlotName(newChild));
const slotNode = getHostSlotNode(this.childNodes, slotName);
const slotNode = getHostSlotNode(this.childNodes, slotName, this.tagName);
if (slotNode) {
const slotPlaceholder: d.RenderNode = document.createTextNode('') as any;
slotPlaceholder['s-nr'] = newChild;
Expand Down Expand Up @@ -322,7 +322,7 @@ export const patchTextContent = (hostElementPrototype: HTMLElement): void => {
get(): string | null {
// get the 'default slot', which would be the first slot in a shadow tree (if we were using one), whose name is
// the empty string
const slotNode = getHostSlotNode(this.childNodes, '');
const slotNode = getHostSlotNode(this.childNodes, '', this.tagName);
// when a slot node is found, the textContent _may_ be found in the next sibling (text) node, depending on how
// nodes were reordered during the vdom render. first try to get the text content from the sibling.
if (slotNode?.nextSibling?.nodeType === NODE_TYPES.TEXT_NODE) {
Expand All @@ -338,7 +338,7 @@ export const patchTextContent = (hostElementPrototype: HTMLElement): void => {
set(value: string | null) {
// get the 'default slot', which would be the first slot in a shadow tree (if we were using one), whose name is
// the empty string
const slotNode = getHostSlotNode(this.childNodes, '');
const slotNode = getHostSlotNode(this.childNodes, '', this.tagName);
// when a slot node is found, the textContent _may_ need to be placed in the next sibling (text) node,
// depending on how nodes were reordered during the vdom render. first try to set the text content on the
// sibling.
Expand Down Expand Up @@ -431,18 +431,19 @@ const getSlotName = (node: d.RenderNode) =>
* Recursively searches a series of child nodes for a slot with the provided name.
* @param childNodes the nodes to search for a slot with a specific name.
* @param slotName the name of the slot to match on.
* @param hostName the host name of the slot to match on.
* @returns a reference to the slot node that matches the provided name, `null` otherwise
*/
const getHostSlotNode = (childNodes: NodeListOf<ChildNode>, slotName: string) => {
const getHostSlotNode = (childNodes: NodeListOf<ChildNode>, slotName: string, hostName: string) => {
let i = 0;
let childNode: d.RenderNode;

for (; i < childNodes.length; i++) {
childNode = childNodes[i] as any;
if (childNode['s-sr'] && childNode['s-sn'] === slotName) {
if (childNode['s-sr'] && childNode['s-sn'] === slotName && childNode['s-hn'] === hostName) {
return childNode;
}
childNode = getHostSlotNode(childNode.childNodes, slotName);
childNode = getHostSlotNode(childNode.childNodes, slotName, hostName);
if (childNode) {
return childNode;
}
Expand Down
10 changes: 8 additions & 2 deletions src/runtime/vdom/vdom-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,12 +1061,18 @@ render() {
(insertBeforeNode && insertBeforeNode.nodeType === NODE_TYPE.ElementNode)
) {
let orgLocationNode = nodeToRelocate['s-ol']?.previousSibling as d.RenderNode | null;

while (orgLocationNode) {
let refNode = orgLocationNode['s-nr'] ?? null;

if (refNode && refNode['s-sn'] === nodeToRelocate['s-sn'] && parentNodeRef === refNode.parentNode) {
refNode = refNode.nextSibling as any;
refNode = refNode.nextSibling as d.RenderNode | null;

// If the refNode is the same node to be relocated or another element's slot reference, keep searching to find the
// correct relocation target
while (refNode === nodeToRelocate || refNode?.['s-sr']) {
refNode = refNode?.nextSibling as d.RenderNode | null;
}

if (!refNode || !refNode['s-nr']) {
insertBeforeNode = refNode;
break;
Expand Down
26 changes: 0 additions & 26 deletions test/karma/test-app/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,6 @@ export namespace Components {
}
interface SlotChildrenRoot {
}
interface SlotConditionalRendering {
}
interface SlotDynamicNameChangeScoped {
"slotName": string;
}
Expand Down Expand Up @@ -262,8 +260,6 @@ export namespace Components {
interface SlotParentTagChangeRoot {
"element": string;
}
interface SlotRef {
}
interface SlotReorder {
"reordered": boolean;
}
Expand Down Expand Up @@ -843,12 +839,6 @@ declare global {
prototype: HTMLSlotChildrenRootElement;
new (): HTMLSlotChildrenRootElement;
};
interface HTMLSlotConditionalRenderingElement extends Components.SlotConditionalRendering, HTMLStencilElement {
}
var HTMLSlotConditionalRenderingElement: {
prototype: HTMLSlotConditionalRenderingElement;
new (): HTMLSlotConditionalRenderingElement;
};
interface HTMLSlotDynamicNameChangeScopedElement extends Components.SlotDynamicNameChangeScoped, HTMLStencilElement {
}
var HTMLSlotDynamicNameChangeScopedElement: {
Expand Down Expand Up @@ -1011,12 +1001,6 @@ declare global {
prototype: HTMLSlotParentTagChangeRootElement;
new (): HTMLSlotParentTagChangeRootElement;
};
interface HTMLSlotRefElement extends Components.SlotRef, HTMLStencilElement {
}
var HTMLSlotRefElement: {
prototype: HTMLSlotRefElement;
new (): HTMLSlotRefElement;
};
interface HTMLSlotReorderElement extends Components.SlotReorder, HTMLStencilElement {
}
var HTMLSlotReorderElement: {
Expand Down Expand Up @@ -1203,7 +1187,6 @@ declare global {
"slot-basic-order-root": HTMLSlotBasicOrderRootElement;
"slot-basic-root": HTMLSlotBasicRootElement;
"slot-children-root": HTMLSlotChildrenRootElement;
"slot-conditional-rendering": HTMLSlotConditionalRenderingElement;
"slot-dynamic-name-change-scoped": HTMLSlotDynamicNameChangeScopedElement;
"slot-dynamic-name-change-shadow": HTMLSlotDynamicNameChangeShadowElement;
"slot-dynamic-scoped-list": HTMLSlotDynamicScopedListElement;
Expand Down Expand Up @@ -1231,7 +1214,6 @@ declare global {
"slot-no-default": HTMLSlotNoDefaultElement;
"slot-parent-tag-change": HTMLSlotParentTagChangeElement;
"slot-parent-tag-change-root": HTMLSlotParentTagChangeRootElement;
"slot-ref": HTMLSlotRefElement;
"slot-reorder": HTMLSlotReorderElement;
"slot-reorder-root": HTMLSlotReorderRootElement;
"slot-replace-wrapper": HTMLSlotReplaceWrapperElement;
Expand Down Expand Up @@ -1441,8 +1423,6 @@ declare namespace LocalJSX {
}
interface SlotChildrenRoot {
}
interface SlotConditionalRendering {
}
interface SlotDynamicNameChangeScoped {
"slotName"?: string;
}
Expand Down Expand Up @@ -1511,8 +1491,6 @@ declare namespace LocalJSX {
interface SlotParentTagChangeRoot {
"element"?: string;
}
interface SlotRef {
}
interface SlotReorder {
"reordered"?: boolean;
}
Expand Down Expand Up @@ -1629,7 +1607,6 @@ declare namespace LocalJSX {
"slot-basic-order-root": SlotBasicOrderRoot;
"slot-basic-root": SlotBasicRoot;
"slot-children-root": SlotChildrenRoot;
"slot-conditional-rendering": SlotConditionalRendering;
"slot-dynamic-name-change-scoped": SlotDynamicNameChangeScoped;
"slot-dynamic-name-change-shadow": SlotDynamicNameChangeShadow;
"slot-dynamic-scoped-list": SlotDynamicScopedList;
Expand Down Expand Up @@ -1657,7 +1634,6 @@ declare namespace LocalJSX {
"slot-no-default": SlotNoDefault;
"slot-parent-tag-change": SlotParentTagChange;
"slot-parent-tag-change-root": SlotParentTagChangeRoot;
"slot-ref": SlotRef;
"slot-reorder": SlotReorder;
"slot-reorder-root": SlotReorderRoot;
"slot-replace-wrapper": SlotReplaceWrapper;
Expand Down Expand Up @@ -1759,7 +1735,6 @@ declare module "@stencil/core" {
"slot-basic-order-root": LocalJSX.SlotBasicOrderRoot & JSXBase.HTMLAttributes<HTMLSlotBasicOrderRootElement>;
"slot-basic-root": LocalJSX.SlotBasicRoot & JSXBase.HTMLAttributes<HTMLSlotBasicRootElement>;
"slot-children-root": LocalJSX.SlotChildrenRoot & JSXBase.HTMLAttributes<HTMLSlotChildrenRootElement>;
"slot-conditional-rendering": LocalJSX.SlotConditionalRendering & JSXBase.HTMLAttributes<HTMLSlotConditionalRenderingElement>;
"slot-dynamic-name-change-scoped": LocalJSX.SlotDynamicNameChangeScoped & JSXBase.HTMLAttributes<HTMLSlotDynamicNameChangeScopedElement>;
"slot-dynamic-name-change-shadow": LocalJSX.SlotDynamicNameChangeShadow & JSXBase.HTMLAttributes<HTMLSlotDynamicNameChangeShadowElement>;
"slot-dynamic-scoped-list": LocalJSX.SlotDynamicScopedList & JSXBase.HTMLAttributes<HTMLSlotDynamicScopedListElement>;
Expand Down Expand Up @@ -1787,7 +1762,6 @@ declare module "@stencil/core" {
"slot-no-default": LocalJSX.SlotNoDefault & JSXBase.HTMLAttributes<HTMLSlotNoDefaultElement>;
"slot-parent-tag-change": LocalJSX.SlotParentTagChange & JSXBase.HTMLAttributes<HTMLSlotParentTagChangeElement>;
"slot-parent-tag-change-root": LocalJSX.SlotParentTagChangeRoot & JSXBase.HTMLAttributes<HTMLSlotParentTagChangeRootElement>;
"slot-ref": LocalJSX.SlotRef & JSXBase.HTMLAttributes<HTMLSlotRefElement>;
"slot-reorder": LocalJSX.SlotReorder & JSXBase.HTMLAttributes<HTMLSlotReorderElement>;
"slot-reorder-root": LocalJSX.SlotReorderRoot & JSXBase.HTMLAttributes<HTMLSlotReorderRootElement>;
"slot-replace-wrapper": LocalJSX.SlotReplaceWrapper & JSXBase.HTMLAttributes<HTMLSlotReplaceWrapperElement>;
Expand Down
9 changes: 0 additions & 9 deletions test/karma/test-app/slot-conditional-rendering/index.html

This file was deleted.

39 changes: 0 additions & 39 deletions test/karma/test-app/slot-conditional-rendering/karma.spec.ts

This file was deleted.

6 changes: 0 additions & 6 deletions test/karma/test-app/slot-ref/index.html

This file was deleted.

19 changes: 0 additions & 19 deletions test/karma/test-app/slot-ref/karma.spec.ts

This file was deleted.

2 changes: 1 addition & 1 deletion test/wdio/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions test/wdio/slot-conditional-rendering/cmp.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { h } from '@stencil/core';
import { render } from '@wdio/browser-runner/stencil';

describe('slot-conditional-rendering', function () {
const getHeaderVisibilityToggle = () => $('#header-visibility-toggle');
const getContentVisibilityToggle = () => $('#content-visibility-toggle');
const getHeaderElementInLightDOM = () => $('#slotted-header-element-id');
const getContentElementInLightDOM = () => $('#slotted-content-element-id');

beforeEach(() => {
render({
template: () => (
<slot-conditional-rendering>
<span slot="header" id="slotted-header-element-id">
Hello
</span>
<span id="slotted-content-element-id">World!</span>
</slot-conditional-rendering>
),
});
});

it('slots are not hidden', async () => {
await expect(await getHeaderElementInLightDOM().getAttribute('hidden')).toBeNull();
await expect(await getContentElementInLightDOM().getAttribute('hidden')).toBeNull();
});

it('header slot becomes hidden after hit the toggle button', async () => {
await expect(await getHeaderElementInLightDOM().getAttribute('hidden')).toBeNull();

await getHeaderVisibilityToggle()?.click();
await expect(await getHeaderElementInLightDOM().getAttribute('hidden')).not.toBeNull();
});

it('content slot becomes hidden after hit the toggle button', async () => {
await expect(await getContentElementInLightDOM().getAttribute('hidden')).toBeNull();

await getContentVisibilityToggle()?.click();
await expect(await getContentElementInLightDOM().getAttribute('hidden')).not.toBeNull();
});
});
22 changes: 22 additions & 0 deletions test/wdio/slot-nested-dynamic/cmp-child.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Component, h, Host } from '@stencil/core';

@Component({
tag: 'slot-nested-dynamic-child',
shadow: false,
scoped: true,
})
export class SlotNestedDynamicChild {
render() {
return (
<Host>
<slot-nested-dynamic-wrapper id="banner">Banner notification</slot-nested-dynamic-wrapper>
<slot name="header" />
<slot-nested-dynamic-wrapper id="level-1">
<slot-nested-dynamic-wrapper id="level-2">
<slot />
</slot-nested-dynamic-wrapper>
</slot-nested-dynamic-wrapper>
</Host>
);
}
}
19 changes: 19 additions & 0 deletions test/wdio/slot-nested-dynamic/cmp-parent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Component, h, Host } from '@stencil/core';

@Component({
tag: 'slot-nested-dynamic-parent',
shadow: false,
scoped: true,
})
export class SlotNestedDynamicParent {
render() {
return (
<Host>
<slot-nested-dynamic-child>
<span slot="header">Header Text</span>
<slot />
</slot-nested-dynamic-child>
</Host>
);
}
}
16 changes: 16 additions & 0 deletions test/wdio/slot-nested-dynamic/cmp-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Component, h, Host } from '@stencil/core';

@Component({
tag: 'slot-nested-dynamic-wrapper',
shadow: false,
scoped: true,
})
export class SlotNestedDynamicWrapper {
render() {
return (
<Host>
<slot />
</Host>
);
}
}
Loading
Loading