react-native-molecules 0.5.0-beta.22 → 0.5.0-beta.24

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.
Files changed (82) hide show
  1. package/components/Accordion/Accordion.tsx +1 -1
  2. package/components/Accordion/AccordionItem.tsx +1 -1
  3. package/components/Button/Button.tsx +3 -1
  4. package/components/Checkbox/Checkbox.tsx +2 -1
  5. package/components/DateField/useDateFieldState.ts +2 -2
  6. package/components/DatePicker/DatePickerProvider.tsx +1 -1
  7. package/components/DatePicker/utils.ts +2 -0
  8. package/components/DatePickerInline/DatePickerInline.tsx +1 -1
  9. package/components/DatePickerInline/DatePickerInlineBase.tsx +1 -1
  10. package/components/DatePickerInline/Day.tsx +1 -1
  11. package/components/DatePickerInline/MonthPicker.tsx +24 -40
  12. package/components/DatePickerInline/Swiper.tsx +1 -1
  13. package/components/DatePickerInline/SwiperUtils.ts +1 -1
  14. package/components/DatePickerInline/YearPicker.tsx +44 -79
  15. package/components/DatePickerInline/dateUtils.tsx +1 -1
  16. package/components/DatePickerInline/store.tsx +2 -1
  17. package/components/Divider/index.tsx +2 -3
  18. package/components/ElementGroup/ElementGroup.tsx +1 -1
  19. package/components/FilePicker/FilePicker.tsx +1 -1
  20. package/components/Icon/iconFactory.tsx +2 -1
  21. package/components/IconButton/IconButton.tsx +39 -13
  22. package/components/IconButton/index.tsx +1 -0
  23. package/components/IconButton/types.ts +2 -0
  24. package/components/List/List.tsx +156 -387
  25. package/components/List/context.tsx +4 -5
  26. package/components/List/index.ts +0 -1
  27. package/components/List/types.ts +77 -109
  28. package/components/List/utils.ts +4 -37
  29. package/components/Menu/Menu.tsx +13 -30
  30. package/components/Menu/index.tsx +0 -2
  31. package/components/Popover/Popover.tsx +7 -10
  32. package/components/Popover/PopoverRoot.tsx +6 -20
  33. package/components/Popover/common.ts +4 -0
  34. package/components/Popover/index.ts +2 -8
  35. package/components/Popover/usePlatformMeasure.ts +4 -2
  36. package/components/Portal/Portal.tsx +1 -2
  37. package/components/RadioButton/RadioButtonGroup.tsx +1 -2
  38. package/components/Rating/Rating.tsx +1 -1
  39. package/components/Select/Select.tsx +304 -71
  40. package/components/Select/context.tsx +30 -3
  41. package/components/Select/index.ts +20 -2
  42. package/components/Select/types.ts +43 -25
  43. package/components/Select/utils.ts +18 -4
  44. package/components/Switch/Switch.ios.tsx +1 -1
  45. package/components/Switch/Switch.tsx +2 -1
  46. package/components/Tabs/Tabs.tsx +2 -2
  47. package/components/TextInput/TextInput.tsx +4 -3
  48. package/components/TimePicker/AnalogClock.tsx +1 -1
  49. package/components/TimePicker/TimeInputs.tsx +1 -1
  50. package/components/TimePicker/TimePicker.tsx +1 -1
  51. package/components/TimePicker/TimePickerModal.tsx +1 -1
  52. package/components/Tooltip/Tooltip.tsx +1 -1
  53. package/components/TouchableRipple/TouchableRipple.tsx +76 -152
  54. package/hocs/index.tsx +1 -1
  55. package/hocs/withKeyboardAccessibility.tsx +2 -3
  56. package/hooks/index.tsx +2 -6
  57. package/hooks/useContrastColor.ts +1 -2
  58. package/hooks/useFilePicker.tsx +1 -1
  59. package/hooks/useHandleNumberFormat.tsx +2 -2
  60. package/hooks/useMediaQuery.tsx +1 -2
  61. package/package.json +5 -28
  62. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +1 -1
  63. package/shortcuts-manager/ShortcutsManager/utils.tsx +1 -1
  64. package/shortcuts-manager/useSetScopes/useSetScopes.tsx +1 -1
  65. package/shortcuts-manager/useShortcut/useShortcut.tsx +1 -1
  66. package/utils/extractTextStyles.ts +1 -2
  67. package/utils/formatNumberWithMask/formatNumberWithMask.ts +2 -1
  68. package/utils/index.ts +0 -3
  69. package/utils/normalizeToNumberString/normalizeToNumberString.ts +1 -1
  70. package/context-bridge/index.tsx +0 -87
  71. package/fast-context/index.tsx +0 -190
  72. package/hocs/typedMemo.tsx +0 -5
  73. package/hooks/useControlledValue.tsx +0 -84
  74. package/hooks/useLatest.tsx +0 -9
  75. package/hooks/useMergedRefs.ts +0 -14
  76. package/hooks/usePrevious.ts +0 -13
  77. package/hooks/useToggle.tsx +0 -24
  78. package/hooks/useWhatHasUpdated.tsx +0 -48
  79. package/utils/color.ts +0 -22
  80. package/utils/compare/index.ts +0 -54
  81. package/utils/lodash.ts +0 -121
  82. package/utils/repository.ts +0 -53
@@ -1,4 +1,14 @@
1
- import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ import { useControlledValue, useToggle } from '@react-native-molecules/utils/hooks';
2
+ import {
3
+ Fragment,
4
+ memo,
5
+ type ReactNode,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
2
12
  import {
3
13
  type AccessibilityRole,
4
14
  type GestureResponderEvent,
@@ -9,38 +19,112 @@ import {
9
19
  } from 'react-native';
10
20
 
11
21
  import { typedMemo } from '../../hocs';
12
- import { useActionState } from '../../hooks';
13
- import { useToggle } from '../../hooks';
14
22
  import { resolveStateVariant } from '../../utils';
23
+ import { extractSubcomponents } from '../../utils/extractSubcomponents';
15
24
  import { Chip } from '../Chip';
16
25
  import { Icon } from '../Icon';
26
+ import { IconButton } from '../IconButton';
17
27
  import { List } from '../List';
18
28
  import { Popover } from '../Popover';
19
29
  import { Text } from '../Text';
30
+ import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
20
31
  import {
21
32
  SelectDropdownContextProvider,
33
+ SelectSearchContextProvider,
22
34
  useSelectContextValue,
23
35
  useSelectDropdownContextValue,
36
+ useSelectDropdownStoreRef,
37
+ useSelectSearchContextValue,
24
38
  } from './context';
25
39
  import type {
26
40
  DefaultItemT,
41
+ SelectContentProps,
27
42
  SelectDropdownProps,
28
43
  SelectOptionProps,
29
44
  SelectProps,
45
+ SelectSearchContextValue,
46
+ SelectSearchInputProps,
47
+ SelectSearchKey,
48
+ SelectTriggerOutlineProps,
30
49
  SelectTriggerProps,
31
50
  SelectValueProps,
32
51
  } from './types';
33
- import { collectWebSelectKeyboardOptionElements, styles, triggerStyles } from './utils';
52
+ import {
53
+ collectWebSelectKeyboardOptionElements,
54
+ selectOutlineStyles,
55
+ styles,
56
+ triggerStyles,
57
+ } from './utils';
34
58
 
35
59
  const emptyArr: unknown[] = [];
36
60
 
37
- const SelectDropdownProvider = memo(
61
+ export const getSelectTriggerState = ({
62
+ isOpen,
63
+ hovered,
64
+ disabled,
65
+ error,
66
+ }: {
67
+ isOpen: boolean;
68
+ hovered: boolean;
69
+ disabled: boolean;
70
+ error: boolean;
71
+ }) =>
72
+ resolveStateVariant({
73
+ focused: isOpen,
74
+ hovered,
75
+ disabled,
76
+ error,
77
+ hoveredAndFocused: hovered && isOpen,
78
+ errorFocused: error && isOpen,
79
+ errorHovered: error && hovered,
80
+ errorFocusedAndHovered: error && isOpen && hovered,
81
+ errorDisabled: error && disabled,
82
+ }) as any;
83
+
84
+ export const getDisplayLabel = (item: DefaultItemT, labelKey?: string) => {
85
+ const itemLabelKey = typeof item.labelKey === 'string' ? item.labelKey : undefined;
86
+ const key = labelKey ?? itemLabelKey ?? 'label';
87
+ const value = item[key];
88
+ return value == null ? String(item.id) : String(value);
89
+ };
90
+
91
+ export const getNested = (item: unknown, path: string): unknown => {
92
+ if (item == null || typeof item !== 'object') return undefined;
93
+ if (!path.includes('.')) return (item as Record<string, unknown>)[path];
94
+ let val: unknown = item;
95
+ for (const part of path.split('.')) {
96
+ if (val == null || typeof val !== 'object') return undefined;
97
+ val = (val as Record<string, unknown>)[part];
98
+ }
99
+ return val;
100
+ };
101
+
102
+ export const matchesByKey = (item: unknown, key: string, lowerQuery: string): boolean =>
103
+ String(getNested(item, key) ?? '')
104
+ .toLowerCase()
105
+ .includes(lowerQuery);
106
+
107
+ export const applySearch = <T extends object>(
108
+ items: T[],
109
+ searchKey: SelectSearchKey<T> | undefined,
110
+ query: string,
111
+ ): T[] => {
112
+ if (!query) return items;
113
+ if (typeof searchKey === 'function') {
114
+ return items.filter(item => searchKey(item, query));
115
+ }
116
+ const keys = Array.isArray(searchKey) ? searchKey : [searchKey || 'label'];
117
+ const lowerQuery = query.toLowerCase();
118
+ return items.filter(item => keys.some(key => matchesByKey(item, key, lowerQuery)));
119
+ };
120
+
121
+ export const SelectDropdownProvider = memo(
38
122
  ({
39
123
  children,
40
124
  isOpen: isOpenProp,
41
125
  onClose: onCloseProp,
42
126
  }: {
43
- children: React.ReactNode;
127
+ children: ReactNode;
44
128
  isOpen?: boolean;
45
129
  onClose?: () => void;
46
130
  }) => {
@@ -86,49 +170,124 @@ const SelectDropdownProvider = memo(
86
170
  },
87
171
  );
88
172
 
89
- const Select = typedMemo(
173
+ export const SelectRoot = typedMemo(
90
174
  <Option extends DefaultItemT = DefaultItemT>({
91
175
  children,
92
176
  options = emptyArr as Option[],
177
+ searchKey,
178
+ searchQuery: searchQueryProp,
179
+ defaultSearchQuery,
180
+ onSearchChange,
181
+ searchMode = 'client',
182
+ getItemId,
93
183
  ...listProps
94
184
  }: SelectProps<Option>) => {
185
+ const [searchQuery, setSearchQuery] = useControlledValue<string>({
186
+ value: searchQueryProp,
187
+ defaultValue: defaultSearchQuery ?? '',
188
+ onChange: onSearchChange,
189
+ });
190
+
191
+ const getOptionId = useMemo(
192
+ () => (getItemId ?? ((item: Option) => item.id)) as (item: Option) => string | number,
193
+ [getItemId],
194
+ );
195
+
196
+ const filteredOptions = useMemo(() => {
197
+ if (searchMode === 'external') return options;
198
+ return applySearch(options, searchKey, searchQuery);
199
+ }, [options, searchKey, searchMode, searchQuery]);
200
+
201
+ const optionById = useMemo(() => {
202
+ const map = new Map<string | number, Option>();
203
+ for (const option of options) {
204
+ map.set(getOptionId(option), option);
205
+ }
206
+ return map;
207
+ }, [getOptionId, options]);
208
+
209
+ const searchContextValue = useMemo(
210
+ () =>
211
+ ({
212
+ searchQuery,
213
+ setSearchQuery,
214
+ allOptions: options,
215
+ options: filteredOptions,
216
+ optionById,
217
+ getOptionId,
218
+ } as unknown as SelectSearchContextValue<DefaultItemT>),
219
+ [filteredOptions, getOptionId, optionById, options, searchQuery, setSearchQuery],
220
+ );
221
+
222
+ return (
223
+ <SelectSearchContextProvider value={searchContextValue}>
224
+ <List {...listProps}>
225
+ <SelectDropdownProvider>{children}</SelectDropdownProvider>
226
+ </List>
227
+ </SelectSearchContextProvider>
228
+ );
229
+ },
230
+ );
231
+
232
+ export const SelectContent = typedMemo(
233
+ <Option extends DefaultItemT = DefaultItemT>({
234
+ children,
235
+ ...rest
236
+ }: SelectContentProps<Option>) => {
237
+ const { options, getOptionId } = useSelectSearchContextValue(state => ({
238
+ options: state.options as Option[],
239
+ getOptionId: state.getOptionId as (item: Option) => string | number,
240
+ }));
241
+ const isSelectedId = useSelectContextValue(state => state.isSelectedId);
242
+
243
+ if (typeof children !== 'function') {
244
+ return <List.Content {...rest}>{children}</List.Content>;
245
+ }
246
+
95
247
  return (
96
- <List {...listProps} items={options}>
97
- <SelectDropdownProvider>{children}</SelectDropdownProvider>
98
- </List>
248
+ <List.Content {...rest}>
249
+ {options.map(item => (
250
+ <Fragment key={String(getOptionId(item))}>
251
+ {children(item, isSelectedId(getOptionId(item)))}
252
+ </Fragment>
253
+ ))}
254
+ </List.Content>
99
255
  );
100
256
  },
101
257
  );
102
258
 
103
- const SelectTrigger = ({ children, style, ...rest }: SelectTriggerProps) => {
104
- const { onOpen, isOpen, triggerRef, setTriggerLayout } = useSelectDropdownContextValue(
259
+ export const SelectTrigger = memo(({ children, style, ...rest }: SelectTriggerProps) => {
260
+ const { isOpen, onOpen, onClose, triggerRef, setTriggerLayout } = useSelectDropdownContextValue(
105
261
  state => ({
106
- onOpen: state.onOpen,
107
262
  isOpen: state.isOpen,
263
+ onOpen: state.onOpen,
264
+ onClose: state.onClose,
108
265
  triggerRef: state.triggerRef,
109
266
  setTriggerLayout: state.setTriggerLayout,
110
267
  }),
111
268
  );
269
+ const setSelectDropdownContext = useSelectDropdownStoreRef().set;
112
270
 
113
271
  const { disabled, error } = useSelectContextValue(state => ({
114
272
  disabled: state.disabled,
115
273
  error: state.error,
116
274
  }));
117
275
 
118
- const { hovered } = useActionState({ ref: triggerRef, actionsToListen: ['hover'] });
276
+ const [hovered, setHovered] = useState(false);
277
+
278
+ const { Select_TriggerOutline, rest: restChildren } = extractSubcomponents({
279
+ children,
280
+ allowedChildren: [{ name: 'Select_TriggerOutline', allowMultiple: false }] as const,
281
+ includeRest: true,
282
+ });
119
283
 
120
284
  triggerStyles.useVariants({
121
- state: resolveStateVariant({
122
- focused: isOpen,
285
+ state: getSelectTriggerState({
286
+ isOpen,
123
287
  hovered,
124
288
  disabled: !!disabled,
125
289
  error: !!error,
126
- hoveredAndFocused: hovered && isOpen,
127
- errorFocused: !!error && isOpen,
128
- errorHovered: !!error && hovered,
129
- errorFocusedAndHovered: !!error && isOpen && hovered,
130
- errorDisabled: !!error && !!disabled,
131
- }) as any,
290
+ }),
132
291
  });
133
292
 
134
293
  const handleLayout = useCallback(
@@ -140,72 +299,114 @@ const SelectTrigger = ({ children, style, ...rest }: SelectTriggerProps) => {
140
299
  );
141
300
 
142
301
  const handlePress = useCallback(() => {
143
- if (!isOpen && !disabled) {
302
+ if (disabled) return;
303
+ if (!isOpen) {
144
304
  onOpen();
305
+ } else {
306
+ onClose();
145
307
  }
146
- }, [isOpen, onOpen, disabled]);
308
+ }, [isOpen, onOpen, onClose, disabled]);
309
+
310
+ const handleHoverIn = useCallback(() => {
311
+ setHovered(true);
312
+ setSelectDropdownContext(() => ({ triggerHovered: true }));
313
+ }, [setSelectDropdownContext]);
314
+
315
+ const handleHoverOut = useCallback(() => {
316
+ setHovered(false);
317
+ setSelectDropdownContext(() => ({ triggerHovered: false }));
318
+ }, [setSelectDropdownContext]);
319
+
320
+ const outlineElement =
321
+ Select_TriggerOutline.length > 0 ? Select_TriggerOutline : <SelectTriggerOutline />;
147
322
 
148
323
  return (
149
324
  <Pressable
150
325
  ref={triggerRef}
151
326
  onPress={handlePress}
152
327
  onLayout={handleLayout}
328
+ onHoverIn={handleHoverIn}
329
+ onHoverOut={handleHoverOut}
153
330
  style={[triggerStyles.trigger, style]}
154
331
  accessibilityRole="combobox"
155
332
  accessibilityState={{ expanded: isOpen, disabled: !!disabled }}
156
333
  disabled={disabled}
157
334
  {...rest}>
158
- {children}
335
+ {restChildren}
159
336
  <Icon
160
337
  name={isOpen ? 'chevron-up' : 'chevron-down'}
161
338
  size={20}
162
339
  style={triggerStyles.triggerIcon}
163
340
  />
164
- <View style={triggerStyles.outline} />
341
+ {outlineElement}
165
342
  </Pressable>
166
343
  );
167
- };
344
+ });
168
345
 
169
346
  SelectTrigger.displayName = 'Select_Trigger';
170
347
 
171
- const SelectValue = memo(
348
+ export const SelectTriggerOutline = memo(({ style }: SelectTriggerOutlineProps) => {
349
+ const { isOpen, triggerHovered } = useSelectDropdownContextValue(state => ({
350
+ isOpen: state.isOpen,
351
+ triggerHovered: state.triggerHovered,
352
+ }));
353
+ const { disabled, error } = useSelectContextValue(state => ({
354
+ disabled: state.disabled,
355
+ error: state.error,
356
+ }));
357
+ selectOutlineStyles.useVariants({
358
+ state: getSelectTriggerState({
359
+ isOpen,
360
+ hovered: !!triggerHovered,
361
+ disabled: !!disabled,
362
+ error: !!error,
363
+ }),
364
+ });
365
+
366
+ return <View pointerEvents="none" style={[selectOutlineStyles.outline, style]} />;
367
+ });
368
+
369
+ SelectTriggerOutline.displayName = 'Select_TriggerOutline';
370
+
371
+ export const SelectValue = memo(
172
372
  ({ placeholder, labelKey, renderValue, style, ...rest }: SelectValueProps) => {
173
- const { value, multiple, onRemove, options } = useSelectContextValue(state => ({
373
+ const { value, multiple, onRemove } = useSelectContextValue(state => ({
174
374
  value: state.value,
175
375
  multiple: state.multiple,
176
376
  onRemove: state.onRemove,
177
- options: state.items,
377
+ }));
378
+ const { optionById } = useSelectSearchContextValue(state => ({
379
+ optionById: state.optionById,
178
380
  }));
179
381
 
180
382
  const resolvedValue = useMemo(() => {
181
- const resolve = (item: any) => {
182
- if (item === null || item === undefined) return null;
183
- const id = typeof item === 'object' ? item.id : item;
184
- const found = options.find(o => o.id === id);
185
- return found || item;
383
+ const resolve = (id: unknown) => {
384
+ if (id === null || id === undefined) return null;
385
+ const found = optionById.get(id as string | number);
386
+ return found || { id: id as string | number };
186
387
  };
187
388
 
188
389
  if (multiple) {
189
390
  return (Array.isArray(value) ? value : []).map(resolve).filter(Boolean);
190
391
  }
191
392
  return resolve(value);
192
- }, [value, multiple, options]);
393
+ }, [optionById, value, multiple]);
193
394
 
194
395
  const displayValue = useMemo(() => {
195
396
  if (!resolvedValue) return placeholder || '';
196
397
  if (multiple && (resolvedValue as any[]).length === 0) return placeholder || '';
197
398
 
198
399
  if (renderValue) {
199
- return renderValue(resolvedValue as any);
400
+ return renderValue(resolvedValue as DefaultItemT | DefaultItemT[] | null);
200
401
  }
201
402
 
202
403
  if (multiple) {
203
404
  const values = resolvedValue as DefaultItemT[];
204
405
  // For multi-select, show chips
205
- return values.map(item => item[labelKey || 'label'] || String(item.id)).join(', ');
406
+ return values.map(item => getDisplayLabel(item, labelKey)).join(', ');
206
407
  } else {
207
408
  const singleValue = resolvedValue as DefaultItemT;
208
- return singleValue[labelKey || 'label'] || String(singleValue.id || singleValue);
409
+ return getDisplayLabel(singleValue, labelKey);
209
410
  }
210
411
  }, [resolvedValue, multiple, labelKey, placeholder, renderValue]);
211
412
 
@@ -225,7 +426,7 @@ const SelectValue = memo(
225
426
  }
226
427
 
227
428
  return (
228
- <Text style={style} {...rest}>
429
+ <Text style={[styles.valueText, style]} selectable={false} {...rest}>
229
430
  {displayValue}
230
431
  </Text>
231
432
  );
@@ -246,7 +447,7 @@ const SelectValueItem = typedMemo(
246
447
 
247
448
  return (
248
449
  <Chip.Input
249
- label={item[item.labelKey || 'label'] || String(item.id || item)}
450
+ label={getDisplayLabel(item)}
250
451
  size="sm"
251
452
  selected
252
453
  left={<></>}
@@ -259,7 +460,7 @@ const SelectValueItem = typedMemo(
259
460
  SelectValue.displayName = 'Select_Value';
260
461
 
261
462
  // Select.Dropdown - popover with keyboard navigation
262
- const SelectDropdown = memo(
463
+ export const SelectDropdown = memo(
263
464
  ({
264
465
  children,
265
466
  WrapperComponent,
@@ -417,10 +618,8 @@ const KeyboardNavigationWrapper = memo(({ children }: { children: React.ReactNod
417
618
 
418
619
  SelectDropdown.displayName = 'Select_Dropdown';
419
620
 
420
- const SelectGroup = List.Group;
421
-
422
621
  // Select.Item - select item that uses context
423
- const SelectOption = memo(
622
+ export const SelectOption = memo(
424
623
  <Option extends DefaultItemT = DefaultItemT>({
425
624
  value,
426
625
  children,
@@ -434,33 +633,29 @@ const SelectOption = memo(
434
633
  onAdd,
435
634
  onRemove,
436
635
  disabled: selectDisabled,
437
- items,
636
+ isSelectedId,
438
637
  } = useSelectContextValue(state => ({
439
638
  multiple: state.multiple,
440
639
  onAdd: state.onAdd,
441
640
  onRemove: state.onRemove,
442
641
  disabled: state.disabled,
443
- items: state.items,
642
+ isSelectedId: state.isSelectedId,
643
+ }));
644
+ const { allOptions, getOptionId } = useSelectSearchContextValue(state => ({
645
+ allOptions: state.allOptions,
646
+ getOptionId: state.getOptionId,
444
647
  }));
445
648
 
446
649
  const option = useMemo(() => {
447
- const found = items.find(i => i.id === value);
650
+ const found = allOptions.find(i => getOptionId(i as Option) === value);
448
651
  if (found) return found as Option;
449
652
  return {
450
653
  id: value,
451
654
  ...(optionDisabledProp ? { selectable: false } : {}),
452
655
  } as Option;
453
- }, [items, optionDisabledProp, value]);
656
+ }, [allOptions, getOptionId, optionDisabledProp, value]);
454
657
 
455
- const isSelected = useSelectContextValue(state => {
456
- if (multiple) {
457
- const values = state.value as any[];
458
- return values?.some(v => (v?.id ?? v) === option.id) || false;
459
- }
460
-
461
- const singleValue = state.value as any;
462
- return (singleValue?.id ?? singleValue) === option.id || false;
463
- });
658
+ const isSelected = isSelectedId(value);
464
659
 
465
660
  const { onClose } = useSelectDropdownContextValue(state => ({
466
661
  onClose: state.onClose,
@@ -495,7 +690,7 @@ const SelectOption = memo(
495
690
  style={style}
496
691
  value={value}
497
692
  shouldToggleOnPress={false}
498
- onPress={(_, event) => handlePress(event)}
693
+ onPress={handlePress}
499
694
  disabled={isOptionDisabled}
500
695
  accessibilityState={{ selected: isSelected, disabled: isOptionDisabled }}
501
696
  {...(Platform.OS === 'web'
@@ -523,19 +718,57 @@ const SelectOption = memo(
523
718
 
524
719
  SelectOption.displayName = 'Select_Option';
525
720
 
526
- const SelectSearchInput = List.SearchInput;
721
+ export const SelectSearchInput = memo(({ children, ...textInputProps }: SelectSearchInputProps) => {
722
+ const { searchQuery, setSearchQuery } = useSelectSearchContextValue(state => ({
723
+ searchQuery: state.searchQuery,
724
+ setSearchQuery: state.setSearchQuery,
725
+ }));
527
726
 
528
- SelectSearchInput.displayName = 'Select_SearchInput';
727
+ const textInputRef = useRef<TextInputHandles>(null);
728
+
729
+ const handleChangeText = useCallback(
730
+ (text: string) => {
731
+ setSearchQuery(text);
732
+ },
733
+ [setSearchQuery],
734
+ );
735
+
736
+ const inputProps = {
737
+ ...textInputProps,
738
+ value: searchQuery,
739
+ onChangeText: handleChangeText,
740
+ placeholder: textInputProps.placeholder || 'Search...',
741
+ inputStyle: styles.searchInputInput,
742
+ } as TextInputProps;
743
+
744
+ const onPressLeftIcon = useCallback(() => {
745
+ textInputRef.current?.focus();
746
+ }, []);
529
747
 
530
- const SelectWithSubcomponents = Object.assign(Select, {
531
- Trigger: SelectTrigger,
532
- Value: SelectValue,
533
- Dropdown: SelectDropdown,
534
- Content: List.Content,
535
- Group: SelectGroup,
536
- Option: SelectOption,
537
- SearchInput: SelectSearchInput,
748
+ const onClearSearchQuery = useCallback(() => {
749
+ handleChangeText('');
750
+ }, [handleChangeText]);
751
+
752
+ return (
753
+ <TextInput
754
+ ref={textInputRef}
755
+ style={styles.searchInput}
756
+ size="sm"
757
+ variant="outlined"
758
+ {...inputProps}>
759
+ <TextInput.Left>
760
+ <Icon onPress={onPressLeftIcon} name="magnify" size={20} />
761
+ </TextInput.Left>
762
+ {searchQuery ? (
763
+ <TextInput.Right>
764
+ <IconButton name="close" size={20} onPress={onClearSearchQuery} />
765
+ </TextInput.Right>
766
+ ) : null}
767
+ {children}
768
+ </TextInput>
769
+ );
538
770
  });
539
771
 
540
- export default SelectWithSubcomponents;
541
- export { SelectDropdownProvider };
772
+ SelectSearchInput.displayName = 'Select_SearchInput';
773
+
774
+ export default SelectRoot;
@@ -1,6 +1,6 @@
1
+ import { createFastContext } from '@react-native-molecules/utils/fast-context';
1
2
  import type { View } from 'react-native';
2
3
 
3
- import { createFastContext } from '../../fast-context';
4
4
  import {
5
5
  ListContext,
6
6
  ListContextProvider,
@@ -9,7 +9,7 @@ import {
9
9
  useListStoreRef,
10
10
  } from '../List';
11
11
  import { registerPortalContext } from '../Portal';
12
- import type { SelectDropdownContextValue } from './types';
12
+ import type { DefaultItemT, SelectDropdownContextValue, SelectSearchContextValue } from './types';
13
13
 
14
14
  export {
15
15
  ListContext as SelectContext,
@@ -24,6 +24,7 @@ export type SelectDropdownContextType = SelectDropdownContextValue & {
24
24
  triggerRef: React.RefObject<View> | null;
25
25
  triggerLayout: { width: number; height: number } | null;
26
26
  setTriggerLayout: (layout: { width: number; height: number }) => void;
27
+ triggerHovered?: boolean;
27
28
  };
28
29
 
29
30
  const selectDropdownContextDefaultValue: SelectDropdownContextType = {
@@ -33,6 +34,7 @@ const selectDropdownContextDefaultValue: SelectDropdownContextType = {
33
34
  triggerRef: null,
34
35
  triggerLayout: null,
35
36
  setTriggerLayout: () => {},
37
+ triggerHovered: false,
36
38
  };
37
39
 
38
40
  const {
@@ -43,12 +45,37 @@ const {
43
45
  Context: SelectDropdownContext,
44
46
  } = createFastContext<SelectDropdownContextType>(selectDropdownContextDefaultValue, true);
45
47
 
48
+ const selectSearchContextDefaultValue: SelectSearchContextValue<DefaultItemT> = {
49
+ searchQuery: '',
50
+ setSearchQuery: () => {},
51
+ allOptions: [],
52
+ options: [],
53
+ optionById: new Map(),
54
+ getOptionId: item => item.id,
55
+ };
56
+
57
+ const {
58
+ useStoreRef: useSelectSearchStoreRef,
59
+ Provider: SelectSearchContextProvider,
60
+ useContext: useSelectSearchContext,
61
+ useContextValue: useSelectSearchContextValue,
62
+ Context: SelectSearchContext,
63
+ } = createFastContext<SelectSearchContextValue<DefaultItemT>>(
64
+ selectSearchContextDefaultValue,
65
+ true,
66
+ );
67
+
46
68
  export {
47
69
  SelectDropdownContext,
48
70
  SelectDropdownContextProvider,
71
+ SelectSearchContext,
72
+ SelectSearchContextProvider,
49
73
  useSelectDropdownContext,
50
74
  useSelectDropdownContextValue,
51
75
  useSelectDropdownStoreRef,
76
+ useSelectSearchContext,
77
+ useSelectSearchContextValue,
78
+ useSelectSearchStoreRef,
52
79
  };
53
80
 
54
- registerPortalContext([SelectDropdownContext]);
81
+ registerPortalContext([SelectDropdownContext, SelectSearchContext]);
@@ -1,7 +1,25 @@
1
1
  import { getRegisteredComponentWithFallback } from '../../core';
2
- import SelectDefault from './Select';
2
+ import SelectRoot, {
3
+ SelectContent,
4
+ SelectDropdown,
5
+ SelectOption,
6
+ SelectSearchInput,
7
+ SelectTrigger,
8
+ SelectTriggerOutline,
9
+ SelectValue,
10
+ } from './Select';
3
11
 
4
- export const Select = getRegisteredComponentWithFallback('Select', SelectDefault);
12
+ const SelectWithSubcomponents = Object.assign(SelectRoot, {
13
+ Trigger: SelectTrigger,
14
+ TriggerOutline: SelectTriggerOutline,
15
+ Value: SelectValue,
16
+ Dropdown: SelectDropdown,
17
+ Content: SelectContent,
18
+ Option: SelectOption,
19
+ SearchInput: SelectSearchInput,
20
+ });
21
+
22
+ export const Select = getRegisteredComponentWithFallback('Select', SelectWithSubcomponents);
5
23
 
6
24
  export * from './context';
7
25
  export type * from './types';