Skip to content

Commit 3cb70bb

Browse files
lunaleapsfacebook-github-bot
authored andcommitted
Introduce root to IntersectionObserver (#47394)
Summary: Pull Request resolved: #47394 ## Changelog: [General] [Added] - Support for observation `root` for Intersection Observer Reviewed By: rubennorte Differential Revision: D65085733 fbshipit-source-id: e67a74ace16c3f6f8ebba7eb811c1a9e0d559b0a
1 parent ff62ad1 commit 3cb70bb

12 files changed

Lines changed: 758 additions & 48 deletions

File tree

packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9607,6 +9607,7 @@ exports[`public API should not change unintentionally src/private/webapis/inters
96079607
observer: IntersectionObserver
96089608
) => mixed;
96099609
export interface IntersectionObserverInit {
9610+
root?: ?ReactNativeElement;
96109611
threshold?: number | $ReadOnlyArray<number>;
96119612
rnRootThreshold?: number | $ReadOnlyArray<number>;
96129613
}

packages/react-native/ReactCommon/react/nativemodule/intersectionobserver/NativeIntersectionObserver.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,20 @@ jsi::Object NativeIntersectionObserver::observeV2(
7070
auto shadowNode =
7171
shadowNodeFromValue(runtime, std::move(options.targetShadowNode));
7272
auto shadowNodeFamily = shadowNode->getFamilyShared();
73+
74+
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily;
75+
if (options.rootShadowNode.isObject()) {
76+
observationRootShadowNodeFamily =
77+
shadowNodeFromValue(runtime, options.rootShadowNode)->getFamilyShared();
78+
}
79+
7380
auto thresholds = options.thresholds;
7481
auto rootThresholds = options.rootThresholds;
7582
auto& uiManager = getUIManagerFromRuntime(runtime);
7683

7784
intersectionObserverManager_.observe(
7885
intersectionObserverId,
86+
observationRootShadowNodeFamily,
7987
shadowNodeFamily,
8088
thresholds,
8189
rootThresholds,

packages/react-native/ReactCommon/react/nativemodule/intersectionobserver/NativeIntersectionObserver.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ using NativeIntersectionObserverObserveOptions =
2222
NativeIntersectionObserverNativeIntersectionObserverObserveOptions<
2323
// intersectionObserverId
2424
NativeIntersectionObserverIntersectionObserverId,
25+
// rootShadowNode
26+
jsi::Value,
2527
// targetShadowNode
2628
jsi::Object,
2729
// thresholds

packages/react-native/ReactCommon/react/renderer/observers/intersection/IntersectionObserver.cpp

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,41 @@ namespace facebook::react {
1616

1717
IntersectionObserver::IntersectionObserver(
1818
IntersectionObserverObserverId intersectionObserverId,
19+
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
1920
ShadowNodeFamily::Shared targetShadowNodeFamily,
2021
std::vector<Float> thresholds,
2122
std::optional<std::vector<Float>> rootThresholds)
2223
: intersectionObserverId_(intersectionObserverId),
24+
observationRootShadowNodeFamily_(
25+
std::move(observationRootShadowNodeFamily)),
2326
targetShadowNodeFamily_(std::move(targetShadowNodeFamily)),
2427
thresholds_(std::move(thresholds)),
2528
rootThresholds_(std::move(rootThresholds)) {}
2629

27-
static Rect getRootBoundingRect(
28-
const LayoutableShadowNode& layoutableRootShadowNode) {
29-
auto layoutMetrics = layoutableRootShadowNode.getLayoutMetrics();
30+
static std::shared_ptr<const ShadowNode> getShadowNode(
31+
const ShadowNodeFamily::AncestorList& ancestors) {
32+
if (ancestors.empty()) {
33+
return nullptr;
34+
}
35+
36+
const auto& lastAncestor = ancestors.back();
37+
const ShadowNode& parentNode = lastAncestor.first.get();
38+
int childIndex = lastAncestor.second;
39+
40+
const std::shared_ptr<const ShadowNode>& childNode =
41+
parentNode.getChildren().at(childIndex);
42+
return childNode;
43+
}
44+
45+
static Rect getRootNodeBoundingRect(const RootShadowNode& rootShadowNode) {
46+
const auto layoutableRootShadowNode =
47+
dynamic_cast<const LayoutableShadowNode*>(&rootShadowNode);
48+
49+
react_native_assert(
50+
layoutableRootShadowNode != nullptr &&
51+
"RootShadowNode instances must always inherit from LayoutableShadowNode.");
52+
53+
auto layoutMetrics = layoutableRootShadowNode->getLayoutMetrics();
3054

3155
if (layoutMetrics == EmptyLayoutMetrics ||
3256
layoutMetrics.displayType == DisplayType::None) {
@@ -35,13 +59,12 @@ static Rect getRootBoundingRect(
3559

3660
// Apply the transform to translate the root view to its location in the
3761
// viewport.
38-
return layoutMetrics.frame * layoutableRootShadowNode.getTransform();
62+
return layoutMetrics.frame * layoutableRootShadowNode->getTransform();
3963
}
4064

41-
static Rect getTargetBoundingRect(
42-
const ShadowNodeFamily::AncestorList& targetAncestors) {
65+
static Rect getBoundingRect(const ShadowNodeFamily::AncestorList& ancestors) {
4366
auto layoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics(
44-
targetAncestors,
67+
ancestors,
4568
{/* .includeTransform = */ true,
4669
/* .includeViewportOffset = */ true});
4770
return layoutMetrics == EmptyLayoutMetrics ? Rect{} : layoutMetrics.frame;
@@ -63,7 +86,7 @@ static Rect getClippedTargetBoundingRect(
6386
static Rect computeIntersection(
6487
const Rect& rootBoundingRect,
6588
const Rect& targetBoundingRect,
66-
const ShadowNodeFamily::AncestorList& targetAncestors) {
89+
const ShadowNodeFamily::AncestorList& targetToRootAncestors) {
6790
auto absoluteIntersectionRect =
6891
Rect::intersect(rootBoundingRect, targetBoundingRect);
6992

@@ -79,10 +102,15 @@ static Rect computeIntersection(
79102
return {};
80103
}
81104

82-
// Coordinates of the target after clipping the parts hidden by a parent
83-
// (e.g.: in scroll views, or in views with a parent with overflow: hidden)
84-
auto clippedTargetBoundingRect =
85-
getClippedTargetBoundingRect(targetAncestors);
105+
// Coordinates of the target after clipping the parts hidden by a parent,
106+
// until till the root (e.g.: in scroll views, or in views with a parent with
107+
// overflow: hidden)
108+
auto clippedTargetFromRoot =
109+
getClippedTargetBoundingRect(targetToRootAncestors);
110+
111+
auto clippedTargetBoundingRect = Rect{
112+
rootBoundingRect.origin + clippedTargetFromRoot.origin,
113+
clippedTargetFromRoot.size};
86114

87115
return Rect::intersect(rootBoundingRect, clippedTargetBoundingRect);
88116
}
@@ -105,23 +133,34 @@ std::optional<IntersectionObserverEntry>
105133
IntersectionObserver::updateIntersectionObservation(
106134
const RootShadowNode& rootShadowNode,
107135
double time) {
108-
const auto layoutableRootShadowNode =
109-
dynamic_cast<const LayoutableShadowNode*>(&rootShadowNode);
110-
111-
react_native_assert(
112-
layoutableRootShadowNode != nullptr &&
113-
"RootShadowNode instances must always inherit from LayoutableShadowNode.");
136+
bool hasCustomRoot = observationRootShadowNodeFamily_.has_value();
114137

115-
auto targetAncestors = targetShadowNodeFamily_->getAncestors(rootShadowNode);
138+
auto rootAncestors = hasCustomRoot
139+
? observationRootShadowNodeFamily_.value()->getAncestors(rootShadowNode)
140+
: ShadowNodeFamily::AncestorList{};
116141

117142
// Absolute coordinates of the root
118-
auto rootBoundingRect = getRootBoundingRect(*layoutableRootShadowNode);
143+
auto rootBoundingRect = hasCustomRoot
144+
? getBoundingRect(rootAncestors)
145+
: getRootNodeBoundingRect(rootShadowNode);
146+
147+
auto targetAncestors = targetShadowNodeFamily_->getAncestors(rootShadowNode);
119148

120149
// Absolute coordinates of the target
121-
auto targetBoundingRect = getTargetBoundingRect(targetAncestors);
150+
auto targetBoundingRect = getBoundingRect(targetAncestors);
151+
152+
if ((hasCustomRoot && rootAncestors.empty()) || targetAncestors.empty()) {
153+
// If observation root or target is not a descendant of `rootShadowNode`
154+
return setNotIntersectingState(
155+
rootBoundingRect, targetBoundingRect, {}, time);
156+
}
157+
158+
auto targetToRootAncestors = hasCustomRoot
159+
? targetShadowNodeFamily_->getAncestors(*getShadowNode(rootAncestors))
160+
: targetAncestors;
122161

123162
auto intersectionRect = computeIntersection(
124-
rootBoundingRect, targetBoundingRect, targetAncestors);
163+
rootBoundingRect, targetBoundingRect, targetToRootAncestors);
125164

126165
Float targetBoundingRectArea =
127166
targetBoundingRect.size.width * targetBoundingRect.size.height;

packages/react-native/ReactCommon/react/renderer/observers/intersection/IntersectionObserver.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class IntersectionObserver {
4040
public:
4141
IntersectionObserver(
4242
IntersectionObserverObserverId intersectionObserverId,
43+
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
4344
ShadowNodeFamily::Shared targetShadowNodeFamily,
4445
std::vector<Float> thresholds,
4546
std::optional<std::vector<Float>> rootThresholds = std::nullopt);
@@ -81,6 +82,7 @@ class IntersectionObserver {
8182
double time);
8283

8384
IntersectionObserverObserverId intersectionObserverId_;
85+
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily_;
8486
ShadowNodeFamily::Shared targetShadowNodeFamily_;
8587
std::vector<Float> thresholds_;
8688
std::optional<std::vector<Float>> rootThresholds_;

packages/react-native/ReactCommon/react/renderer/observers/intersection/IntersectionObserverManager.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ IntersectionObserverManager::IntersectionObserverManager() = default;
3939

4040
void IntersectionObserverManager::observe(
4141
IntersectionObserverObserverId intersectionObserverId,
42+
const std::optional<ShadowNodeFamily::Shared>&
43+
observationRootShadowNodeFamily,
4244
const ShadowNodeFamily::Shared& shadowNodeFamily,
4345
std::vector<Float> thresholds,
4446
std::optional<std::vector<Float>> rootThresholds,
@@ -58,6 +60,7 @@ void IntersectionObserverManager::observe(
5860
auto& observers = observersBySurfaceId_[surfaceId];
5961
observers.emplace_back(std::make_unique<IntersectionObserver>(
6062
intersectionObserverId,
63+
observationRootShadowNodeFamily,
6164
shadowNodeFamily,
6265
std::move(thresholds),
6366
std::move(rootThresholds)));

packages/react-native/ReactCommon/react/renderer/observers/intersection/IntersectionObserverManager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class IntersectionObserverManager final
2727

2828
void observe(
2929
IntersectionObserverObserverId intersectionObserverId,
30+
const std::optional<ShadowNodeFamily::Shared>& observationRootShadowNode,
3031
const ShadowNodeFamily::Shared& shadowNode,
3132
std::vector<Float> thresholds,
3233
std::optional<std::vector<Float>> rootThresholds,

packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserver.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type IntersectionObserverCallback = (
2323
) => mixed;
2424

2525
export interface IntersectionObserverInit {
26-
// root?: ReactNativeElement, // This option exists on the Web but it's not currently supported in React Native.
26+
root?: ?ReactNativeElement;
2727
// rootMargin?: string, // This option exists on the Web but it's not currently supported in React Native.
2828
threshold?: number | $ReadOnlyArray<number>;
2929

@@ -67,6 +67,7 @@ export default class IntersectionObserver {
6767
_observationTargets: Set<ReactNativeElement> = new Set();
6868
_intersectionObserverId: ?IntersectionObserverId;
6969
_rootThresholds: $ReadOnlyArray<number> | null;
70+
_root: ReactNativeElement | null;
7071

7172
constructor(
7273
callback: IntersectionObserverCallback,
@@ -85,16 +86,18 @@ export default class IntersectionObserver {
8586
}
8687

8788
// $FlowExpectedError[prop-missing] it's not typed in React Native but exists on Web.
88-
if (options?.root != null) {
89+
if (options?.rootMargin != null) {
8990
throw new TypeError(
90-
"Failed to construct 'IntersectionObserver': root is not supported",
91+
"Failed to construct 'IntersectionObserver': rootMargin is not supported",
9192
);
9293
}
9394

94-
// $FlowExpectedError[prop-missing] it's not typed in React Native but exists on Web.
95-
if (options?.rootMargin != null) {
95+
if (
96+
options?.root != null &&
97+
!(options?.root instanceof ReactNativeElement)
98+
) {
9699
throw new TypeError(
97-
"Failed to construct 'IntersectionObserver': rootMargin is not supported",
100+
"Failed to construct 'IntersectionObserver': Failed to read the 'root' property from 'IntersectionObserverInit': The provided value is not of type '(null or ReactNativeElement)",
98101
);
99102
}
100103

@@ -105,6 +108,7 @@ export default class IntersectionObserver {
105108
options?.threshold,
106109
this._rootThresholds != null, // only provide default if no rootThreshold
107110
);
111+
this._root = options?.root ?? null;
108112
}
109113

110114
/**
@@ -116,7 +120,7 @@ export default class IntersectionObserver {
116120
* NOTE: This cannot currently be configured and `root` is always `null`.
117121
*/
118122
get root(): ReactNativeElement | null {
119-
return null;
123+
return this._root;
120124
}
121125

122126
/**
@@ -183,6 +187,7 @@ export default class IntersectionObserver {
183187

184188
const didStartObserving = IntersectionObserverManager.observe({
185189
intersectionObserverId: this._getOrCreateIntersectionObserverId(),
190+
root: this._root,
186191
target,
187192
});
188193

0 commit comments

Comments
 (0)