@momo-kits/foundation 0.168.1-beta.35 → 0.168.1-beta.37

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.
@@ -1,65 +1,50 @@
1
- import React, {
2
- useCallback,
3
- useContext,
4
- useEffect,
5
- useRef,
6
- useState,
7
- } from 'react';
1
+ import React, { useCallback, useContext, useEffect, useRef } from 'react';
8
2
  import {
9
- BackHandler,
10
- Dimensions,
11
- Keyboard,
12
3
  KeyboardAvoidingView,
13
- KeyboardEvent,
14
- LayoutAnimation,
15
4
  Modal,
16
5
  PanResponder,
17
6
  Platform,
18
7
  Pressable,
19
8
  StyleSheet,
20
- useWindowDimensions,
21
9
  View,
22
10
  } from 'react-native';
23
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
24
11
  import { ApplicationContext, MiniAppContext } from '../Context';
25
12
  import { BottomSheetParams } from './types';
26
- import { Colors, Radius, Spacing, Styles } from '../Consts';
27
- import { Text } from '../Text';
28
- import { useHeaderHeight } from '@react-navigation/elements';
29
13
  import Animated, {
30
14
  Easing,
31
- Extrapolation,
32
- interpolate,
33
- useAnimatedStyle,
34
15
  useSharedValue,
35
16
  withSpring,
36
17
  withTiming,
37
18
  } from 'react-native-reanimated';
38
19
  import { scheduleOnRN } from 'react-native-worklets';
39
- import layoutStyles from '../Layout/styles';
40
- import { isIPhoneDuo } from './device';
41
- import { useModalLayout } from './modalLayout';
42
- import { MODAL_ICON_BUTTON_SIZE, ModalIconButton } from './ModalIconButton';
43
-
44
- const INDICATOR_HEIGHT = 16;
45
- const HEADER_HEIGHT = 56;
46
- const DUO_ORIENTATIONS = ['portrait', 'landscape'];
20
+ import { DUO_ORIENTATIONS, styles } from './BottomSheet.styles';
21
+ import {
22
+ useBottomSheetLayout,
23
+ useDuoKeyboardLift,
24
+ useSheetDismiss,
25
+ useSheetAnimatedStyles,
26
+ } from './BottomSheet.hooks';
27
+ import {
28
+ getSheetStyle,
29
+ renderSheetFooter,
30
+ renderSheetHeader,
31
+ } from './BottomSheet.parts';
47
32
 
48
33
  const BottomSheet: React.FC<BottomSheetParams> = props => {
49
34
  const { theme, navigator } = useContext(ApplicationContext);
50
35
  const context = useContext<any>(MiniAppContext);
51
- const windowSize = useWindowDimensions();
52
- const isDuo = isIPhoneDuo();
53
- const { isFoldable, placement, sheet, scrimOpacity } = useModalLayout();
54
- // 'screen' is the outer display on iPhone Duo, even when unfolded
55
- const screenHeight = Dimensions.get('screen').height;
56
- const heightDevice = isDuo ? windowSize.height : screenHeight;
36
+ const {
37
+ windowSize,
38
+ isDuo,
39
+ isFoldable,
40
+ placement,
41
+ sheet,
42
+ scrimOpacity,
43
+ heightDevice,
44
+ bottomInset,
45
+ keyboardOffset,
46
+ } = useBottomSheetLayout();
57
47
  const action = useRef<undefined | string>(undefined);
58
- const insets = useSafeAreaInsets();
59
- const heightHeader = useHeaderHeight();
60
- const isIOS = Platform.OS === 'ios';
61
- const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
62
- const keyboardOffset = heightHeader - bottomInset;
63
48
 
64
49
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
65
50
 
@@ -81,43 +66,13 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
81
66
  const translateY = useSharedValue(heightDevice);
82
67
  const onDismissRef = useRef(props.route.params?.onDismiss);
83
68
  onDismissRef.current = props.route.params?.onDismiss;
84
- const [keyboardLift, setKeyboardLift] = useState(0);
85
-
86
- // KeyboardAvoidingView lifts the whole sheet past the top on iPhone Duo,
87
- // so lift it here and shrink maxHeight by the same amount instead
88
- useEffect(() => {
89
- if (!isDuo || !useKeyboardAvoidingView) {
90
- return;
91
- }
92
- const onKeyboardChange = (e: KeyboardEvent) => {
93
- const overlap = windowSize.height - e.endCoordinates.screenY;
94
- const lift = Math.max(
95
- overlap - bottomInset - (keyboardVerticalOffset ?? 0),
96
- 0,
97
- );
98
- if (e.duration) {
99
- LayoutAnimation.configureNext({
100
- duration: e.duration,
101
- update: {
102
- duration: e.duration,
103
- type: LayoutAnimation.Types[e.easing] || 'keyboard',
104
- },
105
- });
106
- }
107
- setKeyboardLift(lift);
108
- };
109
- const subscriptions = [
110
- Keyboard.addListener('keyboardWillChangeFrame', onKeyboardChange),
111
- Keyboard.addListener('keyboardWillHide', onKeyboardChange),
112
- ];
113
- return () => subscriptions.forEach(sub => sub.remove());
114
- }, [
115
- bottomInset,
69
+ const keyboardLift = useDuoKeyboardLift(
116
70
  isDuo,
117
- keyboardVerticalOffset,
118
71
  useKeyboardAvoidingView,
119
72
  windowSize.height,
120
- ]);
73
+ bottomInset,
74
+ keyboardVerticalOffset,
75
+ );
121
76
 
122
77
  const openAnimation = useCallback(() => {
123
78
  translateY.value = withTiming(0, {
@@ -187,126 +142,35 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
187
142
  };
188
143
  }, [openAnimation]);
189
144
 
190
- /**
191
- * handler dismiss
192
- */
193
- const onDismiss = useCallback(
194
- (force = false, callback?: () => void) => {
195
- if (barrierDismissible && !force) {
196
- return;
197
- }
198
- closeAnimation(() => {
199
- navigator?.pop();
200
- scheduleOnRN(() => {
201
- callback?.();
202
- });
203
- });
204
- },
205
- [barrierDismissible, closeAnimation, navigator],
206
- );
207
-
208
- /**
209
- * on request close
210
- */
211
- const onRequestClose = useCallback(
212
- (callback?: () => void) => {
213
- onDismiss(true, callback);
214
- },
215
- [onDismiss],
216
- );
217
-
218
- useEffect(() => {
219
- const backHandler = BackHandler.addEventListener(
220
- 'hardwareBackPress',
221
- () => {
222
- onDismiss();
223
- return true;
224
- },
225
- );
226
- return () => backHandler.remove();
227
- }, [barrierDismissible, onDismiss]);
228
-
229
- const onPressBack = useCallback(() => {
230
- onDismiss(true, leftOptions?.onPressIconLeft);
231
- }, [leftOptions, onDismiss]);
232
-
233
- const onPressClose = useCallback(() => {
234
- action.current = 'icon_close';
235
- onDismiss(true);
236
- }, [onDismiss]);
145
+ const { onDismiss, onRequestClose, onPressBack, onPressClose } =
146
+ useSheetDismiss({
147
+ barrierDismissible,
148
+ closeAnimation,
149
+ navigator,
150
+ leftOptions,
151
+ action,
152
+ });
237
153
 
238
154
  const showBack = !!leftOptions?.onPressIconLeft;
239
155
  const iconBack = leftOptions?.iconLeft ?? 'ic_back';
240
156
 
241
- /**
242
- * render header
243
- */
244
- const renderHeader = useCallback(() => {
245
- return (
246
- <View {...panResponder.panHandlers}>
247
- <View style={{ height: INDICATOR_HEIGHT }}>
248
- <View style={styles.indicator} />
249
- </View>
250
- <View
251
- style={[
252
- styles.header,
253
- useDivider && {
254
- borderBottomColor: theme.colors.border.default,
255
- borderBottomWidth: 1,
256
- },
257
- ]}
258
- >
259
- {showBack ? (
260
- <ModalIconButton icon={iconBack} onPress={onPressBack} />
261
- ) : null}
262
-
263
- {options.header ? (
264
- <View style={Styles.flex}>{options.header}</View>
265
- ) : (
266
- <>
267
- {!leftOptions ? <View style={styles.iconButtonSpace} /> : null}
268
- <Text
269
- style={[Styles.flex, styles.title]}
270
- numberOfLines={1}
271
- typography="header_default_bold"
272
- >
273
- {options.title ?? ''}
274
- </Text>
275
- </>
276
- )}
277
- <ModalIconButton icon="navigation_close" onPress={onPressClose} />
278
- </View>
279
- </View>
280
- );
281
- }, [
282
- iconBack,
283
- leftOptions,
157
+ const headerParams = {
158
+ panHandlers: panResponder.panHandlers,
159
+ useDivider,
160
+ theme,
161
+ showBack,
284
162
  onPressBack,
285
163
  onPressClose,
286
- options.header,
287
- options.title,
288
- panResponder.panHandlers,
289
- showBack,
290
- theme.colors.border.default,
291
- useDivider,
292
- ]);
293
-
294
- const animatedStyle = useAnimatedStyle(() => {
295
- return {
296
- transform: [{ translateY: translateY.value }],
297
- };
298
- });
164
+ iconBack,
165
+ options,
166
+ leftOptions,
167
+ };
299
168
 
300
- const animatedOverlayStyle = useAnimatedStyle(() => {
301
- return {
302
- opacity: interpolate(
303
- translateY.value,
304
- [0, heightDevice],
305
- [scrimOpacity, 0],
306
- Extrapolation.CLAMP,
307
- ),
308
- };
309
- });
169
+ const { animatedStyle, animatedOverlayStyle } = useSheetAnimatedStyles(
170
+ translateY,
171
+ heightDevice,
172
+ scrimOpacity,
173
+ );
310
174
 
311
175
  const screen = (
312
176
  <Screen
@@ -350,51 +214,30 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
350
214
  <Animated.View
351
215
  style={[
352
216
  animatedStyle,
353
- {
217
+ getSheetStyle({
354
218
  backgroundColor,
355
- borderTopLeftRadius: sheet.radius,
356
- borderTopRightRadius: sheet.radius,
357
- overflow: 'hidden',
358
- },
359
- isDuo && { marginBottom: keyboardLift },
360
- isFoldable &&
361
- sheet.width !== undefined &&
362
- sheet.maxHeight !== undefined && {
363
- // centered in the placement area, clear of the iPhone Duo
364
- // control column and on the right half when half folded
365
- width: sheet.width,
366
- marginLeft: placement.x + (placement.width - sheet.width) / 2,
367
- maxHeight: sheet.maxHeight - keyboardLift,
368
- minHeight: sheet.minHeight,
369
- },
370
- showBaseLineDebug && styles.debugBaseLine,
219
+ isDuo,
220
+ isFoldable,
221
+ placement,
222
+ sheet,
223
+ keyboardLift,
224
+ showBaseLineDebug,
225
+ }),
371
226
  ]}
372
227
  >
373
228
  {isFoldable ? (
374
229
  // content shrinks so a tall sheet keeps its header and footer
375
230
  <View style={styles.shrink}>
376
- {renderHeader()}
231
+ {renderSheetHeader(headerParams)}
377
232
  <View style={styles.shrink}>{screen}</View>
378
233
  </View>
379
234
  ) : (
380
235
  <>
381
- {renderHeader()}
236
+ {renderSheetHeader(headerParams)}
382
237
  {screen}
383
238
  </>
384
239
  )}
385
- {footerComponent && (
386
- <View
387
- style={[
388
- layoutStyles.shadow,
389
- {
390
- paddingBottom: Spacing.S,
391
- backgroundColor: theme.colors.background.surface,
392
- },
393
- ]}
394
- >
395
- {footerComponent}
396
- </View>
397
- )}
240
+ {renderSheetFooter({ footerComponent, theme })}
398
241
  {useBottomInset && (
399
242
  <View
400
243
  style={{
@@ -409,38 +252,4 @@ const BottomSheet: React.FC<BottomSheetParams> = props => {
409
252
  );
410
253
  };
411
254
 
412
- const styles = StyleSheet.create({
413
- container: {
414
- flex: 1,
415
- justifyContent: 'flex-end',
416
- },
417
- overlay: {
418
- flex: 1,
419
- backgroundColor: Colors.black_20,
420
- },
421
- indicator: {
422
- width: 40,
423
- height: 4,
424
- borderRadius: Radius.S,
425
- backgroundColor: Colors.black_06,
426
- position: 'absolute',
427
- alignSelf: 'center',
428
- top: Spacing.S,
429
- },
430
- header: {
431
- height: HEADER_HEIGHT,
432
- flexDirection: 'row',
433
- alignItems: 'center',
434
- paddingHorizontal: Spacing.M,
435
- gap: Spacing.S,
436
- },
437
- title: { textAlign: 'center' },
438
- iconButtonSpace: {
439
- width: MODAL_ICON_BUTTON_SIZE,
440
- height: MODAL_ICON_BUTTON_SIZE,
441
- },
442
- shrink: { flexShrink: 1 },
443
- debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
444
- });
445
-
446
255
  export default BottomSheet;
@@ -0,0 +1,65 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { Colors, Spacing } from '../../Consts';
3
+ import { CTA_SHADOW, TabBarMetrics } from './metrics';
4
+
5
+ export const styles = StyleSheet.create({
6
+ bar: {
7
+ position: 'absolute',
8
+ left: 0,
9
+ right: 0,
10
+ bottom: 0,
11
+ alignItems: 'center',
12
+ },
13
+ // transparent strip that keeps the rising center action touchable
14
+ barCtaRise: {
15
+ paddingTop: TabBarMetrics.ctaLift,
16
+ },
17
+ container: {
18
+ height: TabBarMetrics.height,
19
+ },
20
+ content: {
21
+ flex: 1,
22
+ flexDirection: 'row',
23
+ alignItems: 'center',
24
+ gap: TabBarMetrics.itemGap,
25
+ paddingVertical: TabBarMetrics.paddingVertical,
26
+ paddingHorizontal: TabBarMetrics.paddingHorizontal,
27
+ },
28
+ indicator: {
29
+ height: TabBarMetrics.indicatorHeight,
30
+ },
31
+ ctaSlot: {
32
+ width: TabBarMetrics.ctaSlotWidth,
33
+ alignSelf: 'stretch',
34
+ },
35
+ // the button rises ctaLift above the container top, the label ends on the item baseline
36
+ cta: {
37
+ position: 'absolute',
38
+ left: 0,
39
+ right: 0,
40
+ top: -TabBarMetrics.ctaLift - TabBarMetrics.paddingVertical,
41
+ alignItems: 'center',
42
+ gap: TabBarMetrics.ctaLabelGap,
43
+ },
44
+ ctaButton: {
45
+ boxShadow: CTA_SHADOW,
46
+ width: TabBarMetrics.ctaSize,
47
+ height: TabBarMetrics.ctaSize,
48
+ borderRadius: TabBarMetrics.ctaSize / 2,
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ },
52
+ // custom label content over the label text, centered and unclipped
53
+ ctaLabelOverlay: {
54
+ ...StyleSheet.absoluteFillObject,
55
+ alignItems: 'center',
56
+ justifyContent: 'center',
57
+ },
58
+ ctaLabel: {
59
+ maxWidth: TabBarMetrics.ctaSlotWidth,
60
+ minHeight: TabBarMetrics.ctaLabelHeight,
61
+ paddingHorizontal: Spacing.XXS,
62
+ justifyContent: 'center',
63
+ },
64
+ debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
65
+ });
@@ -1,12 +1,6 @@
1
1
  import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
2
2
  import React, { useContext, useState } from 'react';
3
- import {
4
- Platform,
5
- Pressable,
6
- StyleSheet,
7
- useWindowDimensions,
8
- View,
9
- } from 'react-native';
3
+ import { Platform, Pressable, useWindowDimensions, View } from 'react-native';
10
4
  import Animated, {
11
5
  useAnimatedStyle,
12
6
  useSharedValue,
@@ -21,14 +15,10 @@ import CustomBottomTabItem from './CustomBottomTabItem';
21
15
  import { ApplicationContext, MiniAppContext } from '../../Context';
22
16
  import { CurvedContainer } from './CurvedContainer';
23
17
  import { ActiveIndicator } from './ActiveIndicator';
24
- import {
25
- CTA_SHADOW,
26
- getHorizontalBarWidth,
27
- TabBarMetrics,
28
- TabBarMotion,
29
- } from './metrics';
18
+ import { getHorizontalBarWidth, TabBarMetrics, TabBarMotion } from './metrics';
30
19
  import { getTabItems, TabItem } from './tabItems';
31
20
  import { FixedScaleText } from './TabBarIcon';
21
+ import { styles } from './BottomTabBar.styles';
32
22
 
33
23
  type Props = BottomTabBarProps & {
34
24
  activeTintColor: string;
@@ -258,8 +248,8 @@ export default function BottomTabBar(props: Props) {
258
248
  pointerEvents={'box-none'}
259
249
  style={[
260
250
  styles.bar,
251
+ withCta && styles.barCtaRise,
261
252
  {
262
- paddingTop: withCta ? TabBarMetrics.ctaLift : 0,
263
253
  paddingBottom: getSystemIndicatorHeight(insets.bottom),
264
254
  // centered between the side safe areas
265
255
  paddingLeft: insets.left,
@@ -278,64 +268,6 @@ export default function BottomTabBar(props: Props) {
278
268
  );
279
269
  }
280
270
 
281
- const styles = StyleSheet.create({
282
- bar: {
283
- position: 'absolute',
284
- left: 0,
285
- right: 0,
286
- bottom: 0,
287
- alignItems: 'center',
288
- },
289
- container: {
290
- height: TabBarMetrics.height,
291
- },
292
- content: {
293
- flex: 1,
294
- flexDirection: 'row',
295
- alignItems: 'center',
296
- gap: TabBarMetrics.itemGap,
297
- paddingVertical: TabBarMetrics.paddingVertical,
298
- paddingHorizontal: TabBarMetrics.paddingHorizontal,
299
- },
300
- indicator: {
301
- height: TabBarMetrics.indicatorHeight,
302
- },
303
- ctaSlot: {
304
- width: TabBarMetrics.ctaSlotWidth,
305
- alignSelf: 'stretch',
306
- },
307
- // the button rises ctaLift above the container top, the label ends on the item baseline
308
- cta: {
309
- position: 'absolute',
310
- left: 0,
311
- right: 0,
312
- top: -TabBarMetrics.ctaLift - TabBarMetrics.paddingVertical,
313
- alignItems: 'center',
314
- gap: TabBarMetrics.ctaLabelGap,
315
- },
316
- ctaButton: {
317
- boxShadow: CTA_SHADOW,
318
- width: TabBarMetrics.ctaSize,
319
- height: TabBarMetrics.ctaSize,
320
- borderRadius: TabBarMetrics.ctaSize / 2,
321
- alignItems: 'center',
322
- justifyContent: 'center',
323
- },
324
- // custom label content over the label text, centered and unclipped
325
- ctaLabelOverlay: {
326
- ...StyleSheet.absoluteFillObject,
327
- alignItems: 'center',
328
- justifyContent: 'center',
329
- },
330
- ctaLabel: {
331
- maxWidth: TabBarMetrics.ctaSlotWidth,
332
- minHeight: TabBarMetrics.ctaLabelHeight,
333
- paddingHorizontal: Spacing.XXS,
334
- justifyContent: 'center',
335
- },
336
- debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
337
- });
338
-
339
271
  export {
340
272
  HorizontalTabBar,
341
273
  getSystemIndicatorHeight,