@react-navigation/lynx 0.4.5 → 0.4.6
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
CHANGED
package/src/stack/types.ts
CHANGED
|
@@ -21,6 +21,20 @@ export type LynxStackPresentation = 'card' | 'formSheet';
|
|
|
21
21
|
export type LynxStackNavigationOptions = {
|
|
22
22
|
presentation?: LynxStackPresentation | undefined;
|
|
23
23
|
contentStyle?: Lynx.CSSProperties | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* What should happen when screens become inactive.
|
|
26
|
+
* - `pause`: Effects are cleaned up.
|
|
27
|
+
* - `unmount`: Screen is unmounted
|
|
28
|
+
* - `none`: Screen renders normally
|
|
29
|
+
*
|
|
30
|
+
* Defaults to `pause`.
|
|
31
|
+
*
|
|
32
|
+
* Preloaded screens won't be paused until after navigated to.
|
|
33
|
+
* This makes sure that effects are run to initialize the screen.
|
|
34
|
+
*
|
|
35
|
+
* Screens with nested navigators and last 2 screens won't be unmounted.
|
|
36
|
+
*/
|
|
37
|
+
inactiveBehavior?: 'pause' | 'unmount' | 'none' | undefined;
|
|
24
38
|
/**
|
|
25
39
|
* Heights the sheet can rest at, as fractions of the screen, or
|
|
26
40
|
* `'fitToContents'` to measure the content. `formSheet` only, as is every
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Copyright 2026 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
|
|
5
|
+
import type { ReactNode } from '@lynx-js/react';
|
|
6
|
+
|
|
7
|
+
import type { LynxStackDescriptor } from '../types';
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
descriptor: LynxStackDescriptor;
|
|
11
|
+
activityMode: 'normal' | 'inert' | 'paused' | 'unmounted';
|
|
12
|
+
backgroundColor: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function CardContent({
|
|
16
|
+
descriptor,
|
|
17
|
+
activityMode,
|
|
18
|
+
backgroundColor,
|
|
19
|
+
}: Props) {
|
|
20
|
+
const { options, render } = descriptor;
|
|
21
|
+
|
|
22
|
+
const { contentStyle } = options;
|
|
23
|
+
|
|
24
|
+
let contentElement: ReactNode;
|
|
25
|
+
|
|
26
|
+
if (activityMode === 'unmounted') {
|
|
27
|
+
contentElement = null;
|
|
28
|
+
} else {
|
|
29
|
+
contentElement = (
|
|
30
|
+
<view
|
|
31
|
+
user-interaction-enabled={activityMode !== 'inert'}
|
|
32
|
+
style={styles.content}
|
|
33
|
+
>
|
|
34
|
+
{render()}
|
|
35
|
+
</view>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<view style={{ ...styles.container, backgroundColor, ...contentStyle }}>
|
|
41
|
+
{contentElement}
|
|
42
|
+
</view>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const styles = {
|
|
47
|
+
container: {
|
|
48
|
+
display: 'flex',
|
|
49
|
+
flexDirection: 'column',
|
|
50
|
+
width: '100%',
|
|
51
|
+
height: '100%',
|
|
52
|
+
},
|
|
53
|
+
content: {
|
|
54
|
+
display: 'flex',
|
|
55
|
+
flexDirection: 'column',
|
|
56
|
+
width: '100%',
|
|
57
|
+
height: '100%',
|
|
58
|
+
},
|
|
59
|
+
} as const;
|
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
3
|
// LICENSE file in the root directory of this source tree.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
NavigationProvider,
|
|
7
|
+
usePreventRemoveContext,
|
|
8
|
+
useTheme,
|
|
9
|
+
} from '@react-navigation/core';
|
|
6
10
|
import { StackScreenNativeComponent } from 'lynx-screens';
|
|
7
11
|
|
|
8
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
LynxStackDescriptor,
|
|
14
|
+
LynxStackNavigationHelpers,
|
|
15
|
+
} from '../types';
|
|
16
|
+
import { CardContent } from './CardContent';
|
|
9
17
|
|
|
10
18
|
type Props = {
|
|
11
19
|
descriptor: LynxStackDescriptor;
|
|
@@ -30,31 +38,49 @@ export function CardScreen({
|
|
|
30
38
|
onNativeDismiss,
|
|
31
39
|
onNativeDismissPrevented,
|
|
32
40
|
}: Props) {
|
|
41
|
+
const { colors } = useTheme();
|
|
33
42
|
const { preventedRoutes } = usePreventRemoveContext();
|
|
34
43
|
|
|
35
44
|
const { route, options } = descriptor;
|
|
36
|
-
const { contentStyle } = options;
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
// while it sits above the focused index (preloaded / retained).
|
|
40
|
-
const activityMode = isPopped || isDetached ? 'detached' : 'attached';
|
|
46
|
+
const { inactiveBehavior = 'pause' } = options;
|
|
41
47
|
|
|
42
|
-
// Prevention comes from `usePreventRemove` and nothing else. A static option
|
|
43
|
-
// would be a trap: the native side would block the gesture, then the
|
|
44
|
-
// `onNativeDismissPrevented` round-trip below would pop the route anyway,
|
|
45
|
-
// because only a `beforeRemove` listener can cancel that dispatch.
|
|
46
48
|
const isRemovePrevented = preventedRoutes[route.key]?.preventRemove;
|
|
49
|
+
const hasNestedState = 'state' in route && route.state != null;
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
let activityMode: 'normal' | 'inert' | 'paused' | 'unmounted';
|
|
52
|
+
|
|
53
|
+
if (isPopped) {
|
|
54
|
+
// The screen is animating out, so don't let it handle any interaction
|
|
55
|
+
activityMode = 'inert';
|
|
56
|
+
} else if (
|
|
57
|
+
// Render focused screens normally
|
|
58
|
+
isFocused ||
|
|
59
|
+
// Unpause previous screen so update isn't delayed for swipe back
|
|
60
|
+
isBeforeLast ||
|
|
61
|
+
// Unpause preloaded and retained screens so updates are visible
|
|
62
|
+
// This lets effects on those screens run
|
|
63
|
+
isDetached
|
|
64
|
+
) {
|
|
65
|
+
activityMode = 'normal';
|
|
66
|
+
} else {
|
|
67
|
+
switch (inactiveBehavior) {
|
|
68
|
+
case 'none':
|
|
69
|
+
activityMode = 'normal';
|
|
70
|
+
break;
|
|
71
|
+
case 'unmount':
|
|
72
|
+
activityMode = hasNestedState ? 'paused' : 'unmounted';
|
|
73
|
+
break;
|
|
74
|
+
case 'pause':
|
|
75
|
+
activityMode = 'paused';
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
53
79
|
|
|
54
80
|
return (
|
|
55
81
|
<StackScreenNativeComponent
|
|
56
82
|
screenKey={route.key}
|
|
57
|
-
activityMode={
|
|
83
|
+
activityMode={isPopped || isDetached ? 'detached' : 'attached'}
|
|
58
84
|
preventNativeDismiss={isRemovePrevented}
|
|
59
85
|
onWillAppear={() =>
|
|
60
86
|
navigation.emit({
|
|
@@ -89,17 +115,11 @@ export function CardScreen({
|
|
|
89
115
|
onNativeDismissPrevented={onNativeDismissPrevented}
|
|
90
116
|
>
|
|
91
117
|
<NavigationProvider navigation={descriptor.navigation} route={route}>
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
height: '100%',
|
|
98
|
-
...contentStyle,
|
|
99
|
-
}}
|
|
100
|
-
>
|
|
101
|
-
{isLive ? descriptor.render() : null}
|
|
102
|
-
</view>
|
|
118
|
+
<CardContent
|
|
119
|
+
descriptor={descriptor}
|
|
120
|
+
activityMode={activityMode}
|
|
121
|
+
backgroundColor={colors.background}
|
|
122
|
+
/>
|
|
103
123
|
</NavigationProvider>
|
|
104
124
|
</StackScreenNativeComponent>
|
|
105
125
|
);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Copyright 2026 The Lynx Authors. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License Version 2.0 that can be found in the
|
|
3
|
+
// LICENSE file in the root directory of this source tree.
|
|
4
|
+
|
|
5
|
+
import { expect, test } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
createNavigationContainerRef,
|
|
8
|
+
type ParamListBase,
|
|
9
|
+
} from '@react-navigation/core';
|
|
10
|
+
import { act, render } from '@lynx-js/react/testing-library';
|
|
11
|
+
|
|
12
|
+
import { NavigationContainer } from '../../../NavigationContainer';
|
|
13
|
+
import type { LynxStackNavigationOptions } from '../../types';
|
|
14
|
+
import { createLynxStackNavigator } from '../../navigators/createLynxStackNavigator';
|
|
15
|
+
|
|
16
|
+
const renderStack = (screenOptions: LynxStackNavigationOptions = {}) => {
|
|
17
|
+
const Stack = createLynxStackNavigator();
|
|
18
|
+
const ref = createNavigationContainerRef<ParamListBase>();
|
|
19
|
+
|
|
20
|
+
const screen = (name: string) => () => <text>{`content of ${name}`}</text>;
|
|
21
|
+
|
|
22
|
+
const result = render(
|
|
23
|
+
<NavigationContainer ref={ref}>
|
|
24
|
+
<Stack.Navigator screenOptions={screenOptions}>
|
|
25
|
+
<Stack.Screen name='A' component={screen('A')} />
|
|
26
|
+
<Stack.Screen name='B' component={screen('B')} />
|
|
27
|
+
<Stack.Screen name='C' component={screen('C')} />
|
|
28
|
+
</Stack.Navigator>
|
|
29
|
+
</NavigationContainer>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return { ...result, ref };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
test('keeps a popped screen rendered until the native side dismisses it', () => {
|
|
36
|
+
const { container, ref } = renderStack();
|
|
37
|
+
|
|
38
|
+
act(() => ref.navigate('B'));
|
|
39
|
+
expect(container.textContent).toContain('content of B');
|
|
40
|
+
|
|
41
|
+
act(() => ref.goBack());
|
|
42
|
+
|
|
43
|
+
expect(container.textContent).toContain('content of B');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('keeps screens deeper than the one behind the top rendered by default', () => {
|
|
47
|
+
const { container, ref } = renderStack();
|
|
48
|
+
|
|
49
|
+
act(() => ref.navigate('B'));
|
|
50
|
+
act(() => ref.navigate('C'));
|
|
51
|
+
|
|
52
|
+
expect(container.textContent).toContain('content of A');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('unmounts screens deeper than the one behind the top with inactiveBehavior unmount', () => {
|
|
56
|
+
const { container, ref } = renderStack({ inactiveBehavior: 'unmount' });
|
|
57
|
+
|
|
58
|
+
act(() => ref.navigate('B'));
|
|
59
|
+
act(() => ref.navigate('C'));
|
|
60
|
+
|
|
61
|
+
expect(container.textContent).not.toContain('content of A');
|
|
62
|
+
expect(container.textContent).toContain('content of B');
|
|
63
|
+
});
|