Skip to content

Commit 4653b24

Browse files
committed
Bug 1514074 - Retarget results of offset* DOM APIs. r=smaug
Per w3c/csswg-drafts#159. The test is imported from WebKit, who has already implemented this, with a few fixes to avoid duplicate test names and non-undefined return values from add_cleanup. See the diff attached to the bug. Differential Revision: https://phabricator.services.mozilla.com/D15938 --HG-- extra : moz-landing-system : lando
1 parent 46bdab5 commit 4653b24

File tree

2 files changed

+236
-8
lines changed

2 files changed

+236
-8
lines changed

dom/html/nsGenericHTMLElement.cpp

+46-8
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,15 @@ static bool IsOffsetParent(nsIFrame* aFrame) {
203203
return false;
204204
}
205205

206-
Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) {
207-
aRect = CSSIntRect();
206+
struct OffsetResult {
207+
Element* mParent = nullptr;
208+
CSSIntRect mRect;
209+
};
208210

209-
nsIFrame* frame = GetPrimaryFrame(FlushType::Layout);
211+
static OffsetResult GetUnretargetedOffsetsFor(const Element& aElement) {
212+
nsIFrame* frame = aElement.GetPrimaryFrame();
210213
if (!frame) {
211-
return nullptr;
214+
return {};
212215
}
213216

214217
nsIFrame* styleFrame = nsLayoutUtils::GetStyleFrame(frame);
@@ -217,7 +220,7 @@ Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) {
217220
nsPoint origin(0, 0);
218221

219222
nsIContent* offsetParent = nullptr;
220-
Element* docElement = GetComposedDoc()->GetRootElement();
223+
Element* docElement = aElement.GetComposedDoc()->GetRootElement();
221224
nsIContent* content = frame->GetContent();
222225

223226
if (content &&
@@ -270,7 +273,7 @@ Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) {
270273
//
271274
// We use GetBodyElement() here, not GetBody(), because we don't want to
272275
// end up with framesets here.
273-
offsetParent = GetComposedDoc()->GetBodyElement();
276+
offsetParent = aElement.GetComposedDoc()->GetBodyElement();
274277
}
275278
}
276279

@@ -289,9 +292,44 @@ Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) {
289292
// we only care about the size. We just have to use something non-null.
290293
nsRect rcFrame = nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame);
291294
rcFrame.MoveTo(origin);
292-
aRect = CSSIntRect::FromAppUnitsRounded(rcFrame);
295+
return {Element::FromNodeOrNull(offsetParent),
296+
CSSIntRect::FromAppUnitsRounded(rcFrame)};
297+
}
298+
299+
static bool ShouldBeRetargeted(const Element& aReferenceElement,
300+
const Element& aElementToMaybeRetarget) {
301+
ShadowRoot* shadow = aElementToMaybeRetarget.GetContainingShadow();
302+
if (!shadow) {
303+
return false;
304+
}
305+
for (ShadowRoot* scope = aReferenceElement.GetContainingShadow(); scope;
306+
scope = scope->Host()->GetContainingShadow()) {
307+
if (scope == shadow) {
308+
return false;
309+
}
310+
}
311+
312+
return true;
313+
}
314+
315+
Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) {
316+
aRect = CSSIntRect();
317+
318+
if (!GetPrimaryFrame(FlushType::Layout)) {
319+
return nullptr;
320+
}
321+
322+
OffsetResult thisResult = GetUnretargetedOffsetsFor(*this);
323+
aRect = thisResult.mRect;
324+
325+
Element* parent = thisResult.mParent;
326+
while (parent && ShouldBeRetargeted(*this, *parent)) {
327+
OffsetResult result = GetUnretargetedOffsetsFor(*parent);
328+
aRect += result.mRect.TopLeft();
329+
parent = result.mParent;
330+
}
293331

294-
return offsetParent ? offsetParent->AsElement() : nullptr;
332+
return parent;
295333
}
296334

297335
bool nsGenericHTMLElement::Spellcheck() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta name="author" title="Ryosuke Niwa" href="mailto:[email protected]">
5+
<meta name="assert" content="offsetParent should only return nodes that are shadow including ancestor">
6+
<link rel="help" href="https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent">
7+
<link rel="help" href="https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor">
8+
<script src="/resources/testharness.js"></script>
9+
<script src="/resources/testharnessreport.js"></script>
10+
<script src="resources/event-path-test-helpers.js"></script>
11+
</head>
12+
<body>
13+
<div id="log"></div>
14+
<div id="container" style="position: relative"></div>
15+
<script>
16+
17+
const container = document.getElementById('container');
18+
19+
function testOffsetParentInShadowTree(mode) {
20+
test(function () {
21+
const host = document.createElement('div');
22+
container.appendChild(host);
23+
this.add_cleanup(() => host.remove());
24+
const shadowRoot = host.attachShadow({mode});
25+
shadowRoot.innerHTML = '<div id="relativeParent" style="position: relative; padding-left: 100px; padding-top: 70px;"><div id="target"></div></div>';
26+
const relativeParent = shadowRoot.getElementById('relativeParent');
27+
28+
assert_true(relativeParent instanceof HTMLDivElement);
29+
const target = shadowRoot.getElementById('target');
30+
assert_equals(target.offsetParent, relativeParent);
31+
assert_equals(target.offsetLeft, 100);
32+
assert_equals(target.offsetTop, 70);
33+
}, `offsetParent must return the offset parent in the same shadow tree of ${mode} mode`);
34+
}
35+
36+
testOffsetParentInShadowTree('open');
37+
testOffsetParentInShadowTree('closed');
38+
39+
function testOffsetParentInNestedShadowTrees(mode) {
40+
test(function () {
41+
const outerHost = document.createElement('section');
42+
container.appendChild(outerHost);
43+
this.add_cleanup(() => outerHost.remove());
44+
const outerShadow = outerHost.attachShadow({mode});
45+
outerShadow.innerHTML = '<section id="outerParent" style="position: absolute; top: 50px; left: 50px;"></section>';
46+
47+
const innerHost = document.createElement('div');
48+
outerShadow.firstChild.appendChild(innerHost);
49+
const innerShadow = innerHost.attachShadow({mode});
50+
innerShadow.innerHTML = '<div id="innerParent" style="position: relative; padding-left: 60px; padding-top: 40px;"><div id="target"></div></div>';
51+
const innerParent = innerShadow.getElementById('innerParent');
52+
53+
const target = innerShadow.getElementById('target');
54+
assert_true(innerParent instanceof HTMLDivElement);
55+
assert_equals(target.offsetParent, innerParent);
56+
assert_equals(target.offsetLeft, 60);
57+
assert_equals(target.offsetTop, 40);
58+
59+
outerHost.remove();
60+
}, `offsetParent must return the offset parent in the same shadow tree of ${mode} mode even when nested`);
61+
}
62+
63+
testOffsetParentInNestedShadowTrees('open');
64+
testOffsetParentInNestedShadowTrees('closed');
65+
66+
function testOffsetParentOnElementAssignedToSlotInsideOffsetParent(mode) {
67+
test(function () {
68+
const host = document.createElement('div');
69+
host.innerHTML = '<div id="target"></div>'
70+
container.appendChild(host);
71+
this.add_cleanup(() => host.remove());
72+
const shadowRoot = host.attachShadow({mode});
73+
shadowRoot.innerHTML = '<div style="position: relative; padding-left: 85px; padding-top: 45px;"><slot></slot></div>';
74+
const target = host.querySelector('#target');
75+
assert_equals(target.offsetParent, container);
76+
assert_equals(target.offsetLeft, 85);
77+
assert_equals(target.offsetTop, 45);
78+
}, `offsetParent must skip offset parents of an element when the context object is assigned to a slot in a shadow tree of ${mode} mode`);
79+
}
80+
81+
testOffsetParentOnElementAssignedToSlotInsideOffsetParent('open');
82+
testOffsetParentOnElementAssignedToSlotInsideOffsetParent('closed');
83+
84+
function testOffsetParentOnElementAssignedToSlotInsideNestedOffsetParents(mode) {
85+
test(function () {
86+
const host = document.createElement('div');
87+
host.innerHTML = '<div id="target" style="border:solid 1px blue;">hi</div>';
88+
const previousBlock = document.createElement('div');
89+
previousBlock.style.height = '12px';
90+
container.append(previousBlock, host);
91+
this.add_cleanup(() => { container.innerHTML = ''; });
92+
const shadowRoot = host.attachShadow({mode});
93+
shadowRoot.innerHTML = '<section style="position: relative; margin-left: 20px; margin-top: 100px; background: #ccc"><div style="position: absolute; top: 10px; left: 10px;"><slot></slot></div></section>';
94+
const target = host.querySelector('#target');
95+
assert_equals(target.offsetParent, container);
96+
assert_equals(target.offsetLeft, 30);
97+
assert_equals(target.offsetTop, 122);
98+
}, `offsetParent must skip multiple offset parents of an element when the context object is assigned to a slot in a shadow tree of ${mode} mode`);
99+
}
100+
101+
testOffsetParentOnElementAssignedToSlotInsideNestedOffsetParents('open');
102+
testOffsetParentOnElementAssignedToSlotInsideNestedOffsetParents('closed');
103+
104+
function testOffsetParentOnElementAssignedToSlotInsideNestedShadowTrees(mode) {
105+
test(function () {
106+
const outerHost = document.createElement('section');
107+
outerHost.innerHTML = '<div id="target"></div>';
108+
container.appendChild(outerHost);
109+
this.add_cleanup(() => outerHost.remove());
110+
const outerShadow = outerHost.attachShadow({mode});
111+
outerShadow.innerHTML = '<section style="position: absolute; top: 40px; left: 50px;"><div id="innerHost"><slot></slot></div></section>';
112+
113+
const innerShadow = outerShadow.getElementById('innerHost').attachShadow({mode});
114+
innerShadow.innerHTML = '<div style="position: absolute; top: 200px; margin-left: 100px;"><slot></slot></div>';
115+
116+
const target = outerHost.querySelector('#target');
117+
assert_equals(target.offsetParent, container);
118+
assert_equals(target.offsetLeft, 150);
119+
assert_equals(target.offsetTop, 240);
120+
outerHost.remove();
121+
}, `offsetParent must skip offset parents of an element when the context object is assigned to a slot in nested shadow trees of ${mode} mode`);
122+
}
123+
124+
testOffsetParentOnElementAssignedToSlotInsideNestedShadowTrees('open');
125+
testOffsetParentOnElementAssignedToSlotInsideNestedShadowTrees('closed');
126+
127+
function testOffsetParentOnElementInsideShadowTreeWithoutOffsetParent(mode) {
128+
test(function () {
129+
const outerHost = document.createElement('section');
130+
container.appendChild(outerHost);
131+
this.add_cleanup(() => outerHost.remove());
132+
const outerShadow = outerHost.attachShadow({mode});
133+
outerShadow.innerHTML = '<div id="innerHost"><div id="target"></div></div>';
134+
135+
const innerShadow = outerShadow.getElementById('innerHost').attachShadow({mode});
136+
innerShadow.innerHTML = '<div style="position: absolute; top: 23px; left: 24px;"><slot></slot></div>';
137+
138+
const target = outerShadow.querySelector('#target');
139+
assert_equals(target.offsetParent, container);
140+
assert_equals(target.offsetLeft, 24);
141+
assert_equals(target.offsetTop, 23);
142+
}, `offsetParent must find the first offset parent which is a shadow-including ancestor of the context object even some shadow tree of ${mode} mode did not have any offset parent`);
143+
}
144+
145+
testOffsetParentOnElementInsideShadowTreeWithoutOffsetParent('open');
146+
testOffsetParentOnElementInsideShadowTreeWithoutOffsetParent('closed');
147+
148+
function testOffsetParentOnUnassignedChild(mode) {
149+
test(function () {
150+
const host = document.createElement('section');
151+
host.innerHTML = '<div id="target"></div>';
152+
this.add_cleanup(() => host.remove());
153+
container.appendChild(host);
154+
const shadowRoot = host.attachShadow({mode});
155+
shadowRoot.innerHTML = '<section style="position: absolute; top: 50px; left: 50px;">content</section>';
156+
const target = host.querySelector('#target');
157+
assert_equals(target.offsetParent, null);
158+
assert_equals(target.offsetLeft, 0);
159+
assert_equals(target.offsetTop, 0);
160+
}, `offsetParent must return null on a child element of a shadow host for the shadow tree in ${mode} mode which is not assigned to any slot`);
161+
}
162+
163+
testOffsetParentOnUnassignedChild('open');
164+
testOffsetParentOnUnassignedChild('closed');
165+
166+
function testOffsetParentOnAssignedChildNotInFlatTree(mode) {
167+
test(function () {
168+
const outerHost = document.createElement('section');
169+
outerHost.innerHTML = '<div id="target"></div>';
170+
container.appendChild(outerHost);
171+
this.add_cleanup(() => outerHost.remove());
172+
const outerShadow = outerHost.attachShadow({mode});
173+
outerShadow.innerHTML = '<div id="innerHost"><div style="position: absolute; top: 50px; left: 50px;"><slot></slot></div></div>';
174+
175+
const innerShadow = outerShadow.getElementById('innerHost').attachShadow({mode});
176+
innerShadow.innerHTML = '<div>content</div>';
177+
178+
const target = outerHost.querySelector('#target');
179+
assert_equals(target.offsetParent, null);
180+
assert_equals(target.offsetLeft, 0);
181+
assert_equals(target.offsetTop, 0);
182+
}, `offsetParent must return null on a child element of a shadow host for the shadow tree in ${mode} mode which is not in the flat tree`);
183+
}
184+
185+
testOffsetParentOnAssignedChildNotInFlatTree('open');
186+
testOffsetParentOnAssignedChildNotInFlatTree('closed');
187+
188+
</script>
189+
</body>
190+
</html>

0 commit comments

Comments
 (0)