Skip to content

Commit

Permalink
fix(synthetic-shadow): fix light DOM serialization (#2502)
Browse files Browse the repository at this point in the history
* fix(synthetic-shadow): fix light DOM serialization

Fixes #2425

* fix: fix deep shadows within light

* fix: get it working in IE11

* fix: fix code comment

* fix: fix code comment

* fix: extract into shared function

* fix: use treewalker

* test: add some more sanity tests for light DOM depth
  • Loading branch information
nolanlawson committed Oct 8, 2021
1 parent 13661ae commit 9560942
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 25 deletions.
9 changes: 7 additions & 2 deletions packages/@lwc/synthetic-shadow/src/faux-shadow/element.ts
Expand Up @@ -50,6 +50,7 @@ import {
getNodeKey,
getNodeNearestOwnerKey,
getNodeOwnerKey,
isNodeOrDescendantsShadowed,
isNodeShadowed,
} from '../shared/node-ownership';
import { arrayFromCollection, isGlobalPatchingSkipped } from '../shared/utils';
Expand Down Expand Up @@ -119,7 +120,10 @@ 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 descendants are synthetic shadow hosts, then we can't
// use the native innerHTML because it would expose private node internals.
if (isNodeOrDescendantsShadowed(this)) {
return innerHTMLGetterPatched.call(this);
}

Expand All @@ -141,7 +145,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 (isNodeOrDescendantsShadowed(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 @@ -45,7 +45,12 @@ import {
} from './traverse';
import { getTextContent } from '../3rdparty/polymer/text-content';
import { getShadowRoot, isHostElement, getIE11FakeShadowRootPlaceholder } from './shadow-root';
import { getNodeNearestOwnerKey, getNodeOwnerKey, isNodeShadowed } from '../shared/node-ownership';
import {
getNodeNearestOwnerKey,
getNodeOwnerKey,
isNodeOrDescendantsShadowed,
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 (isNodeOrDescendantsShadowed(this)) {
return textContentGetterPatched.call(this);
}

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

// Return true if any descendant is a host element
export function containsHost(node: Node) {
// IE requires all arguments
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker#browser_compatibility
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, null, false);
let descendant;
while (!isNull((descendant = walker.nextNode()))) {
if (isHostElement(descendant)) {
return true;
}
}
return false;
}

let uid = 0;

export function attachShadow(elm: Element, options: ShadowRootInit): SyntheticShadowRootInterface {
Expand Down
11 changes: 10 additions & 1 deletion packages/@lwc/synthetic-shadow/src/shared/node-ownership.ts
Expand Up @@ -5,7 +5,8 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { defineProperty, isNull, isUndefined } from '@lwc/shared';
import { Node, parentNodeGetter } from '../env/node';
import { parentNodeGetter } from '../env/node';
import { containsHost, isHostElement } from '../faux-shadow/shadow-root';

// Used as a back reference to identify the host element
const HostElementKey = '$$HostElementKey$$';
Expand Down Expand Up @@ -77,3 +78,11 @@ export function isNodeShadowed(node: Node): boolean {
export function isNodeDeepShadowed(node: Node): boolean {
return !isUndefined(getNodeNearestOwnerKey(node));
}

/**
* Returns true if this node is a shadow host, is in a shadow host, or contains a shadow host
* anywhere in its tree.
*/
export function isNodeOrDescendantsShadowed(node: Node): boolean {
return isNodeShadowed(node) || isHostElement(node) || containsHost(node);
}
Expand Up @@ -55,10 +55,9 @@ describe('Slotting', () => {
it('shadow container, light consumer', () => {
const nodes = createTestElement('x-light-consumer', LightConsumer);

const expected = process.env.NATIVE_SHADOW // 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 @@ -3,6 +3,8 @@ import { extractDataIds } from 'test-utils';

import LightContainer from 'x/lightContainer';
import ShadowContainer from 'x/shadowContainer';
import LightContainerDeepShadow from 'x/lightContainerDeepShadow';
import LightContainerDeeperShadow from 'x/lightContainerDeeperShadow';

describe('Light DOM + Synthetic Shadow DOM', () => {
describe('light -> shadow', () => {
Expand Down Expand Up @@ -30,7 +32,32 @@ describe('Light DOM + Synthetic Shadow DOM', () => {

it('childNodes', () => {
expect(Array.from(nodes.slot.childNodes)).toEqual([]);
});
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([]);
});
if (!process.env.COMPAT) {
it('childNodes - text nodes', () => {
// TreeWalker is just a convenient way of getting text nodes without using childNodes
// Sadly it throws errors in IE11 (even when adding arguments to `createTreeWalker()`)
const textNodes = {};
const walker = document.createTreeWalker(elm, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
textNodes[node.wholeText] = node;
}
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.'],
]);
});
}
it('parentNode', () => {
expect(nodes.p.parentNode).toEqual(nodes.consumer);
expect(nodes.consumer.parentNode).toEqual(elm);
Expand All @@ -44,16 +71,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 @@ -62,8 +87,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 All @@ -74,6 +98,87 @@ describe('Light DOM + Synthetic Shadow DOM', () => {
});
});

describe('light -> deep shadow', () => {
let elm, nodes;
beforeEach(() => {
elm = createElement('x-light-container-deep-shadow', {
is: LightContainerDeepShadow,
});
document.body.appendChild(elm);
nodes = extractDataIds(elm);
});
it('childNodes', () => {
expect(Array.from(elm.childNodes)).toEqual([nodes.wrapper]);
expect(Array.from(nodes.wrapper.childNodes)).toEqual([nodes.consumer]);
});
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.');
});
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.'
);
expect(elm.innerHTML).toEqual(
'<div data-id="wrapper"><x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer></div>'
);
});
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>'
);
expect(elm.outerHTML).toEqual(
'<x-light-container-deep-shadow><div data-id="wrapper"><x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer></div></x-light-container-deep-shadow>'
);
});
});

describe('light -> deeper shadow', () => {
let elm, nodes;
beforeEach(() => {
elm = createElement('x-light-container-deeper-shadow', {
is: LightContainerDeeperShadow,
});
document.body.appendChild(elm);
nodes = extractDataIds(elm);
});
it('childNodes', () => {
expect(Array.from(elm.childNodes)).toEqual([nodes.wrapper]);
expect(Array.from(nodes.wrapper.childNodes)).toEqual([nodes.innerWrapper]);
expect(Array.from(nodes.innerWrapper.childNodes)).toEqual([nodes.consumer]);
});
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.');
});
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.'
);
expect(elm.innerHTML).toEqual(
'<div data-id="wrapper"><div data-id="innerWrapper"><x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer></div></div>'
);
});
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>'
);
expect(elm.outerHTML).toEqual(
'<x-light-container-deeper-shadow><div data-id="wrapper"><div data-id="innerWrapper"><x-consumer data-id="consumer"><p data-id="p">I am an assigned element.</p>I am an assigned text.</x-consumer></div></div></x-light-container-deeper-shadow>'
);
});
});

describe('shadow -> light -> shadow', () => {
let elm, nodes;
beforeEach(() => {
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>
@@ -0,0 +1,8 @@
<template lwc:render-mode="light">
<div data-id="wrapper">
<x-consumer data-id="consumer">
<p data-id="p">I am an assigned element.</p>
I am an assigned text.
</x-consumer>
</div>
</template>
@@ -0,0 +1,5 @@
import { LightningElement } from 'lwc';

export default class LightContainerDeepShadow extends LightningElement {
static renderMode = 'light';
}
@@ -0,0 +1,10 @@
<template lwc:render-mode="light">
<div data-id="wrapper">
<div data-id="innerWrapper">
<x-consumer data-id="consumer">
<p data-id="p">I am an assigned element.</p>
I am an assigned text.
</x-consumer>
</div>
</div>
</template>
@@ -0,0 +1,5 @@
import { LightningElement } from 'lwc';

export default class LightContainerDeeperShadow extends LightningElement {
static renderMode = 'light';
}
Expand Up @@ -67,11 +67,11 @@ if (!process.env.NATIVE_SHADOW) {

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.NATIVE_SHADOW) {
});

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.NATIVE_SHADOW) {
});

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 9560942

Please sign in to comment.