@momo-kits/foundation 0.168.1-beta.37 → 0.168.1-beta.39
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/BottomSheet.hooks.ts +8 -8
- package/Application/BottomSheet.parts.tsx +44 -34
- package/Application/BottomSheet.tsx +35 -26
- package/Application/BottomTab/ActiveIndicator.tsx +15 -14
- package/Application/BottomTab/BottomTabBar.tsx +49 -62
- package/Application/BottomTab/BottomTabInset.tsx +8 -4
- package/Application/BottomTab/CurvedContainer.tsx +14 -12
- package/Application/BottomTab/CustomBottomTabItem.tsx +13 -13
- package/Application/BottomTab/DuoBarCompression.tsx +13 -19
- package/Application/BottomTab/DuoRailTabBar.tsx +20 -24
- package/Application/BottomTab/DuoTabBar.tsx +8 -10
- package/Application/BottomTab/TabBarIcon.tsx +15 -29
- package/Application/BottomTab/TabScreen.tsx +1 -5
- package/Application/BottomTab/index.tsx +33 -30
- package/Application/BottomTab/metrics.ts +23 -23
- package/Application/BottomTab/tabItems.ts +1 -1
- package/Application/BottomTab/usePressLift.ts +2 -2
- package/Application/Components/BackgroundImageView.tsx +4 -0
- package/Application/Components/DuoGlassBackground.tsx +5 -4
- package/Application/Components/HeaderAnimated.tsx +40 -12
- package/Application/Components/HeaderBackground.tsx +19 -2
- package/Application/Components/HeaderExtendHeader.tsx +51 -37
- package/Application/Components/HeaderLeft.tsx +5 -18
- package/Application/Components/HeaderRight.styles.ts +15 -13
- package/Application/Components/HeaderRight.tsx +2 -1
- package/Application/Components/HeaderTitle.shared.ts +17 -16
- package/Application/Components/HeaderTitle.tsx +4 -3
- package/Application/Components/HeaderToolkitAction.tsx +29 -57
- package/Application/Components/NavigationButton.tsx +10 -6
- package/Application/Components/SearchHeader.tsx +37 -17
- package/Application/Components/TitleJourney.tsx +2 -2
- package/Application/Components/TitleLocation.tsx +2 -2
- package/Application/Components/TitleUser.tsx +2 -2
- package/Application/Components/useToolkitActions.tsx +17 -32
- package/Application/Localize.ts +7 -0
- package/Application/ModalScreen.tsx +9 -3
- package/Application/Navigation.ts +14 -16
- package/Application/NavigationContainer.tsx +19 -16
- package/Application/Navigator.ts +20 -48
- package/Application/ScaleSizeProvider.tsx +13 -6
- package/Application/StackScreen.tracking.ts +11 -26
- package/Application/StackScreen.tsx +17 -35
- package/Application/TooltipPortal.tsx +12 -5
- package/Application/WidgetContainer.tsx +10 -4
- package/Application/headerLayout.ts +158 -0
- package/Application/index.ts +11 -1
- package/Application/modalLayout.ts +13 -14
- package/Application/navigationOptions.tsx +27 -32
- package/Application/types.header.ts +7 -2
- package/Application/types.ts +7 -0
- package/Badge/Badge.tsx +5 -0
- package/Badge/BadgeDot.tsx +1 -0
- package/Badge/BadgeDotAnimation.tsx +15 -7
- package/Badge/BadgeRibbon.tsx +46 -38
- package/Badge/Shape.tsx +2 -0
- package/Badge/types.ts +2 -2
- package/Button/index.tsx +5 -16
- package/Button/utils.ts +9 -15
- package/CheckBox/index.tsx +4 -0
- package/Consts/theme.ts +10 -10
- package/Context/index.ts +5 -1
- package/Divider/DashDivider.tsx +1 -0
- package/Divider/index.tsx +4 -0
- package/FoundationList/index.tsx +4 -0
- package/Icon/index.tsx +6 -6
- package/Icon/types.ts +1 -1
- package/IconButton/index.tsx +4 -12
- package/Image/index.tsx +9 -7
- package/Input/Input.tsx +8 -9
- package/Input/InputDropDown.tsx +10 -12
- package/Input/InputMoney.tsx +12 -8
- package/Input/InputMoneyView.tsx +14 -7
- package/Input/InputOTP.tsx +35 -33
- package/Input/InputPhoneNumber.tsx +8 -9
- package/Input/InputSearch.tsx +22 -8
- package/Input/InputTextArea.tsx +4 -0
- package/Input/SearchTextTyping.tsx +4 -0
- package/Input/SystemTextInput.tsx +7 -2
- package/Input/TextTyping.tsx +4 -0
- package/Input/common.tsx +26 -12
- package/Input/index.tsx +24 -21
- package/Input/moneyUtils.ts +10 -4
- package/Input/styles.ts +0 -5
- package/Input/utils.ts +14 -12
- package/Layout/Card.tsx +7 -4
- package/Layout/FloatingButton.tsx +4 -0
- package/Layout/Item.tsx +6 -2
- package/Layout/ItemList.tsx +9 -7
- package/Layout/ItemSectionList.tsx +2 -1
- package/Layout/ReservedRegions.tsx +25 -25
- package/Layout/Screen.tsx +59 -58
- package/Layout/Section.tsx +8 -5
- package/Layout/TrackingScope.tsx +1 -0
- package/Layout/screenContent.tsx +4 -2
- package/Layout/screenFooter.tsx +28 -18
- package/Layout/screenHeader.tsx +88 -82
- package/Layout/screenQuickReturn.tsx +17 -20
- package/Layout/screenScroll.ts +4 -7
- package/Layout/screenTypes.ts +13 -4
- package/Layout/types.ts +2 -4
- package/Layout/utils.ts +31 -37
- package/Loader/DotLoader.tsx +4 -0
- package/Loader/Loader.tsx +4 -0
- package/Loader/ProgressBar.tsx +1 -0
- package/Loader/Spinner.tsx +4 -0
- package/Loader/utils.ts +11 -7
- package/Pagination/Dot.tsx +4 -0
- package/Pagination/PaginationDot.tsx +1 -0
- package/Pagination/PaginationNumber.tsx +5 -3
- package/Pagination/PaginationScroll.tsx +5 -0
- package/Pagination/PaginationWhiteDot.tsx +1 -0
- package/Pagination/index.tsx +4 -0
- package/Pagination/types.ts +2 -2
- package/Popup/PopupNotify.tsx +44 -48
- package/Popup/PopupPromotion.tsx +4 -7
- package/Popup/types.ts +4 -4
- package/Popup/utils.ts +4 -0
- package/Radio/index.tsx +4 -0
- package/Skeleton/index.tsx +8 -0
- package/Switch/index.tsx +1 -0
- package/Tag/index.tsx +5 -1
- package/Tag/types.ts +3 -3
- package/Text/index.tsx +14 -3
- package/Text/styles.ts +1 -1
- package/Text/types.ts +1 -6
- package/Text/utils.ts +20 -4
- package/Title/index.tsx +41 -32
- package/ViewBoundary/index.tsx +4 -2
- package/index.ts +0 -6
- package/package.json +37 -37
- package/Application/__tests__/modalLayout.test.ts +0 -128
package/Application/Navigator.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {CommonActions, StackActions} from '@react-navigation/native';
|
|
2
2
|
import {BottomSheetParams, ModalParams, ScreenParams} from './types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Imperative navigation API exposed to features through `ApplicationContext`.
|
|
6
|
+
* Every action is a no-op until the container reports ready.
|
|
7
|
+
*/
|
|
4
8
|
class Navigator {
|
|
5
9
|
ref?: any;
|
|
6
10
|
isReady?: any;
|
|
@@ -14,40 +18,28 @@ class Navigator {
|
|
|
14
18
|
this.isWidget = isWidget;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
/**
|
|
18
|
-
* push new stack screen
|
|
19
|
-
* @param params
|
|
20
|
-
*/
|
|
21
|
+
/** Pushes a new stack screen. */
|
|
21
22
|
push = (params: ScreenParams) => {
|
|
22
23
|
if (this.isReady.current) {
|
|
23
24
|
this.ref.current?.dispatch?.(StackActions.push('Stack', params));
|
|
24
25
|
}
|
|
25
26
|
};
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
* replace current screen with new screen
|
|
29
|
-
* @param params
|
|
30
|
-
*/
|
|
28
|
+
/** Replaces the current screen with a new stack screen. */
|
|
31
29
|
replace = (params: ScreenParams) => {
|
|
32
30
|
if (this.isReady.current) {
|
|
33
31
|
this.ref.current?.dispatch?.(StackActions.replace('Stack', params));
|
|
34
32
|
}
|
|
35
33
|
};
|
|
36
34
|
|
|
37
|
-
/**
|
|
38
|
-
* pop to dismiss a screen
|
|
39
|
-
* @param count
|
|
40
|
-
*/
|
|
35
|
+
/** Pops `count` screens, 1 by default. */
|
|
41
36
|
pop = (count?: number) => {
|
|
42
37
|
if (this.isReady.current) {
|
|
43
38
|
this.ref.current?.dispatch?.(StackActions.pop(count ?? 1));
|
|
44
39
|
}
|
|
45
40
|
};
|
|
46
41
|
|
|
47
|
-
/**
|
|
48
|
-
* present a new screen, show from bottom iOS
|
|
49
|
-
* @param params
|
|
50
|
-
*/
|
|
42
|
+
/** Presents a screen as a dialog (slides up from the bottom on iOS). */
|
|
51
43
|
present = (params: ScreenParams) => {
|
|
52
44
|
if (this.isReady.current) {
|
|
53
45
|
this.ref.current?.dispatch?.(StackActions.push('Dialog', params));
|
|
@@ -55,9 +47,8 @@ class Navigator {
|
|
|
55
47
|
};
|
|
56
48
|
|
|
57
49
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* @param onError
|
|
50
|
+
* Shows a modal popup; inside a widget it always uses the native modal.
|
|
51
|
+
* Failures are reported through `onError` instead of throwing.
|
|
61
52
|
*/
|
|
62
53
|
showModal = (params: ModalParams, onError?: (error: string) => void) => {
|
|
63
54
|
try {
|
|
@@ -76,11 +67,7 @@ class Navigator {
|
|
|
76
67
|
}
|
|
77
68
|
};
|
|
78
69
|
|
|
79
|
-
/**
|
|
80
|
-
* show a bottom sheet
|
|
81
|
-
* @param params
|
|
82
|
-
* @param onError
|
|
83
|
-
*/
|
|
70
|
+
/** Shows a bottom sheet through the modal route; failures go to `onError`. */
|
|
84
71
|
showBottomSheet = (
|
|
85
72
|
params: BottomSheetParams,
|
|
86
73
|
onError?: (error: string) => void
|
|
@@ -102,31 +89,21 @@ class Navigator {
|
|
|
102
89
|
}
|
|
103
90
|
};
|
|
104
91
|
|
|
105
|
-
/**
|
|
106
|
-
* pop all screen route
|
|
107
|
-
*/
|
|
92
|
+
/** Pops back to the first screen of the stack. */
|
|
108
93
|
popToTop = () => {
|
|
109
94
|
if (this.isReady.current) {
|
|
110
95
|
this.ref.current?.dispatch?.(StackActions.popToTop());
|
|
111
96
|
}
|
|
112
97
|
};
|
|
113
98
|
|
|
114
|
-
/**
|
|
115
|
-
* navigate route name bottom tab only
|
|
116
|
-
* @param name
|
|
117
|
-
*/
|
|
99
|
+
/** Navigates to a route by name; meant for bottom tab routes only. */
|
|
118
100
|
navigate = (name: string) => {
|
|
119
101
|
if (this.isReady.current) {
|
|
120
|
-
this.ref.current?.dispatch?.(
|
|
121
|
-
CommonActions.navigate(name)
|
|
122
|
-
);
|
|
102
|
+
this.ref.current?.dispatch?.(CommonActions.navigate(name));
|
|
123
103
|
}
|
|
124
104
|
};
|
|
125
105
|
|
|
126
|
-
/**
|
|
127
|
-
* reset a navigation flow with new screen
|
|
128
|
-
* @param params
|
|
129
|
-
*/
|
|
106
|
+
/** Resets the flow to a single new stack screen. */
|
|
130
107
|
reset = (params: ScreenParams) => {
|
|
131
108
|
if (this.isReady.current) {
|
|
132
109
|
this.ref.current?.dispatch?.(
|
|
@@ -144,26 +121,21 @@ class Navigator {
|
|
|
144
121
|
}
|
|
145
122
|
};
|
|
146
123
|
|
|
147
|
-
/**
|
|
148
|
-
* dismiss a feature data
|
|
149
|
-
* @param data
|
|
150
|
-
*/
|
|
124
|
+
/** Sets the payload the header passes to `maxApi.dismiss` on close. */
|
|
151
125
|
setDismissData = (data: any) => {
|
|
152
126
|
this.dismissData = data;
|
|
153
127
|
};
|
|
154
128
|
|
|
155
129
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
130
|
+
* No-op placeholder; `NavigationContainer` and `WidgetContainer` replace it
|
|
131
|
+
* with their context state setter.
|
|
158
132
|
*/
|
|
159
|
-
setCurrentContext = (
|
|
133
|
+
setCurrentContext = (_context: {
|
|
160
134
|
code?: string;
|
|
161
135
|
name?: {vi: string; en: string};
|
|
162
136
|
description?: {vi: string; en: string};
|
|
163
137
|
icon?: string;
|
|
164
|
-
}) => {
|
|
165
|
-
console.log(context);
|
|
166
|
-
};
|
|
138
|
+
}) => {};
|
|
167
139
|
}
|
|
168
140
|
|
|
169
141
|
export default Navigator;
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import React, { FC } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import React, { FC, useContext, useMemo } from 'react';
|
|
2
|
+
import { MiniAppContext } from '../Context';
|
|
3
3
|
import { ScaleSizeProviderProps } from './types';
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Caps the font scale of its subtree at `scaleSizeMaxRate` × the design size
|
|
7
|
+
* (`useScaleSize`), passed down in MiniAppContext. The nearest provider wins;
|
|
8
|
+
* an undefined rate lifts an outer cap.
|
|
9
|
+
*/
|
|
6
10
|
const ScaleSizeProvider: FC<ScaleSizeProviderProps> = ({
|
|
7
11
|
scaleSizeMaxRate,
|
|
8
12
|
children,
|
|
9
13
|
}) => {
|
|
14
|
+
const context = useContext<object | undefined>(MiniAppContext);
|
|
15
|
+
const value = useMemo(() => {
|
|
16
|
+
return { ...context, scaleSizeMaxRate };
|
|
17
|
+
}, [context, scaleSizeMaxRate]);
|
|
18
|
+
|
|
10
19
|
return (
|
|
11
|
-
<
|
|
12
|
-
{children}
|
|
13
|
-
</ScaleSizeContext.Provider>
|
|
20
|
+
<MiniAppContext.Provider value={value}>{children}</MiniAppContext.Provider>
|
|
14
21
|
);
|
|
15
22
|
};
|
|
16
23
|
|
|
@@ -8,15 +8,17 @@ type ScreenTrackingDeps = {
|
|
|
8
8
|
screenName: string;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Auto-tracking reporters of a stack screen. Each event fires at most once per
|
|
13
|
+
* screen, guarded by the `release*` flags on `tracking.current`.
|
|
14
|
+
*/
|
|
11
15
|
export const createScreenTracking = ({
|
|
12
16
|
tracking,
|
|
13
17
|
context,
|
|
14
18
|
navigator,
|
|
15
19
|
screenName,
|
|
16
20
|
}: ScreenTrackingDeps) => {
|
|
17
|
-
/**
|
|
18
|
-
* tracking for screen load
|
|
19
|
-
*/
|
|
21
|
+
/** Reports the screen load time. */
|
|
20
22
|
const onScreenLoad = () => {
|
|
21
23
|
if (!tracking.current?.releaseLoad) {
|
|
22
24
|
let timeLoad = tracking.current.timeLoad;
|
|
@@ -42,29 +44,22 @@ export const createScreenTracking = ({
|
|
|
42
44
|
);
|
|
43
45
|
tracking.current.releaseLoad = true;
|
|
44
46
|
|
|
45
|
-
/**
|
|
46
|
-
* debug
|
|
47
|
-
*/
|
|
48
47
|
navigator?.maxApi?.showToastDebug?.({
|
|
49
48
|
appId: context.appId,
|
|
50
49
|
message: `${screenName} screen_load_time ${timeLoad}`,
|
|
51
50
|
type: 'ERROR',
|
|
52
51
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
tracking.current.lastElement?.children?.current?.length > 0
|
|
56
|
-
) {
|
|
52
|
+
const lastChildren = tracking.current.lastElement?.children?.current;
|
|
53
|
+
if (__DEV__ && lastChildren?.length > 0) {
|
|
57
54
|
Alert.alert(
|
|
58
55
|
`${screenName}- load ${timeLoad}ms`,
|
|
59
|
-
JSON.stringify(
|
|
56
|
+
JSON.stringify(lastChildren),
|
|
60
57
|
);
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
};
|
|
64
61
|
|
|
65
|
-
/**
|
|
66
|
-
* tracking for screen load
|
|
67
|
-
*/
|
|
62
|
+
/** Reports the time until the screen became interactive. */
|
|
68
63
|
const onScreenInteraction = () => {
|
|
69
64
|
if (!tracking.current?.releaseInteraction) {
|
|
70
65
|
let timeLoad = tracking.current.timeLoad;
|
|
@@ -92,9 +87,6 @@ export const createScreenTracking = ({
|
|
|
92
87
|
);
|
|
93
88
|
tracking.current.releaseInteraction = true;
|
|
94
89
|
|
|
95
|
-
/**
|
|
96
|
-
* debug toast
|
|
97
|
-
*/
|
|
98
90
|
navigator?.maxApi?.showToastDebug?.({
|
|
99
91
|
appId: context.appId,
|
|
100
92
|
message: `${screenName} screen_interaction_time ${tracking.current.timeInteraction}`,
|
|
@@ -103,9 +95,7 @@ export const createScreenTracking = ({
|
|
|
103
95
|
}
|
|
104
96
|
};
|
|
105
97
|
|
|
106
|
-
/**
|
|
107
|
-
* tracking for first interaction by user
|
|
108
|
-
*/
|
|
98
|
+
/** Reports the user's first interaction with the screen. */
|
|
109
99
|
const onFirstInteraction = (action: string) => {
|
|
110
100
|
if (!tracking.current?.releaseUserInteraction) {
|
|
111
101
|
const timeLoad = tracking.current.endTime - tracking.current.startTime;
|
|
@@ -119,9 +109,6 @@ export const createScreenTracking = ({
|
|
|
119
109
|
});
|
|
120
110
|
tracking.current.releaseUserInteraction = true;
|
|
121
111
|
|
|
122
|
-
/**
|
|
123
|
-
* debug
|
|
124
|
-
*/
|
|
125
112
|
navigator?.maxApi?.showToastDebug?.({
|
|
126
113
|
appId: context.appId,
|
|
127
114
|
message: `${screenName} user_interaction ${timeLoad}`,
|
|
@@ -130,9 +117,7 @@ export const createScreenTracking = ({
|
|
|
130
117
|
}
|
|
131
118
|
};
|
|
132
119
|
|
|
133
|
-
/**
|
|
134
|
-
* tracking for loading screen
|
|
135
|
-
*/
|
|
120
|
+
/** Reports how long the skeleton loading state lasted. */
|
|
136
121
|
const onScreenLoading = () => {
|
|
137
122
|
const start = tracking.current.timeStartLoading;
|
|
138
123
|
const end = tracking.current.timeEndLoading;
|
|
@@ -8,13 +8,13 @@ import { SwipeBackHaptic } from './Components/SwipeBackHaptic';
|
|
|
8
8
|
import { useAppState } from './hooks';
|
|
9
9
|
import { createScreenTracking } from './StackScreen.tracking';
|
|
10
10
|
import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
|
|
11
|
+
import { useHeaderFrameOptions } from './headerLayout';
|
|
11
12
|
|
|
12
13
|
const runAfterInteractions = InteractionManager.runAfterInteractions;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @constructor
|
|
16
|
+
* Hosts a feature screen in the stack: applies its header options, provides
|
|
17
|
+
* `ScreenContext` and reports navigation, load and interaction tracking.
|
|
18
18
|
*/
|
|
19
19
|
const StackScreen: React.FC<any> = props => {
|
|
20
20
|
const { navigator } = useContext(ApplicationContext);
|
|
@@ -52,6 +52,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
52
52
|
const navigation = useRef(new Navigation(props.navigation, context)).current;
|
|
53
53
|
const heightHeader = useHeaderHeight();
|
|
54
54
|
const { isBackgroundToForeground } = useAppState();
|
|
55
|
+
const headerFrameOptions = useHeaderFrameOptions(options?.contentLayout);
|
|
55
56
|
|
|
56
57
|
const data = {
|
|
57
58
|
...initialParams,
|
|
@@ -64,18 +65,19 @@ const StackScreen: React.FC<any> = props => {
|
|
|
64
65
|
|
|
65
66
|
const screenName = Component?.name || Component?.type?.name || 'Invalid';
|
|
66
67
|
|
|
67
|
-
/**
|
|
68
|
-
* set options for screen
|
|
69
|
-
*/
|
|
70
68
|
useLayoutEffect(() => {
|
|
71
69
|
if (options) {
|
|
72
70
|
navigation.setOptions(options);
|
|
73
71
|
}
|
|
74
72
|
}, [navigation, options]);
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
// re-applied on fold, unfold and rotation, apart from the other options
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
if (headerFrameOptions) {
|
|
77
|
+
navigation.instance.setOptions(headerFrameOptions);
|
|
78
|
+
}
|
|
79
|
+
}, [navigation, headerFrameOptions]);
|
|
80
|
+
|
|
79
81
|
useEffect(() => {
|
|
80
82
|
let focusScreen: any;
|
|
81
83
|
let onFocusApp: any;
|
|
@@ -149,6 +151,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
149
151
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
152
|
}, []);
|
|
151
153
|
|
|
154
|
+
/** Reports a navigation to this screen: `back` if it was shown before. */
|
|
152
155
|
const onScreenNavigated = (pre: string, current: string) => {
|
|
153
156
|
if (!isBackgroundToForeground) {
|
|
154
157
|
const item: any = {
|
|
@@ -166,9 +169,6 @@ const StackScreen: React.FC<any> = props => {
|
|
|
166
169
|
|
|
167
170
|
tracking.current.mounted = true;
|
|
168
171
|
|
|
169
|
-
/**
|
|
170
|
-
* debug toast
|
|
171
|
-
*/
|
|
172
172
|
navigator?.maxApi?.showToastDebug?.({
|
|
173
173
|
appId: context.appId,
|
|
174
174
|
message: `${screenName} screen_navigated`,
|
|
@@ -190,9 +190,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
190
190
|
screenName,
|
|
191
191
|
bottomTab: !!bottomTab,
|
|
192
192
|
onElementLoad: (item: any) => {
|
|
193
|
-
|
|
194
|
-
* widget handle
|
|
195
|
-
*/
|
|
193
|
+
// Widgets record their own load span and don't end the screen load.
|
|
196
194
|
if (item?.componentName === 'Widget') {
|
|
197
195
|
const index = widgets.current.findIndex(
|
|
198
196
|
(element: any) => element.componentId === item.componentId,
|
|
@@ -216,35 +214,25 @@ const StackScreen: React.FC<any> = props => {
|
|
|
216
214
|
return;
|
|
217
215
|
}
|
|
218
216
|
|
|
219
|
-
/**
|
|
220
|
-
* tracking for element screen load
|
|
221
|
-
*/
|
|
222
217
|
clearTimeout(tracking.current.timeoutLoad);
|
|
223
218
|
tracking.current.endTime = Date.now();
|
|
224
219
|
tracking.current.timeoutInteraction?.cancel?.();
|
|
225
220
|
tracking.current.timeoutInteraction = runAfterInteractions(() => {
|
|
226
|
-
tracking.current.
|
|
227
|
-
|
|
221
|
+
const elapsed = Date.now() - tracking.current.startTime;
|
|
222
|
+
tracking.current.timeInteraction = elapsed;
|
|
228
223
|
});
|
|
229
224
|
|
|
230
|
-
/**
|
|
231
|
-
* support for debug last element
|
|
232
|
-
*/
|
|
233
225
|
if (item?.componentName) {
|
|
234
226
|
tracking.current.lastElement = item;
|
|
235
227
|
}
|
|
236
228
|
|
|
237
|
-
|
|
238
|
-
* for stop tracking when user interaction
|
|
239
|
-
*/
|
|
229
|
+
// A user interaction settles load and interaction tracking at once.
|
|
240
230
|
if (item?.interaction) {
|
|
241
231
|
onScreenLoad();
|
|
242
232
|
onScreenInteraction();
|
|
243
233
|
onFirstInteraction(item?.action);
|
|
244
234
|
}
|
|
245
|
-
|
|
246
|
-
* timeout for handle tracking screen
|
|
247
|
-
*/
|
|
235
|
+
// Reports once no element has loaded for 2s.
|
|
248
236
|
tracking.current.timeoutLoad = setTimeout(() => {
|
|
249
237
|
const time = tracking.current.endTime - tracking.current.startTime;
|
|
250
238
|
tracking.current.widgets = widgets.current;
|
|
@@ -262,16 +250,10 @@ const StackScreen: React.FC<any> = props => {
|
|
|
262
250
|
tracking.current.timeStartLoading = Date.now();
|
|
263
251
|
}
|
|
264
252
|
|
|
265
|
-
/**
|
|
266
|
-
* tracking for loading screen
|
|
267
|
-
*/
|
|
268
253
|
if (!loading) {
|
|
269
254
|
tracking.current.timeEndLoading = Date.now();
|
|
270
255
|
}
|
|
271
256
|
|
|
272
|
-
/**
|
|
273
|
-
* timeout for handle tracking screen
|
|
274
|
-
*/
|
|
275
257
|
clearTimeout(tracking.current.timeoutLoading);
|
|
276
258
|
tracking.current.timeoutLoading = setTimeout(() => {
|
|
277
259
|
onScreenLoading();
|
|
@@ -21,10 +21,14 @@ const TooltipPortalContext = createContext<TooltipPortalContextValue | null>(
|
|
|
21
21
|
null,
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Registry of tooltip contents that `TooltipPortalHost` renders in one layer
|
|
26
|
+
* above the screen, so tooltips aren't clipped by their anchor's parents.
|
|
27
|
+
*/
|
|
24
28
|
export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
|
|
25
29
|
children,
|
|
26
30
|
}) => {
|
|
27
|
-
//
|
|
31
|
+
// The ref map updates synchronously; the reducer only triggers re-renders.
|
|
28
32
|
const portalsRef = useRef<Map<string, ReactNode>>(new Map());
|
|
29
33
|
const hostRef = useRef<View>(null);
|
|
30
34
|
const [updateCounter, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
|
@@ -32,16 +36,15 @@ export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
|
|
|
32
36
|
|
|
33
37
|
const register = useCallback((id: string, content: ReactNode) => {
|
|
34
38
|
portalsRef.current.set(id, content);
|
|
35
|
-
forceUpdate();
|
|
39
|
+
forceUpdate();
|
|
36
40
|
}, []);
|
|
37
41
|
|
|
38
42
|
const unregister = useCallback((id: string, immediate = false) => {
|
|
39
43
|
const hasItem = portalsRef.current.has(id);
|
|
40
|
-
if (!hasItem) return;
|
|
44
|
+
if (!hasItem) return;
|
|
41
45
|
|
|
42
46
|
portalsRef.current.delete(id);
|
|
43
47
|
|
|
44
|
-
// Clear any pending updates
|
|
45
48
|
if (pendingUpdateRef.current) {
|
|
46
49
|
clearTimeout(pendingUpdateRef.current);
|
|
47
50
|
pendingUpdateRef.current = null;
|
|
@@ -72,6 +75,7 @@ export const TooltipPortalProvider: React.FC<{ children: ReactNode }> = ({
|
|
|
72
75
|
);
|
|
73
76
|
};
|
|
74
77
|
|
|
78
|
+
/** Full-screen, touch-through layer that renders every registered tooltip. */
|
|
75
79
|
export const TooltipPortalHost: React.FC = () => {
|
|
76
80
|
const context = useContext(TooltipPortalContext);
|
|
77
81
|
|
|
@@ -95,6 +99,10 @@ export const TooltipPortalHost: React.FC = () => {
|
|
|
95
99
|
);
|
|
96
100
|
};
|
|
97
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Tooltip portal API. Outside a provider it returns no-ops (and warns in dev)
|
|
104
|
+
* so a tooltip degrades instead of crashing.
|
|
105
|
+
*/
|
|
98
106
|
export const useTooltipPortal = () => {
|
|
99
107
|
const context = useContext(TooltipPortalContext);
|
|
100
108
|
|
|
@@ -104,7 +112,6 @@ export const useTooltipPortal = () => {
|
|
|
104
112
|
'useTooltipPortal must be used within TooltipPortalProvider. Tooltips may not appear correctly.',
|
|
105
113
|
);
|
|
106
114
|
}
|
|
107
|
-
// Return no-op functions if context is missing
|
|
108
115
|
return {
|
|
109
116
|
register: () => { },
|
|
110
117
|
unregister: () => { },
|
|
@@ -18,6 +18,7 @@ import { StyleSheet, View } from 'react-native';
|
|
|
18
18
|
|
|
19
19
|
const Stack = createStackNavigator();
|
|
20
20
|
|
|
21
|
+
/** Widget screen that reports its laid-out height to size the container. */
|
|
21
22
|
const WidgetContent: React.FC = ({ route }: any) => {
|
|
22
23
|
const Component = route?.params?.screen;
|
|
23
24
|
return (
|
|
@@ -31,6 +32,10 @@ const WidgetContent: React.FC = ({ route }: any) => {
|
|
|
31
32
|
);
|
|
32
33
|
};
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Root of an embedded widget: its own navigator with the widget screen and the
|
|
37
|
+
* `Modal` route, sized to the widget's content height.
|
|
38
|
+
*/
|
|
34
39
|
const WidgetContainer: React.FC<WidgetContainerProps> = ({
|
|
35
40
|
widget,
|
|
36
41
|
theme = defaultTheme,
|
|
@@ -61,11 +66,9 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
|
|
|
61
66
|
headerGradient = context?.designConfig?.headerGradient;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
/**
|
|
65
|
-
* inject data for navigator
|
|
66
|
-
*/
|
|
67
69
|
navigator.current.maxApi = maxApi;
|
|
68
70
|
|
|
71
|
+
/** Text for a localization key, or the current-language side of `{ vi, en }`. */
|
|
69
72
|
const translate = (data?: string | { vi: string; en: string }) => {
|
|
70
73
|
if (data && typeof data !== 'string') {
|
|
71
74
|
return localize?.translateData(data);
|
|
@@ -81,7 +84,10 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
|
|
|
81
84
|
};
|
|
82
85
|
const isWhiteList = isWhiteListDesignSystem(mergedContext);
|
|
83
86
|
const fontFamily = isWhiteList ? 'MomoTrustSans' : 'SFProText';
|
|
84
|
-
|
|
87
|
+
let navigationFontFamily = 'SFProText';
|
|
88
|
+
if (isWhiteList) {
|
|
89
|
+
navigationFontFamily = 'MomoTrustSans-Regular';
|
|
90
|
+
}
|
|
85
91
|
|
|
86
92
|
return (
|
|
87
93
|
<View style={[styles.container, { height: height ?? '100%' }]}>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useWindowDimensions, type ViewStyle } from 'react-native';
|
|
3
|
+
import type { StackNavigationOptions } from '@react-navigation/stack';
|
|
4
|
+
import { Spacing } from '../Consts';
|
|
5
|
+
import { getHeaderMargin } from '../Layout/utils';
|
|
6
|
+
import { FOLDABLE_MIN_WIDTH } from './modalLayout';
|
|
7
|
+
import { isIPhoneDuo } from './utils';
|
|
8
|
+
|
|
9
|
+
// Top navigation spec, shared with the Compose / SwiftUI kits
|
|
10
|
+
const HEADER_FRAME_MAX_WIDTH = 600;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Top navigation geometry on every device
|
|
14
|
+
* - buttonSize / iconSize: round back and shortcut buttons, icons inside them and the pill
|
|
15
|
+
* - pillWidth / pillHeight: help center | home capsule
|
|
16
|
+
* - clusterWidth: shortcut, gap and pill on the right of the bar
|
|
17
|
+
* - searchHeight: search field inside the header
|
|
18
|
+
* - titleGap: space between back and the title, and the title and the right cluster
|
|
19
|
+
* - sidePadding: bar edge to back and to the right cluster
|
|
20
|
+
* - gradientHeight: header gradient, kept at the phone height on wide windows
|
|
21
|
+
* - bannerWidth: animated banner image, kept at the phone width on wide windows
|
|
22
|
+
*/
|
|
23
|
+
const HeaderBarMetrics = {
|
|
24
|
+
buttonSize: 32,
|
|
25
|
+
iconSize: 20,
|
|
26
|
+
pillWidth: 72,
|
|
27
|
+
pillHeight: 32,
|
|
28
|
+
clusterWidth: 112,
|
|
29
|
+
searchHeight: 32,
|
|
30
|
+
titleGap: Spacing.S,
|
|
31
|
+
sidePadding: Spacing.M,
|
|
32
|
+
gradientHeight: 154,
|
|
33
|
+
bannerWidth: 375,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `grid` keeps the header row at the bar edges, `stretch` centers it in a 600pt
|
|
38
|
+
* frame on wide windows. Background and status bar always span the window.
|
|
39
|
+
*/
|
|
40
|
+
type HeaderContentLayout = 'grid' | 'stretch';
|
|
41
|
+
|
|
42
|
+
type HeaderLayout = typeof HeaderBarMetrics & {
|
|
43
|
+
/**
|
|
44
|
+
* Window whose short side is at least 600 (Galaxy Z Fold / Pixel Fold inner
|
|
45
|
+
* display, tablets), iPhone Duo excluded. Phones, landscape included, are not.
|
|
46
|
+
*/
|
|
47
|
+
isWide: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Window edge to back and to the right cluster: `sidePadding`, or centering the
|
|
50
|
+
* row in the 600pt frame with `stretch` on a wide window.
|
|
51
|
+
*/
|
|
52
|
+
frameInset: number;
|
|
53
|
+
/**
|
|
54
|
+
* Header row width between the two `frameInset`s.
|
|
55
|
+
*/
|
|
56
|
+
frameWidth: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type HeaderLayoutInput = {
|
|
60
|
+
width: number;
|
|
61
|
+
height: number;
|
|
62
|
+
isDuo: boolean;
|
|
63
|
+
contentLayout?: HeaderContentLayout;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Header geometry for a window, pure so it can be tested without a device.
|
|
68
|
+
*/
|
|
69
|
+
const getHeaderLayout = ({
|
|
70
|
+
width,
|
|
71
|
+
height,
|
|
72
|
+
isDuo,
|
|
73
|
+
contentLayout = 'grid',
|
|
74
|
+
}: HeaderLayoutInput): HeaderLayout => {
|
|
75
|
+
const { sidePadding } = HeaderBarMetrics;
|
|
76
|
+
const isWide = !isDuo && Math.min(width, height) >= FOLDABLE_MIN_WIDTH;
|
|
77
|
+
let frameInset = sidePadding;
|
|
78
|
+
if (isWide && contentLayout === 'stretch') {
|
|
79
|
+
const centered = (width - HEADER_FRAME_MAX_WIDTH) / 2;
|
|
80
|
+
frameInset = Math.max(sidePadding, centered);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
...HeaderBarMetrics,
|
|
84
|
+
isWide,
|
|
85
|
+
frameInset,
|
|
86
|
+
frameWidth: width - frameInset * 2,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Widest a title can be between back and the right cluster of the header row.
|
|
92
|
+
*/
|
|
93
|
+
const getTitleBudget = (layout: HeaderLayout) => {
|
|
94
|
+
const { buttonSize, titleGap, clusterWidth } = layout;
|
|
95
|
+
return layout.frameWidth - buttonSize - titleGap * 2 - clusterWidth;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Header geometry for the current window; follows fold, unfold and rotation.
|
|
100
|
+
*/
|
|
101
|
+
const useHeaderLayout = (contentLayout?: HeaderContentLayout): HeaderLayout => {
|
|
102
|
+
const { width, height } = useWindowDimensions();
|
|
103
|
+
const isDuo = isIPhoneDuo();
|
|
104
|
+
return useMemo(() => {
|
|
105
|
+
return getHeaderLayout({ width, height, isDuo, contentLayout });
|
|
106
|
+
}, [width, height, isDuo, contentLayout]);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const TITLE_CONTAINER_RESET: ViewStyle = {
|
|
110
|
+
marginLeft: 0,
|
|
111
|
+
marginEnd: 0,
|
|
112
|
+
marginStart: 0,
|
|
113
|
+
marginHorizontal: 0,
|
|
114
|
+
marginVertical: 0,
|
|
115
|
+
margin: 0,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* React Navigation container styles that keep back, title and the right cluster
|
|
120
|
+
* in the `stretch` frame. `undefined` for `grid`, and on iPhone Duo, which keeps
|
|
121
|
+
* its own header row.
|
|
122
|
+
*/
|
|
123
|
+
const useHeaderFrameOptions = (
|
|
124
|
+
contentLayout?: HeaderContentLayout,
|
|
125
|
+
): StackNavigationOptions | undefined => {
|
|
126
|
+
const layout = useHeaderLayout(contentLayout);
|
|
127
|
+
const { width } = useWindowDimensions();
|
|
128
|
+
const isDuo = isIPhoneDuo();
|
|
129
|
+
return useMemo(() => {
|
|
130
|
+
if (contentLayout !== 'stretch' || isDuo) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
// the header content row already sits this far in on large iOS windows
|
|
134
|
+
const shift = layout.frameInset - layout.sidePadding;
|
|
135
|
+
const containerShift = Math.max(0, shift - getHeaderMargin(width));
|
|
136
|
+
return {
|
|
137
|
+
headerLeftContainerStyle: { paddingLeft: containerShift },
|
|
138
|
+
headerRightContainerStyle: { paddingRight: containerShift },
|
|
139
|
+
headerTitleContainerStyle: {
|
|
140
|
+
...TITLE_CONTAINER_RESET,
|
|
141
|
+
left: layout.titleGap,
|
|
142
|
+
maxWidth: getTitleBudget(layout),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}, [contentLayout, isDuo, layout, width]);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export type { HeaderContentLayout, HeaderLayout };
|
|
149
|
+
|
|
150
|
+
export {
|
|
151
|
+
HeaderBarMetrics,
|
|
152
|
+
HEADER_FRAME_MAX_WIDTH,
|
|
153
|
+
TITLE_CONTAINER_RESET,
|
|
154
|
+
getHeaderLayout,
|
|
155
|
+
getTitleBudget,
|
|
156
|
+
useHeaderLayout,
|
|
157
|
+
useHeaderFrameOptions,
|
|
158
|
+
};
|
package/Application/index.ts
CHANGED
|
@@ -26,6 +26,13 @@ import {
|
|
|
26
26
|
type ModalLayout,
|
|
27
27
|
type ModalRect,
|
|
28
28
|
} from './modalLayout';
|
|
29
|
+
import {
|
|
30
|
+
HeaderBarMetrics,
|
|
31
|
+
getHeaderLayout,
|
|
32
|
+
useHeaderLayout,
|
|
33
|
+
type HeaderContentLayout,
|
|
34
|
+
type HeaderLayout,
|
|
35
|
+
} from './headerLayout';
|
|
29
36
|
|
|
30
37
|
export {
|
|
31
38
|
NavigationContainer,
|
|
@@ -41,6 +48,9 @@ export {
|
|
|
41
48
|
useTooltipPortal,
|
|
42
49
|
getModalLayout,
|
|
43
50
|
useModalLayout,
|
|
51
|
+
HeaderBarMetrics,
|
|
52
|
+
getHeaderLayout,
|
|
53
|
+
useHeaderLayout,
|
|
44
54
|
};
|
|
45
55
|
|
|
46
|
-
export type { ModalLayout, ModalRect };
|
|
56
|
+
export type { ModalLayout, ModalRect, HeaderContentLayout, HeaderLayout };
|