rnnovaui 0.9.13 → 0.9.15

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.13",
3
+ "version": "0.9.15",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -1,5 +1,7 @@
1
1
  import React, {
2
+ Children,
2
3
  createContext,
4
+ isValidElement,
3
5
  memo,
4
6
  useCallback,
5
7
  useContext,
@@ -10,52 +12,19 @@ import React, {
10
12
  import { StyleSheet, View } from "react-native";
11
13
 
12
14
  import { useUITheme } from "../../theme";
15
+ import { UIAccordionItem } from "./UIAccordionItem";
16
+ import { UIAccordionHeader } from "./UIAccordionHeader";
17
+ import { UIAccordionTrigger } from "./UIAccordionTrigger";
18
+ import { UIAccordionContent } from "./UIAccordionContent";
19
+ import { UIAccordionIcon } from "./UIAccordionIcon";
13
20
 
14
21
  const AccordionContext = createContext(null);
15
22
 
16
- const ANIMATION_PRESETS = {
17
- none: {
18
- content: "none",
19
- icon: "none",
20
- press: "none",
21
- },
22
-
23
- subtle: {
24
- content: "fade",
25
- icon: "rotate",
26
- press: "scale",
27
- },
28
-
29
- smooth: {
30
- content: "fadeExpand",
31
- icon: "rotate",
32
- press: "scale",
33
- },
34
-
35
- snappy: {
36
- content: "expand",
37
- icon: "rotate",
38
- press: "scale",
39
- },
40
-
41
- spring: {
42
- content: "springExpand",
43
- icon: "springRotate",
44
- press: "scale",
45
- },
46
-
47
- bouncy: {
48
- content: "springFadeExpand",
49
- icon: "springRotate",
50
- press: "scale",
51
- },
52
- };
53
-
54
23
  export const UIAccordion = memo(function UIAccordion({
55
24
  children,
56
25
 
57
26
  value,
58
- defaultValue = null,
27
+ defaultValue,
59
28
 
60
29
  multiple = false,
61
30
 
@@ -64,37 +33,15 @@ export const UIAccordion = memo(function UIAccordion({
64
33
  disabled = false,
65
34
 
66
35
  animated = true,
67
-
68
- animationPreset = "smooth",
69
-
70
- contentAnimation,
71
- iconAnimation,
72
- pressAnimation,
73
-
74
- animationDuration = 260,
75
-
76
- spring = {
77
- damping: 18,
78
- stiffness: 180,
79
- mass: 0.8,
80
- },
36
+ animationDuration = 220,
81
37
 
82
38
  variant = "default",
83
-
84
39
  size = "md",
85
40
 
86
41
  separator = true,
87
42
 
88
- border = false,
89
-
90
- radius = 12,
91
-
92
- backgroundColor,
93
-
94
- borderColor,
95
-
96
- style,
97
43
  containerStyle,
44
+ itemStyle,
98
45
 
99
46
  testID,
100
47
  }) {
@@ -102,8 +49,6 @@ export const UIAccordion = memo(function UIAccordion({
102
49
 
103
50
  const colors = theme?.colors || {};
104
51
 
105
- const controlled = value !== undefined;
106
-
107
52
  const normalizeValue = useCallback(
108
53
  (input) => {
109
54
  if (multiple) {
@@ -111,7 +56,7 @@ export const UIAccordion = memo(function UIAccordion({
111
56
  return input;
112
57
  }
113
58
 
114
- if (input === null || input === undefined) {
59
+ if (input === undefined || input === null) {
115
60
  return [];
116
61
  }
117
62
 
@@ -127,26 +72,14 @@ export const UIAccordion = memo(function UIAccordion({
127
72
  [multiple],
128
73
  );
129
74
 
75
+ const controlled = value !== undefined;
76
+
130
77
  const [internalValue, setInternalValue] = useState(() =>
131
78
  normalizeValue(defaultValue),
132
79
  );
133
80
 
134
81
  const currentValue = controlled ? normalizeValue(value) : internalValue;
135
82
 
136
- const preset = ANIMATION_PRESETS[animationPreset] || ANIMATION_PRESETS.smooth;
137
-
138
- const resolvedContentAnimation = animated
139
- ? (contentAnimation ?? preset.content)
140
- : "none";
141
-
142
- const resolvedIconAnimation = animated
143
- ? (iconAnimation ?? preset.icon)
144
- : "none";
145
-
146
- const resolvedPressAnimation = animated
147
- ? (pressAnimation ?? preset.press)
148
- : "none";
149
-
150
83
  const isOpen = useCallback(
151
84
  (itemValue) => {
152
85
  if (multiple) {
@@ -191,36 +124,21 @@ export const UIAccordion = memo(function UIAccordion({
191
124
  () => ({
192
125
  colors,
193
126
 
194
- value: currentValue,
127
+ currentValue,
195
128
 
196
129
  multiple,
197
130
 
198
131
  disabled,
199
132
 
200
- size,
201
-
202
133
  animated,
203
-
204
134
  animationDuration,
205
135
 
206
- spring,
207
-
208
- contentAnimation: resolvedContentAnimation,
209
-
210
- iconAnimation: resolvedIconAnimation,
211
-
212
- pressAnimation: resolvedPressAnimation,
213
-
214
136
  variant,
137
+ size,
215
138
 
216
139
  separator,
217
140
 
218
- border,
219
-
220
- radius,
221
-
222
141
  isOpen,
223
-
224
142
  toggle,
225
143
  }),
226
144
  [
@@ -228,17 +146,11 @@ export const UIAccordion = memo(function UIAccordion({
228
146
  currentValue,
229
147
  multiple,
230
148
  disabled,
231
- size,
232
149
  animated,
233
150
  animationDuration,
234
- spring,
235
- resolvedContentAnimation,
236
- resolvedIconAnimation,
237
- resolvedPressAnimation,
238
151
  variant,
152
+ size,
239
153
  separator,
240
- border,
241
- radius,
242
154
  isOpen,
243
155
  toggle,
244
156
  ],
@@ -252,23 +164,25 @@ export const UIAccordion = memo(function UIAccordion({
252
164
  styles.container,
253
165
 
254
166
  {
255
- backgroundColor:
256
- backgroundColor || colors.card || colors.surface || "transparent",
257
-
258
- borderColor: borderColor || colors.border || "#E5E5E5",
167
+ backgroundColor: colors.card || colors.background || "transparent",
259
168
 
260
- borderRadius: radius,
169
+ borderColor: colors.border || "#E5E5E5",
261
170
  },
262
171
 
263
- border ? styles.border : null,
264
-
265
- variant === "outlined" ? styles.border : null,
172
+ variant === "outlined" ? styles.outlined : null,
266
173
 
267
174
  containerStyle,
268
- style,
269
175
  ]}
270
176
  >
271
- {children}
177
+ {Children.map(children, (child) => {
178
+ if (!isValidElement(child)) {
179
+ return child;
180
+ }
181
+
182
+ return React.cloneElement(child, {
183
+ style: [itemStyle, child.props.style],
184
+ });
185
+ })}
272
186
  </View>
273
187
  </AccordionContext.Provider>
274
188
  );
@@ -278,24 +192,31 @@ export function useUIAccordion() {
278
192
  const context = useContext(AccordionContext);
279
193
 
280
194
  if (!context) {
281
- throw new Error("Accordion components must be used inside <UIAccordion>.");
195
+ throw new Error("Accordion components must be used inside UIAccordion.");
282
196
  }
283
197
 
284
198
  return context;
285
199
  }
286
200
 
287
- export const UIAccordionAnimationPresets = ANIMATION_PRESETS;
288
-
289
201
  const styles = StyleSheet.create({
290
202
  container: {
291
203
  width: "100%",
292
-
293
204
  overflow: "hidden",
294
205
  },
295
206
 
296
- border: {
207
+ outlined: {
297
208
  borderWidth: 1,
209
+ borderRadius: 12,
298
210
  },
299
211
  });
300
212
 
301
213
  UIAccordion.displayName = "UIAccordion";
214
+ UIAccordion.Item = UIAccordionItem;
215
+
216
+ UIAccordion.Header = UIAccordionHeader;
217
+
218
+ UIAccordion.Trigger = UIAccordionTrigger;
219
+
220
+ UIAccordion.Content = UIAccordionContent;
221
+
222
+ UIAccordion.Icon = UIAccordionIcon;
@@ -3,183 +3,163 @@ 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({
10
9
  children,
11
-
12
- animation,
13
-
14
- animationDuration,
15
-
16
10
  style,
17
-
18
11
  innerStyle,
19
-
20
- contentStyle,
21
-
22
12
  testID,
23
13
  }) {
24
14
  const accordion = useUIAccordion();
25
-
26
15
  const item = useUIAccordionItem();
27
16
 
28
- const animationType = animation ?? accordion.contentAnimation;
29
-
30
- const duration = animationDuration ?? accordion.animationDuration;
17
+ const [contentHeight, setContentHeight] = useState(0);
31
18
 
32
- const [mounted, setMounted] = useState(item.open);
19
+ const animation = useRef(new Animated.Value(item.open ? 1 : 0)).current;
33
20
 
34
- const progress = useRef(new Animated.Value(item.open ? 1 : 0)).current;
21
+ /*
22
+ * ------------------------------------------------
23
+ * MEASURE REAL CONTENT
24
+ * ------------------------------------------------
25
+ */
35
26
 
36
- const measuredHeight = useRef(0);
27
+ const handleMeasureLayout = (event) => {
28
+ const height = Math.ceil(event.nativeEvent.layout.height);
37
29
 
38
- const handleLayout = (event) => {
39
- const height = event.nativeEvent.layout.height;
40
-
41
- if (height > measuredHeight.current) {
42
- measuredHeight.current = height;
30
+ if (height > 0 && height !== contentHeight) {
31
+ setContentHeight(height);
43
32
  }
44
33
  };
45
34
 
46
- useEffect(() => {
47
- if (item.open) {
48
- setMounted(true);
49
-
50
- if (animationType === "none" || !accordion.animated) {
51
- progress.setValue(1);
52
- return;
53
- }
35
+ /*
36
+ * ------------------------------------------------
37
+ * OPEN / CLOSE
38
+ * ------------------------------------------------
39
+ */
54
40
 
55
- progress.setValue(0);
56
-
57
- if (
58
- animationType === "springExpand" ||
59
- animationType === "springFadeExpand"
60
- ) {
61
- Animated.spring(progress, {
62
- toValue: 1,
63
-
64
- ...accordion.spring,
65
-
66
- useNativeDriver: false,
67
- }).start();
68
-
69
- return;
70
- }
71
-
72
- Animated.timing(progress, {
73
- toValue: 1,
74
-
75
- duration,
76
-
77
- useNativeDriver: false,
78
- }).start();
79
-
80
- return;
81
- }
82
-
83
- if (animationType === "none" || !accordion.animated) {
84
- progress.setValue(0);
85
- setMounted(false);
41
+ useEffect(() => {
42
+ if (contentHeight <= 0) {
86
43
  return;
87
44
  }
88
45
 
89
- const finish = () => {
90
- setMounted(false);
91
- };
92
-
93
- if (
94
- animationType === "springExpand" ||
95
- animationType === "springFadeExpand"
96
- ) {
97
- Animated.spring(progress, {
98
- toValue: 0,
99
-
100
- ...accordion.spring,
101
-
102
- useNativeDriver: false,
103
- }).start(finish);
46
+ if (!accordion.animated) {
47
+ animation.setValue(item.open ? 1 : 0);
104
48
 
105
49
  return;
106
50
  }
107
51
 
108
- Animated.timing(progress, {
109
- toValue: 0,
52
+ Animated.timing(animation, {
53
+ toValue: item.open ? 1 : 0,
110
54
 
111
- duration,
55
+ duration: accordion.animationDuration,
56
+
57
+ easing: undefined,
112
58
 
113
59
  useNativeDriver: false,
114
- }).start(finish);
60
+ }).start();
115
61
  }, [
116
62
  item.open,
117
- animationType,
63
+ contentHeight,
118
64
  accordion.animated,
119
- accordion.spring,
120
- duration,
121
- progress,
65
+ accordion.animationDuration,
66
+ animation,
122
67
  ]);
123
68
 
124
- if (!mounted) {
125
- return null;
126
- }
69
+ /*
70
+ * ------------------------------------------------
71
+ * ANIMATED HEIGHT
72
+ * ------------------------------------------------
73
+ */
74
+
75
+ const animatedHeight = animation.interpolate({
76
+ inputRange: [0, 1],
77
+
78
+ outputRange: [0, contentHeight],
127
79
 
128
- const shouldFade =
129
- animationType === "fade" ||
130
- animationType === "fadeExpand" ||
131
- animationType === "springFadeExpand";
80
+ extrapolate: "clamp",
81
+ });
132
82
 
133
- const shouldExpand =
134
- animationType === "expand" ||
135
- animationType === "fadeExpand" ||
136
- animationType === "springExpand" ||
137
- animationType === "springFadeExpand";
83
+ /*
84
+ * ------------------------------------------------
85
+ * MEASUREMENT CONTENT
86
+ *
87
+ * This is always rendered.
88
+ *
89
+ * It does NOT participate in normal layout.
90
+ * Therefore it cannot cause accordion shaking.
91
+ * ------------------------------------------------
92
+ */
138
93
 
139
94
  return (
140
- <Animated.View
141
- testID={testID}
142
- accessibilityRole="region"
143
- style={[
144
- styles.container,
145
-
146
- shouldFade
147
- ? {
148
- opacity: progress,
149
- }
150
- : null,
151
-
152
- shouldExpand
153
- ? {
154
- maxHeight: progress.interpolate({
155
- inputRange: [0, 1],
156
-
157
- outputRange: [0, Math.max(measuredHeight.current, 1)],
158
- }),
159
- }
160
- : null,
161
-
162
- style,
163
- ]}
164
- >
165
- <View
166
- onLayout={handleLayout}
167
- style={[styles.inner, innerStyle, contentStyle]}
168
- >
169
- {children}
95
+ <View style={styles.root}>
96
+ <View pointerEvents="none" style={styles.measureContainer}>
97
+ <View
98
+ style={[styles.measureContent, innerStyle]}
99
+ onLayout={handleMeasureLayout}
100
+ >
101
+ {children}
102
+ </View>
170
103
  </View>
171
- </Animated.View>
104
+
105
+ <Animated.View
106
+ testID={testID}
107
+ style={[
108
+ styles.animatedContainer,
109
+
110
+ {
111
+ height: animatedHeight,
112
+ },
113
+
114
+ style,
115
+ ]}
116
+ >
117
+ <View style={[styles.visibleContent, innerStyle]}>{children}</View>
118
+ </Animated.View>
119
+ </View>
172
120
  );
173
121
  });
174
122
 
175
123
  const styles = StyleSheet.create({
176
- container: {
124
+ root: {
125
+ width: "100%",
126
+ },
127
+
128
+ /*
129
+ * Hidden measurement layer.
130
+ *
131
+ * It remains available so onLayout can always
132
+ * calculate the real content height.
133
+ */
134
+ measureContainer: {
135
+ position: "absolute",
136
+
137
+ left: 0,
138
+ right: 0,
139
+
140
+ top: 0,
141
+
142
+ opacity: 0,
143
+
144
+ zIndex: -1,
145
+
146
+ pointerEvents: "none",
147
+ },
148
+
149
+ measureContent: {
150
+ width: "100%",
151
+ },
152
+
153
+ /*
154
+ * Visible animated layer.
155
+ */
156
+ animatedContainer: {
177
157
  width: "100%",
178
158
 
179
159
  overflow: "hidden",
180
160
  },
181
161
 
182
- inner: {
162
+ visibleContent: {
183
163
  width: "100%",
184
164
  },
185
165
  });
@@ -11,20 +11,12 @@ export const UIAccordionHeader = memo(function UIAccordionHeader({
11
11
 
12
12
  style,
13
13
 
14
- borderBottomWidth,
15
-
16
- borderBottomColor,
17
-
18
14
  testID,
19
15
  }) {
20
16
  const accordion = useUIAccordion();
21
17
 
22
18
  const item = useUIAccordionItem();
23
19
 
24
- const resolvedBorderWidth =
25
- borderBottomWidth ??
26
- (accordion.separator && !item.open ? StyleSheet.hairlineWidth : 0);
27
-
28
20
  return (
29
21
  <View
30
22
  testID={testID}
@@ -35,12 +27,11 @@ export const UIAccordionHeader = memo(function UIAccordionHeader({
35
27
  minHeight:
36
28
  accordion.size === "sm" ? 44 : accordion.size === "lg" ? 60 : 52,
37
29
 
38
- borderBottomWidth: resolvedBorderWidth,
39
-
40
- borderBottomColor:
41
- borderBottomColor || accordion.colors.border || "#E5E5E5",
30
+ borderBottomColor: accordion.colors.border || "#E5E5E5",
42
31
  },
43
32
 
33
+ accordion.separator && item.open ? styles.headerOpen : null,
34
+
44
35
  style,
45
36
  ]}
46
37
  >
@@ -55,6 +46,10 @@ const styles = StyleSheet.create({
55
46
 
56
47
  justifyContent: "center",
57
48
  },
49
+
50
+ headerOpen: {
51
+ borderBottomWidth: 1,
52
+ },
58
53
  });
59
54
 
60
55
  UIAccordionHeader.displayName = "UIAccordion.Header";
@@ -12,65 +12,87 @@ export const UIAccordionIcon = memo(function UIAccordionIcon({
12
12
  openIcon,
13
13
  closedIcon,
14
14
 
15
- animation,
16
-
17
- rotation = 180,
18
-
19
- size = 20,
15
+ size,
20
16
 
21
17
  color,
22
18
 
23
19
  style,
20
+
21
+ animated = true,
22
+
23
+ rotation = 180,
24
24
  }) {
25
25
  const accordion = useUIAccordion();
26
26
 
27
27
  const item = useUIAccordionItem();
28
28
 
29
- const animationType = animation ?? accordion.iconAnimation;
30
-
31
- const rotationValue = useRef(new Animated.Value(item.open ? 1 : 0)).current;
29
+ const animation = useRef(new Animated.Value(item.open ? 1 : 0)).current;
32
30
 
33
31
  useEffect(() => {
34
- if (animationType === "none") {
35
- rotationValue.setValue(item.open ? 1 : 0);
36
-
37
- return;
38
- }
39
-
40
- if (animationType === "springRotate") {
41
- Animated.spring(rotationValue, {
42
- toValue: item.open ? 1 : 0,
43
-
44
- ...accordion.spring,
45
-
46
- useNativeDriver: true,
47
- }).start();
32
+ if (!animated) {
33
+ animation.setValue(item.open ? 1 : 0);
48
34
 
49
35
  return;
50
36
  }
51
37
 
52
- Animated.timing(rotationValue, {
38
+ Animated.timing(animation, {
53
39
  toValue: item.open ? 1 : 0,
54
40
 
55
41
  duration: accordion.animationDuration,
56
42
 
57
43
  useNativeDriver: true,
58
44
  }).start();
59
- }, [
60
- item.open,
61
- animationType,
62
- accordion.animationDuration,
63
- accordion.spring,
64
- rotationValue,
65
- ]);
45
+ }, [item.open, animated, accordion.animationDuration, animation]);
66
46
 
67
- const rotate = rotationValue.interpolate({
68
- inputRange: [0, 1],
47
+ if (children || openIcon || closedIcon) {
48
+ const customIcon = item.open ? openIcon : closedIcon;
49
+
50
+ if (customIcon) {
51
+ return (
52
+ <Animated.View
53
+ style={[
54
+ {
55
+ transform: [
56
+ {
57
+ rotate: animation.interpolate({
58
+ inputRange: [0, 1],
59
+
60
+ outputRange: ["0deg", `${rotation}deg`],
61
+ }),
62
+ },
63
+ ],
64
+ },
69
65
 
70
- outputRange: ["0deg", `${rotation}deg`],
71
- });
66
+ style,
67
+ ]}
68
+ >
69
+ {customIcon}
70
+ </Animated.View>
71
+ );
72
+ }
72
73
 
73
- const customIcon = item.open ? openIcon : closedIcon;
74
+ return (
75
+ <Animated.View
76
+ style={[
77
+ {
78
+ transform: [
79
+ {
80
+ rotate: animation.interpolate({
81
+ inputRange: [0, 1],
82
+
83
+ outputRange: ["0deg", `${rotation}deg`],
84
+ }),
85
+ },
86
+ ],
87
+ },
88
+
89
+ style,
90
+ ]}
91
+ >
92
+ {children}
93
+ </Animated.View>
94
+ );
95
+ }
74
96
 
75
97
  return (
76
98
  <Animated.View
@@ -78,7 +100,11 @@ export const UIAccordionIcon = memo(function UIAccordionIcon({
78
100
  {
79
101
  transform: [
80
102
  {
81
- rotate,
103
+ rotate: animation.interpolate({
104
+ inputRange: [0, 1],
105
+
106
+ outputRange: ["0deg", `${rotation}deg`],
107
+ }),
82
108
  },
83
109
  ],
84
110
  },
@@ -86,25 +112,17 @@ export const UIAccordionIcon = memo(function UIAccordionIcon({
86
112
  style,
87
113
  ]}
88
114
  >
89
- {customIcon || children ? (
90
- customIcon || children
91
- ) : (
92
- <View>
93
- <Text
94
- style={{
95
- fontSize: size,
96
-
97
- lineHeight: size,
98
-
99
- color: color || accordion.colors.text || "#111111",
100
-
101
- fontWeight: "400",
102
- }}
103
- >
104
- ›
105
- </Text>
106
- </View>
107
- )}
115
+ <Text
116
+ style={{
117
+ fontSize: size || 20,
118
+
119
+ color: color || accordion.colors.text || "#111111",
120
+
121
+ lineHeight: size || 20,
122
+ }}
123
+ >
124
+ ›
125
+ </Text>
108
126
  </Animated.View>
109
127
  );
110
128
  });
@@ -15,30 +15,33 @@ export const UIAccordionItem = memo(function UIAccordionItem({
15
15
 
16
16
  style,
17
17
 
18
- activeStyle,
19
- inactiveStyle,
20
-
21
18
  testID,
22
19
  }) {
23
20
  const accordion = useUIAccordion();
24
21
 
25
22
  const open = accordion.isOpen(value);
26
23
 
27
- const itemDisabled = disabled || accordion.disabled;
28
-
29
- const contextValue = {
24
+ const itemContext = {
30
25
  value,
31
26
 
32
27
  open,
33
28
 
34
- disabled: itemDisabled,
29
+ disabled: disabled || accordion.disabled,
35
30
  };
36
31
 
37
32
  return (
38
- <AccordionItemContext.Provider value={contextValue}>
33
+ <AccordionItemContext.Provider value={itemContext}>
39
34
  <View
40
35
  testID={testID}
41
- style={[styles.item, inactiveStyle, open ? activeStyle : null, style]}
36
+ style={[
37
+ styles.item,
38
+
39
+ {
40
+ backgroundColor: accordion.colors.card || "transparent",
41
+ },
42
+
43
+ style,
44
+ ]}
42
45
  >
43
46
  {children}
44
47
  </View>
@@ -51,7 +54,7 @@ export function useUIAccordionItem() {
51
54
 
52
55
  if (!context) {
53
56
  throw new Error(
54
- "UIAccordion.Item components must be used inside <UIAccordion.Item>.",
57
+ "UIAccordion.Item components must be used inside UIAccordion.Item.",
55
58
  );
56
59
  }
57
60
 
@@ -115,7 +115,6 @@ export {
115
115
  UIAccordionTrigger,
116
116
  UIAccordionContent,
117
117
  UIAccordionIcon,
118
- UIAccordionAnimationPresets,
119
118
  } from "./Accordion";
120
119
  export {
121
120
  UITabs,
package/src/index.js CHANGED
@@ -125,7 +125,6 @@ export {
125
125
  UIAccordionTrigger,
126
126
  UIAccordionContent,
127
127
  UIAccordionIcon,
128
- UIAccordionAnimationPresets,
129
128
  UITabs,
130
129
  UITabsList,
131
130
  UITabsTrigger,