Skip to content

Commit

Permalink
fix(synthetic-shadow): elementsFromPoint returns invisible hosts (#2534)
Browse files Browse the repository at this point in the history
* fix(synthetic-shadow): elementsFromPoint returns invisible hosts

Fixes #2495

W-9904840

* fix: only consider immediate shadow root

* fix: optimize impl a bit

* fix: fix missing array methods

* test: make test more consistent across browsers

* test: tidy up test

* test: disable test for ie11

* fix: extract function to outer scope
  • Loading branch information
nolanlawson committed Jan 14, 2022
1 parent 12374ed commit e2e0dff
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { isNull, isUndefined } from '@lwc/shared';
import { ArrayIndexOf, ArrayPush, isNull, isUndefined } from '@lwc/shared';
import { elementsFromPoint } from '../env/document';
import { isSyntheticSlotElement } from '../faux-shadow/traverse';

Expand All @@ -19,14 +19,26 @@ function getAllRootNodes(node: Node) {
return rootNodes;
}

// Keep searching up the host tree until we find an element that is within the immediate shadow root
const findAncestorHostInImmediateShadowRoot = (rootNode: Node, targetRootNode: Node) => {
let host;
while (!isUndefined((host = (rootNode as any).host))) {
const thisRootNode = host.getRootNode();
if (thisRootNode === targetRootNode) {
return host;
}
rootNode = thisRootNode;
}
};

export function fauxElementsFromPoint(
context: Node,
doc: Document,
left: number,
top: number
): Element[] {
const elements: Element[] | null = elementsFromPoint.call(doc, left, top);
const result = [];
const result: Element[] = [];

const rootNodes = getAllRootNodes(context);

Expand All @@ -37,11 +49,32 @@ export function fauxElementsFromPoint(
// can be null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/elementsFromPoint#browser_compatibility
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (isSyntheticSlotElement(element)) {
continue;
}
const elementRootNode = element.getRootNode();

if (ArrayIndexOf.call(rootNodes, elementRootNode) !== -1) {
ArrayPush.call(result, element);
continue;
}
// In cases where the host element is not visible but its shadow descendants are, then
// we may get the shadow descendant instead of the host element here. (The
// browser doesn't know the difference in synthetic shadow DOM.)
// In native shadow DOM, however, elementsFromPoint would return the host but not
// the child. So we need to detect if this shadow element's host is accessible from
// the context's shadow root. Note we also need to be careful not to add the host
// multiple times.
const ancestorHost = findAncestorHostInImmediateShadowRoot(
elementRootNode,
rootNodes[0]
);
if (
rootNodes.indexOf(element.getRootNode()) !== -1 &&
!isSyntheticSlotElement(element)
!isUndefined(ancestorHost) &&
ArrayIndexOf.call(elements, ancestorHost) === -1 &&
ArrayIndexOf.call(result, ancestorHost) === -1
) {
result.push(element);
ArrayPush.call(result, ancestorHost);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createElement } from 'lwc';
import Container from 'x/container';
import Grandparent from 'x/grandparent';
import { extractShadowDataIds } from 'test-utils';

describe('elementsFromPoint', () => {
Expand Down Expand Up @@ -44,14 +45,11 @@ describe('elementsFromPoint', () => {
};
})();

function test(element, expectedElements) {
const { left, top, width, height } = element.getBoundingClientRect();
const rootNode = element.getRootNode();
const elementsFromPoint = rootNode.elementsFromPoint(left + width / 2, top + height / 2);
function testElementsFromPoint(rootNode, x, y, expectedElements) {
const elementsFromPoint = rootNode.elementsFromPoint(x, y);

if (onlyIncludesElementsInImmediateShadowRoot()) {
const mainRootNode = element.getRootNode();
expectedElements = expectedElements.filter((el) => el.getRootNode() === mainRootNode);
expectedElements = expectedElements.filter((el) => el.getRootNode() === rootNode);
}

expect(elementsFromPoint).toEqual(expectedElements);
Expand Down Expand Up @@ -92,50 +90,116 @@ describe('elementsFromPoint', () => {
inSlottableInner,
} = nodes;

test(elm, [elm, body, html]);
test(aboveContainer, [aboveContainer, elm, body, html]);
test(inContainer, [slottable, inContainer, elm, body, html]);
test(slottable, [slottable, inContainer, elm, body, html]);
test(aroundSlotted, [slotted, aroundSlotted, slottable, inContainer, elm, body, html]);
test(slotted, [slotted, aroundSlotted, slottable, inContainer, elm, body, html]);
test(inSlotted, [
inSlotted,
slotted,
aroundSlotted,
slottable,
inContainer,
elm,
body,
html,
]);
test(slotWrapper, [
slotted,
aroundSlotted,
slotWrapper,
slottable,
inContainer,
elm,
body,
html,
]);
test(inSlottable, [
inSlottableInner,
inSlottable,
slottable,
inContainer,
elm,
body,
html,
]);
test(inSlottableInner, [
inSlottableInner,
inSlottable,
slottable,
inContainer,
elm,
body,
html,
]);
function test(element, expectedElements) {
const { left, top, width, height } = element.getBoundingClientRect();
const rootNode = element.getRootNode();
const x = left + width / 2;
const y = top + height / 2;
testElementsFromPoint(rootNode, x, y, [...expectedElements, elm, body, html]);
}

test(elm, []);
test(aboveContainer, [aboveContainer]);
test(inContainer, [slottable, inContainer]);
test(slottable, [slottable, inContainer]);
test(aroundSlotted, [slotted, aroundSlotted, slottable, inContainer]);
test(slotted, [slotted, aroundSlotted, slottable, inContainer]);
test(inSlotted, [inSlotted, slotted, aroundSlotted, slottable, inContainer]);
test(slotWrapper, [slotted, aroundSlotted, slotWrapper, slottable, inContainer]);
test(inSlottable, [inSlottableInner, inSlottable, slottable, inContainer]);
test(inSlottableInner, [inSlottableInner, inSlottable, slottable, inContainer]);
});

// IE11 sometimes includes <html> in the elements array, sometimes not. It's not worth testing
if (!process.env.COMPAT) {
it('host elements are not all visible', () => {
const grandparent = createElement('x-grandparent', { is: Grandparent });
document.body.appendChild(grandparent);
const nodes = extractShadowDataIds(grandparent.shadowRoot);
const { child, childDiv, parent, parentDiv, grandparentDiv } = nodes;
const html = document.documentElement;

const resetStyles = () => {
[child, childDiv, parent, parentDiv, grandparent, grandparentDiv].forEach(
(el) => {
el.style = '';
}
);
};

function test(element, expectedElements) {
testElementsFromPoint(element.getRootNode(), 50, 50, [
...expectedElements,
html,
]);
}

test(childDiv, [childDiv, child, parentDiv, parent, grandparentDiv, grandparent]);
test(parentDiv, [child, parentDiv, parent, grandparentDiv, grandparent]);
test(grandparentDiv, [parent, grandparentDiv, grandparent]);

grandparent.style = 'width: 0px; height: 0px;';

test(childDiv, [childDiv, child, parentDiv, parent, grandparentDiv]);
test(parentDiv, [child, parentDiv, parent, grandparentDiv]);
test(grandparentDiv, [parent, grandparentDiv]);

resetStyles();
parent.style = 'width: 0px; height: 0px;';

test(childDiv, [childDiv, child, parentDiv, grandparentDiv, grandparent]);
test(parentDiv, [child, parentDiv, grandparentDiv, grandparent]);
test(grandparentDiv, [parent, grandparentDiv, grandparent]);

resetStyles();
child.style = 'width: 0px; height: 0px;';

test(childDiv, [childDiv, parentDiv, parent, grandparentDiv, grandparent]);
test(parentDiv, [child, parentDiv, parent, grandparentDiv, grandparent]);
test(grandparentDiv, [parent, grandparentDiv, grandparent]);

resetStyles();
parent.style = 'width: 0px; height: 0px;';
parentDiv.style = 'width: 0px; height: 0px;';

test(childDiv, [childDiv, child, grandparentDiv, grandparent]);
test(parentDiv, [child, grandparentDiv, grandparent]);
test(grandparentDiv, [parent, grandparentDiv, grandparent]);

resetStyles();
parent.style = 'width: 0px; height: 0px;';
parentDiv.style = 'width: 0px; height: 0px;';
child.style = 'width: 0px; height: 0px;';

test(childDiv, [childDiv, grandparentDiv, grandparent]);
test(parentDiv, [child, grandparentDiv, grandparent]);
test(grandparentDiv, [parent, grandparentDiv, grandparent]);

resetStyles();
parent.style = 'width: 0px; height: 0px;';
child.style = 'width: 0px; height: 0px;';
test(childDiv, [childDiv, parentDiv, grandparentDiv, grandparent]);
test(parentDiv, [child, parentDiv, grandparentDiv, grandparent]);
test(grandparentDiv, [parent, grandparentDiv, grandparent]);

resetStyles();
parent.style = 'width: 0px; height: 0px;';
parentDiv.style = 'width: 0px; height: 0px;';
child.style = 'width: 0px; height: 0px;';
childDiv.style = 'width: 0px; height: 0px;';

test(childDiv, [grandparentDiv, grandparent]);
test(parentDiv, [grandparentDiv, grandparent]);
test(grandparentDiv, [grandparentDiv, grandparent]);

resetStyles();
parentDiv.style = 'width: 0px; height: 0px;';
child.style = 'width: 0px; height: 0px;';

test(childDiv, [childDiv, parent, grandparentDiv, grandparent]);
test(parentDiv, [child, parent, grandparentDiv, grandparent]);
test(grandparentDiv, [parent, grandparentDiv, grandparent]);
});
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:host, div {
position: absolute;
width: 100px;
height: 100px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div data-id="childDiv"></div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { LightningElement } from 'lwc';

export default class extends LightningElement {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:host, div {
position: absolute;
width: 100px;
height: 100px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div data-id="grandparentDiv">
<x-parent data-id="parent"></x-parent>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { LightningElement } from 'lwc';

export default class extends LightningElement {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:host, div {
position: absolute;
width: 100px;
height: 100px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div data-id="parentDiv">
<x-child data-id="child"></x-child>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { LightningElement } from 'lwc';

export default class extends LightningElement {}

0 comments on commit e2e0dff

Please sign in to comment.