@momo-kits/foundation 0.169.7-beta.4 → 0.169.7-beta.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/Application/BottomSheet.hooks.ts +8 -8
- package/Application/BottomSheet.parts.tsx +44 -34
- package/Application/BottomSheet.tsx +40 -37
- 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 +56 -34
- package/Application/Components/HeaderLeft.tsx +4 -2
- package/Application/Components/HeaderRight.styles.ts +15 -9
- package/Application/Components/HeaderRight.tsx +2 -1
- package/Application/Components/HeaderTitle.shared.ts +19 -12
- package/Application/Components/HeaderTitle.tsx +4 -3
- package/Application/Components/HeaderToolkitAction.tsx +28 -22
- package/Application/Components/NavigationButton.tsx +10 -6
- package/Application/Components/SearchHeader.tsx +41 -15
- 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 +10 -10
- package/Application/Navigation.ts +14 -16
- package/Application/NavigationContainer.tsx +19 -18
- package/Application/Navigator.ts +20 -48
- package/Application/ScaleSizeProvider.tsx +13 -6
- package/Application/StackScreen.tracking.ts +11 -33
- package/Application/StackScreen.tsx +19 -54
- 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 +1 -1
- package/Application/KitAdoption/AdoptionBadge.tsx +0 -119
- package/Application/KitAdoption/AdoptionProbe.tsx +0 -166
- package/Application/KitAdoption/AdoptionStore.ts +0 -80
- package/Application/KitAdoption/Authorship/utils.ts +0 -137
- package/Application/KitAdoption/Slots/utils.ts +0 -68
- package/Application/KitAdoption/constants.ts +0 -120
- package/Application/KitAdoption/index.ts +0 -12
- package/Application/KitAdoption/styles.ts +0 -55
- package/Application/KitAdoption/types.ts +0 -61
- package/Application/KitAdoption/utils.ts +0 -376
- package/Application/__tests__/KitAdoption.integration.test.tsx +0 -253
- package/Application/__tests__/KitAdoption.test.ts +0 -283
- package/Application/__tests__/KitAdoptionAuthorship.test.ts +0 -132
- package/Application/__tests__/KitAdoptionStore.test.ts +0 -110
- package/Application/__tests__/KitAdoptionStructural.test.ts +0 -193
- package/Application/__tests__/modalLayout.test.ts +0 -128
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import { Alert } from 'react-native';
|
|
2
2
|
import { version } from '../package.json';
|
|
3
|
-
import { getAdoptionTrackingParams } from './KitAdoption';
|
|
4
|
-
import type { AdoptionMeasureRef } from './KitAdoption/types';
|
|
5
3
|
|
|
6
4
|
type ScreenTrackingDeps = {
|
|
7
5
|
tracking: { current: any };
|
|
8
6
|
context: any;
|
|
9
7
|
navigator: any;
|
|
10
8
|
screenName: string;
|
|
11
|
-
adoptionMeasure?: AdoptionMeasureRef;
|
|
12
9
|
};
|
|
13
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
|
+
*/
|
|
14
15
|
export const createScreenTracking = ({
|
|
15
16
|
tracking,
|
|
16
17
|
context,
|
|
17
18
|
navigator,
|
|
18
19
|
screenName,
|
|
19
|
-
adoptionMeasure,
|
|
20
20
|
}: ScreenTrackingDeps) => {
|
|
21
|
-
/**
|
|
22
|
-
* tracking for screen load
|
|
23
|
-
*/
|
|
21
|
+
/** Reports the screen load time. */
|
|
24
22
|
const onScreenLoad = () => {
|
|
25
23
|
if (!tracking.current?.releaseLoad) {
|
|
26
24
|
let timeLoad = tracking.current.timeLoad;
|
|
@@ -28,8 +26,6 @@ export const createScreenTracking = ({
|
|
|
28
26
|
timeLoad = tracking.current.endTime - tracking.current.startTime;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
const adoption = getAdoptionTrackingParams(adoptionMeasure?.current?.());
|
|
32
|
-
|
|
33
29
|
context?.autoTracking?.({
|
|
34
30
|
...context,
|
|
35
31
|
screenName,
|
|
@@ -40,7 +36,6 @@ export const createScreenTracking = ({
|
|
|
40
36
|
params: tracking.current.params,
|
|
41
37
|
version: version,
|
|
42
38
|
lastElement: tracking.current.lastElement,
|
|
43
|
-
adoption,
|
|
44
39
|
});
|
|
45
40
|
navigator?.maxApi?.stopTrace?.(
|
|
46
41
|
tracking.current.traceIdLoad,
|
|
@@ -49,29 +44,22 @@ export const createScreenTracking = ({
|
|
|
49
44
|
);
|
|
50
45
|
tracking.current.releaseLoad = true;
|
|
51
46
|
|
|
52
|
-
/**
|
|
53
|
-
* debug
|
|
54
|
-
*/
|
|
55
47
|
navigator?.maxApi?.showToastDebug?.({
|
|
56
48
|
appId: context.appId,
|
|
57
49
|
message: `${screenName} screen_load_time ${timeLoad}`,
|
|
58
50
|
type: 'ERROR',
|
|
59
51
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
tracking.current.lastElement?.children?.current?.length > 0
|
|
63
|
-
) {
|
|
52
|
+
const lastChildren = tracking.current.lastElement?.children?.current;
|
|
53
|
+
if (__DEV__ && lastChildren?.length > 0) {
|
|
64
54
|
Alert.alert(
|
|
65
55
|
`${screenName}- load ${timeLoad}ms`,
|
|
66
|
-
JSON.stringify(
|
|
56
|
+
JSON.stringify(lastChildren),
|
|
67
57
|
);
|
|
68
58
|
}
|
|
69
59
|
}
|
|
70
60
|
};
|
|
71
61
|
|
|
72
|
-
/**
|
|
73
|
-
* tracking for screen load
|
|
74
|
-
*/
|
|
62
|
+
/** Reports the time until the screen became interactive. */
|
|
75
63
|
const onScreenInteraction = () => {
|
|
76
64
|
if (!tracking.current?.releaseInteraction) {
|
|
77
65
|
let timeLoad = tracking.current.timeLoad;
|
|
@@ -99,9 +87,6 @@ export const createScreenTracking = ({
|
|
|
99
87
|
);
|
|
100
88
|
tracking.current.releaseInteraction = true;
|
|
101
89
|
|
|
102
|
-
/**
|
|
103
|
-
* debug toast
|
|
104
|
-
*/
|
|
105
90
|
navigator?.maxApi?.showToastDebug?.({
|
|
106
91
|
appId: context.appId,
|
|
107
92
|
message: `${screenName} screen_interaction_time ${tracking.current.timeInteraction}`,
|
|
@@ -110,9 +95,7 @@ export const createScreenTracking = ({
|
|
|
110
95
|
}
|
|
111
96
|
};
|
|
112
97
|
|
|
113
|
-
/**
|
|
114
|
-
* tracking for first interaction by user
|
|
115
|
-
*/
|
|
98
|
+
/** Reports the user's first interaction with the screen. */
|
|
116
99
|
const onFirstInteraction = (action: string) => {
|
|
117
100
|
if (!tracking.current?.releaseUserInteraction) {
|
|
118
101
|
const timeLoad = tracking.current.endTime - tracking.current.startTime;
|
|
@@ -126,9 +109,6 @@ export const createScreenTracking = ({
|
|
|
126
109
|
});
|
|
127
110
|
tracking.current.releaseUserInteraction = true;
|
|
128
111
|
|
|
129
|
-
/**
|
|
130
|
-
* debug
|
|
131
|
-
*/
|
|
132
112
|
navigator?.maxApi?.showToastDebug?.({
|
|
133
113
|
appId: context.appId,
|
|
134
114
|
message: `${screenName} user_interaction ${timeLoad}`,
|
|
@@ -137,9 +117,7 @@ export const createScreenTracking = ({
|
|
|
137
117
|
}
|
|
138
118
|
};
|
|
139
119
|
|
|
140
|
-
/**
|
|
141
|
-
* tracking for loading screen
|
|
142
|
-
*/
|
|
120
|
+
/** Reports how long the skeleton loading state lasted. */
|
|
143
121
|
const onScreenLoading = () => {
|
|
144
122
|
const start = tracking.current.timeStartLoading;
|
|
145
123
|
const end = tracking.current.timeEndLoading;
|
|
@@ -8,15 +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 {
|
|
12
|
-
import type { WalkResult } from './KitAdoption/types';
|
|
11
|
+
import { useHeaderFrameOptions } from './headerLayout';
|
|
13
12
|
|
|
14
13
|
const runAfterInteractions = InteractionManager.runAfterInteractions;
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* @constructor
|
|
16
|
+
* Hosts a feature screen in the stack: applies its header options, provides
|
|
17
|
+
* `ScreenContext` and reports navigation, load and interaction tracking.
|
|
20
18
|
*/
|
|
21
19
|
const StackScreen: React.FC<any> = props => {
|
|
22
20
|
const { navigator } = useContext(ApplicationContext);
|
|
@@ -43,9 +41,6 @@ const StackScreen: React.FC<any> = props => {
|
|
|
43
41
|
lastElement: undefined,
|
|
44
42
|
});
|
|
45
43
|
const widgets = useRef<any>([]);
|
|
46
|
-
const adoptionMeasure = useRef<(() => WalkResult | undefined) | undefined>(
|
|
47
|
-
undefined,
|
|
48
|
-
);
|
|
49
44
|
const context = useContext<any>(MiniAppContext);
|
|
50
45
|
|
|
51
46
|
const {
|
|
@@ -57,6 +52,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
57
52
|
const navigation = useRef(new Navigation(props.navigation, context)).current;
|
|
58
53
|
const heightHeader = useHeaderHeight();
|
|
59
54
|
const { isBackgroundToForeground } = useAppState();
|
|
55
|
+
const headerFrameOptions = useHeaderFrameOptions(options?.contentLayout);
|
|
60
56
|
|
|
61
57
|
const data = {
|
|
62
58
|
...initialParams,
|
|
@@ -69,18 +65,19 @@ const StackScreen: React.FC<any> = props => {
|
|
|
69
65
|
|
|
70
66
|
const screenName = Component?.name || Component?.type?.name || 'Invalid';
|
|
71
67
|
|
|
72
|
-
/**
|
|
73
|
-
* set options for screen
|
|
74
|
-
*/
|
|
75
68
|
useLayoutEffect(() => {
|
|
76
69
|
if (options) {
|
|
77
70
|
navigation.setOptions(options);
|
|
78
71
|
}
|
|
79
72
|
}, [navigation, options]);
|
|
80
73
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
|
|
84
81
|
useEffect(() => {
|
|
85
82
|
let focusScreen: any;
|
|
86
83
|
let onFocusApp: any;
|
|
@@ -154,6 +151,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
154
151
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
155
152
|
}, []);
|
|
156
153
|
|
|
154
|
+
/** Reports a navigation to this screen: `back` if it was shown before. */
|
|
157
155
|
const onScreenNavigated = (pre: string, current: string) => {
|
|
158
156
|
if (!isBackgroundToForeground) {
|
|
159
157
|
const item: any = {
|
|
@@ -171,9 +169,6 @@ const StackScreen: React.FC<any> = props => {
|
|
|
171
169
|
|
|
172
170
|
tracking.current.mounted = true;
|
|
173
171
|
|
|
174
|
-
/**
|
|
175
|
-
* debug toast
|
|
176
|
-
*/
|
|
177
172
|
navigator?.maxApi?.showToastDebug?.({
|
|
178
173
|
appId: context.appId,
|
|
179
174
|
message: `${screenName} screen_navigated`,
|
|
@@ -187,13 +182,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
187
182
|
onScreenInteraction,
|
|
188
183
|
onFirstInteraction,
|
|
189
184
|
onScreenLoading,
|
|
190
|
-
} = createScreenTracking({
|
|
191
|
-
tracking,
|
|
192
|
-
context,
|
|
193
|
-
navigator,
|
|
194
|
-
screenName,
|
|
195
|
-
adoptionMeasure,
|
|
196
|
-
});
|
|
185
|
+
} = createScreenTracking({ tracking, context, navigator, screenName });
|
|
197
186
|
|
|
198
187
|
return (
|
|
199
188
|
<ScreenContext.Provider
|
|
@@ -201,9 +190,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
201
190
|
screenName,
|
|
202
191
|
bottomTab: !!bottomTab,
|
|
203
192
|
onElementLoad: (item: any) => {
|
|
204
|
-
|
|
205
|
-
* widget handle
|
|
206
|
-
*/
|
|
193
|
+
// Widgets record their own load span and don't end the screen load.
|
|
207
194
|
if (item?.componentName === 'Widget') {
|
|
208
195
|
const index = widgets.current.findIndex(
|
|
209
196
|
(element: any) => element.componentId === item.componentId,
|
|
@@ -227,35 +214,25 @@ const StackScreen: React.FC<any> = props => {
|
|
|
227
214
|
return;
|
|
228
215
|
}
|
|
229
216
|
|
|
230
|
-
/**
|
|
231
|
-
* tracking for element screen load
|
|
232
|
-
*/
|
|
233
217
|
clearTimeout(tracking.current.timeoutLoad);
|
|
234
218
|
tracking.current.endTime = Date.now();
|
|
235
219
|
tracking.current.timeoutInteraction?.cancel?.();
|
|
236
220
|
tracking.current.timeoutInteraction = runAfterInteractions(() => {
|
|
237
|
-
tracking.current.
|
|
238
|
-
|
|
221
|
+
const elapsed = Date.now() - tracking.current.startTime;
|
|
222
|
+
tracking.current.timeInteraction = elapsed;
|
|
239
223
|
});
|
|
240
224
|
|
|
241
|
-
/**
|
|
242
|
-
* support for debug last element
|
|
243
|
-
*/
|
|
244
225
|
if (item?.componentName) {
|
|
245
226
|
tracking.current.lastElement = item;
|
|
246
227
|
}
|
|
247
228
|
|
|
248
|
-
|
|
249
|
-
* for stop tracking when user interaction
|
|
250
|
-
*/
|
|
229
|
+
// A user interaction settles load and interaction tracking at once.
|
|
251
230
|
if (item?.interaction) {
|
|
252
231
|
onScreenLoad();
|
|
253
232
|
onScreenInteraction();
|
|
254
233
|
onFirstInteraction(item?.action);
|
|
255
234
|
}
|
|
256
|
-
|
|
257
|
-
* timeout for handle tracking screen
|
|
258
|
-
*/
|
|
235
|
+
// Reports once no element has loaded for 2s.
|
|
259
236
|
tracking.current.timeoutLoad = setTimeout(() => {
|
|
260
237
|
const time = tracking.current.endTime - tracking.current.startTime;
|
|
261
238
|
tracking.current.widgets = widgets.current;
|
|
@@ -273,16 +250,10 @@ const StackScreen: React.FC<any> = props => {
|
|
|
273
250
|
tracking.current.timeStartLoading = Date.now();
|
|
274
251
|
}
|
|
275
252
|
|
|
276
|
-
/**
|
|
277
|
-
* tracking for loading screen
|
|
278
|
-
*/
|
|
279
253
|
if (!loading) {
|
|
280
254
|
tracking.current.timeEndLoading = Date.now();
|
|
281
255
|
}
|
|
282
256
|
|
|
283
|
-
/**
|
|
284
|
-
* timeout for handle tracking screen
|
|
285
|
-
*/
|
|
286
257
|
clearTimeout(tracking.current.timeoutLoading);
|
|
287
258
|
tracking.current.timeoutLoading = setTimeout(() => {
|
|
288
259
|
onScreenLoading();
|
|
@@ -294,13 +265,7 @@ const StackScreen: React.FC<any> = props => {
|
|
|
294
265
|
}}
|
|
295
266
|
>
|
|
296
267
|
<TooltipPortalProvider>
|
|
297
|
-
<
|
|
298
|
-
screenName={screenName}
|
|
299
|
-
navigation={props.navigation}
|
|
300
|
-
measureRef={adoptionMeasure}
|
|
301
|
-
>
|
|
302
|
-
<Component heightHeader={heightHeader} {...data} />
|
|
303
|
-
</AdoptionProbe>
|
|
268
|
+
<Component heightHeader={heightHeader} {...data} />
|
|
304
269
|
<TooltipPortalHost />
|
|
305
270
|
<SwipeBackHaptic />
|
|
306
271
|
</TooltipPortalProvider>
|
|
@@ -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 };
|
|
@@ -73,11 +73,12 @@ type ModalLayoutInput = {
|
|
|
73
73
|
hinge?: ReservedRegion;
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
const clamp = (value: number, min: number, max: number) =>
|
|
77
|
-
Math.min(Math.max(value, min), max);
|
|
76
|
+
const clamp = (value: number, min: number, max: number) => {
|
|
77
|
+
return Math.min(Math.max(value, min), max);
|
|
78
|
+
};
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
|
-
* Modal geometry for a window, pure so it can be tested without a device
|
|
81
|
+
* Modal geometry for a window, pure so it can be tested without a device.
|
|
81
82
|
*/
|
|
82
83
|
const getModalLayout = ({
|
|
83
84
|
width,
|
|
@@ -91,8 +92,9 @@ const getModalLayout = ({
|
|
|
91
92
|
const isFoldable = isDuo || shortSide >= FOLDABLE_MIN_WIDTH;
|
|
92
93
|
const isDuoOuter = isDuo && shortSide < FOLDABLE_MIN_WIDTH;
|
|
93
94
|
const hingeRight = hinge ? hinge.x + hinge.width : 0;
|
|
94
|
-
const
|
|
95
|
-
|
|
95
|
+
const hasHinge = !!hinge && hinge.x > 0;
|
|
96
|
+
const isHingeInWindow = hingeRight < width - insets.right;
|
|
97
|
+
const isHalfFolded = isDuo && hasHinge && isHingeInWindow;
|
|
96
98
|
|
|
97
99
|
const safeLeft = isHalfFolded ? hingeRight : insets.left;
|
|
98
100
|
const placement = {
|
|
@@ -112,9 +114,9 @@ const getModalLayout = ({
|
|
|
112
114
|
minWidth: popupWidth,
|
|
113
115
|
maxWidth: popupWidth,
|
|
114
116
|
margin: Spacing.XL,
|
|
115
|
-
radius: Radius.
|
|
117
|
+
radius: Radius.XL,
|
|
116
118
|
},
|
|
117
|
-
sheet: { margin: 0, radius: Radius.
|
|
119
|
+
sheet: { margin: 0, radius: Radius.XL },
|
|
118
120
|
scrimOpacity: SCRIM_OPACITY,
|
|
119
121
|
};
|
|
120
122
|
}
|
|
@@ -125,6 +127,7 @@ const getModalLayout = ({
|
|
|
125
127
|
|
|
126
128
|
const sheetMargin = Spacing.M;
|
|
127
129
|
const sheetSpace = placement.width - sheetMargin * 2;
|
|
130
|
+
const sheetMinWidth = Math.min(SHEET_MIN_WIDTH, sheetSpace);
|
|
128
131
|
const sheetMaxHeight = height - insets.top - Spacing.M;
|
|
129
132
|
|
|
130
133
|
return {
|
|
@@ -139,11 +142,7 @@ const getModalLayout = ({
|
|
|
139
142
|
radius: Radius.XL,
|
|
140
143
|
},
|
|
141
144
|
sheet: {
|
|
142
|
-
width: clamp(
|
|
143
|
-
sheetSpace,
|
|
144
|
-
Math.min(SHEET_MIN_WIDTH, sheetSpace),
|
|
145
|
-
SHEET_MAX_WIDTH,
|
|
146
|
-
),
|
|
145
|
+
width: clamp(sheetSpace, sheetMinWidth, SHEET_MAX_WIDTH),
|
|
147
146
|
maxHeight: sheetMaxHeight,
|
|
148
147
|
minHeight: Math.min(SHEET_MIN_HEIGHT, sheetMaxHeight),
|
|
149
148
|
margin: sheetMargin,
|
|
@@ -154,8 +153,8 @@ const getModalLayout = ({
|
|
|
154
153
|
};
|
|
155
154
|
|
|
156
155
|
/**
|
|
157
|
-
* Modal geometry for the current window
|
|
158
|
-
* iPhone Duo hinge
|
|
156
|
+
* Modal geometry for the current window; follows fold, unfold, rotation and
|
|
157
|
+
* the iPhone Duo hinge.
|
|
159
158
|
*/
|
|
160
159
|
const useModalLayout = (): ModalLayout => {
|
|
161
160
|
const { width, height } = useWindowDimensions();
|