@momo-kits/foundation 0.163.1-beta.16 → 0.163.1-beta.17
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/Application/NavigationContainer.tsx +4 -1
- package/Application/utils.tsx +79 -3
- package/Layout/Screen.tsx +4 -4
- package/Layout/utils.ts +3 -72
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@ import { DeviceEventEmitter } from 'react-native';
|
|
|
16
16
|
import StackScreen from './StackScreen';
|
|
17
17
|
import ModalScreen from './ModalScreen';
|
|
18
18
|
import Navigator from './Navigator';
|
|
19
|
-
import { getModalOptions, getStackOptions } from './utils';
|
|
19
|
+
import { getModalOptions, getStackOptions, useNativeCanGoBack } from './utils';
|
|
20
20
|
import { NavigationContainerProps } from './types';
|
|
21
21
|
import { ApplicationContext, MiniAppContext } from '../Context';
|
|
22
22
|
import Localize from './Localize';
|
|
@@ -93,6 +93,7 @@ const Navigation: React.FC<any> = ({
|
|
|
93
93
|
const navigator = useRef(new Navigator(navigationRef, isReady));
|
|
94
94
|
const [showGrid, setShowGrid] = useState(false);
|
|
95
95
|
const insets = useSafeAreaInsets();
|
|
96
|
+
const syncCanGoBack = useNativeCanGoBack();
|
|
96
97
|
|
|
97
98
|
let headerBackground = context?.designSystemConfig?.config?.headerBar;
|
|
98
99
|
let headerGradient = theme.colors?.gradient;
|
|
@@ -192,7 +193,9 @@ const Navigation: React.FC<any> = ({
|
|
|
192
193
|
ref={navigationRef}
|
|
193
194
|
onReady={() => {
|
|
194
195
|
isReady.current = true;
|
|
196
|
+
syncCanGoBack(navigationRef.current?.getRootState?.());
|
|
195
197
|
}}
|
|
198
|
+
onStateChange={syncCanGoBack}
|
|
196
199
|
>
|
|
197
200
|
<Stack.Navigator
|
|
198
201
|
initialRouteName="Stack"
|
package/Application/utils.tsx
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import {
|
|
3
9
|
StackNavigationOptions,
|
|
4
10
|
TransitionPresets,
|
|
5
11
|
} from '@react-navigation/stack';
|
|
12
|
+
import type { NavigationState, PartialState } from '@react-navigation/native';
|
|
6
13
|
import type { HeaderTitleProps, NavigationOptions } from './types';
|
|
7
14
|
import { Colors, Spacing } from '../Consts';
|
|
8
|
-
import { Animated, AppState, Platform } from 'react-native';
|
|
15
|
+
import { Animated, AppState, NativeModules, Platform } from 'react-native';
|
|
9
16
|
import type { SharedValue } from 'react-native-reanimated';
|
|
10
17
|
import { useSharedValue } from 'react-native-reanimated';
|
|
11
18
|
import {
|
|
@@ -38,7 +45,7 @@ const getStackOptions = (): StackNavigationOptions => {
|
|
|
38
45
|
headerLeft: (props: any) => <HeaderLeft {...props} />,
|
|
39
46
|
headerRight: (props: any) => <HeaderRight {...props} />,
|
|
40
47
|
headerTintColor: Colors.black_17,
|
|
41
|
-
gestureEnabled:
|
|
48
|
+
gestureEnabled: Platform.OS === 'ios',
|
|
42
49
|
...TransitionPresets.SlideFromRightIOS,
|
|
43
50
|
};
|
|
44
51
|
};
|
|
@@ -54,9 +61,20 @@ const getModalOptions = (): StackNavigationOptions => {
|
|
|
54
61
|
cardOverlayEnabled: true,
|
|
55
62
|
animation: 'none',
|
|
56
63
|
presentation: 'transparentModal',
|
|
64
|
+
gestureEnabled: false,
|
|
57
65
|
};
|
|
58
66
|
};
|
|
59
67
|
|
|
68
|
+
/**
|
|
69
|
+
* screen owns its back flow (guard popup, custom handler or no back button),
|
|
70
|
+
* so swipe back must not bypass it
|
|
71
|
+
*/
|
|
72
|
+
const hasCustomBackHandling = (params: NavigationOptions) =>
|
|
73
|
+
params.preventBack !== undefined ||
|
|
74
|
+
params.hiddenBack === true ||
|
|
75
|
+
typeof params.onBackHandler === 'function' ||
|
|
76
|
+
typeof params.onPressLeftHeader === 'function';
|
|
77
|
+
|
|
60
78
|
/**
|
|
61
79
|
* build react-navigation options
|
|
62
80
|
*/
|
|
@@ -125,6 +143,13 @@ const getOptions = (
|
|
|
125
143
|
};
|
|
126
144
|
}
|
|
127
145
|
|
|
146
|
+
/**
|
|
147
|
+
* only ever turns swipe back off, screens opt in via gestureEnabled
|
|
148
|
+
*/
|
|
149
|
+
if (params.gestureEnabled === undefined && hasCustomBackHandling(params)) {
|
|
150
|
+
options.gestureEnabled = false;
|
|
151
|
+
}
|
|
152
|
+
|
|
128
153
|
return {
|
|
129
154
|
...params,
|
|
130
155
|
...options,
|
|
@@ -230,6 +255,55 @@ const useAppState = () => {
|
|
|
230
255
|
};
|
|
231
256
|
};
|
|
232
257
|
|
|
258
|
+
/**
|
|
259
|
+
* screens the focused navigation branch can still pop.
|
|
260
|
+
* only stack navigators count, switching bottom tab is not a back step
|
|
261
|
+
*/
|
|
262
|
+
const getStackDepth = (
|
|
263
|
+
state?: NavigationState | PartialState<NavigationState>,
|
|
264
|
+
): number => {
|
|
265
|
+
const routes = state?.routes;
|
|
266
|
+
if (!routes?.length) {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const index = state?.index ?? routes.length - 1;
|
|
271
|
+
const depth = state?.type === 'stack' ? index : 0;
|
|
272
|
+
|
|
273
|
+
return depth + getStackDepth(routes[index]?.state);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* report back ownership to the native container: `true` when the mini app sits
|
|
278
|
+
* on its root screen and native may dismiss the rootview, `false` while
|
|
279
|
+
* react-navigation still has screens (or a modal / bottom sheet) to pop.
|
|
280
|
+
*/
|
|
281
|
+
const useNativeCanGoBack = () => {
|
|
282
|
+
const lastValue = useRef<boolean | undefined>(undefined);
|
|
283
|
+
|
|
284
|
+
return useCallback(
|
|
285
|
+
(state?: NavigationState | PartialState<NavigationState>) => {
|
|
286
|
+
const canDismiss = getStackDepth(state) === 0;
|
|
287
|
+
if (lastValue.current === canDismiss) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const setCanGoBack = NativeModules?.MiniAppModule?.setCanGoBack;
|
|
292
|
+
if (typeof setCanGoBack !== 'function') {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
lastValue.current = canDismiss;
|
|
297
|
+
try {
|
|
298
|
+
setCanGoBack(canDismiss);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.warn('[NavigationContainer] setCanGoBack failed:', error);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
[],
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
233
307
|
/**
|
|
234
308
|
* Union of the two animation runtimes an internal foundation component might
|
|
235
309
|
* receive. Public Screen props still accept React Native Animated.Value only;
|
|
@@ -271,9 +345,11 @@ export {
|
|
|
271
345
|
getStackOptions,
|
|
272
346
|
getModalOptions,
|
|
273
347
|
getOptions,
|
|
348
|
+
getStackDepth,
|
|
274
349
|
exportHeaderTitle,
|
|
275
350
|
setAutomationID,
|
|
276
351
|
useComponentId,
|
|
277
352
|
useAppState,
|
|
353
|
+
useNativeCanGoBack,
|
|
278
354
|
useNormalizedSharedValue,
|
|
279
355
|
};
|
package/Layout/Screen.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
ViewProps,
|
|
27
27
|
} from 'react-native';
|
|
28
28
|
import { useSharedValue, withTiming } from 'react-native-reanimated';
|
|
29
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
29
30
|
import { ApplicationContext, ScreenContext } from '../Context';
|
|
30
31
|
import Navigation from '../Application/Navigation';
|
|
31
32
|
import {
|
|
@@ -40,7 +41,7 @@ import styles from './styles';
|
|
|
40
41
|
import { HeaderType } from './types';
|
|
41
42
|
import { InputRef } from '../Input';
|
|
42
43
|
import { exportHeaderTitle, getOptions } from '../Application/utils';
|
|
43
|
-
import {
|
|
44
|
+
import { validateChildren } from './utils';
|
|
44
45
|
import Section from './Section';
|
|
45
46
|
import {
|
|
46
47
|
HeaderBackground,
|
|
@@ -194,9 +195,7 @@ const Screen = forwardRef(
|
|
|
194
195
|
const { width: widthDevice } = useWindowDimensions();
|
|
195
196
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
196
197
|
const screen: any = useContext(ScreenContext);
|
|
197
|
-
|
|
198
|
-
const bottomInset = useBottomSafeAreaInset();
|
|
199
|
-
// AI-GENERATED END: keyboard-aware bottom inset (DS-747)
|
|
198
|
+
const insets = useSafeAreaInsets();
|
|
200
199
|
const heightHeader = useHeaderHeight();
|
|
201
200
|
|
|
202
201
|
/**
|
|
@@ -239,6 +238,7 @@ const Screen = forwardRef(
|
|
|
239
238
|
let handleScroll;
|
|
240
239
|
let Component: any = View;
|
|
241
240
|
|
|
241
|
+
const bottomInset = Platform.OS === 'ios' ? Math.min(insets.bottom, 21) : insets.bottom;
|
|
242
242
|
let keyboardOffset = heightHeader - bottomInset;
|
|
243
243
|
if (headerType === 'extended' || animatedHeader || inputSearchProps) {
|
|
244
244
|
keyboardOffset = -bottomInset;
|
package/Layout/utils.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import React, { Fragment, ReactElement
|
|
2
|
-
import {
|
|
3
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
1
|
+
import React, { Fragment, ReactElement } from 'react';
|
|
2
|
+
import { useWindowDimensions } from 'react-native';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* validate children type
|
|
@@ -45,72 +44,4 @@ const useGridSystem = () => {
|
|
|
45
44
|
return { numberOfColumns };
|
|
46
45
|
};
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Android 16 (API 36) ignores `android:windowOptOutEdgeToEdgeEnforcement`, so the host
|
|
51
|
-
* app is always edge-to-edge and `insets.bottom` reports the navigation bar height
|
|
52
|
-
* instead of 0. Android <= 15 still honours the opt-out, which is why the footer only
|
|
53
|
-
* jitters on Android 16.
|
|
54
|
-
*/
|
|
55
|
-
const IS_EDGE_TO_EDGE_ENFORCED =
|
|
56
|
-
Platform.OS === 'android' && Number(Platform.Version) >= 36;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Tracks IME visibility. On Android `keyboardDidShow` is emitted as soon as the IME
|
|
60
|
-
* becomes visible, ahead of the root view being resized for it.
|
|
61
|
-
*/
|
|
62
|
-
const useIsKeyboardVisible = () => {
|
|
63
|
-
const [isKeyboardVisible, setIsKeyboardVisible] = useState(
|
|
64
|
-
() => IS_EDGE_TO_EDGE_ENFORCED && Keyboard.isVisible(),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (!IS_EDGE_TO_EDGE_ENFORCED) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const showSubscription = Keyboard.addListener('keyboardDidShow', () =>
|
|
73
|
-
setIsKeyboardVisible(true),
|
|
74
|
-
);
|
|
75
|
-
const hideSubscription = Keyboard.addListener('keyboardDidHide', () =>
|
|
76
|
-
setIsKeyboardVisible(false),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
return () => {
|
|
80
|
-
showSubscription.remove();
|
|
81
|
-
hideSubscription.remove();
|
|
82
|
-
};
|
|
83
|
-
}, []);
|
|
84
|
-
|
|
85
|
-
return isKeyboardVisible;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Bottom safe-area inset for chrome anchored to the bottom edge (footer, floating
|
|
90
|
-
* button, scroll padding).
|
|
91
|
-
*
|
|
92
|
-
* `react-native-safe-area-context` recomputes its insets from an `onPreDraw` pass, so
|
|
93
|
-
* the drop to 0 that happens once the IME covers the navigation bar only reaches JS
|
|
94
|
-
* *after* the root view has already shrunk, and then only as a state update. For those
|
|
95
|
-
* frames the footer keeps padding for a navigation bar the keyboard already covers, so
|
|
96
|
-
* it lifts off the keyboard and snaps back down when the new inset lands. Zeroing the
|
|
97
|
-
* inset on keyboard visibility removes that intermediate state.
|
|
98
|
-
*
|
|
99
|
-
* The keyboard flag is only allowed to *remove* inset, never to restore it: on hide it
|
|
100
|
-
* flips back before the root view grows again, and `insets.bottom` is still 0 at that
|
|
101
|
-
* point, so the restore stays driven by the inset itself and the footer does not jump
|
|
102
|
-
* the other way.
|
|
103
|
-
*/
|
|
104
|
-
const useBottomSafeAreaInset = () => {
|
|
105
|
-
const insets = useSafeAreaInsets();
|
|
106
|
-
const isKeyboardVisible = useIsKeyboardVisible();
|
|
107
|
-
|
|
108
|
-
if (Platform.OS === 'ios') {
|
|
109
|
-
return Math.min(insets.bottom, 21);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return isKeyboardVisible ? 0 : insets.bottom;
|
|
113
|
-
};
|
|
114
|
-
// AI-GENERATED END: keyboard-aware bottom safe-area inset (DS-747)
|
|
115
|
-
|
|
116
|
-
export { useBottomSafeAreaInset, useGridSystem, validateChildren };
|
|
47
|
+
export { useGridSystem, validateChildren };
|