@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-navigation/lynx",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Lynx integration for React Navigation",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -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 { NavigationProvider, usePreventRemoveContext } from '@react-navigation/core';
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 { LynxStackDescriptor, LynxStackNavigationHelpers } from '../types';
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
- // A screen is kept out of the native hierarchy while it animates out or
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
- // Only the focused screen, the one behind it (so a swipe back reveals fresh
49
- // content) and detached screens stay live. `isBeforeLast` and `isFocused`
50
- // are read here rather than in the parent so the reasoning stays with the
51
- // component that acts on it.
52
- const isLive = isFocused || isBeforeLast || isDetached;
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={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
- <view
93
- style={{
94
- display: 'flex',
95
- flexDirection: 'column',
96
- width: '100%',
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
+ });