@momo-kits/foundation 0.162.2-test.9 → 0.162.3-fontscale.2

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.
@@ -6,6 +6,8 @@ import {
6
6
  import type { HeaderTitleProps, NavigationOptions } from './types';
7
7
  import { Colors, Spacing } from '../Consts';
8
8
  import { Animated, AppState, Platform } from 'react-native';
9
+ import type { SharedValue } from 'react-native-reanimated';
10
+ import { useSharedValue } from 'react-native-reanimated';
9
11
  import {
10
12
  MiniAppContext,
11
13
  ScreenContext,
@@ -60,7 +62,7 @@ const getModalOptions = (): StackNavigationOptions => {
60
62
  */
61
63
  const getOptions = (
62
64
  params: NavigationOptions,
63
- animatedValue?: Animated.Value,
65
+ animatedValue?: SharedValue<number>,
64
66
  ) => {
65
67
  let options: StackNavigationOptions = {};
66
68
 
@@ -131,7 +133,7 @@ const getOptions = (
131
133
 
132
134
  const exportHeaderTitle = (
133
135
  params: NavigationOptions,
134
- animatedValue?: Animated.Value,
136
+ animatedValue?: SharedValue<number>,
135
137
  ): StackNavigationOptions => {
136
138
  if (typeof params.headerTitle === 'object') {
137
139
  return {
@@ -228,6 +230,43 @@ const useAppState = () => {
228
230
  };
229
231
  };
230
232
 
233
+ /**
234
+ * Union of the two animation runtimes an internal foundation component might
235
+ * receive. Public Screen props still accept React Native Animated.Value only;
236
+ * this type keeps lower-level header/floating components compatible while they
237
+ * render with reanimated.
238
+ */
239
+ export type AnimatedCompatValue = Animated.Value | SharedValue<number>;
240
+
241
+ const isRNAnimatedValue = (v: unknown): v is Animated.Value =>
242
+ !!v &&
243
+ typeof (v as Animated.Value).setValue === 'function' &&
244
+ typeof (v as Animated.Value).interpolate === 'function';
245
+
246
+ const isSharedValue = (v: unknown): v is SharedValue<number> =>
247
+ !!v && !isRNAnimatedValue(v) && 'value' in (v as object);
248
+
249
+ const useNormalizedSharedValue = (
250
+ input?: AnimatedCompatValue,
251
+ ): SharedValue<number> => {
252
+ const fallback = useSharedValue(0);
253
+
254
+ useEffect(() => {
255
+ if (!isRNAnimatedValue(input)) {
256
+ return;
257
+ }
258
+ const id = input.addListener(({ value }) => {
259
+ fallback.value = value;
260
+ });
261
+ return () => input.removeListener(id);
262
+ }, [input, fallback]);
263
+
264
+ if (isSharedValue(input)) {
265
+ return input;
266
+ }
267
+ return fallback;
268
+ };
269
+
231
270
  export {
232
271
  getStackOptions,
233
272
  getModalOptions,
@@ -236,4 +275,5 @@ export {
236
275
  setAutomationID,
237
276
  useComponentId,
238
277
  useAppState,
278
+ useNormalizedSharedValue,
239
279
  };
@@ -1,93 +1,75 @@
1
- import React, { useEffect, useRef } from 'react';
2
- import { Animated, View } from 'react-native';
1
+ import React, { useEffect } from 'react';
2
+ import { View } from 'react-native';
3
+ import Animated, {
4
+ cancelAnimation,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withRepeat,
8
+ withSequence,
9
+ withSpring,
10
+ withTiming,
11
+ } from 'react-native-reanimated';
3
12
  import { BadgeDotProps } from './types';
4
13
  import styles from './styles';
5
14
 
6
15
  const DURATION = 500;
7
16
 
8
17
  const BadgeDotAnimation = ({ size, style }: BadgeDotProps) => {
9
- // Refs for animated values
10
- const scaleAnim = useRef(new Animated.Value(1)).current;
11
- const waveScaleAnim = useRef(new Animated.Value(1)).current;
12
- const waveOpacityAnim = useRef(new Animated.Value(0)).current;
18
+ const scaleAnim = useSharedValue(1);
19
+ const waveScaleAnim = useSharedValue(1);
20
+ const waveOpacityAnim = useSharedValue(0);
13
21
 
14
22
  const dotStyle =
15
23
  size === 'small' ? styles.dotAnimationSmall : styles.dotAnimation;
16
24
  const waveStyle = size === 'small' ? styles.waveSmall : styles.wave;
17
25
 
18
26
  useEffect(() => {
19
- // Infinite loop animation for the scale and wave effect
20
- const animation = Animated.loop(
21
- Animated.parallel([
22
- // Dot pulse animation
23
- Animated.sequence([
24
- Animated.spring(scaleAnim, {
25
- toValue: 1, // Scale up slightly
26
- friction: 5, // Controls the "bounciness" of the spring
27
- tension: 30, // Controls the "stiffness" of the spring
28
- useNativeDriver: true,
29
- }),
30
- Animated.spring(scaleAnim, {
31
- toValue: 1.1,
32
- friction: 5,
33
- tension: 30,
34
- useNativeDriver: true,
35
- }),
36
- ]), // Wave animation
37
- Animated.sequence([
38
- Animated.timing(waveScaleAnim, {
39
- toValue: 2.5,
40
- duration: DURATION * 3,
41
- useNativeDriver: true,
42
- }),
43
- Animated.timing(waveScaleAnim, {
44
- toValue: 1, // Reset wave size
45
- duration: 0,
46
- useNativeDriver: true,
47
- }),
48
- ]), // Wave opacity animation
49
- Animated.sequence([
50
- Animated.timing(waveOpacityAnim, {
51
- toValue: 0.3, // Wave becomes visible
52
- duration: DURATION * 2,
53
- useNativeDriver: true,
54
- }),
55
- Animated.timing(waveOpacityAnim, {
56
- toValue: 0, // Wave fades out
57
- duration: DURATION,
58
- useNativeDriver: true,
59
- }),
60
- ]),
61
- ]),
62
- );
63
- animation.start();
27
+ const springCfg = { damping: 5, stiffness: 30, mass: 1 };
64
28
 
29
+ scaleAnim.value = withRepeat(
30
+ withSequence(
31
+ withSpring(1, springCfg),
32
+ withSpring(1.1, springCfg),
33
+ ),
34
+ -1,
35
+ false,
36
+ );
37
+ waveScaleAnim.value = withRepeat(
38
+ withSequence(
39
+ withTiming(2.5, { duration: DURATION * 3 }),
40
+ withTiming(1, { duration: 0 }),
41
+ ),
42
+ -1,
43
+ false,
44
+ );
45
+ waveOpacityAnim.value = withRepeat(
46
+ withSequence(
47
+ withTiming(0.3, { duration: DURATION * 2 }),
48
+ withTiming(0, { duration: DURATION }),
49
+ ),
50
+ -1,
51
+ false,
52
+ );
65
53
  return () => {
66
- animation.stop();
54
+ cancelAnimation(scaleAnim);
55
+ cancelAnimation(waveScaleAnim);
56
+ cancelAnimation(waveOpacityAnim);
67
57
  };
68
58
  }, [scaleAnim, waveOpacityAnim, waveScaleAnim]);
69
59
 
60
+ const waveAnimatedStyle = useAnimatedStyle(() => ({
61
+ transform: [{ scale: waveScaleAnim.value }],
62
+ opacity: waveOpacityAnim.value,
63
+ }));
64
+
65
+ const dotAnimatedStyle = useAnimatedStyle(() => ({
66
+ transform: [{ scale: scaleAnim.value }],
67
+ }));
68
+
70
69
  return (
71
70
  <View style={[styles.dotAnimationContainer, style]}>
72
- {/* Wave Animation */}
73
- <Animated.View
74
- style={[
75
- waveStyle,
76
- {
77
- transform: [{ scale: waveScaleAnim }],
78
- opacity: waveOpacityAnim,
79
- },
80
- ]}
81
- />
82
- {/* Dot Animation */}
83
- <Animated.View
84
- style={[
85
- dotStyle,
86
- {
87
- transform: [{ scale: scaleAnim }],
88
- },
89
- ]}
90
- />
71
+ <Animated.View style={[waveStyle, waveAnimatedStyle]} />
72
+ <Animated.View style={[dotStyle, dotAnimatedStyle]} />
91
73
  </View>
92
74
  );
93
75
  };
package/Button/index.tsx CHANGED
@@ -1,6 +1,5 @@
1
1
  import React, { FC, useContext, useRef } from 'react';
2
2
  import {
3
- Animated,
4
3
  StyleSheet,
5
4
  TouchableOpacity,
6
5
  TouchableOpacityProps,
@@ -29,8 +28,6 @@ import Reanimated, {
29
28
  withTiming,
30
29
  } from 'react-native-reanimated';
31
30
 
32
- const AnimationLinear = Animated.createAnimatedComponent(LinearGradient);
33
-
34
31
  export interface ButtonProps extends TouchableOpacityProps {
35
32
  /**
36
33
  * Defines the visual style of the button.
@@ -359,7 +356,7 @@ const Button: FC<ButtonProps> = ({
359
356
  {renderTitle()}
360
357
  {renderIcon('right')}
361
358
  {gradientPros && (
362
- <AnimationLinear {...gradientPros} style={styles.gradientView} />
359
+ <LinearGradient {...gradientPros} style={styles.gradientView} />
363
360
  )}
364
361
  {gradientPros && <View style={styles.strokeView} />}
365
362
  </Reanimated.View>
package/Context/index.ts CHANGED
@@ -9,12 +9,10 @@ const MiniAppContext = (Platform as any).MiniAppContext ?? Context;
9
9
  const ScreenContext = (Platform as any).ScreenContext ?? Context;
10
10
  const ComponentContext = (Platform as any).ComponentContext ?? Context;
11
11
  const SkeletonContext = createContext({ loading: false });
12
- // Single source of truth: holds both the OS flag and the user-adjusted rate.
13
- export type FontScaleConfig = {
14
- useOSFontScale?: boolean;
15
- userScaleRate?: number;
16
- };
17
- const ScaleSizeContext = createContext<FontScaleConfig>({});
12
+ /** @deprecated Max font scale is fixed at 1.5; this context is no longer read. */
13
+ const ScaleSizeContext = createContext<{ scaleSizeMaxRate?: number }>({
14
+ scaleSizeMaxRate: undefined,
15
+ });
18
16
  const TrackingScopeContext = createContext<{ scopeName?: string }>({
19
17
  scopeName: undefined,
20
18
  });
package/Input/Input.tsx CHANGED
@@ -77,7 +77,8 @@ const Input = forwardRef(
77
77
  const scaledFontSize = useScaleSize(14);
78
78
  const scaleHeight = useScaleSize(size === 'small' ? 48 : 56);
79
79
  const [focused, setFocused] = useState(false);
80
- const haveValue = !!value || !!defaultValue;
80
+ const [internalText, setInternalText] = useState(defaultValue || '');
81
+ const haveValue = value !== undefined ? !!value : !!internalText;
81
82
  const inputRef = useRef<TextInput | null>(null);
82
83
  const componentName = 'Input';
83
84
 
@@ -95,6 +96,9 @@ const Input = forwardRef(
95
96
  };
96
97
 
97
98
  const _onChangeText = (text: string) => {
99
+ if (value === undefined) {
100
+ setInternalText(text);
101
+ }
98
102
  onChangeText?.(text);
99
103
  };
100
104
 
@@ -109,6 +113,13 @@ const Input = forwardRef(
109
113
  };
110
114
 
111
115
  const _setText = (text: string) => {
116
+ if (__DEV__ && value !== undefined) {
117
+ console.error(
118
+ `${componentName}: calling ref.setText() on a controlled input (\`value\` is provided) is an anti-pattern. ` +
119
+ 'It creates two sources of truth: setText will be overridden by React on the next render. ' +
120
+ 'Update the state behind `value` instead.',
121
+ );
122
+ }
112
123
  inputRef?.current?.setNativeProps({ text });
113
124
  _onChangeText(text);
114
125
  };
@@ -8,13 +8,22 @@ import React, {
8
8
  useState,
9
9
  } from 'react';
10
10
  import {
11
- Animated,
12
11
  PixelRatio,
13
12
  TextInput,
14
13
  TextInputFocusEvent,
15
14
  TouchableOpacity,
16
15
  View,
17
16
  } from 'react-native';
17
+ import Animated, {
18
+ cancelAnimation,
19
+ Easing,
20
+ useAnimatedStyle,
21
+ useSharedValue,
22
+ withDelay,
23
+ withRepeat,
24
+ withSequence,
25
+ withTiming,
26
+ } from 'react-native-reanimated';
18
27
  import { useComponentId } from '../Application';
19
28
  import { Spacing, Styles } from '../Consts';
20
29
  import { useScaleSize, Text } from '../Text';
@@ -32,37 +41,43 @@ import {
32
41
  const OTPCaret: FC<CaretProps> = ({ index, length }) => {
33
42
  const DURATION = 300;
34
43
  const { theme } = useContext(ApplicationContext);
35
- const opacity = useRef(new Animated.Value(0)).current;
44
+ const opacity = useSharedValue(0);
36
45
 
37
46
  useEffect(() => {
38
- Animated.loop(
39
- Animated.sequence([
40
- Animated.timing(opacity, {
41
- toValue: 1,
42
- duration: DURATION,
43
- useNativeDriver: true,
44
- }),
45
- Animated.delay(DURATION * 2),
46
- Animated.timing(opacity, {
47
- toValue: 0,
48
- duration: DURATION,
49
- useNativeDriver: true,
50
- }),
51
- ]),
52
- ).start();
47
+ opacity.value = withRepeat(
48
+ withSequence(
49
+ withTiming(1, { duration: DURATION, easing: Easing.linear }),
50
+ withDelay(
51
+ DURATION * 2,
52
+ withTiming(0, { duration: DURATION, easing: Easing.linear }),
53
+ ),
54
+ ),
55
+ -1,
56
+ false,
57
+ );
58
+ return () => {
59
+ cancelAnimation(opacity);
60
+ };
53
61
  }, [opacity]);
62
+
63
+ const animatedStyle = useAnimatedStyle(() => ({
64
+ opacity: opacity.value,
65
+ }));
66
+
54
67
  const spacingStyle = !isNaN(Number(length)) &&
55
68
  index !== Number(length) - 1 && { marginRight: Spacing.L };
56
69
 
57
70
  return (
58
71
  <View style={[Styles.rowCenter, spacingStyle]}>
59
72
  <Animated.View
60
- style={{
61
- height: useScaleSize(12),
62
- width: 1,
63
- backgroundColor: theme.colors.primary,
64
- opacity,
65
- }}
73
+ style={[
74
+ {
75
+ height: useScaleSize(12),
76
+ width: 1,
77
+ backgroundColor: theme.colors.primary,
78
+ },
79
+ animatedStyle,
80
+ ]}
66
81
  />
67
82
  <Text color={theme.colors.text.hint} typography={'body_default_regular'}>
68
83
  -
@@ -69,7 +69,8 @@ const InputPhoneNumber = forwardRef(
69
69
  const context = useContext<any>(MiniAppContext);
70
70
  const scaleHeight = useScaleSize(size === 'small' ? 48 : 56);
71
71
  const [focused, setFocused] = useState(false);
72
- const haveValue = !!value || !!defaultValue;
72
+ const [internalText, setInternalText] = useState(defaultValue || '');
73
+ const haveValue = value !== undefined ? !!value : !!internalText;
73
74
  const inputRef = useRef<TextInput | null>(null);
74
75
  const componentName = 'InputPhoneNumber';
75
76
 
@@ -86,6 +87,9 @@ const InputPhoneNumber = forwardRef(
86
87
  };
87
88
 
88
89
  const _onChangeText = (text: string) => {
90
+ if (value === undefined) {
91
+ setInternalText(text);
92
+ }
89
93
  onChangeText?.(text);
90
94
  };
91
95
 
@@ -100,6 +104,13 @@ const InputPhoneNumber = forwardRef(
100
104
  };
101
105
 
102
106
  const _setText = (text: string) => {
107
+ if (__DEV__ && value !== undefined) {
108
+ console.error(
109
+ `${componentName}: calling ref.setText() on a controlled input (\`value\` is provided) is an anti-pattern. ` +
110
+ 'It creates two sources of truth: setText will be overridden by React on the next render. ' +
111
+ 'Update the state behind `value` instead.',
112
+ );
113
+ }
103
114
  inputRef?.current?.setNativeProps({ text });
104
115
  _onChangeText(text);
105
116
  };
@@ -1,20 +1,24 @@
1
+ import React, { useContext, useEffect, useState } from 'react';
1
2
  import {
2
- default as React,
3
- useContext,
4
- useEffect,
5
- useRef,
6
- useState,
7
- } from 'react';
8
- import {
9
- Animated,
10
3
  LayoutChangeEvent,
11
4
  StyleSheet,
12
5
  TouchableOpacity,
13
6
  View,
14
7
  } from 'react-native';
8
+ import Animated, {
9
+ useAnimatedReaction,
10
+ useAnimatedStyle,
11
+ useSharedValue,
12
+ withTiming,
13
+ runOnJS,
14
+ } from 'react-native-reanimated';
15
15
  import { ApplicationContext } from '../Context';
16
16
  import { Icon } from '../Icon';
17
17
  import { useScaleSize } from '../Text';
18
+ import {
19
+ AnimatedCompatValue,
20
+ useNormalizedSharedValue,
21
+ } from '../Application/utils';
18
22
 
19
23
  export interface FloatingButtonProps {
20
24
  label?: string;
@@ -23,7 +27,7 @@ export interface FloatingButtonProps {
23
27
  icon?: string;
24
28
  iconColor?: string;
25
29
  size?: 'small' | 'large';
26
- animatedValue?: Animated.Value;
30
+ animatedValue?: AnimatedCompatValue;
27
31
  bottom?: number;
28
32
  renderComponent?: () => React.ReactNode;
29
33
  }
@@ -40,70 +44,71 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
40
44
  bottom = 12,
41
45
  }: FloatingButtonProps) => {
42
46
  const { theme } = useContext(ApplicationContext);
47
+ const enabled = animatedValue != null;
48
+ const sharedValue = useNormalizedSharedValue(animatedValue);
43
49
  const scaledFontSize = useScaleSize(16);
44
50
  const scaledLineHeight = useScaleSize(22);
45
- const maxWidth = useRef(0);
51
+ const maxWidth = useSharedValue(0);
46
52
  const minWidth = size === 'small' ? 36 : 48;
47
- const [opacityAnimated] = useState(new Animated.Value(0)); // Initial opacity set to 0
48
- const [widthAnimated, setWidthAnimated] = useState<Animated.Value>();
49
- const lastOffset = useRef(0);
50
- const lastDirection = useRef<string>(null);
51
- const [showText, setShowText] = React.useState(true);
53
+ const opacityAnimated = useSharedValue(0);
54
+ const widthAnimated = useSharedValue<number | null>(null);
55
+ const lastOffset = useSharedValue(0);
56
+ const lastDirection = useSharedValue<string | null>(null);
57
+ const [showText, setShowText] = useState(true);
52
58
 
53
59
  useEffect(() => {
54
60
  if (!label) return;
61
+ opacityAnimated.value = withTiming(1, { duration: 100 });
62
+ }, [label, opacityAnimated]);
55
63
 
56
- Animated.timing(opacityAnimated, {
57
- toValue: 1,
58
- duration: 100,
59
- useNativeDriver: true,
60
- }).start();
61
-
62
- const listener = animatedValue?.addListener(({ value }) => {
63
- if (value !== lastOffset.current && value > 0) {
64
- const direction = value > lastOffset.current ? 'down' : 'up';
65
- lastOffset.current = value;
66
- if (lastDirection.current !== direction) {
67
- lastDirection.current = direction;
64
+ useAnimatedReaction(
65
+ () => sharedValue.value,
66
+ (value) => {
67
+ 'worklet';
68
+ if (!label || !enabled) return;
69
+ if (value !== lastOffset.value && value > 0) {
70
+ const direction = value > lastOffset.value ? 'down' : 'up';
71
+ lastOffset.value = value;
72
+ if (lastDirection.value !== direction) {
73
+ lastDirection.value = direction;
68
74
  if (direction === 'down') {
69
- Animated.timing(opacityAnimated, {
70
- toValue: 0,
71
- duration: 100,
72
- useNativeDriver: true,
73
- }).start();
74
- Animated.timing(widthAnimated!, {
75
- toValue: minWidth,
76
- duration: 100,
77
- useNativeDriver: false,
78
- }).start(() => setShowText(false));
75
+ opacityAnimated.value = withTiming(0, { duration: 100 });
76
+ widthAnimated.value = withTiming(
77
+ minWidth,
78
+ { duration: 100 },
79
+ (finished) => {
80
+ if (finished) runOnJS(setShowText)(false);
81
+ },
82
+ );
79
83
  } else {
80
- Animated.timing(opacityAnimated, {
81
- toValue: 1,
82
- duration: 100,
83
- useNativeDriver: true,
84
- }).start();
85
- Animated.timing(widthAnimated!, {
86
- toValue: maxWidth.current,
87
- duration: 100,
88
- useNativeDriver: false,
89
- }).start(() => setShowText(true));
84
+ opacityAnimated.value = withTiming(1, { duration: 100 });
85
+ widthAnimated.value = withTiming(
86
+ maxWidth.value,
87
+ { duration: 100 },
88
+ (finished) => {
89
+ if (finished) runOnJS(setShowText)(true);
90
+ },
91
+ );
90
92
  }
91
93
  }
92
94
  }
93
- });
95
+ },
96
+ [label, enabled, minWidth],
97
+ );
94
98
 
95
- return () => {
96
- if (listener) {
97
- animatedValue?.removeListener(listener);
98
- }
99
- };
100
- }, [animatedValue, label, minWidth, opacityAnimated, widthAnimated]);
99
+ const containerStyle = useAnimatedStyle(() => ({
100
+ width: widthAnimated.value ?? undefined,
101
+ }));
102
+
103
+ const labelStyle = useAnimatedStyle(() => ({
104
+ opacity: opacityAnimated.value,
105
+ }));
101
106
 
102
107
  const handleLayout = (event: LayoutChangeEvent) => {
103
108
  const layout = event.nativeEvent.layout;
104
- if (widthAnimated) return;
105
- maxWidth.current = layout.width;
106
- setWidthAnimated(new Animated.Value(layout.width));
109
+ if (widthAnimated.value != null) return;
110
+ maxWidth.value = layout.width;
111
+ widthAnimated.value = layout.width;
107
112
  };
108
113
 
109
114
  if (renderComponent) {
@@ -132,11 +137,11 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
132
137
  {
133
138
  right: position === 'right' ? 12 : undefined,
134
139
  alignSelf: position === 'center' ? 'center' : 'flex-end',
135
- width: widthAnimated,
136
140
  height: size === 'small' ? 36 : 48,
137
141
  backgroundColor: theme.colors.primary,
138
142
  bottom,
139
143
  },
144
+ containerStyle,
140
145
  ]}
141
146
  >
142
147
  <TouchableOpacity
@@ -156,9 +161,9 @@ export const FloatingButton: React.FC<FloatingButtonProps> = ({
156
161
  {
157
162
  fontSize: scaledFontSize,
158
163
  lineHeight: scaledLineHeight,
159
- opacity: opacityAnimated,
160
164
  color: 'white',
161
165
  },
166
+ labelStyle,
162
167
  ]}
163
168
  numberOfLines={1}
164
169
  >