Skip to content

Commit

Permalink
fix: should always have access to slotted elements (#939)
Browse files Browse the repository at this point in the history
* fix(engine): finding slotted elements when querying
  • Loading branch information
ekashida authored and caridy committed Jan 18, 2019
1 parent be05f37 commit b767131
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 11 deletions.
198 changes: 194 additions & 4 deletions packages/@lwc/engine/src/faux-shadow/__tests__/traverse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/
import { compileTemplate } from 'test-utils';
import { createElement, LightningElement } from '../../framework/main';
import { getShadowRoot } from "../../faux-shadow/shadow-root";
import { getRootNodeGetter } from "../traverse";

describe('#LightDom querySelectorAll()', () => {
Expand Down Expand Up @@ -129,6 +128,198 @@ describe('#LightDom querySelectorAll()', () => {
});

describe('#LightDom querySelector()', () => {
describe('nested slots', () => {
it('should find the slotted element when "slot > x-child > slot > slot > div.slotted"', () => {
let slotted;
class Child extends LightningElement {
render() {
return childHTML;
}
}
const childHTML = compileTemplate(`
<template>
<slot name="full">
<slot name="first">default first</slot>
<slot name="last">default last</slot>
</slot>
</template>
`);
class Parent extends LightningElement {
renderedCallback() {
slotted = this.querySelector('.slotted');
}
render() {
return parentHTML;
}
}
const parentHTML = compileTemplate(`
<template>
<slot></slot>
</template>
`, {
modules: {},
});
class Root extends LightningElement {
render() {
return rootHTML;
}
}
const rootHTML = compileTemplate(`
<template>
<x-parent>
<x-child>
<div slot="first" class="slotted">my first name</div>
</x-child>
</x-parent>
</template>
`, {
modules: {
'x-parent': Parent,
'x-child': Child,
},
});

const elm = createElement('x-root', { is: Root });
document.body.appendChild(elm);
expect(slotted).not.toBe(null);
});
it('should find the slotted element when "slot > slot > div.slotted"', () => {
let slotted;
class Parent extends LightningElement {
renderedCallback() {
slotted = this.querySelector('.slotted');
}
render() {
return parentHTML;
}
}
const parentHTML = compileTemplate(`
<template>
<slot></slot>
</template>
`, {
modules: {},
});
class Root extends LightningElement {
render() {
return rootHTML;
}
}
const rootHTML = compileTemplate(`
<template>
<x-parent>
<slot>
<div class="slotted"></div>
</slot>
</x-parent>
</template>
`, {
modules: {
'x-parent': Parent,
},
});

const elm = createElement('x-root', { is: Root });
document.body.appendChild(elm);
expect(slotted).not.toBe(null);
});

it('should find the slotted element when "slot > x-child > slot > div.slotted"', () => {
let slotted;
class Child extends LightningElement {
render() {
return childHTML;
}
}
const childHTML = compileTemplate(`
<template>
<slot></slot>
</template>
`);
class Parent extends LightningElement {
renderedCallback() {
slotted = this.querySelector('.slotted');
}
render() {
return parentHTML;
}
}
const parentHTML = compileTemplate(`
<template>
<slot></slot>
</template>
`, {
modules: {},
});
class Root extends LightningElement {
render() {
return rootHTML;
}
}
const rootHTML = compileTemplate(`
<template>
<x-parent>
<x-child>
<div class="slotted"></div>
</x-child>
</x-parent>
</template>
`, {
modules: {
'x-parent': Parent,
'x-child': Child,
},
});
const elm = createElement('x-root', { is: Root });
document.body.appendChild(elm);
expect(slotted).not.toBe(null);
});

it('should find the slotted element when "slot > div > slot > div.slotted"', () => {
let slotted;
class Parent extends LightningElement {
renderedCallback() {
slotted = this.querySelector('.slotted');
}
render() {
return parentHTML;
}
}
const parentHTML = compileTemplate(`
<template>
<slot></slot>
</template>
`, {
modules: {},
});
class Root extends LightningElement {
render() {
return rootHTML;
}
}
const rootHTML = compileTemplate(`
<template>
<x-parent>
<div>
<slot>
<div class="slotted"></div>
</slot>
</div>
</x-parent>
</template>
`, {
modules: {
'x-parent': Parent,
},
});

const elm = createElement('x-root', { is: Root });
document.body.appendChild(elm);

expect(slotted).not.toBe(null);
});
});

it('should allow searching for the passed element multiple levels up', () => {
class Root extends LightningElement {
render() {
Expand Down Expand Up @@ -169,7 +360,7 @@ describe('#LightDom querySelector()', () => {
});
const childHTML = compileTemplate(`
<template>
<div onclick={handleClick}>
<div>
<slot></slot>
</div>
</template>
Expand All @@ -178,7 +369,7 @@ describe('#LightDom querySelector()', () => {
});
const elm = createElement('x-root', { is: Root });
document.body.appendChild(elm);
const div = getShadowRoot(elm).querySelector('div');
const div = elm.shadowRoot.querySelector('div');
expect(div).toBe(target);
});
it('should allow searching for the passed element', () => {
Expand Down Expand Up @@ -841,7 +1032,6 @@ describe('#childNodes', () => {
});
});


describe('assignedSlot', () => {
it('should return null when custom element is not in slot', () => {
class NoSlot extends LightningElement {}
Expand Down
56 changes: 49 additions & 7 deletions packages/@lwc/engine/src/faux-shadow/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,31 +70,73 @@ export function isNodeOwnedBy(owner: HTMLElement, node: Node): boolean {
return isUndefined(ownerKey) || getNodeKey(owner) === ownerKey;
}

export function isNodeSlotted(host: Element, node: Node): boolean {
// when finding a slot in the DOM, we can fold it if it is contained
// inside another slot.
function foldSlotElement(slot: HTMLElement) {
let parent = parentElementGetter.call(slot);
while (!isNull(parent) && isSlotElement(parent)) {
slot = parent as HTMLElement;
parent = parentElementGetter.call(slot);
}
return slot;
}

function isNodeSlotted(host: Element, node: Node): boolean {
if (process.env.NODE_ENV !== 'production') {
assert.invariant(host instanceof HTMLElement, `isNodeSlotted() should be called with a host as the first argument instead of ${host}`);
assert.invariant(node instanceof Node, `isNodeSlotted() should be called with a node as the second argument instead of ${node}`);
assert.isTrue(compareDocumentPosition.call(node, host) & DOCUMENT_POSITION_CONTAINS, `isNodeSlotted() should never be called with a node that is not a child node of ${host}`);
}
const hostKey = getNodeKey(host);
// this routine assumes that the node is coming from a different shadow (it is not owned by the host)
// just in case the provided node is not an element
let currentElement = node instanceof Element ? node : parentElementGetter.call(node);
while (!isNull(currentElement) && currentElement !== host) {
const elmOwnerKey = getNodeNearestOwnerKey(currentElement);
const parent = parentElementGetter.call(currentElement);
if (elmOwnerKey === hostKey) {
// we have reached a host's node element, and only if
// we have reached an element inside the host's template, and only if
// that element is an slot, then the node is considered slotted
// TODO: add the examples
return isSlotElement(currentElement);
} else if (!isNull(parent) && parent !== host && getNodeNearestOwnerKey(parent) !== elmOwnerKey) {
} else if (parent === host) {
return false;
} else if (!isNull(parent) && getNodeNearestOwnerKey(parent) !== elmOwnerKey) {
// we are crossing a boundary of some sort since the elm and its parent
// have different owner key. for slotted elements, this is only possible
// if the parent happens to be a slot that is not owned by the host
if (!isSlotElement(parent)) {
// have different owner key. for slotted elements, this is possible
// if the parent happens to be a slot.
if (isSlotElement(parent)) {
/**
* the slot parent might be allocated inside another slot, think of:
* <x-root> (<--- root element)
* <x-parent> (<--- own by x-root)
* <x-child> (<--- own by x-root)
* <slot> (<--- own by x-child)
* <slot> (<--- own by x-parent)
* <div> (<--- own by x-root)
*
* while checking if x-parent has the div slotted, we need to traverse
* up, but when finding the first slot, we skip that one in favor of the
* most outer slot parent before jumping into its corresponding host.
*/
currentElement = getNodeOwner(foldSlotElement(parent as HTMLElement));
if (!isNull(currentElement)) {
if (currentElement === host) {
// the slot element is a top level element inside the shadow
// of a host that was allocated into host in question
return true;
} else if (getNodeNearestOwnerKey(currentElement) === hostKey) {
// the slot element is an element inside the shadow
// of a host that was allocated into host in question
return true;
}
}
} else {
return false;
}
} else {
currentElement = parent;
}
currentElement = parent;
}
return false;
}
Expand Down

0 comments on commit b767131

Please sign in to comment.