@hero-design/rn 8.59.0 → 8.60.0

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,7 +1,7 @@
1
1
  import React, { forwardRef } from 'react';
2
2
  import {
3
+ Animated,
3
4
  LayoutAnimation,
4
- LayoutAnimationConfig,
5
5
  Platform,
6
6
  StyleProp,
7
7
  StyleSheet,
@@ -18,18 +18,18 @@ import {
18
18
  StyledIconContainer,
19
19
  } from './StyledFAB';
20
20
 
21
- if (Platform.OS === 'android') {
22
- if (UIManager.setLayoutAnimationEnabledExperimental) {
23
- UIManager.setLayoutAnimationEnabledExperimental(true);
24
- }
25
- }
26
-
27
21
  export type FABHandles = {
28
22
  show: () => void;
29
23
  collapse: () => void;
30
24
  hide: () => void;
31
25
  };
32
26
 
27
+ if (Platform.OS === 'android') {
28
+ if (UIManager.setLayoutAnimationEnabledExperimental) {
29
+ UIManager.setLayoutAnimationEnabledExperimental(true);
30
+ }
31
+ }
32
+
33
33
  export interface FABProps {
34
34
  /**
35
35
  * Name of the Icon.
@@ -105,65 +105,104 @@ const IconWithTextContent = ({
105
105
  </>
106
106
  );
107
107
 
108
- const defaultAnimation: LayoutAnimationConfig = {
109
- create: {
110
- type: 'easeInEaseOut',
111
- property: 'opacity',
112
- },
113
- update: {
114
- type: 'spring',
115
- springDamping: Platform.OS === 'ios' ? 0.7 : 1.2,
116
- },
117
- duration: Platform.OS === 'ios' ? 300 : 400,
108
+ const animateWidth = () => {
109
+ LayoutAnimation.configureNext({
110
+ duration: Platform.OS === 'ios' ? 200 : 400,
111
+ update: {
112
+ type: 'spring',
113
+ springDamping: Platform.OS === 'ios' ? 1 : 1.5,
114
+ },
115
+ });
118
116
  };
119
117
 
120
118
  const FAB = forwardRef<FABHandles, FABProps>(
121
119
  ({ onPress, title, icon, animated, testID, active, style }, ref) => {
122
120
  const theme = useTheme();
123
- const [canAnimate, setCanAnimate] = React.useState(false);
124
121
  const [displayState, setDisplayState] = React.useState({
125
122
  hideTitle: false,
126
123
  hideButton: false,
127
124
  });
128
125
  const isIconOnly = displayState.hideTitle || active || !title;
129
126
 
127
+ const animatedValues = {
128
+ opacity: React.useRef(new Animated.Value(1)).current,
129
+ width: React.useRef(new Animated.Value(1)).current,
130
+ translateY: React.useRef(new Animated.Value(0)).current,
131
+ };
132
+
133
+ const marginBottom = Number(StyleSheet.flatten(style)?.marginBottom) || 0;
134
+ const [buttonWidth, setButtonWidth] = React.useState(0);
135
+ const hasSetButtonWidth = buttonWidth > 0;
136
+
130
137
  React.useImperativeHandle(
131
138
  ref,
132
139
  () => ({
133
140
  show: () => {
141
+ Animated.spring(animatedValues.translateY, {
142
+ toValue: 0,
143
+ useNativeDriver: true,
144
+ }).start();
145
+
134
146
  setDisplayState({
135
147
  hideButton: false,
136
148
  hideTitle: false,
137
149
  });
150
+
151
+ animateWidth();
152
+
153
+ Animated.spring(animatedValues.opacity, {
154
+ toValue: 1,
155
+ useNativeDriver: true,
156
+ }).start();
138
157
  },
139
158
  collapse: () => {
159
+ Animated.parallel([
160
+ Animated.spring(animatedValues.opacity, {
161
+ toValue: 1,
162
+ useNativeDriver: true,
163
+ }),
164
+ Animated.spring(animatedValues.translateY, {
165
+ toValue: 0,
166
+ useNativeDriver: true,
167
+ }),
168
+ ]).start();
169
+
170
+ animateWidth();
171
+
140
172
  setDisplayState({
141
173
  hideButton: false,
142
174
  hideTitle: true,
143
175
  });
144
176
  },
145
177
  hide: () => {
146
- setDisplayState((previousState) => ({
147
- ...previousState,
148
- hideButton: true,
149
- }));
178
+ Animated.stagger(20, [
179
+ Animated.spring(animatedValues.opacity, {
180
+ toValue: 0,
181
+ useNativeDriver: true,
182
+ }),
183
+ Animated.spring(animatedValues.translateY, {
184
+ toValue: 1,
185
+ useNativeDriver: true,
186
+ }),
187
+ ]).start(() => {
188
+ animateWidth();
189
+ setDisplayState((previousState) => ({
190
+ ...previousState,
191
+ hideButton: true,
192
+ }));
193
+ });
150
194
  },
151
195
  }),
152
196
  []
153
197
  );
154
198
 
155
- React.useEffect(() => {
156
- if (canAnimate) {
157
- LayoutAnimation.configureNext(defaultAnimation);
158
- }
159
- }, [isIconOnly, displayState.hideButton, canAnimate]);
160
-
161
- const marginBottom = Number(StyleSheet.flatten(style)?.marginBottom) || 0;
162
-
163
199
  return (
164
200
  <StyledFAB
165
- /** Add a small timeout before executing animation to prevent flakiness */
166
- onLayout={() => setTimeout(() => setCanAnimate(true), 500)}
201
+ onLayout={(event) =>
202
+ !hasSetButtonWidth &&
203
+ !active &&
204
+ setButtonWidth(event.nativeEvent.layout.width)
205
+ }
167
206
  underlayColor={theme.__hd__.fab.colors.buttonPressedBackground}
168
207
  onPress={onPress}
169
208
  style={[
@@ -172,20 +211,42 @@ const FAB = forwardRef<FABHandles, FABProps>(
172
211
  bottom: displayState.hideButton
173
212
  ? -(marginBottom + theme.__hd__.fab.sizes.height * 2)
174
213
  : StyleSheet.flatten(style)?.bottom,
214
+
215
+ transform: [
216
+ {
217
+ translateY: animatedValues.translateY.interpolate({
218
+ inputRange: [0, 1],
219
+ outputRange: [
220
+ 0,
221
+ marginBottom + theme.__hd__.fab.sizes.height * 2,
222
+ ],
223
+ }),
224
+ },
225
+ ],
175
226
  },
176
227
  ]}
177
228
  testID={testID}
178
229
  themeActive={active}
179
230
  >
180
- {isIconOnly ? (
181
- <IconOnlyContent
182
- animated={animated}
183
- active={active}
184
- icon={active ? 'add' : icon}
185
- />
186
- ) : (
187
- <IconWithTextContent icon={icon} title={title} />
188
- )}
231
+ <Animated.View
232
+ style={{
233
+ flexDirection: 'row',
234
+ opacity: animatedValues.opacity.interpolate({
235
+ inputRange: [0, 1],
236
+ outputRange: [0, 1],
237
+ }),
238
+ }}
239
+ >
240
+ {isIconOnly ? (
241
+ <IconOnlyContent
242
+ animated={animated}
243
+ active={active}
244
+ icon={active ? 'add' : icon}
245
+ />
246
+ ) : (
247
+ <IconWithTextContent icon={icon} title={title} />
248
+ )}
249
+ </Animated.View>
189
250
  </StyledFAB>
190
251
  );
191
252
  }
@@ -1,16 +1,17 @@
1
1
  import styled from '@emotion/native';
2
- import type { TextProps, TouchableHighlightProps } from 'react-native';
3
- import { TouchableHighlight } from 'react-native';
2
+ import type { TextProps } from 'react-native';
3
+ import { Animated, TouchableHighlight } from 'react-native';
4
+ import Box from '../Box';
4
5
  import type { IconProps } from '../Icon';
5
6
  import Icon from '../Icon';
6
7
  import Typography from '../Typography';
7
- import Box from '../Box';
8
8
 
9
- const StyledFAB = styled(TouchableHighlight)<
10
- TouchableHighlightProps & {
11
- themeActive?: boolean;
12
- }
13
- >(({ theme, themeActive }) => ({
9
+ const AnimatedTouchableHighlight =
10
+ Animated.createAnimatedComponent(TouchableHighlight);
11
+
12
+ const StyledFAB = styled(AnimatedTouchableHighlight)<{
13
+ themeActive?: boolean;
14
+ }>(({ theme, themeActive }) => ({
14
15
  backgroundColor: themeActive
15
16
  ? theme.__hd__.fab.colors.buttonActiveBackground
16
17
  : theme.__hd__.fab.colors.buttonBackground,
@@ -25,6 +26,7 @@ const StyledFAB = styled(TouchableHighlight)<
25
26
  shadowOffset: theme.__hd__.fab.shadows.offset,
26
27
  shadowRadius: theme.__hd__.fab.shadows.radius,
27
28
  shadowOpacity: theme.__hd__.fab.shadows.opacity,
29
+ height: theme.__hd__.fab.sizes.height,
28
30
  }));
29
31
 
30
32
  const StyledFABIcon = styled(Icon)<IconProps>(({ theme }) => ({
@@ -27,26 +27,24 @@ exports[`StyledFAB renders correctly 1`] = `
27
27
  onResponderTerminationRequest={[Function]}
28
28
  onStartShouldSetResponder={[Function]}
29
29
  style={
30
- [
31
- {
32
- "alignItems": "center",
33
- "alignSelf": "flex-start",
34
- "backgroundColor": "#401960",
35
- "borderRadius": 999,
36
- "elevation": 3,
37
- "flexDirection": "row",
38
- "justifyContent": "center",
39
- "padding": 20,
40
- "shadowColor": "#001f23",
41
- "shadowOffset": {
42
- "height": 2,
43
- "width": 0,
44
- },
45
- "shadowOpacity": 0.12,
46
- "shadowRadius": 4,
30
+ {
31
+ "alignItems": "center",
32
+ "alignSelf": "flex-start",
33
+ "backgroundColor": "#401960",
34
+ "borderRadius": 999,
35
+ "elevation": 3,
36
+ "flexDirection": "row",
37
+ "height": 64,
38
+ "justifyContent": "center",
39
+ "padding": 20,
40
+ "shadowColor": "#001f23",
41
+ "shadowOffset": {
42
+ "height": 2,
43
+ "width": 0,
47
44
  },
48
- undefined,
49
- ]
45
+ "shadowOpacity": 0.12,
46
+ "shadowRadius": 4,
47
+ }
50
48
  }
51
49
  >
52
50
  <Text
@@ -120,26 +118,24 @@ exports[`StyledFAB renders correctly 2`] = `
120
118
  onResponderTerminationRequest={[Function]}
121
119
  onStartShouldSetResponder={[Function]}
122
120
  style={
123
- [
124
- {
125
- "alignItems": "center",
126
- "alignSelf": "flex-start",
127
- "backgroundColor": "#33144d",
128
- "borderRadius": 999,
129
- "elevation": 3,
130
- "flexDirection": "row",
131
- "justifyContent": "center",
132
- "padding": 20,
133
- "shadowColor": "#001f23",
134
- "shadowOffset": {
135
- "height": 2,
136
- "width": 0,
137
- },
138
- "shadowOpacity": 0.12,
139
- "shadowRadius": 4,
121
+ {
122
+ "alignItems": "center",
123
+ "alignSelf": "flex-start",
124
+ "backgroundColor": "#33144d",
125
+ "borderRadius": 999,
126
+ "elevation": 3,
127
+ "flexDirection": "row",
128
+ "height": 64,
129
+ "justifyContent": "center",
130
+ "padding": 20,
131
+ "shadowColor": "#001f23",
132
+ "shadowOffset": {
133
+ "height": 2,
134
+ "width": 0,
140
135
  },
141
- undefined,
142
- ]
136
+ "shadowOpacity": 0.12,
137
+ "shadowRadius": 4,
138
+ }
143
139
  }
144
140
  >
145
141
  <Text