@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,256 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native'
2
+
3
+ import type { IconName } from '@wecareu/icons'
4
+
5
+ export interface SelectOption {
6
+ /**
7
+ * Background color applied to this option's row
8
+ */
9
+ color?: string
10
+ /**
11
+ * Optional description rendered below the label
12
+ */
13
+ description?: string
14
+ /**
15
+ * Prevents this option from being selected
16
+ */
17
+ disabled?: boolean
18
+ /**
19
+ * ISO 3166-1 alpha-2 country code used to render an emoji flag (e.g. 'BR', 'US').
20
+ * Takes precedence over `icon` when both are provided.
21
+ */
22
+ flagCode?: string
23
+ /**
24
+ * Icon identifier from @wecareu/icons displayed to the left of the label
25
+ */
26
+ icon?: IconName
27
+ /**
28
+ * Color applied to the label text of this option
29
+ */
30
+ labelColor?: string
31
+ /**
32
+ * Text displayed for this option
33
+ */
34
+ label: string
35
+ /**
36
+ * Unique value for this option
37
+ */
38
+ value: string | number
39
+ }
40
+
41
+ export interface SelectIconProps {
42
+ /**
43
+ * Accessibility label announced for screen readers
44
+ */
45
+ accessibilityLabel?: string
46
+ /**
47
+ * Icon tint color. Falls back to theme.colors.text.tertiary when undefined
48
+ */
49
+ color?: string
50
+ /**
51
+ * Icon identifier available in WeCareU icons package
52
+ */
53
+ name: IconName
54
+ /**
55
+ * Press handler triggered when icon is tapped
56
+ */
57
+ onPress?: () => void
58
+ /**
59
+ * Icon size in pixels. Defaults to theme.spacing.lg
60
+ */
61
+ size?: number
62
+ /**
63
+ * Testing identifier used in automated tests
64
+ */
65
+ testID?: string
66
+ }
67
+
68
+ export interface SelectLabels {
69
+ /**
70
+ * Error message shown when a required field has no selection. Default: 'This field is required'
71
+ */
72
+ requiredError?: string
73
+ }
74
+
75
+ interface SelectBaseProps {
76
+ /**
77
+ * Shows a clear button to remove the current selection
78
+ */
79
+ clearable?: boolean
80
+ /**
81
+ * Displays a disabled appearance and blocks user interaction
82
+ */
83
+ disabled?: boolean
84
+ /**
85
+ * Optional error message rendered below the field when in error state
86
+ */
87
+ errorMessage?: string
88
+ /**
89
+ * Shows the component using error tokens, triggering shake animation
90
+ */
91
+ inputError?: boolean
92
+ /**
93
+ * Overrides for UI strings to enable i18n. Defaults to English.
94
+ */
95
+ labels?: SelectLabels
96
+ /**
97
+ * Icon rendered on the left side of the field
98
+ */
99
+ leftIcon?: SelectIconProps
100
+ /**
101
+ * Callback invoked with the validation status.
102
+ * Only fires when `required` is true.
103
+ */
104
+ onValidation?: (isValid: boolean) => void
105
+ /**
106
+ * List of selectable options
107
+ */
108
+ options: SelectOption[]
109
+ /**
110
+ * Placeholder text displayed when no option is selected
111
+ */
112
+ placeholder?: string
113
+ /**
114
+ * Renders the field as read-only, blocking interaction without changing visuals significantly
115
+ */
116
+ readonly?: boolean
117
+ /**
118
+ * Marks this field as required for validation purposes
119
+ */
120
+ required?: boolean
121
+ /**
122
+ * Enables a search input inside the dropdown to filter options
123
+ */
124
+ searchable?: boolean
125
+ /**
126
+ * Applies style overrides to the container View
127
+ */
128
+ style?: StyleProp<ViewStyle>
129
+ /**
130
+ * Test identifier exposed on the container
131
+ */
132
+ testID?: string
133
+ }
134
+
135
+ export interface SelectSingleProps extends SelectBaseProps {
136
+ /**
137
+ * Enables single selection mode (default)
138
+ */
139
+ multiple?: false
140
+ /**
141
+ * Callback invoked with the selected value, or null when cleared
142
+ */
143
+ onChange: (value: string | number | null) => void
144
+ /**
145
+ * Currently selected value. Pass null when nothing is selected
146
+ */
147
+ value: string | number | null
148
+ }
149
+
150
+ export interface SelectMultiProps extends SelectBaseProps {
151
+ /**
152
+ * Enables multiple selection mode
153
+ */
154
+ multiple: true
155
+ /**
156
+ * Callback invoked with the array of selected values
157
+ */
158
+ onChange: (values: Array<string | number>) => void
159
+ /**
160
+ * Currently selected values
161
+ */
162
+ value: Array<string | number>
163
+ }
164
+
165
+ export type SelectProps = SelectSingleProps | SelectMultiProps
166
+
167
+ export interface SelectChipProps {
168
+ /**
169
+ * Whether the chip should display the remove button
170
+ */
171
+ disabled?: boolean
172
+ /**
173
+ * Label text displayed inside the chip
174
+ */
175
+ label: string
176
+ /**
177
+ * Called when the remove button is pressed
178
+ */
179
+ onRemove?: () => void
180
+ }
181
+
182
+ export interface SelectOptionItemProps {
183
+ /**
184
+ * Whether the option is interactive
185
+ */
186
+ disabled?: boolean
187
+ /**
188
+ * Whether checkboxes should appear (multi mode)
189
+ */
190
+ isMulti?: boolean
191
+ /**
192
+ * Whether this option is currently selected
193
+ */
194
+ isSelected: boolean
195
+ /**
196
+ * The option data to render
197
+ */
198
+ option: SelectOption
199
+ /**
200
+ * Called when this item is pressed
201
+ */
202
+ onPress: (option: SelectOption) => void
203
+ }
204
+
205
+ export interface SelectSearchProps {
206
+ /**
207
+ * Called with the new query when the user types
208
+ */
209
+ onChangeText: (text: string) => void
210
+ /**
211
+ * Current search query
212
+ */
213
+ value: string
214
+ }
215
+
216
+ export interface SelectDropdownProps {
217
+ /**
218
+ * Whether multiple selection is enabled
219
+ */
220
+ isMulti?: boolean
221
+ /**
222
+ * Whether the dropdown is visible
223
+ */
224
+ isOpen: boolean
225
+ /**
226
+ * List of all options
227
+ */
228
+ options: SelectOption[]
229
+ /**
230
+ * Currently selected value(s)
231
+ */
232
+ selectedValues: Array<string | number>
233
+ /**
234
+ * Whether the search field is shown
235
+ */
236
+ searchable?: boolean
237
+ /**
238
+ * Called when the dropdown requests to close
239
+ */
240
+ onClose: () => void
241
+ /**
242
+ * Called when an option is toggled
243
+ */
244
+ onToggleOption: (option: SelectOption) => void
245
+ }
246
+
247
+ export interface SelectErrorMessageProps {
248
+ /**
249
+ * Text displayed underneath the field when it is in error state
250
+ */
251
+ message?: string
252
+ /**
253
+ * Optional identifier exposed for automated tests
254
+ */
255
+ testID?: string
256
+ }
@@ -0,0 +1,75 @@
1
+ import React from 'react'
2
+
3
+ import { Pressable, StyleSheet, Text, View } from 'react-native'
4
+
5
+ import { Icon } from '@wecareu/icons'
6
+ import { useTheme } from '@wecareu/theme'
7
+ import type { Theme } from '@wecareu/theme'
8
+
9
+ import type { SelectChipProps } from './Select.types'
10
+
11
+ export function SelectChip({ disabled, label, onRemove }: SelectChipProps): JSX.Element {
12
+ const theme = useTheme()
13
+ const styles = React.useMemo(() => createStyles(theme), [theme])
14
+
15
+ return (
16
+ <View
17
+ accessibilityLabel={label}
18
+ accessibilityRole='none'
19
+ style={[styles.chip, disabled && styles.chipDisabled]}
20
+ >
21
+ <Text numberOfLines={1} style={[styles.label, disabled && styles.labelDisabled]}>
22
+ {label}
23
+ </Text>
24
+
25
+ {onRemove && !disabled ? (
26
+ <Pressable
27
+ accessibilityHint={`Remove ${label}`}
28
+ accessibilityLabel={`Remove ${label}`}
29
+ accessibilityRole='button'
30
+ hitSlop={4}
31
+ onPress={onRemove}
32
+ style={styles.removeButton}
33
+ >
34
+ <Icon color={theme.colors.brand.primary} name='close' size={10} />
35
+ </Pressable>
36
+ ) : null}
37
+ </View>
38
+ )
39
+ }
40
+
41
+ function createStyles(theme: Theme) {
42
+ return StyleSheet.create({
43
+ chip: {
44
+ alignItems: 'center',
45
+ backgroundColor: theme.colors.surface.secondary,
46
+ borderColor: theme.colors.brand.primary,
47
+ borderRadius: theme.radius.sm,
48
+ borderWidth: 1,
49
+ flexDirection: 'row',
50
+ flexShrink: 1,
51
+ gap: 4,
52
+ maxWidth: 120,
53
+ paddingHorizontal: theme.spacing.xs,
54
+ paddingVertical: 2
55
+ },
56
+ chipDisabled: {
57
+ backgroundColor: theme.colors.surface.disabled,
58
+ borderColor: theme.colors.border.disabled
59
+ },
60
+ label: {
61
+ color: theme.colors.brand.primary,
62
+ flexShrink: 1,
63
+ fontFamily: theme.typography.fontFamily.caption,
64
+ fontSize: theme.typography.fontSize.xs,
65
+ lineHeight: theme.typography.lineHeight.labelSmall
66
+ },
67
+ labelDisabled: {
68
+ color: theme.colors.text.disabled
69
+ },
70
+ removeButton: {
71
+ alignItems: 'center',
72
+ justifyContent: 'center'
73
+ }
74
+ })
75
+ }
@@ -0,0 +1,184 @@
1
+ import React from 'react'
2
+
3
+ import {
4
+ Animated,
5
+ FlatList,
6
+ KeyboardAvoidingView,
7
+ Modal,
8
+ Platform,
9
+ Pressable,
10
+ StyleSheet,
11
+ View
12
+ } from 'react-native'
13
+
14
+ import { useTheme } from '@wecareu/theme'
15
+ import type { Theme } from '@wecareu/theme'
16
+
17
+ import type { SelectDropdownProps, SelectOption } from './Select.types'
18
+ import { SelectOptionItem } from './SelectOption'
19
+ import { SelectSearch } from './SelectSearch'
20
+
21
+ const ANIMATION_DURATION = 250
22
+ const INITIAL_RENDER_COUNT = 20
23
+ const MAX_RENDER_BATCH = 20
24
+ const WINDOW_SIZE = 5
25
+
26
+ export function SelectDropdown({
27
+ isMulti,
28
+ isOpen,
29
+ onClose,
30
+ onToggleOption,
31
+ options,
32
+ searchable,
33
+ selectedValues
34
+ }: SelectDropdownProps): JSX.Element {
35
+ const theme = useTheme()
36
+ const styles = React.useMemo(() => createStyles(theme), [theme])
37
+
38
+ const translateY = React.useRef(new Animated.Value(500)).current
39
+ const overlayOpacity = React.useRef(new Animated.Value(0)).current
40
+ const [searchQuery, setSearchQuery] = React.useState('')
41
+
42
+ React.useEffect(() => {
43
+ if (isOpen) {
44
+ setSearchQuery('')
45
+ Animated.parallel([
46
+ Animated.timing(translateY, {
47
+ duration: ANIMATION_DURATION,
48
+ toValue: 0,
49
+ useNativeDriver: true
50
+ }),
51
+ Animated.timing(overlayOpacity, {
52
+ duration: ANIMATION_DURATION,
53
+ toValue: 1,
54
+ useNativeDriver: true
55
+ })
56
+ ]).start()
57
+ } else {
58
+ Animated.parallel([
59
+ Animated.timing(translateY, {
60
+ duration: ANIMATION_DURATION,
61
+ toValue: 500,
62
+ useNativeDriver: true
63
+ }),
64
+ Animated.timing(overlayOpacity, {
65
+ duration: ANIMATION_DURATION,
66
+ toValue: 0,
67
+ useNativeDriver: true
68
+ })
69
+ ]).start()
70
+ }
71
+ }, [isOpen, overlayOpacity, translateY])
72
+
73
+ const filteredOptions = React.useMemo(() => {
74
+ if (!searchQuery.trim()) {
75
+ return options
76
+ }
77
+ const query = searchQuery.toLowerCase().trim()
78
+ return options.filter(opt => opt.label.toLowerCase().includes(query))
79
+ }, [options, searchQuery])
80
+
81
+ const renderItem = React.useCallback(
82
+ ({ item }: { item: SelectOption }) => (
83
+ <SelectOptionItem
84
+ disabled={item.disabled}
85
+ isMulti={isMulti}
86
+ isSelected={selectedValues.includes(item.value)}
87
+ onPress={onToggleOption}
88
+ option={item}
89
+ />
90
+ ),
91
+ [isMulti, onToggleOption, selectedValues]
92
+ )
93
+
94
+ const keyExtractor = React.useCallback((item: SelectOption) => String(item.value), [])
95
+
96
+ const itemSeparator = React.useCallback(
97
+ () => <View style={styles.separator} />,
98
+ [styles.separator]
99
+ )
100
+
101
+ return (
102
+ <Modal
103
+ accessibilityViewIsModal
104
+ animationType='none'
105
+ onRequestClose={onClose}
106
+ statusBarTranslucent
107
+ transparent
108
+ visible={isOpen}
109
+ >
110
+ <KeyboardAvoidingView
111
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
112
+ style={styles.keyboardView}
113
+ >
114
+ <Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
115
+ <Pressable accessibilityLabel='Close dropdown' style={styles.overlayPressable} onPress={onClose} />
116
+ </Animated.View>
117
+
118
+ <Animated.View style={[styles.sheet, { transform: [{ translateY }] }]}>
119
+ <View style={styles.dragHandle} />
120
+
121
+ {searchable ? (
122
+ <SelectSearch
123
+ onChangeText={setSearchQuery}
124
+ value={searchQuery}
125
+ />
126
+ ) : null}
127
+
128
+ <FlatList
129
+ ItemSeparatorComponent={itemSeparator}
130
+ contentContainerStyle={styles.listContent}
131
+ data={filteredOptions}
132
+ extraData={selectedValues}
133
+ initialNumToRender={INITIAL_RENDER_COUNT}
134
+ keyExtractor={keyExtractor}
135
+ keyboardShouldPersistTaps='handled'
136
+ maxToRenderPerBatch={MAX_RENDER_BATCH}
137
+ renderItem={renderItem}
138
+ windowSize={WINDOW_SIZE}
139
+ />
140
+ </Animated.View>
141
+ </KeyboardAvoidingView>
142
+ </Modal>
143
+ )
144
+ }
145
+
146
+ function createStyles(theme: Theme) {
147
+ return StyleSheet.create({
148
+ dragHandle: {
149
+ alignSelf: 'center',
150
+ backgroundColor: theme.colors.border.secondary,
151
+ borderRadius: 2,
152
+ height: 4,
153
+ marginBottom: theme.spacing.xs,
154
+ marginTop: theme.spacing.sm,
155
+ width: 40
156
+ },
157
+ keyboardView: {
158
+ flex: 1,
159
+ justifyContent: 'flex-end'
160
+ },
161
+ listContent: {
162
+ paddingBottom: theme.spacing.xl
163
+ },
164
+ overlay: {
165
+ ...StyleSheet.absoluteFillObject,
166
+ backgroundColor: theme.colors.alpha?.overlay ?? 'rgba(0,0,0,0.5)'
167
+ },
168
+ overlayPressable: {
169
+ flex: 1
170
+ },
171
+ separator: {
172
+ backgroundColor: theme.colors.border.secondary,
173
+ height: StyleSheet.hairlineWidth,
174
+ marginLeft: theme.spacing.md
175
+ },
176
+ sheet: {
177
+ backgroundColor: theme.colors.background.primary,
178
+ borderTopLeftRadius: theme.radius.xl,
179
+ borderTopRightRadius: theme.radius.xl,
180
+ maxHeight: '70%',
181
+ ...theme.colors.shadowStyles.card
182
+ }
183
+ })
184
+ }
@@ -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 { SelectErrorMessageProps } from './Select.types'
8
+
9
+ export function SelectErrorMessage({ message, testID }: SelectErrorMessageProps): 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
+ }
@@ -0,0 +1,63 @@
1
+ import React from 'react'
2
+
3
+ import { Pressable, StyleSheet, View } from 'react-native'
4
+
5
+ import { Icon } from '@wecareu/icons'
6
+ import { useTheme } from '@wecareu/theme'
7
+
8
+ import type { SelectIconProps } from './Select.types'
9
+
10
+ export function SelectIcon({
11
+ accessibilityLabel,
12
+ color,
13
+ name,
14
+ onPress,
15
+ size,
16
+ testID
17
+ }: SelectIconProps): 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
+ return (
34
+ <Pressable
35
+ accessibilityLabel={accessibilityLabel ?? name}
36
+ accessibilityRole='button'
37
+ hitSlop={hitSlop}
38
+ onPress={onPress}
39
+ style={styles.pressable}
40
+ testID={testID}
41
+ >
42
+ <Icon color={iconColor} name={name} size={iconSize} />
43
+ </Pressable>
44
+ )
45
+ }
46
+
47
+ return (
48
+ <View accessible={false} style={styles.wrapper} testID={testID}>
49
+ <Icon accessibilityLabel={accessibilityLabel} color={iconColor} name={name} size={iconSize} />
50
+ </View>
51
+ )
52
+ }
53
+
54
+ const styles = StyleSheet.create({
55
+ pressable: {
56
+ alignItems: 'center',
57
+ justifyContent: 'center'
58
+ },
59
+ wrapper: {
60
+ alignItems: 'center',
61
+ justifyContent: 'center'
62
+ }
63
+ })