Skip to content

Commit ee60f3b

Browse files
fix(runtime): slot name forwarding & attribute reset (#4993)
* always update slot attributes when relocating * add test case * darn apostrophe Co-authored-by: Ryan Waskiewicz <ryanwaskiewicz@gmail.com> * remove slot ref check --------- Co-authored-by: Ryan Waskiewicz <ryanwaskiewicz@gmail.com>
1 parent 4959295 commit ee60f3b

7 files changed

Lines changed: 138 additions & 24 deletions

File tree

src/runtime/vdom/vdom-render.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,13 @@ const putBackInOriginalLocation = (parentElm: Node, recursive: boolean) => {
187187
// Reset so we can correctly move the node around again.
188188
childNode['s-sh'] = undefined;
189189

190+
// When putting an element node back in its original location,
191+
// we need to reset the `slot` attribute back to the value it originally had
192+
// so we can correctly relocate it again in the future
193+
if (childNode.nodeType === NODE_TYPE.ElementNode) {
194+
childNode.setAttribute('slot', childNode['s-sn'] ?? '');
195+
}
196+
190197
checkSlotRelocate = true;
191198
}
192199

@@ -666,23 +673,29 @@ const updateFallbackSlotVisibility = (elm: d.RenderNode) => {
666673
// we need to check all of its sibling nodes in order to see if
667674
// `childNode` should be hidden
668675
for (const siblingNode of childNodes) {
669-
if (siblingNode['s-hn'] !== childNode['s-hn'] || slotName !== '') {
670-
// this sibling node is from a different component OR is a named
671-
// fallback slot node
672-
if (siblingNode.nodeType === NODE_TYPE.ElementNode && slotName === siblingNode.getAttribute('slot')) {
673-
childNode.hidden = true;
674-
break;
675-
}
676-
} else {
677-
// this is a default fallback slot node
678-
// any element or text node (with content)
679-
// should hide the default fallback slot node
680-
if (
681-
siblingNode.nodeType === NODE_TYPE.ElementNode ||
682-
(siblingNode.nodeType === NODE_TYPE.TextNode && siblingNode.textContent.trim() !== '')
683-
) {
684-
childNode.hidden = true;
685-
break;
676+
// Don't check the node against itself
677+
if (siblingNode !== childNode) {
678+
if (siblingNode['s-hn'] !== childNode['s-hn'] || slotName !== '') {
679+
// this sibling node is from a different component OR is a named
680+
// fallback slot node
681+
if (
682+
siblingNode.nodeType === NODE_TYPE.ElementNode &&
683+
(slotName === siblingNode.getAttribute('slot') || slotName === siblingNode['s-sn'])
684+
) {
685+
childNode.hidden = true;
686+
break;
687+
}
688+
} else {
689+
// this is a default fallback slot node
690+
// any element or text node (with content)
691+
// should hide the default fallback slot node
692+
if (
693+
siblingNode.nodeType === NODE_TYPE.ElementNode ||
694+
(siblingNode.nodeType === NODE_TYPE.TextNode && siblingNode.textContent.trim() !== '')
695+
) {
696+
childNode.hidden = true;
697+
break;
698+
}
686699
}
687700
}
688701
}
@@ -1009,8 +1022,7 @@ render() {
10091022
nodeToRelocate['s-hn'] = nodeToRelocate['s-ol'].parentNode.nodeName;
10101023
}
10111024

1012-
// Handle a niche use-case where we relocate a slot from a non-shadow
1013-
// to shadow component (and potentially into further nested components) where
1025+
// Handle a use-case where we relocate a slot where
10141026
// the slot name changes along the way (for instance, a default to a named slot).
10151027
// In this case, we need to update the relocated node's slot attribute to match
10161028
// the slot name it is being relocated into.
@@ -1021,7 +1033,6 @@ render() {
10211033
if (
10221034
BUILD.experimentalSlotFixes &&
10231035
nodeToRelocate.nodeType === NODE_TYPE.ElementNode &&
1024-
slotRefNode.parentElement?.shadowRoot != null &&
10251036
slotRefNode['s-fs'] !== nodeToRelocate.getAttribute('slot')
10261037
) {
10271038
if (!slotRefNode['s-fs']) {

test/karma/test-app/components.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,11 @@ export namespace Components {
331331
}
332332
interface SlotMapOrderRoot {
333333
}
334+
interface SlotNestedNameChange {
335+
}
336+
interface SlotNestedNameChangeChild {
337+
"state": boolean;
338+
}
334339
interface SlotNestedOrderChild {
335340
}
336341
interface SlotNestedOrderParent {
@@ -1240,6 +1245,18 @@ declare global {
12401245
prototype: HTMLSlotMapOrderRootElement;
12411246
new (): HTMLSlotMapOrderRootElement;
12421247
};
1248+
interface HTMLSlotNestedNameChangeElement extends Components.SlotNestedNameChange, HTMLStencilElement {
1249+
}
1250+
var HTMLSlotNestedNameChangeElement: {
1251+
prototype: HTMLSlotNestedNameChangeElement;
1252+
new (): HTMLSlotNestedNameChangeElement;
1253+
};
1254+
interface HTMLSlotNestedNameChangeChildElement extends Components.SlotNestedNameChangeChild, HTMLStencilElement {
1255+
}
1256+
var HTMLSlotNestedNameChangeChildElement: {
1257+
prototype: HTMLSlotNestedNameChangeChildElement;
1258+
new (): HTMLSlotNestedNameChangeChildElement;
1259+
};
12431260
interface HTMLSlotNestedOrderChildElement extends Components.SlotNestedOrderChild, HTMLStencilElement {
12441261
}
12451262
var HTMLSlotNestedOrderChildElement: {
@@ -1487,6 +1504,8 @@ declare global {
14871504
"slot-list-light-scoped-root": HTMLSlotListLightScopedRootElement;
14881505
"slot-map-order": HTMLSlotMapOrderElement;
14891506
"slot-map-order-root": HTMLSlotMapOrderRootElement;
1507+
"slot-nested-name-change": HTMLSlotNestedNameChangeElement;
1508+
"slot-nested-name-change-child": HTMLSlotNestedNameChangeChildElement;
14901509
"slot-nested-order-child": HTMLSlotNestedOrderChildElement;
14911510
"slot-nested-order-parent": HTMLSlotNestedOrderParentElement;
14921511
"slot-ng-if": HTMLSlotNgIfElement;
@@ -1837,6 +1856,11 @@ declare namespace LocalJSX {
18371856
}
18381857
interface SlotMapOrderRoot {
18391858
}
1859+
interface SlotNestedNameChange {
1860+
}
1861+
interface SlotNestedNameChangeChild {
1862+
"state"?: boolean;
1863+
}
18401864
interface SlotNestedOrderChild {
18411865
}
18421866
interface SlotNestedOrderParent {
@@ -2006,6 +2030,8 @@ declare namespace LocalJSX {
20062030
"slot-list-light-scoped-root": SlotListLightScopedRoot;
20072031
"slot-map-order": SlotMapOrder;
20082032
"slot-map-order-root": SlotMapOrderRoot;
2033+
"slot-nested-name-change": SlotNestedNameChange;
2034+
"slot-nested-name-change-child": SlotNestedNameChangeChild;
20092035
"slot-nested-order-child": SlotNestedOrderChild;
20102036
"slot-nested-order-parent": SlotNestedOrderParent;
20112037
"slot-ng-if": SlotNgIf;
@@ -2158,6 +2184,8 @@ declare module "@stencil/core" {
21582184
"slot-list-light-scoped-root": LocalJSX.SlotListLightScopedRoot & JSXBase.HTMLAttributes<HTMLSlotListLightScopedRootElement>;
21592185
"slot-map-order": LocalJSX.SlotMapOrder & JSXBase.HTMLAttributes<HTMLSlotMapOrderElement>;
21602186
"slot-map-order-root": LocalJSX.SlotMapOrderRoot & JSXBase.HTMLAttributes<HTMLSlotMapOrderRootElement>;
2187+
"slot-nested-name-change": LocalJSX.SlotNestedNameChange & JSXBase.HTMLAttributes<HTMLSlotNestedNameChangeElement>;
2188+
"slot-nested-name-change-child": LocalJSX.SlotNestedNameChangeChild & JSXBase.HTMLAttributes<HTMLSlotNestedNameChangeChildElement>;
21612189
"slot-nested-order-child": LocalJSX.SlotNestedOrderChild & JSXBase.HTMLAttributes<HTMLSlotNestedOrderChildElement>;
21622190
"slot-nested-order-parent": LocalJSX.SlotNestedOrderParent & JSXBase.HTMLAttributes<HTMLSlotNestedOrderParentElement>;
21632191
"slot-ng-if": LocalJSX.SlotNgIf & JSXBase.HTMLAttributes<HTMLSlotNgIfElement>;

test/karma/test-app/slot-html/karma.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ describe('slot-html', () => {
2424
expect(results[1].textContent.trim()).toBe('default slot element 2');
2525
expect(results[2].textContent.trim()).toBe('default slot element 3');
2626

27-
results = app.querySelectorAll('.results4 div article span content-start[slot="start"]');
27+
results = app.querySelectorAll('.results4 div article span content-start');
2828
expect(results[0].textContent.trim()).toBe('start slot 1');
2929
expect(results[1].textContent.trim()).toBe('start slot 2');
3030

3131
result = app.querySelector('.results4 div content-default');
3232
expect(result.textContent.trim()).toBe('default slot');
3333

34-
results = app.querySelectorAll('.results5 div article span content-start[slot="start"]');
34+
results = app.querySelectorAll('.results5 div article span content-start');
3535
expect(results[0].textContent.trim()).toBe('start slot 1');
3636
expect(results[1].textContent.trim()).toBe('start slot 2');
3737

@@ -54,11 +54,11 @@ describe('slot-html', () => {
5454
expect(results[0].textContent.trim()).toBe('default slot 1');
5555
expect(results[1].textContent.trim()).toBe('default slot 2');
5656

57-
results = app.querySelectorAll('.results8 div section content-end[slot="end"]');
57+
results = app.querySelectorAll('.results8 div section content-end');
5858
expect(results[0].textContent.trim()).toBe('end slot 1');
5959
expect(results[1].textContent.trim()).toBe('end slot 2');
6060

61-
results = app.querySelectorAll('.results9 div section content-end[slot="end"]');
61+
results = app.querySelectorAll('.results9 div section content-end');
6262
expect(results[0].textContent.trim()).toBe('end slot 1');
6363
expect(results[1].textContent.trim()).toBe('end slot 2');
6464

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Component, h, Host, Prop } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'slot-nested-name-change-child',
5+
})
6+
export class SlotNestedNameChangeChild {
7+
@Prop() state: boolean;
8+
9+
render() {
10+
return (
11+
<Host>
12+
<div>State: {this.state.toString()}</div>
13+
<slot name="default" />
14+
</Host>
15+
);
16+
}
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Component, h, Host } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'slot-nested-name-change',
5+
})
6+
export class SlotNestedNameChange {
7+
render() {
8+
return (
9+
<Host>
10+
<div>
11+
<slot-nested-name-change-child state={true}>
12+
<slot slot="default" />
13+
</slot-nested-name-change-child>
14+
</div>
15+
</Host>
16+
);
17+
}
18+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!doctype html>
2+
<meta charset="utf8" />
3+
<script src="./build/testapp.esm.js" type="module"></script>
4+
<script src="./build/testapp.js" nomodule></script>
5+
6+
<slot-nested-name-change>
7+
<p>Hello</p>
8+
</slot-nested-name-change>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { setupDomTests } from '../util';
2+
3+
describe('slot-nested-name-change', function () {
4+
const { setupDom, tearDownDom } = setupDomTests(document);
5+
let app: HTMLElement | undefined;
6+
let host: HTMLElement | undefined;
7+
8+
beforeEach(async () => {
9+
app = await setupDom('/slot-nested-name-change/index.html');
10+
host = app.querySelector('slot-nested-name-change');
11+
});
12+
13+
afterEach(tearDownDom);
14+
15+
it('should render', () => {
16+
expect(host).toBeDefined();
17+
});
18+
19+
it('should correctly render the slot content', () => {
20+
const childCmp = host.querySelector('slot-nested-name-change-child');
21+
22+
expect(childCmp.children.length).toBe(2);
23+
24+
const firstChild = childCmp.children[0];
25+
expect(firstChild.tagName).toBe('DIV');
26+
expect(firstChild.textContent.trim()).toBe('State: true');
27+
28+
const secondChild = childCmp.children[1];
29+
expect(secondChild.tagName).toBe('P');
30+
expect(secondChild.textContent.trim()).toBe('Hello');
31+
});
32+
});

0 commit comments

Comments
 (0)