From c58c715938af6b2f10891a17bb4a0f84e5dc3bc3 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 13:04:03 -0700 Subject: [PATCH 1/3] Support pseudo-element shadow node creation for view transitions (#56456) Summary: ## Changelog: [Internal] [Added] - Support pseudo-element shadow node creation for view transitions Implement `createViewTransitionInstance`, which is invoked by the React reconciler to create pseudo-element shadow nodes that visually represent the old state of elements participating in a view transition. Key changes: - Add `createViewTransitionInstance` JSI binding in `UIManagerBinding`, accepting a transition name and pseudo-element tag - Add virtual `createViewTransitionInstance` method to `UIManagerViewTransitionDelegate` - Implement the method in `ViewTransitionModule`: creates an absolutely-positioned, non-interactive `View` shadow node matching the old element's layout metrics (position, size) - Manage two pseudo-element node maps: `oldPseudoElementNodes_` for the current transition and `oldPseudoElementNodesForNextTransition_` for entering nodes that may exit in a future transition - Update `getOldViewTransitionInstance` to return the pseudo-element's tag (instead of the original element's tag) when a pseudo-element exists - Add `applySnapshotsOnPseudoElementShadowNodes` stub for future platform-level bitmap snapshot integration `createViewTransitionInstance` is typically called after `applyViewTransitionName` in the React reconciler. See the diagram below for the full flow. {F1987481080} Reviewed By: Abbondanzo Differential Revision: D98981886 --- .../renderer/uimanager/UIManagerBinding.cpp | 31 ++++++ .../UIManagerViewTransitionDelegate.h | 2 + .../viewtransition/ViewTransitionModule.cpp | 95 +++++++++++++++++-- .../viewtransition/ViewTransitionModule.h | 11 +++ 4 files changed, 133 insertions(+), 6 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 1aa9fdf2ab2..9b6b08f4642 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -981,6 +981,37 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "createViewTransitionInstance") { + auto paramCount = 2; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [uiManager, methodName, paramCount]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + + auto transitionName = arguments[0].isString() + ? stringFromValue(runtime, arguments[0]) + : ""; + auto pseudoElementTag = tagFromValue(arguments[1]); + + if (!transitionName.empty()) { + auto* viewTransitionDelegate = + uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->createViewTransitionInstance( + transitionName, pseudoElementTag); + } + } + + return jsi::Value::undefined(); + }); + } + if (methodName == "cancelViewTransitionName") { auto paramCount = 2; return jsi::Function::createFromHostFunction( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h index 9d4d83637f4..f8f82d1433f 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -23,6 +23,8 @@ class UIManagerViewTransitionDelegate { { } + virtual void createViewTransitionInstance(const std::string & /*name*/, Tag /*pseudoElementTag*/) {} + virtual void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) {} virtual void restoreViewTransitionName(const ShadowNode &shadowNode) {} diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index b4d759bf495..5fd165304b3 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -7,9 +7,8 @@ #include "ViewTransitionModule.h" -#include - #include +#include #include namespace facebook::react { @@ -45,6 +44,16 @@ void ViewTransitionModule::applyViewTransitionName( AnimationKeyFrameView oldView{ .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; oldLayout_[name] = oldView; + + // TODO: capture bitmap snapshot of old view via platform + + if (auto it = oldPseudoElementNodesForNextTransition_.find(name); + it != oldPseudoElementNodesForNextTransition_.end()) { + auto pseudoElementNode = it->second; + oldPseudoElementNodes_[name] = pseudoElementNode; + oldPseudoElementNodesForNextTransition_.erase(it); + } + } else { AnimationKeyFrameView newView{ .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; @@ -52,6 +61,67 @@ void ViewTransitionModule::applyViewTransitionName( } } +void ViewTransitionModule::createViewTransitionInstance( + const std::string& name, + Tag pseudoElementTag) { + if (uiManager_ == nullptr) { + return; + } + + // if createViewTransitionInstance is called before transition started, it + // creates the old pseudo elements for exiting nodes that potentially + // participate in current transition that's about to happen; if called after + // transition started, it creates old pseudo elements for entering nodes, and + // will be used in next transition when these node are exiting + bool forNextTransition = false; + AnimationKeyFrameView view = {}; + auto it = oldLayout_.find(name); + if (it == oldLayout_.end()) { + forNextTransition = true; + if (auto newIt = newLayout_.find(name); newIt != newLayout_.end()) { + view = newIt->second; + } + } else { + view = it->second; + } + + // Build props: absolute position matching old element, non-interactive + if (pseudoElementTag > 0 && view.tag > 0) { + // Create a base node with layout props via createNode + // TODO: T262559684 created dedicated shadow node type for old pseudo + // element + auto rawProps = RawProps( + folly::dynamic::object("position", "absolute")( + "left", view.layoutMetrics.originFromRoot.x)( + "top", view.layoutMetrics.originFromRoot.y)( + "width", view.layoutMetrics.size.width)( + "height", view.layoutMetrics.size.height)("pointerEvents", "none")( + "opacity", 0)("collapsable", false)); + + auto baseNode = uiManager_->createNode( + pseudoElementTag, + "View", + view.surfaceId, + std::move(rawProps), + nullptr /* instanceHandle */); + + if (baseNode == nullptr) { + return; + } + + // Clone the shadow node — bitmap will be set by platform + auto pseudoElementNode = baseNode->clone({}); + + if (pseudoElementNode != nullptr) { + if (!forNextTransition) { + oldPseudoElementNodes_[name] = pseudoElementNode; + } else { + oldPseudoElementNodesForNextTransition_[name] = pseudoElementNode; + } + } + } +} + void ViewTransitionModule::cancelViewTransitionName( const ShadowNode& shadowNode, const std::string& name) { @@ -67,6 +137,14 @@ void ViewTransitionModule::restoreViewTransitionName( cancelledNameRegistry_.erase(shadowNode.getTag()); } +void ViewTransitionModule::applySnapshotsOnPseudoElementShadowNodes() { + if (oldPseudoElementNodes_.empty() || uiManager_ == nullptr) { + return; + } + + // TODO: set bitmap snapshots on pseudo-element views via platform +} + LayoutMetrics ViewTransitionModule::captureLayoutMetricsFromRoot( const ShadowNode& shadowNode) { if (uiManager_ == nullptr) { @@ -100,13 +178,13 @@ void ViewTransitionModule::startViewTransition( // Mark transition as started transitionStarted_ = true; - // Call mutation callback (including commitRoot, measureInstance - // applyViewTransitionName for old & new) + // Call mutation callback (including commitRoot, measureInstance, + // applyViewTransitionName, createViewTransitionInstance for old & new) if (mutationCallback) { mutationCallback(); } - // TODO: capture pseudo elements + applySnapshotsOnPseudoElementShadowNodes(); if (onReadyCallback) { onReadyCallback(); @@ -128,6 +206,7 @@ void ViewTransitionModule::startViewTransitionEnd() { } } nameRegistry_.clear(); + oldPseudoElementNodes_.clear(); transitionStarted_ = false; } @@ -152,12 +231,16 @@ ViewTransitionModule::getViewTransitionInstance( auto it = oldLayout_.find(name); if (it != oldLayout_.end()) { const auto& view = it->second; + auto pseudoElementIt = oldPseudoElementNodes_.find(name); + auto nativeTag = pseudoElementIt != oldPseudoElementNodes_.end() + ? pseudoElementIt->second->getTag() + : view.tag; return ViewTransitionInstance{ .x = view.layoutMetrics.originFromRoot.x, .y = view.layoutMetrics.originFromRoot.y, .width = view.layoutMetrics.size.width, .height = view.layoutMetrics.size.height, - .nativeTag = view.tag}; + .nativeTag = nativeTag}; } } diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index f5d1f59fdc5..a076e3796ba 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -29,6 +29,10 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { void applyViewTransitionName(const ShadowNode &shadowNode, const std::string &name, const std::string &className) override; + // creates a pseudo-element shadow node for a given transition name using the + // captured old layout metrics + void createViewTransitionInstance(const std::string &name, Tag pseudoElementTag) override; + // if a viewTransitionName is cancelled, the element doesn't have view-transition-name and browser won't be taking // snapshot void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) override; @@ -72,8 +76,15 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { // used for cancel/restore viewTransitionName std::unordered_map> cancelledNameRegistry_{}; + // pseudo-element nodes keyed by transition name + std::unordered_map> oldPseudoElementNodes_{}; + // will be restored into oldPseudoElementNodes_ in next transition + std::unordered_map> oldPseudoElementNodesForNextTransition_{}; + LayoutMetrics captureLayoutMetricsFromRoot(const ShadowNode &shadowNode); + void applySnapshotsOnPseudoElementShadowNodes(); + UIManager *uiManager_{nullptr}; bool transitionStarted_{false}; From bf7cb8247beaea906b9f0283dcd8bdf9f5c83edd Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 13:04:03 -0700 Subject: [PATCH 2/3] Append pseudo-element shadow nodes to root at commit (#56457) Summary: ## Changelog: [Internal] [Added] - Append pseudo-element shadow nodes to root at commit Append view transition pseudo-element shadow nodes to the root's children at commit time (`shadowTreeWillCommit`), so they are committed into the shadow tree and rendered by the platform. Key changes: - Add `getPseudoElementNodes(surfaceId)` virtual method to `UIManagerViewTransitionDelegate`, returning pseudo-element shadow nodes filtered by surface ID - Implement the method in `ViewTransitionModule`, iterating over `oldPseudoElementNodes_` and collecting nodes matching the given surface - In `shadowTreeWillCommit`, when view transitions are enabled, query the delegate for pseudo-element nodes and insert them at the end of `rootChildren` before committing This ensures pseudo-element nodes (created by `createViewTransitionInstance`) are included in the committed shadow tree and ultimately mounted as platform views that display old-element snapshots during transitions. ## alternatives considered we could also create pseudo element shadow node at React level, but (1) it doesn't make sense to have pseudo element Fiber node type in React just for RN use case, since web doesn't have it, (2) there's no React component mapped to the pseudo element on web, so it's more like something managed on the platform Reviewed By: sammy-SC Differential Revision: D98982122 --- .../react/renderer/scheduler/Scheduler.cpp | 5 +- .../react/renderer/scheduler/Scheduler.h | 2 +- .../viewtransition/ViewTransitionModule.cpp | 121 ++++++++++++++++-- .../viewtransition/ViewTransitionModule.h | 46 ++++++- 4 files changed, 156 insertions(+), 18 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 3a1393ef40b..477ecb8a882 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -160,9 +160,8 @@ Scheduler::Scheduler( // Initialize ViewTransitionModule if (ReactNativeFeatureFlags::viewTransitionEnabled()) { - viewTransitionModule_ = std::make_unique(); - viewTransitionModule_->setUIManager(uiManager_.get()); - uiManager_->setViewTransitionDelegate(viewTransitionModule_.get()); + viewTransitionModule_ = std::make_shared(); + viewTransitionModule_->initialize(uiManager_.get(), viewTransitionModule_); } uiManager->registerMountHook(*eventPerformanceLogger_); diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index ad16e3e4087..00ed0f43ed0 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -147,7 +147,7 @@ class Scheduler final : public UIManagerDelegate { RuntimeScheduler *runtimeScheduler_{nullptr}; - std::unique_ptr viewTransitionModule_; + std::shared_ptr viewTransitionModule_; mutable std::shared_mutex onSurfaceStartCallbackMutex_; OnSurfaceStartCallback onSurfaceStartCallback_; diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index 5fd165304b3..30a891fd83c 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -7,14 +7,51 @@ #include "ViewTransitionModule.h" +#include #include #include +#include +#include #include namespace facebook::react { -void ViewTransitionModule::setUIManager(UIManager* uiManager) { +ViewTransitionModule::~ViewTransitionModule() { + if (uiManager_ != nullptr) { + if (uiManager_->getViewTransitionDelegate() == this) { + uiManager_->setViewTransitionDelegate(nullptr); + } + uiManager_->unregisterCommitHook(*this); + uiManager_ = nullptr; + } +} + +void ViewTransitionModule::initialize( + UIManager* uiManager, + std::weak_ptr weakThis) { + if (uiManager_ != nullptr) { + uiManager_->unregisterCommitHook(*this); + } uiManager_ = uiManager; + if (uiManager_ != nullptr) { + uiManager_->registerCommitHook(*this); + + // Register as MountingOverrideDelegate on existing surfaces + uiManager_->getShadowTreeRegistry().enumerate( + [weakThis](const ShadowTree& shadowTree, bool& /*stop*/) { + shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( + weakThis); + }); + + // Register on surfaces started in the future + uiManager_->setOnSurfaceStartCallback( + [weakThis](const ShadowTree& shadowTree) { + shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( + weakThis); + }); + + uiManager_->setViewTransitionDelegate(this); + } } void ViewTransitionModule::applyViewTransitionName( @@ -47,11 +84,9 @@ void ViewTransitionModule::applyViewTransitionName( // TODO: capture bitmap snapshot of old view via platform - if (auto it = oldPseudoElementNodesForNextTransition_.find(name); - it != oldPseudoElementNodesForNextTransition_.end()) { - auto pseudoElementNode = it->second; - oldPseudoElementNodes_[name] = pseudoElementNode; - oldPseudoElementNodesForNextTransition_.erase(it); + if (auto it = oldPseudoElementNodesRepository_.find(name); + it != oldPseudoElementNodesRepository_.end()) { + oldPseudoElementNodes_[name] = it->second.node; } } else { @@ -115,11 +150,81 @@ void ViewTransitionModule::createViewTransitionInstance( if (pseudoElementNode != nullptr) { if (!forNextTransition) { oldPseudoElementNodes_[name] = pseudoElementNode; - } else { - oldPseudoElementNodesForNextTransition_[name] = pseudoElementNode; + } + oldPseudoElementNodesRepository_[name] = InactivePseudoElement{ + .node = pseudoElementNode, .sourceTag = view.tag}; + } + } +} + +RootShadowNode::Unshared ViewTransitionModule::shadowTreeWillCommit( + const ShadowTree& shadowTree, + const RootShadowNode::Shared& /*oldRootShadowNode*/, + const RootShadowNode::Unshared& newRootShadowNode, + const ShadowTreeCommitOptions& /*commitOptions*/) noexcept { + if (oldPseudoElementNodes_.empty()) { + return newRootShadowNode; + } + + auto surfaceId = shadowTree.getSurfaceId(); + + // Collect pseudo-element nodes for this surface, skipping any that are + // already present in the children list (from a previous commit hook run). + const auto& existingChildren = newRootShadowNode->getChildren(); + std::unordered_set existingTags; + existingTags.reserve(existingChildren.size()); + for (const auto& child : existingChildren) { + existingTags.insert(child->getTag()); + } + + auto newChildren = + std::make_shared>>( + existingChildren); + bool appended = false; + for (const auto& [name, node] : oldPseudoElementNodes_) { + if (node->getSurfaceId() == surfaceId && + existingTags.find(node->getTag()) == existingTags.end()) { + newChildren->push_back(node); + appended = true; + } + } + + if (!appended) { + return newRootShadowNode; + } + + return std::make_shared( + *newRootShadowNode, + ShadowNodeFragment{ + .props = ShadowNodeFragment::propsPlaceholder(), + .children = newChildren, + }); +} + +bool ViewTransitionModule::shouldOverridePullTransaction() const { + return !oldPseudoElementNodesRepository_.empty(); +} + +std::optional ViewTransitionModule::pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number number, + const TransactionTelemetry& telemetry, + ShadowViewMutationList mutations) const { + for (const auto& mutation : mutations) { + if (mutation.type == ShadowViewMutation::Delete) { + auto tag = mutation.oldChildShadowView.tag; + for (auto it = oldPseudoElementNodesRepository_.begin(); + it != oldPseudoElementNodesRepository_.end();) { + if (it->second.sourceTag == tag) { + it = oldPseudoElementNodesRepository_.erase(it); + } else { + ++it; + } } } } + return MountingTransaction{ + surfaceId, number, std::move(mutations), telemetry}; } void ViewTransitionModule::cancelViewTransitionName( diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index a076e3796ba..3a340a96883 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -11,18 +11,25 @@ #include #include +#include #include +#include #include namespace facebook::react { +class ShadowTree; class UIManager; -class ViewTransitionModule : public UIManagerViewTransitionDelegate { +class ViewTransitionModule : public UIManagerViewTransitionDelegate, + public UIManagerCommitHook, + public MountingOverrideDelegate { public: - ~ViewTransitionModule() override = default; + ~ViewTransitionModule() override; - void setUIManager(UIManager *uiManager); + void initialize(UIManager *uiManager, std::weak_ptr weakThis); + +#pragma mark - UIManagerViewTransitionDelegate // will be called when a view will transition. if a view already has a view-transition-name, it may not be called // again until it's removed @@ -50,6 +57,25 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { std::optional getViewTransitionInstance(const std::string &name, const std::string &pseudo) override; +#pragma mark - UIManagerCommitHook + + void commitHookWasRegistered(const UIManager & /*uiManager*/) noexcept override {} + void commitHookWasUnregistered(const UIManager & /*uiManager*/) noexcept override {} + RootShadowNode::Unshared shadowTreeWillCommit( + const ShadowTree &shadowTree, + const RootShadowNode::Shared &oldRootShadowNode, + const RootShadowNode::Unshared &newRootShadowNode, + const ShadowTreeCommitOptions &commitOptions) noexcept override; + +#pragma mark - MountingOverrideDelegate + + bool shouldOverridePullTransaction() const override; + std::optional pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number number, + const TransactionTelemetry &telemetry, + ShadowViewMutationList mutations) const override; + // Animation state structure for storing minimal view data struct AnimationKeyFrameViewLayoutMetrics { Point originFromRoot; @@ -76,10 +102,18 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { // used for cancel/restore viewTransitionName std::unordered_map> cancelledNameRegistry_{}; - // pseudo-element nodes keyed by transition name + // pseudo-element nodes keyed by transition name, appended to/removed from root children at next ShadowTree commit + // TODO: T262559264 should be cleaned up from ShadowTree as soon as transition animation ends std::unordered_map> oldPseudoElementNodes_{}; - // will be restored into oldPseudoElementNodes_ in next transition - std::unordered_map> oldPseudoElementNodesForNextTransition_{}; + + struct InactivePseudoElement { + std::shared_ptr node; + Tag sourceTag{0}; // tag of the original view this was created from + }; + // pseudo-element nodes created for entering nodes, to be copied into + // oldPseudoElementNodes_ during the next applyViewTransitionName call. + // Mutable because pullTransaction (const) needs to erase unmounted entries. + mutable std::unordered_map oldPseudoElementNodesRepository_{}; LayoutMetrics captureLayoutMetricsFromRoot(const ShadowNode &shadowNode); From 756d1d5e7ccca87d70b110ab4548110aea4d3438 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 15 Apr 2026 13:04:03 -0700 Subject: [PATCH 3/3] Allow pseudo-element shadow node lookup in JS (#56455) Summary: ## Changelog: [Internal] [Added] - Allow pseudo-element shadow node lookup in JS Add `findPseudoElementShadowNodeByTag` to look up pseudo-element shadow nodes by tag from JS, and update the animation helper to connect animated nodes to pseudo-element shadow nodes for old-element animations. Key changes: - Add `findPseudoElementShadowNodeByTag(tag)` virtual method to `UIManagerViewTransitionDelegate` and implement in `ViewTransitionModule` (linear scan over `oldPseudoElementNodes_`) - Expose the method via `NativeViewTransition` TurboModule so JS can resolve a pseudo-element's shadow node by its native tag - Add `findPseudoElementShadowNodeByTag` to `NativeViewTransition.js` specs Reviewed By: sammy-SC Differential Revision: D98982251 --- .../viewtransition/NativeViewTransition.cpp | 16 ++++++++++ .../viewtransition/NativeViewTransition.h | 2 ++ .../UIManagerViewTransitionDelegate.h | 10 +++++++ .../viewtransition/ViewTransitionModule.cpp | 30 +++++++++++++++++++ .../viewtransition/ViewTransitionModule.h | 6 ++-- .../specs/NativeViewTransition.js | 1 + 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp index 6fb69012df4..8190c7c2f76 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp @@ -51,4 +51,20 @@ std::optional NativeViewTransition::getViewTransitionInstance( return result; } +jsi::Value NativeViewTransition::findPseudoElementShadowNodeByTag( + jsi::Runtime& rt, + double reactTag) { + auto& uiManager = UIManagerBinding::getBinding(rt)->getUIManager(); + auto* viewTransitionDelegate = uiManager.getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + auto shadowNode = viewTransitionDelegate->findPseudoElementShadowNodeByTag( + static_cast(reactTag)); + if (shadowNode) { + return Bridging>::toJs(rt, shadowNode); + } + } + + return jsi::Value::null(); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h index bd5cb3b424f..f3311f40bef 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h +++ b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h @@ -26,6 +26,8 @@ class NativeViewTransition : public NativeViewTransitionCxxSpec getViewTransitionInstance(jsi::Runtime &rt, const std::string &name, const std::string &pseudo); + + jsi::Value findPseudoElementShadowNodeByTag(jsi::Runtime &rt, double reactTag); }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h index f8f82d1433f..d3685775c35 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -57,6 +57,16 @@ class UIManagerViewTransitionDelegate { { return std::nullopt; } + + // Similar to UIManager::findShadowNodeByTag, but searches all direct children + // of the root node (where pseudo-element nodes live) rather than just the + // first child. Pseudo-element nodes are appended as additional children of the + // root node, rather than inserted into the main React tree, to avoid + // disrupting the user-created component tree. + virtual std::shared_ptr findPseudoElementShadowNodeByTag(Tag /*tag*/) const + { + return nullptr; + } }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index 30a891fd83c..d67ad6e24a8 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -227,6 +227,36 @@ std::optional ViewTransitionModule::pullTransaction( surfaceId, number, std::move(mutations), telemetry}; } +std::shared_ptr +ViewTransitionModule::findPseudoElementShadowNodeByTag(Tag tag) const { + if (uiManager_ == nullptr) { + return nullptr; + } + + auto shadowNode = std::shared_ptr{}; + + uiManager_->getShadowTreeRegistry().enumerate( + [&](const ShadowTree& shadowTree, bool& stop) { + const auto rootShadowNode = + shadowTree.getCurrentRevision().rootShadowNode; + + if (rootShadowNode != nullptr) { + const auto& children = rootShadowNode->getChildren(); + // Pseudo element nodes are appended after the first child (the main + // React tree), so iterate from index 1 onwards. + for (size_t i = 1; i < children.size(); ++i) { + if (children[i]->getTag() == tag) { + shadowNode = children[i]; + stop = true; + return; + } + } + } + }); + + return shadowNode; +} + void ViewTransitionModule::cancelViewTransitionName( const ShadowNode& shadowNode, const std::string& name) { diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index 3a340a96883..ae585978b40 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -76,6 +76,8 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, const TransactionTelemetry &telemetry, ShadowViewMutationList mutations) const override; + std::shared_ptr findPseudoElementShadowNodeByTag(Tag tag) const override; + // Animation state structure for storing minimal view data struct AnimationKeyFrameViewLayoutMetrics { Point originFromRoot; @@ -102,8 +104,8 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate, // used for cancel/restore viewTransitionName std::unordered_map> cancelledNameRegistry_{}; - // pseudo-element nodes keyed by transition name, appended to/removed from root children at next ShadowTree commit - // TODO: T262559264 should be cleaned up from ShadowTree as soon as transition animation ends + // pseudo-element nodes keyed by transition name, appended to root children via UIManagerCommitHook + // TODO: T262559264 pseudo elements should be cleaned up as soon as transition animation ends std::unordered_map> oldPseudoElementNodes_{}; struct InactivePseudoElement { diff --git a/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js b/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js index 1c56f51e1f3..c9fa79cd363 100644 --- a/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js +++ b/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js @@ -23,6 +23,7 @@ export interface Spec extends TurboModule { height: number, nativeTag: number, }; + +findPseudoElementShadowNodeByTag: (reactTag: number) => ?unknown /* Node */; } export default TurboModuleRegistry.get(