@idealyst/components 1.0.82 → 1.0.83
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/package.json +7 -7
- package/src/Select/README.md +166 -0
- package/src/Select/Select.native.tsx +270 -0
- package/src/Select/Select.styles.tsx +325 -0
- package/src/Select/Select.web.tsx +436 -0
- package/src/Select/index.native.ts +2 -0
- package/src/Select/index.ts +2 -0
- package/src/Select/index.web.ts +2 -0
- package/src/Select/types.ts +118 -0
- package/src/examples/AllExamples.tsx +4 -0
- package/src/examples/SelectExamples.tsx +423 -0
- package/src/examples/index.ts +1 -0
- package/src/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.83",
|
|
4
4
|
"description": "Shared component library for React and React Native",
|
|
5
5
|
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
|
|
6
6
|
"readme": "README.md",
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"publish:npm": "npm publish"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@idealyst/theme": "^1.0.
|
|
45
|
-
"@mdi/js": "
|
|
46
|
-
"@mdi/react": "
|
|
47
|
-
"@react-native-vector-icons/common": "
|
|
48
|
-
"@react-native-vector-icons/material-design-icons": "
|
|
44
|
+
"@idealyst/theme": "^1.0.83",
|
|
45
|
+
"@mdi/js": ">=7.0.0",
|
|
46
|
+
"@mdi/react": ">=1.0.0",
|
|
47
|
+
"@react-native-vector-icons/common": ">=12.0.0",
|
|
48
|
+
"@react-native-vector-icons/material-design-icons": ">=12.0.0",
|
|
49
49
|
"@react-native/normalize-colors": "*",
|
|
50
50
|
"react": ">=16.8.0",
|
|
51
51
|
"react-native": ">=0.60.0",
|
|
52
52
|
"react-native-edge-to-edge": "*",
|
|
53
53
|
"react-native-nitro-modules": "*",
|
|
54
|
-
"react-native-unistyles": "
|
|
54
|
+
"react-native-unistyles": ">=3.0.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
|
57
57
|
"@idealyst/theme": {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Select Component
|
|
2
|
+
|
|
3
|
+
A cross-platform Select component for choosing from a list of options.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Cross-platform**: Works on both web and React Native
|
|
8
|
+
- **iOS ActionSheet support**: Native presentation mode on iOS
|
|
9
|
+
- **Searchable**: Optional search/filter functionality
|
|
10
|
+
- **Keyboard navigation**: Full keyboard support on web
|
|
11
|
+
- **Customizable styling**: Variants, intents, and sizes
|
|
12
|
+
- **Accessibility**: Proper ARIA attributes and screen reader support
|
|
13
|
+
|
|
14
|
+
## Basic Usage
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { Select } from '@idealyst/components';
|
|
18
|
+
|
|
19
|
+
const options = [
|
|
20
|
+
{ value: 'apple', label: 'Apple' },
|
|
21
|
+
{ value: 'banana', label: 'Banana' },
|
|
22
|
+
{ value: 'orange', label: 'Orange' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function MyComponent() {
|
|
26
|
+
const [value, setValue] = useState('');
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Select
|
|
30
|
+
options={options}
|
|
31
|
+
value={value}
|
|
32
|
+
onValueChange={setValue}
|
|
33
|
+
placeholder="Choose a fruit"
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## With Icons
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
const options = [
|
|
43
|
+
{
|
|
44
|
+
value: 'user',
|
|
45
|
+
label: 'User Profile',
|
|
46
|
+
icon: <Icon name="user" />
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
value: 'settings',
|
|
50
|
+
label: 'Settings',
|
|
51
|
+
icon: <Icon name="settings" />
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
<Select
|
|
56
|
+
options={options}
|
|
57
|
+
value={value}
|
|
58
|
+
onValueChange={setValue}
|
|
59
|
+
placeholder="Choose an option"
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Searchable Select
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Select
|
|
67
|
+
options={largeOptionsList}
|
|
68
|
+
value={value}
|
|
69
|
+
onValueChange={setValue}
|
|
70
|
+
searchable
|
|
71
|
+
placeholder="Search and select..."
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## iOS ActionSheet (Native only)
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<Select
|
|
79
|
+
options={options}
|
|
80
|
+
value={value}
|
|
81
|
+
onValueChange={setValue}
|
|
82
|
+
presentationMode="actionSheet" // iOS only
|
|
83
|
+
label="Choose an option"
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Form Integration
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<View spacing="md">
|
|
91
|
+
<Select
|
|
92
|
+
label="Country"
|
|
93
|
+
options={countryOptions}
|
|
94
|
+
value={country}
|
|
95
|
+
onValueChange={setCountry}
|
|
96
|
+
error={!!errors.country}
|
|
97
|
+
helperText={errors.country || "Select your country"}
|
|
98
|
+
variant="outlined"
|
|
99
|
+
intent="primary"
|
|
100
|
+
/>
|
|
101
|
+
</View>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API Reference
|
|
105
|
+
|
|
106
|
+
### SelectProps
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Default | Description |
|
|
109
|
+
|------|------|---------|-------------|
|
|
110
|
+
| `options` | `SelectOption[]` | - | Array of options to display |
|
|
111
|
+
| `value` | `string` | - | Currently selected value |
|
|
112
|
+
| `onValueChange` | `(value: string) => void` | - | Called when selection changes |
|
|
113
|
+
| `placeholder` | `string` | `"Select an option"` | Placeholder text |
|
|
114
|
+
| `disabled` | `boolean` | `false` | Whether the select is disabled |
|
|
115
|
+
| `error` | `boolean` | `false` | Whether to show error state |
|
|
116
|
+
| `helperText` | `string` | - | Helper text below select |
|
|
117
|
+
| `label` | `string` | - | Label text above select |
|
|
118
|
+
| `variant` | `'outlined' \| 'filled'` | `'outlined'` | Visual variant |
|
|
119
|
+
| `intent` | `IntentVariant` | `'neutral'` | Color scheme |
|
|
120
|
+
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Component size |
|
|
121
|
+
| `searchable` | `boolean` | `false` | Enable search functionality |
|
|
122
|
+
| `filterOption` | `(option, searchTerm) => boolean` | - | Custom filter function |
|
|
123
|
+
| `presentationMode` | `'dropdown' \| 'actionSheet'` | `'dropdown'` | Native presentation mode (iOS) |
|
|
124
|
+
| `maxHeight` | `number` | `240` | Max height for dropdown |
|
|
125
|
+
|
|
126
|
+
### SelectOption
|
|
127
|
+
|
|
128
|
+
| Prop | Type | Description |
|
|
129
|
+
|------|------|-------------|
|
|
130
|
+
| `value` | `string` | Unique value for the option |
|
|
131
|
+
| `label` | `string` | Display label |
|
|
132
|
+
| `disabled` | `boolean` | Whether option is disabled |
|
|
133
|
+
| `icon` | `ReactNode` | Optional icon or custom content |
|
|
134
|
+
|
|
135
|
+
## Platform Differences
|
|
136
|
+
|
|
137
|
+
### Web
|
|
138
|
+
- Uses a custom dropdown overlay
|
|
139
|
+
- Full keyboard navigation support
|
|
140
|
+
- Hover effects and focus states
|
|
141
|
+
- Searchable with text input
|
|
142
|
+
|
|
143
|
+
### React Native
|
|
144
|
+
- Modal-based dropdown by default
|
|
145
|
+
- iOS ActionSheet support via `presentationMode="actionSheet"`
|
|
146
|
+
- Touch-optimized interactions
|
|
147
|
+
- Native keyboard handling
|
|
148
|
+
|
|
149
|
+
## Styling
|
|
150
|
+
|
|
151
|
+
The Select component uses Unistyles v3 for cross-platform styling with support for:
|
|
152
|
+
|
|
153
|
+
- Variants (`outlined`, `filled`)
|
|
154
|
+
- Intents (`primary`, `neutral`, `success`, `error`, `warning`, `info`)
|
|
155
|
+
- Sizes (`small`, `medium`, `large`)
|
|
156
|
+
- Error states
|
|
157
|
+
- Disabled states
|
|
158
|
+
- Focus states
|
|
159
|
+
|
|
160
|
+
## Accessibility
|
|
161
|
+
|
|
162
|
+
- Proper ARIA roles and attributes
|
|
163
|
+
- Keyboard navigation (web)
|
|
164
|
+
- Screen reader support
|
|
165
|
+
- Focus management
|
|
166
|
+
- Touch target sizing (44px minimum)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
Pressable,
|
|
6
|
+
Modal,
|
|
7
|
+
ScrollView,
|
|
8
|
+
TextInput,
|
|
9
|
+
ActionSheetIOS,
|
|
10
|
+
Platform,
|
|
11
|
+
Animated,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { SelectProps, SelectOption } from './types';
|
|
14
|
+
import { selectStyles } from './Select.styles';
|
|
15
|
+
|
|
16
|
+
const Select: React.FC<SelectProps> = ({
|
|
17
|
+
options,
|
|
18
|
+
value,
|
|
19
|
+
onValueChange,
|
|
20
|
+
placeholder = 'Select an option',
|
|
21
|
+
disabled = false,
|
|
22
|
+
error = false,
|
|
23
|
+
helperText,
|
|
24
|
+
label,
|
|
25
|
+
variant = 'outlined',
|
|
26
|
+
intent = 'neutral',
|
|
27
|
+
size = 'medium',
|
|
28
|
+
searchable = false,
|
|
29
|
+
filterOption,
|
|
30
|
+
presentationMode = 'dropdown',
|
|
31
|
+
maxHeight = 240,
|
|
32
|
+
style,
|
|
33
|
+
testID,
|
|
34
|
+
accessibilityLabel,
|
|
35
|
+
}) => {
|
|
36
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
37
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
38
|
+
const scaleAnim = useRef(new Animated.Value(0)).current;
|
|
39
|
+
|
|
40
|
+
const selectedOption = options.find(option => option.value === value);
|
|
41
|
+
|
|
42
|
+
// Filter options based on search term
|
|
43
|
+
const filteredOptions = searchable && searchTerm
|
|
44
|
+
? options.filter(option => {
|
|
45
|
+
if (filterOption) {
|
|
46
|
+
return filterOption(option, searchTerm);
|
|
47
|
+
}
|
|
48
|
+
return option.label.toLowerCase().includes(searchTerm.toLowerCase());
|
|
49
|
+
})
|
|
50
|
+
: options;
|
|
51
|
+
|
|
52
|
+
// Apply styles with variants
|
|
53
|
+
selectStyles.useVariants({
|
|
54
|
+
variant: variant as any,
|
|
55
|
+
size,
|
|
56
|
+
intent,
|
|
57
|
+
disabled,
|
|
58
|
+
error,
|
|
59
|
+
focused: isOpen,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const handleTriggerPress = () => {
|
|
63
|
+
if (disabled) return;
|
|
64
|
+
|
|
65
|
+
if (Platform.OS === 'ios' && presentationMode === 'actionSheet') {
|
|
66
|
+
showIOSActionSheet();
|
|
67
|
+
} else {
|
|
68
|
+
setIsOpen(true);
|
|
69
|
+
setSearchTerm('');
|
|
70
|
+
// Animate dropdown appearance
|
|
71
|
+
Animated.spring(scaleAnim, {
|
|
72
|
+
toValue: 1,
|
|
73
|
+
useNativeDriver: true,
|
|
74
|
+
tension: 300,
|
|
75
|
+
friction: 20,
|
|
76
|
+
}).start();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const showIOSActionSheet = () => {
|
|
81
|
+
const actionOptions = options.map(option => option.label);
|
|
82
|
+
const cancelButtonIndex = actionOptions.length;
|
|
83
|
+
actionOptions.push('Cancel');
|
|
84
|
+
|
|
85
|
+
ActionSheetIOS.showActionSheetWithOptions(
|
|
86
|
+
{
|
|
87
|
+
options: actionOptions,
|
|
88
|
+
cancelButtonIndex,
|
|
89
|
+
title: label || 'Select an option',
|
|
90
|
+
message: helperText,
|
|
91
|
+
},
|
|
92
|
+
(buttonIndex) => {
|
|
93
|
+
if (buttonIndex !== cancelButtonIndex && buttonIndex < options.length) {
|
|
94
|
+
const selectedOption = options[buttonIndex];
|
|
95
|
+
if (!selectedOption.disabled) {
|
|
96
|
+
onValueChange?.(selectedOption.value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleOptionSelect = (option: SelectOption) => {
|
|
104
|
+
if (!option.disabled) {
|
|
105
|
+
onValueChange?.(option.value);
|
|
106
|
+
closeDropdown();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const closeDropdown = () => {
|
|
111
|
+
Animated.spring(scaleAnim, {
|
|
112
|
+
toValue: 0,
|
|
113
|
+
useNativeDriver: true,
|
|
114
|
+
tension: 300,
|
|
115
|
+
friction: 20,
|
|
116
|
+
}).start(() => {
|
|
117
|
+
setIsOpen(false);
|
|
118
|
+
setSearchTerm('');
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleSearchChange = (text: string) => {
|
|
123
|
+
setSearchTerm(text);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const renderChevron = () => (
|
|
127
|
+
<View style={selectStyles.chevron}>
|
|
128
|
+
<Text style={{ color: 'currentColor' }}>▼</Text>
|
|
129
|
+
</View>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const renderDropdown = () => (
|
|
133
|
+
<Modal
|
|
134
|
+
visible={isOpen}
|
|
135
|
+
transparent
|
|
136
|
+
animationType="none"
|
|
137
|
+
onRequestClose={closeDropdown}
|
|
138
|
+
>
|
|
139
|
+
<Pressable style={selectStyles.overlay} onPress={closeDropdown}>
|
|
140
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
141
|
+
<Animated.View
|
|
142
|
+
style={[
|
|
143
|
+
selectStyles.dropdown,
|
|
144
|
+
{
|
|
145
|
+
maxHeight,
|
|
146
|
+
transform: [{ scale: scaleAnim }],
|
|
147
|
+
minWidth: 280,
|
|
148
|
+
maxWidth: '90%',
|
|
149
|
+
},
|
|
150
|
+
]}
|
|
151
|
+
>
|
|
152
|
+
{searchable && (
|
|
153
|
+
<View style={selectStyles.searchContainer}>
|
|
154
|
+
<TextInput
|
|
155
|
+
style={selectStyles.searchInput}
|
|
156
|
+
placeholder="Search options..."
|
|
157
|
+
value={searchTerm}
|
|
158
|
+
onChangeText={handleSearchChange}
|
|
159
|
+
autoFocus
|
|
160
|
+
/>
|
|
161
|
+
</View>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
<ScrollView style={selectStyles.optionsList} showsVerticalScrollIndicator={false}>
|
|
165
|
+
{filteredOptions.map((option) => {
|
|
166
|
+
const isSelected = option.value === value;
|
|
167
|
+
|
|
168
|
+
selectStyles.useVariants({
|
|
169
|
+
selected: isSelected,
|
|
170
|
+
disabled: option.disabled,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Pressable
|
|
175
|
+
key={option.value}
|
|
176
|
+
style={selectStyles.option}
|
|
177
|
+
onPress={() => handleOptionSelect(option)}
|
|
178
|
+
disabled={option.disabled}
|
|
179
|
+
android_ripple={{ color: 'rgba(0, 0, 0, 0.1)' }}
|
|
180
|
+
>
|
|
181
|
+
<View style={selectStyles.optionContent}>
|
|
182
|
+
{option.icon && (
|
|
183
|
+
<View style={selectStyles.optionIcon}>
|
|
184
|
+
{option.icon}
|
|
185
|
+
</View>
|
|
186
|
+
)}
|
|
187
|
+
<Text
|
|
188
|
+
style={[
|
|
189
|
+
selectStyles.optionText,
|
|
190
|
+
option.disabled && selectStyles.optionTextDisabled,
|
|
191
|
+
]}
|
|
192
|
+
>
|
|
193
|
+
{option.label}
|
|
194
|
+
</Text>
|
|
195
|
+
</View>
|
|
196
|
+
</Pressable>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
|
|
200
|
+
{filteredOptions.length === 0 && (
|
|
201
|
+
<View style={selectStyles.option}>
|
|
202
|
+
<Text style={selectStyles.optionText}>
|
|
203
|
+
No options found
|
|
204
|
+
</Text>
|
|
205
|
+
</View>
|
|
206
|
+
)}
|
|
207
|
+
</ScrollView>
|
|
208
|
+
</Animated.View>
|
|
209
|
+
</View>
|
|
210
|
+
</Pressable>
|
|
211
|
+
</Modal>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<View style={[selectStyles.container, style]} testID={testID}>
|
|
216
|
+
{label && (
|
|
217
|
+
<Text style={selectStyles.label}>
|
|
218
|
+
{label}
|
|
219
|
+
</Text>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
<Pressable
|
|
223
|
+
style={selectStyles.trigger}
|
|
224
|
+
onPress={handleTriggerPress}
|
|
225
|
+
disabled={disabled}
|
|
226
|
+
accessibilityLabel={accessibilityLabel || label}
|
|
227
|
+
accessibilityRole="button"
|
|
228
|
+
accessibilityState={{
|
|
229
|
+
expanded: isOpen,
|
|
230
|
+
disabled,
|
|
231
|
+
}}
|
|
232
|
+
android_ripple={{ color: 'rgba(0, 0, 0, 0.1)' }}
|
|
233
|
+
>
|
|
234
|
+
<View style={selectStyles.triggerContent}>
|
|
235
|
+
{selectedOption?.icon && (
|
|
236
|
+
<View style={selectStyles.icon}>
|
|
237
|
+
{selectedOption.icon}
|
|
238
|
+
</View>
|
|
239
|
+
)}
|
|
240
|
+
<Text
|
|
241
|
+
style={[
|
|
242
|
+
selectedOption ? selectStyles.triggerText : selectStyles.placeholder,
|
|
243
|
+
]}
|
|
244
|
+
numberOfLines={1}
|
|
245
|
+
>
|
|
246
|
+
{selectedOption ? selectedOption.label : placeholder}
|
|
247
|
+
</Text>
|
|
248
|
+
</View>
|
|
249
|
+
|
|
250
|
+
{renderChevron()}
|
|
251
|
+
</Pressable>
|
|
252
|
+
|
|
253
|
+
{/* Only render dropdown modal if not using iOS ActionSheet */}
|
|
254
|
+
{!(Platform.OS === 'ios' && presentationMode === 'actionSheet') && renderDropdown()}
|
|
255
|
+
|
|
256
|
+
{helperText && (
|
|
257
|
+
<Text
|
|
258
|
+
style={[
|
|
259
|
+
selectStyles.helperText,
|
|
260
|
+
error && selectStyles.helperText.variants?.error?.true,
|
|
261
|
+
]}
|
|
262
|
+
>
|
|
263
|
+
{helperText}
|
|
264
|
+
</Text>
|
|
265
|
+
)}
|
|
266
|
+
</View>
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export default Select;
|