rnnovaui 0.8.13 → 0.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnnovaui",
3
- "version": "0.8.13",
3
+ "version": "0.9.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -0,0 +1,222 @@
1
+ import React, {
2
+ Children,
3
+ createContext,
4
+ isValidElement,
5
+ memo,
6
+ useCallback,
7
+ useContext,
8
+ useMemo,
9
+ useState,
10
+ } from "react";
11
+
12
+ import { StyleSheet, View } from "react-native";
13
+
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";
20
+
21
+ const AccordionContext = createContext(null);
22
+
23
+ export const UIAccordion = memo(function UIAccordion({
24
+ children,
25
+
26
+ value,
27
+ defaultValue,
28
+
29
+ multiple = false,
30
+
31
+ onValueChange,
32
+
33
+ disabled = false,
34
+
35
+ animated = true,
36
+ animationDuration = 220,
37
+
38
+ variant = "default",
39
+ size = "md",
40
+
41
+ separator = true,
42
+
43
+ containerStyle,
44
+ itemStyle,
45
+
46
+ testID,
47
+ }) {
48
+ const { theme } = useUITheme();
49
+
50
+ const colors = theme?.colors || {};
51
+
52
+ const normalizeValue = useCallback(
53
+ (input) => {
54
+ if (multiple) {
55
+ if (Array.isArray(input)) {
56
+ return input;
57
+ }
58
+
59
+ if (input === undefined || input === null) {
60
+ return [];
61
+ }
62
+
63
+ return [input];
64
+ }
65
+
66
+ if (Array.isArray(input)) {
67
+ return input[0] ?? null;
68
+ }
69
+
70
+ return input ?? null;
71
+ },
72
+ [multiple],
73
+ );
74
+
75
+ const controlled = value !== undefined;
76
+
77
+ const [internalValue, setInternalValue] = useState(() =>
78
+ normalizeValue(defaultValue),
79
+ );
80
+
81
+ const currentValue = controlled ? normalizeValue(value) : internalValue;
82
+
83
+ const isOpen = useCallback(
84
+ (itemValue) => {
85
+ if (multiple) {
86
+ return currentValue.includes(itemValue);
87
+ }
88
+
89
+ return currentValue === itemValue;
90
+ },
91
+ [currentValue, multiple],
92
+ );
93
+
94
+ const toggle = useCallback(
95
+ (itemValue) => {
96
+ if (disabled) {
97
+ return;
98
+ }
99
+
100
+ let nextValue;
101
+
102
+ if (multiple) {
103
+ const current = Array.isArray(currentValue) ? currentValue : [];
104
+
105
+ if (current.includes(itemValue)) {
106
+ nextValue = current.filter((item) => item !== itemValue);
107
+ } else {
108
+ nextValue = [...current, itemValue];
109
+ }
110
+ } else {
111
+ nextValue = currentValue === itemValue ? null : itemValue;
112
+ }
113
+
114
+ if (!controlled) {
115
+ setInternalValue(nextValue);
116
+ }
117
+
118
+ onValueChange?.(nextValue);
119
+ },
120
+ [disabled, multiple, currentValue, controlled, onValueChange],
121
+ );
122
+
123
+ const contextValue = useMemo(
124
+ () => ({
125
+ colors,
126
+
127
+ currentValue,
128
+
129
+ multiple,
130
+
131
+ disabled,
132
+
133
+ animated,
134
+ animationDuration,
135
+
136
+ variant,
137
+ size,
138
+
139
+ separator,
140
+
141
+ isOpen,
142
+ toggle,
143
+ }),
144
+ [
145
+ colors,
146
+ currentValue,
147
+ multiple,
148
+ disabled,
149
+ animated,
150
+ animationDuration,
151
+ variant,
152
+ size,
153
+ separator,
154
+ isOpen,
155
+ toggle,
156
+ ],
157
+ );
158
+
159
+ return (
160
+ <AccordionContext.Provider value={contextValue}>
161
+ <View
162
+ testID={testID}
163
+ style={[
164
+ styles.container,
165
+
166
+ {
167
+ backgroundColor: colors.card || colors.background || "transparent",
168
+
169
+ borderColor: colors.border || "#E5E5E5",
170
+ },
171
+
172
+ variant === "outlined" ? styles.outlined : null,
173
+
174
+ containerStyle,
175
+ ]}
176
+ >
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
+ })}
186
+ </View>
187
+ </AccordionContext.Provider>
188
+ );
189
+ });
190
+
191
+ export function useUIAccordion() {
192
+ const context = useContext(AccordionContext);
193
+
194
+ if (!context) {
195
+ throw new Error("Accordion components must be used inside UIAccordion.");
196
+ }
197
+
198
+ return context;
199
+ }
200
+
201
+ const styles = StyleSheet.create({
202
+ container: {
203
+ width: "100%",
204
+ overflow: "hidden",
205
+ },
206
+
207
+ outlined: {
208
+ borderWidth: 1,
209
+ borderRadius: 12,
210
+ },
211
+ });
212
+
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;
@@ -0,0 +1,103 @@
1
+ import React, { memo, useEffect, useRef, useState } from "react";
2
+
3
+ import { Animated, StyleSheet, View } from "react-native";
4
+
5
+ import { useUIAccordion } from "./UIAccordion";
6
+
7
+ import { useUIAccordionItem } from "./UIAccordionItem";
8
+
9
+ export const UIAccordionContent = memo(function UIAccordionContent({
10
+ children,
11
+
12
+ style,
13
+
14
+ innerStyle,
15
+
16
+ testID,
17
+ }) {
18
+ const accordion = useUIAccordion();
19
+
20
+ const item = useUIAccordionItem();
21
+
22
+ const progress = useRef(new Animated.Value(item.open ? 1 : 0)).current;
23
+
24
+ const [mounted, setMounted] = useState(item.open);
25
+
26
+ useEffect(() => {
27
+ if (item.open) {
28
+ setMounted(true);
29
+
30
+ if (!accordion.animated) {
31
+ progress.setValue(1);
32
+ return;
33
+ }
34
+
35
+ Animated.timing(progress, {
36
+ toValue: 1,
37
+
38
+ duration: accordion.animationDuration,
39
+
40
+ useNativeDriver: false,
41
+ }).start();
42
+
43
+ return;
44
+ }
45
+
46
+ if (!accordion.animated) {
47
+ progress.setValue(0);
48
+ setMounted(false);
49
+ return;
50
+ }
51
+
52
+ Animated.timing(progress, {
53
+ toValue: 0,
54
+
55
+ duration: accordion.animationDuration,
56
+
57
+ useNativeDriver: false,
58
+ }).start(() => {
59
+ setMounted(false);
60
+ });
61
+ }, [item.open, accordion.animated, accordion.animationDuration, progress]);
62
+
63
+ if (!mounted) {
64
+ return null;
65
+ }
66
+
67
+ return (
68
+ <Animated.View
69
+ testID={testID}
70
+ style={[
71
+ styles.animatedContainer,
72
+
73
+ {
74
+ opacity: progress,
75
+
76
+ maxHeight: progress.interpolate({
77
+ inputRange: [0, 1],
78
+
79
+ outputRange: [0, 1000],
80
+ }),
81
+ },
82
+
83
+ style,
84
+ ]}
85
+ >
86
+ <View style={[styles.inner, innerStyle]}>{children}</View>
87
+ </Animated.View>
88
+ );
89
+ });
90
+
91
+ const styles = StyleSheet.create({
92
+ animatedContainer: {
93
+ width: "100%",
94
+
95
+ overflow: "hidden",
96
+ },
97
+
98
+ inner: {
99
+ width: "100%",
100
+ },
101
+ });
102
+
103
+ UIAccordionContent.displayName = "UIAccordion.Content";
@@ -0,0 +1,55 @@
1
+ import React, { memo } from "react";
2
+
3
+ import { StyleSheet, View } from "react-native";
4
+
5
+ import { useUIAccordion } from "./UIAccordion";
6
+
7
+ import { useUIAccordionItem } from "./UIAccordionItem";
8
+
9
+ export const UIAccordionHeader = memo(function UIAccordionHeader({
10
+ children,
11
+
12
+ style,
13
+
14
+ testID,
15
+ }) {
16
+ const accordion = useUIAccordion();
17
+
18
+ const item = useUIAccordionItem();
19
+
20
+ return (
21
+ <View
22
+ testID={testID}
23
+ style={[
24
+ styles.header,
25
+
26
+ {
27
+ minHeight:
28
+ accordion.size === "sm" ? 44 : accordion.size === "lg" ? 60 : 52,
29
+
30
+ borderBottomColor: accordion.colors.border || "#E5E5E5",
31
+ },
32
+
33
+ accordion.separator && item.open ? styles.headerOpen : null,
34
+
35
+ style,
36
+ ]}
37
+ >
38
+ {children}
39
+ </View>
40
+ );
41
+ });
42
+
43
+ const styles = StyleSheet.create({
44
+ header: {
45
+ width: "100%",
46
+
47
+ justifyContent: "center",
48
+ },
49
+
50
+ headerOpen: {
51
+ borderBottomWidth: 1,
52
+ },
53
+ });
54
+
55
+ UIAccordionHeader.displayName = "UIAccordion.Header";
@@ -0,0 +1,130 @@
1
+ import React, { memo, useEffect, useRef } from "react";
2
+
3
+ import { Animated, Text, View } from "react-native";
4
+
5
+ import { useUIAccordion } from "./UIAccordion";
6
+
7
+ import { useUIAccordionItem } from "./UIAccordionItem";
8
+
9
+ export const UIAccordionIcon = memo(function UIAccordionIcon({
10
+ children,
11
+
12
+ openIcon,
13
+ closedIcon,
14
+
15
+ size,
16
+
17
+ color,
18
+
19
+ style,
20
+
21
+ animated = true,
22
+
23
+ rotation = 180,
24
+ }) {
25
+ const accordion = useUIAccordion();
26
+
27
+ const item = useUIAccordionItem();
28
+
29
+ const animation = useRef(new Animated.Value(item.open ? 1 : 0)).current;
30
+
31
+ useEffect(() => {
32
+ if (!animated) {
33
+ animation.setValue(item.open ? 1 : 0);
34
+
35
+ return;
36
+ }
37
+
38
+ Animated.timing(animation, {
39
+ toValue: item.open ? 1 : 0,
40
+
41
+ duration: accordion.animationDuration,
42
+
43
+ useNativeDriver: true,
44
+ }).start();
45
+ }, [item.open, animated, accordion.animationDuration, animation]);
46
+
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
+ },
65
+
66
+ style,
67
+ ]}
68
+ >
69
+ {customIcon}
70
+ </Animated.View>
71
+ );
72
+ }
73
+
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
+ }
96
+
97
+ return (
98
+ <Animated.View
99
+ style={[
100
+ {
101
+ transform: [
102
+ {
103
+ rotate: animation.interpolate({
104
+ inputRange: [0, 1],
105
+
106
+ outputRange: ["0deg", `${rotation}deg`],
107
+ }),
108
+ },
109
+ ],
110
+ },
111
+
112
+ style,
113
+ ]}
114
+ >
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>
126
+ </Animated.View>
127
+ );
128
+ });
129
+
130
+ UIAccordionIcon.displayName = "UIAccordion.Icon";
@@ -0,0 +1,70 @@
1
+ import React, { createContext, memo, useContext } from "react";
2
+
3
+ import { StyleSheet, View } from "react-native";
4
+
5
+ import { useUIAccordion } from "./UIAccordion";
6
+
7
+ const AccordionItemContext = createContext(null);
8
+
9
+ export const UIAccordionItem = memo(function UIAccordionItem({
10
+ children,
11
+
12
+ value,
13
+
14
+ disabled = false,
15
+
16
+ style,
17
+
18
+ testID,
19
+ }) {
20
+ const accordion = useUIAccordion();
21
+
22
+ const open = accordion.isOpen(value);
23
+
24
+ const itemContext = {
25
+ value,
26
+
27
+ open,
28
+
29
+ disabled: disabled || accordion.disabled,
30
+ };
31
+
32
+ return (
33
+ <AccordionItemContext.Provider value={itemContext}>
34
+ <View
35
+ testID={testID}
36
+ style={[
37
+ styles.item,
38
+
39
+ {
40
+ backgroundColor: accordion.colors.card || "transparent",
41
+ },
42
+
43
+ style,
44
+ ]}
45
+ >
46
+ {children}
47
+ </View>
48
+ </AccordionItemContext.Provider>
49
+ );
50
+ });
51
+
52
+ export function useUIAccordionItem() {
53
+ const context = useContext(AccordionItemContext);
54
+
55
+ if (!context) {
56
+ throw new Error(
57
+ "UIAccordion.Item components must be used inside UIAccordion.Item.",
58
+ );
59
+ }
60
+
61
+ return context;
62
+ }
63
+
64
+ const styles = StyleSheet.create({
65
+ item: {
66
+ width: "100%",
67
+ },
68
+ });
69
+
70
+ UIAccordionItem.displayName = "UIAccordion.Item";
@@ -0,0 +1,92 @@
1
+ import React, { memo } from "react";
2
+
3
+ import { Pressable, StyleSheet, View } from "react-native";
4
+
5
+ import { useUIAccordion } from "./UIAccordion";
6
+
7
+ import { useUIAccordionItem } from "./UIAccordionItem";
8
+
9
+ export const UIAccordionTrigger = memo(function UIAccordionTrigger({
10
+ children,
11
+
12
+ onPress,
13
+
14
+ disabled,
15
+
16
+ style,
17
+
18
+ contentStyle,
19
+
20
+ activeOpacity = 0.7,
21
+
22
+ testID,
23
+
24
+ accessibilityLabel,
25
+
26
+ ...props
27
+ }) {
28
+ const accordion = useUIAccordion();
29
+
30
+ const item = useUIAccordionItem();
31
+
32
+ const isDisabled = disabled ?? item.disabled;
33
+
34
+ const handlePress = (event) => {
35
+ if (isDisabled) {
36
+ return;
37
+ }
38
+
39
+ accordion.toggle(item.value);
40
+
41
+ onPress?.(event);
42
+ };
43
+
44
+ return (
45
+ <Pressable
46
+ {...props}
47
+ testID={testID}
48
+ disabled={isDisabled}
49
+ onPress={handlePress}
50
+ accessibilityRole="button"
51
+ accessibilityState={{
52
+ expanded: item.open,
53
+ disabled: isDisabled,
54
+ }}
55
+ accessibilityLabel={accessibilityLabel}
56
+ style={({ pressed }) => [
57
+ styles.trigger,
58
+
59
+ {
60
+ minHeight:
61
+ accordion.size === "sm" ? 44 : accordion.size === "lg" ? 60 : 52,
62
+
63
+ opacity: isDisabled ? 0.5 : pressed ? activeOpacity : 1,
64
+ },
65
+
66
+ style,
67
+ ]}
68
+ >
69
+ <View style={[styles.content, contentStyle]}>{children}</View>
70
+ </Pressable>
71
+ );
72
+ });
73
+
74
+ const styles = StyleSheet.create({
75
+ trigger: {
76
+ width: "100%",
77
+
78
+ justifyContent: "center",
79
+ },
80
+
81
+ content: {
82
+ width: "100%",
83
+
84
+ flexDirection: "row",
85
+
86
+ alignItems: "center",
87
+
88
+ justifyContent: "space-between",
89
+ },
90
+ });
91
+
92
+ UIAccordionTrigger.displayName = "UIAccordion.Trigger";
@@ -0,0 +1,11 @@
1
+ export { UIAccordion } from "./UIAccordion";
2
+
3
+ export { UIAccordionItem } from "./UIAccordionItem";
4
+
5
+ export { UIAccordionHeader } from "./UIAccordionHeader";
6
+
7
+ export { UIAccordionTrigger } from "./UIAccordionTrigger";
8
+
9
+ export { UIAccordionContent } from "./UIAccordionContent";
10
+
11
+ export { UIAccordionIcon } from "./UIAccordionIcon";