Skip to content

Commit

Permalink
fix(synthetic-shadow): fix light DOM serialization
Browse files Browse the repository at this point in the history
Fixes #2425
  • Loading branch information
nolanlawson committed Sep 21, 2021
1 parent 29e9766 commit 66061bc
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 24 deletions.
12 changes: 9 additions & 3 deletions packages/@lwc/synthetic-shadow/src/faux-shadow/element.ts
Expand Up @@ -17,7 +17,7 @@ import {
KEY__SYNTHETIC_MODE,
} from '@lwc/shared';
import featureFlags from '@lwc/features';
import { attachShadow, getShadowRoot, isHostElement } from './shadow-root';
import { attachShadow, getShadowRoot, hasHostChild, isHostElement } from './shadow-root';
import {
getNodeOwner,
getAllMatches,
Expand Down Expand Up @@ -119,7 +119,12 @@ defineProperties(Element.prototype, {
innerHTML: {
get(this: Element): string {
if (!featureFlags.ENABLE_ELEMENT_PATCH) {
if (isNodeShadowed(this) || isHostElement(this)) {
// If this element is in synthetic shadow, if it's a synthetic shadow host,
// or if any of its immediate children are synthetic shadow hosts, then we can't
// use the native innerHTML because it would expose private node internals.
// (Note that innerHTMLGetterPatched calls innerHTML on all children, so we only need to check
// one level deep.)
if (isNodeShadowed(this) || isHostElement(this) || hasHostChild(this)) {
return innerHTMLGetterPatched.call(this);
}

Expand All @@ -141,7 +146,8 @@ defineProperties(Element.prototype, {
outerHTML: {
get(this: Element): string {
if (!featureFlags.ENABLE_ELEMENT_PATCH) {
if (isNodeShadowed(this) || isHostElement(this)) {
// See notes above on get innerHTML
if (isNodeShadowed(this) || isHostElement(this) || hasHostChild(this)) {
return outerHTMLGetterPatched.call(this);
}
return outerHTMLGetter.call(this);
Expand Down
14 changes: 11 additions & 3 deletions packages/@lwc/synthetic-shadow/src/faux-shadow/node.ts
Expand Up @@ -44,7 +44,12 @@ import {
isSyntheticSlotElement,
} from './traverse';
import { getTextContent } from '../3rdparty/polymer/text-content';
import { getShadowRoot, isHostElement, getIE11FakeShadowRootPlaceholder } from './shadow-root';
import {
getShadowRoot,
isHostElement,
getIE11FakeShadowRootPlaceholder,
hasHostChild,
} from './shadow-root';
import { getNodeNearestOwnerKey, getNodeOwnerKey, isNodeShadowed } from '../shared/node-ownership';
import { createStaticNodeList } from '../shared/static-node-list';
import { isGlobalPatchingSkipped } from '../shared/utils';
Expand Down Expand Up @@ -170,7 +175,9 @@ function cloneNodePatched(this: Node, deep?: boolean): Node {
function childNodesGetterPatched(this: Node): NodeListOf<Node> {
if (isHostElement(this)) {
const owner = getNodeOwner(this);
const childNodes = isNull(owner) ? [] : getAllMatches(owner, getFilteredChildNodes(this));
const childNodes = isNull(owner)
? getFilteredChildNodes(this)
: getAllMatches(owner, getFilteredChildNodes(this));
if (
process.env.NODE_ENV !== 'production' &&
isFalse(hasNativeSymbolSupport) &&
Expand Down Expand Up @@ -279,7 +286,8 @@ defineProperties(Node.prototype, {
textContent: {
get(this: Node): string {
if (!featureFlags.ENABLE_NODE_PATCH) {
if (isNodeShadowed(this) || isHostElement(this)) {
// See note on get innerHTML in faux-shadow/element.ts
if (isNodeShadowed(this) || isHostElement(this) || hasHostChild(this)) {
return textContentGetterPatched.call(this);
}

Expand Down
11 changes: 11 additions & 0 deletions packages/@lwc/synthetic-shadow/src/faux-shadow/shadow-root.ts
Expand Up @@ -127,6 +127,17 @@ export function isHostElement(node: unknown): node is HTMLElement {
return !isUndefined(InternalSlot.get(node));
}

// Return true if any immediate child is a host
export function hasHostChild(node: Node) {
const { childNodes } = node;
for (let i = 0, length = childNodes.length; i < length; i++) {
if (isHostElement(childNodes[i])) {
return true;
}
}
return false;
}

let uid = 0;

export function attachShadow(elm: Element, options: ShadowRootInit): SyntheticShadowRootInterface {
Expand Down
Expand Up @@ -55,10 +55,9 @@ describe('Slotting', () => {
it('shadow container, light consumer', () => {
const nodes = createTestElement('x-light-consumer', LightConsumer);

const expected = process.env.DISABLE_SYNTHETIC // native shadow doesn't output slots in innerHTML
? '<x-shadow-container><p data-id="light-consumer-text">Hello from Light DOM</p></x-shadow-container>'
: '<x-shadow-container><slot><p data-id="light-consumer-text">Hello from Light DOM</p></slot></x-shadow-container>';
expect(nodes['x-light-consumer'].innerHTML).toEqual(expected);
expect(nodes['x-light-consumer'].innerHTML).toEqual(
'<x-shadow-container><p data-id="light-consumer-text">Hello from Light DOM</p></x-shadow-container>'
);
});

it('light container, shadow consumer', () => {
Expand Down
Expand Up @@ -25,7 +25,27 @@ describe('Light DOM + Synthetic Shadow DOM', () => {
expect(nodes.p.assignedSlot).toEqual(nodes.slot);
});
it('childNodes', () => {
// TreeWalker is just a convenient way of getting text nodes without using childNodes
const textNodes = {};
const walker = document.createTreeWalker(elm, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
textNodes[node.wholeText] = node;
}
expect(Array.from(nodes.slot.childNodes)).toEqual([]);
expect(Array.from(nodes.p.childNodes)).toEqual([
textNodes['I am an assigned element.'],
]);
expect(Array.from(nodes.consumer.childNodes)).toEqual([
nodes.p,
textNodes['I am an assigned text.'],
]);
expect(Array.from(elm.childNodes)).toEqual([nodes.consumer]);
expect(Array.from(nodes['consumer.shadowRoot'].childNodes)).toEqual([
nodes.pInShadow,
nodes.slot,
]);
expect(Array.from(nodes.slot)).toEqual([]);
});
it('parentNode', () => {
expect(nodes.p.parentNode).toEqual(nodes.consumer);
Expand All @@ -40,16 +60,14 @@ describe('Light DOM + Synthetic Shadow DOM', () => {
expect(nodes.p.getRootNode()).toEqual(document);
expect(nodes.consumer.getRootNode()).toEqual(document);
});
// TODO [#2425]: Incorrect serialization
xit('textContent', () => {
it('textContent', () => {
expect(nodes.p.textContent).toEqual('I am an assigned element.');
expect(nodes.consumer.textContent).toEqual(
'I am an assigned element.I am an assigned text.'
);
expect(elm.textContent).toEqual('I am an assigned element.I am an assigned text.');
});
// TODO [#2425]: Incorrect serialization
xit('innerHTML', () => {
it('innerHTML', () => {
expect(nodes.p.innerHTML).toEqual('I am an assigned element.');
expect(nodes.consumer.innerHTML).toEqual(
'<p data-id="p">I am an assigned element.</p>I am an assigned text.'
Expand All @@ -58,8 +76,7 @@ describe('Light DOM + Synthetic Shadow DOM', () => {
'<x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer>'
);
});
// TODO [#2425]: Incorrect serialization
xit('outerHTML', () => {
it('outerHTML', () => {
expect(nodes.p.outerHTML).toEqual('<p data-id="p">I am an assigned element.</p>');
expect(nodes.consumer.outerHTML).toEqual(
'<x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer>'
Expand Down
@@ -1,4 +1,4 @@
<template>
<p>I am in the shadow</p>
<p data-id="pInShadow">I am in the shadow</p>
<slot data-id="slot"></slot>
</template>
</template>
Expand Up @@ -67,11 +67,11 @@ if (!process.env.DISABLE_SYNTHETIC) {

describe('Element.prototype API', () => {
it('should keep behavior for innerHTML', () => {
expect(elementOutsideLWC.innerHTML.length).toBe(455);
expect(elementOutsideLWC.innerHTML.length).toBe(27);
expect(rootLwcElement.innerHTML.length).toBe(0);
expect(lwcElementInsideShadow.innerHTML.length).toBe(0);

expect(divManuallyApendedToShadow.innerHTML.length).toBe(176); // <x-manually-inserted><p>slot-container text</p><x-with-slot><p>with
expect(divManuallyApendedToShadow.innerHTML.length).toBe(43); // <x-manually-inserted><p>slot-container text</p><x-with-slot><p>with

expect(cmpShadow.innerHTML.length).toBe(99);

Expand All @@ -80,11 +80,11 @@ if (!process.env.DISABLE_SYNTHETIC) {
});

it('should keep behavior for outerHTML', () => {
expect(elementOutsideLWC.outerHTML.length).toBe(466);
expect(elementOutsideLWC.outerHTML.length).toBe(38);
expect(rootLwcElement.outerHTML.length).toBe(27);
expect(lwcElementInsideShadow.outerHTML.length).toBe(27);

expect(divManuallyApendedToShadow.outerHTML.length).toBe(206); // <div class="manual-ctx"><x-manually-inserted><p>slot-container text</p><x-with-slot><p>wi ....
expect(divManuallyApendedToShadow.outerHTML.length).toBe(73); // <div class="manual-ctx"><x-manually-inserted><p>slot-container text</p><x-with-slot><p>wi ....

expect(cmpShadow.outerHTML).toBe(undefined);

Expand Down Expand Up @@ -257,12 +257,12 @@ if (!process.env.DISABLE_SYNTHETIC) {
});

it('should preserve behaviour for textContent', () => {
expect(elementOutsideLWC.textContent.length).toBe(117);
expect(elementOutsideLWC.textContent.length).toBe(0);
expect(rootLwcElement.textContent.length).toBe(0);
expect(lwcElementInsideShadow.textContent.length).toBe(0);

expect(elementInShadow.textContent.length).toBe(0);
expect(divManuallyApendedToShadow.textContent.length).toBe(45);
expect(divManuallyApendedToShadow.textContent.length).toBe(0);

expect(cmpShadow.textContent.length).toBe(31);

Expand Down

0 comments on commit 66061bc

Please sign in to comment.