@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
@@ -0,0 +1,65 @@
1
+ import React from 'react'
2
+
3
+ import { Pressable, StyleSheet, TouchableOpacity, View } from 'react-native'
4
+
5
+ import { Icon } from '@wecareu/icons'
6
+ import { useTheme } from '@wecareu/theme'
7
+
8
+ import type { InputTextIconProps } from './InputText.types'
9
+
10
+ export function InputTextIcon({
11
+ accessibilityLabel,
12
+ color,
13
+ name,
14
+ onPress,
15
+ size,
16
+ testID
17
+ }: InputTextIconProps): JSX.Element {
18
+ const theme = useTheme()
19
+
20
+ const iconColor = color ?? theme.colors.text.tertiary
21
+ const iconSize = size ?? theme.spacing.lg
22
+ const hitSlop = React.useMemo(
23
+ () => ({
24
+ bottom: theme.spacing.xs,
25
+ left: theme.spacing.xs,
26
+ right: theme.spacing.xs,
27
+ top: theme.spacing.xs
28
+ }),
29
+ [theme.spacing.xs]
30
+ )
31
+
32
+ if (onPress) {
33
+ const InteractiveComponent = Pressable ?? TouchableOpacity
34
+
35
+ return (
36
+ <InteractiveComponent
37
+ accessibilityLabel={accessibilityLabel ?? name}
38
+ accessibilityRole='button'
39
+ hitSlop={hitSlop}
40
+ onPress={onPress}
41
+ style={styles.iconPressable}
42
+ testID={testID}
43
+ >
44
+ <Icon color={iconColor} name={name} size={iconSize} />
45
+ </InteractiveComponent>
46
+ )
47
+ }
48
+
49
+ return (
50
+ <View accessible={false} style={styles.iconWrapper} testID={testID}>
51
+ <Icon accessibilityLabel={accessibilityLabel} color={iconColor} name={name} size={iconSize} />
52
+ </View>
53
+ )
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ iconPressable: {
58
+ alignItems: 'center',
59
+ justifyContent: 'center'
60
+ },
61
+ iconWrapper: {
62
+ alignItems: 'center',
63
+ justifyContent: 'center'
64
+ }
65
+ })
@@ -0,0 +1,76 @@
1
+ import { useCallback, useMemo, useRef } from 'react'
2
+
3
+ import { Animated } from 'react-native'
4
+
5
+ interface UseShakeAnimationParams {
6
+ /**
7
+ * Distance in pixels applied to the shake translation
8
+ */
9
+ distance?: number
10
+ /**
11
+ * Duration of each shake step in milliseconds
12
+ */
13
+ duration?: number
14
+ /**
15
+ * Number of oscillations executed per trigger
16
+ */
17
+ iterations?: number
18
+ }
19
+
20
+ interface UseShakeAnimationReturn {
21
+ /**
22
+ * Animated style applied to the container that needs feedback
23
+ */
24
+ animatedStyle: {
25
+ transform: { translateX: Animated.AnimatedInterpolation<number> }[]
26
+ }
27
+ /**
28
+ * Function responsible for starting the shake effect
29
+ */
30
+ triggerShake: () => void
31
+ }
32
+
33
+ export function useShakeAnimation({
34
+ distance = 6,
35
+ duration = 60,
36
+ iterations = 2
37
+ }: UseShakeAnimationParams = {}): UseShakeAnimationReturn {
38
+ const translation = useRef(new Animated.Value(0)).current
39
+
40
+ const animatedStyle = useMemo(
41
+ () => ({
42
+ transform: [
43
+ {
44
+ translateX: translation.interpolate({
45
+ inputRange: [-1, -0.5, 0, 0.5, 1],
46
+ outputRange: [-distance, -distance / 2, 0, distance / 2, distance]
47
+ })
48
+ }
49
+ ]
50
+ }),
51
+ [distance, translation]
52
+ )
53
+
54
+ const triggerShake = useCallback(() => {
55
+ translation.setValue(0)
56
+
57
+ const animation = Animated.sequence([
58
+ Animated.timing(translation, {
59
+ duration,
60
+ toValue: 1,
61
+ useNativeDriver: true
62
+ }),
63
+ Animated.timing(translation, {
64
+ duration,
65
+ toValue: -1,
66
+ useNativeDriver: true
67
+ })
68
+ ])
69
+
70
+ Animated.loop(animation, { iterations }).start(() => {
71
+ translation.setValue(0)
72
+ })
73
+ }, [duration, iterations, translation])
74
+
75
+ return { animatedStyle, triggerShake }
76
+ }
@@ -0,0 +1,387 @@
1
+ import React from 'react'
2
+
3
+ import { StyleSheet, TextInput, View } from 'react-native'
4
+ import type { KeyboardTypeOptions } from 'react-native'
5
+
6
+ import { action } from '@storybook/addon-actions'
7
+ import type { Meta, StoryObj } from '@storybook/react'
8
+
9
+ import { ICON_NAMES } from '@wecareu/icons'
10
+
11
+ import { InputText } from '.'
12
+ import type { InputTextProps } from './InputText.types'
13
+
14
+ const ICON_OPTIONS = ['none', ...ICON_NAMES] as const
15
+ const INPUT_TYPE_OPTIONS = ['text', 'email', 'password', 'number', 'phone', 'url'] as const
16
+ const KEYBOARD_OPTIONS: KeyboardTypeOptions[] = [
17
+ 'default',
18
+ 'email-address',
19
+ 'numeric',
20
+ 'phone-pad',
21
+ 'url',
22
+ 'visible-password'
23
+ ]
24
+ const TEXT_CONTENT_OPTIONS = [
25
+ 'none',
26
+ 'emailAddress',
27
+ 'username',
28
+ 'password',
29
+ 'oneTimeCode',
30
+ 'telephoneNumber',
31
+ 'URL'
32
+ ] as const
33
+
34
+ interface StatefulInputProps extends Omit<InputTextProps, 'onChangeText' | 'value'> {
35
+ initialValue?: string
36
+ onChangeText: (value: string) => void
37
+ }
38
+
39
+ interface DefaultStoryArgs extends StatefulInputProps {
40
+ inputType: (typeof INPUT_TYPE_OPTIONS)[number]
41
+ keyboardType: KeyboardTypeOptions
42
+ leftIconName?: (typeof ICON_OPTIONS)[number]
43
+ rightIconName?: (typeof ICON_OPTIONS)[number]
44
+ textContentType?: (typeof TEXT_CONTENT_OPTIONS)[number]
45
+ validatorPattern?: string
46
+ }
47
+
48
+ type Story = StoryObj<DefaultStoryArgs>
49
+
50
+ function StatefulInput({ initialValue = '', onChangeText, ...rest }: StatefulInputProps) {
51
+ const [value, setValue] = React.useState(initialValue)
52
+
53
+ const handleChange = React.useCallback(
54
+ (nextValue: string) => {
55
+ setValue(nextValue)
56
+ onChangeText(nextValue)
57
+ },
58
+ [onChangeText]
59
+ )
60
+
61
+ return <InputText {...rest} onChangeText={handleChange} value={value} />
62
+ }
63
+
64
+ const meta: Meta<DefaultStoryArgs> = {
65
+ args: {
66
+ disabled: false,
67
+ initialValue: '',
68
+ inputError: false,
69
+ inputType: 'text',
70
+ keyboardType: 'default',
71
+ leftIconName: 'none',
72
+ locale: 'pt',
73
+ maskNumber: false,
74
+ maxLength: 255,
75
+ minLength: 3,
76
+ onChangeText: action('onChangeText'),
77
+ placeholder: 'Digite seu texto',
78
+ readonly: false,
79
+ rightIconName: 'none',
80
+ secureTextEntry: false,
81
+ textContentType: 'none',
82
+ validateOnBlur: true,
83
+ validationMessage: 'Digite ao menos 3 caracteres',
84
+ validatorMessage: 'Informe um valor válido',
85
+ validatorPattern: ''
86
+ },
87
+ argTypes: {
88
+ disabled: { control: 'boolean' },
89
+ formatter: { control: false },
90
+ initialValue: { control: 'text' },
91
+ inputError: { control: 'boolean' },
92
+ inputStyle: { control: false },
93
+ inputType: {
94
+ control: { type: 'select' },
95
+ options: INPUT_TYPE_OPTIONS
96
+ },
97
+ keyboardType: {
98
+ control: { type: 'select' },
99
+ options: KEYBOARD_OPTIONS
100
+ },
101
+ leftIcon: { control: false },
102
+ leftIconName: {
103
+ control: { type: 'select' },
104
+ options: ICON_OPTIONS
105
+ },
106
+ locale: {
107
+ control: { type: 'select' },
108
+ options: ['pt', 'en']
109
+ },
110
+ maskNumber: { control: 'boolean' },
111
+ maxLength: { control: { min: 1, step: 1, type: 'number' } },
112
+ minLength: { control: { min: 0, step: 1, type: 'number' } },
113
+ nextInputRef: { control: false },
114
+ onChangeText: { control: false },
115
+ parser: { control: false },
116
+ placeholder: { control: 'text' },
117
+ readonly: { control: 'boolean' },
118
+ rightIcon: { control: false },
119
+ rightIconName: {
120
+ control: { type: 'select' },
121
+ options: ICON_OPTIONS
122
+ },
123
+ secureTextEntry: { control: 'boolean' },
124
+ style: { control: false },
125
+ textContentType: {
126
+ control: { type: 'select' },
127
+ options: TEXT_CONTENT_OPTIONS
128
+ },
129
+ validateOnBlur: { control: 'boolean' },
130
+ validationMessage: { control: 'text' },
131
+ validator: { control: false },
132
+ validatorMessage: { control: 'text' },
133
+ validatorPattern: { control: 'text' },
134
+ value: { control: false }
135
+ },
136
+ component: InputText,
137
+ parameters: {
138
+ layout: 'centered'
139
+ },
140
+ tags: ['autodocs'],
141
+ title: 'Forms/InputText'
142
+ }
143
+
144
+ export default meta
145
+
146
+ export const Default: Story = {
147
+ render: args => {
148
+ const {
149
+ initialValue,
150
+ inputType,
151
+ keyboardType,
152
+ leftIconName,
153
+ locale,
154
+ maskNumber,
155
+ rightIconName,
156
+ textContentType,
157
+ validatorPattern,
158
+ validatorMessage,
159
+ ...inputArgs
160
+ } = args
161
+
162
+ let derivedPlaceholder = inputArgs.placeholder
163
+ let derivedSecureTextEntry = inputArgs.secureTextEntry
164
+ let derivedKeyboardType = keyboardType
165
+ let derivedTextContentType = textContentType
166
+ let derivedValidatorPattern = validatorPattern
167
+ let derivedValidatorMessage = validatorMessage
168
+
169
+ switch (inputType) {
170
+ case 'email':
171
+ derivedPlaceholder = 'Email'
172
+ derivedSecureTextEntry = false
173
+ derivedKeyboardType = 'email-address'
174
+ derivedTextContentType = 'emailAddress'
175
+ derivedValidatorPattern = '.+@.+\\..+'
176
+ derivedValidatorMessage = validatorMessage || 'Informe um email válido'
177
+ break
178
+ case 'password':
179
+ derivedPlaceholder = 'Senha'
180
+ derivedSecureTextEntry = true
181
+ derivedKeyboardType = 'visible-password'
182
+ derivedTextContentType = 'password'
183
+ derivedValidatorPattern = validatorPattern
184
+ derivedValidatorMessage = validatorMessage || 'Informe uma senha válida'
185
+ break
186
+ case 'number':
187
+ derivedPlaceholder = 'Número'
188
+ derivedSecureTextEntry = false
189
+ derivedKeyboardType = 'numeric'
190
+ derivedTextContentType = 'none'
191
+ derivedValidatorPattern = validatorPattern || '^[0-9]+$'
192
+ derivedValidatorMessage = validatorMessage || 'Informe apenas números'
193
+ break
194
+ case 'phone':
195
+ derivedPlaceholder = 'Telefone'
196
+ derivedSecureTextEntry = false
197
+ derivedKeyboardType = 'phone-pad'
198
+ derivedTextContentType = 'telephoneNumber'
199
+ derivedValidatorPattern = validatorPattern || '^[0-9()\\s+-]{6,}$'
200
+ derivedValidatorMessage = validatorMessage || 'Informe um telefone válido'
201
+ break
202
+ case 'url':
203
+ derivedPlaceholder = 'URL'
204
+ derivedSecureTextEntry = false
205
+ derivedKeyboardType = 'url'
206
+ derivedTextContentType = 'URL'
207
+ derivedValidatorPattern = validatorPattern || '^(https?:\\/\\/).+'
208
+ derivedValidatorMessage = validatorMessage || 'Informe uma URL válida'
209
+ break
210
+ default:
211
+ derivedPlaceholder = derivedPlaceholder || 'Digite seu texto'
212
+ derivedSecureTextEntry = false
213
+ derivedKeyboardType = 'default'
214
+ derivedTextContentType = 'none'
215
+ derivedValidatorPattern = validatorPattern
216
+ derivedValidatorMessage = validatorMessage || 'Informe um valor válido'
217
+ break
218
+ }
219
+
220
+ let patternValidator: ((value: string) => boolean) | undefined
221
+
222
+ if (derivedValidatorPattern && derivedValidatorPattern.trim().length > 0) {
223
+ try {
224
+ const regex = new RegExp(derivedValidatorPattern)
225
+ patternValidator = value => regex.test(value)
226
+ } catch {
227
+ patternValidator = undefined
228
+ }
229
+ }
230
+
231
+ const leftIcon = leftIconName && leftIconName !== 'none' ? { name: leftIconName } : undefined
232
+ const rightIcon = rightIconName && rightIconName !== 'none' ? { name: rightIconName } : undefined
233
+
234
+ const finalValidator = patternValidator ?? inputArgs.validator
235
+ const finalValidatorMessage = patternValidator ? derivedValidatorMessage : inputArgs.validatorMessage
236
+
237
+ return (
238
+ <View style={styles.container}>
239
+ <StatefulInput
240
+ {...inputArgs}
241
+ initialValue={initialValue ?? ''}
242
+ keyboardType={derivedKeyboardType}
243
+ leftIcon={leftIcon}
244
+ locale={locale}
245
+ maskNumber={maskNumber}
246
+ placeholder={derivedPlaceholder}
247
+ rightIcon={rightIcon}
248
+ secureTextEntry={derivedSecureTextEntry}
249
+ textContentType={derivedTextContentType === 'none' ? undefined : derivedTextContentType}
250
+ type={inputType}
251
+ validator={finalValidator}
252
+ validatorMessage={finalValidatorMessage}
253
+ />
254
+ </View>
255
+ )
256
+ }
257
+ }
258
+
259
+ export const WithError: Story = {
260
+ render: args => (
261
+ <View style={styles.container}>
262
+ <StatefulInput {...args} errorMessage='Campo obrigatório' initialValue='texto' inputError />
263
+ </View>
264
+ )
265
+ }
266
+
267
+ export const Disabled: Story = {
268
+ render: args => (
269
+ <View style={styles.container}>
270
+ <StatefulInput {...args} disabled initialValue='texto desabilitado' />
271
+ </View>
272
+ )
273
+ }
274
+
275
+ export const Readonly: Story = {
276
+ render: args => (
277
+ <View style={styles.container}>
278
+ <StatefulInput {...args} initialValue='valor somente leitura' readonly />
279
+ </View>
280
+ )
281
+ }
282
+
283
+ export const WithLeftIcon: Story = {
284
+ render: args => (
285
+ <View style={styles.container}>
286
+ <StatefulInput {...args} initialValue='' leftIcon={{ name: 'search' }} placeholder='Buscar' />
287
+ </View>
288
+ )
289
+ }
290
+
291
+ export const WithRightIcon: Story = {
292
+ render: args => (
293
+ <View style={styles.container}>
294
+ <StatefulInput {...args} initialValue='andre@wecareu.com' placeholder='Email' rightIcon={{ name: 'mail' }} />
295
+ </View>
296
+ )
297
+ }
298
+
299
+ export const PasswordToggle: Story = {
300
+ render: args => {
301
+ const [secure, setSecure] = React.useState(true)
302
+
303
+ return (
304
+ <View style={styles.container}>
305
+ <StatefulInput
306
+ {...args}
307
+ initialValue=''
308
+ placeholder='Senha'
309
+ rightIcon={{
310
+ accessibilityLabel: secure ? 'Mostrar senha' : 'Ocultar senha',
311
+ name: secure ? 'eyeOff' : 'eyeOn',
312
+ onPress: () => setSecure(previous => !previous)
313
+ }}
314
+ secureTextEntry={secure}
315
+ />
316
+ </View>
317
+ )
318
+ }
319
+ }
320
+
321
+ export const WithMask: Story = {
322
+ render: args => {
323
+ const formatter = React.useCallback((raw: string) => {
324
+ const digits = raw.replace(/\D/g, '').slice(0, 11)
325
+ return digits
326
+ .replace(/(\d{3})(\d)/, '$1.$2')
327
+ .replace(/(\d{3})(\d)/, '$1.$2')
328
+ .replace(/(\d{3})(\d{1,2})$/, '$1-$2')
329
+ }, [])
330
+
331
+ const parser = React.useCallback((masked: string) => masked.replace(/\D/g, ''), [])
332
+
333
+ return (
334
+ <View style={styles.container}>
335
+ <StatefulInput {...args} formatter={formatter} initialValue='12345678901' placeholder='CPF' parser={parser} />
336
+ </View>
337
+ )
338
+ }
339
+ }
340
+
341
+ export const FocusChain: Story = {
342
+ render: args => {
343
+ const secondInputRef = React.useRef<TextInput>(null)
344
+ const [first, setFirst] = React.useState('')
345
+ const [second, setSecond] = React.useState('')
346
+ const { onChangeText, ...rest } = args
347
+
348
+ return (
349
+ <View style={styles.column}>
350
+ <InputText
351
+ {...rest}
352
+ nextInputRef={secondInputRef}
353
+ onChangeText={value => {
354
+ setFirst(value)
355
+ onChangeText(value)
356
+ }}
357
+ placeholder='Primeiro campo'
358
+ value={first}
359
+ />
360
+ <View style={styles.columnSpacing}>
361
+ <InputText
362
+ {...rest}
363
+ onChangeText={value => {
364
+ setSecond(value)
365
+ onChangeText(value)
366
+ }}
367
+ placeholder='Segundo campo'
368
+ ref={secondInputRef}
369
+ value={second}
370
+ />
371
+ </View>
372
+ </View>
373
+ )
374
+ }
375
+ }
376
+
377
+ const styles = StyleSheet.create({
378
+ column: {
379
+ width: '100%'
380
+ },
381
+ columnSpacing: {
382
+ marginTop: 16
383
+ },
384
+ container: {
385
+ width: 320
386
+ }
387
+ })
package/src/index.tsx ADDED
@@ -0,0 +1,2 @@
1
+ export { InputText } from './InputText'
2
+ export type { InputTextErrorMessageProps, InputTextIconProps, InputTextProps } from './InputText.types'