@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/components",
3
- "version": "1.0.82",
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.82",
45
- "@mdi/js": "^7.4.47",
46
- "@mdi/react": "^1.6.1",
47
- "@react-native-vector-icons/common": "^12.0.1",
48
- "@react-native-vector-icons/material-design-icons": "^12.0.1",
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": "^3.0.4"
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;