@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,168 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
|
4
|
+
|
|
5
|
+
import { FlagIcon, Icon } from '@wecareu/icons'
|
|
6
|
+
|
|
7
|
+
import { useTheme } from '@wecareu/theme'
|
|
8
|
+
import type { Theme } from '@wecareu/theme'
|
|
9
|
+
|
|
10
|
+
import type { SelectOptionItemProps } from './Select.types'
|
|
11
|
+
|
|
12
|
+
export function SelectOptionItem({
|
|
13
|
+
disabled,
|
|
14
|
+
isMulti,
|
|
15
|
+
isSelected,
|
|
16
|
+
onPress,
|
|
17
|
+
option
|
|
18
|
+
}: SelectOptionItemProps): JSX.Element {
|
|
19
|
+
const theme = useTheme()
|
|
20
|
+
const styles = React.useMemo(() => createStyles(theme), [theme])
|
|
21
|
+
|
|
22
|
+
const handlePress = React.useCallback(() => {
|
|
23
|
+
if (!disabled) {
|
|
24
|
+
onPress(option)
|
|
25
|
+
}
|
|
26
|
+
}, [disabled, onPress, option])
|
|
27
|
+
|
|
28
|
+
const resolvedLabelColor = option.labelColor
|
|
29
|
+
? option.labelColor
|
|
30
|
+
: isSelected
|
|
31
|
+
? theme.colors.brand.primary
|
|
32
|
+
: theme.colors.text.primary
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Pressable
|
|
36
|
+
accessibilityRole='menuitem'
|
|
37
|
+
accessibilityState={{ disabled, selected: isSelected }}
|
|
38
|
+
disabled={disabled}
|
|
39
|
+
onPress={handlePress}
|
|
40
|
+
style={({ pressed }) => [
|
|
41
|
+
styles.container,
|
|
42
|
+
option.color ? { backgroundColor: option.color } : isSelected && styles.containerSelected,
|
|
43
|
+
disabled && styles.containerDisabled,
|
|
44
|
+
pressed && !disabled && !option.color && styles.containerPressed
|
|
45
|
+
]}
|
|
46
|
+
>
|
|
47
|
+
{option.flagCode ? (
|
|
48
|
+
<View style={styles.iconWrapper}>
|
|
49
|
+
<FlagIcon
|
|
50
|
+
countryCode={option.flagCode}
|
|
51
|
+
size={theme.spacing.md + 2}
|
|
52
|
+
/>
|
|
53
|
+
</View>
|
|
54
|
+
) : option.icon ? (
|
|
55
|
+
<View style={styles.iconWrapper}>
|
|
56
|
+
<Icon
|
|
57
|
+
color={disabled ? theme.colors.text.disabled : resolvedLabelColor}
|
|
58
|
+
name={option.icon}
|
|
59
|
+
size={theme.spacing.lg}
|
|
60
|
+
/>
|
|
61
|
+
</View>
|
|
62
|
+
) : null}
|
|
63
|
+
|
|
64
|
+
<View style={styles.textWrapper}>
|
|
65
|
+
<Text
|
|
66
|
+
numberOfLines={1}
|
|
67
|
+
style={[
|
|
68
|
+
styles.label,
|
|
69
|
+
{ color: disabled ? theme.colors.text.disabled : resolvedLabelColor },
|
|
70
|
+
isSelected && !option.labelColor && styles.labelSelected,
|
|
71
|
+
disabled && styles.labelDisabled
|
|
72
|
+
]}
|
|
73
|
+
>
|
|
74
|
+
{option.label}
|
|
75
|
+
</Text>
|
|
76
|
+
|
|
77
|
+
{option.description ? (
|
|
78
|
+
<Text
|
|
79
|
+
numberOfLines={2}
|
|
80
|
+
style={[styles.description, disabled && styles.descriptionDisabled]}
|
|
81
|
+
>
|
|
82
|
+
{option.description}
|
|
83
|
+
</Text>
|
|
84
|
+
) : null}
|
|
85
|
+
</View>
|
|
86
|
+
|
|
87
|
+
{isMulti ? (
|
|
88
|
+
<View style={[styles.checkbox, isSelected && styles.checkboxSelected, disabled && styles.checkboxDisabled]}>
|
|
89
|
+
{isSelected ? (
|
|
90
|
+
<Icon color={theme.colors.text.white} name='correct' size={12} />
|
|
91
|
+
) : null}
|
|
92
|
+
</View>
|
|
93
|
+
) : (
|
|
94
|
+
isSelected ? (
|
|
95
|
+
<Icon color={option.labelColor ?? theme.colors.brand.primary} name='correct' size={theme.spacing.md} />
|
|
96
|
+
) : null
|
|
97
|
+
)}
|
|
98
|
+
</Pressable>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createStyles(theme: Theme) {
|
|
103
|
+
return StyleSheet.create({
|
|
104
|
+
checkbox: {
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
borderColor: theme.colors.border.primary,
|
|
107
|
+
borderRadius: theme.radius.xs,
|
|
108
|
+
borderWidth: 1.5,
|
|
109
|
+
height: 20,
|
|
110
|
+
justifyContent: 'center',
|
|
111
|
+
width: 20
|
|
112
|
+
},
|
|
113
|
+
checkboxDisabled: {
|
|
114
|
+
borderColor: theme.colors.border.disabled
|
|
115
|
+
},
|
|
116
|
+
checkboxSelected: {
|
|
117
|
+
backgroundColor: theme.colors.brand.primary,
|
|
118
|
+
borderColor: theme.colors.brand.primary
|
|
119
|
+
},
|
|
120
|
+
container: {
|
|
121
|
+
alignItems: 'center',
|
|
122
|
+
flexDirection: 'row',
|
|
123
|
+
gap: theme.spacing.sm,
|
|
124
|
+
paddingHorizontal: theme.spacing.md,
|
|
125
|
+
paddingVertical: theme.spacing.sm
|
|
126
|
+
},
|
|
127
|
+
containerDisabled: {
|
|
128
|
+
opacity: 0.4
|
|
129
|
+
},
|
|
130
|
+
containerPressed: {
|
|
131
|
+
backgroundColor: theme.colors.surface.secondary
|
|
132
|
+
},
|
|
133
|
+
containerSelected: {
|
|
134
|
+
backgroundColor: theme.colors.surface.secondary
|
|
135
|
+
},
|
|
136
|
+
description: {
|
|
137
|
+
color: theme.colors.text.secondary,
|
|
138
|
+
fontFamily: theme.typography.fontFamily.caption,
|
|
139
|
+
fontSize: theme.typography.fontSize.xs,
|
|
140
|
+
lineHeight: theme.typography.lineHeight.labelSmall,
|
|
141
|
+
marginTop: 2
|
|
142
|
+
},
|
|
143
|
+
descriptionDisabled: {
|
|
144
|
+
color: theme.colors.text.disabled
|
|
145
|
+
},
|
|
146
|
+
iconWrapper: {
|
|
147
|
+
alignItems: 'center',
|
|
148
|
+
height: theme.spacing.lg,
|
|
149
|
+
justifyContent: 'center',
|
|
150
|
+
marginRight: theme.spacing.md,
|
|
151
|
+
width: theme.spacing.lg
|
|
152
|
+
},
|
|
153
|
+
label: {
|
|
154
|
+
fontFamily: theme.typography.fontFamily.body,
|
|
155
|
+
fontSize: theme.typography.fontSize.md,
|
|
156
|
+
lineHeight: theme.typography.lineHeight.bodySmall
|
|
157
|
+
},
|
|
158
|
+
labelDisabled: {
|
|
159
|
+
color: theme.colors.text.disabled
|
|
160
|
+
},
|
|
161
|
+
labelSelected: {
|
|
162
|
+
fontFamily: theme.typography.fontFamily.semibold
|
|
163
|
+
},
|
|
164
|
+
textWrapper: {
|
|
165
|
+
flex: 1
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { StyleSheet, TextInput, 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 { SelectSearchProps } from './Select.types'
|
|
10
|
+
|
|
11
|
+
const DEBOUNCE_MS = 200
|
|
12
|
+
|
|
13
|
+
export function SelectSearch({ onChangeText, value }: SelectSearchProps): JSX.Element {
|
|
14
|
+
const theme = useTheme()
|
|
15
|
+
const styles = React.useMemo(() => createStyles(theme), [theme])
|
|
16
|
+
const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
17
|
+
|
|
18
|
+
const handleChangeText = React.useCallback(
|
|
19
|
+
(text: string) => {
|
|
20
|
+
if (debounceRef.current) {
|
|
21
|
+
clearTimeout(debounceRef.current)
|
|
22
|
+
}
|
|
23
|
+
debounceRef.current = setTimeout(() => {
|
|
24
|
+
onChangeText(text)
|
|
25
|
+
}, DEBOUNCE_MS)
|
|
26
|
+
},
|
|
27
|
+
[onChangeText]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
if (debounceRef.current) {
|
|
33
|
+
clearTimeout(debounceRef.current)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View style={styles.container}>
|
|
40
|
+
<View style={styles.iconWrapper}>
|
|
41
|
+
<Icon color={theme.colors.text.tertiary} name='search' size={theme.spacing.md} />
|
|
42
|
+
</View>
|
|
43
|
+
<TextInput
|
|
44
|
+
accessibilityLabel='Search options'
|
|
45
|
+
autoCapitalize='none'
|
|
46
|
+
autoCorrect={false}
|
|
47
|
+
clearButtonMode='while-editing'
|
|
48
|
+
defaultValue={value}
|
|
49
|
+
onChangeText={handleChangeText}
|
|
50
|
+
placeholderTextColor={theme.colors.text.tertiary}
|
|
51
|
+
returnKeyType='search'
|
|
52
|
+
style={styles.input}
|
|
53
|
+
/>
|
|
54
|
+
</View>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createStyles(theme: Theme) {
|
|
59
|
+
return StyleSheet.create({
|
|
60
|
+
container: {
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
borderBottomColor: theme.colors.border.secondary,
|
|
63
|
+
borderBottomWidth: 1,
|
|
64
|
+
flexDirection: 'row',
|
|
65
|
+
marginBottom: theme.spacing.xs,
|
|
66
|
+
paddingHorizontal: theme.spacing.md,
|
|
67
|
+
paddingVertical: theme.spacing.sm
|
|
68
|
+
},
|
|
69
|
+
iconWrapper: {
|
|
70
|
+
marginRight: theme.spacing.xs
|
|
71
|
+
},
|
|
72
|
+
input: {
|
|
73
|
+
color: theme.colors.text.primary,
|
|
74
|
+
flex: 1,
|
|
75
|
+
fontFamily: theme.typography.fontFamily.body,
|
|
76
|
+
fontSize: theme.typography.fontSize.md,
|
|
77
|
+
lineHeight: theme.typography.lineHeight.bodySmall,
|
|
78
|
+
paddingVertical: theme.spacing.xs
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
@@ -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,336 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { StyleSheet, View } from 'react-native'
|
|
4
|
+
|
|
5
|
+
import { action } from '@storybook/addon-actions'
|
|
6
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
7
|
+
|
|
8
|
+
import { Select } from '.'
|
|
9
|
+
import type { SelectMultiProps, SelectOption, SelectSingleProps } from './Select.types'
|
|
10
|
+
|
|
11
|
+
const GENDER_OPTIONS: SelectOption[] = [
|
|
12
|
+
{ label: 'Female', value: 'F' },
|
|
13
|
+
{ label: 'Male', value: 'M' },
|
|
14
|
+
{ label: 'Non-binary', value: 'NB' },
|
|
15
|
+
{ label: 'Prefer not to say', value: 'X' }
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const COUNTRIES_OPTIONS: SelectOption[] = [
|
|
19
|
+
{ description: 'South America', flagCode: 'AR', label: 'Argentina', value: 'AR' },
|
|
20
|
+
{ description: 'Oceania', flagCode: 'AU', label: 'Australia', value: 'AU' },
|
|
21
|
+
{ description: 'South America', flagCode: 'BR', label: 'Brazil', value: 'BR' },
|
|
22
|
+
{ description: 'North America', flagCode: 'CA', label: 'Canada', value: 'CA' },
|
|
23
|
+
{ description: 'South America', flagCode: 'CL', label: 'Chile', value: 'CL' },
|
|
24
|
+
{ description: 'Asia', flagCode: 'CN', label: 'China', value: 'CN' },
|
|
25
|
+
{ description: 'Europe', flagCode: 'FR', label: 'France', value: 'FR' },
|
|
26
|
+
{ description: 'Europe', flagCode: 'DE', label: 'Germany', value: 'DE' },
|
|
27
|
+
{ description: 'Asia', flagCode: 'IN', label: 'India', value: 'IN' },
|
|
28
|
+
{ description: 'Europe', flagCode: 'IT', label: 'Italy', value: 'IT' },
|
|
29
|
+
{ description: 'Asia', flagCode: 'JP', label: 'Japan', value: 'JP' },
|
|
30
|
+
{ description: 'Europe', flagCode: 'PT', label: 'Portugal', value: 'PT' },
|
|
31
|
+
{ description: 'Europe', flagCode: 'ES', label: 'Spain', value: 'ES' },
|
|
32
|
+
{ description: 'North America', flagCode: 'US', label: 'United States', value: 'US' }
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const TAGS_OPTIONS: SelectOption[] = [
|
|
36
|
+
{ label: 'Design System', value: 'ds' },
|
|
37
|
+
{ label: 'Expo', value: 'expo' },
|
|
38
|
+
{ label: 'React Native', value: 'rn' },
|
|
39
|
+
{ label: 'Storybook', value: 'sb' },
|
|
40
|
+
{ label: 'Testing', value: 'test' },
|
|
41
|
+
{ label: 'TypeScript', value: 'ts' }
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const STATUS_OPTIONS: SelectOption[] = [
|
|
45
|
+
{ description: 'User is available', icon: 'correct', label: 'Available', labelColor: '#22c55e', value: 'available' },
|
|
46
|
+
{ description: 'User is in a meeting', icon: 'time', label: 'Busy', labelColor: '#f59e0b', value: 'busy' },
|
|
47
|
+
{ description: 'User is not here', disabled: true, icon: 'close', label: 'Away', value: 'away' },
|
|
48
|
+
{ description: 'Appears offline to others', icon: 'notificationOff', label: 'Offline', labelColor: '#94a3b8', value: 'offline' }
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
const ROLE_OPTIONS: SelectOption[] = [
|
|
52
|
+
{ color: '#fef9c3', label: 'Admin', labelColor: '#854d0e', value: 'admin' },
|
|
53
|
+
{ color: '#dcfce7', label: 'Editor', labelColor: '#166534', value: 'editor' },
|
|
54
|
+
{ color: '#dbeafe', label: 'Viewer', labelColor: '#1e40af', value: 'viewer' },
|
|
55
|
+
{ color: '#fee2e2', disabled: true, label: 'Banned', labelColor: '#991b1b', value: 'banned' }
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
interface StatefulSingleProps extends Omit<SelectSingleProps, 'onChange' | 'value'> {
|
|
59
|
+
initialValue?: string | number | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface StatefulMultiProps extends Omit<SelectMultiProps, 'multiple' | 'onChange' | 'value'> {
|
|
63
|
+
initialValues?: Array<string | number>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function StatefulSingleSelect({ initialValue = null, ...rest }: StatefulSingleProps) {
|
|
67
|
+
const [value, setValue] = React.useState<string | number | null>(initialValue)
|
|
68
|
+
|
|
69
|
+
const handleChange = React.useCallback(
|
|
70
|
+
(next: string | number | null) => {
|
|
71
|
+
setValue(next)
|
|
72
|
+
action('onChange')(next)
|
|
73
|
+
},
|
|
74
|
+
[]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return <Select {...rest} onChange={handleChange} value={value} />
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function StatefulMultiSelect({ initialValues = [], ...rest }: StatefulMultiProps) {
|
|
81
|
+
const [values, setValues] = React.useState<Array<string | number>>(initialValues)
|
|
82
|
+
|
|
83
|
+
const handleChange = React.useCallback(
|
|
84
|
+
(next: Array<string | number>) => {
|
|
85
|
+
setValues(next)
|
|
86
|
+
action('onChange')(next)
|
|
87
|
+
},
|
|
88
|
+
[]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return <Select {...rest} multiple onChange={handleChange} value={values} />
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const meta: Meta = {
|
|
95
|
+
component: Select,
|
|
96
|
+
parameters: {
|
|
97
|
+
layout: 'centered'
|
|
98
|
+
},
|
|
99
|
+
tags: ['autodocs'],
|
|
100
|
+
title: 'Forms/Select'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default meta
|
|
104
|
+
type Story = StoryObj<typeof meta>
|
|
105
|
+
|
|
106
|
+
export const Default: Story = {
|
|
107
|
+
render: () => (
|
|
108
|
+
<View style={styles.container}>
|
|
109
|
+
<StatefulSingleSelect
|
|
110
|
+
options={GENDER_OPTIONS}
|
|
111
|
+
placeholder='Select gender'
|
|
112
|
+
/>
|
|
113
|
+
</View>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const Selected: Story = {
|
|
118
|
+
render: () => (
|
|
119
|
+
<View style={styles.container}>
|
|
120
|
+
<StatefulSingleSelect
|
|
121
|
+
initialValue='BR'
|
|
122
|
+
options={COUNTRIES_OPTIONS}
|
|
123
|
+
placeholder='Select country'
|
|
124
|
+
/>
|
|
125
|
+
</View>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const WithFlags: Story = {
|
|
130
|
+
render: () => (
|
|
131
|
+
<View style={styles.container}>
|
|
132
|
+
<StatefulSingleSelect
|
|
133
|
+
clearable
|
|
134
|
+
options={COUNTRIES_OPTIONS}
|
|
135
|
+
placeholder='Select country'
|
|
136
|
+
searchable
|
|
137
|
+
/>
|
|
138
|
+
</View>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const WithIconsAndLabelColors: Story = {
|
|
143
|
+
render: () => (
|
|
144
|
+
<View style={styles.container}>
|
|
145
|
+
<StatefulSingleSelect
|
|
146
|
+
clearable
|
|
147
|
+
options={STATUS_OPTIONS}
|
|
148
|
+
placeholder='Select status'
|
|
149
|
+
/>
|
|
150
|
+
</View>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const WithRowColors: Story = {
|
|
155
|
+
render: () => (
|
|
156
|
+
<View style={styles.container}>
|
|
157
|
+
<StatefulSingleSelect
|
|
158
|
+
clearable
|
|
159
|
+
options={ROLE_OPTIONS}
|
|
160
|
+
placeholder='Select role'
|
|
161
|
+
/>
|
|
162
|
+
</View>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const MultipleSelect: Story = {
|
|
167
|
+
render: () => (
|
|
168
|
+
<View style={styles.container}>
|
|
169
|
+
<StatefulMultiSelect
|
|
170
|
+
clearable
|
|
171
|
+
options={TAGS_OPTIONS}
|
|
172
|
+
placeholder='Select tags'
|
|
173
|
+
/>
|
|
174
|
+
</View>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const MultipleWithInitialValues: Story = {
|
|
179
|
+
render: () => (
|
|
180
|
+
<View style={styles.container}>
|
|
181
|
+
<StatefulMultiSelect
|
|
182
|
+
clearable
|
|
183
|
+
initialValues={['rn', 'ts']}
|
|
184
|
+
options={TAGS_OPTIONS}
|
|
185
|
+
placeholder='Select tags'
|
|
186
|
+
/>
|
|
187
|
+
</View>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const MultipleSearchable: Story = {
|
|
192
|
+
render: () => (
|
|
193
|
+
<View style={styles.container}>
|
|
194
|
+
<StatefulMultiSelect
|
|
195
|
+
clearable
|
|
196
|
+
options={COUNTRIES_OPTIONS}
|
|
197
|
+
placeholder='Select countries'
|
|
198
|
+
searchable
|
|
199
|
+
/>
|
|
200
|
+
</View>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const MultipleWithDisabledOptions: Story = {
|
|
205
|
+
render: () => (
|
|
206
|
+
<View style={styles.container}>
|
|
207
|
+
<StatefulMultiSelect
|
|
208
|
+
clearable
|
|
209
|
+
options={STATUS_OPTIONS}
|
|
210
|
+
placeholder='Select statuses'
|
|
211
|
+
/>
|
|
212
|
+
</View>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const Clearable: Story = {
|
|
217
|
+
render: () => (
|
|
218
|
+
<View style={styles.container}>
|
|
219
|
+
<StatefulSingleSelect
|
|
220
|
+
clearable
|
|
221
|
+
initialValue='M'
|
|
222
|
+
options={GENDER_OPTIONS}
|
|
223
|
+
placeholder='Select gender'
|
|
224
|
+
/>
|
|
225
|
+
</View>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const WithError: Story = {
|
|
230
|
+
render: () => (
|
|
231
|
+
<View style={styles.container}>
|
|
232
|
+
<StatefulSingleSelect
|
|
233
|
+
errorMessage='This field is required'
|
|
234
|
+
inputError
|
|
235
|
+
options={GENDER_OPTIONS}
|
|
236
|
+
placeholder='Select gender'
|
|
237
|
+
required
|
|
238
|
+
/>
|
|
239
|
+
</View>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const Disabled: Story = {
|
|
244
|
+
render: () => (
|
|
245
|
+
<View style={styles.container}>
|
|
246
|
+
<StatefulSingleSelect
|
|
247
|
+
disabled
|
|
248
|
+
initialValue='M'
|
|
249
|
+
options={GENDER_OPTIONS}
|
|
250
|
+
placeholder='Select gender'
|
|
251
|
+
/>
|
|
252
|
+
</View>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export const Readonly: Story = {
|
|
257
|
+
render: () => (
|
|
258
|
+
<View style={styles.container}>
|
|
259
|
+
<StatefulSingleSelect
|
|
260
|
+
initialValue='BR'
|
|
261
|
+
options={COUNTRIES_OPTIONS}
|
|
262
|
+
placeholder='Select country'
|
|
263
|
+
readonly
|
|
264
|
+
/>
|
|
265
|
+
</View>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const WithDisabledOptions: Story = {
|
|
270
|
+
render: () => (
|
|
271
|
+
<View style={styles.container}>
|
|
272
|
+
<StatefulSingleSelect
|
|
273
|
+
options={STATUS_OPTIONS}
|
|
274
|
+
placeholder='Select status'
|
|
275
|
+
/>
|
|
276
|
+
</View>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const AllStates: Story = {
|
|
281
|
+
render: () => (
|
|
282
|
+
<View style={styles.column}>
|
|
283
|
+
<View style={styles.storyItem}>
|
|
284
|
+
<StatefulSingleSelect
|
|
285
|
+
options={GENDER_OPTIONS}
|
|
286
|
+
placeholder='Default - empty'
|
|
287
|
+
/>
|
|
288
|
+
</View>
|
|
289
|
+
<View style={styles.storyItem}>
|
|
290
|
+
<StatefulSingleSelect
|
|
291
|
+
clearable
|
|
292
|
+
initialValue='M'
|
|
293
|
+
options={GENDER_OPTIONS}
|
|
294
|
+
placeholder='With value + clearable'
|
|
295
|
+
/>
|
|
296
|
+
</View>
|
|
297
|
+
<View style={styles.storyItem}>
|
|
298
|
+
<StatefulSingleSelect
|
|
299
|
+
errorMessage='Required field'
|
|
300
|
+
inputError
|
|
301
|
+
options={GENDER_OPTIONS}
|
|
302
|
+
placeholder='Error state'
|
|
303
|
+
/>
|
|
304
|
+
</View>
|
|
305
|
+
<View style={styles.storyItem}>
|
|
306
|
+
<StatefulSingleSelect
|
|
307
|
+
disabled
|
|
308
|
+
initialValue='F'
|
|
309
|
+
options={GENDER_OPTIONS}
|
|
310
|
+
placeholder='Disabled'
|
|
311
|
+
/>
|
|
312
|
+
</View>
|
|
313
|
+
<View style={styles.storyItem}>
|
|
314
|
+
<StatefulMultiSelect
|
|
315
|
+
clearable
|
|
316
|
+
initialValues={['ts', 'rn']}
|
|
317
|
+
options={TAGS_OPTIONS}
|
|
318
|
+
placeholder='Multi select with chips'
|
|
319
|
+
/>
|
|
320
|
+
</View>
|
|
321
|
+
</View>
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const styles = StyleSheet.create({
|
|
326
|
+
column: {
|
|
327
|
+
gap: 12,
|
|
328
|
+
width: 320
|
|
329
|
+
},
|
|
330
|
+
container: {
|
|
331
|
+
width: 320
|
|
332
|
+
},
|
|
333
|
+
storyItem: {
|
|
334
|
+
width: '100%'
|
|
335
|
+
}
|
|
336
|
+
})
|
package/src/index.tsx
ADDED