eletrotupi / tcc/ frontend/components/ui/Input.tsxc6faa4c901e5f6c4ca8395c08d412ed57e708f6a
5.5 KB | 231 lines Raw
1
import React, { useState } from 'react';
2
import {
3
  TextInput,
4
  TextInputProps,
5
  StyleSheet,
6
  View,
7
  TouchableOpacity,
8
  Platform,
9
} from 'react-native';
10
import { Text } from '@/components/ui/Text';
11
import { useThemeColor } from '@/hooks/use-theme-color';
12
import { Ionicons } from '@expo/vector-icons';
13

14
function toTestId(label: string): string {
15
  return label
16
    .normalize('NFD')
17
    .replace(/[\u0300-\u036f]/g, '')
18
    .toLowerCase()
19
    .replace(/[^a-z0-9]+/g, '-')
20
    .replace(/^-+|-+$/g, '');
21
}
22

23
type InputVariant = 'default' | 'ghost' | 'darkGhost';
24

25
interface BaseInputProps {
26
  label?: string;
27
  error?: string;
28
  variant?: InputVariant;
29
}
30

31
function useInputStyles(variant: InputVariant, error?: string, hasPasswordToggle?: boolean) {
32
  let backgroundColor = null;
33

34
  const textColor = useThemeColor({}, 'text');
35
  const defaultBackgroundColor = useThemeColor({}, 'inputBackgroundColor');
36
  const backgroundDarkGhostColor = useThemeColor({}, 'inputBackgroundDarkGhostColor');
37
  const borderColor = useThemeColor({}, 'inputBorderColor');
38
  const placeholderColor = useThemeColor({}, 'inputPlaceholderColor');
39
  const errorColor = '#ef4444';
40

41
  const isGhost = variant === 'ghost';
42
  const isDarkGhost = variant === 'darkGhost';
43

44
  if (isDarkGhost) {
45
    backgroundColor = backgroundDarkGhostColor;
46
  } else if (isGhost) {
47
    backgroundColor = 'transparent';
48
  } else {
49
    backgroundColor = defaultBackgroundColor;
50
  }
51

52
  const inputStyle = {
53
    color: textColor,
54
    backgroundColor: backgroundColor,
55
    borderColor: error ? errorColor : borderColor,
56
    borderWidth: isGhost || isDarkGhost ? 0 : 1,
57
    paddingRight: hasPasswordToggle ? 44 : 0
58
  };
59

60
  return { textColor, placeholderColor, errorColor, inputStyle };
61
}
62

63
interface InputProps extends TextInputProps, BaseInputProps {
64
  type?: 'text' | 'email' | 'password';
65
  showPasswordToggle?: boolean;
66
}
67

68
export const Input: React.FC<InputProps> = ({
69
  label,
70
  error,
71
  variant = 'default',
72
  type = 'text',
73
  showPasswordToggle = false,
74
  style,
75
  ...props
76
}) => {
77
  const [showPassword, setShowPassword] = useState(false);
78
  const {
79
    textColor,
80
    placeholderColor,
81
    errorColor,
82
    inputStyle
83
  } = useInputStyles(variant, error, showPasswordToggle);
84

85
  const keyboardType = type === 'email' ? 'email-address' : 'default';
86
  const secureTextEntry = type === 'password' && !showPassword;
87
  const testID = props.testID ?? (label ? `input-${toTestId(label)}` : undefined);
88

89
  return (
90
    <View style={styles.container}>
91
      {label && (
92
        <Label text={label} />
93
      )}
94
      <View style={styles.inputContainer}>
95
        <TextInput
96
          key={`${type}-${showPassword}`}
97
          testID={testID}
98
          style={[styles.input, inputStyle, style]}
99
          placeholderTextColor={placeholderColor}
100
          keyboardType={keyboardType}
101
          secureTextEntry={secureTextEntry}
102
          autoCapitalize={type === 'email' ? 'none' : props.autoCapitalize}
103
          autoCorrect={type === 'email' || type === 'password' ? false : props.autoCorrect}
104
          {...props}
105
        />
106
        {type === 'password' && showPasswordToggle && (
107
          <TouchableOpacity
108
            style={styles.passwordToggle}
109
            onPress={() => setShowPassword((prev) => !prev)}
110
            hitSlop={8}
111
          >
112
            <Ionicons
113
              name={showPassword ? 'eye-off' : 'eye'}
114
              size={22}
115
              color={placeholderColor}
116
            />
117
          </TouchableOpacity>
118
        )}
119
      </View>
120
      {error && (
121
        <Text style={[styles.errorText, { color: errorColor }]}>{error}</Text>
122
      )}
123
    </View>
124
  );
125
};
126

127
interface TextAreaProps extends TextInputProps, BaseInputProps {
128
  minRows?: number;
129
  maxRows?: number;
130
}
131

132
export const TextArea: React.FC<TextAreaProps> = ({
133
  label,
134
  error,
135
  variant = 'default',
136
  minRows = 3,
137
  maxRows,
138
  style,
139
  ...props
140
}) => {
141
  const {
142
    textColor,
143
    placeholderColor,
144
    errorColor,
145
    inputStyle
146
  } = useInputStyles(variant, error);
147

148
  const minHeight = minRows * 24;
149
  const maxHeight = maxRows ? maxRows * 24 : undefined;
150
  const testID = props.testID ?? (label ? `input-${toTestId(label)}` : undefined);
151

152
  return (
153
    <View style={styles.container}>
154
      {label && (
155
        <Label text={label} />
156
      )}
157
      <TextInput
158
        testID={testID}
159
        style={[
160
          styles.textArea,
161
          inputStyle,
162
          { minHeight, maxHeight },
163
          style,
164
        ]}
165
        placeholderTextColor={placeholderColor}
166
        multiline
167
        textAlignVertical="top"
168
        scrollEnabled={!!maxRows}
169
        {...props}
170
      />
171
      {error && (
172
        <Text style={[styles.errorText, { color: errorColor }]}>{error}</Text>
173
      )}
174
    </View>
175
  );
176
};
177

178
interface LabelProps extends React.ComponentProps<typeof Text> {
179
  text: string;
180
}
181

182
export const Label: React.FC<LabelProps> = ({
183
  text,
184
  style,
185
  ...props
186
}) => {
187
  const textColor = useThemeColor({}, 'text');
188

189
  return (
190
    <Text style={[styles.label, { color: textColor }, style]} {...props}>
191
      {text}
192
    </Text>
193
  );
194
};
195

196
const styles = StyleSheet.create({
197
  container: {
198
    marginBottom: 16,
199
  },
200
  label: {
201
    fontSize: 14,
202
    fontWeight: '500',
203
    marginBottom: 6,
204
  },
205
  inputContainer: {
206
    position: 'relative',
207
  },
208
  input: {
209
    height: 48,
210
    paddingHorizontal: 12,
211
    borderWidth: 1,
212
    borderRadius: 8,
213
    fontSize: 16,
214
  },
215
  textArea: {
216
    paddingHorizontal: 12,
217
    paddingVertical: 12,
218
    borderWidth: 1,
219
    borderRadius: 8,
220
    fontSize: 16,
221
  },
222
  passwordToggle: {
223
    position: 'absolute',
224
    right: 12,
225
    top: 13,
226
  },
227
  errorText: {
228
    fontSize: 12,
229
    marginTop: 4,
230
  },
231
});