@wecareu/select 0.1.0

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 (77) hide show
  1. package/README.md +165 -0
  2. package/lib/commonjs/Select.js +272 -0
  3. package/lib/commonjs/Select.js.map +1 -0
  4. package/lib/commonjs/Select.types.js +6 -0
  5. package/lib/commonjs/Select.types.js.map +1 -0
  6. package/lib/commonjs/SelectChip.js +74 -0
  7. package/lib/commonjs/SelectChip.js.map +1 -0
  8. package/lib/commonjs/SelectDropdown.js +156 -0
  9. package/lib/commonjs/SelectDropdown.js.map +1 -0
  10. package/lib/commonjs/SelectErrorMessage.js +32 -0
  11. package/lib/commonjs/SelectErrorMessage.js.map +1 -0
  12. package/lib/commonjs/SelectIcon.js +64 -0
  13. package/lib/commonjs/SelectIcon.js.map +1 -0
  14. package/lib/commonjs/SelectOption.js +140 -0
  15. package/lib/commonjs/SelectOption.js.map +1 -0
  16. package/lib/commonjs/SelectSearch.js +79 -0
  17. package/lib/commonjs/SelectSearch.js.map +1 -0
  18. package/lib/commonjs/animations/shake.js +45 -0
  19. package/lib/commonjs/animations/shake.js.map +1 -0
  20. package/lib/commonjs/index.js +13 -0
  21. package/lib/commonjs/index.js.map +1 -0
  22. package/lib/commonjs/index.stories.js +383 -0
  23. package/lib/commonjs/index.stories.js.map +1 -0
  24. package/lib/module/Select.js +265 -0
  25. package/lib/module/Select.js.map +1 -0
  26. package/lib/module/Select.types.js +2 -0
  27. package/lib/module/Select.types.js.map +1 -0
  28. package/lib/module/SelectChip.js +67 -0
  29. package/lib/module/SelectChip.js.map +1 -0
  30. package/lib/module/SelectDropdown.js +149 -0
  31. package/lib/module/SelectDropdown.js.map +1 -0
  32. package/lib/module/SelectErrorMessage.js +25 -0
  33. package/lib/module/SelectErrorMessage.js.map +1 -0
  34. package/lib/module/SelectIcon.js +57 -0
  35. package/lib/module/SelectIcon.js.map +1 -0
  36. package/lib/module/SelectOption.js +133 -0
  37. package/lib/module/SelectOption.js.map +1 -0
  38. package/lib/module/SelectSearch.js +72 -0
  39. package/lib/module/SelectSearch.js.map +1 -0
  40. package/lib/module/animations/shake.js +39 -0
  41. package/lib/module/animations/shake.js.map +1 -0
  42. package/lib/module/index.js +2 -0
  43. package/lib/module/index.js.map +1 -0
  44. package/lib/module/index.stories.js +376 -0
  45. package/lib/module/index.stories.js.map +1 -0
  46. package/lib/typescript/src/Select.d.ts +3 -0
  47. package/lib/typescript/src/Select.d.ts.map +1 -0
  48. package/lib/typescript/src/Select.types.d.ts +245 -0
  49. package/lib/typescript/src/Select.types.d.ts.map +1 -0
  50. package/lib/typescript/src/SelectChip.d.ts +3 -0
  51. package/lib/typescript/src/SelectChip.d.ts.map +1 -0
  52. package/lib/typescript/src/SelectDropdown.d.ts +3 -0
  53. package/lib/typescript/src/SelectDropdown.d.ts.map +1 -0
  54. package/lib/typescript/src/SelectErrorMessage.d.ts +3 -0
  55. package/lib/typescript/src/SelectErrorMessage.d.ts.map +1 -0
  56. package/lib/typescript/src/SelectIcon.d.ts +3 -0
  57. package/lib/typescript/src/SelectIcon.d.ts.map +1 -0
  58. package/lib/typescript/src/SelectOption.d.ts +3 -0
  59. package/lib/typescript/src/SelectOption.d.ts.map +1 -0
  60. package/lib/typescript/src/SelectSearch.d.ts +3 -0
  61. package/lib/typescript/src/SelectSearch.d.ts.map +1 -0
  62. package/lib/typescript/src/animations/shake.d.ts +32 -0
  63. package/lib/typescript/src/animations/shake.d.ts.map +1 -0
  64. package/lib/typescript/src/index.d.ts +3 -0
  65. package/lib/typescript/src/index.d.ts.map +1 -0
  66. package/package.json +66 -0
  67. package/src/Select.tsx +338 -0
  68. package/src/Select.types.ts +256 -0
  69. package/src/SelectChip.tsx +75 -0
  70. package/src/SelectDropdown.tsx +184 -0
  71. package/src/SelectErrorMessage.tsx +31 -0
  72. package/src/SelectIcon.tsx +63 -0
  73. package/src/SelectOption.tsx +168 -0
  74. package/src/SelectSearch.tsx +81 -0
  75. package/src/animations/shake.ts +76 -0
  76. package/src/index.stories.tsx +336 -0
  77. package/src/index.tsx +2 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectIcon.d.ts","sourceRoot":"","sources":["../../../src/SelectIcon.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD,wBAAgB,UAAU,CAAC,EACzB,kBAAkB,EAClB,KAAK,EACL,IAAI,EACJ,OAAO,EACP,IAAI,EACJ,MAAM,EACP,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CAmC/B"}
@@ -0,0 +1,3 @@
1
+ import type { SelectOptionItemProps } from './Select.types';
2
+ export declare function SelectOptionItem({ disabled, isMulti, isSelected, onPress, option }: SelectOptionItemProps): JSX.Element;
3
+ //# sourceMappingURL=SelectOption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectOption.d.ts","sourceRoot":"","sources":["../../../src/SelectOption.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE3D,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,OAAO,EACP,UAAU,EACV,OAAO,EACP,MAAM,EACP,EAAE,qBAAqB,GAAG,GAAG,CAAC,OAAO,CAkFrC"}
@@ -0,0 +1,3 @@
1
+ import type { SelectSearchProps } from './Select.types';
2
+ export declare function SelectSearch({ onChangeText, value }: SelectSearchProps): JSX.Element;
3
+ //# sourceMappingURL=SelectSearch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectSearch.d.ts","sourceRoot":"","sources":["../../../src/SelectSearch.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAIvD,wBAAgB,YAAY,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,iBAAiB,GAAG,GAAG,CAAC,OAAO,CA2CpF"}
@@ -0,0 +1,32 @@
1
+ import { Animated } from 'react-native';
2
+ interface UseShakeAnimationParams {
3
+ /**
4
+ * Distance in pixels applied to the shake translation
5
+ */
6
+ distance?: number;
7
+ /**
8
+ * Duration of each shake step in milliseconds
9
+ */
10
+ duration?: number;
11
+ /**
12
+ * Number of oscillations executed per trigger
13
+ */
14
+ iterations?: number;
15
+ }
16
+ interface UseShakeAnimationReturn {
17
+ /**
18
+ * Animated style applied to the container that needs feedback
19
+ */
20
+ animatedStyle: {
21
+ transform: {
22
+ translateX: Animated.AnimatedInterpolation<number>;
23
+ }[];
24
+ };
25
+ /**
26
+ * Function responsible for starting the shake effect
27
+ */
28
+ triggerShake: () => void;
29
+ }
30
+ export declare function useShakeAnimation({ distance, duration, iterations }?: UseShakeAnimationParams): UseShakeAnimationReturn;
31
+ export {};
32
+ //# sourceMappingURL=shake.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shake.d.ts","sourceRoot":"","sources":["../../../../src/animations/shake.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEvC,UAAU,uBAAuB;IAC/B;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,UAAU,uBAAuB;IAC/B;;OAEG;IACH,aAAa,EAAE;QACb,SAAS,EAAE;YAAE,UAAU,EAAE,QAAQ,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAA;SAAE,EAAE,CAAA;KACpE,CAAA;IACD;;OAEG;IACH,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,QAAY,EACZ,QAAa,EACb,UAAc,EACf,GAAE,uBAA4B,GAAG,uBAAuB,CAuCxD"}
@@ -0,0 +1,3 @@
1
+ export { Select } from './Select';
2
+ export type { SelectMultiProps, SelectOption, SelectProps, SelectSingleProps } from './Select.types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA"}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@wecareu/select",
3
+ "version": "0.1.0",
4
+ "description": "Componente de seleção controlado do design system WeCareU com suporte a modal, busca e múltipla seleção",
5
+ "keywords": [
6
+ "react-native",
7
+ "select",
8
+ "dropdown",
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
+ }
package/src/Select.tsx ADDED
@@ -0,0 +1,338 @@
1
+ import React from 'react'
2
+
3
+ import { Animated, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
4
+
5
+ import { useTheme } from '@wecareu/theme'
6
+ import type { Theme } from '@wecareu/theme'
7
+
8
+ import { useShakeAnimation } from './animations/shake'
9
+ import type { SelectMultiProps, SelectOption, SelectProps, SelectSingleProps } from './Select.types'
10
+ import { SelectChip } from './SelectChip'
11
+ import { SelectDropdown } from './SelectDropdown'
12
+ import { SelectErrorMessage } from './SelectErrorMessage'
13
+ import { SelectIcon } from './SelectIcon'
14
+
15
+ export function Select(props: SelectProps): JSX.Element | null {
16
+ const theme = useTheme()
17
+ const styles = React.useMemo(() => createStyles(theme), [theme])
18
+
19
+ const {
20
+ clearable = false,
21
+ disabled = false,
22
+ errorMessage,
23
+ inputError = false,
24
+ labels,
25
+ leftIcon,
26
+ multiple,
27
+ onValidation,
28
+ options,
29
+ placeholder,
30
+ readonly = false,
31
+ required = false,
32
+ searchable = false,
33
+ style,
34
+ testID,
35
+ value
36
+ } = props
37
+
38
+ if (!options || options.length === 0) {
39
+ console.error('[Select] The "options" prop must be a non-empty array. Received:', options)
40
+ return null
41
+ }
42
+
43
+ const isInteractive = !(disabled || readonly)
44
+ const [isOpen, setIsOpen] = React.useState(false)
45
+ const [touched, setTouched] = React.useState(false)
46
+
47
+ const { animatedStyle, triggerShake } = useShakeAnimation({
48
+ distance: theme.spacing.sm
49
+ })
50
+
51
+ const isEmpty = multiple ? (value as Array<string | number>).length === 0 : value === null || value === undefined
52
+
53
+ const internalHasError = React.useMemo(() => {
54
+ if (!required || !touched) {
55
+ return false
56
+ }
57
+ return isEmpty
58
+ }, [isEmpty, required, touched])
59
+
60
+ React.useEffect(() => {
61
+ if (!required || !touched) {
62
+ return
63
+ }
64
+ onValidation?.(!internalHasError)
65
+ }, [internalHasError, onValidation, required, touched])
66
+
67
+ const shouldShowError = inputError || internalHasError
68
+ const requiredErrorMessage = labels?.requiredError ?? 'This field is required'
69
+ const effectiveErrorMessage = inputError ? errorMessage : internalHasError ? requiredErrorMessage : undefined
70
+
71
+ const previousErrorRef = React.useRef(false)
72
+
73
+ React.useEffect(() => {
74
+ if (shouldShowError && !previousErrorRef.current) {
75
+ triggerShake()
76
+ }
77
+ previousErrorRef.current = shouldShowError
78
+ }, [shouldShowError, triggerShake])
79
+
80
+ const selectedValues = React.useMemo<Array<string | number>>(() => {
81
+ if (multiple) {
82
+ return (value as Array<string | number>) ?? []
83
+ }
84
+ if (value !== null && value !== undefined) {
85
+ return [value as string | number]
86
+ }
87
+ return []
88
+ }, [multiple, value])
89
+
90
+ const selectedOptions = React.useMemo(
91
+ () => options.filter(o => selectedValues.includes(o.value)),
92
+ [options, selectedValues]
93
+ )
94
+
95
+ const singleLabel = React.useMemo(() => {
96
+ if (multiple || isEmpty) {
97
+ return null
98
+ }
99
+ return options.find(o => o.value === (value as string | number))?.label ?? null
100
+ }, [isEmpty, multiple, options, value])
101
+
102
+ const handleOpen = React.useCallback(() => {
103
+ if (!isInteractive) {
104
+ return
105
+ }
106
+ setTouched(true)
107
+ setIsOpen(true)
108
+ }, [isInteractive])
109
+
110
+ const handleClose = React.useCallback(() => {
111
+ setIsOpen(false)
112
+ }, [])
113
+
114
+ const handleToggleOption = React.useCallback(
115
+ (option: SelectOption) => {
116
+ if (multiple) {
117
+ const multiProps = props as SelectMultiProps
118
+ const current = multiProps.value
119
+ const isSelected = current.includes(option.value)
120
+ const next = isSelected ? current.filter(v => v !== option.value) : [...current, option.value]
121
+ multiProps.onChange(next)
122
+ } else {
123
+ const singleProps = props as SelectSingleProps
124
+ singleProps.onChange(option.value)
125
+ setIsOpen(false)
126
+ }
127
+ },
128
+ [multiple, props]
129
+ )
130
+
131
+ const handleClear = React.useCallback(
132
+ () => {
133
+ if (multiple) {
134
+ const multiProps = props as SelectMultiProps
135
+ multiProps.onChange([])
136
+ } else {
137
+ const singleProps = props as SelectSingleProps
138
+ singleProps.onChange(null)
139
+ }
140
+ },
141
+ [multiple, props]
142
+ )
143
+
144
+ const handleRemoveChip = React.useCallback(
145
+ (optionValue: string | number) => {
146
+ if (!multiple) {
147
+ return
148
+ }
149
+ const multiProps = props as SelectMultiProps
150
+ multiProps.onChange(multiProps.value.filter(v => v !== optionValue))
151
+ },
152
+ [multiple, props]
153
+ )
154
+
155
+ const showClearButton = clearable && !isEmpty && isInteractive
156
+
157
+ const renderFieldContent = () => {
158
+ if (multiple && selectedOptions.length > 0) {
159
+ return (
160
+ <ScrollView
161
+ horizontal
162
+ keyboardShouldPersistTaps='handled'
163
+ showsHorizontalScrollIndicator={false}
164
+ style={styles.chipsScroll}
165
+ contentContainerStyle={styles.chipsContainer}
166
+ >
167
+ {selectedOptions.map(opt => (
168
+ <SelectChip
169
+ key={String(opt.value)}
170
+ disabled={disabled}
171
+ label={opt.label}
172
+ onRemove={() => handleRemoveChip(opt.value)}
173
+ />
174
+ ))}
175
+ </ScrollView>
176
+ )
177
+ }
178
+
179
+ if (!isEmpty && singleLabel) {
180
+ return (
181
+ <Text numberOfLines={1} style={styles.selectedLabel}>
182
+ {singleLabel}
183
+ </Text>
184
+ )
185
+ }
186
+
187
+ return (
188
+ <Text numberOfLines={1} style={styles.placeholder}>
189
+ {placeholder ?? 'Select...'}
190
+ </Text>
191
+ )
192
+ }
193
+
194
+ return (
195
+ <View style={styles.wrapper}>
196
+ <Animated.View
197
+ accessible={false}
198
+ style={[
199
+ styles.container,
200
+ !isInteractive && styles.containerDisabled,
201
+ shouldShowError && styles.containerError,
202
+ animatedStyle,
203
+ style
204
+ ]}
205
+ testID={testID}
206
+ >
207
+ {leftIcon ? (
208
+ <View style={styles.iconLeft}>
209
+ <SelectIcon {...leftIcon} />
210
+ </View>
211
+ ) : null}
212
+
213
+ <Pressable
214
+ accessibilityHint={placeholder}
215
+ accessibilityLabel={singleLabel ?? placeholder ?? 'Select option'}
216
+ accessibilityRole='combobox'
217
+ accessibilityState={{ disabled: !isInteractive, expanded: isOpen }}
218
+ disabled={!isInteractive}
219
+ onPress={handleOpen}
220
+ style={styles.fieldPressable}
221
+ >
222
+ {renderFieldContent()}
223
+ </Pressable>
224
+
225
+ <View style={styles.rightIcons}>
226
+ {showClearButton ? (
227
+ <Pressable
228
+ accessibilityLabel='Clear selection'
229
+ accessibilityRole='button'
230
+ hitSlop={8}
231
+ onPress={handleClear}
232
+ >
233
+ <SelectIcon
234
+ color={theme.colors.text.tertiary}
235
+ name='close'
236
+ size={theme.spacing.md}
237
+ />
238
+ </Pressable>
239
+ ) : (
240
+ <Pressable
241
+ accessibilityLabel={isOpen ? 'Close options' : 'Open options'}
242
+ accessibilityRole='button'
243
+ disabled={!isInteractive}
244
+ hitSlop={8}
245
+ onPress={handleOpen}
246
+ >
247
+ <SelectIcon
248
+ color={theme.colors.text.tertiary}
249
+ name={isOpen ? 'chevronUp' : 'chevronDown'}
250
+ size={theme.spacing.md}
251
+ />
252
+ </Pressable>
253
+ )}
254
+ </View>
255
+ </Animated.View>
256
+
257
+ <SelectErrorMessage
258
+ message={shouldShowError ? effectiveErrorMessage : undefined}
259
+ testID={effectiveErrorMessage ? `${testID ?? 'select'}-error` : undefined}
260
+ />
261
+
262
+ <SelectDropdown
263
+ isMulti={multiple}
264
+ isOpen={isOpen}
265
+ onClose={handleClose}
266
+ onToggleOption={handleToggleOption}
267
+ options={options}
268
+ searchable={searchable}
269
+ selectedValues={selectedValues}
270
+ />
271
+ </View>
272
+ )
273
+ }
274
+
275
+ function createStyles(theme: Theme) {
276
+ return StyleSheet.create({
277
+ chipsContainer: {
278
+ alignItems: 'center',
279
+ flexDirection: 'row',
280
+ gap: theme.spacing.xs,
281
+ paddingVertical: 2
282
+ },
283
+ chipsScroll: {
284
+ flex: 1
285
+ },
286
+ container: {
287
+ alignItems: 'center',
288
+ backgroundColor: 'transparent',
289
+ borderColor: theme.colors.brand.primary,
290
+ borderRadius: theme.radius.xxl,
291
+ borderWidth: 1,
292
+ flexDirection: 'row',
293
+ minHeight: 48,
294
+ paddingHorizontal: theme.spacing.xl,
295
+ paddingVertical: theme.spacing.xs,
296
+ width: '100%'
297
+ },
298
+ containerDisabled: {
299
+ backgroundColor: theme.colors.surface.disabled,
300
+ borderColor: theme.colors.border.disabled
301
+ },
302
+ containerError: {
303
+ borderColor: theme.colors.status.error
304
+ },
305
+ fieldPressable: {
306
+ alignItems: 'center',
307
+ flex: 1,
308
+ flexDirection: 'row',
309
+ justifyContent: 'center',
310
+ minHeight: 32
311
+ },
312
+ iconLeft: {
313
+ marginRight: theme.spacing.xs
314
+ },
315
+ placeholder: {
316
+ color: theme.colors.text.tertiary,
317
+ flex: 1,
318
+ fontFamily: theme.typography.fontFamily.body,
319
+ lineHeight: theme.typography.lineHeight.bodySmall,
320
+ textAlign: 'center'
321
+ },
322
+ rightIcons: {
323
+ alignItems: 'center',
324
+ flexDirection: 'row',
325
+ marginLeft: theme.spacing.xs
326
+ },
327
+ selectedLabel: {
328
+ color: theme.colors.text.primary,
329
+ flex: 1,
330
+ fontFamily: theme.typography.fontFamily.body,
331
+ lineHeight: theme.typography.lineHeight.bodySmall,
332
+ textAlign: 'center'
333
+ },
334
+ wrapper: {
335
+ width: '100%'
336
+ }
337
+ })
338
+ }