Skip to content

Commit

Permalink
Attempt to introduce root
Browse files Browse the repository at this point in the history
Differential Revision: D65085733
  • Loading branch information
lunaleaps authored and facebook-github-bot committed Nov 4, 2024
1 parent 35f0e1c commit c0f2fde
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@ void NativeIntersectionObserver::observe(
auto intersectionObserverId = options.intersectionObserverId;
auto shadowNode =
shadowNodeFromValue(runtime, std::move(options.targetShadowNode));
auto observationRootShadowNode = options.rootShadowNode.isObject()
? shadowNodeFromValue(runtime, std::move(options.rootShadowNode))
: nullptr;
auto thresholds = options.thresholds;
auto& uiManager = getUIManagerFromRuntime(runtime);

intersectionObserverManager_.observe(
intersectionObserverId, shadowNode, thresholds, uiManager);
intersectionObserverId,
observationRootShadowNode,
shadowNode,
thresholds,
uiManager);
}

void NativeIntersectionObserver::unobserve(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ using NativeIntersectionObserverObserveOptions =
// targetShadowNode
jsi::Object,
// thresholds
std::vector<Float>>;
std::vector<Float>,
// rootShadowNode
jsi::Value>;

template <>
struct Bridging<NativeIntersectionObserverObserveOptions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,23 @@ namespace facebook::react {

IntersectionObserver::IntersectionObserver(
IntersectionObserverObserverId intersectionObserverId,
ShadowNode::Shared observationRootShadowNode,
ShadowNode::Shared targetShadowNode,
std::vector<Float> thresholds)
: intersectionObserverId_(intersectionObserverId),
observationRootShadowNode_(std::move(observationRootShadowNode)),
targetShadowNode_(std::move(targetShadowNode)),
thresholds_(std::move(thresholds)) {}

static Rect getRootBoundingRect(
const LayoutableShadowNode& layoutableRootShadowNode) {
auto layoutMetrics = layoutableRootShadowNode.getLayoutMetrics();
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();

if (layoutMetrics == EmptyLayoutMetrics ||
layoutMetrics.displayType == DisplayType::None) {
Expand All @@ -33,7 +41,7 @@ static Rect getRootBoundingRect(

// 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 getTargetBoundingRect(
Expand All @@ -45,10 +53,24 @@ static Rect getTargetBoundingRect(
return layoutMetrics == EmptyLayoutMetrics ? Rect{} : layoutMetrics.frame;
}

static Rect getClippedTargetBoundingRect(
const ShadowNodeFamily::AncestorList& targetAncestors) {
static Rect getRootBoundingRect(
const ShadowNode::Shared& observationRootShadowNode,
const RootShadowNode& rootShadowNode,
const ShadowNodeFamily::AncestorList& rootAncestors) {
if (observationRootShadowNode == nullptr) {
return getRootNodeBoundingRect(rootShadowNode);
}

// Absolute coordinates of the root
auto rootBoundingRect = getTargetBoundingRect(rootAncestors);

return rootBoundingRect;
}

static Rect getClippedBoundingRect(
const ShadowNodeFamily::AncestorList& ancestors) {
auto layoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics(
targetAncestors,
ancestors,
{/* .includeTransform = */ true,
/* .includeViewportOffset = */ true,
/* .applyParentClipping = */ true});
Expand All @@ -60,6 +82,7 @@ static Rect getClippedTargetBoundingRect(
// https://w3c.github.io/IntersectionObserver/#compute-the-intersection
static Rect computeIntersection(
const Rect& rootBoundingRect,
const ShadowNodeFamily::AncestorList& rootAncestors,
const Rect& targetBoundingRect,
const ShadowNodeFamily::AncestorList& targetAncestors) {
auto absoluteIntersectionRect =
Expand All @@ -79,10 +102,12 @@ static Rect computeIntersection(

// 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);
auto clippedRootBoundingRect = rootAncestors.empty()
? rootBoundingRect
: getClippedBoundingRect(rootAncestors);
auto clippedTargetBoundingRect = getClippedBoundingRect(targetAncestors);

return Rect::intersect(rootBoundingRect, clippedTargetBoundingRect);
return Rect::intersect(clippedRootBoundingRect, clippedTargetBoundingRect);
}

// Partially equivalent to
Expand All @@ -91,24 +116,29 @@ std::optional<IntersectionObserverEntry>
IntersectionObserver::updateIntersectionObservation(
const RootShadowNode& rootShadowNode,
double time) {
const auto layoutableRootShadowNode =
dynamic_cast<const LayoutableShadowNode*>(&rootShadowNode);

react_native_assert(
layoutableRootShadowNode != nullptr &&
"RootShadowNode instances must always inherit from LayoutableShadowNode.");
auto rootAncestors = observationRootShadowNode_ == nullptr
? ShadowNodeFamily::AncestorList{}
: observationRootShadowNode_->getFamily().getAncestors(rootShadowNode);

auto targetAncestors =
targetShadowNode_->getFamily().getAncestors(rootShadowNode);

// Absolute coordinates of the root
auto rootBoundingRect = getRootBoundingRect(*layoutableRootShadowNode);
auto rootBoundingRect = getRootBoundingRect(
observationRootShadowNode_, rootShadowNode, rootAncestors);

// Absolute coordinates of the target
auto targetBoundingRect = getTargetBoundingRect(targetAncestors);

if ((observationRootShadowNode_ != nullptr && rootAncestors.empty()) ||
targetAncestors.empty()) {
// If observation root is not a descendant of root
// Or target is not a descendant of observation root
return setNotIntersectingState(rootBoundingRect, targetBoundingRect, time);
}

auto intersectionRect = computeIntersection(
rootBoundingRect, targetBoundingRect, targetAncestors);
rootBoundingRect, rootAncestors, targetBoundingRect, targetAncestors);

Float targetBoundingRectArea =
targetBoundingRect.size.width * targetBoundingRect.size.height;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class IntersectionObserver {
public:
IntersectionObserver(
IntersectionObserverObserverId intersectionObserverId,
ShadowNode::Shared observationRootShadowNode,
ShadowNode::Shared targetShadowNode,
std::vector<Float> thresholds);

Expand Down Expand Up @@ -74,6 +75,7 @@ class IntersectionObserver {
double time);

IntersectionObserverObserverId intersectionObserverId_;
ShadowNode::Shared observationRootShadowNode_;
ShadowNode::Shared targetShadowNode_;
std::vector<Float> thresholds_;
mutable IntersectionObserverState state_ =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ IntersectionObserverManager::IntersectionObserverManager() = default;

void IntersectionObserverManager::observe(
IntersectionObserverObserverId intersectionObserverId,
const ShadowNode::Shared& observationRootShadowNode,
const ShadowNode::Shared& shadowNode,
std::vector<Float> thresholds,
const UIManager& uiManager) {
Expand All @@ -34,7 +35,10 @@ void IntersectionObserverManager::observe(

auto& observers = observersBySurfaceId_[surfaceId];
observers.emplace_back(IntersectionObserver{
intersectionObserverId, shadowNode, std::move(thresholds)});
intersectionObserverId,
observationRootShadowNode,
shadowNode,
std::move(thresholds)});
observer = &observers.back();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class IntersectionObserverManager final : public UIManagerMountHook {

void observe(
IntersectionObserverObserverId intersectionObserverId,
const ShadowNode::Shared& observationRootShadowNode,
const ShadowNode::Shared& shadowNode,
std::vector<Float> thresholds,
const UIManager& uiManager);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type IntersectionObserverCallback = (
) => mixed;

type IntersectionObserverInit = {
// root?: ReactNativeElement, // This option exists on the Web but it's not currently supported in React Native.
root?: ?ReactNativeElement,
// rootMargin?: string, // This option exists on the Web but it's not currently supported in React Native.
threshold?: number | $ReadOnlyArray<number>,
};
Expand Down Expand Up @@ -51,6 +51,7 @@ export default class IntersectionObserver {
_thresholds: $ReadOnlyArray<number>;
_observationTargets: Set<ReactNativeElement> = new Set();
_intersectionObserverId: ?IntersectionObserverId;
_root: ReactNativeElement | null;

constructor(
callback: IntersectionObserverCallback,
Expand All @@ -68,13 +69,6 @@ export default class IntersectionObserver {
);
}

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

// $FlowExpectedError[prop-missing] it's not typed in React Native but exists on Web.
if (options?.rootMargin != null) {
throw new TypeError(
Expand All @@ -84,6 +78,7 @@ export default class IntersectionObserver {

this._callback = callback;
this._thresholds = normalizeThresholds(options?.threshold);
this._root = validateRoot(options?.root ?? null);
}

/**
Expand All @@ -95,7 +90,7 @@ export default class IntersectionObserver {
* NOTE: This cannot currently be configured and `root` is always `null`.
*/
get root(): ReactNativeElement | null {
return null;
return this._root;
}

/**
Expand Down Expand Up @@ -149,6 +144,7 @@ export default class IntersectionObserver {

const didStartObserving = IntersectionObserverManager.observe({
intersectionObserverId: this._getOrCreateIntersectionObserverId(),
root: this._root,
target,
});

Expand Down Expand Up @@ -217,6 +213,21 @@ export default class IntersectionObserver {
}
}

function validateRoot(
root: ReactNativeElement | null,
): ReactNativeElement | null {
if (root == null) {
return null;
}

if (!(root instanceof ReactNativeElement)) {
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)",
);
}
return root;
}

/**
* Converts the user defined `threshold` value into an array of sorted valid
* threshold options for `IntersectionObserver` (double ∈ [0, 1]).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,11 @@ export function unregisterObserver(
*/
export function observe({
intersectionObserverId,
root,
target,
}: {
intersectionObserverId: IntersectionObserverId,
root: ?ReactNativeElement,
target: ReactNativeElement,
}): boolean {
if (NativeIntersectionObserver == null) {
Expand Down Expand Up @@ -146,6 +148,14 @@ export function observe({
return false;
}

const rootShadowNode = root != null ? getShadowNode(root) : null;
if (root != null && rootShadowNode == 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);
Expand All @@ -160,6 +170,7 @@ export function observe({

NativeIntersectionObserver.observe({
intersectionObserverId,
rootShadowNode,
targetShadowNode,
thresholds: registeredObserver.observer.thresholds,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type NativeIntersectionObserverObserveOptions = {
intersectionObserverId: number,
targetShadowNode: mixed,
thresholds: $ReadOnlyArray<number>,
rootShadowNode?: ?mixed,
};

export interface Spec extends TurboModule {
Expand Down

0 comments on commit c0f2fde

Please sign in to comment.