IntersectionObserver bug fix

This commit is contained in:
Fedor 2025-01-30 22:11:48 +02:00
parent 8dc6679bf5
commit a86941a552
14 changed files with 155 additions and 60 deletions

View File

@ -11,6 +11,7 @@
#include "nsRefreshDriver.h"
#include "mozilla/PresShell.h"
#include "mozilla/ServoBindings.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/DocumentInlines.h"
namespace mozilla {
@ -79,7 +80,20 @@ already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor(
RefPtr<DOMIntersectionObserver> observer =
new DOMIntersectionObserver(window.forget(), aCb);
observer->mRoot = aOptions.mRoot;
if (!aOptions.mRoot.IsNull()) {
if (aOptions.mRoot.Value().IsElement()) {
observer->mRoot = aOptions.mRoot.Value().GetAsElement();
} else {
MOZ_ASSERT(aOptions.mRoot.Value().IsDocument());
if (!StaticPrefs::
dom_IntersectionObserverExplicitDocumentRoot_enabled()) {
aRv.ThrowTypeError<dom::MSG_DOES_NOT_IMPLEMENT_INTERFACE>(
"'root' member of IntersectionObserverInit", "Element");
return nullptr;
}
observer->mRoot = aOptions.mRoot.Value().GetAsDocument();
}
}
if (!observer->SetRootMargin(aOptions.mRootMargin)) {
aRv.ThrowSyntaxError("rootMargin must be specified in pixels or percent.");
@ -207,7 +221,7 @@ enum class BrowsingContextOrigin { Similar, Different, Unknown };
// contexts" is gone, but this is still in the spec, see
// https://github.com/w3c/IntersectionObserver/issues/161
static BrowsingContextOrigin SimilarOrigin(const Element& aTarget,
const Element* aRoot) {
const nsINode* aRoot) {
if (!aRoot) {
return BrowsingContextOrigin::Unknown;
}
@ -330,9 +344,9 @@ void DOMIntersectionObserver::Update(Document* aDocument,
// the inflation until later.
nsRect rootRect;
nsIFrame* rootFrame = nullptr;
Element* root = mRoot;
if (mRoot) {
if ((rootFrame = mRoot->GetPrimaryFrame())) {
nsINode* root = mRoot;
if (mRoot && mRoot->IsElement()) {
if ((rootFrame = mRoot->AsElement()->GetPrimaryFrame())) {
nsRect rootRectRelativeToRootFrame;
if (rootFrame->IsScrollFrame()) {
// rootRectRelativeToRootFrame should be the content rect of rootFrame,
@ -348,28 +362,32 @@ void DOMIntersectionObserver::Update(Document* aDocument,
rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
rootFrame, rootRectRelativeToRootFrame, containingBlock);
}
} else if (PresShell* presShell = aDocument->GetPresShell()) {
// FIXME(emilio): This shouldn't probably go through the presShell and just
// through the document tree.
rootFrame = presShell->GetRootScrollFrame();
if (rootFrame) {
nsPresContext* presContext = rootFrame->PresContext();
while (!presContext->IsRootContentDocument()) {
presContext = presContext->GetParentPresContext();
if (!presContext) {
break;
}
nsIFrame* rootScrollFrame =
presContext->PresShell()->GetRootScrollFrame();
if (rootScrollFrame) {
rootFrame = rootScrollFrame;
} else {
break;
} else {
MOZ_ASSERT(!mRoot || mRoot->IsDocument());
Document* rootDocument = mRoot->AsDocument();
if (rootDocument) {
if (PresShell* presShell = rootDocument->GetPresShell()) {
rootFrame = presShell->GetRootScrollFrame();
if (rootFrame) {
nsPresContext* presContext = rootFrame->PresContext();
while (!presContext->IsRootContentDocument()) {
presContext = presContext->GetParentPresContext();
if (!presContext) {
break;
}
nsIFrame* rootScrollFrame =
presContext->PresShell()->GetRootScrollFrame();
if (rootScrollFrame) {
rootFrame = rootScrollFrame;
} else {
break;
}
}
root = rootFrame->GetContent()->AsElement();
nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
rootRect = scrollFrame->GetScrollPortRect();
}
}
root = rootFrame->GetContent()->AsElement();
nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
rootRect = scrollFrame->GetScrollPortRect();
}
}
@ -416,10 +434,7 @@ void DOMIntersectionObserver::Update(Document* aDocument,
// 2.3. Let targetRect be a DOMRectReadOnly obtained by running the
// getBoundingClientRect() algorithm on target.
targetRect = nsLayoutUtils::GetAllInFlowRectsUnion(
targetFrame,
nsLayoutUtils::GetContainingBlockForClientRect(targetFrame),
nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
targetRect = targetFrame->GetBoundingClientRect();
// 2.4. Let intersectionRect be the result of running the compute the
// intersection algorithm on target.
@ -458,7 +473,9 @@ void DOMIntersectionObserver::Update(Document* aDocument,
// length of observer.thresholds if intersectionRatio is greater than or
// equal to the last entry in observer.thresholds.
int32_t thresholdIndex = -1;
// FIXME(emilio): Why the isIntersecting check?
// If not intersecting, we can just shortcut, as we know that the thresholds
// are always between 0 and 1.
if (isIntersecting) {
thresholdIndex = mThresholds.IndexOfFirstElementGt(intersectionRatio);
if (thresholdIndex == 0) {
@ -468,19 +485,22 @@ void DOMIntersectionObserver::Update(Document* aDocument,
// neither Chrome nor the WPT tests expect this behavior, so treat these
// two cases as one.
//
// FIXME(emilio): Looks like a good candidate for a spec issue.
// See https://github.com/w3c/IntersectionObserver/issues/432 about
// this.
thresholdIndex = -1;
}
}
// Steps 2.10 - 2.15.
if (target->UpdateIntersectionObservation(this, thresholdIndex)) {
QueueIntersectionObserverEntry(target, time,
origin == BrowsingContextOrigin::Different
? Nothing()
: Some(rootBounds),
targetRect, intersectionRect,
intersectionRatio);
// See https://github.com/w3c/IntersectionObserver/issues/432 about
// why we use thresholdIndex > 0 rather than isIntersecting for the
// entry's isIntersecting value.
QueueIntersectionObserverEntry(
target, time,
origin == BrowsingContextOrigin::Different ? Some(rootBounds)
: Nothing(),
targetRect, intersectionRect, thresholdIndex > 0, intersectionRatio);
}
}
}
@ -488,7 +508,7 @@ void DOMIntersectionObserver::Update(Document* aDocument,
void DOMIntersectionObserver::QueueIntersectionObserverEntry(
Element* aTarget, DOMHighResTimeStamp time, const Maybe<nsRect>& aRootRect,
const nsRect& aTargetRect, const Maybe<nsRect>& aIntersectionRect,
double aIntersectionRatio) {
bool aIsIntersecting, double aIntersectionRatio) {
RefPtr<DOMRect> rootBounds;
if (aRootRect.isSome()) {
rootBounds = new DOMRect(this);
@ -502,7 +522,7 @@ void DOMIntersectionObserver::QueueIntersectionObserverEntry(
}
RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
this, time, rootBounds.forget(), boundingClientRect.forget(),
intersectionRect.forget(), aIntersectionRect.isSome(), aTarget,
intersectionRect.forget(), aIsIntersecting, aTarget,
aIntersectionRatio);
mQueuedEntries.AppendElement(entry.forget());
}

View File

@ -28,9 +28,9 @@ class DOMIntersectionObserverEntry final : public nsISupports,
double aIntersectionRatio)
: mOwner(aOwner),
mTime(aTime),
mRootBounds(aRootBounds),
mBoundingClientRect(aBoundingClientRect),
mIntersectionRect(aIntersectionRect),
mRootBounds(std::move(aRootBounds)),
mBoundingClientRect(std::move(aBoundingClientRect)),
mIntersectionRect(std::move(aIntersectionRect)),
mIsIntersecting(aIsIntersecting),
mTarget(aTarget),
mIntersectionRatio(aIntersectionRatio) {}
@ -104,7 +104,7 @@ class DOMIntersectionObserver final : public nsISupports,
nsISupports* GetParentObject() const { return mOwner; }
Element* GetRoot() const { return mRoot; }
nsINode* GetRoot() const { return mRoot; }
void GetRootMargin(nsACString&);
bool SetRootMargin(const nsACString&);
@ -128,12 +128,13 @@ class DOMIntersectionObserver final : public nsISupports,
const Maybe<nsRect>& aRootRect,
const nsRect& aTargetRect,
const Maybe<nsRect>& aIntersectionRect,
bool aIsIntersecting,
double aIntersectionRatio);
nsCOMPtr<nsPIDOMWindowInner> mOwner;
RefPtr<Document> mDocument;
RefPtr<dom::IntersectionCallback> mCallback;
RefPtr<Element> mRoot;
RefPtr<nsINode> mRoot;
StyleRect<LengthPercentage> mRootMargin;
nsTArray<double> mThresholds;

View File

@ -1075,10 +1075,7 @@ already_AddRefed<DOMRect> Element::GetBoundingClientRect() {
return rect.forget();
}
nsRect r = nsLayoutUtils::GetAllInFlowRectsUnion(
frame, nsLayoutUtils::GetContainingBlockForClientRect(frame),
nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
rect->SetLayoutRect(r);
rect->SetLayoutRect(frame->GetBoundingClientRect());
return rect.forget();
}

View File

@ -33,7 +33,7 @@ interface IntersectionObserver {
optional IntersectionObserverInit options = {});
[Constant]
readonly attribute Element? root;
readonly attribute Node? root;
[Constant]
readonly attribute UTF8String rootMargin;
[Constant,Cached]
@ -56,7 +56,7 @@ dictionary IntersectionObserverEntryInit {
};
dictionary IntersectionObserverInit {
Element? root = null;
(Element or Document)? root = null;
UTF8String rootMargin = "0px";
(double or sequence<double>) threshold = 0;
};

View File

@ -4735,6 +4735,8 @@ UniquePtr<RangePaintInfo> PresShell::CreateRangePaintInfo(
// appropriate nsDisplayAsyncZoom display items. This code handles the general
// case with nested async zooms (even though that never actually happens),
// because it fell out of the implementation for free.
//
// TODO: Do we need to do the same for ancestor transforms?
for (nsPresContext* ctx = GetPresContext(); ctx;
ctx = ctx->GetParentPresContext()) {
PresShell* shell = ctx->PresShell();
@ -4775,7 +4777,7 @@ UniquePtr<RangePaintInfo> PresShell::CreateRangePaintInfo(
// determine the offset of the reference frame for the display list
// to the root frame. This will allow the coordinates used when painting
// to all be offset from the same point
info->mRootOffset = ancestorFrame->GetOffsetTo(rootFrame);
info->mRootOffset = ancestorFrame->GetBoundingClientRect().TopLeft();
rangeRect.MoveBy(info->mRootOffset);
aSurfaceRect.UnionRect(aSurfaceRect, rangeRect);

View File

@ -7432,6 +7432,12 @@ nsRect nsIFrame::GetNormalRect() const {
return GetRect();
}
nsRect nsIFrame::GetBoundingClientRect() {
return nsLayoutUtils::GetAllInFlowRectsUnion(
this, nsLayoutUtils::GetContainingBlockForClientRect(this),
nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
}
nsPoint nsIFrame::GetPositionIgnoringScrolling() const {
return GetParent() ? GetParent()->GetPositionOfChildIgnoringScrolling(this)
: GetPosition();

View File

@ -1271,6 +1271,11 @@ class nsIFrame : public nsQueryFrame {
*/
nsRect GetNormalRect() const;
/**
* Returns frame's rect as required by the GetBoundingClientRect() DOM API.
*/
nsRect GetBoundingClientRect();
/**
* Return frame's position without relative positioning.
* If aHasProperty is provided, returns whether the normal position

View File

@ -1640,6 +1640,11 @@
value: true
mirror: always
- name: dom.IntersectionObserverExplicitDocumentRoot.enabled
type: bool
value: true
mirror: always
- name: dom.ipc.cancel_content_js_when_navigating
type: bool
value: true

View File

@ -1 +1 @@
prefs: [dom.IntersectionObserver.enabled:true]
prefs: [dom.IntersectionObserver.enabled:true, dom.IntersectionObserverExplicitDocumentRoot.enabled:true]

View File

@ -1,4 +0,0 @@
[initial-observation-with-threshold.html]
[First rAF]
expected: FAIL

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<title>A height: 100% descendant should trigger a relayout when stretching.</title>
<link rel="help" href="https://drafts.csswg.org/css-flexbox-1/#definite-sizes" />
<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1043071" />
<link rel="match" href="../reference/ref-filled-green-100px-square.xht" />
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div style="display: flex; width: 100px;">
<div style="display: flex; flex-direction: column; flex: 1; min-height: 100px;">
<div style="flex: 1; background: red;">
<div style="height: 100%; background-color: green;"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/intersection-observer-test-utils.js"></script>
<style>
pre, #log {
position: absolute;
top: 0;
left: 200px;
}
iframe {
height: 250px;
width: 150px;
border: 0;
}
</style>
<iframe id="target-iframe" src="resources/iframe-no-root-subframe.html"></iframe>
<script>
var iframe = document.getElementById("target-iframe");
var target;
var root;
var entries = [];
iframe.onload = function() {
runTestCycle(function() {
assert_true(!!iframe, "iframe exists");
target = iframe.contentDocument.getElementById("target");
assert_true(!!target, "Target element exists.");
var observer = new IntersectionObserver(function(changes) {
entries = entries.concat(changes)
}, { root: iframe.contentDocument });
observer.observe(target);
entries = entries.concat(observer.takeRecords());
assert_equals(entries.length, 0, "No initial notifications.");
runTestCycle(step0, "First rAF.");
}, "Observer with explicit root which is the document.");
};
function step0() {
let vw = iframe.contentDocument.documentElement.clientWidth;
let vh = iframe.contentDocument.documentElement.clientHeight;
// The target element is partially clipped by the iframe's root scroller, so
// height of the intersection rect is (250 - 208) == 42.
checkLastEntry(entries, 0, [8, 108, 208, 308, 8, 108, 208, 250, 0, vw, 0, vh, true]);
}
</script>

View File

@ -43,7 +43,8 @@ function step3() {
assert_equals(entries.length, 2);
assert_true(entries[1].intersectionRatio >= 0.5 &&
entries[1].intersectionRatio < 1);
assert_equals(entries[1].isIntersecting, true);
// See https://github.com/w3c/IntersectionObserver/issues/432
assert_equals(entries[1].isIntersecting, false);
scroller.scrollTop = 100;
}

View File

@ -676,9 +676,8 @@ nsresult nsBaseDragService::DrawDrag(nsINode* aDOMNode,
// otherwise, there was no region so just set the rectangle to
// the size of the primary frame of the content.
nsCOMPtr<nsIContent> content = do_QueryInterface(dragNode);
nsIFrame* frame = content->GetPrimaryFrame();
if (frame) {
presLayoutRect = frame->GetRect();
if (nsIFrame* frame = content->GetPrimaryFrame()) {
presLayoutRect = frame->GetBoundingClientRect();
}
}