LibWeb: Exclude UA internal shadow root elements in elementFromPoint()

When `elementFromPoint()` or `elementsFromPoint()` returns an element
that is inside a UA internal shadow root, we now return the shadow host
for that element.
This commit is contained in:
Tim Ledbetter 2026-02-02 11:35:38 +00:00 committed by Sam Atkins
parent 5f434a442a
commit c44b30f0f1
12 changed files with 338 additions and 19 deletions

View file

@ -5755,6 +5755,17 @@ WebIDL::ExceptionOr<void> Document::set_design_mode(String const& design_mode)
return {};
}
static Element* retarget_from_ua_internal_shadow_root(Element& element)
{
auto* result = &element;
while (auto shadow_root = result->containing_shadow_root()) {
if (!shadow_root->is_user_agent_internal())
break;
result = shadow_root->host();
}
return result;
}
// https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint
Element const* Document::element_from_point(double x, double y)
{
@ -5772,19 +5783,20 @@ Element const* Document::element_from_point(double x, double y)
// 2. If there is a box in the viewport that would be a target for hit testing at coordinates x,y, when applying the transforms
// that apply to the descendants of the viewport, return the associated element and terminate these steps.
Optional<Painting::HitTestResult> hit_test_result;
if (auto const* paintable_box = this->paintable_box(); paintable_box) {
GC::Ptr<Element> hit_element;
if (auto const* paintable_box = this->paintable_box()) {
(void)paintable_box->hit_test(position, Painting::HitTestType::Exact, [&](Painting::HitTestResult result) {
auto* dom_node = result.dom_node();
if (dom_node && dom_node->is_element()) {
hit_test_result = result;
if (auto* element = as_if<Element>(result.dom_node())) {
hit_element = element;
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
});
}
if (hit_test_result.has_value())
return static_cast<Element*>(hit_test_result->dom_node());
if (hit_element) {
// AD-HOC: If element is inside a UA internal shadow root, retarget to the host.
return retarget_from_ua_internal_shadow_root(*hit_element);
}
// 3. If the document has a root element, return the root element and terminate these steps.
if (auto const* root_element = document_element())
@ -5817,8 +5829,13 @@ GC::RootVector<GC::Ref<Element>> Document::elements_from_point(double x, double
// apply to the descendants of the viewport, append the associated element to sequence.
if (auto const* paintable_box = this->paintable_box()) {
(void)paintable_box->hit_test(position, Painting::HitTestType::Exact, [&](Painting::HitTestResult result) {
if (auto* element = as_if<Element>(result.dom_node()))
sequence.append(*element);
if (auto* element = as_if<Element>(result.dom_node())) {
// AD-HOC: If element is inside a UA internal shadow root, retarget to the host.
element = retarget_from_ua_internal_shadow_root(*element);
// AD-HOC: Avoid adding duplicates when multiple internal elements retarget to the same host.
if (sequence.is_empty() || sequence.last() != element)
sequence.append(*element);
}
return TraversalDecision::Continue;
});
}

View file

@ -292,3 +292,6 @@ Text/input/wpt-import/css/css-shapes/shape-outside/values/shape-outside-circle-0
Text/input/wpt-import/css/css-shapes/shape-outside/values/shape-outside-ellipse-004.html
Text/input/wpt-import/css/css-shapes/shape-outside/values/shape-outside-ellipse-005.html
Text/input/wpt-import/css/css-shapes/shape-outside/values/shape-outside-inset-003.html
; This is used internally by cssom-view elementsFromPoint tests.
Text/input/wpt-import/css/cssom-view/iframe.html

View file

@ -0,0 +1,7 @@
Harness status: OK
Found 2 tests
2 Pass
Pass elementsFromPoint on the root document for points in iframe elements
Pass elementsFromPoint on inner documents

View file

@ -2,11 +2,11 @@ Harness status: OK
Found 6 tests
2 Pass
4 Fail
Fail elementsFromPoint for each corner of a simple div
4 Pass
2 Fail
Pass elementsFromPoint for each corner of a simple div
Fail elementsFromPoint for each corner of a div that has a pseudo-element
Fail elementsFromPoint for each corner of a div that is between another div and its pseudo-element
Fail elementsFromPoint for each corner of a div that has a margin
Pass elementsFromPoint for each corner of a div that has a margin
Pass elementsFromPoint for each corner of a div with pointer-events:none
Pass elementsFromPoint for each corner of a div with a 3d transform

View file

@ -0,0 +1,15 @@
Harness status: OK
Found 9 tests
7 Pass
2 Fail
Pass Negative co-ordinates
Pass co-ordinates larger than the viewport
Pass co-ordinates larger than the viewport from in iframe
Pass Return first element that is the target for hit testing
Pass First element to get mouse events with pointer-events css
Pass SVG element at x,y
Fail transformed element at x,y
Pass no hit target at x,y
Fail No viewport available

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 3 tests
2 Pass
1 Fail
3 Pass
Pass The dialog element should support :open.
Pass The details element should support :open.
Fail The select element should support :open.
Pass The select element should support :open.

View file

@ -2,9 +2,9 @@ Harness status: OK
Found 4 tests
1 Pass
3 Fail
Fail :user-valid selector should respond to user action
2 Pass
2 Fail
Pass :user-valid selector should respond to user action
Pass :user-valid selector properly interacts with submit & reset buttons
Fail Checkboxes should match :user-valid after the user clicks on it.
Fail Date inputs should match :user-valid after the user types a value into it.

View file

@ -0,0 +1,83 @@
<!DOCTYPE HTML>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="resources/elementsFromPoint.js"></script>
<script>
var loadedFrameCount = 0;
var t1 = async_test('elementsFromPoint on the root document for points in iframe elements');
var t2 = async_test('elementsFromPoint on inner documents');
function onFrameLoaded() {
loadedFrameCount++;
if (loadedFrameCount < 2)
return;
var body = document.body;
var html = document.documentElement;
var iframe = document.getElementById('iframe');
var scrollableIframe = document.getElementById('scrollableIframe');
t1.step(function() {
checkElementsFromPointFourCorners('document', 'iframe',
[iframe, body, html],
[iframe, body, html],
[iframe, body, html],
[scrollableIframe, iframe, body, html]);
checkElementsFromPointFourCorners('document', 'scrollableIframe',
[scrollableIframe, iframe, body, html],
[scrollableIframe, iframe, body, html],
[scrollableIframe, iframe, body, html],
[scrollableIframe, iframe, body, html]);
});
t1.done();
t2.step(function() {
var iframeDocument = document.getElementById('iframe').contentDocument;
var iframeRoot = iframeDocument.documentElement;
var iframeBody = iframeDocument.body;
var iframeDiv = iframeDocument.getElementById('div');
checkElementsFromPointFourCorners('document.getElementById(\'iframe\').contentDocument', 'div',
[iframeDiv, iframeBody, iframeRoot],
[iframeDiv, iframeBody, iframeRoot],
[iframeDiv, iframeBody, iframeRoot],
[iframeDiv, iframeBody, iframeRoot]);
var iframeDocument2 = document.getElementById('scrollableIframe').contentDocument;
var iframeRoot2 = iframeDocument2.documentElement;
var iframeBody2 = iframeDocument2.body;
var iframeSmallDiv = iframeDocument2.getElementById('small');
var iframeBigDiv = iframeDocument2.getElementById('big');
checkElementsFromPointFourCorners('document.getElementById(\'scrollableIframe\').contentDocument', 'big',
[iframeSmallDiv, iframeBigDiv, iframeBody2, iframeRoot2],
[iframeBigDiv, iframeBody2, iframeRoot2],
[],
[]);
});
t2.done();
}
</script>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
height: 500px;
}
#iframe {
width: 200px;
height: 200px;
}
#scrollableIframe {
position: absolute;
top: 0;
left: 0;
transform: translate(50px, 50px);
width: 150px;
height: 150px;
overflow-y: scroll;
overflow-x: scroll;
}
</style>
<iframe id="iframe" src="resources/iframe1.html"></iframe>
<iframe id="scrollableIframe" src="resources/iframe2.html"></iframe>

View file

@ -0,0 +1,150 @@
<!DOCTYPE html>
<title>cssom-view - elementsFromPoint</title>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<style>
.size {
width:60px;
height:60px;
}
.overlay {
position:absolute;
top:69px;
pointer-events:none;
}
.purple {
background-color: rebeccapurple;
}
.yellow {
background-color: yellow;
}
.teal {
background-color: teal;
}
.pink {
background-color: pink;
}
</style>
<body>
<div id='purple' class="size purple" >&nbsp;</div>
<div id='yellow' class="size yellow">&nbsp;</div>
<div id='teal' class="size overlay teal">&nbsp;</div>
<iframe id=iframe-1 src="iframe.html" style='display:none;position:absolute; left:300px;'></iframe>
<iframe id=iframe-2 src="iframe.html" width="" height=""></iframe>
<iframe id=iframe-3 width="" height=""></iframe>
<svg id=squiggle xmlns="http://www.w3.org/2000/svg" height="98" width="500" viewBox="0 0 581 98">
<path stroke-dashoffset="0.00" stroke-dasharray="" d="M62.9 14.9c-25-7.74-56.6 4.8-60.4 24.3-3.73 19.6 21.6 35 39.6 37.6 42.8 6.2 72.9-53.4 116-58.9 65-18.2 191 101 215 28.8 5-16.7-7-49.1-34-44-34 11.5-31 46.5-14 69.3 9.38 12.6 24.2 20.6 39.8 22.9 91.4 9.05 102-98.9 176-86.7 18.8 3.81 33 17.3 36.7 34.6 2.01 10.2.124 21.1-5.18 30.1" stroke="#000" stroke-width="4.3" fill="none">
</path>
</svg>
<svg id=svg-transform width="180" height="140"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Now we add a text element and apply rotate and translate to both -->
<rect x="50" y="50" height="60" width="60" style="stroke:#000; fill: #0086B2" transform="translate(30) rotate(45 50 50)"></rect>
<text x="60" y="105" transform="translate(30) rotate(45 50 50)"> Hello! </text>
</svg>
<div id='pink' class='size pink' style='transform: translate(10px)'>&nbsp;</div>
<div id='anotherteal' class='size teal' style='pointer-events:none'>Another teal</div>
<script>
setup({explicit_done:true});
window.onload = function () {
test(function () {
assert_array_equals(document.elementsFromPoint(-1, -1), [],
"both co-ordinates passed in are negative so should have returned a []");
assert_array_equals(document.elementsFromPoint(-1, -1), [],
"x co-ordinates passed in are negative so should have returned a []");
assert_array_equals(document.elementsFromPoint(-1, -1), [],
"y co-ordinates passed in are negative so should have returned a []");
}, "Negative co-ordinates");
test(function () {
var viewportX = window.innerWidth;
var viewportY = window.innerHeight;
assert_array_equals(document.elementsFromPoint(viewportX + 100, 10), [],
"X co-ordinates larger than viewport so should have returned a []");
assert_array_equals(document.elementsFromPoint(10, viewportY + 100), [],
"Y co-ordinates larger than viewport so should have returned a []");
assert_array_equals(document.elementsFromPoint(viewportX + 100, viewportY + 100), [],
"X, Y co-ordinates larger than viewport so should have returned a []");
}, "co-ordinates larger than the viewport");
test(function () {
var viewportX = window.frames[1].innerWidth;
var viewportY = window.frames[1].innerHeight;
var iframeRect = document.getElementById('iframe-2').getBoundingClientRect();
assert_array_equals([], window.frames[1].document.elementsFromPoint(iframeRect.right + viewportX + 100, 10),
"X co-ordinates larger than viewport so should have returned a []");
assert_array_equals([], window.frames[1].document.elementsFromPoint(10, iframeRect.bottom + viewportY + 100),
"Y co-ordinates larger than viewport so should have returned a []");
assert_array_equals([], window.frames[1].document.elementsFromPoint(iframeRect.right + viewportX + 100,
iframeRect.bottom + viewportY + 100),
"X, Y co-ordinates larger than viewport so should have returned a []");
}, "co-ordinates larger than the viewport from in iframe");
test(function () {
assert_array_equals(document.elementsFromPoint(10, 10),
[document.getElementById('purple'), document.body, document.querySelector('html')],
"Should have returned a sequence with `[purple element, document.body, html]`");
}, "Return first element that is the target for hit testing");
test(function () {
assert_array_equals(document.elementsFromPoint(10, 80),
[document.getElementById('yellow'), document.body, document.querySelector('html')],
"Should have returned a sequence with `[yellow element, document.body, html]`");
}, "First element to get mouse events with pointer-events css");
test(function () {
var svg = document.getElementById('squiggle');
var svgRect = svg.getBoundingClientRect();
assert_array_equals(document.elementsFromPoint(svgRect.left + Math.round(svgRect.width/2),
svgRect.top + Math.round(svgRect.height/2)),
[svg, document.body, document.querySelector('html')],
"Should have returned a sequence with [svg, body, html]");
}, "SVG element at x,y");
test(function () {
var svg = document.getElementById('svg-transform');
var svgRect = svg.getBoundingClientRect();
assert_array_equals(document.elementsFromPoint(svgRect.left + Math.round(svgRect.width/2),
svgRect.top + Math.round(svgRect.height/2)),
[svg.querySelector("rect"), svg, document.body, document.querySelector('html')],
"Should have returned [svg rect, svg, body, html]");
var pink = document.getElementById('pink');
var pinkRect = pink.getBoundingClientRect();
assert_array_equals(document.elementsFromPoint(pinkRect.left + Math.round(pinkRect.width/2),
pinkRect.top + Math.round(pinkRect.height/2)),
[pink, document.body, document.querySelector('html')],
"Should have returned a sequence with [pink, body, html]");
}, "transformed element at x,y");
test(function () {
var anotherteal = document.getElementById('anotherteal');
var anothertealRect = anotherteal.getBoundingClientRect();
assert_array_equals(document.elementsFromPoint(anothertealRect.left + Math.round(anothertealRect.width/2),
anothertealRect.top + Math.round(anothertealRect.height/2)),
[document.body, document.querySelector('html')],
"Should have returned the sequence [body, html]");
var doc = frames[1].document;
assert_array_equals([doc.querySelector('html')], doc.elementsFromPoint(0, 100),
"Should have returned the sequence [html]")
var doc = frames[2].document;
doc.removeChild(doc.documentElement);
assert_array_equals(doc.elementsFromPoint(0, 0), [],
"Should have returned [] as no root element");
}, "no hit target at x,y");
test(function () {
var doc = document.implementation.createHTMLDocument('foo');
assert_array_equals(doc.elementsFromPoint(0, 0), [],
"Should have returned []")
}, "No viewport available");
done();
}
</script>

View file

@ -0,0 +1,4 @@
<!DOCTYPE html>
<style>
html, body { padding:9px; border:1px solid }
</style>

View file

@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<style>
html, body {
margin: 0;
padding: 0;
}
#div {
width: 100px;
height: 100px;
background: red;
}
</style>
<div id='div'></div>
<script>
window.onload = window.parent.onFrameLoaded();
</script>

View file

@ -0,0 +1,25 @@
<!DOCTYPE HTML>
<style>
html, body {
margin: 0;
padding: 0;
}
#big {
width: 125px;
height: 500px;
background: blue;
}
#small {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
background: green;
}
</style>
<div id='big'></div>
<div id='small'></div>
<script>
window.onload = window.parent.onFrameLoaded();
</script>