From fdd49f6c9b8124ba1ad16ec402d45d2cb7b8dff9 Mon Sep 17 00:00:00 2001 From: Mark de Vocht Date: Sun, 24 May 2026 10:11:01 +0300 Subject: [PATCH 1/2] adding ios 18 zoom functionality to RNN with options --- ios/RNNScreenTransition.h | 8 ++++ ios/RNNScreenTransition.mm | 52 +++++++++++++++++++++ ios/UINavigationController+RNNCommands.mm | 7 +++ src/commands/OptionsProcessor.ts | 3 +- src/interfaces/Options.ts | 28 ++++++++++- website/docs/api/options-animations.mdx | 39 ++++++++++++++++ website/docs/docs/style-animations.mdx | 57 +++++++++++++++++++++++ 7 files changed, 191 insertions(+), 3 deletions(-) diff --git a/ios/RNNScreenTransition.h b/ios/RNNScreenTransition.h index cbe8edcd051..e1d80d3a08e 100644 --- a/ios/RNNScreenTransition.h +++ b/ios/RNNScreenTransition.h @@ -2,6 +2,9 @@ #import "RNNEnterExitAnimation.h" #import "RNNOptions.h" #import "SharedElementTransitionOptions.h" +#import "Text.h" + +@class UIViewController; @interface RNNScreenTransition : RNNOptions @@ -10,13 +13,18 @@ @property(nonatomic, strong) ElementTransitionOptions *bottomTabs; @property(nonatomic, strong) NSArray *elementTransitions; @property(nonatomic, strong) NSArray *sharedElementTransitions; +@property(nonatomic, strong) Text *zoomFromId; +@property(nonatomic, strong) Bool *zoomEnabled; @property(nonatomic, strong) Bool *enable; @property(nonatomic, strong) Bool *waitForRender; @property(nonatomic, strong) TimeInterval *duration; - (BOOL)hasCustomAnimation; +- (BOOL)hasZoomTransition; - (BOOL)shouldWaitForRender; - (NSTimeInterval)maxDuration; +- (void)applyZoomToViewController:(UIViewController *)destination + fromSourceViewController:(UIViewController *)source; @end diff --git a/ios/RNNScreenTransition.mm b/ios/RNNScreenTransition.mm index 6a5dbb13bf8..637f8f0c875 100644 --- a/ios/RNNScreenTransition.mm +++ b/ios/RNNScreenTransition.mm @@ -1,6 +1,12 @@ #import "RNNScreenTransition.h" +#import "BoolParser.h" #import "OptionsArrayParser.h" +#import "RNNElementFinder.h" +#import "RNNLayoutProtocol.h" #import "RNNUtils.h" +#import "TextParser.h" +#import "UIViewController+LayoutProtocol.h" +#import @implementation RNNScreenTransition @@ -19,6 +25,11 @@ - (instancetype)initWithDict:(NSDictionary *)dict { self.elementTransitions = [OptionsArrayParser parse:dict key:@"elementTransitions" ofClass:ElementTransitionOptions.class]; + NSDictionary *zoom = dict[@"zoom"]; + if ([zoom isKindOfClass:[NSDictionary class]]) { + self.zoomFromId = [TextParser parse:zoom key:@"fromId"]; + self.zoomEnabled = [BoolParser parse:zoom key:@"enabled"]; + } return self; } @@ -38,6 +49,10 @@ - (void)mergeOptions:(RNNScreenTransition *)options { self.sharedElementTransitions = options.sharedElementTransitions; if (options.elementTransitions) self.elementTransitions = options.elementTransitions; + if (options.zoomFromId.hasValue) + self.zoomFromId = options.zoomFromId; + if (options.zoomEnabled.hasValue) + self.zoomEnabled = options.zoomEnabled; } - (BOOL)hasCustomAnimation { @@ -45,10 +60,47 @@ - (BOOL)hasCustomAnimation { self.sharedElementTransitions || self.elementTransitions); } +- (BOOL)hasZoomTransition { + if (self.hasCustomAnimation) { + return NO; + } + + NSString *fromId = [self.zoomFromId withDefault:@""]; + return [self.zoomEnabled withDefault:YES] && fromId.length > 0; +} + - (BOOL)shouldWaitForRender { return [self.waitForRender withDefault: [RNNUtils getDefaultWaitForRender]] || self.hasCustomAnimation; } +- (void)applyZoomToViewController:(UIViewController *)destination + fromSourceViewController:(UIViewController *)source { + if (![self hasZoomTransition]) { + return; + } + + if (@available(iOS 18.0, *)) { + NSString *fromId = [[self.zoomFromId withDefault:@""] copy]; + destination.preferredTransition = [UIViewControllerTransition + zoomWithOptions:nil + sourceViewProvider:^UIView *(UIZoomTransitionSourceViewProviderContext *context) { + UIViewController *sourceVC = context.sourceViewController ?: source; + if (![sourceVC conformsToProtocol:@protocol(RNNLayoutProtocol)]) { + return nil; + } + + UIViewController *rnnSourceVC = + (UIViewController *)sourceVC; + UIView *reactView = rnnSourceVC.presentedComponentViewController.reactView; + if (reactView == nil) { + return nil; + } + + return [RNNElementFinder findElementForId:fromId inView:reactView]; + }]; + } +} + - (NSTimeInterval)maxDuration { NSTimeInterval maxDuration = 0; if ([self.topBar maxDuration] > maxDuration) { diff --git a/ios/UINavigationController+RNNCommands.mm b/ios/UINavigationController+RNNCommands.mm index 2c5b1c470eb..7ac929625a0 100644 --- a/ios/UINavigationController+RNNCommands.mm +++ b/ios/UINavigationController+RNNCommands.mm @@ -1,5 +1,7 @@ #import "RNNErrorHandler.h" +#import "RNNScreenTransition.h" #import "UINavigationController+RNNCommands.h" +#import "UIViewController+LayoutProtocol.h" #import typedef void (^RNNAnimationBlock)(void); @@ -19,6 +21,11 @@ - (void)push:(UIViewController *)newTop self.navigationBar.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight; } + RNNScreenTransition *pushTransition = newTop.resolveOptionsWithDefault.animations.push; + if (animated && [pushTransition hasZoomTransition]) { + [pushTransition applyZoomToViewController:newTop fromSourceViewController:onTopViewController]; + } + [self performBlock:^{ NSLog(@"About to push a controller %@", newTop); diff --git a/src/commands/OptionsProcessor.ts b/src/commands/OptionsProcessor.ts index 1fc0316c8d6..eb00f3872e1 100644 --- a/src/commands/OptionsProcessor.ts +++ b/src/commands/OptionsProcessor.ts @@ -20,6 +20,7 @@ import { OptionsSearchBar, OptionsTopBar, StackAnimationOptions, + StackPushAnimationOptions, StatusBarAnimationOptions, TopBarAnimationOptions, ViewAnimationOptions, @@ -390,7 +391,7 @@ export class OptionsProcessor { private processPush( key: string, - animation: StackAnimationOptions, + animation: StackPushAnimationOptions, parentOptions: AnimationOptions ) { if (key !== 'push') return; diff --git a/src/interfaces/Options.ts b/src/interfaces/Options.ts index bebca9b8ab5..e5451e7d310 100644 --- a/src/interfaces/Options.ts +++ b/src/interfaces/Options.ts @@ -829,6 +829,18 @@ export interface SharedElementTransition { interpolation?: Interpolation; } +export interface ZoomTransitionOptions { + /** + * `nativeID` of the view to zoom from when pushing, and zoom back to when popping. + * #### (iOS 18+ specific) + */ + fromId: string; + /** + * @default true + */ + enabled?: boolean; +} + export interface ElementTransition { id: string; alpha?: AppearingElementAnimation | DisappearingElementAnimation; @@ -1537,6 +1549,19 @@ export interface StackAnimationOptions { elementTransitions?: ElementTransition[]; } +/** + * Stack push animations. Extends {@link StackAnimationOptions} with iOS 18+ zoom support. + */ +export interface StackPushAnimationOptions extends StackAnimationOptions { + /** + * UIKit fluid zoom from a source view (`nativeID` must match `fromId`). + * Only used for `animations.push` — ignored on `pop`, `setStackRoot`, and Android. + * Mutually exclusive with `content` / `sharedElementTransitions` on the same push. + * #### (iOS 18+ specific) + */ + zoom?: ZoomTransitionOptions; +} + /** * Used for configuring command animations */ @@ -1551,9 +1576,8 @@ export interface AnimationOptions { setRoot?: ViewAnimationOptions | EnterExitAnimationOptions; /** * Configure the animation of the pushed screen - * #### (Android specific) */ - push?: StackAnimationOptions; + push?: StackPushAnimationOptions; /** * Configure what animates when a screen is popped */ diff --git a/website/docs/api/options-animations.mdx b/website/docs/api/options-animations.mdx index 5b0b414d94d..02f3968b2d2 100644 --- a/website/docs/api/options-animations.mdx +++ b/website/docs/api/options-animations.mdx @@ -3,3 +3,42 @@ id: options-animations title: Animations sidebar_label: Animations --- + +Animation options are declared on layout `options.animations`. See the [Animations guide](../docs/style-animations) for examples (stack, modal, shared elements, zoom). + +## Stack `push` / `pop` + +Stack command animations support `content`, `topBar`, `bottomTabs`, `sharedElementTransitions`, `elementTransitions`, and on iOS 18+ [`zoom`](#zoom-ios-18). + +## Zoom (iOS 18+) + +Declared under **`animations.push.zoom` only** when pushing onto a stack. Ignored on `animations.pop`, `animations.setStackRoot`, and Android. + +```js +animations: { + push: { + zoom: { + fromId: 'my-thumb', + enabled: true, // optional, default true + }, + }, +} +``` + +| Property | Type | Required | Platform | Description | +| -------- | ---- | -------- | -------- | ----------- | +| `fromId` | `string` | Yes | iOS 18+ | Matches `nativeID` on the source view in the screen being pushed from. | +| `enabled` | `boolean` | No | iOS 18+ | Default `true`. | + +See [Zoom transition (iOS 18+)](../docs/style-animations#zoom-transition-ios-18) for usage and behavior. + +## Shared element transitions + +Array under `animations.push.sharedElementTransitions` / `animations.pop.sharedElementTransitions`. Each item: + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `fromId` | `string` | `nativeID` on the source screen | +| `toId` | `string` | `nativeID` on the destination screen | +| `duration` | `number` | Duration in ms | +| `interpolation` | `object` | Easing — see [Animations guide](../docs/style-animations#step-3---declare-the-shared-element-animation-when-pushing-the-screen) | diff --git a/website/docs/docs/style-animations.mdx b/website/docs/docs/style-animations.mdx index 4e6b79d1375..0ae20ed8394 100644 --- a/website/docs/docs/style-animations.mdx +++ b/website/docs/docs/style-animations.mdx @@ -113,6 +113,63 @@ options: { +### Zoom transition (iOS 18+) + +Use UIKit's system **fluid zoom transition** when pushing onto a stack: the tapped view morphs into the next screen. The transition is interactive — users can drag to slow, reverse, or dismiss. + +:::info Platform support +Configure under **`animations.push` only** (not `setStackRoot` or `pop`). Available on **iOS 18 and later**. Android ignores this option. Reverse zoom on pop is automatic when the detail screen is popped. +::: + +This uses `UIViewController.preferredTransition` under the hood. It is separate from [shared element transitions](#shared-element-transitions): you do not declare `sharedElementTransitions` or `content` animations for zoom. If those custom animations are set on the same push, they take precedence and zoom is not applied. + +#### Step 1 — Set `nativeID` on the source view + +Mark the view that should expand (thumbnail, card, hero image, etc.): + +```jsx + + + +``` + +#### Step 2 — Pass the same id in push options + +```jsx +const fromId = `product-thumb-${item.id}`; + +Navigation.push(componentId, { + component: { + name: 'ProductDetail', + passProps: { item }, + options: { + animations: { + push: { + zoom: { + fromId, + }, + }, + }, + }, + }, +}); +``` + +On pop, UIKit runs the reverse zoom automatically using the same `fromId` and `nativeID`. You do not need a separate `animations.pop` block for zoom. + +#### Options + +| Property | Type | Required | Description | +| -------- | ---- | -------- | ----------- | +| `fromId` | `string` | Yes | Must match the `nativeID` of the source view on the screen below. | +| `enabled` | `boolean` | No | Default `true`. Set `false` to skip zoom while keeping the option object. | + +#### Notes + +- The source view must be mounted and visible when the push runs. If RNN cannot resolve `fromId`, the push falls back to the default slide animation. +- `fromId` is resolved with the same mechanism as shared element `fromId` / `toId` (search in the source screen's React view hierarchy). +- Zoom is intended for list → detail flows. For fully custom cross-screen animations, use [shared element transitions](#shared-element-transitions). + ### Modal animations Modal animations are declared similarly to stack animations, only this time we animate the entire view and not only part of the UI (content). From 4c0b2bf65e3fcf3503908fc3855a216057c79caf Mon Sep 17 00:00:00 2001 From: Yedidya Kennard Date: Tue, 2 Jun 2026 14:36:15 +0300 Subject: [PATCH 2/2] test: cover iOS zoom push transition options --- .../RNNScreenTransitionTest.mm | 89 ++++++++++++++ .../ios/playground.xcodeproj/project.pbxproj | 4 + src/commands/OptionsProcessor.test.ts | 110 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 playground/ios/NavigationTests/RNNScreenTransitionTest.mm diff --git a/playground/ios/NavigationTests/RNNScreenTransitionTest.mm b/playground/ios/NavigationTests/RNNScreenTransitionTest.mm new file mode 100644 index 00000000000..605e3e29659 --- /dev/null +++ b/playground/ios/NavigationTests/RNNScreenTransitionTest.mm @@ -0,0 +1,89 @@ +#import "RNNScreenTransition.h" +#import "RNNUtils.h" +#import + +@interface RNNScreenTransitionTest : XCTestCase + +@end + +@implementation RNNScreenTransitionTest + +- (void)testParsesZoomFromId { + RNNScreenTransition *transition = + [[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1"}}]; + + XCTAssertEqualObjects(transition.zoomFromId.get, @"thumb-1"); + XCTAssertTrue(transition.hasZoomTransition); +} + +- (void)testZoomEnabledDefaultsToTrue { + RNNScreenTransition *transition = + [[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1"}}]; + + XCTAssertTrue(transition.hasZoomTransition); +} + +- (void)testZoomEnabledFalseDisablesZoomTransition { + RNNScreenTransition *transition = [[RNNScreenTransition alloc] + initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1", @"enabled" : @NO}}]; + + XCTAssertFalse(transition.hasZoomTransition); +} + +- (void)testZoomTransitionRequiresNonEmptyFromId { + RNNScreenTransition *missingFromId = + [[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{}}]; + RNNScreenTransition *emptyFromId = + [[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @""}}]; + + XCTAssertFalse(missingFromId.hasZoomTransition); + XCTAssertFalse(emptyFromId.hasZoomTransition); +} + +- (void)testCustomContentAnimationTakesPrecedenceOverZoomTransition { + RNNScreenTransition *transition = [[RNNScreenTransition alloc] initWithDict:@{ + @"zoom" : @{@"fromId" : @"thumb-1"}, + @"content" : @{@"enter" : @{@"alpha" : @{@"from" : @0, @"to" : @1}}} + }]; + + XCTAssertTrue(transition.hasCustomAnimation); + XCTAssertFalse(transition.hasZoomTransition); +} + +- (void)testSharedElementTransitionTakesPrecedenceOverZoomTransition { + RNNScreenTransition *transition = [[RNNScreenTransition alloc] initWithDict:@{ + @"zoom" : @{@"fromId" : @"thumb-1"}, + @"sharedElementTransitions" : @[ @{@"fromId" : @"image-1", @"toId" : @"image-2"} ] + }]; + + XCTAssertTrue(transition.hasCustomAnimation); + XCTAssertFalse(transition.hasZoomTransition); +} + +- (void)testMergeOptionsUpdatesZoomFromIdAndEnabled { + RNNScreenTransition *transition = + [[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1"}}]; + RNNScreenTransition *updatedFromId = + [[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-2"}}]; + RNNScreenTransition *disabled = + [[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"enabled" : @NO}}]; + + [transition mergeOptions:updatedFromId]; + XCTAssertEqualObjects(transition.zoomFromId.get, @"thumb-2"); + XCTAssertTrue(transition.hasZoomTransition); + + [transition mergeOptions:disabled]; + XCTAssertFalse(transition.hasZoomTransition); +} + +- (void)testShouldWaitForRenderUsesDefaultForZoomOnlyAndCustomAnimationsWait { + RNNScreenTransition *zoomOnly = + [[RNNScreenTransition alloc] initWithDict:@{@"zoom" : @{@"fromId" : @"thumb-1"}}]; + RNNScreenTransition *customAnimation = [[RNNScreenTransition alloc] + initWithDict:@{@"content" : @{@"enter" : @{@"alpha" : @{@"from" : @0, @"to" : @1}}}}]; + + XCTAssertEqual(zoomOnly.shouldWaitForRender, [RNNUtils getDefaultWaitForRender]); + XCTAssertTrue(customAnimation.shouldWaitForRender); +} + +@end diff --git a/playground/ios/playground.xcodeproj/project.pbxproj b/playground/ios/playground.xcodeproj/project.pbxproj index 4e76a4d4b3f..3513c25212a 100644 --- a/playground/ios/playground.xcodeproj/project.pbxproj +++ b/playground/ios/playground.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 50C9A8D4240FB9D000BD699F /* RNNComponentViewController+Utils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50C9A8D3240FB9D000BD699F /* RNNComponentViewController+Utils.mm */; }; 50CF233D240695B10098042D /* RNNBottomTabsController+Helpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50CF233C240695B10098042D /* RNNBottomTabsController+Helpers.mm */; }; 50FDEFBC258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50FDEFBB258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm */; }; + 57C883092EB720D100830800 /* RNNScreenTransitionTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 57C883082EB720D100830800 /* RNNScreenTransitionTest.mm */; }; 6B102251DCC578519C2DC6A4 /* libPods-NavigationIOS12Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C10F72071A488F801E1F1116 /* libPods-NavigationIOS12Tests.a */; }; 8EB60A6CB93C527CC6A870A2 /* libPods-SnapshotTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E8B4CFA18A5ACE953124E129 /* libPods-SnapshotTests.a */; }; 9F9A3A9625260DA900AAAF37 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9F9A3A9525260DA900AAAF37 /* LaunchScreen.storyboard */; }; @@ -166,6 +167,7 @@ 50CF233C240695B10098042D /* RNNBottomTabsController+Helpers.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "RNNBottomTabsController+Helpers.mm"; sourceTree = ""; }; 50E4888A2427DA4800B11A8E /* StackOptionsTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = StackOptionsTest.mm; sourceTree = ""; }; 50FDEFBB258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNNSearchBarOptionsTest.mm; sourceTree = ""; }; + 57C883082EB720D100830800 /* RNNScreenTransitionTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RNNScreenTransitionTest.mm; sourceTree = ""; }; 60BCFCC02B7F812F00FCDB38 /* libLLVM.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libLLVM.dylib; path = usr/lib/libLLVM.dylib; sourceTree = SDKROOT; }; 60BCFCCA2B7F817400FCDB38 /* libReactNativeNavigation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReactNativeNavigation.a; sourceTree = BUILT_PRODUCTS_DIR; }; 7F8E255E2E08F6ECE7DF6FE3 /* Pods-playground.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-playground.release.xcconfig"; path = "Target Support Files/Pods-playground/Pods-playground.release.xcconfig"; sourceTree = ""; }; @@ -429,6 +431,7 @@ 5022EDCB240551EE00852BA6 /* RNNBottomTabsAppearancePresenterTest.mm */, E58D26342385888B003F36BA /* RNNTestRootViewCreator.h */, E58D263D2385888C003F36BA /* RNNTestRootViewCreator.mm */, + 57C883082EB720D100830800 /* RNNScreenTransitionTest.mm */, E58D26382385888B003F36BA /* RNNTransitionStateHolderTest.mm */, E58D263B2385888C003F36BA /* UITabBarController+RNNOptionsTest.mm */, E58D26252385888B003F36BA /* UIViewController+LayoutProtocolTest.mm */, @@ -979,6 +982,7 @@ E58D26582385888C003F36BA /* UITabBarController+RNNOptionsTest.mm in Sources */, E58D265A2385888C003F36BA /* RNNTestRootViewCreator.mm in Sources */, E58D26492385888C003F36BA /* RNNFontAttributesCreatorTest.mm in Sources */, + 57C883092EB720D100830800 /* RNNScreenTransitionTest.mm in Sources */, E58D26552385888C003F36BA /* RNNTransitionStateHolderTest.mm in Sources */, E58D26462385888C003F36BA /* UIViewController+LayoutProtocolTest.mm in Sources */, E58D26512385888C003F36BA /* RNNExternalComponentStoreTest.mm in Sources */, diff --git a/src/commands/OptionsProcessor.test.ts b/src/commands/OptionsProcessor.test.ts index 754256d66cf..c52375efb4b 100644 --- a/src/commands/OptionsProcessor.test.ts +++ b/src/commands/OptionsProcessor.test.ts @@ -890,6 +890,9 @@ describe('navigation options', () => { waitForRender: true, sharedElementTransitions: [], elementTransitions: [], + zoom: { + fromId: 'thumb-1', + }, }, }, }; @@ -899,9 +902,116 @@ describe('navigation options', () => { waitForRender: true, sharedElementTransitions: [], elementTransitions: [], + zoom: { + fromId: 'thumb-1', + }, }); }); }); + + it('preserves push zoom options', () => { + const options: Options = { + animations: { + push: { + zoom: { + fromId: 'thumb-1', + }, + }, + }, + }; + + uut.processOptions(CommandName.SetRoot, options); + + expect(options.animations!!.push).toStrictEqual({ + zoom: { + fromId: 'thumb-1', + }, + }); + }); + + it('preserves disabled push zoom options', () => { + const options: Options = { + animations: { + push: { + zoom: { + fromId: 'thumb-1', + enabled: false, + }, + }, + }, + }; + + uut.processOptions(CommandName.SetRoot, options); + + expect(options.animations!!.push).toStrictEqual({ + zoom: { + fromId: 'thumb-1', + enabled: false, + }, + }); + }); + + it('does not treat zoom as a legacy push animation shape', () => { + const options: Options = { + animations: { + push: { + zoom: { + fromId: 'thumb-1', + }, + content: { + alpha: { + from: 0, + to: 1, + }, + }, + topBar: { + alpha: { + from: 0, + to: 1, + }, + }, + bottomTabs: { + alpha: { + from: 0, + to: 1, + }, + }, + }, + }, + }; + + uut.processOptions(CommandName.SetRoot, options); + + expect(options.animations!!.push).toStrictEqual({ + zoom: { + fromId: 'thumb-1', + }, + content: { + enter: { + alpha: { + from: 0, + to: 1, + }, + }, + }, + topBar: { + enter: { + alpha: { + from: 0, + to: 1, + }, + }, + }, + bottomTabs: { + enter: { + alpha: { + from: 0, + to: 1, + }, + }, + }, + }); + }); }); describe('pop', () => {