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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

175
interface LabelProps extends React.ComponentProps<typeof Text> {
176
  text: string;
177
}
178

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

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

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