react-native-molecules 0.5.0-beta.21 → 0.5.0-beta.23
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/components/Button/Button.tsx +3 -1
- package/components/Card/Card.tsx +1 -1
- package/components/Checkbox/CheckboxBase.ios.tsx +1 -4
- package/components/Checkbox/CheckboxBase.tsx +2 -7
- package/components/DatePicker/DateCalendar.tsx +4 -4
- package/components/DatePicker/DatePickerModal.tsx +2 -1
- package/components/DatePicker/utils.ts +2 -0
- package/components/DatePickerInline/DatePickerDockedHeader.tsx +3 -3
- package/components/DatePickerInline/DatePickerInline.tsx +1 -1
- package/components/DatePickerInline/DatePickerInlineBase.tsx +2 -2
- package/components/DatePickerInline/DatePickerInlineHeader.tsx +43 -17
- package/components/DatePickerInline/HeaderItem.tsx +2 -2
- package/components/DatePickerInline/MonthPicker.tsx +58 -64
- package/components/DatePickerInline/Swiper.native.tsx +2 -2
- package/components/DatePickerInline/Swiper.tsx +3 -3
- package/components/DatePickerInline/YearPicker.tsx +108 -119
- package/components/DatePickerInline/{DatePickerContext.tsx → store.tsx} +7 -3
- package/components/DatePickerInline/types.ts +1 -1
- package/components/Divider/Divider.tsx +192 -0
- package/components/Divider/index.tsx +11 -0
- package/components/Drawer/DrawerItemGroup.tsx +3 -7
- package/components/IconButton/IconButton.tsx +2 -12
- package/components/List/List.tsx +275 -0
- package/components/List/context.tsx +26 -0
- package/components/List/index.ts +8 -0
- package/components/List/types.ts +117 -0
- package/components/List/utils.ts +79 -0
- package/components/Menu/Menu.tsx +146 -19
- package/components/Menu/index.tsx +9 -7
- package/components/Menu/utils.ts +21 -70
- package/components/Popover/Popover.tsx +7 -10
- package/components/Popover/PopoverRoot.tsx +6 -20
- package/components/Popover/common.ts +4 -0
- package/components/Popover/index.ts +2 -8
- package/components/Popover/usePlatformMeasure.ts +4 -2
- package/components/RadioButton/RadioButtonAndroid.tsx +38 -54
- package/components/RadioButton/RadioButtonIOS.tsx +2 -16
- package/components/Select/Select.tsx +307 -501
- package/components/Select/context.tsx +39 -32
- package/components/Select/types.ts +63 -56
- package/components/Select/utils.ts +19 -44
- package/components/Text/textFactory.tsx +17 -5
- package/components/TimePicker/TimeInput.tsx +2 -7
- package/components/TimePicker/utils.ts +0 -4
- package/components/TouchableRipple/TouchableRipple.native.tsx +36 -5
- package/components/TouchableRipple/TouchableRipple.tsx +121 -163
- package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
- package/package.json +6 -3
- package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
- package/components/HorizontalDivider/index.tsx +0 -9
- package/components/ListItem/ListItem.tsx +0 -138
- package/components/ListItem/ListItemDescription.tsx +0 -25
- package/components/ListItem/ListItemTitle.tsx +0 -25
- package/components/ListItem/index.tsx +0 -14
- package/components/ListItem/utils.ts +0 -115
- package/components/Menu/MenuDivider.tsx +0 -13
- package/components/Menu/MenuItem.tsx +0 -128
- package/components/VerticalDivider/VerticalDivider.tsx +0 -100
- package/components/VerticalDivider/index.tsx +0 -9
|
@@ -1,178 +1,84 @@
|
|
|
1
|
-
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
1
|
+
import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
type AccessibilityRole,
|
|
4
4
|
type GestureResponderEvent,
|
|
5
5
|
type LayoutChangeEvent,
|
|
6
6
|
Platform,
|
|
7
7
|
Pressable,
|
|
8
|
-
ScrollView,
|
|
9
8
|
View,
|
|
10
9
|
} from 'react-native';
|
|
11
10
|
|
|
12
11
|
import { typedMemo } from '../../hocs';
|
|
13
|
-
import { useActionState, useControlledValue
|
|
12
|
+
import { useActionState, useControlledValue } from '../../hooks';
|
|
14
13
|
import { useToggle } from '../../hooks';
|
|
15
14
|
import { resolveStateVariant } from '../../utils';
|
|
16
15
|
import { Chip } from '../Chip';
|
|
17
16
|
import { Icon } from '../Icon';
|
|
18
17
|
import { IconButton } from '../IconButton';
|
|
18
|
+
import { List } from '../List';
|
|
19
19
|
import { Popover } from '../Popover';
|
|
20
20
|
import { Text } from '../Text';
|
|
21
21
|
import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
|
|
22
22
|
import {
|
|
23
|
-
SelectContextProvider,
|
|
24
23
|
SelectDropdownContextProvider,
|
|
24
|
+
SelectSearchContextProvider,
|
|
25
25
|
useSelectContextValue,
|
|
26
26
|
useSelectDropdownContextValue,
|
|
27
|
+
useSelectSearchContextValue,
|
|
27
28
|
} from './context';
|
|
28
29
|
import type {
|
|
29
30
|
DefaultItemT,
|
|
30
31
|
SelectContentProps,
|
|
31
|
-
SelectContextValue,
|
|
32
32
|
SelectDropdownProps,
|
|
33
|
-
SelectGroupProps,
|
|
34
33
|
SelectOptionProps,
|
|
35
34
|
SelectProps,
|
|
35
|
+
SelectSearchContextValue,
|
|
36
36
|
SelectSearchInputProps,
|
|
37
|
+
SelectSearchKey,
|
|
37
38
|
SelectTriggerProps,
|
|
38
39
|
SelectValueProps,
|
|
39
40
|
} from './types';
|
|
40
|
-
import { styles, triggerStyles } from './utils';
|
|
41
|
+
import { collectWebSelectKeyboardOptionElements, styles, triggerStyles } from './utils';
|
|
41
42
|
|
|
42
43
|
const emptyArr: unknown[] = [];
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
onChange,
|
|
51
|
-
multiple = false,
|
|
52
|
-
disabled = false,
|
|
53
|
-
error = false,
|
|
54
|
-
labelKey = 'label',
|
|
55
|
-
options = emptyArr as Option[],
|
|
56
|
-
searchKey,
|
|
57
|
-
onSearchChange,
|
|
58
|
-
hideSelected: hideSelectedProp,
|
|
59
|
-
}: SelectProps<Option>) => {
|
|
60
|
-
const [value, onValueChange] = useControlledValue<Option['id'] | Option['id'][] | null>({
|
|
61
|
-
value: valueProp,
|
|
62
|
-
defaultValue: defaultValue ?? (multiple ? (emptyArr as Option['id'][]) : null),
|
|
63
|
-
onChange,
|
|
64
|
-
});
|
|
65
|
-
const valueRef = useLatest(value);
|
|
66
|
-
|
|
67
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
68
|
-
|
|
69
|
-
const handleSearchQueryChange = useCallback(
|
|
70
|
-
(query: string) => {
|
|
71
|
-
setSearchQuery(query);
|
|
72
|
-
onSearchChange?.(query);
|
|
73
|
-
},
|
|
74
|
-
[onSearchChange],
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Default hideSelected to multiple (true for multi-select, false for single select)
|
|
78
|
-
const hideSelected = hideSelectedProp !== undefined ? hideSelectedProp : multiple;
|
|
79
|
-
|
|
80
|
-
const filteredOptions = useMemo(() => {
|
|
81
|
-
let result = options;
|
|
82
|
-
|
|
83
|
-
// Filter out selected items if hideSelected is true
|
|
84
|
-
if (hideSelected) {
|
|
85
|
-
result = result.filter(item => {
|
|
86
|
-
if (multiple) {
|
|
87
|
-
const values = (value as Option['id'][]) || [];
|
|
88
|
-
return !values.some(v => v === item.id);
|
|
89
|
-
} else {
|
|
90
|
-
const singleValue = value as Option['id'] | null;
|
|
91
|
-
return singleValue !== item.id;
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Apply search filter if there's a search query
|
|
97
|
-
if (searchQuery) {
|
|
98
|
-
const key = searchKey || labelKey || 'label';
|
|
99
|
-
const lowerQuery = searchQuery.toLowerCase();
|
|
100
|
-
result = result.filter(item => {
|
|
101
|
-
const itemValue = item[key];
|
|
102
|
-
return String(itemValue || '')
|
|
103
|
-
.toLowerCase()
|
|
104
|
-
.includes(lowerQuery);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return result;
|
|
109
|
-
}, [options, searchQuery, searchKey, labelKey, hideSelected, multiple, value]);
|
|
110
|
-
|
|
111
|
-
const onAdd = useCallback(
|
|
112
|
-
(item: Option) => {
|
|
113
|
-
if (multiple) {
|
|
114
|
-
const currentValue = (valueRef.current as Option['id'][]) || [];
|
|
115
|
-
if (!currentValue.find(v => v === item.id)) {
|
|
116
|
-
onValueChange([...currentValue, item.id] as Option['id'][], item);
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
onValueChange(item.id, item);
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
[multiple, valueRef, onValueChange],
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const onRemove = useCallback(
|
|
126
|
-
(item: Option) => {
|
|
127
|
-
if (multiple) {
|
|
128
|
-
const currentValue = (valueRef.current as Option['id'][]) || [];
|
|
129
|
-
onValueChange(currentValue.filter(v => v !== item.id) as Option['id'][], item);
|
|
130
|
-
} else {
|
|
131
|
-
onValueChange(null, item);
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
[multiple, valueRef, onValueChange],
|
|
135
|
-
);
|
|
45
|
+
const getDisplayLabel = (item: DefaultItemT, labelKey?: string) => {
|
|
46
|
+
const itemLabelKey = typeof item.labelKey === 'string' ? item.labelKey : undefined;
|
|
47
|
+
const key = labelKey ?? itemLabelKey ?? 'label';
|
|
48
|
+
const value = item[key];
|
|
49
|
+
return value == null ? String(item.id) : String(value);
|
|
50
|
+
};
|
|
136
51
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
searchQuery,
|
|
148
|
-
setSearchQuery: handleSearchQueryChange,
|
|
149
|
-
filteredOptions,
|
|
150
|
-
}),
|
|
151
|
-
[
|
|
152
|
-
value,
|
|
153
|
-
multiple,
|
|
154
|
-
onAdd,
|
|
155
|
-
onRemove,
|
|
156
|
-
disabled,
|
|
157
|
-
error,
|
|
158
|
-
labelKey,
|
|
159
|
-
options,
|
|
160
|
-
searchQuery,
|
|
161
|
-
handleSearchQueryChange,
|
|
162
|
-
filteredOptions,
|
|
163
|
-
],
|
|
164
|
-
);
|
|
52
|
+
const getNested = (item: unknown, path: string): unknown => {
|
|
53
|
+
if (item == null || typeof item !== 'object') return undefined;
|
|
54
|
+
if (!path.includes('.')) return (item as Record<string, unknown>)[path];
|
|
55
|
+
let val: unknown = item;
|
|
56
|
+
for (const part of path.split('.')) {
|
|
57
|
+
if (val == null || typeof val !== 'object') return undefined;
|
|
58
|
+
val = (val as Record<string, unknown>)[part];
|
|
59
|
+
}
|
|
60
|
+
return val;
|
|
61
|
+
};
|
|
165
62
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
63
|
+
const matchesByKey = (item: unknown, key: string, lowerQuery: string): boolean =>
|
|
64
|
+
String(getNested(item, key) ?? '')
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.includes(lowerQuery);
|
|
67
|
+
|
|
68
|
+
const applySearch = <T extends object>(
|
|
69
|
+
items: T[],
|
|
70
|
+
searchKey: SelectSearchKey<T> | undefined,
|
|
71
|
+
query: string,
|
|
72
|
+
): T[] => {
|
|
73
|
+
if (!query) return items;
|
|
74
|
+
if (typeof searchKey === 'function') {
|
|
75
|
+
return items.filter(item => searchKey(item, query));
|
|
76
|
+
}
|
|
77
|
+
const keys = Array.isArray(searchKey) ? searchKey : [searchKey || 'label'];
|
|
78
|
+
const lowerQuery = query.toLowerCase();
|
|
79
|
+
return items.filter(item => keys.some(key => matchesByKey(item, key, lowerQuery)));
|
|
80
|
+
};
|
|
174
81
|
|
|
175
|
-
// SelectDropdownProvider - manages dropdown state
|
|
176
82
|
const SelectDropdownProvider = memo(
|
|
177
83
|
({
|
|
178
84
|
children,
|
|
@@ -185,7 +91,6 @@ const SelectDropdownProvider = memo(
|
|
|
185
91
|
}) => {
|
|
186
92
|
const { state: isOpen, handleOpen, handleClose } = useToggle(false);
|
|
187
93
|
const triggerRef = useRef<View>(null);
|
|
188
|
-
const contentRef = useRef<any>(null);
|
|
189
94
|
const [triggerLayout, setTriggerLayout] = useState<{
|
|
190
95
|
width: number;
|
|
191
96
|
height: number;
|
|
@@ -212,7 +117,6 @@ const SelectDropdownProvider = memo(
|
|
|
212
117
|
onClose,
|
|
213
118
|
onOpen,
|
|
214
119
|
triggerRef: triggerRef as React.RefObject<View>,
|
|
215
|
-
contentRef,
|
|
216
120
|
triggerLayout,
|
|
217
121
|
setTriggerLayout,
|
|
218
122
|
}),
|
|
@@ -227,23 +131,98 @@ const SelectDropdownProvider = memo(
|
|
|
227
131
|
},
|
|
228
132
|
);
|
|
229
133
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
134
|
+
export const SelectRoot = typedMemo(
|
|
135
|
+
<Option extends DefaultItemT = DefaultItemT>({
|
|
136
|
+
children,
|
|
137
|
+
options = emptyArr as Option[],
|
|
138
|
+
searchKey,
|
|
139
|
+
searchQuery: searchQueryProp,
|
|
140
|
+
defaultSearchQuery,
|
|
141
|
+
onSearchChange,
|
|
142
|
+
searchMode = 'client',
|
|
143
|
+
getItemId,
|
|
144
|
+
...listProps
|
|
145
|
+
}: SelectProps<Option>) => {
|
|
146
|
+
const [searchQuery, setSearchQuery] = useControlledValue<string>({
|
|
147
|
+
value: searchQueryProp,
|
|
148
|
+
defaultValue: defaultSearchQuery ?? '',
|
|
149
|
+
onChange: onSearchChange,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const getOptionId = useMemo(
|
|
153
|
+
() => (getItemId ?? ((item: Option) => item.id)) as (item: Option) => string | number,
|
|
154
|
+
[getItemId],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const filteredOptions = useMemo(() => {
|
|
158
|
+
if (searchMode === 'external') return options;
|
|
159
|
+
return applySearch(options, searchKey, searchQuery);
|
|
160
|
+
}, [options, searchKey, searchMode, searchQuery]);
|
|
161
|
+
|
|
162
|
+
const optionById = useMemo(() => {
|
|
163
|
+
const map = new Map<string | number, Option>();
|
|
164
|
+
for (const option of options) {
|
|
165
|
+
map.set(getOptionId(option), option);
|
|
166
|
+
}
|
|
167
|
+
return map;
|
|
168
|
+
}, [getOptionId, options]);
|
|
169
|
+
|
|
170
|
+
const searchContextValue = useMemo(
|
|
171
|
+
() =>
|
|
172
|
+
({
|
|
173
|
+
searchQuery,
|
|
174
|
+
setSearchQuery,
|
|
175
|
+
allOptions: options,
|
|
176
|
+
options: filteredOptions,
|
|
177
|
+
optionById,
|
|
178
|
+
getOptionId,
|
|
179
|
+
} as unknown as SelectSearchContextValue<DefaultItemT>),
|
|
180
|
+
[filteredOptions, getOptionId, optionById, options, searchQuery, setSearchQuery],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<SelectSearchContextProvider value={searchContextValue}>
|
|
185
|
+
<List {...listProps}>
|
|
186
|
+
<SelectDropdownProvider>{children}</SelectDropdownProvider>
|
|
187
|
+
</List>
|
|
188
|
+
</SelectSearchContextProvider>
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
export const SelectContent = typedMemo(
|
|
194
|
+
<Option extends DefaultItemT = DefaultItemT>({
|
|
195
|
+
children,
|
|
196
|
+
...rest
|
|
197
|
+
}: SelectContentProps<Option>) => {
|
|
198
|
+
const { options, getOptionId } = useSelectSearchContextValue(state => ({
|
|
199
|
+
options: state.options as Option[],
|
|
200
|
+
getOptionId: state.getOptionId as (item: Option) => string | number,
|
|
201
|
+
}));
|
|
202
|
+
const isSelectedId = useSelectContextValue(state => state.isSelectedId);
|
|
203
|
+
|
|
204
|
+
if (typeof children !== 'function') {
|
|
205
|
+
return <List.Content {...rest}>{children}</List.Content>;
|
|
206
|
+
}
|
|
207
|
+
|
|
233
208
|
return (
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
209
|
+
<List.Content {...rest}>
|
|
210
|
+
{options.map(item => (
|
|
211
|
+
<Fragment key={String(getOptionId(item))}>
|
|
212
|
+
{children(item, isSelectedId(getOptionId(item)))}
|
|
213
|
+
</Fragment>
|
|
214
|
+
))}
|
|
215
|
+
</List.Content>
|
|
237
216
|
);
|
|
238
217
|
},
|
|
239
218
|
);
|
|
240
219
|
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
const { onOpen, isOpen, triggerRef, setTriggerLayout } = useSelectDropdownContextValue(
|
|
220
|
+
export const SelectTrigger = ({ children, style, ...rest }: SelectTriggerProps) => {
|
|
221
|
+
const { isOpen, onOpen, onClose, triggerRef, setTriggerLayout } = useSelectDropdownContextValue(
|
|
244
222
|
state => ({
|
|
245
|
-
onOpen: state.onOpen,
|
|
246
223
|
isOpen: state.isOpen,
|
|
224
|
+
onOpen: state.onOpen,
|
|
225
|
+
onClose: state.onClose,
|
|
247
226
|
triggerRef: state.triggerRef,
|
|
248
227
|
setTriggerLayout: state.setTriggerLayout,
|
|
249
228
|
}),
|
|
@@ -279,10 +258,13 @@ const SelectTrigger = ({ children, style, ...rest }: SelectTriggerProps) => {
|
|
|
279
258
|
);
|
|
280
259
|
|
|
281
260
|
const handlePress = useCallback(() => {
|
|
282
|
-
if (
|
|
261
|
+
if (disabled) return;
|
|
262
|
+
if (!isOpen) {
|
|
283
263
|
onOpen();
|
|
264
|
+
} else {
|
|
265
|
+
onClose();
|
|
284
266
|
}
|
|
285
|
-
}, [isOpen, onOpen, disabled]);
|
|
267
|
+
}, [isOpen, onOpen, onClose, disabled]);
|
|
286
268
|
|
|
287
269
|
return (
|
|
288
270
|
<Pressable
|
|
@@ -307,69 +289,70 @@ const SelectTrigger = ({ children, style, ...rest }: SelectTriggerProps) => {
|
|
|
307
289
|
|
|
308
290
|
SelectTrigger.displayName = 'Select_Trigger';
|
|
309
291
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
292
|
+
export const SelectValue = memo(
|
|
293
|
+
({ placeholder, labelKey, renderValue, style, ...rest }: SelectValueProps) => {
|
|
294
|
+
const { value, multiple, onRemove } = useSelectContextValue(state => ({
|
|
295
|
+
value: state.value,
|
|
296
|
+
multiple: state.multiple,
|
|
297
|
+
onRemove: state.onRemove,
|
|
298
|
+
}));
|
|
299
|
+
const { optionById } = useSelectSearchContextValue(state => ({
|
|
300
|
+
optionById: state.optionById,
|
|
301
|
+
}));
|
|
319
302
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
};
|
|
303
|
+
const resolvedValue = useMemo(() => {
|
|
304
|
+
const resolve = (id: unknown) => {
|
|
305
|
+
if (id === null || id === undefined) return null;
|
|
306
|
+
const found = optionById.get(id as string | number);
|
|
307
|
+
return found || { id: id as string | number };
|
|
308
|
+
};
|
|
327
309
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
310
|
+
if (multiple) {
|
|
311
|
+
return (Array.isArray(value) ? value : []).map(resolve).filter(Boolean);
|
|
312
|
+
}
|
|
313
|
+
return resolve(value);
|
|
314
|
+
}, [optionById, value, multiple]);
|
|
333
315
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
316
|
+
const displayValue = useMemo(() => {
|
|
317
|
+
if (!resolvedValue) return placeholder || '';
|
|
318
|
+
if (multiple && (resolvedValue as any[]).length === 0) return placeholder || '';
|
|
337
319
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
320
|
+
if (renderValue) {
|
|
321
|
+
return renderValue(resolvedValue as DefaultItemT | DefaultItemT[] | null);
|
|
322
|
+
}
|
|
341
323
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
324
|
+
if (multiple) {
|
|
325
|
+
const values = resolvedValue as DefaultItemT[];
|
|
326
|
+
// For multi-select, show chips
|
|
327
|
+
return values.map(item => getDisplayLabel(item, labelKey)).join(', ');
|
|
328
|
+
} else {
|
|
329
|
+
const singleValue = resolvedValue as DefaultItemT;
|
|
330
|
+
return getDisplayLabel(singleValue, labelKey);
|
|
331
|
+
}
|
|
332
|
+
}, [resolvedValue, multiple, labelKey, placeholder, renderValue]);
|
|
333
|
+
|
|
334
|
+
if (multiple && Array.isArray(resolvedValue) && resolvedValue.length > 0) {
|
|
335
|
+
// Render chips for multi-select
|
|
336
|
+
return (
|
|
337
|
+
<View style={[styles.chipContainer, style]} {...rest}>
|
|
338
|
+
{(resolvedValue as DefaultItemT[]).map(item => (
|
|
339
|
+
<SelectValueItem
|
|
340
|
+
key={item.id || String(item)}
|
|
341
|
+
item={item}
|
|
342
|
+
onRemoveItem={onRemove}
|
|
343
|
+
/>
|
|
344
|
+
))}
|
|
345
|
+
</View>
|
|
346
|
+
);
|
|
349
347
|
}
|
|
350
|
-
}, [resolvedValue, multiple, labelKey, placeholder, renderValue]);
|
|
351
348
|
|
|
352
|
-
if (multiple && Array.isArray(resolvedValue) && resolvedValue.length > 0) {
|
|
353
|
-
// Render chips for multi-select
|
|
354
349
|
return (
|
|
355
|
-
<
|
|
356
|
-
{
|
|
357
|
-
|
|
358
|
-
key={item.id || String(item)}
|
|
359
|
-
item={item}
|
|
360
|
-
onRemoveItem={onRemove}
|
|
361
|
-
/>
|
|
362
|
-
))}
|
|
363
|
-
</View>
|
|
350
|
+
<Text style={[styles.valueText, style]} selectable={false} {...rest}>
|
|
351
|
+
{displayValue}
|
|
352
|
+
</Text>
|
|
364
353
|
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return (
|
|
368
|
-
<Text style={style} {...rest}>
|
|
369
|
-
{displayValue}
|
|
370
|
-
</Text>
|
|
371
|
-
);
|
|
372
|
-
});
|
|
354
|
+
},
|
|
355
|
+
);
|
|
373
356
|
|
|
374
357
|
const SelectValueItem = typedMemo(
|
|
375
358
|
({
|
|
@@ -385,7 +368,7 @@ const SelectValueItem = typedMemo(
|
|
|
385
368
|
|
|
386
369
|
return (
|
|
387
370
|
<Chip.Input
|
|
388
|
-
label={
|
|
371
|
+
label={getDisplayLabel(item)}
|
|
389
372
|
size="sm"
|
|
390
373
|
selected
|
|
391
374
|
left={<></>}
|
|
@@ -398,7 +381,7 @@ const SelectValueItem = typedMemo(
|
|
|
398
381
|
SelectValue.displayName = 'Select_Value';
|
|
399
382
|
|
|
400
383
|
// Select.Dropdown - popover with keyboard navigation
|
|
401
|
-
const SelectDropdown = memo(
|
|
384
|
+
export const SelectDropdown = memo(
|
|
402
385
|
({
|
|
403
386
|
children,
|
|
404
387
|
WrapperComponent,
|
|
@@ -456,28 +439,21 @@ const SelectDropdown = memo(
|
|
|
456
439
|
},
|
|
457
440
|
);
|
|
458
441
|
|
|
459
|
-
// Keyboard navigation wrapper for web
|
|
442
|
+
// Keyboard navigation wrapper for web. Captures its own DOM ref via a `display: contents`
|
|
443
|
+
// wrapper so the keyboard navigator can query options without needing the dropdown content
|
|
444
|
+
// itself to plumb a contentRef.
|
|
460
445
|
const KeyboardNavigationWrapper = memo(({ children }: { children: React.ReactNode }) => {
|
|
461
|
-
const { onClose,
|
|
446
|
+
const { onClose, isOpen } = useSelectDropdownContextValue(state => ({
|
|
462
447
|
onClose: state.onClose,
|
|
463
|
-
contentRef: state.contentRef,
|
|
464
448
|
isOpen: state.isOpen,
|
|
465
449
|
}));
|
|
450
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
466
451
|
|
|
467
452
|
const handleKeyDown = useCallback(
|
|
468
453
|
(e: globalThis.KeyboardEvent) => {
|
|
469
|
-
if (!
|
|
470
|
-
|
|
471
|
-
// Find all focusable options
|
|
472
|
-
// We assume options have role="option" and are descendants of the contentRef
|
|
473
|
-
// On React Native Web, refs often point to the host node (div)
|
|
474
|
-
const container = contentRef.current as HTMLElement;
|
|
475
|
-
if (!container || !container.querySelectorAll) return;
|
|
476
|
-
|
|
477
|
-
const options = Array.from(
|
|
478
|
-
container.querySelectorAll('[role="option"]:not([disabled])'),
|
|
479
|
-
) as HTMLElement[];
|
|
454
|
+
if (!containerRef.current) return;
|
|
480
455
|
|
|
456
|
+
const options = collectWebSelectKeyboardOptionElements(containerRef.current);
|
|
481
457
|
if (options.length === 0) return;
|
|
482
458
|
|
|
483
459
|
const currentIndex = options.findIndex(el => el === document.activeElement);
|
|
@@ -516,161 +492,58 @@ const KeyboardNavigationWrapper = memo(({ children }: { children: React.ReactNod
|
|
|
516
492
|
case 'Escape':
|
|
517
493
|
e.preventDefault();
|
|
518
494
|
onClose();
|
|
519
|
-
// Return focus to trigger? This should be handled by the caller/Popover usually.
|
|
520
495
|
break;
|
|
521
496
|
}
|
|
522
497
|
},
|
|
523
|
-
[
|
|
498
|
+
[onClose],
|
|
524
499
|
);
|
|
525
500
|
|
|
526
501
|
useEffect(() => {
|
|
527
|
-
if (Platform.OS
|
|
528
|
-
const controller = new AbortController();
|
|
529
|
-
// We attach listener to the window or the container?
|
|
530
|
-
// If we attach to container, it needs focus to receive keys.
|
|
531
|
-
// Popovers usually trap focus.
|
|
532
|
-
// Let's attach to window to be safe, but only when open (which this component implies).
|
|
533
|
-
// Actually, best practice is to attach to the container if it captures focus.
|
|
534
|
-
// But SelectDropdown usually renders in a Portal.
|
|
535
|
-
// Let's attach to window but check if the event target is inside our content.
|
|
536
|
-
// Or rely on the fact that if an option is focused, the keydown bubbles up.
|
|
537
|
-
// If nothing is focused, where do keys go? Body.
|
|
538
|
-
const listener = (e: KeyboardEvent) => {
|
|
539
|
-
// Only handle navigation keys when dropdown is open
|
|
540
|
-
if (!isOpen) return;
|
|
541
|
-
|
|
542
|
-
// For arrow keys, Enter, and Escape, allow navigation regardless of focus location
|
|
543
|
-
// This ensures keyboard navigation works even when focus is still on the trigger
|
|
544
|
-
const isNavigationKey = ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key);
|
|
545
|
-
|
|
546
|
-
if (isNavigationKey) {
|
|
547
|
-
handleKeyDown(e);
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// For other keys, only handle if focus is within the dropdown
|
|
552
|
-
const contentEl = contentRef?.current as HTMLElement | null;
|
|
553
|
-
const dropdownContainer = contentEl?.parentElement ?? contentEl;
|
|
554
|
-
const targetNode = e.target as Node;
|
|
555
|
-
|
|
556
|
-
const isWithinDropdown =
|
|
557
|
-
!!dropdownContainer &&
|
|
558
|
-
(dropdownContainer === targetNode || dropdownContainer.contains(targetNode));
|
|
559
|
-
|
|
560
|
-
if (isWithinDropdown || e.target === document.body) {
|
|
561
|
-
handleKeyDown(e);
|
|
562
|
-
}
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
window.addEventListener('keydown', listener, {
|
|
566
|
-
capture: true,
|
|
567
|
-
signal: controller.signal,
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
return () => {
|
|
571
|
-
controller.abort();
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
return undefined;
|
|
575
|
-
}, [handleKeyDown, contentRef, isOpen]);
|
|
576
|
-
|
|
577
|
-
return <>{children}</>;
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
SelectDropdown.displayName = 'Select_Dropdown';
|
|
502
|
+
if (Platform.OS !== 'web') return undefined;
|
|
581
503
|
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
children,
|
|
586
|
-
ContainerComponent = ScrollView,
|
|
587
|
-
style,
|
|
588
|
-
emptyState,
|
|
589
|
-
...rest
|
|
590
|
-
}: SelectContentProps) => {
|
|
591
|
-
const { contentRef } = useSelectDropdownContextValue(state => ({
|
|
592
|
-
contentRef: state.contentRef,
|
|
593
|
-
}));
|
|
594
|
-
|
|
595
|
-
const { filteredOptions, value, multiple, searchQuery, options } = useSelectContextValue(
|
|
596
|
-
state => ({
|
|
597
|
-
filteredOptions: state.filteredOptions,
|
|
598
|
-
value: state.value,
|
|
599
|
-
multiple: state.multiple,
|
|
600
|
-
searchQuery: state.searchQuery,
|
|
601
|
-
options: state.options,
|
|
602
|
-
}),
|
|
603
|
-
);
|
|
504
|
+
const controller = new AbortController();
|
|
505
|
+
const listener = (e: KeyboardEvent) => {
|
|
506
|
+
if (!isOpen) return;
|
|
604
507
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
return children(option, !!isSelected);
|
|
612
|
-
});
|
|
613
|
-
}, [filteredOptions, value, multiple, children]);
|
|
614
|
-
|
|
615
|
-
const defaultEmptyState = useMemo(() => {
|
|
616
|
-
const hasSearchQuery = searchQuery && searchQuery.trim().length > 0;
|
|
617
|
-
const hasNoOptions = options.length === 0;
|
|
618
|
-
|
|
619
|
-
if (hasNoOptions) {
|
|
620
|
-
return (
|
|
621
|
-
<View style={styles.emptyState}>
|
|
622
|
-
<Text style={styles.emptyStateText}>No options available</Text>
|
|
623
|
-
</View>
|
|
624
|
-
);
|
|
508
|
+
// Navigation keys are handled regardless of focus location so keyboard nav works
|
|
509
|
+
// even while focus is still on the trigger.
|
|
510
|
+
const isNavigationKey = ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key);
|
|
511
|
+
if (isNavigationKey) {
|
|
512
|
+
handleKeyDown(e);
|
|
513
|
+
return;
|
|
625
514
|
}
|
|
626
515
|
|
|
627
|
-
if
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
516
|
+
// Other keys: only handle if focus is inside the dropdown.
|
|
517
|
+
const container = containerRef.current;
|
|
518
|
+
const targetNode = e.target as Node;
|
|
519
|
+
const isWithinDropdown =
|
|
520
|
+
!!container && (container === targetNode || container.contains(targetNode));
|
|
521
|
+
if (isWithinDropdown || e.target === document.body) {
|
|
522
|
+
handleKeyDown(e);
|
|
633
523
|
}
|
|
524
|
+
};
|
|
634
525
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
return (
|
|
643
|
-
<ContainerComponent
|
|
644
|
-
ref={contentRef}
|
|
645
|
-
style={style}
|
|
646
|
-
{...rest}
|
|
647
|
-
accessibilityRole="listbox">
|
|
648
|
-
{filteredOptions.length === 0 ? emptyState ?? defaultEmptyState : content}
|
|
649
|
-
</ContainerComponent>
|
|
650
|
-
);
|
|
651
|
-
},
|
|
652
|
-
);
|
|
653
|
-
|
|
654
|
-
SelectContent.displayName = 'Select_Content';
|
|
526
|
+
window.addEventListener('keydown', listener, {
|
|
527
|
+
capture: true,
|
|
528
|
+
signal: controller.signal,
|
|
529
|
+
});
|
|
530
|
+
return () => controller.abort();
|
|
531
|
+
}, [handleKeyDown, isOpen]);
|
|
655
532
|
|
|
656
|
-
// Select.Group - groups items with label
|
|
657
|
-
const SelectGroup = memo(({ children, label, style, ...rest }: SelectGroupProps) => {
|
|
658
533
|
return (
|
|
659
|
-
<
|
|
660
|
-
{label && <Text style={styles.groupLabel}>{label}</Text>}
|
|
534
|
+
<div ref={containerRef} style={{ display: 'contents' }}>
|
|
661
535
|
{children}
|
|
662
|
-
</
|
|
536
|
+
</div>
|
|
663
537
|
);
|
|
664
538
|
});
|
|
665
539
|
|
|
666
|
-
|
|
540
|
+
SelectDropdown.displayName = 'Select_Dropdown';
|
|
667
541
|
|
|
668
542
|
// Select.Item - select item that uses context
|
|
669
|
-
const SelectOption = memo(
|
|
543
|
+
export const SelectOption = memo(
|
|
670
544
|
<Option extends DefaultItemT = DefaultItemT>({
|
|
671
545
|
value,
|
|
672
546
|
children,
|
|
673
|
-
renderItem,
|
|
674
547
|
onPress,
|
|
675
548
|
style,
|
|
676
549
|
disabled: optionDisabledProp = false,
|
|
@@ -681,30 +554,29 @@ const SelectOption = memo(
|
|
|
681
554
|
onAdd,
|
|
682
555
|
onRemove,
|
|
683
556
|
disabled: selectDisabled,
|
|
557
|
+
isSelectedId,
|
|
684
558
|
} = useSelectContextValue(state => ({
|
|
685
559
|
multiple: state.multiple,
|
|
686
560
|
onAdd: state.onAdd,
|
|
687
561
|
onRemove: state.onRemove,
|
|
688
562
|
disabled: state.disabled,
|
|
563
|
+
isSelectedId: state.isSelectedId,
|
|
564
|
+
}));
|
|
565
|
+
const { allOptions, getOptionId } = useSelectSearchContextValue(state => ({
|
|
566
|
+
allOptions: state.allOptions,
|
|
567
|
+
getOptionId: state.getOptionId,
|
|
689
568
|
}));
|
|
690
569
|
|
|
691
570
|
const option = useMemo(() => {
|
|
571
|
+
const found = allOptions.find(i => getOptionId(i as Option) === value);
|
|
572
|
+
if (found) return found as Option;
|
|
692
573
|
return {
|
|
693
574
|
id: value,
|
|
694
|
-
...(typeof children === 'string' ? { label: children } : {}),
|
|
695
575
|
...(optionDisabledProp ? { selectable: false } : {}),
|
|
696
576
|
} as Option;
|
|
697
|
-
}, [
|
|
577
|
+
}, [allOptions, getOptionId, optionDisabledProp, value]);
|
|
698
578
|
|
|
699
|
-
const isSelected =
|
|
700
|
-
if (multiple) {
|
|
701
|
-
const values = state.value as any[];
|
|
702
|
-
return values?.some(v => (v?.id ?? v) === option.id) || false;
|
|
703
|
-
} else {
|
|
704
|
-
const singleValue = state.value as any;
|
|
705
|
-
return (singleValue?.id ?? singleValue) === option.id || false;
|
|
706
|
-
}
|
|
707
|
-
});
|
|
579
|
+
const isSelected = isSelectedId(value);
|
|
708
580
|
|
|
709
581
|
const { onClose } = useSelectDropdownContextValue(state => ({
|
|
710
582
|
onClose: state.onClose,
|
|
@@ -717,10 +589,7 @@ const SelectOption = memo(
|
|
|
717
589
|
const handlePress = useCallback(
|
|
718
590
|
(event: GestureResponderEvent) => {
|
|
719
591
|
if (isOptionDisabled) return;
|
|
720
|
-
|
|
721
|
-
if (onPress) {
|
|
722
|
-
onPress(option, event);
|
|
723
|
-
}
|
|
592
|
+
onPress?.(option, event);
|
|
724
593
|
|
|
725
594
|
if (isSelected) {
|
|
726
595
|
onRemove(option);
|
|
@@ -736,164 +605,101 @@ const SelectOption = memo(
|
|
|
736
605
|
[isOptionDisabled, option, isSelected, onPress, onAdd, onRemove, multiple, onClose],
|
|
737
606
|
);
|
|
738
607
|
|
|
739
|
-
const content = useMemo(() => {
|
|
740
|
-
if (typeof children === 'string') {
|
|
741
|
-
return <Text style={isOptionDisabled && styles.itemDisabledText}>{children}</Text>;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if (children) return children;
|
|
745
|
-
|
|
746
|
-
return (
|
|
747
|
-
<Text style={isOptionDisabled && styles.itemDisabledText}>
|
|
748
|
-
{option.label || String(option.id)}
|
|
749
|
-
</Text>
|
|
750
|
-
);
|
|
751
|
-
}, [children, option.id, option.label, isOptionDisabled]);
|
|
752
|
-
|
|
753
|
-
const accessibilityProps = {
|
|
754
|
-
accessibilityRole: 'button' as AccessibilityRole, // Fallback for native
|
|
755
|
-
accessibilityState: { selected: isSelected, disabled: isOptionDisabled },
|
|
756
|
-
...Platform.select({
|
|
757
|
-
web: {
|
|
758
|
-
accessibilityRole: 'option' as AccessibilityRole,
|
|
759
|
-
tabIndex: -1 as 0 | -1 | undefined,
|
|
760
|
-
// Use a dataset attribute to help the keyboard navigator find this
|
|
761
|
-
'data-option-id': String(option.id),
|
|
762
|
-
// Prevent Pressable's native Enter key handling since we handle it in KeyboardNavigationWrapper
|
|
763
|
-
// This prevents double-triggering of onPress when Enter is pressed
|
|
764
|
-
onKeyDown: (e: React.KeyboardEvent) => {
|
|
765
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
766
|
-
e.preventDefault();
|
|
767
|
-
e.stopPropagation();
|
|
768
|
-
}
|
|
769
|
-
},
|
|
770
|
-
},
|
|
771
|
-
}),
|
|
772
|
-
};
|
|
773
|
-
|
|
774
|
-
if (renderItem) {
|
|
775
|
-
return (
|
|
776
|
-
<Pressable
|
|
777
|
-
onPress={handlePress}
|
|
778
|
-
disabled={isOptionDisabled}
|
|
779
|
-
style={[isOptionDisabled && styles.itemDisabled, style]}
|
|
780
|
-
{...accessibilityProps}
|
|
781
|
-
{...rest}>
|
|
782
|
-
{renderItem(option, isSelected)}
|
|
783
|
-
</Pressable>
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
608
|
return (
|
|
788
|
-
<
|
|
609
|
+
<List.Item
|
|
610
|
+
{...rest}
|
|
611
|
+
style={style}
|
|
612
|
+
value={value}
|
|
613
|
+
shouldToggleOnPress={false}
|
|
789
614
|
onPress={handlePress}
|
|
790
615
|
disabled={isOptionDisabled}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
616
|
+
accessibilityState={{ selected: isSelected, disabled: isOptionDisabled }}
|
|
617
|
+
{...(Platform.OS === 'web'
|
|
618
|
+
? {
|
|
619
|
+
// Force role="option" on web — the keyboard navigator finds rows by
|
|
620
|
+
// [role="option"], so callers must not override these.
|
|
621
|
+
accessibilityRole: 'option' as AccessibilityRole,
|
|
622
|
+
role: 'option',
|
|
623
|
+
tabIndex: -1 as 0 | -1 | undefined,
|
|
624
|
+
'data-molecules-select-option': '',
|
|
625
|
+
'data-option-id': String(option.id),
|
|
626
|
+
onKeyDown: (e: React.KeyboardEvent) => {
|
|
627
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
628
|
+
e.preventDefault();
|
|
629
|
+
e.stopPropagation();
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
}
|
|
633
|
+
: { accessibilityRole: 'button' as AccessibilityRole })}>
|
|
634
|
+
{children}
|
|
635
|
+
</List.Item>
|
|
801
636
|
);
|
|
802
637
|
},
|
|
803
638
|
);
|
|
804
639
|
|
|
805
640
|
SelectOption.displayName = 'Select_Option';
|
|
806
641
|
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
setSearchQuery: state.setSearchQuery,
|
|
813
|
-
}));
|
|
814
|
-
const textInputRef = useRef<TextInputHandles>(null);
|
|
815
|
-
|
|
816
|
-
const handleChangeText = useCallback(
|
|
817
|
-
(text: string) => {
|
|
818
|
-
setSearchQuery(text);
|
|
819
|
-
},
|
|
820
|
-
[setSearchQuery],
|
|
821
|
-
);
|
|
822
|
-
|
|
823
|
-
const inputProps = {
|
|
824
|
-
...textInputProps,
|
|
825
|
-
value: searchQuery,
|
|
826
|
-
onChangeText: handleChangeText,
|
|
827
|
-
placeholder: textInputProps.placeholder || 'Search...',
|
|
828
|
-
inputStyle: styles.searchInputInput,
|
|
829
|
-
} as TextInputProps;
|
|
830
|
-
|
|
831
|
-
useEffect(() => {
|
|
832
|
-
if (Platform.OS !== 'web') return;
|
|
833
|
-
if (!autoFocus || !textInputRef.current) {
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
642
|
+
export const SelectSearchInput = memo(({ children, ...textInputProps }: SelectSearchInputProps) => {
|
|
643
|
+
const { searchQuery, setSearchQuery } = useSelectSearchContextValue(state => ({
|
|
644
|
+
searchQuery: state.searchQuery,
|
|
645
|
+
setSearchQuery: state.setSearchQuery,
|
|
646
|
+
}));
|
|
836
647
|
|
|
837
|
-
|
|
838
|
-
focus?: (options?: { preventScroll?: boolean }) => void;
|
|
839
|
-
};
|
|
648
|
+
const textInputRef = useRef<TextInputHandles>(null);
|
|
840
649
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
window.scrollTo(scrollX, scrollY);
|
|
848
|
-
}
|
|
849
|
-
};
|
|
650
|
+
const handleChangeText = useCallback(
|
|
651
|
+
(text: string) => {
|
|
652
|
+
setSearchQuery(text);
|
|
653
|
+
},
|
|
654
|
+
[setSearchQuery],
|
|
655
|
+
);
|
|
850
656
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
657
|
+
const inputProps = {
|
|
658
|
+
...textInputProps,
|
|
659
|
+
value: searchQuery,
|
|
660
|
+
onChangeText: handleChangeText,
|
|
661
|
+
placeholder: textInputProps.placeholder || 'Search...',
|
|
662
|
+
inputStyle: styles.searchInputInput,
|
|
663
|
+
} as TextInputProps;
|
|
854
664
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
665
|
+
const onPressLeftIcon = useCallback(() => {
|
|
666
|
+
textInputRef.current?.focus();
|
|
667
|
+
}, []);
|
|
858
668
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
669
|
+
const onClearSearchQuery = useCallback(() => {
|
|
670
|
+
handleChangeText('');
|
|
671
|
+
}, [handleChangeText]);
|
|
862
672
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
<
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
<
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
},
|
|
883
|
-
);
|
|
673
|
+
return (
|
|
674
|
+
<TextInput
|
|
675
|
+
ref={textInputRef}
|
|
676
|
+
style={styles.searchInput}
|
|
677
|
+
size="sm"
|
|
678
|
+
variant="outlined"
|
|
679
|
+
{...inputProps}>
|
|
680
|
+
<TextInput.Left>
|
|
681
|
+
<Icon onPress={onPressLeftIcon} name="magnify" size={20} />
|
|
682
|
+
</TextInput.Left>
|
|
683
|
+
{searchQuery ? (
|
|
684
|
+
<TextInput.Right>
|
|
685
|
+
<IconButton name="close" size={20} onPress={onClearSearchQuery} />
|
|
686
|
+
</TextInput.Right>
|
|
687
|
+
) : null}
|
|
688
|
+
{children}
|
|
689
|
+
</TextInput>
|
|
690
|
+
);
|
|
691
|
+
});
|
|
884
692
|
|
|
885
693
|
SelectSearchInput.displayName = 'Select_SearchInput';
|
|
886
694
|
|
|
887
|
-
|
|
888
|
-
const SelectWithSubcomponents = Object.assign(Select, {
|
|
695
|
+
const SelectWithSubcomponents = Object.assign(SelectRoot, {
|
|
889
696
|
Trigger: SelectTrigger,
|
|
890
697
|
Value: SelectValue,
|
|
891
698
|
Dropdown: SelectDropdown,
|
|
892
699
|
Content: SelectContent,
|
|
893
|
-
Group: SelectGroup,
|
|
894
700
|
Option: SelectOption,
|
|
895
701
|
SearchInput: SelectSearchInput,
|
|
896
702
|
});
|
|
897
703
|
|
|
898
704
|
export default SelectWithSubcomponents;
|
|
899
|
-
export { SelectDropdownProvider
|
|
705
|
+
export { SelectDropdownProvider };
|