@wecareu/input-text 0.1.1
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/README.md +36 -0
- package/lib/commonjs/InputText.js +350 -0
- package/lib/commonjs/InputText.js.map +1 -0
- package/lib/commonjs/InputText.types.js +6 -0
- package/lib/commonjs/InputText.types.js.map +1 -0
- package/lib/commonjs/InputTextErrorMessage.js +32 -0
- package/lib/commonjs/InputTextErrorMessage.js.map +1 -0
- package/lib/commonjs/InputTextIcon.js +65 -0
- package/lib/commonjs/InputTextIcon.js.map +1 -0
- package/lib/commonjs/animations/shake.js +45 -0
- package/lib/commonjs/animations/shake.js.map +1 -0
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/index.stories.js +409 -0
- package/lib/commonjs/index.stories.js.map +1 -0
- package/lib/module/InputText.js +343 -0
- package/lib/module/InputText.js.map +1 -0
- package/lib/module/InputText.types.js +2 -0
- package/lib/module/InputText.types.js.map +1 -0
- package/lib/module/InputTextErrorMessage.js +25 -0
- package/lib/module/InputTextErrorMessage.js.map +1 -0
- package/lib/module/InputTextIcon.js +58 -0
- package/lib/module/InputTextIcon.js.map +1 -0
- package/lib/module/animations/shake.js +39 -0
- package/lib/module/animations/shake.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/index.stories.js +402 -0
- package/lib/module/index.stories.js.map +1 -0
- package/lib/typescript/src/InputText.d.ts +5 -0
- package/lib/typescript/src/InputText.d.ts.map +1 -0
- package/lib/typescript/src/InputText.types.d.ts +146 -0
- package/lib/typescript/src/InputText.types.d.ts.map +1 -0
- package/lib/typescript/src/InputTextErrorMessage.d.ts +3 -0
- package/lib/typescript/src/InputTextErrorMessage.d.ts.map +1 -0
- package/lib/typescript/src/InputTextIcon.d.ts +3 -0
- package/lib/typescript/src/InputTextIcon.d.ts.map +1 -0
- package/lib/typescript/src/animations/shake.d.ts +32 -0
- package/lib/typescript/src/animations/shake.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +66 -0
- package/src/InputText.tsx +451 -0
- package/src/InputText.types.ts +153 -0
- package/src/InputTextErrorMessage.tsx +31 -0
- package/src/InputTextIcon.tsx +65 -0
- package/src/animations/shake.ts +76 -0
- package/src/index.stories.tsx +387 -0
- package/src/index.tsx +2 -0
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wecareu/input-text",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Campo de texto controlado do design system WeCareU com suporte a ícones e feedback de erro",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react-native",
|
|
7
|
+
"input",
|
|
8
|
+
"text",
|
|
9
|
+
"design-system",
|
|
10
|
+
"wecareu"
|
|
11
|
+
],
|
|
12
|
+
"license": "UNLICENSED",
|
|
13
|
+
"private": false,
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"main": "lib/commonjs/index.js",
|
|
18
|
+
"module": "lib/module/index.js",
|
|
19
|
+
"react-native": "lib/module/index.js",
|
|
20
|
+
"types": "lib/typescript/src/index.d.ts",
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"src",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "bob build",
|
|
28
|
+
"clean": "rimraf lib",
|
|
29
|
+
"lint": "eslint .",
|
|
30
|
+
"test": "jest"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@wecareu/icons": "*",
|
|
34
|
+
"@wecareu/theme": "*",
|
|
35
|
+
"@wecareu/tokens": "*",
|
|
36
|
+
"react": ">=18.2.0",
|
|
37
|
+
"react-native": ">=0.74.0-0 <0.76.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@testing-library/jest-native": "^5.4.3",
|
|
41
|
+
"@testing-library/react-native": "^12.7.2",
|
|
42
|
+
"@types/jest": "^29.5.0",
|
|
43
|
+
"jest": "^29.7.0",
|
|
44
|
+
"react": "18.2.0",
|
|
45
|
+
"react-native": "0.74.5",
|
|
46
|
+
"react-native-builder-bob": "^0.23.2",
|
|
47
|
+
"typescript": "^5.4.0"
|
|
48
|
+
},
|
|
49
|
+
"exports": {
|
|
50
|
+
".": {
|
|
51
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
52
|
+
"react-native": "./lib/module/index.js",
|
|
53
|
+
"import": "./lib/module/index.js",
|
|
54
|
+
"require": "./lib/commonjs/index.js"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"react-native-builder-bob": {
|
|
58
|
+
"source": "src",
|
|
59
|
+
"output": "lib",
|
|
60
|
+
"targets": [
|
|
61
|
+
"commonjs",
|
|
62
|
+
"module",
|
|
63
|
+
"typescript"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Animated, StyleSheet, TextInput, View } from 'react-native'
|
|
4
|
+
import type { KeyboardTypeOptions, TextInputProps } from 'react-native'
|
|
5
|
+
|
|
6
|
+
import { useTheme } from '@wecareu/theme'
|
|
7
|
+
import type { Theme } from '@wecareu/theme'
|
|
8
|
+
|
|
9
|
+
import type { InputTextLocale, InputTextProps, InputTextType } from './InputText.types'
|
|
10
|
+
import { InputTextErrorMessage } from './InputTextErrorMessage'
|
|
11
|
+
import { InputTextIcon } from './InputTextIcon'
|
|
12
|
+
import { useShakeAnimation } from './animations/shake'
|
|
13
|
+
|
|
14
|
+
interface AutoConfig {
|
|
15
|
+
formatter?: (value: string) => string
|
|
16
|
+
parser?: (input: string) => string
|
|
17
|
+
validator?: (value: string) => boolean
|
|
18
|
+
keyboardType?: KeyboardTypeOptions
|
|
19
|
+
textContentType?: TextInputProps['textContentType']
|
|
20
|
+
secureTextEntry?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const InputText = React.forwardRef<TextInput, InputTextProps>(function InputText(
|
|
24
|
+
{
|
|
25
|
+
accessibilityRole,
|
|
26
|
+
accessibilityState,
|
|
27
|
+
accessibilityValue: accessibilityValueProp,
|
|
28
|
+
disabled = false,
|
|
29
|
+
errorMessage,
|
|
30
|
+
formatter,
|
|
31
|
+
inputError = false,
|
|
32
|
+
inputStyle,
|
|
33
|
+
leftIcon,
|
|
34
|
+
locale = 'pt',
|
|
35
|
+
maskNumber = false,
|
|
36
|
+
maxLength = 255,
|
|
37
|
+
minLength = 3,
|
|
38
|
+
nextInputRef,
|
|
39
|
+
onChangeText,
|
|
40
|
+
onSubmitEditing,
|
|
41
|
+
onValidation,
|
|
42
|
+
parser,
|
|
43
|
+
placeholder,
|
|
44
|
+
readonly = false,
|
|
45
|
+
rightIcon,
|
|
46
|
+
style,
|
|
47
|
+
testID,
|
|
48
|
+
type = 'text',
|
|
49
|
+
validateOnBlur = true,
|
|
50
|
+
validationMessage,
|
|
51
|
+
validator,
|
|
52
|
+
validatorMessage,
|
|
53
|
+
value,
|
|
54
|
+
...rest
|
|
55
|
+
}: InputTextProps,
|
|
56
|
+
ref
|
|
57
|
+
) {
|
|
58
|
+
const theme = useTheme()
|
|
59
|
+
const styles = React.useMemo(() => createStyles(theme), [theme])
|
|
60
|
+
const isEditable = !(disabled || readonly)
|
|
61
|
+
const placeholderColor = theme.colors.text.tertiary
|
|
62
|
+
const { animatedStyle, triggerShake } = useShakeAnimation({
|
|
63
|
+
distance: theme.spacing.sm
|
|
64
|
+
})
|
|
65
|
+
const [touched, setTouched] = React.useState(false)
|
|
66
|
+
|
|
67
|
+
const autoConfig = React.useMemo(() => getAutoConfig({ locale, maskNumber, type }), [locale, maskNumber, type])
|
|
68
|
+
|
|
69
|
+
const appliedFormatter = formatter ?? autoConfig.formatter
|
|
70
|
+
const appliedParser = parser ?? autoConfig.parser
|
|
71
|
+
const appliedValidator = validator ?? autoConfig.validator
|
|
72
|
+
|
|
73
|
+
const {
|
|
74
|
+
keyboardType: propKeyboardType,
|
|
75
|
+
secureTextEntry: propSecureTextEntry,
|
|
76
|
+
textContentType: propTextContentType,
|
|
77
|
+
onBlur: textInputOnBlur,
|
|
78
|
+
onEndEditing: textInputOnEndEditing,
|
|
79
|
+
...textInputProps
|
|
80
|
+
} = rest as TextInputProps
|
|
81
|
+
|
|
82
|
+
const appliedKeyboardType = propKeyboardType ?? autoConfig.keyboardType
|
|
83
|
+
const appliedTextContentType = propTextContentType ?? autoConfig.textContentType
|
|
84
|
+
const appliedSecureTextEntry = propSecureTextEntry ?? autoConfig.secureTextEntry ?? false
|
|
85
|
+
|
|
86
|
+
const displayValue = React.useMemo(
|
|
87
|
+
() => (appliedFormatter ? appliedFormatter(value) : value),
|
|
88
|
+
[appliedFormatter, value]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const lengthInvalid = value.length < minLength || value.length > maxLength
|
|
92
|
+
const validatorInvalid = appliedValidator ? !appliedValidator(value) : false
|
|
93
|
+
const internalHasError = touched && validateOnBlur && isEditable && (lengthInvalid || validatorInvalid)
|
|
94
|
+
|
|
95
|
+
const shouldShowError = inputError || internalHasError
|
|
96
|
+
const effectiveErrorMessage = inputError
|
|
97
|
+
? errorMessage
|
|
98
|
+
: internalHasError
|
|
99
|
+
? validatorInvalid
|
|
100
|
+
? validatorMessage
|
|
101
|
+
: lengthInvalid
|
|
102
|
+
? validationMessage
|
|
103
|
+
: undefined
|
|
104
|
+
: undefined
|
|
105
|
+
|
|
106
|
+
const previousErrorRef = React.useRef(false)
|
|
107
|
+
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
if (!validateOnBlur || !isEditable) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
if (!touched) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
onValidation?.(!internalHasError)
|
|
116
|
+
}, [internalHasError, isEditable, onValidation, touched, validateOnBlur])
|
|
117
|
+
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
if (shouldShowError && !previousErrorRef.current) {
|
|
120
|
+
triggerShake()
|
|
121
|
+
}
|
|
122
|
+
previousErrorRef.current = shouldShowError
|
|
123
|
+
}, [shouldShowError, triggerShake])
|
|
124
|
+
|
|
125
|
+
const handleChangeText = React.useCallback(
|
|
126
|
+
(text: string) => {
|
|
127
|
+
if (!isEditable) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const parsedValue = appliedParser ? appliedParser(text) : text
|
|
132
|
+
onChangeText(parsedValue)
|
|
133
|
+
},
|
|
134
|
+
[appliedParser, isEditable, onChangeText]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const notifyValidIfDisabled = React.useCallback(() => {
|
|
138
|
+
if (!validateOnBlur) {
|
|
139
|
+
onValidation?.(true)
|
|
140
|
+
}
|
|
141
|
+
}, [onValidation, validateOnBlur])
|
|
142
|
+
|
|
143
|
+
const handleBlur = React.useCallback(
|
|
144
|
+
(event: Parameters<NonNullable<TextInputProps['onBlur']>>[0]) => {
|
|
145
|
+
setTouched(true)
|
|
146
|
+
notifyValidIfDisabled()
|
|
147
|
+
textInputOnBlur?.(event)
|
|
148
|
+
},
|
|
149
|
+
[notifyValidIfDisabled, textInputOnBlur]
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const handleEndEditing = React.useCallback(
|
|
153
|
+
(event: Parameters<NonNullable<TextInputProps['onEndEditing']>>[0]) => {
|
|
154
|
+
setTouched(true)
|
|
155
|
+
notifyValidIfDisabled()
|
|
156
|
+
textInputOnEndEditing?.(event)
|
|
157
|
+
},
|
|
158
|
+
[notifyValidIfDisabled, textInputOnEndEditing]
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const handleSubmitEditing = React.useCallback(
|
|
162
|
+
(event: Parameters<NonNullable<typeof onSubmitEditing>>[0]) => {
|
|
163
|
+
setTouched(true)
|
|
164
|
+
notifyValidIfDisabled()
|
|
165
|
+
|
|
166
|
+
onSubmitEditing?.(event)
|
|
167
|
+
|
|
168
|
+
if (nextInputRef?.current) {
|
|
169
|
+
nextInputRef.current.focus()
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
[nextInputRef, notifyValidIfDisabled, onSubmitEditing]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const mergedAccessibilityState = React.useMemo(
|
|
176
|
+
() => ({
|
|
177
|
+
...accessibilityState,
|
|
178
|
+
disabled: !isEditable,
|
|
179
|
+
invalid: shouldShowError
|
|
180
|
+
}),
|
|
181
|
+
[accessibilityState, isEditable, shouldShowError]
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const mergedAccessibilityValue = React.useMemo(
|
|
185
|
+
() => ({
|
|
186
|
+
...accessibilityValueProp,
|
|
187
|
+
max: maxLength,
|
|
188
|
+
min: minLength,
|
|
189
|
+
now: value.length
|
|
190
|
+
}),
|
|
191
|
+
[accessibilityValueProp, maxLength, minLength, value]
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const role = accessibilityRole ?? 'text'
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<View style={styles.wrapper}>
|
|
198
|
+
<Animated.View
|
|
199
|
+
accessible={false}
|
|
200
|
+
style={[
|
|
201
|
+
styles.container,
|
|
202
|
+
!isEditable && styles.containerDisabled,
|
|
203
|
+
shouldShowError && styles.containerError,
|
|
204
|
+
animatedStyle,
|
|
205
|
+
style
|
|
206
|
+
]}
|
|
207
|
+
testID={testID}
|
|
208
|
+
>
|
|
209
|
+
{leftIcon ? (
|
|
210
|
+
<View style={styles.iconLeft}>
|
|
211
|
+
<InputTextIcon {...leftIcon} />
|
|
212
|
+
</View>
|
|
213
|
+
) : null}
|
|
214
|
+
|
|
215
|
+
<TextInput
|
|
216
|
+
{...textInputProps}
|
|
217
|
+
accessibilityRole={role}
|
|
218
|
+
accessibilityState={mergedAccessibilityState}
|
|
219
|
+
accessibilityValue={mergedAccessibilityValue}
|
|
220
|
+
editable={isEditable}
|
|
221
|
+
keyboardType={appliedKeyboardType}
|
|
222
|
+
maxLength={maxLength}
|
|
223
|
+
onBlur={handleBlur}
|
|
224
|
+
onChangeText={handleChangeText}
|
|
225
|
+
onEndEditing={handleEndEditing}
|
|
226
|
+
onSubmitEditing={handleSubmitEditing}
|
|
227
|
+
placeholder={placeholder}
|
|
228
|
+
placeholderTextColor={placeholderColor}
|
|
229
|
+
ref={ref}
|
|
230
|
+
secureTextEntry={appliedSecureTextEntry}
|
|
231
|
+
selectionColor={theme.colors.brand.primary}
|
|
232
|
+
style={[styles.input, !isEditable && styles.inputDisabled, inputStyle]}
|
|
233
|
+
textContentType={appliedTextContentType}
|
|
234
|
+
value={displayValue}
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
{rightIcon ? (
|
|
238
|
+
<View style={styles.iconRight}>
|
|
239
|
+
<InputTextIcon {...rightIcon} />
|
|
240
|
+
</View>
|
|
241
|
+
) : null}
|
|
242
|
+
</Animated.View>
|
|
243
|
+
|
|
244
|
+
<InputTextErrorMessage
|
|
245
|
+
message={shouldShowError ? effectiveErrorMessage : undefined}
|
|
246
|
+
testID={effectiveErrorMessage ? `${testID ?? 'input-text'}-error` : undefined}
|
|
247
|
+
/>
|
|
248
|
+
</View>
|
|
249
|
+
)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
function createStyles(theme: Theme) {
|
|
253
|
+
return StyleSheet.create({
|
|
254
|
+
container: {
|
|
255
|
+
alignItems: 'center',
|
|
256
|
+
backgroundColor: 'transparent',
|
|
257
|
+
borderColor: theme.colors.brand.primary,
|
|
258
|
+
borderRadius: theme.radius.xxl,
|
|
259
|
+
borderWidth: 1,
|
|
260
|
+
flexDirection: 'row',
|
|
261
|
+
paddingHorizontal: theme.spacing.md,
|
|
262
|
+
paddingVertical: theme.spacing.sm,
|
|
263
|
+
width: '100%'
|
|
264
|
+
},
|
|
265
|
+
containerDisabled: {
|
|
266
|
+
backgroundColor: theme.colors.surface.disabled,
|
|
267
|
+
borderColor: theme.colors.border.disabled
|
|
268
|
+
},
|
|
269
|
+
containerError: {
|
|
270
|
+
borderColor: theme.colors.status.error
|
|
271
|
+
},
|
|
272
|
+
iconLeft: {
|
|
273
|
+
marginRight: theme.spacing.xs
|
|
274
|
+
},
|
|
275
|
+
iconRight: {
|
|
276
|
+
marginLeft: theme.spacing.xs
|
|
277
|
+
},
|
|
278
|
+
input: {
|
|
279
|
+
color: theme.colors.text.primary,
|
|
280
|
+
flex: 1,
|
|
281
|
+
fontFamily: theme.typography.fontFamily.body,
|
|
282
|
+
fontSize: theme.typography.fontSize.md,
|
|
283
|
+
lineHeight: theme.typography.lineHeight.bodySmall,
|
|
284
|
+
textAlign: 'center'
|
|
285
|
+
},
|
|
286
|
+
inputDisabled: {
|
|
287
|
+
color: theme.colors.text.disabled
|
|
288
|
+
},
|
|
289
|
+
wrapper: {
|
|
290
|
+
width: '100%'
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface AutoConfigOptions {
|
|
296
|
+
locale: InputTextLocale
|
|
297
|
+
maskNumber: boolean
|
|
298
|
+
type: InputTextType
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getAutoConfig({ locale, maskNumber, type }: AutoConfigOptions): AutoConfig {
|
|
302
|
+
switch (type) {
|
|
303
|
+
case 'email':
|
|
304
|
+
return {
|
|
305
|
+
keyboardType: 'email-address',
|
|
306
|
+
textContentType: 'emailAddress',
|
|
307
|
+
validator: validateEmail
|
|
308
|
+
}
|
|
309
|
+
case 'password':
|
|
310
|
+
return {
|
|
311
|
+
secureTextEntry: true,
|
|
312
|
+
textContentType: 'password'
|
|
313
|
+
}
|
|
314
|
+
case 'number':
|
|
315
|
+
return {
|
|
316
|
+
formatter: maskNumber ? value => formatNumber(value, locale) : undefined,
|
|
317
|
+
keyboardType: 'numeric',
|
|
318
|
+
parser: maskNumber ? input => parseNumberInput(input) : input => sanitizeDigits(input)
|
|
319
|
+
}
|
|
320
|
+
case 'url':
|
|
321
|
+
return {
|
|
322
|
+
keyboardType: 'url',
|
|
323
|
+
textContentType: 'URL',
|
|
324
|
+
validator: validateUrl
|
|
325
|
+
}
|
|
326
|
+
case 'tel':
|
|
327
|
+
return {
|
|
328
|
+
formatter: value => formatTel(value, locale),
|
|
329
|
+
keyboardType: 'phone-pad',
|
|
330
|
+
parser: input => parseTelInput(input, locale),
|
|
331
|
+
textContentType: 'telephoneNumber',
|
|
332
|
+
validator: value => validateTel(value, locale)
|
|
333
|
+
}
|
|
334
|
+
default:
|
|
335
|
+
return {}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function sanitizeDigits(value: string): string {
|
|
340
|
+
return value.replace(/\D/g, '')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function formatNumber(value: string, locale: InputTextLocale): string {
|
|
344
|
+
const digits = sanitizeDigits(value)
|
|
345
|
+
if (!digits) {
|
|
346
|
+
return ''
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const reversed = digits.split('').reverse()
|
|
350
|
+
const groups: string[] = []
|
|
351
|
+
|
|
352
|
+
for (let i = 0; i < reversed.length; i += 3) {
|
|
353
|
+
groups.push(
|
|
354
|
+
reversed
|
|
355
|
+
.slice(i, i + 3)
|
|
356
|
+
.reverse()
|
|
357
|
+
.join('')
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const separator = locale === 'pt' ? '.' : ','
|
|
362
|
+
return groups.reverse().join(separator)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function parseNumberInput(input: string): string {
|
|
366
|
+
return sanitizeDigits(input)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function formatTel(value: string, locale: InputTextLocale): string {
|
|
370
|
+
const digits = sanitizeDigits(value)
|
|
371
|
+
if (!digits) {
|
|
372
|
+
return ''
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (locale === 'en') {
|
|
376
|
+
const cc = digits.slice(0, 1)
|
|
377
|
+
const area = digits.slice(1, 4)
|
|
378
|
+
const first = digits.slice(4, 7)
|
|
379
|
+
const last = digits.slice(7, 11)
|
|
380
|
+
|
|
381
|
+
let result = cc ? `+${cc}` : '+'
|
|
382
|
+
if (area) {
|
|
383
|
+
result += ` (${area}`
|
|
384
|
+
if (area.length === 3) {
|
|
385
|
+
result += ')'
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (first) {
|
|
389
|
+
result += area.length < 3 ? ')' : ''
|
|
390
|
+
result += ` ${first}`
|
|
391
|
+
}
|
|
392
|
+
if (last) {
|
|
393
|
+
result += `-${last}`
|
|
394
|
+
}
|
|
395
|
+
return result.trim()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const cc = digits.slice(0, 2)
|
|
399
|
+
const area = digits.slice(2, 4)
|
|
400
|
+
const withoutArea = digits.slice(4)
|
|
401
|
+
|
|
402
|
+
let result = cc ? `+${cc}` : '+'
|
|
403
|
+
if (area) {
|
|
404
|
+
result += ` (${area}`
|
|
405
|
+
if (area.length === 2) {
|
|
406
|
+
result += ')'
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (withoutArea.length >= 5) {
|
|
411
|
+
result += area.length < 2 ? ')' : ''
|
|
412
|
+
const firstBlock = withoutArea.slice(0, withoutArea.length - 4)
|
|
413
|
+
const lastBlock = withoutArea.slice(-4)
|
|
414
|
+
result += ` ${firstBlock}-${lastBlock}`
|
|
415
|
+
} else if (withoutArea) {
|
|
416
|
+
result += area.length < 2 ? ')' : ''
|
|
417
|
+
result += ` ${withoutArea}`
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return result.trim()
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseTelInput(input: string, locale: InputTextLocale): string {
|
|
424
|
+
const digits = sanitizeDigits(input)
|
|
425
|
+
const limit = locale === 'en' ? 11 : 13
|
|
426
|
+
return digits.slice(0, limit)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function validateEmail(value: string): boolean {
|
|
430
|
+
if (!value) {
|
|
431
|
+
return false
|
|
432
|
+
}
|
|
433
|
+
const regex = /.+@.+\..+/
|
|
434
|
+
return regex.test(value)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function validateUrl(value: string): boolean {
|
|
438
|
+
if (!value) {
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
const regex = /^(https?:\/\/).+/i
|
|
442
|
+
return regex.test(value)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function validateTel(value: string, locale: InputTextLocale): boolean {
|
|
446
|
+
const digits = sanitizeDigits(value)
|
|
447
|
+
if (locale === 'en') {
|
|
448
|
+
return digits.length === 11
|
|
449
|
+
}
|
|
450
|
+
return digits.length >= 12 && digits.length <= 13
|
|
451
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { StyleProp, TextInput, TextInputProps, TextStyle, ViewStyle } from 'react-native'
|
|
4
|
+
|
|
5
|
+
import type { IconName } from '@wecareu/icons'
|
|
6
|
+
|
|
7
|
+
export type InputTextType = 'text' | 'email' | 'password' | 'number' | 'url' | 'tel'
|
|
8
|
+
|
|
9
|
+
export type InputTextLocale = 'pt' | 'en'
|
|
10
|
+
|
|
11
|
+
export interface InputTextErrorMessageProps {
|
|
12
|
+
/**
|
|
13
|
+
* Text displayed underneath the field when it is in error state
|
|
14
|
+
*/
|
|
15
|
+
message?: string
|
|
16
|
+
/**
|
|
17
|
+
* Optional identifier exposed for automated tests
|
|
18
|
+
*/
|
|
19
|
+
testID?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface InputTextIconProps {
|
|
23
|
+
/**
|
|
24
|
+
* Accessibility label announced for screen readers
|
|
25
|
+
*/
|
|
26
|
+
accessibilityLabel?: string
|
|
27
|
+
/**
|
|
28
|
+
* Icon tint color. Falls back to theme.colors.text.tertiary when undefined
|
|
29
|
+
*/
|
|
30
|
+
color?: string
|
|
31
|
+
/**
|
|
32
|
+
* Icon identifier available in WeCareU icons package
|
|
33
|
+
*/
|
|
34
|
+
name: IconName
|
|
35
|
+
/**
|
|
36
|
+
* Press handler triggered when icon is tapped
|
|
37
|
+
*/
|
|
38
|
+
onPress?: () => void
|
|
39
|
+
/**
|
|
40
|
+
* Icon size in pixels. Defaults to theme.spacing.lg
|
|
41
|
+
*/
|
|
42
|
+
size?: number
|
|
43
|
+
/**
|
|
44
|
+
* Testing identifier used in automated tests
|
|
45
|
+
*/
|
|
46
|
+
testID?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface InputTextProps
|
|
50
|
+
extends Omit<TextInputProps, 'editable' | 'onChangeText' | 'placeholderTextColor' | 'style' | 'value'> {
|
|
51
|
+
/**
|
|
52
|
+
* Callback invoked with the raw value (without mask) whenever text changes
|
|
53
|
+
*/
|
|
54
|
+
onChangeText: (value: string) => void
|
|
55
|
+
/**
|
|
56
|
+
* Defines the controlled value without mask formatting
|
|
57
|
+
*/
|
|
58
|
+
value: string
|
|
59
|
+
/**
|
|
60
|
+
* Defines locale used for automatic formatting/validation when type requires it
|
|
61
|
+
*/
|
|
62
|
+
locale?: InputTextLocale
|
|
63
|
+
/**
|
|
64
|
+
* Enables automatic masking for numeric inputs (thousands separators)
|
|
65
|
+
*/
|
|
66
|
+
maskNumber?: boolean
|
|
67
|
+
/**
|
|
68
|
+
* Semantic type used to infer validation and masking rules
|
|
69
|
+
*/
|
|
70
|
+
type?: InputTextType
|
|
71
|
+
/**
|
|
72
|
+
* Displays a disabled appearance and blocks user interaction
|
|
73
|
+
*/
|
|
74
|
+
disabled?: boolean
|
|
75
|
+
/**
|
|
76
|
+
* Optional error message rendered below the field when in error state
|
|
77
|
+
*/
|
|
78
|
+
errorMessage?: string
|
|
79
|
+
/**
|
|
80
|
+
* Transforms the controlled raw value into a formatted string for display
|
|
81
|
+
*/
|
|
82
|
+
formatter?: (value: string) => string
|
|
83
|
+
/**
|
|
84
|
+
* Shows the component using error tokens, triggering shake animation
|
|
85
|
+
*/
|
|
86
|
+
inputError?: boolean
|
|
87
|
+
/**
|
|
88
|
+
* Applies style overrides directly to the TextInput element
|
|
89
|
+
*/
|
|
90
|
+
inputStyle?: StyleProp<TextStyle>
|
|
91
|
+
/**
|
|
92
|
+
* Icon rendered on the left side of the field
|
|
93
|
+
*/
|
|
94
|
+
leftIcon?: InputTextIconProps
|
|
95
|
+
/**
|
|
96
|
+
* Maximum length allowed for the displayed value. Defaults to 255
|
|
97
|
+
*/
|
|
98
|
+
maxLength?: number
|
|
99
|
+
/**
|
|
100
|
+
* Minimum length expectation for the raw value. Defaults to 3
|
|
101
|
+
*/
|
|
102
|
+
minLength?: number
|
|
103
|
+
/**
|
|
104
|
+
* Reference to the next input triggered when submitting the keyboard action
|
|
105
|
+
*/
|
|
106
|
+
nextInputRef?: RefObject<TextInput>
|
|
107
|
+
/**
|
|
108
|
+
* Receives the validation status whenever internal validation runs.
|
|
109
|
+
*/
|
|
110
|
+
onValidation?: (isValid: boolean) => void
|
|
111
|
+
/**
|
|
112
|
+
* Parses the masked value typed by the user back into raw format
|
|
113
|
+
*/
|
|
114
|
+
parser?: (value: string) => string
|
|
115
|
+
/**
|
|
116
|
+
* Placeholder string displayed when empty
|
|
117
|
+
*/
|
|
118
|
+
placeholder?: string
|
|
119
|
+
/**
|
|
120
|
+
* Renders the field as read-only, mirroring disabled visuals
|
|
121
|
+
*/
|
|
122
|
+
readonly?: boolean
|
|
123
|
+
/**
|
|
124
|
+
* Icon rendered on the right side of the field
|
|
125
|
+
*/
|
|
126
|
+
rightIcon?: InputTextIconProps
|
|
127
|
+
/**
|
|
128
|
+
* Applies style overrides to the container View
|
|
129
|
+
*/
|
|
130
|
+
style?: StyleProp<ViewStyle>
|
|
131
|
+
/**
|
|
132
|
+
* Test identifier exposed on the container
|
|
133
|
+
*/
|
|
134
|
+
testID?: string
|
|
135
|
+
/**
|
|
136
|
+
* Enables internal validation when the field loses focus.
|
|
137
|
+
* Defaults to true.
|
|
138
|
+
*/
|
|
139
|
+
validateOnBlur?: boolean
|
|
140
|
+
/**
|
|
141
|
+
* Message displayed when internal validation fails (min/max length).
|
|
142
|
+
*/
|
|
143
|
+
validationMessage?: string
|
|
144
|
+
/**
|
|
145
|
+
* Custom validator executed when validateOnBlur is enabled.
|
|
146
|
+
* Return true for valid values.
|
|
147
|
+
*/
|
|
148
|
+
validator?: (value: string) => boolean
|
|
149
|
+
/**
|
|
150
|
+
* Message displayed when custom validator returns false.
|
|
151
|
+
*/
|
|
152
|
+
validatorMessage?: string
|
|
153
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Text } from 'react-native'
|
|
4
|
+
|
|
5
|
+
import { useTheme } from '@wecareu/theme'
|
|
6
|
+
|
|
7
|
+
import type { InputTextErrorMessageProps } from './InputText.types'
|
|
8
|
+
|
|
9
|
+
export function InputTextErrorMessage({ message, testID }: InputTextErrorMessageProps): JSX.Element | null {
|
|
10
|
+
const theme = useTheme()
|
|
11
|
+
const errorTextStyle = React.useMemo(
|
|
12
|
+
() => ({
|
|
13
|
+
color: theme.colors.status.error,
|
|
14
|
+
fontFamily: theme.typography.fontFamily.caption,
|
|
15
|
+
fontSize: theme.typography.fontSize.sm,
|
|
16
|
+
lineHeight: theme.typography.lineHeight.labelMedium,
|
|
17
|
+
marginTop: theme.spacing.xs
|
|
18
|
+
}),
|
|
19
|
+
[theme]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if (!message) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Text accessibilityLiveRegion='polite' style={errorTextStyle} testID={testID}>
|
|
28
|
+
{message}
|
|
29
|
+
</Text>
|
|
30
|
+
)
|
|
31
|
+
}
|