Skip to content

Commit

Permalink
Bug 1514074 - Retarget results of offset* DOM APIs. r=smaug
Browse files Browse the repository at this point in the history
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
  • Loading branch information
emilio committed Jan 8, 2019
1 parent 44c7058 commit 210e540
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 8 deletions.
54 changes: 46 additions & 8 deletions dom/html/nsGenericHTMLElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,15 @@ static bool IsOffsetParent(nsIFrame* aFrame) {
return false;
}

Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) {
aRect = CSSIntRect();
struct OffsetResult {
Element* mParent = nullptr;
CSSIntRect mRect;
};

nsIFrame* frame = GetPrimaryFrame(FlushType::Layout);
static OffsetResult GetUnretargetedOffsetsFor(const Element& aElement) {
nsIFrame* frame = aElement.GetPrimaryFrame();
if (!frame) {
return nullptr;
return {};
}

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

nsIContent* offsetParent = nullptr;
Element* docElement = GetComposedDoc()->GetRootElement();
Element* docElement = aElement.GetComposedDoc()->GetRootElement();
nsIContent* content = frame->GetContent();

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

Expand All @@ -289,9 +292,44 @@ Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) {
// we only care about the size. We just have to use something non-null.
nsRect rcFrame = nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame);
rcFrame.MoveTo(origin);
aRect = CSSIntRect::FromAppUnitsRounded(rcFrame);
return {Element::FromNodeOrNull(offsetParent),
CSSIntRect::FromAppUnitsRounded(rcFrame)};
}

static bool ShouldBeRetargeted(const Element& aReferenceElement,
const Element& aElementToMaybeRetarget) {
ShadowRoot* shadow = aElementToMaybeRetarget.GetContainingShadow();
if (!shadow) {
return false;
}
for (ShadowRoot* scope = aReferenceElement.GetContainingShadow(); scope;
scope = scope->Host()->GetContainingShadow()) {
if (scope == shadow) {
return false;
}
}

return true;
}

Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) {
aRect = CSSIntRect();

if (!GetPrimaryFrame(FlushType::Layout)) {
return nullptr;
}

OffsetResult thisResult = GetUnretargetedOffsetsFor(*this);
aRect = thisResult.mRect;

Element* parent = thisResult.mParent;
while (parent && ShouldBeRetargeted(*this, *parent)) {
OffsetResult result = GetUnretargetedOffsetsFor(*parent);
aRect += result.mRect.TopLeft();
parent = result.mParent;
}

return offsetParent ? offsetParent->AsElement() : nullptr;
return parent;
}

bool nsGenericHTMLElement::Spellcheck() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<!DOCTYPE html>
<html>
<head>
<meta name="author" title="Ryosuke Niwa" href="mailto:[email protected]">
<meta name="assert" content="offsetParent should only return nodes that are shadow including ancestor">
<link rel="help" href="https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent">
<link rel="help" href="https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/event-path-test-helpers.js"></script>
</head>
<body>
<div id="log"></div>
<div id="container" style="position: relative"></div>
<script>

const container = document.getElementById('container');

function testOffsetParentInShadowTree(mode) {
test(function () {
const host = document.createElement('div');
container.appendChild(host);
this.add_cleanup(() => host.remove());
const shadowRoot = host.attachShadow({mode});
shadowRoot.innerHTML = '<div id="relativeParent" style="position: relative; padding-left: 100px; padding-top: 70px;"><div id="target"></div></div>';
const relativeParent = shadowRoot.getElementById('relativeParent');

assert_true(relativeParent instanceof HTMLDivElement);
const target = shadowRoot.getElementById('target');
assert_equals(target.offsetParent, relativeParent);
assert_equals(target.offsetLeft, 100);
assert_equals(target.offsetTop, 70);
}, `offsetParent must return the offset parent in the same shadow tree of ${mode} mode`);
}

testOffsetParentInShadowTree('open');
testOffsetParentInShadowTree('closed');

function testOffsetParentInNestedShadowTrees(mode) {
test(function () {
const outerHost = document.createElement('section');
container.appendChild(outerHost);
this.add_cleanup(() => outerHost.remove());
const outerShadow = outerHost.attachShadow({mode});
outerShadow.innerHTML = '<section id="outerParent" style="position: absolute; top: 50px; left: 50px;"></section>';

const innerHost = document.createElement('div');
outerShadow.firstChild.appendChild(innerHost);
const innerShadow = innerHost.attachShadow({mode});
innerShadow.innerHTML = '<div id="innerParent" style="position: relative; padding-left: 60px; padding-top: 40px;"><div id="target"></div></div>';
const innerParent = innerShadow.getElementById('innerParent');

const target = innerShadow.getElementById('target');
assert_true(innerParent instanceof HTMLDivElement);
assert_equals(target.offsetParent, innerParent);
assert_equals(target.offsetLeft, 60);
assert_equals(target.offsetTop, 40);

outerHost.remove();
}, `offsetParent must return the offset parent in the same shadow tree of ${mode} mode even when nested`);
}

testOffsetParentInNestedShadowTrees('open');
testOffsetParentInNestedShadowTrees('closed');

function testOffsetParentOnElementAssignedToSlotInsideOffsetParent(mode) {
test(function () {
const host = document.createElement('div');
host.innerHTML = '<div id="target"></div>'
container.appendChild(host);
this.add_cleanup(() => host.remove());
const shadowRoot = host.attachShadow({mode});
shadowRoot.innerHTML = '<div style="position: relative; padding-left: 85px; padding-top: 45px;"><slot></slot></div>';
const target = host.querySelector('#target');
assert_equals(target.offsetParent, container);
assert_equals(target.offsetLeft, 85);
assert_equals(target.offsetTop, 45);
}, `offsetParent must skip offset parents of an element when the context object is assigned to a slot in a shadow tree of ${mode} mode`);
}

testOffsetParentOnElementAssignedToSlotInsideOffsetParent('open');
testOffsetParentOnElementAssignedToSlotInsideOffsetParent('closed');

function testOffsetParentOnElementAssignedToSlotInsideNestedOffsetParents(mode) {
test(function () {
const host = document.createElement('div');
host.innerHTML = '<div id="target" style="border:solid 1px blue;">hi</div>';
const previousBlock = document.createElement('div');
previousBlock.style.height = '12px';
container.append(previousBlock, host);
this.add_cleanup(() => { container.innerHTML = ''; });
const shadowRoot = host.attachShadow({mode});
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>';
const target = host.querySelector('#target');
assert_equals(target.offsetParent, container);
assert_equals(target.offsetLeft, 30);
assert_equals(target.offsetTop, 122);
}, `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`);
}

testOffsetParentOnElementAssignedToSlotInsideNestedOffsetParents('open');
testOffsetParentOnElementAssignedToSlotInsideNestedOffsetParents('closed');

function testOffsetParentOnElementAssignedToSlotInsideNestedShadowTrees(mode) {
test(function () {
const outerHost = document.createElement('section');
outerHost.innerHTML = '<div id="target"></div>';
container.appendChild(outerHost);
this.add_cleanup(() => outerHost.remove());
const outerShadow = outerHost.attachShadow({mode});
outerShadow.innerHTML = '<section style="position: absolute; top: 40px; left: 50px;"><div id="innerHost"><slot></slot></div></section>';

const innerShadow = outerShadow.getElementById('innerHost').attachShadow({mode});
innerShadow.innerHTML = '<div style="position: absolute; top: 200px; margin-left: 100px;"><slot></slot></div>';

const target = outerHost.querySelector('#target');
assert_equals(target.offsetParent, container);
assert_equals(target.offsetLeft, 150);
assert_equals(target.offsetTop, 240);
outerHost.remove();
}, `offsetParent must skip offset parents of an element when the context object is assigned to a slot in nested shadow trees of ${mode} mode`);
}

testOffsetParentOnElementAssignedToSlotInsideNestedShadowTrees('open');
testOffsetParentOnElementAssignedToSlotInsideNestedShadowTrees('closed');

function testOffsetParentOnElementInsideShadowTreeWithoutOffsetParent(mode) {
test(function () {
const outerHost = document.createElement('section');
container.appendChild(outerHost);
this.add_cleanup(() => outerHost.remove());
const outerShadow = outerHost.attachShadow({mode});
outerShadow.innerHTML = '<div id="innerHost"><div id="target"></div></div>';

const innerShadow = outerShadow.getElementById('innerHost').attachShadow({mode});
innerShadow.innerHTML = '<div style="position: absolute; top: 23px; left: 24px;"><slot></slot></div>';

const target = outerShadow.querySelector('#target');
assert_equals(target.offsetParent, container);
assert_equals(target.offsetLeft, 24);
assert_equals(target.offsetTop, 23);
}, `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`);
}

testOffsetParentOnElementInsideShadowTreeWithoutOffsetParent('open');
testOffsetParentOnElementInsideShadowTreeWithoutOffsetParent('closed');

function testOffsetParentOnUnassignedChild(mode) {
test(function () {
const host = document.createElement('section');
host.innerHTML = '<div id="target"></div>';
this.add_cleanup(() => host.remove());
container.appendChild(host);
const shadowRoot = host.attachShadow({mode});
shadowRoot.innerHTML = '<section style="position: absolute; top: 50px; left: 50px;">content</section>';
const target = host.querySelector('#target');
assert_equals(target.offsetParent, null);
assert_equals(target.offsetLeft, 0);
assert_equals(target.offsetTop, 0);
}, `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`);
}

testOffsetParentOnUnassignedChild('open');
testOffsetParentOnUnassignedChild('closed');

function testOffsetParentOnAssignedChildNotInFlatTree(mode) {
test(function () {
const outerHost = document.createElement('section');
outerHost.innerHTML = '<div id="target"></div>';
container.appendChild(outerHost);
this.add_cleanup(() => outerHost.remove());
const outerShadow = outerHost.attachShadow({mode});
outerShadow.innerHTML = '<div id="innerHost"><div style="position: absolute; top: 50px; left: 50px;"><slot></slot></div></div>';

const innerShadow = outerShadow.getElementById('innerHost').attachShadow({mode});
innerShadow.innerHTML = '<div>content</div>';

const target = outerHost.querySelector('#target');
assert_equals(target.offsetParent, null);
assert_equals(target.offsetLeft, 0);
assert_equals(target.offsetTop, 0);
}, `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`);
}

testOffsetParentOnAssignedChildNotInFlatTree('open');
testOffsetParentOnAssignedChildNotInFlatTree('closed');

</script>
</body>
</html>

0 comments on commit 210e540

Please sign in to comment.