@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.
Files changed (49) hide show
  1. package/README.md +36 -0
  2. package/lib/commonjs/InputText.js +350 -0
  3. package/lib/commonjs/InputText.js.map +1 -0
  4. package/lib/commonjs/InputText.types.js +6 -0
  5. package/lib/commonjs/InputText.types.js.map +1 -0
  6. package/lib/commonjs/InputTextErrorMessage.js +32 -0
  7. package/lib/commonjs/InputTextErrorMessage.js.map +1 -0
  8. package/lib/commonjs/InputTextIcon.js +65 -0
  9. package/lib/commonjs/InputTextIcon.js.map +1 -0
  10. package/lib/commonjs/animations/shake.js +45 -0
  11. package/lib/commonjs/animations/shake.js.map +1 -0
  12. package/lib/commonjs/index.js +13 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/index.stories.js +409 -0
  15. package/lib/commonjs/index.stories.js.map +1 -0
  16. package/lib/module/InputText.js +343 -0
  17. package/lib/module/InputText.js.map +1 -0
  18. package/lib/module/InputText.types.js +2 -0
  19. package/lib/module/InputText.types.js.map +1 -0
  20. package/lib/module/InputTextErrorMessage.js +25 -0
  21. package/lib/module/InputTextErrorMessage.js.map +1 -0
  22. package/lib/module/InputTextIcon.js +58 -0
  23. package/lib/module/InputTextIcon.js.map +1 -0
  24. package/lib/module/animations/shake.js +39 -0
  25. package/lib/module/animations/shake.js.map +1 -0
  26. package/lib/module/index.js +2 -0
  27. package/lib/module/index.js.map +1 -0
  28. package/lib/module/index.stories.js +402 -0
  29. package/lib/module/index.stories.js.map +1 -0
  30. package/lib/typescript/src/InputText.d.ts +5 -0
  31. package/lib/typescript/src/InputText.d.ts.map +1 -0
  32. package/lib/typescript/src/InputText.types.d.ts +146 -0
  33. package/lib/typescript/src/InputText.types.d.ts.map +1 -0
  34. package/lib/typescript/src/InputTextErrorMessage.d.ts +3 -0
  35. package/lib/typescript/src/InputTextErrorMessage.d.ts.map +1 -0
  36. package/lib/typescript/src/InputTextIcon.d.ts +3 -0
  37. package/lib/typescript/src/InputTextIcon.d.ts.map +1 -0
  38. package/lib/typescript/src/animations/shake.d.ts +32 -0
  39. package/lib/typescript/src/animations/shake.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +3 -0
  41. package/lib/typescript/src/index.d.ts.map +1 -0
  42. package/package.json +66 -0
  43. package/src/InputText.tsx +451 -0
  44. package/src/InputText.types.ts +153 -0
  45. package/src/InputTextErrorMessage.tsx +31 -0
  46. package/src/InputTextIcon.tsx +65 -0
  47. package/src/animations/shake.ts +76 -0
  48. package/src/index.stories.tsx +387 -0
  49. 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
+ }