@react-navigation/lynx 0.0.0 → 0.1.0
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/package.json +52 -4
- package/src/NavigationContainer.tsx +79 -0
- package/src/createStaticNavigation.tsx +41 -0
- package/src/index.tsx +13 -0
- package/src/react-compat.ts +40 -0
- package/src/stack/index.tsx +23 -0
- package/src/stack/navigators/createLynxStackNavigator.tsx +72 -0
- package/src/stack/types.ts +66 -0
- package/src/stack/views/CardScreen.tsx +102 -0
- package/src/stack/views/LynxStackView.tsx +145 -0
- package/src/stack/views/LynxStackViewState.ts +258 -0
- package/src/stack/views/__tests__/LynxStackViewState.test.tsx +506 -0
- package/src/theming/DarkTheme.ts +16 -0
- package/src/theming/LightTheme.ts +17 -0
- package/src/theming/fonts.ts +25 -0
- package/src/types.ts +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-navigation/lynx",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lynx integration for React Navigation",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"lynx",
|
|
8
|
+
"react-navigation"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://reactnavigation.org",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/react-navigation/react-navigation-lynx/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/react-navigation/react-navigation-lynx.git",
|
|
17
|
+
"directory": "packages/lynx"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./src/index.tsx",
|
|
24
|
+
"types": "./src/index.tsx",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./src/index.tsx",
|
|
27
|
+
"./stack": "./src/stack/index.tsx",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@react-navigation/core": "^8.0.0-alpha.33"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@lynx-js/react": "*",
|
|
38
|
+
"lynx-screens": "*"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@lynx-js/react": "^0.125.0",
|
|
42
|
+
"@lynx-js/testing-environment": "^0.3.3",
|
|
43
|
+
"@lynx-js/types": "^4.1.0",
|
|
44
|
+
"@types/react": "~19.2.17",
|
|
45
|
+
"jsdom": "^26.0.0",
|
|
46
|
+
"typescript": "^6.0.3",
|
|
47
|
+
"vitest": "^3.2.0",
|
|
48
|
+
"lynx-screens": "0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseNavigationContainer,
|
|
3
|
+
type NavigationContainerProps,
|
|
4
|
+
type NavigationContainerRef,
|
|
5
|
+
type NavigationState,
|
|
6
|
+
type ParamListBase,
|
|
7
|
+
type Theme,
|
|
8
|
+
ThemeProvider,
|
|
9
|
+
} from '@react-navigation/core';
|
|
10
|
+
import * as React from 'react';
|
|
11
|
+
|
|
12
|
+
import { LightTheme } from './theming/LightTheme';
|
|
13
|
+
|
|
14
|
+
export type NavigationContainerLynxProps<
|
|
15
|
+
ParamList extends {} = ParamListBase,
|
|
16
|
+
> = NavigationContainerProps & {
|
|
17
|
+
/**
|
|
18
|
+
* Theme handed to `useTheme` and to any navigator that reads colors.
|
|
19
|
+
*/
|
|
20
|
+
theme?: Theme | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Rendered while persisted state is being restored.
|
|
23
|
+
*/
|
|
24
|
+
fallback?: React.ReactNode | undefined;
|
|
25
|
+
ref?: React.Ref<NavigationContainerRef<ParamList>> | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The Lynx counterpart of `@react-navigation/native`'s `NavigationContainer`.
|
|
30
|
+
*
|
|
31
|
+
* It is the platform layer's entry point: everything a navigator needs that is
|
|
32
|
+
* not navigation state itself - the theme, and eventually deep linking and
|
|
33
|
+
* state persistence - is wired here rather than in each navigator.
|
|
34
|
+
*
|
|
35
|
+
* Not yet ported from React Native:
|
|
36
|
+
*
|
|
37
|
+
* - deep linking (`linking`), which needs a Lynx URL source
|
|
38
|
+
* - state persistence, which needs a Lynx storage binding
|
|
39
|
+
* - `useDocumentTitle`, which is a browser concern and has no Lynx meaning
|
|
40
|
+
*
|
|
41
|
+
* The hardware back button is deliberately absent: on Lynx it is handled by
|
|
42
|
+
* the native stack per screen, through `preventNativeDismiss` and the dismiss
|
|
43
|
+
* callbacks, so a container-level handler would fight with it.
|
|
44
|
+
*/
|
|
45
|
+
export function NavigationContainer<ParamList extends {} = ParamListBase>({
|
|
46
|
+
theme = LightTheme,
|
|
47
|
+
fallback = null,
|
|
48
|
+
onStateChange,
|
|
49
|
+
ref,
|
|
50
|
+
...rest
|
|
51
|
+
}: NavigationContainerLynxProps<ParamList>) {
|
|
52
|
+
const refContainer =
|
|
53
|
+
React.useRef<NavigationContainerRef<ParamListBase>>(null);
|
|
54
|
+
|
|
55
|
+
React.useImperativeHandle(
|
|
56
|
+
ref,
|
|
57
|
+
() => refContainer.current as NavigationContainerRef<ParamList>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handleStateChange = (state: Readonly<NavigationState> | undefined) => {
|
|
61
|
+
onStateChange?.(state);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Kept for parity with React Native, where this renders while persisted
|
|
65
|
+
// state is being restored. With no persistence yet there is nothing to wait
|
|
66
|
+
// for, so it only shows if a caller passes `fallback` and no children.
|
|
67
|
+
if (rest.children == null) {
|
|
68
|
+
return <ThemeProvider value={theme}>{fallback}</ThemeProvider>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<BaseNavigationContainer
|
|
73
|
+
{...rest}
|
|
74
|
+
theme={theme}
|
|
75
|
+
onStateChange={handleStateChange}
|
|
76
|
+
ref={refContainer}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type RootParamList,
|
|
3
|
+
type StaticNavigation,
|
|
4
|
+
} from '@react-navigation/core';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
import { NavigationContainer } from './NavigationContainer';
|
|
8
|
+
|
|
9
|
+
type Props<ParamList extends {}> = Omit<
|
|
10
|
+
React.ComponentProps<typeof NavigationContainer<ParamList>>,
|
|
11
|
+
'children'
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a navigation component from a static navigation config, the same way
|
|
16
|
+
* `@react-navigation/native` does. The returned component wraps
|
|
17
|
+
* `NavigationContainer`.
|
|
18
|
+
*
|
|
19
|
+
* React Native's version also derives a linking config from the tree here.
|
|
20
|
+
* That is left out until the container can act on one - generating paths that
|
|
21
|
+
* nothing consumes would only look like deep linking works.
|
|
22
|
+
*
|
|
23
|
+
* @param tree Static navigation config.
|
|
24
|
+
* @returns Navigation component to use in your app.
|
|
25
|
+
*/
|
|
26
|
+
export function createStaticNavigation(tree: StaticNavigation<any>) {
|
|
27
|
+
const Component = tree.getComponent();
|
|
28
|
+
|
|
29
|
+
function Navigation<ParamList extends {} = RootParamList>({
|
|
30
|
+
ref,
|
|
31
|
+
...rest
|
|
32
|
+
}: Props<ParamList>) {
|
|
33
|
+
return (
|
|
34
|
+
<NavigationContainer {...rest} ref={ref}>
|
|
35
|
+
<Component />
|
|
36
|
+
</NavigationContainer>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Navigation;
|
|
41
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The platform layer, mirroring what `@react-navigation/native` does for
|
|
2
|
+
// React Native: it owns everything Lynx-specific that every navigator needs,
|
|
3
|
+
// and re-exports core so apps have a single import surface.
|
|
4
|
+
export { createStaticNavigation } from './createStaticNavigation';
|
|
5
|
+
export type { LynxTheme } from './types';
|
|
6
|
+
export {
|
|
7
|
+
NavigationContainer,
|
|
8
|
+
type NavigationContainerLynxProps,
|
|
9
|
+
} from './NavigationContainer';
|
|
10
|
+
export { DarkTheme } from './theming/DarkTheme';
|
|
11
|
+
export { LightTheme as DefaultTheme } from './theming/LightTheme';
|
|
12
|
+
|
|
13
|
+
export * from '@react-navigation/core';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// ReactLynx is Preact-based and does not implement every hook that
|
|
2
|
+
// `@react-navigation/core` imports from `react`. The Lynx build aliases
|
|
3
|
+
// `react` to this module (runtime alias in the bundler config, `paths` in
|
|
4
|
+
// tsconfig) so those imports link and both sides share one set of types.
|
|
5
|
+
//
|
|
6
|
+
// Tracking the gaps, counted against core's runtime source:
|
|
7
|
+
//
|
|
8
|
+
// - `use` (4 files): every call site passes a Context, never a promise, so
|
|
9
|
+
// `useContext` is a faithful stand-in today. Preact 11 ships the real one.
|
|
10
|
+
// - `startTransition` / `useTransition` (2 files): ReactLynx keeps these on
|
|
11
|
+
// its compat entry rather than the main one.
|
|
12
|
+
// - `useInsertionEffect` (15 files): no equivalent. `useLayoutEffect` is
|
|
13
|
+
// close but not equal - insertion effects run before layout effects, and
|
|
14
|
+
// layout effects run bottom-up, so a parent writing a ref in an insertion
|
|
15
|
+
// effect is guaranteed fresh when a child's layout effect reads it. Twelve
|
|
16
|
+
// of the fifteen are plain latest-ref writes where that gap is invisible;
|
|
17
|
+
// `useRegisterNavigator`, `usePreventRemove` and `PreventRemoveProvider`
|
|
18
|
+
// also register cleanups, and those are the ones to watch.
|
|
19
|
+
import * as ReactLynx from '@lynx-js/react';
|
|
20
|
+
import { useContext, useLayoutEffect } from '@lynx-js/react';
|
|
21
|
+
import { startTransition, useTransition } from '@lynx-js/react/compat';
|
|
22
|
+
|
|
23
|
+
export * from '@lynx-js/react';
|
|
24
|
+
export { startTransition, useTransition };
|
|
25
|
+
|
|
26
|
+
export const use = useContext;
|
|
27
|
+
export const useInsertionEffect = useLayoutEffect;
|
|
28
|
+
|
|
29
|
+
// `@lynx-js/react` declares no default export. Build one that keeps every
|
|
30
|
+
// generic signature intact, otherwise `import React from 'react'` degrades to
|
|
31
|
+
// `any` and callers lose inference (e.g. `React.useState` setter callbacks).
|
|
32
|
+
const React = {
|
|
33
|
+
...ReactLynx,
|
|
34
|
+
use,
|
|
35
|
+
useInsertionEffect,
|
|
36
|
+
startTransition,
|
|
37
|
+
useTransition,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default React;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigators
|
|
3
|
+
*/
|
|
4
|
+
export {
|
|
5
|
+
createLynxStackNavigator,
|
|
6
|
+
createLynxStackScreen,
|
|
7
|
+
type LynxStackTypeBag,
|
|
8
|
+
} from './navigators/createLynxStackNavigator';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Types
|
|
12
|
+
*/
|
|
13
|
+
export type {
|
|
14
|
+
LynxStackDescriptor,
|
|
15
|
+
LynxStackDescriptorMap,
|
|
16
|
+
LynxStackNavigationEventMap,
|
|
17
|
+
LynxStackNavigationHelpers,
|
|
18
|
+
LynxStackNavigationOptions,
|
|
19
|
+
LynxStackNavigationProp,
|
|
20
|
+
LynxStackNavigatorProps,
|
|
21
|
+
LynxStackPresentation,
|
|
22
|
+
LynxStackScreenProps,
|
|
23
|
+
} from './types';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createNavigatorFactory,
|
|
3
|
+
createScreenFactory,
|
|
4
|
+
type NavigatorTypeBagBase,
|
|
5
|
+
type ParamListBase,
|
|
6
|
+
type StackActionHelpers,
|
|
7
|
+
type StackNavigationState,
|
|
8
|
+
StackRouter,
|
|
9
|
+
type StackRouterOptions,
|
|
10
|
+
useNavigationBuilder,
|
|
11
|
+
} from '@react-navigation/core';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
LynxStackNavigationEventMap,
|
|
15
|
+
LynxStackNavigationOptions,
|
|
16
|
+
LynxStackNavigatorProps,
|
|
17
|
+
} from '../types';
|
|
18
|
+
import { LynxStackView } from '../views/LynxStackView';
|
|
19
|
+
|
|
20
|
+
function LynxStackNavigator({
|
|
21
|
+
initialRouteName,
|
|
22
|
+
routeNamesChangeBehavior,
|
|
23
|
+
children,
|
|
24
|
+
layout,
|
|
25
|
+
screenListeners,
|
|
26
|
+
screenOptions,
|
|
27
|
+
screenLayout,
|
|
28
|
+
router,
|
|
29
|
+
...rest
|
|
30
|
+
}: LynxStackNavigatorProps) {
|
|
31
|
+
const { state, descriptors, navigation, NavigationContent } =
|
|
32
|
+
useNavigationBuilder<
|
|
33
|
+
StackNavigationState<ParamListBase>,
|
|
34
|
+
StackRouterOptions,
|
|
35
|
+
StackActionHelpers<ParamListBase>,
|
|
36
|
+
LynxStackNavigationOptions,
|
|
37
|
+
LynxStackNavigationEventMap
|
|
38
|
+
>(StackRouter, {
|
|
39
|
+
initialRouteName,
|
|
40
|
+
routeNamesChangeBehavior,
|
|
41
|
+
children,
|
|
42
|
+
layout,
|
|
43
|
+
screenListeners,
|
|
44
|
+
screenOptions,
|
|
45
|
+
screenLayout,
|
|
46
|
+
router,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<NavigationContent>
|
|
51
|
+
<LynxStackView
|
|
52
|
+
{...rest}
|
|
53
|
+
state={state}
|
|
54
|
+
navigation={navigation}
|
|
55
|
+
descriptors={descriptors}
|
|
56
|
+
/>
|
|
57
|
+
</NavigationContent>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LynxStackTypeBag extends NavigatorTypeBagBase {
|
|
62
|
+
State: StackNavigationState<this['ParamList']>;
|
|
63
|
+
ScreenOptions: LynxStackNavigationOptions;
|
|
64
|
+
EventMap: LynxStackNavigationEventMap;
|
|
65
|
+
ActionHelpers: StackActionHelpers<this['ParamList']>;
|
|
66
|
+
Navigator: typeof LynxStackNavigator;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const createLynxStackNavigator =
|
|
70
|
+
createNavigatorFactory<LynxStackTypeBag>(LynxStackNavigator);
|
|
71
|
+
|
|
72
|
+
export const createLynxStackScreen = createScreenFactory<LynxStackTypeBag>();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DefaultNavigatorOptions,
|
|
3
|
+
Descriptor,
|
|
4
|
+
NavigationHelpers,
|
|
5
|
+
NavigationProp,
|
|
6
|
+
ParamListBase,
|
|
7
|
+
RouteProp,
|
|
8
|
+
StackActionHelpers,
|
|
9
|
+
StackNavigationState,
|
|
10
|
+
StackRouterOptions,
|
|
11
|
+
} from '@react-navigation/core';
|
|
12
|
+
import type * as Lynx from '@lynx-js/types';
|
|
13
|
+
|
|
14
|
+
export type LynxStackPresentation = 'card' | 'formSheet';
|
|
15
|
+
|
|
16
|
+
export type LynxStackNavigationOptions = {
|
|
17
|
+
presentation?: LynxStackPresentation | undefined;
|
|
18
|
+
contentStyle?: Lynx.CSSProperties | undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type LynxStackNavigationEventMap = {
|
|
22
|
+
transitionStart: { data: { closing: boolean } };
|
|
23
|
+
transitionEnd: { data: { closing: boolean } };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type LynxStackNavigationProp<
|
|
27
|
+
ParamList extends ParamListBase,
|
|
28
|
+
RouteName extends keyof ParamList = string,
|
|
29
|
+
> = NavigationProp<
|
|
30
|
+
ParamList,
|
|
31
|
+
RouteName,
|
|
32
|
+
StackNavigationState<ParamList>,
|
|
33
|
+
LynxStackNavigationOptions,
|
|
34
|
+
LynxStackNavigationEventMap,
|
|
35
|
+
StackActionHelpers<ParamList>
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
export type LynxStackScreenProps<
|
|
39
|
+
ParamList extends ParamListBase,
|
|
40
|
+
RouteName extends keyof ParamList = string,
|
|
41
|
+
> = {
|
|
42
|
+
navigation: LynxStackNavigationProp<ParamList, RouteName>;
|
|
43
|
+
route: RouteProp<ParamList, RouteName>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type LynxStackNavigationHelpers = NavigationHelpers<
|
|
47
|
+
ParamListBase,
|
|
48
|
+
LynxStackNavigationEventMap
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
export type LynxStackDescriptor = Descriptor<
|
|
52
|
+
LynxStackNavigationOptions,
|
|
53
|
+
LynxStackNavigationProp<ParamListBase>,
|
|
54
|
+
RouteProp<ParamListBase>
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
export type LynxStackDescriptorMap = Record<string, LynxStackDescriptor>;
|
|
58
|
+
|
|
59
|
+
export type LynxStackNavigatorProps = DefaultNavigatorOptions<
|
|
60
|
+
ParamListBase,
|
|
61
|
+
StackNavigationState<ParamListBase>,
|
|
62
|
+
LynxStackNavigationOptions,
|
|
63
|
+
LynxStackNavigationEventMap,
|
|
64
|
+
LynxStackNavigationHelpers
|
|
65
|
+
> &
|
|
66
|
+
StackRouterOptions;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { NavigationProvider, usePreventRemoveContext } from '@react-navigation/core';
|
|
2
|
+
import { StackScreenNativeComponent } from 'lynx-screens';
|
|
3
|
+
|
|
4
|
+
import type { LynxStackDescriptor, LynxStackNavigationHelpers } from '../types';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
descriptor: LynxStackDescriptor;
|
|
8
|
+
navigation: LynxStackNavigationHelpers;
|
|
9
|
+
isFocused: boolean;
|
|
10
|
+
isBeforeLast: boolean;
|
|
11
|
+
isPopped: boolean;
|
|
12
|
+
isDetached: boolean;
|
|
13
|
+
onRemovePoppedRoute: (key: string) => void;
|
|
14
|
+
onNativeDismiss: () => void;
|
|
15
|
+
onNativeDismissPrevented: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function CardScreen({
|
|
19
|
+
descriptor,
|
|
20
|
+
navigation,
|
|
21
|
+
isFocused,
|
|
22
|
+
isBeforeLast,
|
|
23
|
+
isPopped,
|
|
24
|
+
isDetached,
|
|
25
|
+
onRemovePoppedRoute,
|
|
26
|
+
onNativeDismiss,
|
|
27
|
+
onNativeDismissPrevented,
|
|
28
|
+
}: Props) {
|
|
29
|
+
const { preventedRoutes } = usePreventRemoveContext();
|
|
30
|
+
|
|
31
|
+
const { route, options } = descriptor;
|
|
32
|
+
const { contentStyle } = options;
|
|
33
|
+
|
|
34
|
+
// A screen is kept out of the native hierarchy while it animates out or
|
|
35
|
+
// while it sits above the focused index (preloaded / retained).
|
|
36
|
+
const activityMode = isPopped || isDetached ? 'detached' : 'attached';
|
|
37
|
+
|
|
38
|
+
// Prevention comes from `usePreventRemove` and nothing else. A static option
|
|
39
|
+
// would be a trap: the native side would block the gesture, then the
|
|
40
|
+
// `onNativeDismissPrevented` round-trip below would pop the route anyway,
|
|
41
|
+
// because only a `beforeRemove` listener can cancel that dispatch.
|
|
42
|
+
const isRemovePrevented = preventedRoutes[route.key]?.preventRemove;
|
|
43
|
+
|
|
44
|
+
// Only the focused screen, the one behind it (so a swipe back reveals fresh
|
|
45
|
+
// content) and detached screens stay live. `isBeforeLast` and `isFocused`
|
|
46
|
+
// are read here rather than in the parent so the reasoning stays with the
|
|
47
|
+
// component that acts on it.
|
|
48
|
+
const isLive = isFocused || isBeforeLast || isDetached;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<StackScreenNativeComponent
|
|
52
|
+
screenKey={route.key}
|
|
53
|
+
activityMode={activityMode}
|
|
54
|
+
preventNativeDismiss={isRemovePrevented}
|
|
55
|
+
onWillAppear={() =>
|
|
56
|
+
navigation.emit({
|
|
57
|
+
type: 'transitionStart',
|
|
58
|
+
data: { closing: false },
|
|
59
|
+
target: route.key,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
onDidAppear={() =>
|
|
63
|
+
navigation.emit({
|
|
64
|
+
type: 'transitionEnd',
|
|
65
|
+
data: { closing: false },
|
|
66
|
+
target: route.key,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
onWillDisappear={() =>
|
|
70
|
+
navigation.emit({
|
|
71
|
+
type: 'transitionStart',
|
|
72
|
+
data: { closing: true },
|
|
73
|
+
target: route.key,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
onDidDisappear={() =>
|
|
77
|
+
navigation.emit({
|
|
78
|
+
type: 'transitionEnd',
|
|
79
|
+
data: { closing: true },
|
|
80
|
+
target: route.key,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
onDismiss={onRemovePoppedRoute}
|
|
84
|
+
onNativeDismiss={onNativeDismiss}
|
|
85
|
+
onNativeDismissPrevented={onNativeDismissPrevented}
|
|
86
|
+
>
|
|
87
|
+
<NavigationProvider navigation={descriptor.navigation} route={route}>
|
|
88
|
+
<view
|
|
89
|
+
style={{
|
|
90
|
+
display: 'flex',
|
|
91
|
+
flexDirection: 'column',
|
|
92
|
+
width: '100%',
|
|
93
|
+
height: '100%',
|
|
94
|
+
...contentStyle,
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
{isLive ? descriptor.render() : null}
|
|
98
|
+
</view>
|
|
99
|
+
</NavigationProvider>
|
|
100
|
+
</StackScreenNativeComponent>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ParamListBase,
|
|
3
|
+
StackActions,
|
|
4
|
+
type StackNavigationState,
|
|
5
|
+
} from '@react-navigation/core';
|
|
6
|
+
import { StackHostNativeComponent } from 'lynx-screens';
|
|
7
|
+
import type { Dispatch, ReactElement } from 'react';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
LynxStackDescriptorMap,
|
|
11
|
+
LynxStackNavigationHelpers,
|
|
12
|
+
} from '../types';
|
|
13
|
+
import { CardScreen } from './CardScreen';
|
|
14
|
+
import {
|
|
15
|
+
type LynxStackViewState,
|
|
16
|
+
type LynxStackViewStateAction,
|
|
17
|
+
useViewState,
|
|
18
|
+
} from './LynxStackViewState';
|
|
19
|
+
|
|
20
|
+
type Props = {
|
|
21
|
+
state: StackNavigationState<ParamListBase>;
|
|
22
|
+
navigation: LynxStackNavigationHelpers;
|
|
23
|
+
descriptors: LynxStackDescriptorMap;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ContentProps = Props &
|
|
27
|
+
Pick<LynxStackViewState, 'renderedRoutes' | 'poppedByKey'> & {
|
|
28
|
+
dispatch: Dispatch<LynxStackViewStateAction>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function LynxStackViewContent({
|
|
32
|
+
state,
|
|
33
|
+
navigation,
|
|
34
|
+
descriptors,
|
|
35
|
+
renderedRoutes,
|
|
36
|
+
poppedByKey,
|
|
37
|
+
dispatch,
|
|
38
|
+
}: ContentProps) {
|
|
39
|
+
const routeIndexByKey = new Map(
|
|
40
|
+
state.routes.map((route, index) => [route.key, index])
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const onRemovePoppedRoute = (key: string) => {
|
|
44
|
+
dispatch({ type: 'REMOVE_POPPED_ROUTE', key });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const onNativeDismiss = (key: string) => {
|
|
48
|
+
const currentState = navigation.getState();
|
|
49
|
+
const index = currentState.routes.findIndex((route) => route.key === key);
|
|
50
|
+
|
|
51
|
+
if (index === -1) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const dismissCount = currentState.index - index + 1;
|
|
56
|
+
|
|
57
|
+
if (dismissCount < 1) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The native side has already taken these screens off the stack, so the
|
|
62
|
+
// reducer must not keep them rendered waiting for a pop animation.
|
|
63
|
+
dispatch({
|
|
64
|
+
type: 'ADD_NATIVELY_DISMISSED_ROUTES',
|
|
65
|
+
keys: currentState.routes
|
|
66
|
+
.slice(index, currentState.index + 1)
|
|
67
|
+
.map((route) => route.key),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
navigation.dispatch({
|
|
71
|
+
...StackActions.pop(dismissCount),
|
|
72
|
+
source: key,
|
|
73
|
+
target: currentState.key,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// A prevented dismiss still has to reach the router: that is what gives
|
|
78
|
+
// `usePreventRemove` its `beforeRemove` event to act on.
|
|
79
|
+
const onNativeDismissPrevented = (key: string) => {
|
|
80
|
+
const currentState = navigation.getState();
|
|
81
|
+
|
|
82
|
+
navigation.dispatch({
|
|
83
|
+
...StackActions.pop(),
|
|
84
|
+
source: key,
|
|
85
|
+
target: currentState.key,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const cards = renderedRoutes.reduce<ReactElement[]>((result, route) => {
|
|
90
|
+
const index = routeIndexByKey.get(route.key);
|
|
91
|
+
const popped = poppedByKey.get(route.key);
|
|
92
|
+
const descriptor = descriptors[route.key] ?? popped?.descriptor;
|
|
93
|
+
|
|
94
|
+
if (descriptor == null) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Couldn't find descriptor for route ${route.name} (${route.key}). This is likely a bug.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const presentation = descriptor.options.presentation ?? 'card';
|
|
101
|
+
|
|
102
|
+
if (presentation !== 'card') {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`The route '${route.name}' uses the '${presentation}' presentation, which the Lynx stack does not support yet. Only 'card' is available while form sheet support lands in lynx-screens.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
result.push(
|
|
109
|
+
<CardScreen
|
|
110
|
+
key={route.key}
|
|
111
|
+
descriptor={descriptor}
|
|
112
|
+
navigation={navigation}
|
|
113
|
+
isFocused={index === state.index}
|
|
114
|
+
isBeforeLast={index === state.index - 1}
|
|
115
|
+
isPopped={popped != null}
|
|
116
|
+
isDetached={index != null && index > state.index}
|
|
117
|
+
onRemovePoppedRoute={onRemovePoppedRoute}
|
|
118
|
+
onNativeDismiss={() => onNativeDismiss(route.key)}
|
|
119
|
+
onNativeDismissPrevented={() => onNativeDismissPrevented(route.key)}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
return <StackHostNativeComponent>{cards}</StackHostNativeComponent>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function LynxStackView({ state, navigation, descriptors }: Props) {
|
|
130
|
+
const [{ renderedRoutes, poppedByKey }, dispatch] = useViewState({
|
|
131
|
+
state,
|
|
132
|
+
descriptors,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<LynxStackViewContent
|
|
137
|
+
state={state}
|
|
138
|
+
navigation={navigation}
|
|
139
|
+
descriptors={descriptors}
|
|
140
|
+
renderedRoutes={renderedRoutes}
|
|
141
|
+
poppedByKey={poppedByKey}
|
|
142
|
+
dispatch={dispatch}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}
|