@apexnative/navigation 1.0.0-alpha.6 → 1.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ApexNavigation.podspec +2 -1
- package/CHANGELOG.md +7 -0
- package/android/CLAUDE.md +19 -10
- package/android/CORE.md +3 -2
- package/android/consumer-rules.pro +6 -0
- package/android/src/main/java/dev/apexnative/navigation/options/BottomTabOptions.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/options/LargeTitleOptions.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/options/LayoutAnimation.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/options/OptionKey.kt +18 -0
- package/android/src/main/java/dev/apexnative/navigation/options/OptionPaths.kt +91 -0
- package/android/src/main/java/dev/apexnative/navigation/options/SheetOptions.kt +7 -4
- package/android/src/main/java/dev/apexnative/navigation/options/TitleOptions.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/TopBarAppearanceOptions.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/options/TopBarOptions.kt +16 -7
- package/android/src/main/java/dev/apexnative/navigation/react/ContentHeights.kt +95 -0
- package/android/src/main/java/dev/apexnative/navigation/react/ContentMounts.kt +66 -0
- package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +4 -0
- package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +41 -3
- package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +16 -1
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +27 -7
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +12 -1
- package/android/src/main/java/dev/apexnative/navigation/utils/MaterialTheme.kt +5 -6
- package/android/src/main/java/dev/apexnative/navigation/utils/SystemAnimations.kt +36 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +329 -55
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +26 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +60 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +32 -4
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +46 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +15 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +7 -4
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabMotion.kt +142 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +215 -43
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +25 -26
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarRowTransition.kt +3 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/ButtonPlaces.kt +11 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +36 -38
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +124 -181
- package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +8 -1
- package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionLayer.kt +37 -112
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +56 -3
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +170 -153
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMorph.kt +199 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +55 -45
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/{TextChangeAnimator.kt → TextMorph.kt} +79 -129
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewMorph.kt +108 -0
- package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabCopy.kt +85 -0
- package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabMenu.kt +13 -0
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +5 -24
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +51 -34
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonComponentCell.kt +6 -6
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonView.kt +113 -0
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarMenu.kt +219 -0
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +121 -175
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonMenus.kt +34 -64
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonRow.kt +216 -0
- package/android/src/main/jni/ANNContentHeights.cpp +82 -0
- package/android/src/main/res/drawable/apex_navigation_ic_arrow_right.xml +14 -0
- package/android/src/main/res/layout/apex_navigation_menu_header.xml +19 -0
- package/android/src/main/res/layout/apex_navigation_menu_item.xml +68 -0
- package/android/src/main/res/values/styles.xml +1 -1
- package/android/src/main/res/values-night/styles.xml +1 -1
- package/cli/doctor/checks/app.js +16 -1
- package/cli/project.js +4 -0
- package/cli/setup/index.js +5 -0
- package/cli/setup/steps/metro.js +46 -0
- package/docs-md/architecture/native-screens.md +4 -0
- package/docs-md/architecture/theme-and-context.md +131 -12
- package/docs-md/architecture/transitions.md +43 -4
- package/docs-md/flows/bottom-sheets.md +4 -2
- package/docs-md/fundamentals/screens-and-props.md +9 -1
- package/docs-md/guides/cli.md +3 -1
- package/docs-md/help/troubleshooting.md +1 -1
- package/docs-md/index.json +29 -1
- package/docs-md/screen-ui/back-and-gestures.md +3 -1
- package/docs-md/screen-ui/bar-buttons.md +1 -1
- package/docs-md/screen-ui/toolbar-and-fab.md +2 -0
- package/docs-md/start/installation.md +16 -1
- package/ios/Animations/ANNFrameDriver.h +55 -15
- package/ios/Animations/ANNFrameDriver.mm +214 -60
- package/ios/Animations/ANNTransitionAnimator.h +26 -5
- package/ios/Animations/ANNTransitionAnimator.mm +49 -3
- package/ios/Animations/ANNTransitionPlan.h +2 -2
- package/ios/Animations/ANNTransitionPlan.mm +10 -0
- package/ios/Animations/Elements/ANNElementFinder.h +2 -2
- package/ios/Animations/Elements/ANNElementFinder.mm +2 -2
- package/ios/Animations/Elements/ANNElementTransition.h +3 -3
- package/ios/Animations/Elements/ANNElementTransition.mm +2 -2
- package/ios/Animations/Elements/ANNSharedElementFlight.h +3 -3
- package/ios/Animations/Elements/ANNSharedElementFlight.mm +181 -43
- package/ios/Animations/Interpolators/ANNViewTracks.h +6 -5
- package/ios/Animations/Interpolators/ANNViewTracks.mm +51 -21
- package/ios/CLAUDE.md +15 -7
- package/ios/CORE.md +1 -1
- package/ios/Core/ANNAppDelegate.mm +0 -2
- package/ios/Core/ANNDeepLinkQueue.h +6 -5
- package/ios/Core/ANNDeepLinkQueue.mm +22 -10
- package/ios/Core/ANNSceneDelegate.mm +2 -2
- package/ios/Core/ApexNavigation.mm +6 -2
- package/ios/Layouts/Root/ANNRootCoordinator.mm +45 -15
- package/ios/Layouts/Screen/ANNComponentViewController.mm +4 -1
- package/ios/Layouts/Stack/ANNStackAnimatingDelegate.h +11 -2
- package/ios/Layouts/Stack/ANNStackAnimatingDelegate.mm +14 -7
- package/ios/Layouts/Stack/ANNStackController.h +8 -0
- package/ios/Layouts/Stack/ANNStackController.mm +153 -11
- package/ios/Layouts/Stack/ANNSwipePopInteraction.h +26 -0
- package/ios/Layouts/Stack/ANNSwipePopInteraction.mm +185 -0
- package/ios/Layouts/Stack/InteractivePopGestureDelegate.mm +28 -4
- package/ios/Options/ANNAnimationOptions.h +2 -1
- package/ios/Options/ANNAnimationOptions.mm +1 -1
- package/ios/Options/ANNOptions.h +7 -0
- package/ios/Options/ANNOptions.mm +45 -0
- package/ios/Options/ANNOptionsReader.mm +2 -4
- package/ios/Options/ANNStackPopAnimationOptions.h +13 -0
- package/ios/Options/ANNStackPopAnimationOptions.mm +11 -0
- package/ios/Options/Params/ColorParser.h +4 -3
- package/ios/Options/Params/ColorParser.mm +22 -4
- package/ios/React/ANNSurfaceHostingView.h +8 -0
- package/ios/React/ANNSurfaceHostingView.mm +28 -1
- package/ios/React/Slots/ANNSlotScreenView.mm +8 -2
- package/ios/TurboModules/ANNTurboModule.mm +46 -0
- package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.cpp +20 -0
- package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.h +5 -14
- package/lib/module/ARCHITECTURE.md +9 -3
- package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
- package/lib/module/api/app/AppRegistry.js +14 -2
- package/lib/module/api/app/AppRegistry.js.map +1 -1
- package/lib/module/api/app/createNavigation.js +188 -66
- package/lib/module/api/app/createNavigation.js.map +1 -1
- package/lib/module/api/events/navigationEvents.js +6 -2
- package/lib/module/api/events/navigationEvents.js.map +1 -1
- package/lib/module/api/linking/linking.js +11 -0
- package/lib/module/api/linking/linking.js.map +1 -1
- package/lib/module/api/screens/defineScreen.js +45 -3
- package/lib/module/api/screens/defineScreen.js.map +1 -1
- package/lib/module/api/screens/nativeScreens.js +31 -6
- package/lib/module/api/screens/nativeScreens.js.map +1 -1
- package/lib/module/api/screens/registerScreens.js +172 -35
- package/lib/module/api/screens/registerScreens.js.map +1 -1
- package/lib/module/api/theme/theme.js +5 -0
- package/lib/module/api/theme/theme.js.map +1 -1
- package/lib/module/engine/commands.js +21 -1
- package/lib/module/engine/commands.js.map +1 -1
- package/lib/module/engine/layout.js +7 -2
- package/lib/module/engine/layout.js.map +1 -1
- package/lib/module/engine/nativeScreens.js +9 -0
- package/lib/module/engine/nativeScreens.js.map +1 -1
- package/lib/module/engine/options.js +5 -4
- package/lib/module/engine/options.js.map +1 -1
- package/lib/module/engine/optionsChange.js +102 -0
- package/lib/module/engine/optionsChange.js.map +1 -0
- package/lib/module/engine/refresh.js +85 -0
- package/lib/module/engine/refresh.js.map +1 -0
- package/lib/module/engine/rememberedOptions.js +60 -0
- package/lib/module/engine/rememberedOptions.js.map +1 -1
- package/lib/module/engine/screens.js +26 -0
- package/lib/module/engine/screens.js.map +1 -1
- package/lib/typescript/adapters/NativeANNTurboModule.d.ts +7 -0
- package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
- package/lib/typescript/api/app/AppRegistry.d.ts.map +1 -1
- package/lib/typescript/api/app/createNavigation.d.ts +5 -0
- package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
- package/lib/typescript/api/events/navigationEvents.d.ts +2 -2
- package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
- package/lib/typescript/api/index.d.ts +1 -1
- package/lib/typescript/api/index.d.ts.map +1 -1
- package/lib/typescript/api/linking/linking.d.ts +8 -0
- package/lib/typescript/api/linking/linking.d.ts.map +1 -1
- package/lib/typescript/api/screens/defineScreen.d.ts +23 -4
- package/lib/typescript/api/screens/defineScreen.d.ts.map +1 -1
- package/lib/typescript/api/screens/nativeScreens.d.ts +8 -1
- package/lib/typescript/api/screens/nativeScreens.d.ts.map +1 -1
- package/lib/typescript/api/screens/registerScreens.d.ts.map +1 -1
- package/lib/typescript/api/theme/theme.d.ts +2 -0
- package/lib/typescript/api/theme/theme.d.ts.map +1 -1
- package/lib/typescript/engine/commands.d.ts +5 -0
- package/lib/typescript/engine/commands.d.ts.map +1 -1
- package/lib/typescript/engine/layout.d.ts.map +1 -1
- package/lib/typescript/engine/nativeScreens.d.ts +5 -0
- package/lib/typescript/engine/nativeScreens.d.ts.map +1 -1
- package/lib/typescript/engine/optionsChange.d.ts +28 -0
- package/lib/typescript/engine/optionsChange.d.ts.map +1 -0
- package/lib/typescript/engine/refresh.d.ts +35 -0
- package/lib/typescript/engine/refresh.d.ts.map +1 -0
- package/lib/typescript/engine/rememberedOptions.d.ts +13 -0
- package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
- package/lib/typescript/engine/screens.d.ts +7 -0
- package/lib/typescript/engine/screens.d.ts.map +1 -1
- package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
- package/lib/typescript/interfaces/options/animations.d.ts +40 -15
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -2
- package/lib/typescript/interfaces/options/buttons.d.ts +2 -2
- package/lib/typescript/interfaces/options/modals.d.ts +6 -7
- package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/stack.d.ts +8 -3
- package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/topBar.d.ts +14 -0
- package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
- package/metro/acceptPlugin.js +57 -0
- package/metro/babelTransformer.js +45 -0
- package/metro/index.js +36 -0
- package/package.json +3 -1
- package/src/ARCHITECTURE.md +9 -3
- package/src/adapters/NativeANNTurboModule.ts +7 -0
- package/src/api/app/AppRegistry.ts +16 -2
- package/src/api/app/createNavigation.ts +198 -66
- package/src/api/events/navigationEvents.ts +9 -3
- package/src/api/index.ts +1 -0
- package/src/api/linking/linking.ts +22 -1
- package/src/api/screens/defineScreen.ts +49 -5
- package/src/api/screens/nativeScreens.ts +40 -4
- package/src/api/screens/registerScreens.tsx +170 -42
- package/src/api/theme/theme.ts +5 -0
- package/src/engine/commands.ts +21 -0
- package/src/engine/layout.ts +11 -2
- package/src/engine/nativeScreens.ts +10 -1
- package/src/engine/options.ts +6 -8
- package/src/engine/optionsChange.ts +107 -0
- package/src/engine/refresh.ts +97 -0
- package/src/engine/rememberedOptions.ts +70 -0
- package/src/engine/screens.tsx +24 -0
- package/src/interfaces/ComponentEvents.ts +2 -2
- package/src/interfaces/options/animations.ts +40 -15
- package/src/interfaces/options/bottomTabs.ts +2 -2
- package/src/interfaces/options/buttons.ts +2 -2
- package/src/interfaces/options/modals.ts +6 -7
- package/src/interfaces/options/stack.ts +8 -3
- package/src/interfaces/options/topBar.ts +14 -0
- package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStub.kt +0 -31
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +0 -134
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +0 -78
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PropertyAnimatorCreator.kt +0 -33
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +0 -57
- package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +0 -146
|
@@ -6,32 +6,39 @@ import React, {
|
|
|
6
6
|
lazy,
|
|
7
7
|
type ReactNode,
|
|
8
8
|
Suspense,
|
|
9
|
-
useEffect,
|
|
10
9
|
useMemo,
|
|
11
10
|
useState,
|
|
12
11
|
} from 'react';
|
|
13
|
-
import { Platform, StyleSheet } from 'react-native';
|
|
12
|
+
import { DevSettings, Platform, StyleSheet } from 'react-native';
|
|
14
13
|
import ANNTurboModule from '../../adapters/NativeANNTurboModule';
|
|
15
14
|
import type { NativeControllerProps } from '../../engine/nativeScreens';
|
|
16
15
|
import { isButtonRoot } from '../../engine/props';
|
|
17
16
|
import {
|
|
18
17
|
checkArrangements,
|
|
19
|
-
type EngineArrangement,
|
|
20
18
|
onScreenUnmounted,
|
|
21
19
|
registerScreen as registerEngineScreen,
|
|
22
20
|
type ScreenRootProps,
|
|
21
|
+
shownScreens,
|
|
23
22
|
} from '../../engine/screens';
|
|
24
23
|
import { screenUnmounted } from '../events/screenLifecycle';
|
|
25
24
|
import { mergeOptions } from '../../engine/mergeOptions';
|
|
26
25
|
import { preparedPadding } from '../../engine/slots';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
26
|
+
import { clearOptions, mergeOptions as sendMergeOptions } from '../../engine/commands';
|
|
27
|
+
import { changeOverOwn, optionsChange } from '../../engine/optionsChange';
|
|
28
|
+
import { notedScreens, ownOptionsOf } from '../../engine/rememberedOptions';
|
|
29
|
+
import { openNativeScreens } from '../../engine/nativeScreens';
|
|
30
|
+
import { placeOf } from '../../engine/places';
|
|
31
|
+
import { propsOf } from '../../engine/props';
|
|
32
|
+
import { familyOf, newestType, onRedefined } from '../../engine/refresh';
|
|
29
33
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
buildScreenOptions,
|
|
35
|
+
definitionOf,
|
|
36
|
+
ownDefinitionOf,
|
|
37
|
+
type ScreenDefinition,
|
|
38
|
+
type ScreenOptionsInput,
|
|
39
|
+
} from './defineScreen';
|
|
40
|
+
import { isNativeScreen, type NativeScreen, type NativeScreenHookProps } from './nativeScreens';
|
|
41
|
+
import { type Arrangement, isArrangement, toEngineArrangement } from './arrangement';
|
|
35
42
|
import { type ErrorFallbackProps, ScreenBoundary } from './ScreenBoundary';
|
|
36
43
|
import ANNSafeAreaView from '../safeArea/ANNSafeAreaViewNativeComponent';
|
|
37
44
|
|
|
@@ -98,6 +105,117 @@ export interface ScreenSet {
|
|
|
98
105
|
// The root of a screen tells when it unmounts: what the screen left behind goes.
|
|
99
106
|
onScreenUnmounted(screenUnmounted);
|
|
100
107
|
|
|
108
|
+
// The screens of defineScreen by name, in development: what `screens` gives (a lazy screen's module once loaded) and
|
|
109
|
+
// the options of its group.
|
|
110
|
+
const editable = new Map<
|
|
111
|
+
string,
|
|
112
|
+
{ screen: () => unknown; groupOptions: ScreenOptionsInput<object> | undefined }
|
|
113
|
+
>();
|
|
114
|
+
|
|
115
|
+
onRedefined(applyEdit);
|
|
116
|
+
|
|
117
|
+
// Fast Refresh ran the module of a screen again: the screens of it that show take what the edit changed in their
|
|
118
|
+
// options, as a command would have shown them. The family's newest is still the version before the edit here, as React
|
|
119
|
+
// Refresh moves it on only after the update.
|
|
120
|
+
function applyEdit(next: object) {
|
|
121
|
+
const family = familyOf(next);
|
|
122
|
+
const edited = ownDefinitionOf(next) ?? (isNativeScreen(next) ? next : undefined);
|
|
123
|
+
if (!family || !edited) return;
|
|
124
|
+
for (const [name, { screen, groupOptions }] of editable) {
|
|
125
|
+
const current = screen();
|
|
126
|
+
if (current === undefined || familyOf(current) !== family) continue;
|
|
127
|
+
const native = isNativeScreen(current);
|
|
128
|
+
const before = native ? newestType(current) : definitionOf(current);
|
|
129
|
+
// The screen as it was registered: its own module running, nothing edited yet.
|
|
130
|
+
if (before === edited) continue;
|
|
131
|
+
// What a native screen is made of goes to the native side when it is registered and shown: a change of it starts
|
|
132
|
+
// the app again.
|
|
133
|
+
if (native && isNativeScreen(edited) && madeOtherwise(newestType(current), edited)) {
|
|
134
|
+
DevSettings.reload('[navigation] A native screen changed its name or its useProps');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
applyChange(
|
|
138
|
+
name,
|
|
139
|
+
native,
|
|
140
|
+
{ group: groupOptions, definition: before },
|
|
141
|
+
{ group: groupOptions, definition: edited }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The options a screen is shown with: those of its group, and those of its definition over them. */
|
|
147
|
+
interface ShownOptions {
|
|
148
|
+
group: ScreenOptionsInput<object> | undefined;
|
|
149
|
+
definition: { options?: ScreenOptionsInput<any> } | undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// What an edit changed in a screen's options goes to every screen of it the native side has, a screen React hasn't
|
|
153
|
+
// mounted yet too (a tab not selected yet), as a command would have shown it; only where the screen still has what
|
|
154
|
+
// the definition before gave.
|
|
155
|
+
function applyChange(name: string, native: boolean, before: ShownOptions, after: ShownOptions) {
|
|
156
|
+
const shown = native
|
|
157
|
+
? openNativeScreens(name)
|
|
158
|
+
: [...new Set([...shownScreens(name), ...notedScreens(name).filter(isPlaced)])].map((id) => ({
|
|
159
|
+
id,
|
|
160
|
+
props: propsOf(id),
|
|
161
|
+
}));
|
|
162
|
+
for (const { id, props } of shown) {
|
|
163
|
+
const options = ({ group, definition }: ShownOptions) =>
|
|
164
|
+
mergeOptions(
|
|
165
|
+
buildScreenOptions(group, props),
|
|
166
|
+
buildScreenOptions(definition?.options, props)
|
|
167
|
+
);
|
|
168
|
+
const shownWith = options(before);
|
|
169
|
+
const change = optionsChange(shownWith, options(after));
|
|
170
|
+
const { merge, removed } = changeOverOwn(change, shownWith, ownOptionsOf(id) ?? shownWith);
|
|
171
|
+
if (removed.length > 0) clearOptions(id, removed);
|
|
172
|
+
if (merge) sendMergeOptions(id, merge);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The screens map registered again (Fast Refresh ran the module of the app): a screen written in the map itself, a
|
|
177
|
+
// definition no module exports, has no family to tell its edit, so its definition before and after are compared here.
|
|
178
|
+
function applyRegisteredAgain(
|
|
179
|
+
name: string,
|
|
180
|
+
entry: unknown,
|
|
181
|
+
group: ScreenOptionsInput<object> | undefined
|
|
182
|
+
) {
|
|
183
|
+
const previous = editable.get(name);
|
|
184
|
+
const before = previous?.screen();
|
|
185
|
+
if (before === undefined || before === entry) return;
|
|
186
|
+
if (isNativeScreen(before) && isNativeScreen(entry)) {
|
|
187
|
+
if (madeOtherwise(newestType(before), entry)) {
|
|
188
|
+
DevSettings.reload('[navigation] A native screen changed its name or its useProps');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
applyChange(
|
|
192
|
+
name,
|
|
193
|
+
true,
|
|
194
|
+
{ group: previous!.groupOptions, definition: newestType(before) },
|
|
195
|
+
{ group, definition: entry }
|
|
196
|
+
);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const was = definitionOf(before);
|
|
200
|
+
const now = definitionOf(entry);
|
|
201
|
+
if (!was && !now && previous!.groupOptions === group) return;
|
|
202
|
+
applyChange(
|
|
203
|
+
name,
|
|
204
|
+
false,
|
|
205
|
+
{ group: previous!.groupOptions, definition: was },
|
|
206
|
+
{ group, definition: now }
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// A screen the engine knows the place of: one whose command failed after its layout was built has none.
|
|
211
|
+
function isPlaced(id: string): boolean {
|
|
212
|
+
return placeOf(id) !== undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function madeOtherwise(before: NativeScreen<any>, after: NativeScreen<any>): boolean {
|
|
216
|
+
return before.nativeName !== after.nativeName || !before.useProps !== !after.useProps;
|
|
217
|
+
}
|
|
218
|
+
|
|
101
219
|
export function registerScreens(sets: ScreenSet[], wrapper: ScreenWrapper = {}) {
|
|
102
220
|
// The screens of every set are in one namespace: a command names a screen without its group.
|
|
103
221
|
const sources = new Map<string, string>();
|
|
@@ -131,21 +249,31 @@ function registerScreen(
|
|
|
131
249
|
return;
|
|
132
250
|
}
|
|
133
251
|
if (isNativeScreen(entry)) {
|
|
252
|
+
// Read on every show, the newest after Fast Refresh, as for a screen of defineScreen.
|
|
253
|
+
const screen = () => newestType(entry);
|
|
254
|
+
if (__DEV__) {
|
|
255
|
+
applyRegisteredAgain(name, entry, groupOptions);
|
|
256
|
+
editable.set(name, { screen: () => entry, groupOptions });
|
|
257
|
+
}
|
|
134
258
|
registerEngineScreen(name, {
|
|
135
259
|
nativeName: entry.nativeName,
|
|
136
260
|
options: (props) =>
|
|
137
261
|
mergeOptions(
|
|
138
262
|
buildScreenOptions(groupOptions, props),
|
|
139
|
-
buildScreenOptions(
|
|
263
|
+
buildScreenOptions(screen().options, props)
|
|
140
264
|
),
|
|
141
|
-
controller: entry.useProps && wrapNativeScreen(name, entry
|
|
142
|
-
arrangement:
|
|
265
|
+
controller: entry.useProps && wrapNativeScreen(name, entry, wrapper),
|
|
266
|
+
arrangement: () => {
|
|
267
|
+
const arrangement = screen().arrangement;
|
|
268
|
+
return arrangement && toEngineArrangement(name, arrangement);
|
|
269
|
+
},
|
|
143
270
|
});
|
|
144
271
|
return;
|
|
145
272
|
}
|
|
146
273
|
// React.memo and forwardRef components are objects, so the kind of entry is told by its keys.
|
|
147
274
|
if (isObject(entry) && 'load' in entry) {
|
|
148
275
|
const screen = lazyScreen(entry.load);
|
|
276
|
+
if (__DEV__) editable.set(name, { screen: screen.loaded, groupOptions });
|
|
149
277
|
registerEngineScreen(name, {
|
|
150
278
|
component: () => wrapScreen(name, screen.component, wrapper),
|
|
151
279
|
// A command loads the screen before it reads its options.
|
|
@@ -164,32 +292,34 @@ function registerScreen(
|
|
|
164
292
|
});
|
|
165
293
|
return;
|
|
166
294
|
}
|
|
167
|
-
|
|
295
|
+
// Read on every show: after Fast Refresh a screen of defineScreen gives its new options, key and pair.
|
|
296
|
+
const definition = () => toDefinition(entry);
|
|
297
|
+
if (__DEV__) {
|
|
298
|
+
applyRegisteredAgain(name, entry, groupOptions);
|
|
299
|
+
editable.set(name, { screen: () => entry, groupOptions });
|
|
300
|
+
}
|
|
168
301
|
registerEngineScreen(name, {
|
|
169
|
-
component: () => wrapScreen(name, definition.component, wrapper),
|
|
302
|
+
component: () => wrapScreen(name, definition().component, wrapper),
|
|
170
303
|
options: (props) =>
|
|
171
304
|
mergeOptions(
|
|
172
305
|
buildScreenOptions(groupOptions, props),
|
|
173
|
-
buildScreenOptions(definition.options, props)
|
|
306
|
+
buildScreenOptions(definition().options, props)
|
|
174
307
|
),
|
|
175
|
-
key:
|
|
308
|
+
key: (props) => definition().key?.(props),
|
|
176
309
|
ownRoot,
|
|
177
|
-
arrangement:
|
|
310
|
+
arrangement: () => {
|
|
311
|
+
const arrangement = definition().arrangement;
|
|
312
|
+
return arrangement && toEngineArrangement(name, arrangement);
|
|
313
|
+
},
|
|
178
314
|
});
|
|
179
315
|
}
|
|
180
316
|
|
|
181
|
-
// The pair of a screen whose `arrangement` is known at registration: checked and filled in once.
|
|
182
|
-
function pairOf(
|
|
183
|
-
name: string,
|
|
184
|
-
arrangement: ScreenArrangement<any> | undefined
|
|
185
|
-
): (() => EngineArrangement) | undefined {
|
|
186
|
-
if (!arrangement) return undefined;
|
|
187
|
-
const pair = toEngineArrangement(name, arrangement);
|
|
188
|
-
return () => pair;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
317
|
function toDefinition(value: ScreenComponent | ScreenDefinition): ScreenDefinition {
|
|
192
|
-
|
|
318
|
+
// A screen of defineScreen renders as itself: React Refresh knows it by its module's export, and the version of the
|
|
319
|
+
// newest definition renders in its place.
|
|
320
|
+
const defined = definitionOf(value);
|
|
321
|
+
if (defined) return { ...defined, component: value as ScreenComponent };
|
|
322
|
+
return isObject(value) && 'component' in value ? value : { component: value as ScreenComponent };
|
|
193
323
|
}
|
|
194
324
|
|
|
195
325
|
/**
|
|
@@ -198,15 +328,17 @@ function toDefinition(value: ScreenComponent | ScreenDefinition): ScreenDefiniti
|
|
|
198
328
|
* screen) waits for it in Suspense.
|
|
199
329
|
*/
|
|
200
330
|
function lazyScreen(loadModule: () => ScreenModule | PromiseLike<ScreenModule>) {
|
|
201
|
-
|
|
331
|
+
// The screen the module gives; its definition is read from it each time, the newest after Fast Refresh.
|
|
332
|
+
let loaded: ScreenComponent | ScreenDefinition | undefined;
|
|
202
333
|
let loading: Promise<void> | undefined;
|
|
334
|
+
const definition = () => (loaded ? toDefinition(loaded) : undefined);
|
|
203
335
|
const load = (): Promise<void> | undefined => {
|
|
204
|
-
if (
|
|
336
|
+
if (loaded) return undefined;
|
|
205
337
|
if (!loading) {
|
|
206
338
|
loading = Promise.resolve()
|
|
207
339
|
.then(loadModule)
|
|
208
340
|
.then((module) => {
|
|
209
|
-
|
|
341
|
+
loaded = unwrapModule(module);
|
|
210
342
|
});
|
|
211
343
|
loading.catch(() => {
|
|
212
344
|
loading = undefined;
|
|
@@ -216,14 +348,14 @@ function lazyScreen(loadModule: () => ScreenModule | PromiseLike<ScreenModule>)
|
|
|
216
348
|
};
|
|
217
349
|
const Loaded = lazy(async () => {
|
|
218
350
|
await load();
|
|
219
|
-
return { default: definition!.component };
|
|
351
|
+
return { default: definition()!.component };
|
|
220
352
|
});
|
|
221
353
|
function LazyScreen(props: object) {
|
|
222
354
|
// The same component for the life of the view, so that the load doesn't mount it again.
|
|
223
|
-
const [Screen] = useState(() => definition?.component ?? Loaded);
|
|
355
|
+
const [Screen] = useState(() => definition()?.component ?? Loaded);
|
|
224
356
|
return <Screen {...props} />;
|
|
225
357
|
}
|
|
226
|
-
return { load, definition: () =>
|
|
358
|
+
return { load, definition, loaded: () => loaded, component: LazyScreen };
|
|
227
359
|
}
|
|
228
360
|
|
|
229
361
|
function unwrapModule(loaded: ScreenModule): ScreenComponent | ScreenDefinition {
|
|
@@ -297,15 +429,11 @@ function wrapScreen(name: string, Screen: ScreenComponent, wrapper: ScreenWrappe
|
|
|
297
429
|
*/
|
|
298
430
|
function wrapNativeScreen(
|
|
299
431
|
name: string,
|
|
300
|
-
|
|
432
|
+
entry: NativeScreen<any>,
|
|
301
433
|
{ provider: Provider, onError }: ScreenWrapper
|
|
302
434
|
): ComponentType<NativeControllerProps> {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// After each render React committed, so the native side never shows what the app did not.
|
|
306
|
-
useEffect(() => onProps(result));
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
435
|
+
// The screen's own component runs the hook: Fast Refresh puts a new version of it in place.
|
|
436
|
+
const Hook = entry as unknown as ComponentType<NativeScreenHookProps<object>>;
|
|
309
437
|
function NativeScreenProps({
|
|
310
438
|
componentId,
|
|
311
439
|
props,
|
package/src/api/theme/theme.ts
CHANGED
|
@@ -135,6 +135,11 @@ export function setTheme(next: NavigationTheme) {
|
|
|
135
135
|
listeners.forEach((listener) => listener());
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/** The direction of the app now: the config's, or the one `setDirection` gave since. */
|
|
139
|
+
export function getDirection(): NavigationDirection | undefined {
|
|
140
|
+
return direction;
|
|
141
|
+
}
|
|
142
|
+
|
|
138
143
|
/** Changes the direction of the app: the bars, the transitions and the swipe back follow at once. */
|
|
139
144
|
export function setDirection(next: NavigationDirection) {
|
|
140
145
|
direction = next;
|
package/src/engine/commands.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { columnOf, forgetPlace, layoutsOf, notePlaces, placeOf, stackOfColumn }
|
|
|
13
13
|
import { forgetProps } from './props';
|
|
14
14
|
import {
|
|
15
15
|
forgetMergedOptions,
|
|
16
|
+
forgetMergedPaths,
|
|
17
|
+
noteMergedOwnOptions,
|
|
16
18
|
rememberMergedOptions,
|
|
17
19
|
rootGoesOut,
|
|
18
20
|
sentRoot,
|
|
@@ -144,6 +146,7 @@ export function mergeOptions(componentId: string, options: Options): void {
|
|
|
144
146
|
const nativeOptions = toNativeOptions(options, componentId);
|
|
145
147
|
const send = () => {
|
|
146
148
|
rememberMergedOptions(componentId, options);
|
|
149
|
+
noteMergedOwnOptions(componentId, options);
|
|
147
150
|
native.mergeOptions(componentId, nativeOptions);
|
|
148
151
|
};
|
|
149
152
|
const prepared =
|
|
@@ -152,6 +155,24 @@ export function mergeOptions(componentId: string, options: Options): void {
|
|
|
152
155
|
else heldMerges.push({ id: componentId, after: shows, send });
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Takes the options at the paths, such as `['topBar', 'subtitle']`, out of the own options of a node, so it shows what
|
|
160
|
+
* the layouts around it and the defaults give there (Fast Refresh, development). It waits for the shows as a merge does.
|
|
161
|
+
*/
|
|
162
|
+
export function clearOptions(componentId: string, paths: readonly string[][]): void {
|
|
163
|
+
const send = () => {
|
|
164
|
+
forgetMergedPaths(componentId, paths);
|
|
165
|
+
native.clearOptions(
|
|
166
|
+
componentId,
|
|
167
|
+
paths.map((path) => path.join('.'))
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
const prepared =
|
|
171
|
+
preparedScreens.has(componentId) && !heldMerges.some(({ id }) => id === componentId);
|
|
172
|
+
if (waitingShows.size === 0 || prepared) send();
|
|
173
|
+
else heldMerges.push({ id: componentId, after: shows, send });
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
// Runs a show command; the function `run` gets lets the mergeOptions made after the show go, once it went out. It is
|
|
156
177
|
// called again when the command ends, so a command that fails before it goes out lets them go too.
|
|
157
178
|
function show<Result>(run: (goneOut: () => void) => Promise<Result>): Promise<Result> {
|
package/src/engine/layout.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { nextId } from './ids';
|
|
|
14
14
|
import { showNativeScreen } from './nativeScreens';
|
|
15
15
|
import { toNativeOptions } from './options';
|
|
16
16
|
import { setProps } from './props';
|
|
17
|
+
import { noteOwnOptions } from './rememberedOptions';
|
|
17
18
|
import { getStartInfo } from './native';
|
|
18
19
|
import { noteAlone } from './places';
|
|
19
20
|
import {
|
|
@@ -103,6 +104,13 @@ function screen(component: LayoutComponent, inPair = false, keyed = !inPair): La
|
|
|
103
104
|
if (nativeName) {
|
|
104
105
|
// A native screen: its props go to the native side as data, its own options over those of the show.
|
|
105
106
|
const options = takeButtonHandlers(id, component.options ?? {});
|
|
107
|
+
if (__DEV__) {
|
|
108
|
+
noteOwnOptions(
|
|
109
|
+
id,
|
|
110
|
+
name,
|
|
111
|
+
mergeOptions(screenOptions(name, props ?? {}), component.options ?? {})
|
|
112
|
+
);
|
|
113
|
+
}
|
|
106
114
|
const node = external({
|
|
107
115
|
id,
|
|
108
116
|
name: nativeName,
|
|
@@ -113,8 +121,9 @@ function screen(component: LayoutComponent, inPair = false, keyed = !inPair): La
|
|
|
113
121
|
}
|
|
114
122
|
// The screen's own options under those of the show (a command has them there already: merging again changes
|
|
115
123
|
// nothing).
|
|
116
|
-
const own = screenOptions(name, props ?? {});
|
|
117
|
-
|
|
124
|
+
const own = mergeOptions(screenOptions(name, props ?? {}), component.options ?? {});
|
|
125
|
+
noteOwnOptions(id, name, own);
|
|
126
|
+
const options = takeButtonHandlers(id, own);
|
|
118
127
|
const key = keyed ? screenKey(name, props ?? {}) : undefined;
|
|
119
128
|
setProps(id, props);
|
|
120
129
|
return {
|
|
@@ -38,6 +38,8 @@ interface NativeProps {
|
|
|
38
38
|
interface OpenScreen {
|
|
39
39
|
id: string;
|
|
40
40
|
name: string;
|
|
41
|
+
/** The props the command gave, as they are. */
|
|
42
|
+
given: object;
|
|
41
43
|
props: NativeProps;
|
|
42
44
|
/** The data the native side has, and the names of the functions; none before the command sends the screen. */
|
|
43
45
|
sent?: { data: Map<string, string>; functions: string };
|
|
@@ -78,7 +80,7 @@ export const nativeScreenHooks = {
|
|
|
78
80
|
* screen with `useProps` starts its hook, and its props go in once the hook has returned (`whenNativeScreensReady`).
|
|
79
81
|
*/
|
|
80
82
|
export function showNativeScreen(node: LayoutNode, name: string, props: object): void {
|
|
81
|
-
const screen: OpenScreen = { id: node.id, name, props: toNativeProps(props) };
|
|
83
|
+
const screen: OpenScreen = { id: node.id, name, given: props, props: toNativeProps(props) };
|
|
82
84
|
const Controller = nativeScreenController(name);
|
|
83
85
|
if (Controller) {
|
|
84
86
|
screen.first = deferred();
|
|
@@ -135,6 +137,13 @@ export function callNativeScreenFunction(id: string, name: string, args: unknown
|
|
|
135
137
|
return fn(...args);
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
/** The native screens with the name that are open, each with the props its command gave. */
|
|
141
|
+
export function openNativeScreens(name: string): { id: string; props: object }[] {
|
|
142
|
+
return [...screens.values()]
|
|
143
|
+
.filter((screen) => screen.name === name)
|
|
144
|
+
.map((screen) => ({ id: screen.id, props: screen.given }));
|
|
145
|
+
}
|
|
146
|
+
|
|
138
147
|
/** The native screen with the id, for the navigation of its calls. */
|
|
139
148
|
export function nativeScreenInfo(id: string): { id: string; name: string } | undefined {
|
|
140
149
|
const screen = screens.get(id);
|
package/src/engine/options.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Image, Platform, processColor } from 'react-native';
|
|
2
2
|
import type { ColorValue, ImageSourcePropType } from 'react-native';
|
|
3
3
|
import { nextId } from './ids';
|
|
4
4
|
import { rememberButtonRoot, rememberOptionsComponent, setProps } from './props';
|
|
@@ -162,19 +162,17 @@ function nativeColor(value: unknown): unknown {
|
|
|
162
162
|
return Platform.OS === 'ios' ? iosColor(value) : androidColor(value);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
// One color, a pair for the light and the dark appearance, or a semantic color; null takes the color away.
|
|
165
|
+
// One color, a pair for the light and the dark appearance, or a semantic color; null takes the color away. A pair goes
|
|
166
|
+
// as `{ light, dark }`, an app's `DynamicColorIOS` too; a side without a color is left out and shows the platform's.
|
|
166
167
|
function iosColor(value: unknown): unknown {
|
|
167
168
|
if (value === null) return 'NoColor';
|
|
168
169
|
if (!isFields(value)) return processColor(value as ColorValue);
|
|
169
170
|
if ('semantic' in value) return value;
|
|
170
171
|
const pair = ('dynamic' in value ? value.dynamic : value) as {
|
|
171
|
-
light
|
|
172
|
-
dark
|
|
172
|
+
light?: ColorValue;
|
|
173
|
+
dark?: ColorValue;
|
|
173
174
|
};
|
|
174
|
-
return
|
|
175
|
-
light: processColor(pair.light) as ColorValue,
|
|
176
|
-
dark: processColor(pair.dark) as ColorValue,
|
|
177
|
-
});
|
|
175
|
+
return { light: processColor(pair.light), dark: processColor(pair.dark) };
|
|
178
176
|
}
|
|
179
177
|
|
|
180
178
|
// Android always takes a pair; one color goes to both, and the side without a color has none.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Options } from '../interfaces/Options';
|
|
2
|
+
|
|
3
|
+
/** What an edit of a screen's options changed: the options to merge, and the paths of the options it took out. */
|
|
4
|
+
export interface OptionsChange {
|
|
5
|
+
/** The options that are new or have another value; undefined when none. */
|
|
6
|
+
merge?: Options;
|
|
7
|
+
/** The paths of the options set before and not now, such as `['topBar', 'subtitle']`. */
|
|
8
|
+
removed: string[][];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What `next` changes of `previous`, both the options of one screen. Objects compare key by key; anything else, an
|
|
13
|
+
* array such as buttons too, changes as a whole, and a function by identity (`sameValue`).
|
|
14
|
+
*/
|
|
15
|
+
export function optionsChange(previous: Options, next: Options): OptionsChange {
|
|
16
|
+
const removed: string[][] = [];
|
|
17
|
+
const merge = changed(previous, next, [], removed) as Options | undefined;
|
|
18
|
+
return merge ? { merge, removed } : { removed };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function changed(previous: unknown, next: unknown, path: string[], removed: string[][]): unknown {
|
|
22
|
+
if (isWholeValue(path) && isPlainObject(previous) && isPlainObject(next)) {
|
|
23
|
+
if (sameValue(previous, next)) return undefined;
|
|
24
|
+
// A part of it taken out: the native side may merge the parts, so the value goes first.
|
|
25
|
+
if (Object.keys(previous).some((key) => next[key] === undefined)) removed.push(path);
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
if (!isPlainObject(previous) || !isPlainObject(next)) {
|
|
29
|
+
return sameValue(previous, next) ? undefined : next;
|
|
30
|
+
}
|
|
31
|
+
const result: Record<string, unknown> = {};
|
|
32
|
+
for (const [key, value] of Object.entries(next)) {
|
|
33
|
+
if (value === undefined) continue;
|
|
34
|
+
const change = changed(previous[key], value, [...path, key], removed);
|
|
35
|
+
if (change !== undefined) result[key] = change;
|
|
36
|
+
}
|
|
37
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
38
|
+
if (value !== undefined && next[key] === undefined) removed.push([...path, key]);
|
|
39
|
+
}
|
|
40
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A color (`{ light, dark }`), an icon or an image, and a menu are one value each on the native side, not a group: a
|
|
44
|
+
// part of one taken out goes as the whole value again.
|
|
45
|
+
function isWholeValue(path: string[]): boolean {
|
|
46
|
+
const key = path[path.length - 1];
|
|
47
|
+
return key !== undefined && /^(color|icon|image|menu)$|(Color|Icon|Image)$/.test(key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether two values are the same: plain objects key by key, arrays item by item, anything else, functions too, by
|
|
52
|
+
* identity. A module that ran again makes its functions anew, so they count as changed: Hermes keeps no source to tell
|
|
53
|
+
* an edited one, and sending a button's handler again costs little. With `functionsAlike` any two functions are alike.
|
|
54
|
+
*/
|
|
55
|
+
export function sameValue(a: unknown, b: unknown, functionsAlike = false): boolean {
|
|
56
|
+
if (functionsAlike && typeof a === 'function' && typeof b === 'function') return true;
|
|
57
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
58
|
+
return (
|
|
59
|
+
a.length === b.length && a.every((item, index) => sameValue(item, b[index], functionsAlike))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
63
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
64
|
+
return [...keys].every((key) => sameValue(a[key], b[key], functionsAlike));
|
|
65
|
+
}
|
|
66
|
+
return Object.is(a, b);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
70
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
71
|
+
const prototype = Object.getPrototypeOf(value);
|
|
72
|
+
return prototype === Object.prototype || prototype === null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The part of an edit's change that the screen shows the old definition's value of: where the command that showed it,
|
|
77
|
+
* or a merge since, set another value, that value stays. `before` is what the old definition gave, `own` the screen's
|
|
78
|
+
* own options now.
|
|
79
|
+
*/
|
|
80
|
+
export function changeOverOwn(change: OptionsChange, before: Options, own: Options): OptionsChange {
|
|
81
|
+
// The functions of the definition are made anew each time it is read (a chain's button handlers): any function
|
|
82
|
+
// there stands for the definition's.
|
|
83
|
+
const fromDefinition = (path: string[]) =>
|
|
84
|
+
sameValue(valueAt(own, path), valueAt(before, path), true);
|
|
85
|
+
const removed = change.removed.filter(fromDefinition);
|
|
86
|
+
const merge =
|
|
87
|
+
change.merge && (keptWhere(change.merge, [], fromDefinition) as Options | undefined);
|
|
88
|
+
return merge ? { merge, removed } : { removed };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function keptWhere(value: unknown, path: string[], keep: (path: string[]) => boolean): unknown {
|
|
92
|
+
if (!isPlainObject(value) || isWholeValue(path)) return keep(path) ? value : undefined;
|
|
93
|
+
const result: Record<string, unknown> = {};
|
|
94
|
+
for (const [key, item] of Object.entries(value)) {
|
|
95
|
+
const kept = keptWhere(item, [...path, key], keep);
|
|
96
|
+
if (kept !== undefined) result[key] = kept;
|
|
97
|
+
}
|
|
98
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The value at a path of options, such as `['topBar', 'title', 'text']`; undefined where there is none. */
|
|
102
|
+
export function valueAt(options: unknown, path: readonly string[]): unknown {
|
|
103
|
+
return path.reduce<unknown>(
|
|
104
|
+
(value, key) => (isPlainObject(value) ? value[key] : undefined),
|
|
105
|
+
options
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Fast Refresh runs an edited module again, and every component it exports is a new function. React Refresh keeps
|
|
2
|
+
// the versions of one component in a family: Metro registers each export under its module and name, and React renders
|
|
3
|
+
// the newest version of the family in place of the old one. What the library keeps of a screen (its options, its key,
|
|
4
|
+
// its pair) is read through the same family, so an edit reaches it without a reload.
|
|
5
|
+
|
|
6
|
+
/** The versions of one component React Refresh keeps, `current` the newest. */
|
|
7
|
+
interface Family {
|
|
8
|
+
current: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ReactRefresh {
|
|
12
|
+
getFamilyByType?: (type: unknown) => Family | undefined;
|
|
13
|
+
register?: (type: unknown, id: string) => void;
|
|
14
|
+
createSignatureFunctionForTransform?: () => (
|
|
15
|
+
type: unknown,
|
|
16
|
+
key: string,
|
|
17
|
+
forceReset?: boolean
|
|
18
|
+
) => unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// React Native puts React Refresh where Metro's module system finds it, in development only.
|
|
22
|
+
function reactRefresh(): ReactRefresh | undefined {
|
|
23
|
+
if (!__DEV__) return undefined;
|
|
24
|
+
const global = globalThis as { __METRO_GLOBAL_PREFIX__?: string } & Record<string, unknown>;
|
|
25
|
+
return global[`${global.__METRO_GLOBAL_PREFIX__ ?? ''}__ReactRefresh`] as
|
|
26
|
+
ReactRefresh | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether React Refresh runs: development with Metro. */
|
|
30
|
+
export function refreshRuns(): boolean {
|
|
31
|
+
return reactRefresh()?.getFamilyByType !== undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The family a component's versions are kept in; undefined in a release build or for a value Metro didn't export. */
|
|
35
|
+
export function familyOf(type: unknown): Family | undefined {
|
|
36
|
+
return reactRefresh()?.getFamilyByType?.(type);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The newest version of a component after Fast Refresh ran its module again; the component itself when there is none
|
|
41
|
+
* newer, and in a release build.
|
|
42
|
+
*/
|
|
43
|
+
export function newestType<T extends object>(type: T): T {
|
|
44
|
+
return (familyOf(type)?.current as T | undefined) ?? type;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let redefinedListener: ((screen: object) => void) | undefined;
|
|
48
|
+
|
|
49
|
+
/** Sets what runs when a module makes a screen again: Fast Refresh ran it after an edit. */
|
|
50
|
+
export function onRedefined(listener: (screen: object) => void): void {
|
|
51
|
+
redefinedListener = listener;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Tells about a screen a module made, once the module ran: Metro registers its exports after it, so the family of the
|
|
56
|
+
* screen is known by then, and a new version of a screen that shows is found by it. Development only.
|
|
57
|
+
*/
|
|
58
|
+
export function redefined(screen: object): void {
|
|
59
|
+
if (!__DEV__ || !redefinedListener) return;
|
|
60
|
+
const listener = redefinedListener;
|
|
61
|
+
Promise.resolve().then(() => listener(screen));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The id each family of screens gives the components they render, so their versions join one family too.
|
|
65
|
+
const innerIds = new WeakMap<Family, string>();
|
|
66
|
+
let innerCount = 0;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Puts a component a screen or the app renders (`owner`: the screen, or the app made by createNavigation; `role`: which
|
|
70
|
+
* of its components) in a family of its own, once the owner's module ran and Metro has given the owner its family: the
|
|
71
|
+
* next version of the owner registers its component under the same id, so React Refresh keeps the component's state
|
|
72
|
+
* when the module runs again, also when the module doesn't export it. A component React Refresh knows already (its
|
|
73
|
+
* module exports it, or Babel registered it) keeps its own family. Development only.
|
|
74
|
+
*/
|
|
75
|
+
export function keepStateOf(owner: object, component: unknown, role = 'component'): void {
|
|
76
|
+
const refresh = reactRefresh();
|
|
77
|
+
if (!refresh?.register || component === undefined) return;
|
|
78
|
+
const register = refresh.register;
|
|
79
|
+
Promise.resolve().then(() => {
|
|
80
|
+
const family = familyOf(owner);
|
|
81
|
+
if (!family) return;
|
|
82
|
+
let id = innerIds.get(family);
|
|
83
|
+
if (id === undefined) {
|
|
84
|
+
id = `apexnative.navigation#${++innerCount}`;
|
|
85
|
+
innerIds.set(family, id);
|
|
86
|
+
}
|
|
87
|
+
register(component, `${id}.${role}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Makes React Refresh mount a component of the library again, not keep its state, when a new version of it comes: its
|
|
93
|
+
* hooks are the app's, which may have changed, and the library's code has no signature of them. Development only.
|
|
94
|
+
*/
|
|
95
|
+
export function resetOnEdit(type: object): void {
|
|
96
|
+
reactRefresh()?.createSignatureFunctionForTransform?.()(type, 'apexnative.navigation', true);
|
|
97
|
+
}
|