@momo-kits/foundation 0.92.6-rc.9 → 0.92.8

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.
@@ -0,0 +1,225 @@
1
+ import React, {useContext, useEffect, useRef, useState} from 'react';
2
+ import {
3
+ Animated,
4
+ Dimensions,
5
+ KeyboardAvoidingView,
6
+ Modal,
7
+ PanResponder,
8
+ Platform,
9
+ StyleSheet,
10
+ TouchableOpacity,
11
+ View,
12
+ } from 'react-native';
13
+ import {ApplicationContext} from './index';
14
+ import {BottomSheetParams} from './types';
15
+ import {useSafeAreaInsets} from 'react-native-safe-area-context';
16
+ import {Colors, Radius, Spacing, Styles} from '../Consts';
17
+ import {Text} from '../Text';
18
+ import {Icon} from '../Icon';
19
+
20
+ const BottomSheet: React.FC<BottomSheetParams> = props => {
21
+ const {theme, navigator} = useContext(ApplicationContext);
22
+ const heightDevice = Dimensions.get('screen').height;
23
+ const insets = useSafeAreaInsets();
24
+ const {
25
+ screen: Screen,
26
+ options,
27
+ surface,
28
+ barrierDismissible = false,
29
+ snapPoint = 'content',
30
+ draggable = true,
31
+ dragOnContent = false,
32
+ }: BottomSheetParams = props.route.params;
33
+ const [modalVisible, setModalVisible] = useState(false);
34
+ const animatedHeight = useRef(new Animated.Value(0)).current;
35
+ const pan = useRef(new Animated.ValueXY()).current;
36
+ const panResponder = useRef(
37
+ PanResponder.create({
38
+ onStartShouldSetPanResponder: () => draggable,
39
+ onMoveShouldSetPanResponder: (_, gestureState) => {
40
+ return draggable && dragOnContent && gestureState.dy > 0;
41
+ },
42
+ onPanResponderMove: (e, gestureState) => {
43
+ gestureState.dy > 0 &&
44
+ Animated.event([null, {dy: pan.y}], {useNativeDriver: false})(
45
+ e,
46
+ gestureState,
47
+ );
48
+ },
49
+ onPanResponderRelease: (_, gestureState) => {
50
+ if (gestureState.dy > 100) {
51
+ onDismiss();
52
+ } else {
53
+ Animated.spring(pan, {
54
+ toValue: {x: 0, y: 0},
55
+ useNativeDriver: false,
56
+ }).start();
57
+ }
58
+ },
59
+ }),
60
+ ).current;
61
+ const estimate = useRef(0.9);
62
+
63
+ let backgroundColor = theme.colors.background.default;
64
+ if (surface) {
65
+ backgroundColor = theme.colors.background.surface;
66
+ }
67
+
68
+ /**
69
+ * handle open & emit dismiss event
70
+ */
71
+ useEffect(() => {
72
+ setTimeout(() => {
73
+ const mapSnapPoint: any = {
74
+ '30%': 0.3,
75
+ '50%': 0.5,
76
+ '70%': 0.7,
77
+ '90%': 0.9,
78
+ };
79
+ const point = mapSnapPoint[snapPoint] ?? estimate.current;
80
+ setModalVisible(true);
81
+ Animated.timing(animatedHeight, {
82
+ useNativeDriver: false,
83
+ toValue: heightDevice * point,
84
+ duration: 300,
85
+ }).start();
86
+ }, 100);
87
+
88
+ return () => {
89
+ props.route.params?.onDismiss?.();
90
+ };
91
+ }, []);
92
+
93
+ /**
94
+ * handler dismiss
95
+ */
96
+ const onDismiss = (force = false) => {
97
+ if (barrierDismissible && !force) {
98
+ return;
99
+ }
100
+ Animated.timing(animatedHeight, {
101
+ useNativeDriver: false,
102
+ toValue: 0,
103
+ duration: 200,
104
+ }).start(() => {
105
+ pan.setValue({x: 0, y: 0});
106
+ navigator?.pop();
107
+ });
108
+ };
109
+
110
+ return (
111
+ <>
112
+ <Modal
113
+ transparent
114
+ visible={modalVisible}
115
+ onRequestClose={() => onDismiss()}>
116
+ <KeyboardAvoidingView
117
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
118
+ keyboardVerticalOffset={-20}
119
+ style={Styles.flex}>
120
+ <TouchableOpacity
121
+ style={Styles.flex}
122
+ activeOpacity={1}
123
+ onPress={() => onDismiss()}
124
+ />
125
+ <Animated.View
126
+ {...(dragOnContent && panResponder.panHandlers)}
127
+ style={[
128
+ styles.container,
129
+ {transform: pan.getTranslateTransform()},
130
+ {
131
+ height: animatedHeight,
132
+ backgroundColor,
133
+ },
134
+ ]}>
135
+ {draggable && (
136
+ <View
137
+ {...(!dragOnContent && panResponder.panHandlers)}
138
+ style={styles.draggableContainer}>
139
+ <View style={{height: 16}}>
140
+ <View style={styles.indicator} />
141
+ </View>
142
+ <View
143
+ style={[
144
+ styles.header,
145
+ surface && {
146
+ borderBottomColor: theme.colors.border.default,
147
+ borderBottomWidth: 1,
148
+ },
149
+ ]}>
150
+ <View style={styles.iconButton} />
151
+ <Text
152
+ style={[Styles.flex, {textAlign: 'center'}]}
153
+ numberOfLines={1}
154
+ typography="header_default_bold">
155
+ {options.title ?? ''}
156
+ </Text>
157
+ <TouchableOpacity
158
+ style={styles.iconButton}
159
+ onPress={() => onDismiss(true)}>
160
+ <Icon source="navigation_close" />
161
+ </TouchableOpacity>
162
+ </View>
163
+ </View>
164
+ )}
165
+ <Screen {...props} {...props.route.params} />
166
+ <View style={{height: Math.min(insets.bottom, 21)}} />
167
+ </Animated.View>
168
+ </KeyboardAvoidingView>
169
+ </Modal>
170
+ {snapPoint == 'content' && (
171
+ <View
172
+ style={styles.estimate}
173
+ onLayout={e => {
174
+ estimate.current =
175
+ (e.nativeEvent.layout.height + 68) / heightDevice;
176
+ }}>
177
+ <Screen {...props} {...props.route.params} />
178
+ <View style={{height: Math.min(insets.bottom, 21)}} />
179
+ </View>
180
+ )}
181
+ </>
182
+ );
183
+ };
184
+ export default BottomSheet;
185
+
186
+ const styles = StyleSheet.create({
187
+ container: {
188
+ width: '100%',
189
+ height: 0,
190
+ overflow: 'hidden',
191
+ borderTopLeftRadius: Radius.M,
192
+ borderTopRightRadius: Radius.M,
193
+ },
194
+ draggableContainer: {
195
+ width: '100%',
196
+ alignItems: 'center',
197
+ },
198
+ indicator: {
199
+ width: 40,
200
+ height: 4,
201
+ borderRadius: Radius.S,
202
+ backgroundColor: Colors.black_06,
203
+ position: 'absolute',
204
+ alignSelf: 'center',
205
+ top: Spacing.S,
206
+ },
207
+ header: {
208
+ height: 56,
209
+ flexDirection: 'row',
210
+ alignItems: 'center',
211
+ },
212
+ iconButton: {
213
+ width: 24,
214
+ height: 24,
215
+ alignItems: 'center',
216
+ justifyContent: 'center',
217
+ marginHorizontal: Spacing.M,
218
+ },
219
+ estimate: {
220
+ position: 'absolute',
221
+ top: -9999,
222
+ left: -9999,
223
+ opacity: 0,
224
+ },
225
+ });
@@ -1,33 +1,26 @@
1
1
  import {BottomTabBarProps} from '@react-navigation/bottom-tabs';
2
- import {LabelPosition} from '@react-navigation/bottom-tabs/src/types';
3
- import BottomTabBarHeightCallbackContext from '@react-navigation/bottom-tabs/src/utils/BottomTabBarHeightCallbackContext';
4
2
  import BottomTabBarItem from '@react-navigation/bottom-tabs/src/views/BottomTabItem';
5
3
  import {
6
4
  CommonActions,
7
5
  NavigationContext,
8
6
  NavigationRouteContext,
9
- ParamListBase,
10
- TabNavigationState,
11
7
  useLinkBuilder,
12
- useTheme,
13
8
  } from '@react-navigation/native';
14
- import React, {useCallback} from 'react';
9
+ import React, {useCallback, useContext} from 'react';
15
10
  import {
16
11
  Animated,
17
- LayoutChangeEvent,
18
12
  Platform,
19
- StyleProp,
20
13
  StyleSheet,
21
14
  TouchableOpacity,
22
15
  useWindowDimensions,
23
16
  View,
24
- ViewStyle,
25
17
  } from 'react-native';
26
18
  import {EdgeInsets, useSafeAreaInsets} from 'react-native-safe-area-context';
27
19
  import {Radius} from '../../Consts';
28
20
  import {Icon} from '../../Icon';
29
21
  import {Text} from '../../Text';
30
22
  import {FloatingButtonProps} from '../types';
23
+ import {ApplicationContext} from '../index';
31
24
 
32
25
  type Props = BottomTabBarProps & {
33
26
  activeTintColor?: string;
@@ -36,87 +29,8 @@ type Props = BottomTabBarProps & {
36
29
  accessibilityLabel?: string;
37
30
  };
38
31
 
39
- type Options = {
40
- state: TabNavigationState<ParamListBase>;
41
- layout: {height: number; width: number};
42
- dimensions: {height: number; width: number};
43
- tabStyle: StyleProp<ViewStyle>;
44
- labelPosition: LabelPosition | undefined;
45
- adaptive: boolean | undefined;
46
- };
47
-
48
- const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
49
- const DEFAULT_TABBAR_HEIGHT = 49;
50
- const COMPACT_TABBAR_HEIGHT = 32;
51
-
52
- const shouldUseHorizontalLabels = ({
53
- state,
54
- layout,
55
- dimensions,
56
- adaptive = true,
57
- labelPosition,
58
- tabStyle,
59
- }: Options) => {
60
- if (labelPosition) {
61
- return labelPosition === 'beside-icon';
62
- }
63
-
64
- if (!adaptive) {
65
- return false;
66
- }
67
-
68
- if (layout.width >= 768) {
69
- // Screen size matches a tablet
70
- let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
71
-
72
- const flattenedStyle = StyleSheet.flatten(tabStyle);
73
-
74
- if (flattenedStyle) {
75
- if (typeof flattenedStyle.width === 'number') {
76
- maxTabItemWidth = flattenedStyle.width;
77
- } else if (typeof flattenedStyle.maxWidth === 'number') {
78
- maxTabItemWidth = flattenedStyle.maxWidth;
79
- }
80
- }
81
-
82
- return state.routes.length * maxTabItemWidth <= layout.width;
83
- } else {
84
- return dimensions.width > dimensions.height;
85
- }
86
- };
87
- const getPaddingBottom = (insets: EdgeInsets) =>
88
- Math.max(insets.bottom - Platform.select({ios: 4, default: 0}), 0);
89
-
90
- export const getTabBarHeight = ({
91
- dimensions,
92
- insets,
93
- style,
94
- ...rest
95
- }: Options & {
96
- insets: EdgeInsets;
97
- style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
98
- }) => {
99
- // @ts-ignore
100
- const customHeight = StyleSheet.flatten(style)?.height;
101
-
102
- if (typeof customHeight === 'number') {
103
- return customHeight;
104
- }
105
-
106
- const isLandscape = dimensions.width > dimensions.height;
107
- const horizontalLabels = shouldUseHorizontalLabels({dimensions, ...rest});
108
- const paddingBottom = getPaddingBottom(insets);
109
-
110
- if (
111
- Platform.OS === 'ios' &&
112
- !Platform.isPad &&
113
- isLandscape &&
114
- horizontalLabels
115
- ) {
116
- return COMPACT_TABBAR_HEIGHT + paddingBottom;
117
- }
118
-
119
- return DEFAULT_TABBAR_HEIGHT + paddingBottom;
32
+ const getPaddingBottom = (insets: EdgeInsets) => {
33
+ return Math.max(insets.bottom - Platform.select({ios: 4, default: 0}), 0);
120
34
  };
121
35
 
122
36
  export default function BottomTabBar({
@@ -125,129 +39,30 @@ export default function BottomTabBar({
125
39
  descriptors,
126
40
  activeBackgroundColor,
127
41
  activeTintColor,
128
- adaptive,
129
42
  allowFontScaling,
130
43
  inactiveBackgroundColor,
131
44
  inactiveTintColor,
132
- keyboardHidesTabBar = false,
133
- labelPosition,
134
45
  labelStyle,
135
46
  iconStyle,
136
- safeAreaInsets,
137
47
  showLabel,
138
48
  style,
139
49
  tabStyle,
140
50
  floatingButton,
141
51
  }: Props) {
52
+ const {theme} = useContext(ApplicationContext);
142
53
  const {routes} = state;
143
- const dimensions = useWindowDimensions();
144
- const isFloating = floatingButton?.icon && state.routes.length == 4;
145
- const itemWidth = isFloating
146
- ? dimensions.width / 5
147
- : dimensions.width / state.routes.length;
148
-
149
- const [layout, setLayout] = React.useState({
150
- height: 0,
151
- width: dimensions.width,
152
- });
54
+ const widthDevice = useWindowDimensions()?.width;
55
+ const useFloating = floatingButton && state.routes.length % 2 === 0;
56
+ const itemWidth = widthDevice / (state.routes.length + (useFloating ? 1 : 0));
153
57
  const buildLink = useLinkBuilder();
154
- const {colors} = useTheme();
155
-
156
- const hasHorizontalLabels = shouldUseHorizontalLabels({
157
- state,
158
- dimensions,
159
- layout,
160
- adaptive,
161
- labelPosition,
162
- tabStyle,
163
- });
164
- const focusedRoute = state.routes[state.index] as any;
165
- const focusedDescriptor = descriptors[focusedRoute?.key] as any;
166
- const focusedOptions = focusedDescriptor?.options;
167
- const shouldShowTabBar =
168
- focusedOptions?.tabBarVisible !== false && !keyboardHidesTabBar;
169
- const [visible] = React.useState(
170
- () => new Animated.Value(shouldShowTabBar ? 1 : 0),
171
- );
172
-
173
58
  const indicatorAnimated = React.useRef(new Animated.Value(0)).current;
174
-
175
- const visibilityAnimationConfigRef = React.useRef(
176
- focusedOptions?.tabBarVisibilityAnimationConfig,
177
- );
178
- const onHeightChange = React.useContext(BottomTabBarHeightCallbackContext);
179
- const [isTabBarHidden, setIsTabBarHidden] = React.useState(!shouldShowTabBar);
180
- React.useEffect(() => {
181
- visibilityAnimationConfigRef.current =
182
- focusedOptions?.tabBarVisibilityAnimationConfig;
183
- });
184
-
185
- const handleLayout = (e: LayoutChangeEvent) => {
186
- const {height, width} = e.nativeEvent.layout;
187
-
188
- onHeightChange?.(height);
189
-
190
- setLayout(layout => {
191
- if (height === layout.height && width === layout.width) {
192
- return layout;
193
- } else {
194
- return {
195
- height,
196
- width,
197
- };
198
- }
199
- });
200
- };
201
-
202
- React.useEffect(() => {
203
- const visibilityAnimationConfig = visibilityAnimationConfigRef.current;
204
-
205
- if (shouldShowTabBar) {
206
- const animation =
207
- visibilityAnimationConfig?.show?.animation === 'spring'
208
- ? Animated.spring
209
- : Animated.timing;
210
-
211
- animation(visible, {
212
- toValue: 1,
213
- useNativeDriver: true,
214
- duration: 250,
215
- ...visibilityAnimationConfig?.show?.config,
216
- }).start(({finished}) => {
217
- if (finished) {
218
- setIsTabBarHidden(false);
219
- }
220
- });
221
- } else {
222
- setIsTabBarHidden(true);
223
-
224
- const animation =
225
- visibilityAnimationConfig?.hide?.animation === 'spring'
226
- ? Animated.spring
227
- : Animated.timing;
228
-
229
- animation(visible, {
230
- toValue: 0,
231
- useNativeDriver: true,
232
- duration: 200,
233
- ...visibilityAnimationConfig?.hide?.config,
234
- }).start();
235
- }
236
- }, [visible, shouldShowTabBar]);
237
- const defaultInsets = useSafeAreaInsets();
238
-
239
- const insets = {
240
- top: safeAreaInsets?.top ?? defaultInsets.top,
241
- right: safeAreaInsets?.right ?? defaultInsets.right,
242
- bottom: safeAreaInsets?.bottom ?? defaultInsets.bottom,
243
- left: safeAreaInsets?.left ?? defaultInsets.left,
244
- };
59
+ const insets = useSafeAreaInsets();
245
60
  const paddingBottom = getPaddingBottom(insets);
246
-
247
- const leftArr = routes.slice(0, 2);
248
- const rightArr = routes.slice(2, routes.length);
249
61
  const focusedTab = state?.routes[state.index]?.name || routes[0]?.params;
250
62
 
63
+ /**
64
+ * render tab item
65
+ */
251
66
  const renderTabItem = useCallback(
252
67
  (data: any[]) => {
253
68
  return data?.map((route, index) => {
@@ -297,7 +112,7 @@ export default function BottomTabBar({
297
112
  <BottomTabBarItem
298
113
  route={route}
299
114
  focused={focused}
300
- horizontal={hasHorizontalLabels}
115
+ horizontal={false}
301
116
  onPress={onPress}
302
117
  onLongPress={onLongPress}
303
118
  accessibilityLabel={accessibilityLabel}
@@ -326,22 +141,17 @@ export default function BottomTabBar({
326
141
  [JSON.stringify(routes), state, descriptors, focusedTab],
327
142
  );
328
143
 
329
- const renderCenterComponent = useCallback(() => {
144
+ /**
145
+ * render floating button
146
+ */
147
+ const renderFloatingButton = useCallback(() => {
330
148
  const renderFloatingButton = () => {
331
149
  if (typeof floatingButton?.icon == 'string') {
332
150
  return <Icon source={floatingButton?.icon} size={32} color={'white'} />;
333
- } else if (floatingButton?.icon) {
334
- return floatingButton?.icon;
335
- } else {
336
- return null;
337
151
  }
152
+ return floatingButton?.icon;
338
153
  };
339
- if (floatingButton?.icon && state.routes.length !== 4) {
340
- console.warn(
341
- "Can't render floating button with icon when routes length is not 4",
342
- );
343
- return null;
344
- }
154
+
345
155
  return (
346
156
  <View style={{flex: 1}}>
347
157
  <TouchableOpacity
@@ -350,14 +160,14 @@ export default function BottomTabBar({
350
160
  <View
351
161
  style={{
352
162
  ...styles.floatingContent,
353
- backgroundColor: activeTintColor ?? colors.primary,
163
+ backgroundColor: activeTintColor ?? theme.colors.primary,
354
164
  }}>
355
165
  {renderFloatingButton()}
356
166
  </View>
357
167
  <View
358
168
  style={{
359
169
  top: -6,
360
- backgroundColor: activeTintColor ?? colors.primary,
170
+ backgroundColor: activeTintColor ?? theme.colors.primary,
361
171
  borderRadius: Radius.XL,
362
172
  paddingHorizontal: 6,
363
173
  }}>
@@ -371,53 +181,59 @@ export default function BottomTabBar({
371
181
  </TouchableOpacity>
372
182
  </View>
373
183
  );
374
- }, [floatingButton]);
184
+ }, [useFloating]);
375
185
 
186
+ /**
187
+ * animated indicator
188
+ */
376
189
  React.useEffect(() => {
190
+ let index = state.index;
191
+ if (useFloating && index > (state.routes.length - 1) / 2) {
192
+ index += 1;
193
+ }
377
194
  Animated.timing(indicatorAnimated, {
378
- toValue:
379
- isFloating && state.index > 1
380
- ? itemWidth * (state.index + 1)
381
- : itemWidth * state.index,
382
- useNativeDriver: false,
195
+ toValue: itemWidth * index,
196
+ useNativeDriver: true,
383
197
  duration: 200,
384
198
  }).start();
385
- }, [state.index, itemWidth, indicatorAnimated, isFloating]);
199
+ }, [state.index, itemWidth, indicatorAnimated, useFloating]);
200
+
201
+ /**
202
+ * render tab bar
203
+ */
204
+ const renderTabBar = () => {
205
+ if (useFloating) {
206
+ const middleIndex = routes.length / 2;
207
+ const leftArr = routes.slice(0, middleIndex);
208
+ const rightArr = routes.slice(middleIndex);
209
+ return (
210
+ <>
211
+ {renderTabItem(leftArr)}
212
+ {renderFloatingButton()}
213
+ {renderTabItem(rightArr)}
214
+ </>
215
+ );
216
+ }
217
+ return renderTabItem(routes);
218
+ };
386
219
 
387
220
  return (
388
221
  <Animated.View
389
222
  style={[
390
223
  styles.tabBar,
391
224
  {
392
- backgroundColor: colors.card,
393
- borderTopColor: colors.border,
225
+ backgroundColor: theme.colors.background.surface,
226
+ borderTopColor: theme.colors.border.default,
394
227
  },
395
228
  {
396
- transform: [
397
- {
398
- translateY: visible.interpolate({
399
- inputRange: [0, 1],
400
- outputRange: [
401
- layout.height + paddingBottom + StyleSheet.hairlineWidth,
402
- 0,
403
- ],
404
- }),
405
- },
406
- ],
407
- // Absolutely position the tab bar so that the content is below it
408
- // This is needed to avoid gap at bottom when the tab bar is hidden
409
- position: isTabBarHidden ? 'absolute' : (null as any),
410
- },
411
- {
412
- paddingBottom: paddingBottom - 0,
229
+ paddingBottom: paddingBottom,
413
230
  paddingHorizontal: Math.max(insets.left, insets.right),
414
231
  },
415
232
  style,
416
233
  ]}
417
- pointerEvents={isTabBarHidden ? 'none' : 'auto'}
418
- onLayout={handleLayout}>
234
+ pointerEvents={'auto'}>
419
235
  <View style={styles.content}>
420
- {isFloating && floatingButton?.container}
236
+ {useFloating && floatingButton?.container}
421
237
  <Animated.View
422
238
  style={{
423
239
  width: itemWidth,
@@ -428,13 +244,11 @@ export default function BottomTabBar({
428
244
  <View
429
245
  style={[
430
246
  styles.indicator,
431
- {backgroundColor: activeTintColor ?? colors.primary},
247
+ {backgroundColor: activeTintColor ?? theme.colors.primary},
432
248
  ]}
433
249
  />
434
250
  </Animated.View>
435
- {isFloating ? renderTabItem(leftArr) : renderTabItem(routes)}
436
- {isFloating && renderCenterComponent()}
437
- {isFloating && renderTabItem(rightArr)}
251
+ {renderTabBar()}
438
252
  </View>
439
253
  </Animated.View>
440
254
  );
@@ -464,9 +278,8 @@ const styles = StyleSheet.create({
464
278
  justifyContent: 'center',
465
279
  height: 48,
466
280
  width: 48,
467
-
468
- borderRadius: 100,
469
- top: -12,
281
+ borderRadius: 24,
282
+ top: -10,
470
283
  },
471
284
  indicator: {
472
285
  height: 2,