@momo-kits/foundation 0.92.6-beta.5 → 0.92.6-rc.10
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/ModalScreen.tsx +1 -1
- package/Application/Navigation.ts +2 -2
- package/Application/StackScreen.tsx +1 -1
- package/Image/index.tsx +38 -34
- package/Input/Input.tsx +2 -1
- package/Layout/FloatingButton.tsx +128 -0
- package/Layout/Screen.tsx +26 -75
- package/Layout/utils.ts +1 -16
- package/package.json +2 -4
- package/FloatingButton/FloatingButton.tsx +0 -154
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import {Modalize} from 'react-native-modalize';
|
|
11
11
|
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
|
12
12
|
|
|
13
|
-
import {ApplicationContext} from '
|
|
13
|
+
import {ApplicationContext} from './index';
|
|
14
14
|
import {Radius, Spacing, Styles} from '../Consts';
|
|
15
15
|
import {Icon} from '../Icon';
|
|
16
16
|
import {Text} from '../Text';
|
|
@@ -6,8 +6,8 @@ import {getOptions} from './utils';
|
|
|
6
6
|
import {HeaderToolkitAction} from './Components';
|
|
7
7
|
|
|
8
8
|
class Navigation {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
instance: NavigationProp<any>;
|
|
10
|
+
readonly theme: Theme;
|
|
11
11
|
|
|
12
12
|
constructor(instance: any, theme: Theme) {
|
|
13
13
|
this.instance = instance;
|
package/Image/index.tsx
CHANGED
|
@@ -2,7 +2,11 @@ import React, {useContext, useEffect, useRef, useState} from 'react';
|
|
|
2
2
|
import {StyleSheet, View} from 'react-native';
|
|
3
3
|
import FastImage, {Source} from 'react-native-fast-image';
|
|
4
4
|
import styles from './styles';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ApplicationContext,
|
|
7
|
+
ComponentContext,
|
|
8
|
+
MiniAppContext,
|
|
9
|
+
} from '../Application';
|
|
6
10
|
import {Skeleton} from '../Skeleton';
|
|
7
11
|
import {Icon} from '../Icon';
|
|
8
12
|
import {Styles} from '../Consts';
|
|
@@ -30,9 +34,9 @@ const Image: React.FC<ImageProps> = ({
|
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
useEffect(() => {
|
|
33
|
-
if(app.enableAutoId){
|
|
37
|
+
if (app.enableAutoId) {
|
|
34
38
|
component?.addChildren?.(test.accessibilityLabel, 'add');
|
|
35
|
-
return () => component?.addChildren(test.accessibilityLabel, 'remove');
|
|
39
|
+
return () => component?.addChildren?.(test.accessibilityLabel, 'remove');
|
|
36
40
|
}
|
|
37
41
|
}, []);
|
|
38
42
|
|
|
@@ -57,37 +61,37 @@ const Image: React.FC<ImageProps> = ({
|
|
|
57
61
|
};
|
|
58
62
|
|
|
59
63
|
return (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
64
|
+
<FastImage
|
|
65
|
+
{...rest}
|
|
66
|
+
{...test}
|
|
67
|
+
source={source}
|
|
68
|
+
style={[styles.container, style]}
|
|
69
|
+
onProgress={() => {
|
|
70
|
+
error.current = false;
|
|
71
|
+
if (status !== 'loading' && loading) {
|
|
72
|
+
setStatus('loading');
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
onLoadStart={() => {
|
|
76
|
+
error.current = false;
|
|
77
|
+
}}
|
|
78
|
+
onLoad={() => {
|
|
79
|
+
error.current = false;
|
|
80
|
+
}}
|
|
81
|
+
onLoadEnd={() => {
|
|
82
|
+
let current: Status = 'success';
|
|
83
|
+
if (error.current) {
|
|
84
|
+
current = 'error';
|
|
85
|
+
}
|
|
86
|
+
if (status !== current) {
|
|
87
|
+
setStatus(current);
|
|
88
|
+
}
|
|
89
|
+
}}
|
|
90
|
+
onError={() => {
|
|
91
|
+
error.current = true;
|
|
92
|
+
}}>
|
|
93
|
+
{renderContent()}
|
|
94
|
+
</FastImage>
|
|
91
95
|
);
|
|
92
96
|
};
|
|
93
97
|
|
package/Input/Input.tsx
CHANGED
|
@@ -50,6 +50,7 @@ const Input = forwardRef(
|
|
|
50
50
|
params,
|
|
51
51
|
hintText,
|
|
52
52
|
accessibilityLabel,
|
|
53
|
+
editable = true,
|
|
53
54
|
...props
|
|
54
55
|
}: InputProps,
|
|
55
56
|
ref,
|
|
@@ -168,7 +169,7 @@ const Input = forwardRef(
|
|
|
168
169
|
<TextInput
|
|
169
170
|
{...props}
|
|
170
171
|
accessibilityLabel={accessibilityLabel}
|
|
171
|
-
editable={
|
|
172
|
+
editable={editable && !disabled}
|
|
172
173
|
secureTextEntry={secure}
|
|
173
174
|
textAlignVertical="center"
|
|
174
175
|
ref={inputRef}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {default as React, useContext, useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
LayoutChangeEvent,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import {ApplicationContext} from '../Application';
|
|
9
|
+
import {Icon} from '../Icon';
|
|
10
|
+
import {Text} from '../Text';
|
|
11
|
+
|
|
12
|
+
export interface FloatingButtonProps {
|
|
13
|
+
label?: string;
|
|
14
|
+
onPress?: () => void;
|
|
15
|
+
position?: 'center' | 'right';
|
|
16
|
+
icon?: string;
|
|
17
|
+
iconColor?: string;
|
|
18
|
+
size?: 'small' | 'large';
|
|
19
|
+
animatedValue?: Animated.Value;
|
|
20
|
+
bottom?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
24
|
+
label = '',
|
|
25
|
+
onPress = () => {},
|
|
26
|
+
position = 'right',
|
|
27
|
+
icon = '',
|
|
28
|
+
iconColor = '',
|
|
29
|
+
size = 'small',
|
|
30
|
+
animatedValue,
|
|
31
|
+
bottom = 12,
|
|
32
|
+
}: FloatingButtonProps) => {
|
|
33
|
+
const {theme} = useContext(ApplicationContext);
|
|
34
|
+
const maxWidth = useRef(0);
|
|
35
|
+
const minWidth = size == 'small' ? 36 : 48;
|
|
36
|
+
const [widthAnimated, setWidthAnimated] = useState<Animated.Value>();
|
|
37
|
+
const lastOffset = useRef(0);
|
|
38
|
+
const debounce = useRef<any>();
|
|
39
|
+
const [showText, setShowText] = React.useState(true);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!label) return;
|
|
43
|
+
|
|
44
|
+
const listener = animatedValue?.addListener(({value}) => {
|
|
45
|
+
clearTimeout(debounce.current);
|
|
46
|
+
debounce.current = setTimeout(() => {
|
|
47
|
+
const down = value > lastOffset.current;
|
|
48
|
+
lastOffset.current = value;
|
|
49
|
+
if (down) {
|
|
50
|
+
Animated.timing(widthAnimated!, {
|
|
51
|
+
toValue: minWidth,
|
|
52
|
+
duration: 300,
|
|
53
|
+
useNativeDriver: false,
|
|
54
|
+
}).start(() => setShowText(false));
|
|
55
|
+
} else {
|
|
56
|
+
Animated.timing(widthAnimated!, {
|
|
57
|
+
toValue: maxWidth.current,
|
|
58
|
+
duration: 300,
|
|
59
|
+
useNativeDriver: false,
|
|
60
|
+
}).start(() => setShowText(true));
|
|
61
|
+
}
|
|
62
|
+
}, 500);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
if (listener != null) {
|
|
67
|
+
animatedValue?.removeListener(listener);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}, [label, widthAnimated]);
|
|
71
|
+
|
|
72
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
73
|
+
const layout = event.nativeEvent.layout;
|
|
74
|
+
if (widthAnimated) return;
|
|
75
|
+
maxWidth.current = layout.width;
|
|
76
|
+
setWidthAnimated(new Animated.Value(layout.width));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Animated.View
|
|
81
|
+
onLayout={handleLayout}
|
|
82
|
+
style={[
|
|
83
|
+
styles.container,
|
|
84
|
+
{
|
|
85
|
+
right: position === 'right' ? 12 : undefined,
|
|
86
|
+
alignSelf: position === 'center' ? 'center' : 'flex-end',
|
|
87
|
+
width: widthAnimated,
|
|
88
|
+
height: size == 'small' ? 36 : 48,
|
|
89
|
+
backgroundColor: theme.colors.primary,
|
|
90
|
+
bottom,
|
|
91
|
+
},
|
|
92
|
+
]}>
|
|
93
|
+
<TouchableOpacity
|
|
94
|
+
style={{flexDirection: 'row', alignItems: 'center'}}
|
|
95
|
+
onPress={onPress}>
|
|
96
|
+
<Icon
|
|
97
|
+
source={icon}
|
|
98
|
+
size={size === 'small' ? 12 : 24}
|
|
99
|
+
color={iconColor}
|
|
100
|
+
/>
|
|
101
|
+
{showText && !!label && (
|
|
102
|
+
<Text
|
|
103
|
+
style={styles.label}
|
|
104
|
+
color="white"
|
|
105
|
+
typography="action_default_bold">
|
|
106
|
+
{label}
|
|
107
|
+
</Text>
|
|
108
|
+
)}
|
|
109
|
+
</TouchableOpacity>
|
|
110
|
+
</Animated.View>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const styles = StyleSheet.create({
|
|
115
|
+
container: {
|
|
116
|
+
flexDirection: 'row',
|
|
117
|
+
alignContent: 'center',
|
|
118
|
+
alignItems: 'center',
|
|
119
|
+
position: 'absolute',
|
|
120
|
+
borderRadius: 100,
|
|
121
|
+
paddingHorizontal: 12,
|
|
122
|
+
flexWrap: 'wrap',
|
|
123
|
+
alignSelf: 'flex-end',
|
|
124
|
+
},
|
|
125
|
+
label: {
|
|
126
|
+
marginLeft: 8,
|
|
127
|
+
},
|
|
128
|
+
});
|
package/Layout/Screen.tsx
CHANGED
|
@@ -3,16 +3,13 @@ import React, {
|
|
|
3
3
|
forwardRef,
|
|
4
4
|
Fragment,
|
|
5
5
|
ReactNode,
|
|
6
|
-
useCallback,
|
|
7
6
|
useContext,
|
|
8
7
|
useEffect,
|
|
9
8
|
useRef,
|
|
10
9
|
} from 'react';
|
|
11
10
|
import {
|
|
12
11
|
Animated,
|
|
13
|
-
Easing,
|
|
14
12
|
KeyboardAvoidingView,
|
|
15
|
-
LayoutChangeEvent,
|
|
16
13
|
NativeScrollEvent,
|
|
17
14
|
NativeSyntheticEvent,
|
|
18
15
|
Platform,
|
|
@@ -29,15 +26,11 @@ import {
|
|
|
29
26
|
import Navigation from '../Application/Navigation';
|
|
30
27
|
import {AnimatedHeader, NavigationOptions} from '../Application/types';
|
|
31
28
|
import {Colors, Spacing, Styles} from '../Consts';
|
|
32
|
-
import {
|
|
33
|
-
FloatingButton,
|
|
34
|
-
FloatingButtonProps,
|
|
35
|
-
} from '../FloatingButton/FloatingButton';
|
|
29
|
+
import {FloatingButton, FloatingButtonProps} from './FloatingButton';
|
|
36
30
|
import {Image} from '../Image';
|
|
37
31
|
import {Card, Section, validateChildren} from './index';
|
|
38
32
|
import styles from './styles';
|
|
39
33
|
import {HeaderType} from './types';
|
|
40
|
-
import {debounce} from './utils';
|
|
41
34
|
|
|
42
35
|
export interface ScreenProps extends ViewProps {
|
|
43
36
|
children: React.ReactNode;
|
|
@@ -94,7 +87,12 @@ export interface ScreenProps extends ViewProps {
|
|
|
94
87
|
/**
|
|
95
88
|
* Optional. Props for the floating action button.
|
|
96
89
|
*/
|
|
97
|
-
floatingButtonProps?: Omit<FloatingButtonProps, 'animatedValue'>;
|
|
90
|
+
floatingButtonProps?: Omit<FloatingButtonProps, 'animatedValue' | 'bottom'>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Optional. If `true`, the screen content is using grid layout by span.
|
|
94
|
+
*/
|
|
95
|
+
useGridLayout?: boolean;
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
const Screen = forwardRef(
|
|
@@ -112,24 +110,22 @@ const Screen = forwardRef(
|
|
|
112
110
|
backgroundColor,
|
|
113
111
|
headerType = 'default',
|
|
114
112
|
floatingButtonProps,
|
|
113
|
+
useGridLayout = true,
|
|
115
114
|
}: ScreenProps,
|
|
116
115
|
ref,
|
|
117
116
|
) => {
|
|
118
|
-
const animatedValue = useRef<Animated.Value
|
|
119
|
-
const lastScrollPosition = useRef(0);
|
|
120
|
-
const animatedFABValue = useRef(new Animated.Value(0)).current;
|
|
117
|
+
const animatedValue = useRef<Animated.Value>(new Animated.Value(0));
|
|
121
118
|
const {theme} = useContext(ApplicationContext);
|
|
122
119
|
const insets = useSafeAreaInsets();
|
|
123
120
|
const heightHeader = useHeaderHeight();
|
|
124
|
-
const layoutHeight = useRef(0);
|
|
125
|
-
const contentHeight = useRef(0);
|
|
126
121
|
const currentTint = useRef(Colors.black_01);
|
|
122
|
+
const isTab = navigation?.instance?.getState?.()?.type == 'tab';
|
|
123
|
+
|
|
127
124
|
let styleAnimatedHeader: NavigationOptions;
|
|
128
125
|
let handleScroll;
|
|
129
126
|
let headerBackground: string | undefined = undefined;
|
|
130
127
|
let Component: any = View;
|
|
131
128
|
let keyboardVerticalOffset = heightHeader - 20;
|
|
132
|
-
animatedValue.current = undefined;
|
|
133
129
|
|
|
134
130
|
if (headerType == 'extended' || animatedHeader) {
|
|
135
131
|
keyboardVerticalOffset = -20;
|
|
@@ -194,12 +190,9 @@ const Screen = forwardRef(
|
|
|
194
190
|
image={null}
|
|
195
191
|
backgroundColor={theme.colors.background.surface}
|
|
196
192
|
useBorder={true}
|
|
197
|
-
animatedValue={animatedValue.current}
|
|
198
193
|
/>
|
|
199
194
|
),
|
|
200
|
-
headerTitle: (props: any) =>
|
|
201
|
-
<HeaderTitle {...props} animatedValue={animatedValue.current} />
|
|
202
|
-
),
|
|
195
|
+
headerTitle: (props: any) => <HeaderTitle {...props} />,
|
|
203
196
|
};
|
|
204
197
|
break;
|
|
205
198
|
|
|
@@ -232,8 +225,6 @@ const Screen = forwardRef(
|
|
|
232
225
|
Component = Animated.ScrollView;
|
|
233
226
|
handleScroll = scrollViewProps?.onScroll;
|
|
234
227
|
if (animatedHeader || floatingButtonProps) {
|
|
235
|
-
animatedValue.current = new Animated.Value(0);
|
|
236
|
-
|
|
237
228
|
handleScroll = Animated.event(
|
|
238
229
|
[
|
|
239
230
|
{
|
|
@@ -258,46 +249,12 @@ const Screen = forwardRef(
|
|
|
258
249
|
});
|
|
259
250
|
}
|
|
260
251
|
}
|
|
261
|
-
if (floatingButtonProps) {
|
|
262
|
-
const currentScrollPosition = e.nativeEvent.contentOffset.y;
|
|
263
|
-
const isScrollingDown =
|
|
264
|
-
currentScrollPosition > lastScrollPosition.current;
|
|
265
|
-
scrollViewProps?.onScroll?.(e);
|
|
266
|
-
|
|
267
|
-
if (currentScrollPosition < 0) {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
const isAtBottom =
|
|
271
|
-
currentScrollPosition +
|
|
272
|
-
layoutHeight.current +
|
|
273
|
-
Math.min(insets.bottom, 21) +
|
|
274
|
-
Spacing.S >=
|
|
275
|
-
contentHeight.current;
|
|
276
|
-
if (isAtBottom) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
animateFAB(isScrollingDown);
|
|
280
|
-
|
|
281
|
-
lastScrollPosition.current = currentScrollPosition;
|
|
282
|
-
}
|
|
283
252
|
},
|
|
284
253
|
},
|
|
285
254
|
);
|
|
286
255
|
}
|
|
287
256
|
}
|
|
288
257
|
|
|
289
|
-
const animateFAB = useCallback(
|
|
290
|
-
debounce((isScrollingDown: boolean) => {
|
|
291
|
-
Animated.timing(animatedFABValue, {
|
|
292
|
-
toValue: isScrollingDown ? 1 : 0,
|
|
293
|
-
duration: 100,
|
|
294
|
-
useNativeDriver: false,
|
|
295
|
-
easing: Easing.linear,
|
|
296
|
-
}).start();
|
|
297
|
-
}, 300),
|
|
298
|
-
[],
|
|
299
|
-
);
|
|
300
|
-
|
|
301
258
|
/**
|
|
302
259
|
* render top navigation banner
|
|
303
260
|
*/
|
|
@@ -351,6 +308,7 @@ const Screen = forwardRef(
|
|
|
351
308
|
);
|
|
352
309
|
}
|
|
353
310
|
};
|
|
311
|
+
|
|
354
312
|
return (
|
|
355
313
|
<View style={[Styles.flex, {backgroundColor}]}>
|
|
356
314
|
{!!headerBackground && (
|
|
@@ -373,39 +331,32 @@ const Screen = forwardRef(
|
|
|
373
331
|
android: undefined,
|
|
374
332
|
})}>
|
|
375
333
|
{Header}
|
|
334
|
+
|
|
376
335
|
<Component
|
|
377
336
|
{...scrollViewProps}
|
|
378
337
|
ref={ref}
|
|
379
338
|
showsVerticalScrollIndicator={false}
|
|
380
339
|
onScroll={handleScroll}
|
|
381
|
-
onContentSizeChange={(_: number, height: number) => {
|
|
382
|
-
contentHeight.current = height;
|
|
383
|
-
}}
|
|
384
|
-
onLayout={(event: LayoutChangeEvent) => {
|
|
385
|
-
layoutHeight.current = event.nativeEvent.layout.height;
|
|
386
|
-
}}
|
|
387
340
|
style={Styles.flex}>
|
|
388
341
|
<View
|
|
389
342
|
style={[styles.screenBanner, {maxHeight: 210 + layoutOffset}]}>
|
|
390
343
|
{renderHeader()}
|
|
391
344
|
</View>
|
|
392
|
-
{renderContent(children)}
|
|
345
|
+
{useGridLayout ? renderContent(children) : children}
|
|
393
346
|
</Component>
|
|
394
|
-
|
|
395
|
-
|
|
347
|
+
|
|
348
|
+
{floatingButtonProps && (
|
|
349
|
+
<View>
|
|
396
350
|
<FloatingButton
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
iconColor={floatingButtonProps?.iconColor}
|
|
403
|
-
isBottomTab={floatingButtonProps?.isBottomTab}
|
|
404
|
-
animatedValue={animatedFABValue}
|
|
405
|
-
isFooter={!!Footer}
|
|
351
|
+
{...floatingButtonProps}
|
|
352
|
+
animatedValue={animatedValue.current}
|
|
353
|
+
bottom={
|
|
354
|
+
Footer || isTab ? 12 : Math.min(insets.bottom, 21) + Spacing.S
|
|
355
|
+
}
|
|
406
356
|
/>
|
|
407
|
-
|
|
408
|
-
|
|
357
|
+
</View>
|
|
358
|
+
)}
|
|
359
|
+
|
|
409
360
|
{Footer && (
|
|
410
361
|
<View
|
|
411
362
|
style={[
|
package/Layout/utils.ts
CHANGED
|
@@ -51,20 +51,5 @@ const useGridSystem = () => {
|
|
|
51
51
|
|
|
52
52
|
return {numberOfColumns};
|
|
53
53
|
};
|
|
54
|
-
function debounce<T extends (...args: any[]) => void>(
|
|
55
|
-
func: T,
|
|
56
|
-
wait: number,
|
|
57
|
-
): (...args: Parameters<T>) => void {
|
|
58
|
-
let timeout: NodeJS.Timeout | null = null;
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
if (timeout !== null) {
|
|
62
|
-
clearTimeout(timeout);
|
|
63
|
-
}
|
|
64
|
-
timeout = setTimeout(() => {
|
|
65
|
-
func(...args);
|
|
66
|
-
}, wait);
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export {debounce, useGridSystem, validateChildren};
|
|
55
|
+
export {useGridSystem, validateChildren};
|
package/package.json
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momo-kits/foundation",
|
|
3
|
-
"version": "0.92.6-
|
|
3
|
+
"version": "0.92.6-rc.10",
|
|
4
4
|
"description": "React Native Component Kits",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"postinstall": "node verify.js"
|
|
8
|
-
},
|
|
6
|
+
"scripts": {},
|
|
9
7
|
"keywords": [
|
|
10
8
|
"@momo-kits/foundation"
|
|
11
9
|
],
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
default as React,
|
|
3
|
-
useCallback,
|
|
4
|
-
useContext,
|
|
5
|
-
useEffect,
|
|
6
|
-
useRef,
|
|
7
|
-
useState,
|
|
8
|
-
} from 'react';
|
|
9
|
-
import {
|
|
10
|
-
Animated,
|
|
11
|
-
LayoutChangeEvent,
|
|
12
|
-
StyleSheet,
|
|
13
|
-
TouchableOpacity,
|
|
14
|
-
} from 'react-native';
|
|
15
|
-
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
|
16
|
-
import {ApplicationContext} from '../Application';
|
|
17
|
-
import {Spacing} from '../Consts';
|
|
18
|
-
import {Icon} from '../Icon';
|
|
19
|
-
import {Text} from '../Text';
|
|
20
|
-
|
|
21
|
-
export interface FloatingButtonProps {
|
|
22
|
-
label?: string;
|
|
23
|
-
onPress?: () => void;
|
|
24
|
-
position?: 'center' | 'right';
|
|
25
|
-
icon?: string;
|
|
26
|
-
iconColor?: string;
|
|
27
|
-
size?: 'small' | 'large';
|
|
28
|
-
animatedValue: Animated.Value;
|
|
29
|
-
isBottomTab?: boolean;
|
|
30
|
-
isFooter?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const FloatingButton: React.FC<FloatingButtonProps> = ({
|
|
34
|
-
label = '',
|
|
35
|
-
onPress = () => {},
|
|
36
|
-
position = 'right',
|
|
37
|
-
icon = '',
|
|
38
|
-
iconColor = '',
|
|
39
|
-
size = 'small',
|
|
40
|
-
animatedValue,
|
|
41
|
-
isBottomTab = false,
|
|
42
|
-
isFooter = false,
|
|
43
|
-
}: FloatingButtonProps) => {
|
|
44
|
-
const {theme} = useContext(ApplicationContext);
|
|
45
|
-
const insets = useSafeAreaInsets();
|
|
46
|
-
const [isExpanded, setIsExpanded] = React.useState(true);
|
|
47
|
-
const [init, setInit] = useState(true);
|
|
48
|
-
const width = size == 'small' ? 36 : 48;
|
|
49
|
-
const widthAnim = useRef(new Animated.Value(width)).current;
|
|
50
|
-
const [textWidth, setTextWidth] = useState(0);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (!label) return;
|
|
54
|
-
|
|
55
|
-
const listener = animatedValue.addListener(({value}) => {
|
|
56
|
-
if (value === 1) {
|
|
57
|
-
setIsExpanded(false);
|
|
58
|
-
collapseFab();
|
|
59
|
-
} else {
|
|
60
|
-
setIsExpanded(true);
|
|
61
|
-
expandFab();
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return () => {
|
|
66
|
-
animatedValue?.removeListener(listener);
|
|
67
|
-
};
|
|
68
|
-
}, [textWidth, label, init]);
|
|
69
|
-
|
|
70
|
-
// If the text width is greater than 0 and it's not initialized then expand the fab
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (textWidth > 0 && !init) {
|
|
73
|
-
expandFab();
|
|
74
|
-
}
|
|
75
|
-
}, [textWidth]);
|
|
76
|
-
|
|
77
|
-
const handleLayout = (event: LayoutChangeEvent) => {
|
|
78
|
-
const {width} = event.nativeEvent.layout;
|
|
79
|
-
|
|
80
|
-
if (init) {
|
|
81
|
-
setTextWidth(width);
|
|
82
|
-
setInit(false);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const expandFab = useCallback(() => {
|
|
87
|
-
Animated.timing(widthAnim, {
|
|
88
|
-
toValue: width + textWidth + 8,
|
|
89
|
-
duration: 200,
|
|
90
|
-
useNativeDriver: false,
|
|
91
|
-
}).start();
|
|
92
|
-
}, [textWidth, widthAnim]);
|
|
93
|
-
|
|
94
|
-
const collapseFab = useCallback(() => {
|
|
95
|
-
Animated.timing(widthAnim, {
|
|
96
|
-
toValue: width,
|
|
97
|
-
duration: 200,
|
|
98
|
-
useNativeDriver: false,
|
|
99
|
-
}).start();
|
|
100
|
-
}, [widthAnim]);
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<Animated.View
|
|
104
|
-
style={[
|
|
105
|
-
styles.container,
|
|
106
|
-
{
|
|
107
|
-
right: position === 'right' ? 12 : undefined,
|
|
108
|
-
alignSelf: position === 'center' ? 'center' : 'flex-end',
|
|
109
|
-
width: init ? undefined : widthAnim,
|
|
110
|
-
height: width,
|
|
111
|
-
backgroundColor: theme.colors.primary,
|
|
112
|
-
bottom:
|
|
113
|
-
isFooter || isBottomTab
|
|
114
|
-
? 12
|
|
115
|
-
: Math.min(insets.bottom, 21) + Spacing.S,
|
|
116
|
-
},
|
|
117
|
-
]}>
|
|
118
|
-
<TouchableOpacity
|
|
119
|
-
style={{flexDirection: 'row', alignItems: 'center'}}
|
|
120
|
-
onPress={onPress}>
|
|
121
|
-
<Icon
|
|
122
|
-
source={icon}
|
|
123
|
-
size={size === 'small' ? 12 : 24}
|
|
124
|
-
color={iconColor}
|
|
125
|
-
/>
|
|
126
|
-
{isExpanded && !!label && (
|
|
127
|
-
<Text
|
|
128
|
-
onLayout={handleLayout}
|
|
129
|
-
style={styles.label}
|
|
130
|
-
color="white"
|
|
131
|
-
typography="action_default_bold">
|
|
132
|
-
Hello
|
|
133
|
-
</Text>
|
|
134
|
-
)}
|
|
135
|
-
</TouchableOpacity>
|
|
136
|
-
</Animated.View>
|
|
137
|
-
);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const styles = StyleSheet.create({
|
|
141
|
-
container: {
|
|
142
|
-
flexDirection: 'row',
|
|
143
|
-
alignContent: 'center',
|
|
144
|
-
alignItems: 'center',
|
|
145
|
-
position: 'absolute',
|
|
146
|
-
borderRadius: 100,
|
|
147
|
-
paddingHorizontal: 12,
|
|
148
|
-
flexWrap: 'wrap',
|
|
149
|
-
alignSelf: 'flex-end',
|
|
150
|
-
},
|
|
151
|
-
label: {
|
|
152
|
-
marginLeft: 8,
|
|
153
|
-
},
|
|
154
|
-
});
|