rnnovaui 0.9.1 → 0.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnnovaui",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -3,7 +3,6 @@ import React, { memo, useEffect, useRef, useState } from "react";
3
3
  import { Animated, StyleSheet, View } from "react-native";
4
4
 
5
5
  import { useUIAccordion } from "./UIAccordion";
6
-
7
6
  import { useUIAccordionItem } from "./UIAccordionItem";
8
7
 
9
8
  export const UIAccordionContent = memo(function UIAccordionContent({
@@ -14,88 +13,205 @@ export const UIAccordionContent = memo(function UIAccordionContent({
14
13
  innerStyle,
15
14
 
16
15
  testID,
16
+
17
+ onLayout,
17
18
  }) {
18
19
  const accordion = useUIAccordion();
19
20
 
20
21
  const item = useUIAccordionItem();
21
22
 
22
- const progress = useRef(new Animated.Value(item.open ? 1 : 0)).current;
23
+ const heightAnimation = useRef(new Animated.Value(item.open ? 1 : 0)).current;
24
+
25
+ const opacityAnimation = useRef(
26
+ new Animated.Value(item.open ? 1 : 0),
27
+ ).current;
28
+
29
+ const [contentHeight, setContentHeight] = useState(0);
23
30
 
24
31
  const [mounted, setMounted] = useState(item.open);
25
32
 
33
+ /*
34
+ * ------------------------------------------------
35
+ * MEASURE CONTENT
36
+ * ------------------------------------------------
37
+ */
38
+
39
+ const handleLayout = (event) => {
40
+ const height = event.nativeEvent.layout.height;
41
+
42
+ if (height !== contentHeight) {
43
+ setContentHeight(height);
44
+ }
45
+
46
+ onLayout?.(event);
47
+ };
48
+
49
+ /*
50
+ * ------------------------------------------------
51
+ * OPEN / CLOSE
52
+ * ------------------------------------------------
53
+ */
54
+
26
55
  useEffect(() => {
27
56
  if (item.open) {
57
+ /*
58
+ * Mount before opening.
59
+ */
28
60
  setMounted(true);
29
61
 
30
62
  if (!accordion.animated) {
31
- progress.setValue(1);
63
+ heightAnimation.setValue(1);
64
+
65
+ opacityAnimation.setValue(1);
66
+
32
67
  return;
33
68
  }
34
69
 
35
- Animated.timing(progress, {
36
- toValue: 1,
70
+ /*
71
+ * Smooth opening.
72
+ */
73
+ Animated.parallel([
74
+ Animated.timing(heightAnimation, {
75
+ toValue: 1,
37
76
 
38
- duration: accordion.animationDuration,
77
+ duration: accordion.animationDuration,
39
78
 
40
- useNativeDriver: false,
41
- }).start();
79
+ useNativeDriver: false,
80
+ }),
81
+
82
+ Animated.timing(opacityAnimation, {
83
+ toValue: 1,
84
+
85
+ duration: Math.min(accordion.animationDuration, 180),
86
+
87
+ useNativeDriver: false,
88
+ }),
89
+ ]).start();
42
90
 
43
91
  return;
44
92
  }
45
93
 
94
+ /*
95
+ * ------------------------------------------------
96
+ * CLOSE
97
+ * ------------------------------------------------
98
+ */
99
+
46
100
  if (!accordion.animated) {
47
- progress.setValue(0);
101
+ heightAnimation.setValue(0);
102
+
103
+ opacityAnimation.setValue(0);
104
+
48
105
  setMounted(false);
106
+
49
107
  return;
50
108
  }
51
109
 
52
- Animated.timing(progress, {
53
- toValue: 0,
110
+ /*
111
+ * Animate to zero first.
112
+ */
113
+ Animated.parallel([
114
+ Animated.timing(heightAnimation, {
115
+ toValue: 0,
54
116
 
55
- duration: accordion.animationDuration,
117
+ duration: accordion.animationDuration,
56
118
 
57
- useNativeDriver: false,
58
- }).start(() => {
59
- setMounted(false);
60
- });
61
- }, [item.open, accordion.animated, accordion.animationDuration, progress]);
119
+ useNativeDriver: false,
120
+ }),
121
+
122
+ Animated.timing(opacityAnimation, {
123
+ toValue: 0,
124
+
125
+ duration: Math.min(accordion.animationDuration, 160),
62
126
 
63
- if (!mounted) {
64
- return null;
127
+ useNativeDriver: false,
128
+ }),
129
+ ]).start(({ finished }) => {
130
+ if (finished) {
131
+ setMounted(false);
132
+ }
133
+ });
134
+ }, [
135
+ item.open,
136
+ accordion.animated,
137
+ accordion.animationDuration,
138
+ heightAnimation,
139
+ opacityAnimation,
140
+ ]);
141
+
142
+ /*
143
+ * ------------------------------------------------
144
+ * CLOSED
145
+ * ------------------------------------------------
146
+ */
147
+
148
+ if (!mounted && !item.open) {
149
+ return (
150
+ <View
151
+ style={{
152
+ position: "absolute",
153
+
154
+ opacity: 0,
155
+
156
+ pointerEvents: "none",
157
+ }}
158
+ >
159
+ <View onLayout={handleLayout}>{children}</View>
160
+ </View>
161
+ );
65
162
  }
66
163
 
164
+ /*
165
+ * ------------------------------------------------
166
+ * ANIMATED HEIGHT
167
+ * ------------------------------------------------
168
+ */
169
+
170
+ const animatedHeight =
171
+ contentHeight > 0
172
+ ? heightAnimation.interpolate({
173
+ inputRange: [0, 1],
174
+
175
+ outputRange: [0, contentHeight],
176
+ })
177
+ : undefined;
178
+
179
+ /*
180
+ * ------------------------------------------------
181
+ * RENDER
182
+ * ------------------------------------------------
183
+ */
184
+
67
185
  return (
68
186
  <Animated.View
69
187
  testID={testID}
70
188
  style={[
71
- styles.animatedContainer,
189
+ styles.container,
72
190
 
73
191
  {
74
- opacity: progress,
75
-
76
- maxHeight: progress.interpolate({
77
- inputRange: [0, 1],
192
+ height: animatedHeight,
78
193
 
79
- outputRange: [0, 1000],
80
- }),
194
+ opacity: opacityAnimation,
81
195
  },
82
196
 
83
197
  style,
84
198
  ]}
85
199
  >
86
- <View style={[styles.inner, innerStyle]}>{children}</View>
200
+ <View style={[styles.content, innerStyle]} onLayout={handleLayout}>
201
+ {children}
202
+ </View>
87
203
  </Animated.View>
88
204
  );
89
205
  });
90
206
 
91
207
  const styles = StyleSheet.create({
92
- animatedContainer: {
208
+ container: {
93
209
  width: "100%",
94
210
 
95
211
  overflow: "hidden",
96
212
  },
97
213
 
98
- inner: {
214
+ content: {
99
215
  width: "100%",
100
216
  },
101
217
  });
@@ -1,7 +1,5 @@
1
1
  import React, {
2
- Children,
3
2
  createContext,
4
- isValidElement,
5
3
  memo,
6
4
  useCallback,
7
5
  useContext,
@@ -12,31 +10,80 @@ import React, {
12
10
  import { StyleSheet, View } from "react-native";
13
11
 
14
12
  import { useUITheme } from "../../theme";
15
- import { UITabsList } from "./UITabsList";
16
- import { UITabsTrigger } from "./UITabsTrigger";
17
- import { UITabsContent } from "./UITabsContent";
18
- import { UITabsIndicator } from "./UITabsIndicator";
19
13
 
20
- const TabsContext = createContext(null);
14
+ const UITabsContext = createContext(null);
21
15
 
22
- const UI_TABS_SIZES = {
16
+ const TAB_SIZES = {
23
17
  sm: "sm",
24
18
  md: "md",
25
19
  lg: "lg",
26
20
  };
27
21
 
22
+ const ANIMATION_PRESETS = {
23
+ none: {
24
+ indicator: "none",
25
+ content: "none",
26
+ press: "none",
27
+ },
28
+
29
+ subtle: {
30
+ indicator: "slide",
31
+ content: "fade",
32
+ press: "scale",
33
+ },
34
+
35
+ smooth: {
36
+ indicator: "slide",
37
+ content: "fadeSlide",
38
+ press: "scale",
39
+ },
40
+
41
+ snappy: {
42
+ indicator: "spring",
43
+ content: "slideFade",
44
+ press: "scale",
45
+ },
46
+
47
+ spring: {
48
+ indicator: "spring",
49
+ content: "slideScale",
50
+ press: "scale",
51
+ },
52
+
53
+ bouncy: {
54
+ indicator: "spring",
55
+ content: "springScale",
56
+ press: "scale",
57
+ },
58
+ };
59
+
28
60
  export const UITabs = memo(function UITabs({
29
61
  children,
30
62
 
31
63
  value,
32
- defaultValue,
64
+ defaultValue = null,
33
65
 
34
66
  onValueChange,
35
67
 
36
68
  size = "md",
37
69
 
38
70
  animated = true,
39
- animationDuration = 220,
71
+
72
+ animationPreset = "smooth",
73
+
74
+ indicatorAnimation,
75
+ contentAnimation,
76
+ pressAnimation,
77
+
78
+ animationDuration = 260,
79
+
80
+ spring = {
81
+ damping: 18,
82
+ stiffness: 180,
83
+ mass: 0.8,
84
+ },
85
+
86
+ directionAware = true,
40
87
 
41
88
  variant = "default",
42
89
 
@@ -44,12 +91,14 @@ export const UITabs = memo(function UITabs({
44
91
  inactiveColor,
45
92
 
46
93
  indicatorColor,
94
+
47
95
  indicatorHeight = 2,
48
96
 
49
97
  indicatorWidth = "content",
50
98
 
51
99
  disabled = false,
52
100
 
101
+ style,
53
102
  containerStyle,
54
103
 
55
104
  testID,
@@ -60,25 +109,38 @@ export const UITabs = memo(function UITabs({
60
109
 
61
110
  const controlled = value !== undefined;
62
111
 
63
- const [internalValue, setInternalValue] = useState(defaultValue ?? null);
112
+ const [internalValue, setInternalValue] = useState(defaultValue);
64
113
 
65
114
  const currentValue = controlled ? value : internalValue;
66
115
 
67
- const safeSize = UI_TABS_SIZES[size] ? size : "md";
116
+ const [previousValue, setPreviousValue] = useState(currentValue);
117
+
118
+ const safeSize = TAB_SIZES[size] ? size : "md";
119
+
120
+ const safePreset =
121
+ ANIMATION_PRESETS[animationPreset] || ANIMATION_PRESETS.smooth;
122
+
123
+ const resolvedIndicatorAnimation = indicatorAnimation ?? safePreset.indicator;
124
+
125
+ const resolvedContentAnimation = contentAnimation ?? safePreset.content;
126
+
127
+ const resolvedPressAnimation = pressAnimation ?? safePreset.press;
68
128
 
69
129
  const changeValue = useCallback(
70
130
  (nextValue) => {
71
- if (disabled) {
131
+ if (disabled || nextValue === currentValue) {
72
132
  return;
73
133
  }
74
134
 
135
+ setPreviousValue(currentValue);
136
+
75
137
  if (!controlled) {
76
138
  setInternalValue(nextValue);
77
139
  }
78
140
 
79
141
  onValueChange?.(nextValue);
80
142
  },
81
- [disabled, controlled, onValueChange],
143
+ [disabled, currentValue, controlled, onValueChange],
82
144
  );
83
145
 
84
146
  const contextValue = useMemo(
@@ -87,6 +149,8 @@ export const UITabs = memo(function UITabs({
87
149
 
88
150
  value: currentValue,
89
151
 
152
+ previousValue,
153
+
90
154
  setValue: changeValue,
91
155
 
92
156
  size: safeSize,
@@ -95,11 +159,21 @@ export const UITabs = memo(function UITabs({
95
159
 
96
160
  animationDuration,
97
161
 
162
+ spring,
163
+
164
+ directionAware,
165
+
166
+ indicatorAnimation: animated ? resolvedIndicatorAnimation : "none",
167
+
168
+ contentAnimation: animated ? resolvedContentAnimation : "none",
169
+
170
+ pressAnimation: animated ? resolvedPressAnimation : "none",
171
+
98
172
  variant,
99
173
 
100
174
  activeColor: activeColor || colors.primary || "#FF5A1F",
101
175
 
102
- inactiveColor: inactiveColor || colors.textSecondary || "#525252",
176
+ inactiveColor: inactiveColor || colors.textSecondary || "#737373",
103
177
 
104
178
  indicatorColor:
105
179
  indicatorColor || activeColor || colors.primary || "#FF5A1F",
@@ -113,10 +187,16 @@ export const UITabs = memo(function UITabs({
113
187
  [
114
188
  colors,
115
189
  currentValue,
190
+ previousValue,
116
191
  changeValue,
117
192
  safeSize,
118
193
  animated,
119
194
  animationDuration,
195
+ spring,
196
+ directionAware,
197
+ resolvedIndicatorAnimation,
198
+ resolvedContentAnimation,
199
+ resolvedPressAnimation,
120
200
  variant,
121
201
  activeColor,
122
202
  inactiveColor,
@@ -128,31 +208,28 @@ export const UITabs = memo(function UITabs({
128
208
  );
129
209
 
130
210
  return (
131
- <TabsContext.Provider value={contextValue}>
132
- <View testID={testID} style={[styles.container, containerStyle]}>
133
- {Children.map(children, (child) => {
134
- if (!isValidElement(child)) {
135
- return child;
136
- }
137
-
138
- return child;
139
- })}
211
+ <UITabsContext.Provider value={contextValue}>
212
+ <View testID={testID} style={[styles.container, containerStyle, style]}>
213
+ {children}
140
214
  </View>
141
- </TabsContext.Provider>
215
+ </UITabsContext.Provider>
142
216
  );
143
217
  });
144
218
 
145
219
  export function useUITabs() {
146
- const context = useContext(TabsContext);
220
+ const context = useContext(UITabsContext);
147
221
 
148
222
  if (!context) {
149
- throw new Error("UITabs components must be used inside UITabs.");
223
+ throw new Error("UITabs components must be used inside <UITabs>.");
150
224
  }
151
225
 
152
226
  return context;
153
227
  }
154
228
 
155
- export const UITabsSizes = UI_TABS_SIZES;
229
+ export {
230
+ TAB_SIZES as UITabsSizes,
231
+ ANIMATION_PRESETS as UITabsAnimationPresets,
232
+ };
156
233
 
157
234
  const styles = StyleSheet.create({
158
235
  container: {
@@ -161,10 +238,3 @@ const styles = StyleSheet.create({
161
238
  });
162
239
 
163
240
  UITabs.displayName = "UITabs";
164
- UITabs.List = UITabsList;
165
-
166
- UITabs.Trigger = UITabsTrigger;
167
-
168
- UITabs.Content = UITabsContent;
169
-
170
- UITabs.Indicator = UITabsIndicator;
@@ -1,6 +1,6 @@
1
1
  import React, { memo, useEffect, useRef, useState } from "react";
2
2
 
3
- import { Animated, View } from "react-native";
3
+ import { Animated, StyleSheet } from "react-native";
4
4
 
5
5
  import { useUITabs } from "./UITabs";
6
6
 
@@ -11,6 +11,7 @@ export const UITabsContent = memo(function UITabsContent({
11
11
 
12
12
  style,
13
13
 
14
+ animation,
14
15
  animationDuration,
15
16
 
16
17
  testID,
@@ -19,96 +20,81 @@ export const UITabsContent = memo(function UITabsContent({
19
20
 
20
21
  const active = tabs.value === value;
21
22
 
23
+ const animationType = animation ?? tabs.contentAnimation;
24
+
22
25
  const [mounted, setMounted] = useState(active);
23
26
 
24
- const opacity = useRef(new Animated.Value(active ? 1 : 0)).current;
27
+ const progress = useRef(new Animated.Value(active ? 1 : 0)).current;
25
28
 
26
- const translateY = useRef(new Animated.Value(active ? 0 : 5)).current;
29
+ const direction = getDirection(tabs, value);
27
30
 
28
31
  useEffect(() => {
32
+ const duration = animationDuration ?? tabs.animationDuration;
33
+
29
34
  if (active) {
30
35
  setMounted(true);
31
36
 
32
- if (!tabs.animated) {
33
- opacity.setValue(1);
34
- translateY.setValue(0);
37
+ progress.setValue(0);
38
+
39
+ if (animationType === "none") {
40
+ progress.setValue(1);
35
41
 
36
42
  return;
37
43
  }
38
44
 
39
- Animated.parallel([
40
- Animated.timing(opacity, {
41
- toValue: 1,
42
-
43
- duration: animationDuration ?? tabs.animationDuration,
44
-
45
- useNativeDriver: true,
46
- }),
47
-
48
- Animated.timing(translateY, {
49
- toValue: 0,
45
+ Animated.timing(progress, {
46
+ toValue: 1,
50
47
 
51
- duration: animationDuration ?? tabs.animationDuration,
48
+ duration,
52
49
 
53
- useNativeDriver: true,
54
- }),
55
- ]).start();
50
+ useNativeDriver: true,
51
+ }).start();
56
52
 
57
53
  return;
58
54
  }
59
55
 
60
- if (!tabs.animated) {
61
- opacity.setValue(0);
56
+ if (animationType === "none") {
57
+ progress.setValue(0);
58
+
62
59
  setMounted(false);
63
60
 
64
61
  return;
65
62
  }
66
63
 
67
- Animated.parallel([
68
- Animated.timing(opacity, {
69
- toValue: 0,
70
-
71
- duration: animationDuration ?? tabs.animationDuration,
72
-
73
- useNativeDriver: true,
74
- }),
75
-
76
- Animated.timing(translateY, {
77
- toValue: 5,
64
+ Animated.timing(progress, {
65
+ toValue: 0,
78
66
 
79
- duration: animationDuration ?? tabs.animationDuration,
67
+ duration,
80
68
 
81
- useNativeDriver: true,
82
- }),
83
- ]).start(() => {
69
+ useNativeDriver: true,
70
+ }).start(() => {
84
71
  setMounted(false);
85
72
  });
86
73
  }, [
87
74
  active,
88
- tabs.animated,
89
- tabs.animationDuration,
75
+ animationType,
90
76
  animationDuration,
91
- opacity,
92
- translateY,
77
+ tabs.animationDuration,
78
+ progress,
93
79
  ]);
94
80
 
95
81
  if (!mounted) {
96
82
  return null;
97
83
  }
98
84
 
85
+ const transforms = getContentTransforms(animationType, progress, direction);
86
+
99
87
  return (
100
88
  <Animated.View
101
89
  testID={testID}
102
90
  accessibilityRole="tabpanel"
103
91
  style={[
92
+ styles.container,
93
+
104
94
  {
105
- opacity,
95
+ opacity: getOpacity(animationType, progress),
106
96
 
107
- transform: [
108
- {
109
- translateY,
110
- },
111
- ],
97
+ transform: transforms,
112
98
  },
113
99
 
114
100
  style,
@@ -119,4 +105,86 @@ export const UITabsContent = memo(function UITabsContent({
119
105
  );
120
106
  });
121
107
 
108
+ function getDirection(tabs, value) {
109
+ if (!tabs.directionAware) {
110
+ return 1;
111
+ }
112
+
113
+ if (tabs.previousValue === null || tabs.previousValue === undefined) {
114
+ return 1;
115
+ }
116
+
117
+ return String(value) > String(tabs.previousValue) ? 1 : -1;
118
+ }
119
+
120
+ function getOpacity(type, progress) {
121
+ if (type === "slide" || type === "scale") {
122
+ return 1;
123
+ }
124
+
125
+ return progress;
126
+ }
127
+
128
+ function getContentTransforms(type, progress, direction) {
129
+ switch (type) {
130
+ case "slide":
131
+ return [
132
+ {
133
+ translateX: progress.interpolate({
134
+ inputRange: [0, 1],
135
+ outputRange: [24 * direction, 0],
136
+ }),
137
+ },
138
+ ];
139
+
140
+ case "fadeSlide":
141
+ case "slideFade":
142
+ return [
143
+ {
144
+ translateX: progress.interpolate({
145
+ inputRange: [0, 1],
146
+ outputRange: [20 * direction, 0],
147
+ }),
148
+ },
149
+ ];
150
+
151
+ case "scale":
152
+ return [
153
+ {
154
+ scale: progress.interpolate({
155
+ inputRange: [0, 1],
156
+ outputRange: [0.96, 1],
157
+ }),
158
+ },
159
+ ];
160
+
161
+ case "slideScale":
162
+ case "springScale":
163
+ return [
164
+ {
165
+ translateX: progress.interpolate({
166
+ inputRange: [0, 1],
167
+ outputRange: [18 * direction, 0],
168
+ }),
169
+ },
170
+ {
171
+ scale: progress.interpolate({
172
+ inputRange: [0, 1],
173
+ outputRange: [0.97, 1],
174
+ }),
175
+ },
176
+ ];
177
+
178
+ case "fade":
179
+ default:
180
+ return [];
181
+ }
182
+ }
183
+
184
+ const styles = StyleSheet.create({
185
+ container: {
186
+ width: "100%",
187
+ },
188
+ });
189
+
122
190
  UITabsContent.displayName = "UITabs.Content";
@@ -5,8 +5,9 @@ import { View } from "react-native";
5
5
  import { useUITabs } from "./UITabs";
6
6
 
7
7
  export const UITabsIndicator = memo(function UITabsIndicator({
8
- children,
9
8
  style,
9
+
10
+ children,
10
11
  }) {
11
12
  const tabs = useUITabs();
12
13
 
@@ -15,19 +15,25 @@ export const UITabsList = memo(function UITabsList({
15
15
 
16
16
  style,
17
17
 
18
+ backgroundColor,
19
+
20
+ borderBottomWidth = 0,
21
+
22
+ borderBottomColor,
23
+
18
24
  testID,
19
25
  }) {
20
26
  const tabs = useUITabs();
21
27
 
22
- const sizePadding = tabs.size === "sm" ? 8 : tabs.size === "lg" ? 16 : 12;
28
+ const horizontalPadding =
29
+ tabs.size === "sm" ? 6 : tabs.size === "lg" ? 14 : 10;
23
30
 
24
- const content = (
31
+ const row = (
25
32
  <View
26
33
  style={[
27
34
  styles.row,
28
-
29
35
  {
30
- paddingHorizontal: sizePadding,
36
+ paddingHorizontal: horizontalPadding,
31
37
  },
32
38
  ]}
33
39
  >
@@ -35,25 +41,39 @@ export const UITabsList = memo(function UITabsList({
35
41
  </View>
36
42
  );
37
43
 
44
+ const containerStyle = [
45
+ styles.container,
46
+
47
+ {
48
+ backgroundColor: backgroundColor || "transparent",
49
+
50
+ borderBottomWidth,
51
+
52
+ borderBottomColor: borderBottomColor || tabs.colors.border || "#E5E5E5",
53
+ },
54
+
55
+ style,
56
+ ];
57
+
38
58
  if (scrollable) {
39
59
  return (
40
60
  <ScrollView
41
61
  testID={testID}
42
62
  horizontal
43
63
  showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}
44
- bounces
45
64
  keyboardShouldPersistTaps="handled"
65
+ bounces
46
66
  contentContainerStyle={[styles.scrollContent, contentContainerStyle]}
47
- style={style}
67
+ style={containerStyle}
48
68
  >
49
- {content}
69
+ {row}
50
70
  </ScrollView>
51
71
  );
52
72
  }
53
73
 
54
74
  return (
55
- <View testID={testID} style={[styles.container, style]}>
56
- {content}
75
+ <View testID={testID} style={containerStyle}>
76
+ {row}
57
77
  </View>
58
78
  );
59
79
  });
@@ -61,7 +81,6 @@ export const UITabsList = memo(function UITabsList({
61
81
  const styles = StyleSheet.create({
62
82
  container: {
63
83
  width: "100%",
64
- overflow: "hidden",
65
84
  },
66
85
 
67
86
  scrollContent: {
@@ -22,13 +22,26 @@ export const UITabsTrigger = memo(function UITabsTrigger({
22
22
  inactiveColor,
23
23
 
24
24
  style,
25
+ activeStyle,
26
+ inactiveStyle,
27
+
25
28
  contentStyle,
29
+
26
30
  textStyle,
31
+ activeTextStyle,
32
+ inactiveTextStyle,
33
+
27
34
  iconStyle,
35
+
28
36
  badgeStyle,
37
+ badgeTextStyle,
29
38
 
30
- testID,
39
+ pressAnimation,
40
+ pressScale = 0.96,
41
+
42
+ indicator = true,
31
43
 
44
+ testID,
32
45
  accessibilityLabel,
33
46
  }) {
34
47
  const tabs = useUITabs();
@@ -37,23 +50,45 @@ export const UITabsTrigger = memo(function UITabsTrigger({
37
50
 
38
51
  const isDisabled = disabled || tabs.disabled;
39
52
 
40
- const progress = useRef(new Animated.Value(active ? 1 : 0)).current;
53
+ const scale = useRef(new Animated.Value(1)).current;
54
+
55
+ const resolvedPressAnimation = pressAnimation ?? tabs.pressAnimation;
41
56
 
42
57
  useEffect(() => {
43
- if (!tabs.animated) {
44
- progress.setValue(active ? 1 : 0);
58
+ if (resolvedPressAnimation === "none") {
59
+ scale.setValue(1);
60
+ }
61
+ }, [resolvedPressAnimation, scale]);
45
62
 
63
+ const animatePressIn = () => {
64
+ if (resolvedPressAnimation === "none") {
46
65
  return;
47
66
  }
48
67
 
49
- Animated.timing(progress, {
50
- toValue: active ? 1 : 0,
68
+ if (resolvedPressAnimation === "scale") {
69
+ Animated.spring(scale, {
70
+ toValue: pressScale,
51
71
 
52
- duration: tabs.animationDuration,
72
+ ...tabs.spring,
73
+
74
+ useNativeDriver: true,
75
+ }).start();
76
+ }
77
+ };
78
+
79
+ const animatePressOut = () => {
80
+ if (resolvedPressAnimation === "none") {
81
+ return;
82
+ }
83
+
84
+ Animated.spring(scale, {
85
+ toValue: 1,
86
+
87
+ ...tabs.spring,
53
88
 
54
89
  useNativeDriver: true,
55
90
  }).start();
56
- }, [active, tabs.animated, tabs.animationDuration, progress]);
91
+ };
57
92
 
58
93
  const handlePress = (event) => {
59
94
  if (isDisabled) {
@@ -65,9 +100,9 @@ export const UITabsTrigger = memo(function UITabsTrigger({
65
100
  onPress?.(event);
66
101
  };
67
102
 
68
- const resolvedActiveColor = activeColor || tabs.activeColor;
103
+ const activeTextColor = activeColor || tabs.activeColor;
69
104
 
70
- const resolvedInactiveColor = inactiveColor || tabs.inactiveColor;
105
+ const inactiveTextColor = inactiveColor || tabs.inactiveColor;
71
106
 
72
107
  const iconElement = icon ? (
73
108
  <View
@@ -83,124 +118,189 @@ export const UITabsTrigger = memo(function UITabsTrigger({
83
118
  </View>
84
119
  ) : null;
85
120
 
86
- const textElement = (
87
- <Text
88
- numberOfLines={1}
121
+ return (
122
+ <Animated.View
89
123
  style={[
90
- styles.text,
124
+ styles.animatedWrapper,
91
125
 
92
126
  {
93
- fontSize: tabs.size === "sm" ? 12 : tabs.size === "lg" ? 16 : 14,
94
-
95
- color: active ? resolvedActiveColor : resolvedInactiveColor,
96
-
97
- opacity: isDisabled ? 0.45 : 1,
127
+ transform: [
128
+ {
129
+ scale,
130
+ },
131
+ ],
98
132
  },
99
-
100
- textStyle,
101
133
  ]}
102
134
  >
103
- {children}
104
- </Text>
105
- );
106
-
107
- return (
108
- <Pressable
109
- testID={testID}
110
- disabled={isDisabled}
111
- onPress={handlePress}
112
- accessibilityRole="tab"
113
- accessibilityState={{
114
- selected: active,
115
- disabled: isDisabled,
116
- }}
117
- accessibilityLabel={accessibilityLabel}
118
- style={({ pressed }) => [
119
- styles.trigger,
135
+ <Pressable
136
+ testID={testID}
137
+ disabled={isDisabled}
138
+ onPress={handlePress}
139
+ onPressIn={animatePressIn}
140
+ onPressOut={animatePressOut}
141
+ accessibilityRole="tab"
142
+ accessibilityState={{
143
+ selected: active,
144
+ disabled: isDisabled,
145
+ }}
146
+ accessibilityLabel={accessibilityLabel}
147
+ style={[
148
+ styles.trigger,
120
149
 
121
- {
122
- minHeight: tabs.size === "sm" ? 42 : tabs.size === "lg" ? 56 : 48,
150
+ {
151
+ minHeight: tabs.size === "sm" ? 42 : tabs.size === "lg" ? 58 : 50,
123
152
 
124
- opacity: pressed ? 0.7 : 1,
125
- },
153
+ opacity: isDisabled ? 0.45 : 1,
154
+ },
126
155
 
127
- style,
128
- ]}
129
- >
130
- <View
131
- style={[
132
- styles.content,
156
+ inactiveStyle,
133
157
 
134
- iconPosition === "right" ? styles.contentRight : null,
158
+ active ? activeStyle : null,
135
159
 
136
- contentStyle,
160
+ style,
137
161
  ]}
138
162
  >
139
- {iconPosition === "left" ? iconElement : null}
140
-
141
- {textElement}
142
-
143
- {iconPosition === "right" ? iconElement : null}
163
+ <View style={[styles.content, contentStyle]}>
164
+ {iconPosition === "left" ? iconElement : null}
144
165
 
145
- {badge !== undefined ? (
146
- <View
166
+ <Text
167
+ numberOfLines={1}
147
168
  style={[
148
- styles.badge,
169
+ styles.text,
149
170
 
150
171
  {
151
- backgroundColor: active
152
- ? resolvedActiveColor
153
- : tabs.colors.surfaceSecondary || "#F1F1F1",
172
+ fontSize:
173
+ tabs.size === "sm" ? 12 : tabs.size === "lg" ? 16 : 14,
174
+
175
+ color: active ? activeTextColor : inactiveTextColor,
154
176
  },
155
177
 
156
- badgeStyle,
178
+ textStyle,
179
+
180
+ inactiveTextStyle,
181
+
182
+ active ? activeTextStyle : null,
157
183
  ]}
158
184
  >
159
- <Text
185
+ {children}
186
+ </Text>
187
+
188
+ {iconPosition === "right" ? iconElement : null}
189
+
190
+ {badge !== undefined ? (
191
+ <View
160
192
  style={[
161
- styles.badgeText,
193
+ styles.badge,
162
194
 
163
195
  {
164
- color: active
165
- ? tabs.colors.onPrimary || "#FFFFFF"
166
- : tabs.colors.textSecondary || "#525252",
196
+ backgroundColor: active
197
+ ? activeTextColor
198
+ : tabs.colors.surfaceSecondary || "#EEEEEE",
167
199
  },
200
+
201
+ badgeStyle,
168
202
  ]}
169
203
  >
170
- {badge}
171
- </Text>
204
+ <Text
205
+ style={[
206
+ styles.badgeText,
207
+
208
+ {
209
+ color: active
210
+ ? tabs.colors.onPrimary || "#FFFFFF"
211
+ : tabs.colors.textSecondary || "#525252",
212
+ },
213
+
214
+ badgeTextStyle,
215
+ ]}
216
+ >
217
+ {badge}
218
+ </Text>
219
+ </View>
220
+ ) : null}
221
+ </View>
222
+
223
+ {indicator ? (
224
+ <View style={styles.indicatorContainer}>
225
+ <AnimatedIndicator active={active} />
172
226
  </View>
173
227
  ) : null}
174
- </View>
228
+ </Pressable>
229
+ </Animated.View>
230
+ );
231
+ });
175
232
 
176
- <Animated.View
177
- style={[
178
- styles.indicator,
233
+ function AnimatedIndicator({ active }) {
234
+ const tabs = useUITabs();
179
235
 
180
- {
181
- backgroundColor: tabs.indicatorColor,
236
+ const progress = useRef(new Animated.Value(active ? 1 : 0)).current;
237
+
238
+ useEffect(() => {
239
+ if (tabs.indicatorAnimation === "none") {
240
+ progress.setValue(active ? 1 : 0);
182
241
 
183
- height: tabs.indicatorHeight,
242
+ return;
243
+ }
184
244
 
185
- opacity: progress,
245
+ if (tabs.indicatorAnimation === "spring") {
246
+ Animated.spring(progress, {
247
+ toValue: active ? 1 : 0,
186
248
 
187
- transform: [
188
- {
189
- scaleX: progress,
190
- },
191
- ],
192
- },
249
+ ...tabs.spring,
193
250
 
194
- tabs.indicatorWidth === "full"
195
- ? styles.indicatorFull
196
- : styles.indicatorContent,
197
- ]}
198
- />
199
- </Pressable>
251
+ useNativeDriver: true,
252
+ }).start();
253
+
254
+ return;
255
+ }
256
+
257
+ Animated.timing(progress, {
258
+ toValue: active ? 1 : 0,
259
+
260
+ duration: tabs.animationDuration,
261
+
262
+ useNativeDriver: true,
263
+ }).start();
264
+ }, [
265
+ active,
266
+ tabs.indicatorAnimation,
267
+ tabs.animationDuration,
268
+ tabs.spring,
269
+ progress,
270
+ ]);
271
+
272
+ return (
273
+ <Animated.View
274
+ style={[
275
+ styles.indicator,
276
+
277
+ {
278
+ height: tabs.indicatorHeight,
279
+
280
+ backgroundColor: tabs.indicatorColor,
281
+
282
+ opacity: progress,
283
+
284
+ transform: [
285
+ {
286
+ scaleX: progress,
287
+ },
288
+ ],
289
+ },
290
+
291
+ tabs.indicatorWidth === "full"
292
+ ? styles.indicatorFull
293
+ : styles.indicatorContent,
294
+ ]}
295
+ />
200
296
  );
201
- });
297
+ }
202
298
 
203
299
  const styles = StyleSheet.create({
300
+ animatedWrapper: {
301
+ flexShrink: 0,
302
+ },
303
+
204
304
  trigger: {
205
305
  position: "relative",
206
306
 
@@ -219,8 +319,10 @@ const styles = StyleSheet.create({
219
319
  gap: 6,
220
320
  },
221
321
 
222
- contentRight: {
223
- flexDirection: "row",
322
+ text: {
323
+ fontWeight: "600",
324
+
325
+ includeFontPadding: false,
224
326
  },
225
327
 
226
328
  icon: {
@@ -237,12 +339,6 @@ const styles = StyleSheet.create({
237
339
  marginLeft: 2,
238
340
  },
239
341
 
240
- text: {
241
- fontWeight: "600",
242
-
243
- includeFontPadding: false,
244
- },
245
-
246
342
  badge: {
247
343
  minWidth: 18,
248
344
  height: 18,
@@ -264,14 +360,18 @@ const styles = StyleSheet.create({
264
360
  includeFontPadding: false,
265
361
  },
266
362
 
267
- indicator: {
363
+ indicatorContainer: {
268
364
  position: "absolute",
269
365
 
366
+ left: 0,
367
+ right: 0,
270
368
  bottom: 0,
369
+ },
271
370
 
272
- left: 0,
371
+ indicator: {
372
+ position: "absolute",
273
373
 
274
- right: 0,
374
+ bottom: 0,
275
375
  },
276
376
 
277
377
  indicatorFull: {
@@ -1,9 +1,16 @@
1
- export { UITabs, UITabsSizes, useUITabs } from "./UITabs";
1
+ import { UITabs } from "./UITabs";
2
2
 
3
- export { UITabsList } from "./UITabsList";
3
+ import { UITabsList } from "./UITabsList";
4
+ import { UITabsTrigger } from "./UITabsTrigger";
5
+ import { UITabsContent } from "./UITabsContent";
6
+ import { UITabsIndicator } from "./UITabsIndicator";
4
7
 
5
- export { UITabsTrigger } from "./UITabsTrigger";
8
+ UITabs.List = UITabsList;
6
9
 
7
- export { UITabsContent } from "./UITabsContent";
10
+ UITabs.Trigger = UITabsTrigger;
8
11
 
9
- export { UITabsIndicator } from "./UITabsIndicator";
12
+ UITabs.Content = UITabsContent;
13
+
14
+ UITabs.Indicator = UITabsIndicator;
15
+
16
+ export { UITabs, UITabsList, UITabsTrigger, UITabsContent, UITabsIndicator };
@@ -118,8 +118,6 @@ export {
118
118
  } from "./Accordion";
119
119
  export {
120
120
  UITabs,
121
- UITabsSizes,
122
- useUITabs,
123
121
  UITabsList,
124
122
  UITabsTrigger,
125
123
  UITabsContent,
package/src/index.js CHANGED
@@ -126,8 +126,6 @@ export {
126
126
  UIAccordionContent,
127
127
  UIAccordionIcon,
128
128
  UITabs,
129
- UITabsSizes,
130
- useUITabs,
131
129
  UITabsList,
132
130
  UITabsTrigger,
133
131
  UITabsContent,