@momo-kits/foundation 0.92.20 → 0.92.21-beta.1

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,8 +1,9 @@
1
1
  import React, {
2
- Fragment,
2
+ FC,
3
3
  useCallback,
4
4
  useContext,
5
5
  useEffect,
6
+ useRef,
6
7
  useState,
7
8
  } from 'react';
8
9
  import {
@@ -83,11 +84,6 @@ const NavigationButton: React.FC<NavigationButtonProps> = ({
83
84
 
84
85
  const HeaderTitle: React.FC<any> = props => {
85
86
  const {theme} = useContext(ApplicationContext);
86
- const opacity = props.animatedValue?.interpolate({
87
- inputRange: [0, 200],
88
- outputRange: [0, 1],
89
- extrapolate: 'clamp',
90
- });
91
87
 
92
88
  return (
93
89
  <Animated.Text
@@ -95,7 +91,11 @@ const HeaderTitle: React.FC<any> = props => {
95
91
  accessibilityLabel={'title_navigation_header'}
96
92
  style={[
97
93
  styles.title,
98
- {fontFamily: `${theme.font}-Bold`, opacity, color: props.tintColor},
94
+ {
95
+ fontFamily: `${theme.font}-Bold`,
96
+ color: props.tintColor,
97
+ opacity: props.titleOpacity,
98
+ },
99
99
  ]}
100
100
  />
101
101
  );
@@ -113,7 +113,7 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
113
113
  useEffect(() => {
114
114
  const backHandler = BackHandler.addEventListener(
115
115
  'hardwareBackPress',
116
- goBackSafe,
116
+ goBackSafe
117
117
  );
118
118
 
119
119
  return () => backHandler.remove();
@@ -263,35 +263,40 @@ const HeaderCustom: React.FC<TitleCustomProps> = ({
263
263
  return <View style={styles.headerTitleContainer}>{content ?? header}</View>;
264
264
  };
265
265
 
266
- const HeaderRightAction: React.FC<any> = ({children, ...restProps}) => {
267
- const validateType = (child: React.ReactElement) => {
268
- const valid = child?.type === NavigationButton || child?.type === Fragment;
269
- if (__DEV__ && !valid) {
270
- console.warn(
271
- 'HeaderRightAction contains element type of NavigationButton, Please check again.',
272
- );
273
- }
274
- return child;
275
- };
266
+ const HeaderRight: FC<any> = ({type, ...props}) => {
267
+ let buttons = props?.buttons || [];
268
+ if (!type) {
269
+ return <View />;
270
+ }
276
271
 
277
- const renderAction = () => {
278
- if (Array.isArray(children)) {
279
- return children.map((child, index) => {
280
- return (
281
- <View key={`HeaderRightAction ${index}`} style={styles.headerButton}>
282
- {React.cloneElement(validateType(child), {...restProps})}
283
- </View>
284
- );
285
- });
272
+ if (type === 'iconButton') {
273
+ if (buttons?.length > 3) {
274
+ buttons = buttons.slice(0, 3);
286
275
  }
287
-
288
276
  return (
289
- <View style={styles.headerButton}>
290
- {React.cloneElement(validateType(children), {...restProps})}
277
+ <View style={styles.headerRightButton}>
278
+ {buttons?.map?.(
279
+ ({icon, onPress}: NavigationButtonProps, index: number) => {
280
+ return (
281
+ <View
282
+ key={`HeaderRightAction ${index}`}
283
+ style={{
284
+ marginLeft: index !== 0 ? Spacing.S : 0,
285
+ }}>
286
+ <NavigationButton
287
+ icon={icon}
288
+ onPress={onPress}
289
+ tintColor={props?.tintColor}
290
+ />
291
+ </View>
292
+ );
293
+ }
294
+ )}
291
295
  </View>
292
296
  );
293
- };
294
- return <View style={styles.headerRightButton}>{renderAction()}</View>;
297
+ }
298
+
299
+ return <HeaderToolkitAction {...props} />;
295
300
  };
296
301
 
297
302
  const HeaderToolkitAction: React.FC<any> = ({
@@ -327,7 +332,7 @@ const HeaderToolkitAction: React.FC<any> = ({
327
332
  (config: any) => {
328
333
  navigator.toolkitConfig = config;
329
334
  setToolConfig(navigator?.toolkitConfig);
330
- },
335
+ }
331
336
  );
332
337
  };
333
338
 
@@ -340,7 +345,7 @@ const HeaderToolkitAction: React.FC<any> = ({
340
345
  const {item} = res;
341
346
  navigator?.toolkitCallback?.(item);
342
347
  getToolkitConfig();
343
- },
348
+ }
344
349
  );
345
350
  };
346
351
 
@@ -357,7 +362,7 @@ const HeaderToolkitAction: React.FC<any> = ({
357
362
  navigator?.maxApi?.dispatchFunction?.(
358
363
  'dismiss',
359
364
  undefined,
360
- undefined,
365
+ undefined
361
366
  );
362
367
  },
363
368
  }}
@@ -388,7 +393,7 @@ const HeaderToolkitAction: React.FC<any> = ({
388
393
  [pinTool?.key],
389
394
  () => {
390
395
  getToolkitConfig();
391
- },
396
+ }
392
397
  );
393
398
  navigator?.toolkitCallback?.(pinTool.key);
394
399
  }}
@@ -405,6 +410,7 @@ const HeaderToolkitAction: React.FC<any> = ({
405
410
  {
406
411
  backgroundColor: backgroundColor ?? '#00000066',
407
412
  borderWidth: backgroundColor ? 0.5 : 0,
413
+ marginLeft: renderPinnedTool() ? Spacing.S : 0,
408
414
  },
409
415
  ]}>
410
416
  <TouchableOpacity
@@ -470,6 +476,42 @@ const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
470
476
  );
471
477
  };
472
478
 
479
+ const ExtendBackground: FC<{
480
+ headerBackground: string;
481
+ animatedValue: Animated.Value;
482
+ heightHeader: number;
483
+ }> = ({headerBackground, animatedValue, heightHeader}) => {
484
+ const heightAnimated = useRef(new Animated.Value(0));
485
+ useEffect(() => {
486
+ const listener = animatedValue.addListener(({value}) => {
487
+ heightAnimated.current.setValue(value);
488
+ });
489
+ return () => {
490
+ animatedValue?.removeListener(listener);
491
+ };
492
+ }, []);
493
+
494
+ return (
495
+ <>
496
+ <Image
497
+ source={{
498
+ uri: headerBackground,
499
+ }}
500
+ style={styles.extendedHeader}
501
+ />
502
+ <Animated.View
503
+ style={{
504
+ height: heightAnimated.current.interpolate({
505
+ inputRange: [0, 100],
506
+ outputRange: [heightHeader, heightHeader - 52],
507
+ extrapolate: 'clamp',
508
+ }),
509
+ }}
510
+ />
511
+ </>
512
+ );
513
+ };
514
+
473
515
  const styles = StyleSheet.create({
474
516
  navigationButton: {
475
517
  height: 28,
@@ -509,7 +551,6 @@ const styles = StyleSheet.create({
509
551
  paddingRight: Spacing.M,
510
552
  },
511
553
  toolkitContainer: {
512
- marginLeft: Spacing.S,
513
554
  padding: Spacing.XS,
514
555
  height: 28,
515
556
  borderRadius: 14,
@@ -541,6 +582,12 @@ const styles = StyleSheet.create({
541
582
  top: -Spacing.XS,
542
583
  right: -Spacing.XS,
543
584
  },
585
+ extendedHeader: {
586
+ aspectRatio: 1.75,
587
+ position: 'absolute',
588
+ width: '100%',
589
+ height: 210,
590
+ },
544
591
  });
545
592
 
546
593
  export {
@@ -548,8 +595,9 @@ export {
548
595
  HeaderTitle,
549
596
  HeaderLeft,
550
597
  HeaderBackground,
551
- HeaderRightAction,
552
598
  HeaderToolkitAction,
553
599
  HeaderCustom,
554
600
  HeaderAnimated,
601
+ HeaderRight,
602
+ ExtendBackground,
555
603
  };
@@ -1,9 +1,6 @@
1
- import React from 'react';
2
1
  import {NavigationProp} from '@react-navigation/native';
3
2
  import {NavigationOptions, Theme} from './types';
4
- import {HeaderRightAction} from './index';
5
3
  import {getOptions} from './utils';
6
- import {HeaderToolkitAction} from './Components';
7
4
 
8
5
  class Navigation {
9
6
  instance: NavigationProp<any>;
@@ -19,16 +16,6 @@ class Navigation {
19
16
  * @param params
20
17
  */
21
18
  private filterParams = (params: NavigationOptions) => {
22
- if (params.headerRight) {
23
- const headerRight = params.headerRight?.({}) as React.ReactElement;
24
- const type = headerRight?.type;
25
- if (type !== HeaderRightAction && type !== HeaderToolkitAction) {
26
- console.warn(
27
- 'headerRight contains element type of HeaderRightAction | HeaderToolkitAction, Please check again.',
28
- );
29
- }
30
- }
31
-
32
19
  if (params.headerLeft) {
33
20
  console.warn('headerLeft not allow override render by design system!');
34
21
  delete params.headerLeft;
@@ -144,19 +144,29 @@ export type RuntimeToolType = {
144
144
  key: string;
145
145
  };
146
146
 
147
- export interface NavigationOptions extends StackNavigationOptions {
147
+ export interface NavigationOptions
148
+ extends Omit<StackNavigationOptions, 'headerRight'> {
148
149
  preventBack?: PopupNotifyProps;
149
150
  onPressLeftHeader?: () => void;
150
151
  surface?: boolean;
151
152
  hiddenBack?: boolean;
152
153
  customTitle?: TitleCustomProps;
153
- toolkitParams?: {
154
- pinnedTool?: PinnedToolType;
155
- runtimeTools?: RuntimeToolType[];
156
- preventClose?: PopupNotifyProps;
157
- };
154
+ headerRight?: HeaderRightToolkit | HeaderRightActions | Element;
158
155
  }
159
156
 
157
+ export type HeaderRightToolkit = {
158
+ type?: 'toolkit';
159
+ pinnedTool?: PinnedToolType;
160
+ runtimeTools?: RuntimeToolType[];
161
+ preventClose?: PopupNotifyProps;
162
+ useMore?: boolean;
163
+ };
164
+
165
+ export type HeaderRightActions = {
166
+ type?: 'iconButton';
167
+ buttons: NavigationButtonProps[];
168
+ };
169
+
160
170
  export interface HeaderBackProps extends NavigationButtonProps {
161
171
  preventBack?: PopupNotifyProps;
162
172
  onPressLeftHeader?: () => void;
@@ -122,15 +122,6 @@ const getOptions = (params: NavigationOptions, theme: Theme) => {
122
122
  };
123
123
  }
124
124
 
125
- if (params.toolkitParams) {
126
- options = {
127
- ...options,
128
- headerRight: (props: any) => {
129
- return <HeaderToolkitAction {...params.toolkitParams} {...props} />;
130
- },
131
- };
132
- }
133
-
134
125
  return {
135
126
  ...params,
136
127
  ...options,
package/Button/index.tsx CHANGED
@@ -297,7 +297,6 @@ const Button: FC<ButtonProps> = ({
297
297
  };
298
298
 
299
299
  const activeOpacity = type === 'disabled' ? 0.75 : 0.5;
300
-
301
300
  return (
302
301
  <ComponentContext.Provider
303
302
  value={{
package/Icon/types.ts CHANGED
@@ -16,6 +16,7 @@ export type IconProps = {
16
16
 
17
17
  /**
18
18
  * Optional. Represents the color of the icon.
19
+ * It can be any valid color in @momo-kits/foundation.
19
20
  */
20
21
  color?: string | null;
21
22
 
@@ -61,6 +61,7 @@ const InputDropDown = ({
61
61
  styles.inputWrapper,
62
62
  {backgroundColor: theme.colors.background.surface},
63
63
  getSizeStyle(size),
64
+ // getBorderColor(theme, false, errorMessage, disabled),
64
65
  ]}>
65
66
  <FloatingView
66
67
  floatingValue={floatingValue}
@@ -44,7 +44,7 @@ const InputMoney = forwardRef(
44
44
  value: _value,
45
45
  ...props
46
46
  }: InputMoneyProps,
47
- ref,
47
+ ref
48
48
  ) => {
49
49
  const {theme} = useContext(ApplicationContext);
50
50
  const [focused, setFocused] = useState(false);
@@ -63,7 +63,7 @@ const InputMoney = forwardRef(
63
63
  };
64
64
 
65
65
  const [value, setValue] = useState(
66
- defaultValue ? validateText(defaultValue) : '',
66
+ defaultValue ? validateText(defaultValue) : ''
67
67
  );
68
68
 
69
69
  const onClearText = () => {
@@ -198,7 +198,7 @@ const InputMoney = forwardRef(
198
198
  </View>
199
199
  </ComponentContext.Provider>
200
200
  );
201
- },
201
+ }
202
202
  );
203
203
 
204
204
  export default InputMoney;
@@ -41,7 +41,7 @@ const OTPCaret: FC<CaretProps> = ({index, length}) => {
41
41
  duration: DURATION,
42
42
  useNativeDriver: true,
43
43
  }),
44
- ]),
44
+ ])
45
45
  ).start();
46
46
  }, []);
47
47
  const spacingStyle = !isNaN(Number(length)) &&
@@ -81,7 +81,7 @@ const InputOTP = forwardRef(
81
81
  hintText,
82
82
  ...props
83
83
  }: InputOTPProps,
84
- ref,
84
+ ref
85
85
  ) => {
86
86
  const MAX_LENGTH = 10;
87
87
  const [value, setValue] = useState('');
@@ -143,7 +143,7 @@ const InputOTP = forwardRef(
143
143
  {value[i] || '-'}
144
144
  </Text>
145
145
  )}
146
- </View>,
146
+ </View>
147
147
  );
148
148
  }
149
149
  return TextInputs;
@@ -248,7 +248,7 @@ const InputOTP = forwardRef(
248
248
  </TouchableOpacity>
249
249
  </ComponentContext.Provider>
250
250
  );
251
- },
251
+ }
252
252
  );
253
253
 
254
254
  export default InputOTP;
@@ -1,5 +1,6 @@
1
1
  import React, {
2
2
  forwardRef,
3
+ ForwardRefRenderFunction,
3
4
  useContext,
4
5
  useImperativeHandle,
5
6
  useRef,
@@ -15,177 +16,195 @@ import {
15
16
  import {ApplicationContext, ComponentContext} from '../Application';
16
17
  import {Icon} from '../Icon';
17
18
  import {Text} from '../Text';
18
- import {InputSearchProps} from './index';
19
+ import {InputSearchProps, InputSearchRef} from './index';
19
20
  import styles from './styles';
20
21
  import {checkTyping} from './utils';
22
+ import TextTyping from './TextTyping';
21
23
 
22
- const InputSearch = forwardRef(
23
- (
24
- {
25
- placeholder,
26
- onFocus,
27
- onBlur,
28
- iconColor,
29
- value,
30
- onChangeText,
31
- icon,
32
- buttonText = 'Hủy',
33
- showButtonText = true,
34
- style,
35
- defaultValue,
36
- onPressButtonText,
37
- params,
38
- hasColorBG = false,
39
- accessibilityLabel,
40
- ...props
41
- }: InputSearchProps,
42
- ref,
43
- ) => {
44
- const {theme} = useContext(ApplicationContext);
45
- const [focused, setFocused] = useState(false);
46
- const [haveValue, setHaveValue] = useState(!!value || !!defaultValue);
47
- const inputRef = useRef<TextInput | null>(null);
24
+ const InputSearch: ForwardRefRenderFunction<
25
+ InputSearchRef,
26
+ InputSearchProps
27
+ > = (
28
+ {
29
+ placeholder,
30
+ onFocus,
31
+ onBlur,
32
+ iconColor,
33
+ value,
34
+ onChangeText,
35
+ icon,
36
+ buttonText = 'Hủy',
37
+ showButtonText = true,
38
+ style,
39
+ defaultValue,
40
+ onPressButtonText,
41
+ params,
42
+ hasColorBG = false,
43
+ accessibilityLabel,
44
+ typingData = [],
45
+ onPress,
46
+ ...props
47
+ },
48
+ ref
49
+ ) => {
50
+ const {theme} = useContext(ApplicationContext);
51
+ const [focused, setFocused] = useState(false);
52
+ const [haveValue, setHaveValue] = useState(!!value || !!defaultValue);
53
+ const inputRef = useRef<TextInput | null>(null);
48
54
 
49
- const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
50
- setFocused(true);
51
- onFocus?.(e);
52
- };
55
+ const _onFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
56
+ setFocused(true);
57
+ onFocus?.(e);
58
+ };
53
59
 
54
- const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
55
- setFocused(false);
56
- onBlur?.(e);
57
- };
60
+ const _onBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
61
+ setFocused(false);
62
+ onBlur?.(e);
63
+ };
58
64
 
59
- const onClearText = () => {
60
- inputRef?.current?.clear();
61
- _onChangeText('');
62
- };
65
+ const onClearText = () => {
66
+ inputRef?.current?.clear();
67
+ _onChangeText('');
68
+ };
63
69
 
64
- const _onChangeText = (text: string) => {
65
- checkTyping(text, haveValue, setHaveValue);
66
- onChangeText?.(text);
67
- };
70
+ const _onChangeText = (text: string) => {
71
+ checkTyping(text, haveValue, setHaveValue);
72
+ onChangeText?.(text);
73
+ };
68
74
 
69
- useImperativeHandle(ref, () => {
70
- return {
71
- clear: onClearText,
72
- focus: () => inputRef.current?.focus(),
73
- blur: () => inputRef.current?.blur(),
74
- setText: (text: string) => _onChangeText(text),
75
- };
76
- });
75
+ useImperativeHandle(ref, () => {
76
+ return {
77
+ clear: onClearText,
78
+ focus: () => inputRef.current?.focus(),
79
+ blur: () => inputRef.current?.blur(),
80
+ setText: (text: string) => _onChangeText(text),
81
+ };
82
+ });
77
83
 
78
- const renderInputView = () => {
84
+ const renderInputView = () => {
85
+ if (typingData?.length > 0) {
79
86
  return (
80
- <TextInput
81
- {...props}
82
- accessibilityLabel={accessibilityLabel}
83
- textAlignVertical="center"
84
- ref={inputRef}
87
+ <TouchableOpacity
85
88
  style={[
86
89
  styles.searchInput,
87
90
  {
88
- color: theme.colors.text.default,
91
+ justifyContent: 'center',
89
92
  },
90
93
  ]}
91
- value={value}
92
- onChangeText={_onChangeText}
93
- onFocus={_onFocus}
94
- onBlur={_onBlur}
95
- placeholder={placeholder}
96
- selectionColor={theme.colors.primary}
97
- placeholderTextColor={theme.colors.text.hint}
98
- />
94
+ onPress={onPress}
95
+ activeOpacity={onPress ? 0.6 : 1}>
96
+ <TextTyping data={typingData} />
97
+ </TouchableOpacity>
99
98
  );
100
- };
101
-
102
- const renderIconView = () => {
103
- return (
104
- <View
105
- style={{
106
- flexDirection: 'row',
107
- }}>
108
- {focused && haveValue && (
109
- <TouchableOpacity style={styles.iconWrapper} onPress={onClearText}>
110
- <Icon
111
- source="24_navigation_close_circle_full"
112
- size={16}
113
- color={theme.colors.text.hint}
114
- />
115
- </TouchableOpacity>
116
- )}
117
- {!!icon && (
118
- <View style={{flexDirection: 'row'}}>
119
- <View
120
- style={[
121
- styles.divider,
122
- {
123
- backgroundColor: theme.colors.primary,
124
- },
125
- ]}
126
- />
127
- <Icon
128
- color={iconColor}
129
- source={icon}
130
- style={styles.iconSearchInput}
131
- />
132
- </View>
133
- )}
134
- </View>
135
- );
136
- };
137
-
138
- let inputState = 'empty';
139
-
140
- if (focused) {
141
- inputState = 'focus';
142
- }
143
-
144
- if (value && value?.length > 0) {
145
- inputState = 'filled';
146
99
  }
147
100
 
148
- const backgroundColor = hasColorBG
149
- ? theme.colors.background.default
150
- : theme.colors.background.surface;
101
+ return (
102
+ <TextInput
103
+ {...props}
104
+ accessibilityLabel={accessibilityLabel}
105
+ textAlignVertical="center"
106
+ ref={inputRef}
107
+ style={[
108
+ styles.searchInput,
109
+ {
110
+ color: theme.colors.text.default,
111
+ },
112
+ ]}
113
+ value={value}
114
+ onChangeText={_onChangeText}
115
+ onFocus={_onFocus}
116
+ onBlur={_onBlur}
117
+ placeholder={placeholder}
118
+ selectionColor={theme.colors.primary}
119
+ placeholderTextColor={theme.colors.text.hint}
120
+ />
121
+ );
122
+ };
151
123
 
124
+ const renderIconView = () => {
152
125
  return (
153
- <ComponentContext.Provider
154
- value={{
155
- componentName: 'InputSearch',
156
- params,
157
- state: inputState,
158
- componentId: accessibilityLabel,
126
+ <View
127
+ style={{
128
+ flexDirection: 'row',
159
129
  }}>
160
- <View style={[style, styles.searchInputContainer]}>
161
- <View
162
- style={[
163
- styles.searchInputWrapper,
164
- {
165
- backgroundColor,
166
- },
167
- ]}>
130
+ {focused && haveValue && (
131
+ <TouchableOpacity style={styles.iconWrapper} onPress={onClearText}>
168
132
  <Icon
169
- source={'navigation_search'}
170
- size={24}
133
+ source="24_navigation_close_circle_full"
134
+ size={16}
171
135
  color={theme.colors.text.hint}
172
136
  />
173
- {renderInputView()}
174
- {renderIconView()}
137
+ </TouchableOpacity>
138
+ )}
139
+ {!!icon && (
140
+ <View style={{flexDirection: 'row'}}>
141
+ <View
142
+ style={[
143
+ styles.divider,
144
+ {
145
+ backgroundColor: theme.colors.primary,
146
+ },
147
+ ]}
148
+ />
149
+ <Icon
150
+ color={iconColor}
151
+ source={icon}
152
+ style={styles.iconSearchInput}
153
+ />
175
154
  </View>
176
- {showButtonText && (
177
- <TouchableOpacity onPress={onPressButtonText}>
178
- <Text
179
- typography={'action_default_bold'}
180
- style={styles.textButton}>
181
- {buttonText}
182
- </Text>
183
- </TouchableOpacity>
184
- )}
185
- </View>
186
- </ComponentContext.Provider>
155
+ )}
156
+ </View>
187
157
  );
188
- },
189
- );
158
+ };
159
+
160
+ let inputState = 'empty';
161
+
162
+ if (focused) {
163
+ inputState = 'focus';
164
+ }
165
+
166
+ if (value && value?.length > 0) {
167
+ inputState = 'filled';
168
+ }
169
+
170
+ const backgroundColor = hasColorBG
171
+ ? theme.colors.background.default
172
+ : theme.colors.background.surface;
173
+
174
+ return (
175
+ <ComponentContext.Provider
176
+ value={{
177
+ componentName: 'InputSearch',
178
+ params,
179
+ state: inputState,
180
+ componentId: accessibilityLabel,
181
+ }}>
182
+ <View style={[style, styles.searchInputContainer]}>
183
+ <View
184
+ style={[
185
+ styles.searchInputWrapper,
186
+ {
187
+ backgroundColor,
188
+ },
189
+ ]}>
190
+ <Icon
191
+ source={'navigation_search'}
192
+ size={24}
193
+ color={theme.colors.text.hint}
194
+ />
195
+ {renderInputView()}
196
+ {renderIconView()}
197
+ </View>
198
+ {showButtonText && (
199
+ <TouchableOpacity onPress={onPressButtonText}>
200
+ <Text typography={'action_default_bold'} style={styles.textButton}>
201
+ {buttonText}
202
+ </Text>
203
+ </TouchableOpacity>
204
+ )}
205
+ </View>
206
+ </ComponentContext.Provider>
207
+ );
208
+ };
190
209
 
191
- export default InputSearch;
210
+ export default forwardRef(InputSearch);
@@ -0,0 +1,108 @@
1
+ import React, {FC, memo, useEffect, useRef} from 'react';
2
+ import {StyleSheet, TextInput, TextStyle} from 'react-native';
3
+ import {Colors} from '../Consts';
4
+ import {scaleSize} from '../Text';
5
+
6
+ export interface TextTypingProps {
7
+ data: string[];
8
+ timeDelayEnd?: number;
9
+ timeDelayNextChar?: number;
10
+ inputStyle?: TextStyle;
11
+ }
12
+
13
+ const TextTyping: FC<TextTypingProps> = ({
14
+ data = [],
15
+ timeDelayEnd = 2000,
16
+ timeDelayNextChar = 20,
17
+ inputStyle = {},
18
+ }) => {
19
+ const textRef = useRef<TextInput>(null);
20
+
21
+ const currentIndex = useRef<number>(0);
22
+ const currentCharIndex = useRef<number>(0);
23
+ const currentText = useRef('');
24
+
25
+ useEffect(() => {
26
+ setTimeout(() => {
27
+ playAnimation();
28
+ }, 1000);
29
+ }, []);
30
+
31
+ const playAnimation = () => {
32
+ const handleResetList = () => {
33
+ currentIndex.current = 0;
34
+ handleAnimation();
35
+ };
36
+
37
+ const handleAnimation = () => {
38
+ const listChar = data?.[currentIndex.current]?.split('') || [];
39
+ listChar.length !== 0 && showChar(listChar?.[currentCharIndex.current]);
40
+ };
41
+
42
+ currentIndex.current >= data?.length
43
+ ? handleResetList()
44
+ : handleAnimation();
45
+ };
46
+
47
+ const showChar = (char?: string) => {
48
+ const handleResetString = () => {
49
+ setTimeout(() => {
50
+ reset();
51
+ }, timeDelayEnd);
52
+ };
53
+
54
+ const handleShowChar = () => {
55
+ setTimeout(() => {
56
+ textRef.current?.setNativeProps?.({
57
+ text: currentText.current + char,
58
+ });
59
+ currentText.current += char;
60
+ currentCharIndex.current++;
61
+ playAnimation();
62
+ }, timeDelayNextChar);
63
+ };
64
+
65
+ currentCharIndex.current >= (data?.[currentIndex.current]?.length || 0)
66
+ ? handleResetString()
67
+ : handleShowChar();
68
+ };
69
+
70
+ const reset = () => {
71
+ const handleNextString = () => {
72
+ currentIndex.current++;
73
+ playAnimation();
74
+ };
75
+
76
+ const handleReset = () => {
77
+ setTimeout(() => {
78
+ textRef.current?.setNativeProps?.({
79
+ text: currentText.current.slice(0, -1),
80
+ });
81
+ currentText.current = currentText.current.slice(0, -1);
82
+ currentCharIndex.current--;
83
+ reset();
84
+ }, timeDelayNextChar);
85
+ };
86
+
87
+ currentCharIndex.current <= 0 ? handleNextString() : handleReset();
88
+ };
89
+
90
+ return (
91
+ <TextInput
92
+ style={[styles.inputStyle, inputStyle]}
93
+ ref={textRef}
94
+ editable={false}
95
+ autoCorrect={false}
96
+ pointerEvents={'none'}
97
+ />
98
+ );
99
+ };
100
+
101
+ export default memo(TextTyping);
102
+
103
+ const styles = StyleSheet.create({
104
+ inputStyle: {
105
+ color: Colors.black_12,
106
+ fontSize: scaleSize(12),
107
+ },
108
+ });
package/Input/index.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import {TextInputProps, ViewStyle} from 'react-native';
1
+ import {GestureResponderEvent, TextInputProps, ViewStyle} from 'react-native';
2
2
  import Input from './Input';
3
3
  import InputDropDown from './InputDropDown';
4
4
  import InputMoney from './InputMoney';
@@ -143,6 +143,9 @@ export interface InputSearchProps extends TextInputProps {
143
143
  * Optional. Represents the style of the InputSearch component.
144
144
  */
145
145
  params?: any;
146
+
147
+ typingData?: string[];
148
+ onPress?: (e: GestureResponderEvent) => void;
146
149
  }
147
150
 
148
151
  export interface InputMoneyProps extends Omit<InputProps, 'placeholder'> {}
@@ -282,4 +285,11 @@ export interface InputDropDownProps extends InputProps {
282
285
  style?: ViewStyle | ViewStyle[];
283
286
  }
284
287
 
288
+ export type InputSearchRef = {
289
+ clear: () => void;
290
+ focus: () => void | undefined;
291
+ blur: () => void | undefined;
292
+ setText: (text: string) => void;
293
+ };
294
+
285
295
  export {Input, InputDropDown, InputMoney, InputOTP, InputSearch, InputTextArea};
@@ -0,0 +1,58 @@
1
+ import React, {FC, useEffect, useRef} from 'react';
2
+ import {InputSearch} from '../Input';
3
+ import {Animated, Dimensions} from 'react-native';
4
+ import {AnimatedInputSearchProps} from './types';
5
+ import {Spacing} from '../Consts';
6
+ import {scaleSize} from '../Text';
7
+
8
+ const {width: screenWidth} = Dimensions.get('window');
9
+ const SCREEN_PADDING = 12;
10
+ const BACK_WIDTH = scaleSize(28);
11
+
12
+ const AnimatedInputSearch: FC<AnimatedInputSearchProps> = ({
13
+ animatedValue,
14
+ headerRightWidth,
15
+ inputSearchProps,
16
+ inputRef,
17
+ }) => {
18
+ const widthAnimated = useRef(new Animated.Value(0));
19
+
20
+ useEffect(() => {
21
+ const listener = animatedValue.addListener(({value}) => {
22
+ widthAnimated.current.setValue(value);
23
+ });
24
+ return () => {
25
+ animatedValue?.removeListener(listener);
26
+ };
27
+ }, []);
28
+
29
+ const translateX = widthAnimated.current.interpolate({
30
+ inputRange: [0, 100],
31
+ outputRange: [SCREEN_PADDING, BACK_WIDTH + SCREEN_PADDING * 2],
32
+ extrapolate: 'clamp',
33
+ });
34
+
35
+ return (
36
+ <Animated.View
37
+ style={{
38
+ transform: [{translateX}],
39
+ marginVertical: Spacing.S,
40
+ width: widthAnimated.current.interpolate({
41
+ inputRange: [0, 100],
42
+ outputRange: [
43
+ screenWidth - SCREEN_PADDING * 2,
44
+ screenWidth - SCREEN_PADDING * 3 - BACK_WIDTH - headerRightWidth,
45
+ ],
46
+ extrapolate: 'clamp',
47
+ }),
48
+ }}>
49
+ <InputSearch
50
+ ref={inputRef}
51
+ {...inputSearchProps}
52
+ showButtonText={false}
53
+ />
54
+ </Animated.View>
55
+ );
56
+ };
57
+
58
+ export default AnimatedInputSearch;
package/Layout/Screen.tsx CHANGED
@@ -2,14 +2,17 @@ import {useHeaderHeight} from '@react-navigation/stack';
2
2
  import React, {
3
3
  forwardRef,
4
4
  Fragment,
5
+ LegacyRef,
5
6
  ReactNode,
6
7
  useContext,
7
8
  useEffect,
8
9
  useRef,
10
+ useState,
9
11
  } from 'react';
10
12
  import {
11
13
  Animated,
12
14
  KeyboardAvoidingView,
15
+ LayoutChangeEvent,
13
16
  NativeScrollEvent,
14
17
  NativeSyntheticEvent,
15
18
  Platform,
@@ -24,13 +27,20 @@ import {
24
27
  HeaderTitle,
25
28
  } from '../Application';
26
29
  import Navigation from '../Application/Navigation';
27
- import {AnimatedHeader, NavigationOptions} from '../Application/types';
30
+ import {
31
+ AnimatedHeader,
32
+ HeaderRightActions,
33
+ HeaderRightToolkit,
34
+ NavigationOptions,
35
+ } from '../Application/types';
28
36
  import {Colors, Spacing, Styles} from '../Consts';
29
37
  import {FloatingButton, FloatingButtonProps} from './FloatingButton';
30
- import {Image} from '../Image';
31
38
  import {Card, Section, validateChildren} from './index';
32
39
  import styles from './styles';
33
40
  import {HeaderType} from './types';
41
+ import AnimatedInputSearch from './AnimatedInputSearch';
42
+ import {InputSearchProps, InputSearchRef} from '../Input';
43
+ import {ExtendBackground, HeaderRight} from '../Application/Components';
34
44
 
35
45
  export interface ScreenProps extends ViewProps {
36
46
  /**
@@ -101,8 +111,15 @@ export interface ScreenProps extends ViewProps {
101
111
  * Optional. If `true`, the screen content is using grid layout by span.
102
112
  */
103
113
  useGridLayout?: boolean;
114
+
115
+ inputSearchProps?: InputSearchProps;
116
+
117
+ headerRight?: HeaderRightActions | HeaderRightToolkit | CustomHeaderRight;
118
+ inputSearchRef?: LegacyRef<InputSearchRef>;
104
119
  }
105
120
 
121
+ type CustomHeaderRight = (props: any) => Element;
122
+
106
123
  const Screen = forwardRef(
107
124
  (
108
125
  {
@@ -120,26 +137,31 @@ const Screen = forwardRef(
120
137
  floatingButtonProps,
121
138
  useGridLayout = true,
122
139
  keyboardVerticalOffset,
140
+ inputSearchProps,
141
+ headerRight,
142
+ inputSearchRef,
123
143
  }: ScreenProps,
124
- ref,
144
+ ref
125
145
  ) => {
126
146
  const animatedValue = useRef<Animated.Value>(new Animated.Value(0));
127
147
  const {theme} = useContext(ApplicationContext);
128
148
  const insets = useSafeAreaInsets();
129
149
  const heightHeader = useHeaderHeight();
150
+ const scrollViewRef = useRef<any>(ref);
130
151
  const currentTint = useRef(Colors.black_01);
131
- const isTab = navigation?.instance?.getState?.()?.type == 'tab';
152
+ const isTab = navigation?.instance?.getState?.()?.type === 'tab';
153
+ const [headerRightWidth, setHeaderRightWidth] = useState(0);
132
154
 
133
155
  let styleAnimatedHeader: NavigationOptions;
134
156
  let handleScroll;
135
157
  let headerBackground: string | undefined = undefined;
136
158
  let Component: any = View;
137
159
  let keyboardOffset = heightHeader - 20;
138
- if (headerType == 'extended' || animatedHeader) {
160
+ if (headerType === 'extended' || animatedHeader) {
139
161
  keyboardOffset = -20;
140
162
  }
141
163
 
142
- if (headerType == 'extended') {
164
+ if (headerType === 'extended') {
143
165
  headerBackground = theme.assets?.headerBackground;
144
166
  }
145
167
 
@@ -157,7 +179,7 @@ const Screen = forwardRef(
157
179
  <HeaderTitle {...props} animatedValue={animatedValue.current} />
158
180
  ),
159
181
  };
160
- if (animatedHeader.type == 'surface') {
182
+ if (animatedHeader.type === 'surface') {
161
183
  styleAnimatedHeader = {
162
184
  ...styleAnimatedHeader,
163
185
  headerBackground: (props: any) => (
@@ -180,6 +202,7 @@ const Screen = forwardRef(
180
202
  }),
181
203
  };
182
204
  }
205
+
183
206
  navigation?.setOptions(styleAnimatedHeader);
184
207
  }
185
208
  }, [animatedHeader]);
@@ -187,6 +210,22 @@ const Screen = forwardRef(
187
210
  useEffect(() => {
188
211
  if (!animatedHeader) {
189
212
  let headerOptions: NavigationOptions;
213
+
214
+ const interpolateConfig: Animated.InterpolationConfigType =
215
+ inputSearchProps
216
+ ? {
217
+ inputRange: [0, 100],
218
+ outputRange: [1, 0],
219
+ extrapolate: 'clamp',
220
+ }
221
+ : {
222
+ inputRange: [0, 200],
223
+ outputRange: [0, 1],
224
+ extrapolate: 'clamp',
225
+ };
226
+ const titleOpacity =
227
+ animatedValue.current.interpolate(interpolateConfig);
228
+
190
229
  switch (headerType) {
191
230
  case 'none':
192
231
  headerOptions = {
@@ -214,7 +253,9 @@ const Screen = forwardRef(
214
253
  headerTransparent: true,
215
254
  headerTintColor: Colors.black_01,
216
255
  headerBackground: () => null,
217
- headerTitle: (props: any) => <HeaderTitle {...props} />,
256
+ headerTitle: (props: any) => (
257
+ <HeaderTitle {...props} titleOpacity={titleOpacity} />
258
+ ),
218
259
  };
219
260
  break;
220
261
 
@@ -230,13 +271,33 @@ const Screen = forwardRef(
230
271
  }
231
272
  }, [headerType]);
232
273
 
274
+ useEffect(() => {
275
+ if (headerRight) {
276
+ navigation?.setOptions({
277
+ headerRight: (props: any) => (
278
+ <View onLayout={onHeaderRightLayout}>
279
+ {typeof headerRight === 'function' ? (
280
+ headerRight(props)
281
+ ) : (
282
+ <HeaderRight {...props} {...headerRight} />
283
+ )}
284
+ </View>
285
+ ),
286
+ });
287
+ }
288
+ }, [headerRight]);
289
+
290
+ const onHeaderRightLayout = (e: LayoutChangeEvent) => {
291
+ setHeaderRightWidth(e.nativeEvent.layout.width);
292
+ };
293
+
233
294
  /**
234
295
  * animated when use scroll && animated value
235
296
  */
236
297
  if (scrollable) {
237
298
  Component = Animated.ScrollView;
238
299
  handleScroll = scrollViewProps?.onScroll;
239
- if (animatedHeader || floatingButtonProps?.icon) {
300
+ if (animatedHeader || floatingButtonProps?.icon || inputSearchProps) {
240
301
  handleScroll = Animated.event(
241
302
  [
242
303
  {
@@ -249,13 +310,13 @@ const Screen = forwardRef(
249
310
  useNativeDriver: true,
250
311
  listener: (e: NativeSyntheticEvent<NativeScrollEvent>) => {
251
312
  scrollViewProps?.onScroll?.(e);
252
- if (animatedHeader?.type == 'surface') {
313
+ if (animatedHeader?.type === 'surface') {
253
314
  const offsetY = e.nativeEvent.contentOffset.y;
254
315
  let color = Colors.black_01;
255
316
  if (offsetY > 50) {
256
317
  color = theme.colors.text.default;
257
318
  }
258
- if (color != currentTint.current) {
319
+ if (color !== currentTint.current) {
259
320
  currentTint.current = color;
260
321
  navigation?.setOptions({
261
322
  headerTintColor: color,
@@ -263,11 +324,26 @@ const Screen = forwardRef(
263
324
  }
264
325
  }
265
326
  },
266
- },
327
+ }
267
328
  );
268
329
  }
269
330
  }
270
331
 
332
+ const handleScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
333
+ const offsetY = e.nativeEvent.contentOffset.y;
334
+
335
+ if (inputSearchProps && offsetY < 100 && offsetY > 0) {
336
+ Animated.timing(animatedValue.current, {
337
+ toValue: 0,
338
+ useNativeDriver: true,
339
+ duration: 300,
340
+ }).start();
341
+ scrollViewRef.current?.scrollTo({y: 0, animated: true});
342
+ }
343
+
344
+ scrollViewProps?.onScrollEndDrag?.(e);
345
+ };
346
+
271
347
  /**
272
348
  * render top navigation banner
273
349
  */
@@ -287,7 +363,7 @@ const Screen = forwardRef(
287
363
  if (Array.isArray(results)) {
288
364
  return results.map((item, index) => {
289
365
  const space = item?.props?.useMargin === false ? 0 : Spacing.M;
290
- if (item?.type == Fragment) {
366
+ if (item?.type === Fragment) {
291
367
  return renderContent(item?.props?.children);
292
368
  }
293
369
  if (item) {
@@ -307,7 +383,7 @@ const Screen = forwardRef(
307
383
  } else {
308
384
  const item: any = children;
309
385
  const space = item?.props?.useMargin === false ? 0 : Spacing.M;
310
- if (item?.type == Fragment) {
386
+ if (item?.type === Fragment) {
311
387
  return renderContent(item?.props?.children);
312
388
  }
313
389
  return (
@@ -327,15 +403,21 @@ const Screen = forwardRef(
327
403
  return (
328
404
  <View style={[Styles.flex, {backgroundColor}]}>
329
405
  {!!headerBackground && (
330
- <>
331
- <Image
332
- source={{
333
- uri: headerBackground,
334
- }}
335
- style={styles.extendedHeader}
336
- />
337
- <View style={{height: heightHeader}} />
338
- </>
406
+ <ExtendBackground
407
+ headerBackground={headerBackground}
408
+ heightHeader={heightHeader}
409
+ animatedValue={animatedValue.current}
410
+ />
411
+ )}
412
+ {inputSearchProps && (
413
+ <AnimatedInputSearch
414
+ animatedValue={animatedValue.current}
415
+ headerRightWidth={headerRightWidth}
416
+ headerType={headerType}
417
+ heightHeader={heightHeader}
418
+ inputSearchProps={inputSearchProps}
419
+ inputRef={inputSearchRef}
420
+ />
339
421
  )}
340
422
  <KeyboardAvoidingView
341
423
  style={Styles.flex}
@@ -349,9 +431,11 @@ const Screen = forwardRef(
349
431
 
350
432
  <Component
351
433
  {...scrollViewProps}
352
- ref={ref}
434
+ ref={scrollViewRef}
353
435
  showsVerticalScrollIndicator={false}
354
436
  onScroll={handleScroll}
437
+ onScrollEndDrag={handleScrollEnd}
438
+ scrollEventThrottle={16}
355
439
  style={Styles.flex}>
356
440
  <View
357
441
  style={[styles.screenBanner, {maxHeight: 210 + layoutOffset}]}>
@@ -387,7 +471,7 @@ const Screen = forwardRef(
387
471
  </KeyboardAvoidingView>
388
472
  </View>
389
473
  );
390
- },
474
+ }
391
475
  );
392
476
 
393
477
  export default Screen;
package/Layout/styles.ts CHANGED
@@ -2,12 +2,6 @@ import {Platform, StyleSheet} from 'react-native';
2
2
  import {Colors, Radius, Spacing} from '../Consts';
3
3
 
4
4
  export default StyleSheet.create({
5
- extendedHeader: {
6
- aspectRatio: 1.75,
7
- position: 'absolute',
8
- width: '100%',
9
- height: 210,
10
- },
11
5
  screenBanner: {
12
6
  width: '100%',
13
7
  },
package/Layout/types.ts CHANGED
@@ -1,4 +1,8 @@
1
- export type HeaderType = 'default' | 'surface' | 'extended' | 'none';
1
+ import {Animated} from 'react-native';
2
+ import {InputSearchProps, InputSearchRef} from '../Input';
3
+ import {LegacyRef} from 'react';
4
+
5
+ export type HeaderType = 'default' | 'surface' | 'extended' | 'search' | 'none';
2
6
 
3
7
  export type GridContextProps = {
4
8
  /**
@@ -21,3 +25,18 @@ export type GridContextProps = {
21
25
  */
22
26
  getSizeSpan: (span: number) => number;
23
27
  };
28
+
29
+ export type AnimatedInputSearchProps = {
30
+ animatedValue: Animated.Value;
31
+ headerRightWidth: number;
32
+ heightHeader: number;
33
+ headerType: HeaderType;
34
+ inputSearchProps?: InputSearchProps;
35
+ inputRef?: LegacyRef<InputSearchRef>;
36
+ };
37
+
38
+ export type ExtendHeaderProps = {
39
+ headerBackground: string;
40
+ animatedValue: Animated.Value;
41
+ heightHeader: number;
42
+ };
package/Title/index.tsx CHANGED
@@ -1,13 +1,16 @@
1
1
  import React, {FC, useContext, useState} from 'react';
2
2
  import {Text as RNText, TouchableOpacity, View, ViewStyle} from 'react-native';
3
- import {Icon} from '../Icon';
4
- import {scaleSize, Text} from '../Text';
5
- import {ApplicationContext} from '../Application';
6
- import {Badge} from '../Badge';
7
- import {Colors} from '../Consts';
3
+ import {
4
+ Typography,
5
+ Colors,
6
+ Icon,
7
+ Badge,
8
+ ApplicationContext,
9
+ scaleSize,
10
+ Text,
11
+ } from '@momo-kits/foundation';
8
12
  import styles from './styles';
9
13
  import {TitleProps} from './types';
10
- import {Typography} from '../Text/types';
11
14
 
12
15
  const Title: FC<TitleProps> = ({
13
16
  title = 'Title',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/foundation",
3
- "version": "0.92.20",
3
+ "version": "0.92.21-beta.1",
4
4
  "description": "React Native Component Kits",
5
5
  "main": "index.ts",
6
6
  "scripts": {},