Revert D65085733 (#51520)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/51520
Changelog: [Internal] - revert Intersection Observer `root` due to bug

Reviewed By: lunaleaps

Differential Revision: D75186289

fbshipit-source-id: d644b1ef3261996596af6a243076f7172d500167
This commit is contained in:
generatedunixname89002005232357
2025-05-21 21:08:11 -07:00
committed by Facebook GitHub Bot
parent 50ca7e5730
commit 089621ebbe
12 changed files with 48 additions and 758 deletions
@@ -9607,7 +9607,6 @@ exports[`public API should not change unintentionally src/private/webapis/inters
observer: IntersectionObserver
) => mixed;
export interface IntersectionObserverInit {
root?: ?ReactNativeElement;
threshold?: number | $ReadOnlyArray<number>;
rnRootThreshold?: number | $ReadOnlyArray<number>;
}
@@ -70,20 +70,12 @@ jsi::Object NativeIntersectionObserver::observeV2(
auto shadowNode =
shadowNodeFromValue(runtime, std::move(options.targetShadowNode));
auto shadowNodeFamily = shadowNode->getFamilyShared();
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily;
if (options.rootShadowNode.isObject()) {
observationRootShadowNodeFamily =
shadowNodeFromValue(runtime, options.rootShadowNode)->getFamilyShared();
}
auto thresholds = options.thresholds;
auto rootThresholds = options.rootThresholds;
auto& uiManager = getUIManagerFromRuntime(runtime);
intersectionObserverManager_.observe(
intersectionObserverId,
observationRootShadowNodeFamily,
shadowNodeFamily,
thresholds,
rootThresholds,
@@ -22,8 +22,6 @@ using NativeIntersectionObserverObserveOptions =
NativeIntersectionObserverNativeIntersectionObserverObserveOptions<
// intersectionObserverId
NativeIntersectionObserverIntersectionObserverId,
// rootShadowNode
jsi::Value,
// targetShadowNode
jsi::Object,
// thresholds
@@ -16,41 +16,17 @@ namespace facebook::react {
IntersectionObserver::IntersectionObserver(
IntersectionObserverObserverId intersectionObserverId,
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
ShadowNodeFamily::Shared targetShadowNodeFamily,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds)
: intersectionObserverId_(intersectionObserverId),
observationRootShadowNodeFamily_(
std::move(observationRootShadowNodeFamily)),
targetShadowNodeFamily_(std::move(targetShadowNodeFamily)),
thresholds_(std::move(thresholds)),
rootThresholds_(std::move(rootThresholds)) {}
static std::shared_ptr<const ShadowNode> getShadowNode(
const ShadowNodeFamily::AncestorList& ancestors) {
if (ancestors.empty()) {
return nullptr;
}
const auto& lastAncestor = ancestors.back();
const ShadowNode& parentNode = lastAncestor.first.get();
int childIndex = lastAncestor.second;
const std::shared_ptr<const ShadowNode>& childNode =
parentNode.getChildren().at(childIndex);
return childNode;
}
static Rect getRootNodeBoundingRect(const RootShadowNode& rootShadowNode) {
const auto layoutableRootShadowNode =
dynamic_cast<const LayoutableShadowNode*>(&rootShadowNode);
react_native_assert(
layoutableRootShadowNode != nullptr &&
"RootShadowNode instances must always inherit from LayoutableShadowNode.");
auto layoutMetrics = layoutableRootShadowNode->getLayoutMetrics();
static Rect getRootBoundingRect(
const LayoutableShadowNode& layoutableRootShadowNode) {
auto layoutMetrics = layoutableRootShadowNode.getLayoutMetrics();
if (layoutMetrics == EmptyLayoutMetrics ||
layoutMetrics.displayType == DisplayType::None) {
@@ -59,12 +35,13 @@ static Rect getRootNodeBoundingRect(const RootShadowNode& rootShadowNode) {
// Apply the transform to translate the root view to its location in the
// viewport.
return layoutMetrics.frame * layoutableRootShadowNode->getTransform();
return layoutMetrics.frame * layoutableRootShadowNode.getTransform();
}
static Rect getBoundingRect(const ShadowNodeFamily::AncestorList& ancestors) {
static Rect getTargetBoundingRect(
const ShadowNodeFamily::AncestorList& targetAncestors) {
auto layoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics(
ancestors,
targetAncestors,
{/* .includeTransform = */ true,
/* .includeViewportOffset = */ true});
return layoutMetrics == EmptyLayoutMetrics ? Rect{} : layoutMetrics.frame;
@@ -86,7 +63,7 @@ static Rect getClippedTargetBoundingRect(
static Rect computeIntersection(
const Rect& rootBoundingRect,
const Rect& targetBoundingRect,
const ShadowNodeFamily::AncestorList& targetToRootAncestors) {
const ShadowNodeFamily::AncestorList& targetAncestors) {
auto absoluteIntersectionRect =
Rect::intersect(rootBoundingRect, targetBoundingRect);
@@ -102,15 +79,10 @@ static Rect computeIntersection(
return {};
}
// Coordinates of the target after clipping the parts hidden by a parent,
// until till the root (e.g.: in scroll views, or in views with a parent with
// overflow: hidden)
auto clippedTargetFromRoot =
getClippedTargetBoundingRect(targetToRootAncestors);
auto clippedTargetBoundingRect = Rect{
rootBoundingRect.origin + clippedTargetFromRoot.origin,
clippedTargetFromRoot.size};
// Coordinates of the target after clipping the parts hidden by a parent
// (e.g.: in scroll views, or in views with a parent with overflow: hidden)
auto clippedTargetBoundingRect =
getClippedTargetBoundingRect(targetAncestors);
return Rect::intersect(rootBoundingRect, clippedTargetBoundingRect);
}
@@ -133,34 +105,23 @@ std::optional<IntersectionObserverEntry>
IntersectionObserver::updateIntersectionObservation(
const RootShadowNode& rootShadowNode,
double time) {
bool hasCustomRoot = observationRootShadowNodeFamily_.has_value();
const auto layoutableRootShadowNode =
dynamic_cast<const LayoutableShadowNode*>(&rootShadowNode);
auto rootAncestors = hasCustomRoot
? observationRootShadowNodeFamily_.value()->getAncestors(rootShadowNode)
: ShadowNodeFamily::AncestorList{};
// Absolute coordinates of the root
auto rootBoundingRect = hasCustomRoot
? getBoundingRect(rootAncestors)
: getRootNodeBoundingRect(rootShadowNode);
react_native_assert(
layoutableRootShadowNode != nullptr &&
"RootShadowNode instances must always inherit from LayoutableShadowNode.");
auto targetAncestors = targetShadowNodeFamily_->getAncestors(rootShadowNode);
// Absolute coordinates of the root
auto rootBoundingRect = getRootBoundingRect(*layoutableRootShadowNode);
// Absolute coordinates of the target
auto targetBoundingRect = getBoundingRect(targetAncestors);
if ((hasCustomRoot && rootAncestors.empty()) || targetAncestors.empty()) {
// If observation root or target is not a descendant of `rootShadowNode`
return setNotIntersectingState(
rootBoundingRect, targetBoundingRect, {}, time);
}
auto targetToRootAncestors = hasCustomRoot
? targetShadowNodeFamily_->getAncestors(*getShadowNode(rootAncestors))
: targetAncestors;
auto targetBoundingRect = getTargetBoundingRect(targetAncestors);
auto intersectionRect = computeIntersection(
rootBoundingRect, targetBoundingRect, targetToRootAncestors);
rootBoundingRect, targetBoundingRect, targetAncestors);
Float targetBoundingRectArea =
targetBoundingRect.size.width * targetBoundingRect.size.height;
@@ -40,7 +40,6 @@ class IntersectionObserver {
public:
IntersectionObserver(
IntersectionObserverObserverId intersectionObserverId,
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
ShadowNodeFamily::Shared targetShadowNodeFamily,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds = std::nullopt);
@@ -82,7 +81,6 @@ class IntersectionObserver {
double time);
IntersectionObserverObserverId intersectionObserverId_;
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily_;
ShadowNodeFamily::Shared targetShadowNodeFamily_;
std::vector<Float> thresholds_;
std::optional<std::vector<Float>> rootThresholds_;
@@ -39,8 +39,6 @@ IntersectionObserverManager::IntersectionObserverManager() = default;
void IntersectionObserverManager::observe(
IntersectionObserverObserverId intersectionObserverId,
const std::optional<ShadowNodeFamily::Shared>&
observationRootShadowNodeFamily,
const ShadowNodeFamily::Shared& shadowNodeFamily,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
@@ -60,7 +58,6 @@ void IntersectionObserverManager::observe(
auto& observers = observersBySurfaceId_[surfaceId];
observers.emplace_back(std::make_unique<IntersectionObserver>(
intersectionObserverId,
observationRootShadowNodeFamily,
shadowNodeFamily,
std::move(thresholds),
std::move(rootThresholds)));
@@ -27,7 +27,6 @@ class IntersectionObserverManager final
void observe(
IntersectionObserverObserverId intersectionObserverId,
const std::optional<ShadowNodeFamily::Shared>& observationRootShadowNode,
const ShadowNodeFamily::Shared& shadowNode,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
@@ -23,7 +23,7 @@ export type IntersectionObserverCallback = (
) => mixed;
export interface IntersectionObserverInit {
root?: ?ReactNativeElement;
// root?: ReactNativeElement, // This option exists on the Web but it's not currently supported in React Native.
// rootMargin?: string, // This option exists on the Web but it's not currently supported in React Native.
threshold?: number | $ReadOnlyArray<number>;
@@ -67,7 +67,6 @@ export default class IntersectionObserver {
_observationTargets: Set<ReactNativeElement> = new Set();
_intersectionObserverId: ?IntersectionObserverId;
_rootThresholds: $ReadOnlyArray<number> | null;
_root: ReactNativeElement | null;
constructor(
callback: IntersectionObserverCallback,
@@ -86,18 +85,16 @@ export default class IntersectionObserver {
}
// $FlowExpectedError[prop-missing] it's not typed in React Native but exists on Web.
if (options?.rootMargin != null) {
if (options?.root != null) {
throw new TypeError(
"Failed to construct 'IntersectionObserver': rootMargin is not supported",
"Failed to construct 'IntersectionObserver': root is not supported",
);
}
if (
options?.root != null &&
!(options?.root instanceof ReactNativeElement)
) {
// $FlowExpectedError[prop-missing] it's not typed in React Native but exists on Web.
if (options?.rootMargin != null) {
throw new TypeError(
"Failed to construct 'IntersectionObserver': Failed to read the 'root' property from 'IntersectionObserverInit': The provided value is not of type '(null or ReactNativeElement)",
"Failed to construct 'IntersectionObserver': rootMargin is not supported",
);
}
@@ -108,7 +105,6 @@ export default class IntersectionObserver {
options?.threshold,
this._rootThresholds != null, // only provide default if no rootThreshold
);
this._root = options?.root ?? null;
}
/**
@@ -120,7 +116,7 @@ export default class IntersectionObserver {
* NOTE: This cannot currently be configured and `root` is always `null`.
*/
get root(): ReactNativeElement | null {
return this._root;
return null;
}
/**
@@ -187,7 +183,6 @@ export default class IntersectionObserver {
const didStartObserving = IntersectionObserverManager.observe({
intersectionObserverId: this._getOrCreateIntersectionObserverId(),
root: this._root,
target,
});
@@ -30,8 +30,7 @@ setUpIntersectionObserver();
const nodeRef = createRef<HostInstance>();
let node: ReactNativeElement;
const rootRef = createRef<HostInstance>();
let rootNode: ReactNativeElement;
const scrollViewRef = createRef<HostInstance>();
let scrollViewNode: ReactNativeElement;
let observer: IntersectionObserverType;
@@ -97,27 +96,6 @@ Fantom.unstable_benchmark
},
},
)
.test(
'Create IntersectionObserver with custom root',
() => {
Fantom.runTask(() => {
observer = new IntersectionObserver(mockCallback, {root: rootNode});
});
},
{
beforeEach: () => {
mockCallback = jest.fn();
Fantom.runTask(() => {
root.render(<View ref={rootRef} />);
});
rootNode = ensureInstance(rootRef.current, ReactNativeElement);
},
afterEach: () => {
expect(mockCallback).not.toHaveBeenCalled();
cleanup(root, observer);
},
},
)
.test(
'Observe a mounted view',
() => {
@@ -144,39 +122,6 @@ Fantom.unstable_benchmark
},
},
)
.test(
'Observe a mounted view with custom root',
() => {
Fantom.runTask(() => {
observer.observe(node);
});
},
{
beforeEach: () => {
mockCallback = jest.fn();
Fantom.runTask(() => {
root.render(
<View ref={rootRef}>
<View style={{width: 100, height: 10}} ref={nodeRef} />
</View>,
);
});
node = ensureInstance(nodeRef.current, ReactNativeElement);
rootNode = ensureInstance(rootRef.current, ReactNativeElement);
Fantom.runTask(() => {
observer = new IntersectionObserver(mockCallback, {root: rootNode});
});
},
afterEach: () => {
expect(mockCallback).toHaveBeenCalledTimes(1);
const [entries] = mockCallback.mock.lastCall;
expect(entries.length).toBe(1);
expect(entries[0].isIntersecting).toBe(true);
cleanup(root, observer);
},
},
)
.test(
'ScrollView no intersection, no observation',
() => {
@@ -271,48 +216,6 @@ Fantom.unstable_benchmark
},
},
)
.test(
'ScrollView no intersection, observation with custom root',
() => {
scrollBy1(scrollViewNode, VIEWPORT_HEIGHT);
},
{
beforeEach: () => {
mockCallback = jest.fn();
Fantom.runTask(() => {
root.render(
<ScrollView ref={scrollViewRef}>
{renderElementAtYScrollPosition(
VIEWPORT_HEIGHT + 50,
<View ref={nodeRef} style={{width: 100, height: 10}} />,
)}
</ScrollView>,
);
});
scrollViewNode = ensureInstance(
scrollViewRef.current,
ReactNativeElement,
);
node = ensureInstance(nodeRef.current, ReactNativeElement);
Fantom.runTask(() => {
observer = new IntersectionObserver(mockCallback, {
root: scrollViewNode,
});
observer.observe(node);
});
},
afterEach: () => {
expect(mockCallback).toHaveBeenCalledTimes(1);
const [entries] = mockCallback.mock.lastCall;
expect(entries.length).toBe(1);
expect(entries[0].isIntersecting).toBe(false);
cleanup(root, observer);
},
},
)
.test(
'ScrollView intersection, observation',
() => {
@@ -357,57 +260,6 @@ Fantom.unstable_benchmark
expect(entries3.length).toBe(1);
expect(entries3[0].isIntersecting).toBe(false);
cleanup(root, observer);
},
},
)
.test(
'ScrollView intersection, observation, with custom root',
() => {
scrollBy1(scrollViewNode, VIEWPORT_HEIGHT);
},
{
beforeEach: () => {
mockCallback = jest.fn();
Fantom.runTask(() => {
root.render(
<ScrollView ref={scrollViewRef}>
{renderElementAtYScrollPosition(
-5,
<View ref={nodeRef} style={{width: 100, height: 10}} />,
)}
</ScrollView>,
);
});
scrollViewNode = ensureInstance(
scrollViewRef.current,
ReactNativeElement,
);
node = ensureInstance(nodeRef.current, ReactNativeElement);
Fantom.runTask(() => {
observer = new IntersectionObserver(mockCallback, {
threshold: 1,
root: scrollViewNode,
});
observer.observe(node);
});
},
afterEach: () => {
expect(mockCallback).toHaveBeenCalledTimes(3);
const [entries1] = mockCallback.mock.calls[0];
expect(entries1.length).toBe(1);
expect(entries1[0].isIntersecting).toBe(false);
const [entries2] = mockCallback.mock.calls[1];
expect(entries2.length).toBe(1);
expect(entries2[0].isIntersecting).toBe(true);
const [entries3] = mockCallback.mock.calls[2];
expect(entries3.length).toBe(1);
expect(entries3[0].isIntersecting).toBe(false);
cleanup(root, observer);
},
},
@@ -90,6 +90,24 @@ describe('IntersectionObserver', () => {
);
});
it('should throw if `root` is provided', () => {
const nodeRef = createRef<HostInstance>();
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<View ref={nodeRef} />);
});
const node = ensureReactNativeElement(nodeRef.current);
expect(() => {
// $FlowExpectedError[prop-missing] root is not even defined in Flow.
return new IntersectionObserver(() => {}, {root: node});
}).toThrow(
"Failed to construct 'IntersectionObserver': root is not supported",
);
});
it('should throw if `rootMargin` is provided', () => {
expect(() => {
// $FlowExpectedError[prop-missing] rootMargin is not even defined in Flow.
@@ -122,32 +140,6 @@ describe('IntersectionObserver', () => {
);
});
it('should throw if `root` is not a `ReactNativeElement`', () => {
expect(() => {
// $FlowExpectedError[incompatible-call]
observer = new IntersectionObserver(() => {}, {root: 'something'});
}).toThrow(
"Failed to construct 'IntersectionObserver': Failed to read the 'root' property from 'IntersectionObserverInit': The provided value is not of type '(null or ReactNativeElement)",
);
});
it('should provide access to custom `root`', () => {
const rootRef = React.createRef<HostInstance>();
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<View ref={rootRef} />);
});
const rootNode = ensureReactNativeElement(rootRef.current);
Fantom.runTask(() => {
observer = new IntersectionObserver(() => {}, {root: rootNode});
});
expect(observer?.root).toBe(rootNode);
});
it('should provide access to `root`, `rootMargin` and `thresholds`', () => {
observer = new IntersectionObserver(() => {});
@@ -854,352 +846,6 @@ describe('IntersectionObserver', () => {
});
});
describe('with custom root', () => {
it('should report partial non-intersecting initial state correctly', () => {
const nodeRef = createRef<HostInstance>();
const scrollNodeRef = createRef<HostInstance>();
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
Fantom.runTask(() => {
root.render(
<ScrollView style={{width: 100}} ref={scrollNodeRef}>
<View style={{width: 50, height: 50}} ref={nodeRef} />
</ScrollView>,
);
});
const scrollNode = ensureReactNativeElement(scrollNodeRef.current);
const node = ensureReactNativeElement(nodeRef.current);
// Scroll such that View is partially intersecting
Fantom.scrollTo(scrollNode, {x: 0, y: 25});
const intersectionObserverCallback = jest.fn();
Fantom.runTask(() => {
observer = new IntersectionObserver(intersectionObserverCallback, {
threshold: [1],
root: scrollNode,
});
observer.observe(node);
});
expect(intersectionObserverCallback).toHaveBeenCalledTimes(1);
const [entries, reportedObserver] =
intersectionObserverCallback.mock.lastCall;
expect(reportedObserver).toBe(observer);
expect(entries.length).toBe(1);
expect(entries[0]).toBeInstanceOf(IntersectionObserverEntry);
expect(entries[0].intersectionRatio).toBe(0.5);
// intersectionRectArea / rootboundsArea
expect(entries[0].rnRootIntersectionRatio).toBe(0.0125);
expect(entries[0].isIntersecting).toBe(false);
expect(entries[0].target).toBe(node);
expectRectEquals(entries[0].intersectionRect, {
x: 0,
y: 0,
width: 50,
height: 25,
});
expectRectEquals(entries[0].boundingClientRect, {
x: 0,
y: -25,
width: 50,
height: 50,
});
// Expect this to be width of the viewport
expectRectEquals(entries[0].rootBounds, {
x: 0,
y: 0,
width: 100,
height: 1000,
});
});
it('should report partial intersecting initial state correctly', () => {
const nodeRef = createRef<HostInstance>();
const scrollNodeRef = createRef<HostInstance>();
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
Fantom.runTask(() => {
root.render(
<ScrollView style={{width: 100}} ref={scrollNodeRef}>
<View style={{width: 50, height: 50}} ref={nodeRef} />
</ScrollView>,
);
});
const scrollNode = ensureReactNativeElement(scrollNodeRef.current);
const node = ensureReactNativeElement(nodeRef.current);
// Scroll such that View is partially intersecting
Fantom.scrollTo(scrollNode, {x: 0, y: 25});
const intersectionObserverCallback = jest.fn();
Fantom.runTask(() => {
observer = new IntersectionObserver(intersectionObserverCallback, {
threshold: [],
root: scrollNode,
});
observer.observe(node);
});
expect(intersectionObserverCallback).toHaveBeenCalledTimes(1);
const [entries, reportedObserver] =
intersectionObserverCallback.mock.lastCall;
expect(reportedObserver).toBe(observer);
expect(entries.length).toBe(1);
expect(entries[0]).toBeInstanceOf(IntersectionObserverEntry);
expect(entries[0].intersectionRatio).toBe(0.5);
expect(entries[0].rnRootIntersectionRatio).toBe(0.0125);
expect(entries[0].isIntersecting).toBe(true);
expect(entries[0].target).toBe(node);
expectRectEquals(entries[0].intersectionRect, {
x: 0,
y: 0,
width: 50,
height: 25,
});
expectRectEquals(entries[0].boundingClientRect, {
x: 0,
y: -25,
width: 50,
height: 50,
});
expectRectEquals(entries[0].rootBounds, {
x: 0,
y: 0,
width: 100,
height: 1000,
});
});
it('should report subsequent updates correctly', () => {
const nodeRef = createRef<HostInstance>();
const scrollNodeRef = createRef<HostInstance>();
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
Fantom.runTask(() => {
root.render(
<ScrollView style={{width: 100}} ref={scrollNodeRef}>
<View style={{width: 100, height: 100}} ref={nodeRef} />
</ScrollView>,
);
});
const node = ensureReactNativeElement(nodeRef.current);
const scrollNode = ensureReactNativeElement(scrollNodeRef.current);
expect(node.isConnected).toBe(true);
const intersectionObserverCallback = jest.fn();
Fantom.runTask(() => {
observer = new IntersectionObserver(intersectionObserverCallback, {
root: scrollNode,
});
observer.observe(node);
});
expect(intersectionObserverCallback).toHaveBeenCalledTimes(1);
const [entries, reportedObserver] =
intersectionObserverCallback.mock.lastCall;
expect(entries.length).toBe(1);
expect(entries[0]).toBeInstanceOf(IntersectionObserverEntry);
expect(entries[0].intersectionRatio).toBe(1);
expect(entries[0].rnRootIntersectionRatio).toBe(0.1);
expect(entries[0].isIntersecting).toBe(true);
expectRectEquals(entries[0].intersectionRect, {
x: 0,
y: 0,
width: 100,
height: 100,
});
expectRectEquals(entries[0].boundingClientRect, {
x: 0,
y: 0,
width: 100,
height: 100,
});
expectRectEquals(entries[0].rootBounds, {
x: 0,
y: 0,
width: 100,
height: 1000,
});
expect(reportedObserver).toBe(observer);
// Move the view out of the viewport
Fantom.scrollTo(scrollNode, {x: 0, y: 200});
expect(node.isConnected).toBe(true);
expect(intersectionObserverCallback).toHaveBeenCalledTimes(2);
const [entries2, reportedObserver2] =
intersectionObserverCallback.mock.lastCall;
expect(entries2.length).toBe(1);
expect(entries2[0].isIntersecting).toBe(false);
expect(entries2[0].target).toBe(node);
expectRectEquals(entries2[0].intersectionRect, {
x: 0,
y: 0,
width: 0,
height: 0,
});
expectRectEquals(entries2[0].boundingClientRect, {
x: 0,
y: -200,
width: 100,
height: 100,
});
expectRectEquals(entries2[0].rootBounds, {
x: 0,
y: 0,
width: 100,
height: 1000,
});
expect(reportedObserver2).toBe(observer);
});
it('should report updates to the right observers', () => {
let maybeNode1;
let maybeNode2;
const scrollNodeRef = createRef<HostInstance>();
let observer1: IntersectionObserver;
let observer2: IntersectionObserver;
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
Fantom.runTask(() => {
root.render(
<ScrollView ref={scrollNodeRef}>
<View
style={{width: 50, height: 50}}
ref={receivedNode => {
maybeNode1 = receivedNode;
}}
/>
<View
style={{width: 200, height: 200}}
ref={receivedNode => {
maybeNode2 = receivedNode;
}}
/>
</ScrollView>,
);
});
const node1 = ensureReactNativeElement(maybeNode1);
const node2 = ensureReactNativeElement(maybeNode2);
const scrollNode = ensureReactNativeElement(scrollNodeRef.current);
// Scroll such that node1 is not intersecting and node 2 is intersecting
Fantom.scrollTo(scrollNode, {x: 0, y: 100});
const intersectionObserverCallback1 = jest.fn();
const intersectionObserverCallback2 = jest.fn();
Fantom.runTask(() => {
observer1 = new IntersectionObserver(intersectionObserverCallback1, {
threshold: [0],
});
observer1.observe(node1);
observer1.observe(node2);
observer2 = new IntersectionObserver(intersectionObserverCallback2, {
threshold: [1],
});
observer2.observe(node2);
});
// Verify observer1 is reporting right thing
expect(intersectionObserverCallback1).toHaveBeenCalledTimes(1);
const [entries1, reportedObserver1] =
intersectionObserverCallback1.mock.lastCall;
expect(reportedObserver1).toBe(observer1);
expect(entries1.length).toBe(2);
expect(entries1[0].isIntersecting).toBe(false);
expect(entries1[0].intersectionRatio).toBe(0);
expect(entries1[0].target).toBe(node1);
expectRectEquals(entries1[0].intersectionRect, {
x: 0,
y: 0,
width: 0,
height: 0,
});
expectRectEquals(entries1[0].boundingClientRect, {
x: 0,
y: -100,
width: 50,
height: 50,
});
expectRectEquals(entries1[0].rootBounds, {
x: 0,
y: 0,
width: 1000,
height: 1000,
});
expect(entries1[1].isIntersecting).toBe(true);
expect(entries1[1].intersectionRatio).toBe(0.75);
expect(entries1[1].target).toBe(node2);
// Verify observer2 is reporting no intersection because the threshold is 1
expect(intersectionObserverCallback2).toHaveBeenCalledTimes(1);
const [entries2, reportedObserver2] =
intersectionObserverCallback2.mock.lastCall;
expect(reportedObserver2).toBe(observer2);
expect(entries2.length).toBe(1);
expect(entries2[0].isIntersecting).toBe(false);
expect(entries2[0].intersectionRatio).toBe(0.75);
expect(entries2[0].target).toBe(node2);
expectRectEquals(entries2[0].intersectionRect, {
x: 0,
y: 0,
width: 200,
height: 150,
});
expectRectEquals(entries2[0].boundingClientRect, {
x: 0,
y: -50,
width: 200,
height: 200,
});
expectRectEquals(entries2[0].rootBounds, {
x: 0,
y: 0,
width: 1000,
height: 1000,
});
});
});
it('should not retain initial children of observed targets', () => {
const root = Fantom.createRoot();
observer = new IntersectionObserver(() => {});
@@ -1576,139 +1222,6 @@ describe('IntersectionObserver', () => {
});
});
describe('clipping behavior', () => {
it('should report intersection for clipping ancestor', () => {
const nodeRef = React.createRef<HostInstance>();
const rootRef = React.createRef<HostInstance>();
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
Fantom.runTask(() => {
root.render(
<View style={{width: 99, height: 99, overflow: 'hidden'}}>
<View ref={rootRef} style={{width: 200, height: 200}}>
<View
style={{width: 100, height: 100, marginTop: 100}}
ref={nodeRef}
/>
</View>
,
</View>,
);
});
const node = ensureReactNativeElement(nodeRef.current);
const rootNode = ensureReactNativeElement(rootRef.current);
const intersectionObserverCallback = jest.fn();
Fantom.runTask(() => {
observer = new IntersectionObserver(intersectionObserverCallback, {
root: rootNode,
});
observer.observe(node);
});
expect(intersectionObserverCallback).toHaveBeenCalledTimes(1);
const [entries, reportedObserver] =
intersectionObserverCallback.mock.lastCall;
expect(reportedObserver).toBe(observer);
expect(entries.length).toBe(1);
expect(entries[0]).toBeInstanceOf(IntersectionObserverEntry);
expect(entries[0].intersectionRatio).toBe(1);
// This is the ratio of intersection area / (custom) root area
expect(entries[0].rnRootIntersectionRatio).toBe(0.25);
expect(entries[0].isIntersecting).toBe(true);
expect(entries[0].target).toBe(node);
expectRectEquals(entries[0].intersectionRect, {
x: 0,
y: 100,
width: 100,
height: 100,
});
expectRectEquals(entries[0].boundingClientRect, {
x: 0,
y: 100,
width: 100,
height: 100,
});
expectRectEquals(entries[0].rootBounds, {
x: 0,
y: 0,
width: 200,
height: 200,
});
});
it('should report intersection for clipping root', () => {
const nodeRef = React.createRef<HostInstance>();
const rootRef = React.createRef<HostInstance>();
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
Fantom.runTask(() => {
root.render(
<View
ref={rootRef}
style={{width: 100, height: 100, overflow: 'hidden'}}>
<View
style={{width: 100, height: 100, marginTop: 50}}
ref={nodeRef}
/>
</View>,
);
});
const node = ensureReactNativeElement(nodeRef.current);
const rootNode = ensureReactNativeElement(rootRef.current);
const intersectionObserverCallback = jest.fn();
Fantom.runTask(() => {
observer = new IntersectionObserver(intersectionObserverCallback, {
root: rootNode,
});
observer.observe(node);
});
expect(intersectionObserverCallback).toHaveBeenCalledTimes(1);
const [entries, reportedObserver] =
intersectionObserverCallback.mock.lastCall;
expect(reportedObserver).toBe(observer);
expect(entries.length).toBe(1);
expect(entries[0]).toBeInstanceOf(IntersectionObserverEntry);
expect(entries[0].intersectionRatio).toBe(0.5);
// This is the ratio of intersection area / (custom) root area
expect(entries[0].rnRootIntersectionRatio).toBe(0.5);
expect(entries[0].isIntersecting).toBe(true);
expect(entries[0].target).toBe(node);
expectRectEquals(entries[0].intersectionRect, {
x: 0,
y: 50,
width: 100,
height: 50,
});
expectRectEquals(entries[0].boundingClientRect, {
x: 0,
y: 50,
width: 100,
height: 100,
});
expectRectEquals(entries[0].rootBounds, {
x: 0,
y: 0,
width: 100,
height: 100,
});
});
});
describe('unobserve(target)', () => {
it('should throw if `target` is not a `ReactNativeElement`', () => {
observer = new IntersectionObserver(() => {});
@@ -137,11 +137,9 @@ export function unregisterObserver(
*/
export function observe({
intersectionObserverId,
root,
target,
}: {
intersectionObserverId: IntersectionObserverId,
root: ?ReactNativeElement,
target: ReactNativeElement,
}): boolean {
if (NativeIntersectionObserver == null) {
@@ -173,15 +171,6 @@ export function observe({
return false;
}
const rootNativeNodeReference =
root != null ? getNativeNodeReference(root) : null;
if (root != null && rootNativeNodeReference == null) {
console.error(
'IntersectionObserverManager: could not find shadow node for observation root',
);
return false;
}
// Store the mapping between the instance handle and the target so we can
// access it even after the instance handle has been unmounted.
setTargetForInstanceHandle(instanceHandle, target);
@@ -199,7 +188,6 @@ export function observe({
if (modernNativeIntersectionObserver == null) {
NativeIntersectionObserver.observe({
intersectionObserverId,
rootShadowNode: rootNativeNodeReference,
targetShadowNode: targetNativeNodeReference,
thresholds: registeredObserver.observer.thresholds,
rootThresholds: registeredObserver.observer.rnRootThresholds,
@@ -207,7 +195,6 @@ export function observe({
} else {
const token = modernNativeIntersectionObserver.observe({
intersectionObserverId,
rootShadowNode: rootNativeNodeReference,
targetShadowNode: targetNativeNodeReference,
thresholds: registeredObserver.observer.thresholds,
rootThresholds: registeredObserver.observer.rnRootThresholds,
@@ -25,7 +25,6 @@ export type NativeIntersectionObserverEntry = {
export type NativeIntersectionObserverObserveOptions = {
intersectionObserverId: number,
rootShadowNode?: ?mixed,
targetShadowNode: mixed,
thresholds: $ReadOnlyArray<number>,
rootThresholds?: ?$ReadOnlyArray<number>,