@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.
- package/README.md +165 -0
- package/lib/commonjs/Select.js +272 -0
- package/lib/commonjs/Select.js.map +1 -0
- package/lib/commonjs/Select.types.js +6 -0
- package/lib/commonjs/Select.types.js.map +1 -0
- package/lib/commonjs/SelectChip.js +74 -0
- package/lib/commonjs/SelectChip.js.map +1 -0
- package/lib/commonjs/SelectDropdown.js +156 -0
- package/lib/commonjs/SelectDropdown.js.map +1 -0
- package/lib/commonjs/SelectErrorMessage.js +32 -0
- package/lib/commonjs/SelectErrorMessage.js.map +1 -0
- package/lib/commonjs/SelectIcon.js +64 -0
- package/lib/commonjs/SelectIcon.js.map +1 -0
- package/lib/commonjs/SelectOption.js +140 -0
- package/lib/commonjs/SelectOption.js.map +1 -0
- package/lib/commonjs/SelectSearch.js +79 -0
- package/lib/commonjs/SelectSearch.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 +383 -0
- package/lib/commonjs/index.stories.js.map +1 -0
- package/lib/module/Select.js +265 -0
- package/lib/module/Select.js.map +1 -0
- package/lib/module/Select.types.js +2 -0
- package/lib/module/Select.types.js.map +1 -0
- package/lib/module/SelectChip.js +67 -0
- package/lib/module/SelectChip.js.map +1 -0
- package/lib/module/SelectDropdown.js +149 -0
- package/lib/module/SelectDropdown.js.map +1 -0
- package/lib/module/SelectErrorMessage.js +25 -0
- package/lib/module/SelectErrorMessage.js.map +1 -0
- package/lib/module/SelectIcon.js +57 -0
- package/lib/module/SelectIcon.js.map +1 -0
- package/lib/module/SelectOption.js +133 -0
- package/lib/module/SelectOption.js.map +1 -0
- package/lib/module/SelectSearch.js +72 -0
- package/lib/module/SelectSearch.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 +376 -0
- package/lib/module/index.stories.js.map +1 -0
- package/lib/typescript/src/Select.d.ts +3 -0
- package/lib/typescript/src/Select.d.ts.map +1 -0
- package/lib/typescript/src/Select.types.d.ts +245 -0
- package/lib/typescript/src/Select.types.d.ts.map +1 -0
- package/lib/typescript/src/SelectChip.d.ts +3 -0
- package/lib/typescript/src/SelectChip.d.ts.map +1 -0
- package/lib/typescript/src/SelectDropdown.d.ts +3 -0
- package/lib/typescript/src/SelectDropdown.d.ts.map +1 -0
- package/lib/typescript/src/SelectErrorMessage.d.ts +3 -0
- package/lib/typescript/src/SelectErrorMessage.d.ts.map +1 -0
- package/lib/typescript/src/SelectIcon.d.ts +3 -0
- package/lib/typescript/src/SelectIcon.d.ts.map +1 -0
- package/lib/typescript/src/SelectOption.d.ts +3 -0
- package/lib/typescript/src/SelectOption.d.ts.map +1 -0
- package/lib/typescript/src/SelectSearch.d.ts +3 -0
- package/lib/typescript/src/SelectSearch.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/Select.tsx +338 -0
- package/src/Select.types.ts +256 -0
- package/src/SelectChip.tsx +75 -0
- package/src/SelectDropdown.tsx +184 -0
- package/src/SelectErrorMessage.tsx +31 -0
- package/src/SelectIcon.tsx +63 -0
- package/src/SelectOption.tsx +168 -0
- package/src/SelectSearch.tsx +81 -0
- package/src/animations/shake.ts +76 -0
- package/src/index.stories.tsx +336 -0
- 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
|
+
})
|