From b1d8c84cf752bf235e880076a39192902f952365 Mon Sep 17 00:00:00 2001 From: Eugene Kashida Date: Sat, 19 Jan 2019 05:43:43 +0900 Subject: [PATCH] fix: should always have access to slotted elements (#939) * fix(engine): finding slotted elements when querying --- .../faux-shadow/__tests__/traverse.spec.ts | 198 +++++++++++++++++- .../@lwc/engine/src/faux-shadow/traverse.ts | 56 ++++- 2 files changed, 243 insertions(+), 11 deletions(-) diff --git a/packages/@lwc/engine/src/faux-shadow/__tests__/traverse.spec.ts b/packages/@lwc/engine/src/faux-shadow/__tests__/traverse.spec.ts index cf72e4db38..87ec6453d3 100644 --- a/packages/@lwc/engine/src/faux-shadow/__tests__/traverse.spec.ts +++ b/packages/@lwc/engine/src/faux-shadow/__tests__/traverse.spec.ts @@ -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()', () => { @@ -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(` + + `); + class Parent extends LightningElement { + renderedCallback() { + slotted = this.querySelector('.slotted'); + } + render() { + return parentHTML; + } + } + const parentHTML = compileTemplate(` + + `, { + modules: {}, + }); + class Root extends LightningElement { + render() { + return rootHTML; + } + } + const rootHTML = compileTemplate(` + + `, { + 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(` + + `, { + modules: {}, + }); + class Root extends LightningElement { + render() { + return rootHTML; + } + } + const rootHTML = compileTemplate(` + + `, { + 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(` + + `); + class Parent extends LightningElement { + renderedCallback() { + slotted = this.querySelector('.slotted'); + } + render() { + return parentHTML; + } + } + const parentHTML = compileTemplate(` + + `, { + modules: {}, + }); + class Root extends LightningElement { + render() { + return rootHTML; + } + } + const rootHTML = compileTemplate(` + + `, { + 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(` + + `, { + modules: {}, + }); + class Root extends LightningElement { + render() { + return rootHTML; + } + } + const rootHTML = compileTemplate(` + + `, { + 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() { @@ -169,7 +360,7 @@ describe('#LightDom querySelector()', () => { }); const childHTML = compileTemplate(` @@ -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', () => { @@ -924,7 +1115,6 @@ describe('#childNodes', () => { }); }); - describe('assignedSlot', () => { it('should return null when custom element is not in slot', () => { class NoSlot extends LightningElement {} diff --git a/packages/@lwc/engine/src/faux-shadow/traverse.ts b/packages/@lwc/engine/src/faux-shadow/traverse.ts index f574b3f997..3879115b42 100644 --- a/packages/@lwc/engine/src/faux-shadow/traverse.ts +++ b/packages/@lwc/engine/src/faux-shadow/traverse.ts @@ -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: + * (<--- root element) + * (<--- own by x-root) + * (<--- own by x-root) + * (<--- own by x-child) + * (<--- own by x-parent) + *
(<--- 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; }