@nativetail/ui 0.0.5 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,18 +1,50 @@
1
- import { cn, Pressable, TextInput, View } from "@nativetail/core";
2
- import { useCallback, useRef, useState } from "react";
1
+ import { cn, Pressable, Text, TextInput, View } from "@nativetail/core";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { Control, Controller, Path } from "react-hook-form";
3
4
  import { TextInput as NativeTextInput } from "react-native";
4
5
 
5
- export type PinInputProps = {
6
- value: string;
7
- onChangeText: (text: string) => void;
6
+ export type PinInputProps<T = Record<string, any>> = {
7
+ value?: string;
8
+ onChangeText?: (text: string) => void;
8
9
  length: number;
9
10
  pinBoxClassName?: string;
10
11
  pinBoxFocusedClassName?: string;
11
12
  containerClassName?: string;
12
13
  error?: string;
13
14
  helperText?: string;
15
+ secureTextEntry?: boolean;
16
+ pinHideTime?: number;
17
+ control?: Control<T, any>;
18
+ name?: Path<T>;
19
+ };
20
+ export const PinInput = <T extends Record<string, any>>({
21
+ name,
22
+ control,
23
+ ...props
24
+ }: PinInputProps<T>) => {
25
+ if (control) {
26
+ return (
27
+ <Controller
28
+ name={name}
29
+ control={control}
30
+ render={({ field, fieldState }) => {
31
+ return (
32
+ <BaseInput
33
+ {...props}
34
+ value={field.value}
35
+ onChangeText={(text) => {
36
+ field.onChange(text);
37
+ }}
38
+ error={fieldState.error?.message}
39
+ />
40
+ );
41
+ }}
42
+ />
43
+ );
44
+ }
45
+ return <BaseInput {...props} />;
14
46
  };
15
- export function PinInput({
47
+ function BaseInput<T extends Record<string, any>>({
16
48
  value,
17
49
  onChangeText,
18
50
  containerClassName,
@@ -21,10 +53,12 @@ export function PinInput({
21
53
  pinBoxFocusedClassName,
22
54
  helperText,
23
55
  length,
56
+ secureTextEntry,
57
+ pinHideTime = 300,
24
58
  ...props
25
- }: PinInputProps) {
59
+ }: PinInputProps<T>) {
26
60
  const [isFocused, setIsFocused] = useState(false);
27
- const activeIndex = value.length == length ? length - 1 : value.length;
61
+ const activeIndex = value?.length == length ? length - 1 : value?.length;
28
62
  const textInputRef = useRef<NativeTextInput>();
29
63
  const onFocus = useCallback(() => {
30
64
  setIsFocused(true);
@@ -34,7 +68,7 @@ export function PinInput({
34
68
  }, [setIsFocused]);
35
69
  const _handleChange = useCallback(
36
70
  (text: string) => {
37
- if (text.length <= length) {
71
+ if (text?.length <= length) {
38
72
  onChangeText(text);
39
73
  }
40
74
  },
@@ -45,31 +79,92 @@ export function PinInput({
45
79
  textInputRef.current?.focus();
46
80
  }, [textInputRef, setIsFocused]);
47
81
  return (
48
- <View className={cn(" gap-2 flex-row w-full", containerClassName)}>
49
- {Array.from({ length: length }).map((_, index) => (
50
- <Pressable
51
- key={`pininput-${index}`}
52
- className={cn(
53
- "p-2 bg-card rounded-lg items-center justify-center w-full font-medium aspect-sqaure flex-1 border border-muted/15 h-16 text-foreground text-[16px] text-center",
54
- pinBoxClassName,
55
- isFocused &&
56
- activeIndex === index &&
57
- "border-foreground" + " " + pinBoxFocusedClassName
58
- )}
59
- onPress={onPinBoxPress}
60
- >
61
- {value[index]}
62
- </Pressable>
63
- ))}
64
- <TextInput
65
- ref={textInputRef}
66
- value={value}
67
- onChangeText={_handleChange}
68
- onFocus={onFocus}
69
- onBlur={onBlur}
70
- className="opacity-0 scale-0 absolute"
71
- {...props}
72
- />
73
- </View>
82
+ <>
83
+ <View className={cn(" gap-2 flex-row w-full", containerClassName)}>
84
+ {Array.from({ length: length }).map((_, index) => (
85
+ <PinBox
86
+ key={`pininput-${index}`}
87
+ pinBoxClassName={pinBoxClassName}
88
+ isFocused={isFocused}
89
+ activeIndex={activeIndex}
90
+ index={index}
91
+ pinBoxFocusedClassName={pinBoxFocusedClassName}
92
+ onPinBoxPress={onPinBoxPress}
93
+ secureTextEntry={secureTextEntry}
94
+ value={value}
95
+ pinHideTime={pinHideTime}
96
+ />
97
+ ))}
98
+ <TextInput
99
+ ref={textInputRef}
100
+ value={value}
101
+ onChangeText={_handleChange}
102
+ onFocus={onFocus}
103
+ onBlur={onBlur}
104
+ className="opacity-0 scale-0 absolute"
105
+ {...props}
106
+ />
107
+ </View>
108
+ {error && <Text className="text-sm text-danger">{error}</Text>}
109
+ </>
74
110
  );
75
111
  }
112
+
113
+ const PinBox = ({
114
+ pinBoxClassName,
115
+ activeIndex,
116
+ index,
117
+ onPinBoxPress,
118
+ value,
119
+ isFocused,
120
+ pinBoxFocusedClassName,
121
+ secureTextEntry,
122
+ pinHideTime,
123
+ }: {
124
+ pinBoxClassName?: string;
125
+ isFocused?: boolean;
126
+ activeIndex: number;
127
+ index: number;
128
+ pinBoxFocusedClassName?: string;
129
+ onPinBoxPress: () => void;
130
+ value: string;
131
+ secureTextEntry?: boolean;
132
+ pinHideTime?: number;
133
+ }) => {
134
+ const pinValue = value?.[index];
135
+ const isActive = activeIndex === index;
136
+ const [hide, setHide] = useState(false);
137
+ const timeoutRef = useRef<NodeJS.Timeout>(null);
138
+ useEffect(() => {
139
+ if (!secureTextEntry) return;
140
+ if (pinValue) {
141
+ timeoutRef.current = setTimeout(() => {
142
+ setHide(true);
143
+ }, pinHideTime);
144
+ } else {
145
+ setHide(false);
146
+ }
147
+ return () => {
148
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
149
+ };
150
+ }, [secureTextEntry, pinValue, pinHideTime]);
151
+ return (
152
+ <Pressable
153
+ className={cn(
154
+ "p-2 bg-card rounded-lg items-center justify-center w-full font-medium aspect-sqaure flex-1 border border-muted/15 h-16 text-foreground text-[16px] text-center",
155
+ pinBoxClassName,
156
+ isFocused &&
157
+ isActive &&
158
+ "border-foreground" + " " + pinBoxFocusedClassName
159
+ )}
160
+ onPress={onPinBoxPress}
161
+ onFocus={onPinBoxPress}
162
+ >
163
+ {hide ? (
164
+ <View className="w-2 h-2 rounded-full bg-foreground" />
165
+ ) : (
166
+ pinValue
167
+ )}
168
+ </Pressable>
169
+ );
170
+ };
@@ -1,141 +1,2 @@
1
- import { cn, PressableProps, Text, useTw, View } from "@nativetail/core";
2
- import { Dropdown } from "../dropdown";
3
- import { memo, useCallback, useMemo } from "react";
4
- import { Iconify } from "react-native-iconify";
5
-
6
- type SelectProps = PressableProps & {
7
- containerClassName?: string;
8
- label: string;
9
- error?: string;
10
- helperText?: string;
11
- value: string;
12
- onChange: (value: string) => void;
13
- placeholder?: string;
14
- options: {
15
- label: string;
16
- value: string;
17
- icon?: React.ReactNode;
18
- }[];
19
- };
20
- export function Select({
21
- containerClassName,
22
- label,
23
- error,
24
- className,
25
- value,
26
- onChange,
27
- helperText,
28
- placeholder,
29
- options,
30
- ...props
31
- }: SelectProps) {
32
- const tw = useTw();
33
- const renderOptions = useCallback(() => {
34
- return options.map((option, index) => (
35
- <SelectItem
36
- label={option.label}
37
- value={option.value}
38
- icon={option.icon}
39
- onChange={onChange}
40
- isActive={value === option.value}
41
- key={option.value}
42
- first={index === 0}
43
- last={index === options.length - 1}
44
- />
45
- ));
46
- }, [options, value, onChange, tw]);
47
- return (
48
- <View className={cn("w-full gap-1", containerClassName)}>
49
- <Text className={cn("text-muted/75 duration-75 ")}>{label}</Text>
50
-
51
- <Dropdown.Root>
52
- <SelectTrigger
53
- options={options}
54
- value={value}
55
- placeholder={placeholder}
56
- {...props}
57
- />
58
- <Dropdown.Menu>{renderOptions()}</Dropdown.Menu>
59
- </Dropdown.Root>
60
- </View>
61
- );
62
- }
63
- const SelectTrigger = memo(
64
- ({
65
- options,
66
- className,
67
- value,
68
- placeholder,
69
- ...props
70
- }: Partial<SelectProps>) => {
71
- const selectedOption = useMemo(
72
- () => options.find((option) => option.value === value),
73
- [value]
74
- );
75
- const tw = useTw();
76
- return (
77
- <Dropdown.Trigger
78
- className={cn(
79
- "p-3 bg-card rounded-lg w-full border flex-row items-center justify-between border-muted/15 h-14 text-foreground -z-5 text-[16px]",
80
- className
81
- )}
82
- {...props}
83
- >
84
- {selectedOption && (
85
- <Text className="text-foreground">{selectedOption.label}</Text>
86
- )}
87
- {!selectedOption && placeholder && (
88
- <Text className="text-muted">{placeholder}</Text>
89
- )}
90
- <Iconify
91
- icon="solar:alt-arrow-down-outline"
92
- size={20}
93
- color={tw.color("foreground")}
94
- />
95
- </Dropdown.Trigger>
96
- );
97
- }
98
- );
99
-
100
- const SelectItem = memo(
101
- ({
102
- label,
103
- value,
104
- icon,
105
- onChange,
106
- isActive,
107
- first,
108
- last,
109
- }: {
110
- label: string;
111
- value: string;
112
- icon?: React.ReactNode;
113
-
114
- onChange: (value: string) => void;
115
- isActive?: boolean;
116
- first?: boolean;
117
- last?: boolean;
118
- }) => {
119
- const tw = useTw();
120
- return (
121
- <Dropdown.Item
122
- key={value}
123
- onPress={() => onChange(isActive ? "" : value)}
124
- first={first}
125
- last={last}
126
- >
127
- <View className="flex-row items-center gap-2">
128
- <Text className="text-sm text-foreground">{label}</Text>
129
- {icon}
130
- </View>
131
- {isActive && (
132
- <Iconify
133
- icon="lucide:check"
134
- size={16}
135
- color={tw.color("foreground")}
136
- />
137
- )}
138
- </Dropdown.Item>
139
- );
140
- }
141
- );
1
+ export * from "./select";
2
+ export * from "./multi-select";
@@ -0,0 +1,208 @@
1
+ import {
2
+ cn,
3
+ Pressable,
4
+ PressableProps,
5
+ Text,
6
+ useTw,
7
+ View,
8
+ } from "@nativetail/core";
9
+ import { Dropdown } from "../dropdown";
10
+ import { memo, useCallback, useMemo } from "react";
11
+ import { Iconify } from "react-native-iconify";
12
+ import { Control, Controller, Path } from "react-hook-form";
13
+
14
+ type SelectProps<T extends Record<string, any>> = PressableProps & {
15
+ containerClassName?: string;
16
+ label?: string;
17
+ error?: string;
18
+ helperText?: string;
19
+ value?: string[];
20
+ onChange?: (value: string[]) => void;
21
+ placeholder?: string;
22
+ options: {
23
+ label: string;
24
+ value: string;
25
+ icon?: React.ReactNode;
26
+ }[];
27
+ control?: Control<T, any>;
28
+ name?: Path<T>;
29
+ };
30
+ export const MultiSelect = <T extends Record<string, any>>({
31
+ name,
32
+ control,
33
+ ...props
34
+ }: SelectProps<T>) => {
35
+ if (control) {
36
+ return (
37
+ <Controller
38
+ name={name}
39
+ control={control}
40
+ render={({ field, fieldState }) => {
41
+ return (
42
+ <BaseSelect
43
+ {...props}
44
+ value={field.value}
45
+ onChange={(text) => {
46
+ field.onChange(text);
47
+ }}
48
+ error={fieldState.error?.message}
49
+ />
50
+ );
51
+ }}
52
+ />
53
+ );
54
+ }
55
+ return <BaseSelect {...props} />;
56
+ };
57
+ function BaseSelect<T extends Record<string, any>>({
58
+ containerClassName,
59
+ label,
60
+ error,
61
+ className,
62
+ value,
63
+ onChange,
64
+ helperText,
65
+ placeholder,
66
+ options,
67
+ ...props
68
+ }: SelectProps<T>) {
69
+ const tw = useTw();
70
+ const renderOptions = useCallback(() => {
71
+ return options.map((option, index) => (
72
+ <SelectItem
73
+ label={option.label}
74
+ value={option.value}
75
+ icon={option.icon}
76
+ onChange={(val) => {
77
+ if (!val) onChange(value.filter((v) => v != option.value));
78
+ else onChange([...(value || []), val]);
79
+ }}
80
+ isActive={value?.includes?.(option.value)}
81
+ key={option.value}
82
+ first={index === 0}
83
+ last={index === options.length - 1}
84
+ />
85
+ ));
86
+ }, [options, value, onChange, tw]);
87
+ return (
88
+ <View className={cn("w-full gap-1", containerClassName)}>
89
+ <Text className={cn("text-muted/75 duration-75 ")}>{label}</Text>
90
+
91
+ <Dropdown.Root>
92
+ <SelectTrigger
93
+ options={options}
94
+ value={value}
95
+ placeholder={placeholder}
96
+ onChange={onChange}
97
+ error={error}
98
+ {...props}
99
+ />
100
+ <Dropdown.Menu>{renderOptions()}</Dropdown.Menu>
101
+ </Dropdown.Root>
102
+ </View>
103
+ );
104
+ }
105
+ const SelectTrigger = memo(
106
+ <T extends Record<string, any>>({
107
+ options,
108
+ className,
109
+ value,
110
+ placeholder,
111
+ onChange,
112
+ error,
113
+ ...props
114
+ }: PressableProps & {
115
+ options: SelectProps<T>["options"];
116
+ value: SelectProps<T>["value"];
117
+ onChange: SelectProps<T>["onChange"];
118
+ placeholder: SelectProps<T>["placeholder"];
119
+ error: SelectProps<T>["error"];
120
+ }) => {
121
+ const selectedOptions = useMemo(
122
+ () => options.filter((option) => value?.includes?.(option.value)),
123
+ [value]
124
+ );
125
+ const tw = useTw();
126
+ return (
127
+ <>
128
+ <Dropdown.Trigger
129
+ className={cn(
130
+ "p-3 bg-card rounded-lg w-full border flex-row items-center justify-between border-muted/15 h-14 text-foreground -z-5 text-[16px]",
131
+ className
132
+ )}
133
+ {...props}
134
+ >
135
+ {selectedOptions.length > 0 && (
136
+ <View className="flex-row gap-2 flex-wrap">
137
+ {selectedOptions.map((option) => (
138
+ <Pressable
139
+ className="bg-muted/8 rounded-2xl text-sm px-2 py-1"
140
+ key={option.value}
141
+ onPress={() => {
142
+ onChange(value.filter((val) => val != option.value));
143
+ }}
144
+ >
145
+ {option.label}
146
+ </Pressable>
147
+ ))}
148
+ </View>
149
+ )}
150
+ {selectedOptions.length == 0 && placeholder && (
151
+ <Text className="text-muted">{placeholder}</Text>
152
+ )}
153
+ <Iconify
154
+ icon="solar:alt-arrow-down-outline"
155
+ size={20}
156
+ color={tw.color("foreground")}
157
+ />
158
+ </Dropdown.Trigger>
159
+
160
+ {error && <Text className="text-sm text-danger">{error}</Text>}
161
+ </>
162
+ );
163
+ }
164
+ );
165
+
166
+ const SelectItem = memo(
167
+ ({
168
+ label,
169
+ value,
170
+ icon,
171
+ onChange,
172
+ isActive,
173
+ first,
174
+ last,
175
+ }: {
176
+ label: string;
177
+ value: string;
178
+ icon?: React.ReactNode;
179
+
180
+ onChange: (value: string) => void;
181
+ isActive?: boolean;
182
+ first?: boolean;
183
+ last?: boolean;
184
+ }) => {
185
+ const tw = useTw();
186
+ return (
187
+ <Dropdown.Item
188
+ key={value}
189
+ onPress={() => onChange(isActive ? "" : value)}
190
+ first={first}
191
+ last={last}
192
+ autoClosable={false}
193
+ >
194
+ <View className="flex-row items-center gap-2">
195
+ <Text className="text-sm text-foreground">{label}</Text>
196
+ {icon}
197
+ </View>
198
+ {isActive && (
199
+ <Iconify
200
+ icon="lucide:check"
201
+ size={16}
202
+ color={tw.color("foreground")}
203
+ />
204
+ )}
205
+ </Dropdown.Item>
206
+ );
207
+ }
208
+ );