mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
50ca7e5730
commit
089621ebbe
@@ -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>;
|
||||
}
|
||||
|
||||
-8
@@ -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,
|
||||
|
||||
-2
@@ -22,8 +22,6 @@ using NativeIntersectionObserverObserveOptions =
|
||||
NativeIntersectionObserverNativeIntersectionObserverObserveOptions<
|
||||
// intersectionObserverId
|
||||
NativeIntersectionObserverIntersectionObserverId,
|
||||
// rootShadowNode
|
||||
jsi::Value,
|
||||
// targetShadowNode
|
||||
jsi::Object,
|
||||
// thresholds
|
||||
|
||||
+22
-61
@@ -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;
|
||||
|
||||
-2
@@ -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_;
|
||||
|
||||
-3
@@ -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)));
|
||||
|
||||
-1
@@ -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,
|
||||
|
||||
+7
-12
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+1
-149
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
+18
-505
@@ -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(() => {});
|
||||
|
||||
-13
@@ -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,
|
||||
|
||||
Vendored
-1
@@ -25,7 +25,6 @@ export type NativeIntersectionObserverEntry = {
|
||||
|
||||
export type NativeIntersectionObserverObserveOptions = {
|
||||
intersectionObserverId: number,
|
||||
rootShadowNode?: ?mixed,
|
||||
targetShadowNode: mixed,
|
||||
thresholds: $ReadOnlyArray<number>,
|
||||
rootThresholds?: ?$ReadOnlyArray<number>,
|
||||
|
||||
Reference in New Issue
Block a user