react-native-molecules 0.5.0-beta.2 → 0.5.0-beta.20

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 (157) hide show
  1. package/README.md +1 -1
  2. package/components/Accordion/Accordion.tsx +2 -6
  3. package/components/Accordion/AccordionItem.tsx +16 -12
  4. package/components/Accordion/AccordionItemContent.tsx +6 -1
  5. package/components/Accordion/AccordionItemHeader.tsx +1 -1
  6. package/components/Accordion/utils.ts +6 -0
  7. package/components/ActivityIndicator/ActivityIndicator.tsx +6 -15
  8. package/components/Appbar/AppbarBase.tsx +18 -13
  9. package/components/Button/Button.tsx +209 -264
  10. package/components/Button/index.tsx +9 -3
  11. package/components/Button/types.ts +16 -2
  12. package/components/Button/utils.ts +230 -208
  13. package/components/Checkbox/CheckboxBase.tsx +23 -128
  14. package/components/Checkbox/utils.ts +0 -25
  15. package/components/Chip/Chip.tsx +40 -52
  16. package/components/Chip/utils.ts +3 -7
  17. package/components/DateField/DateField.tsx +110 -0
  18. package/components/DateField/index.tsx +6 -0
  19. package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +17 -49
  20. package/components/DatePicker/DateCalendar.tsx +83 -0
  21. package/components/DatePicker/DatePickerActions.tsx +73 -0
  22. package/components/DatePicker/DatePickerModal.tsx +234 -0
  23. package/components/DatePicker/DatePickerPopover.tsx +79 -0
  24. package/components/DatePicker/DatePickerProvider.tsx +152 -0
  25. package/components/DatePicker/DatePickerTrigger.tsx +23 -0
  26. package/components/DatePicker/context.tsx +82 -0
  27. package/components/DatePicker/index.tsx +44 -0
  28. package/components/DatePicker/utils.ts +293 -0
  29. package/components/DatePickerInline/DatePickerContext.tsx +1 -0
  30. package/components/DatePickerInline/DatePickerDockedHeader.tsx +113 -0
  31. package/components/DatePickerInline/DatePickerInline.tsx +16 -15
  32. package/components/DatePickerInline/DatePickerInlineBase.tsx +7 -1
  33. package/components/DatePickerInline/Day.tsx +25 -1
  34. package/components/DatePickerInline/DayRange.tsx +2 -4
  35. package/components/DatePickerInline/HeaderItem.tsx +42 -27
  36. package/components/DatePickerInline/Month.tsx +45 -65
  37. package/components/DatePickerInline/MonthPicker.tsx +25 -41
  38. package/components/DatePickerInline/Swiper.native.tsx +21 -4
  39. package/components/DatePickerInline/Swiper.tsx +168 -13
  40. package/components/DatePickerInline/Week.tsx +6 -1
  41. package/components/DatePickerInline/YearPicker.tsx +206 -53
  42. package/components/DatePickerInline/dateUtils.tsx +17 -12
  43. package/components/DatePickerInline/types.ts +3 -0
  44. package/components/DatePickerInline/utils.ts +66 -29
  45. package/components/Drawer/Drawer.tsx +17 -6
  46. package/components/ElementGroup/ElementGroup.tsx +16 -14
  47. package/components/FilePicker/FilePicker.tsx +48 -78
  48. package/components/FilePicker/index.tsx +2 -1
  49. package/components/FilePicker/utils.ts +9 -0
  50. package/components/HelperText/HelperText.tsx +0 -35
  51. package/components/Icon/iconFactory.tsx +3 -3
  52. package/components/Icon/index.tsx +1 -1
  53. package/components/Icon/types.ts +17 -6
  54. package/components/IconButton/IconButton.tsx +42 -57
  55. package/components/IconButton/utils.ts +142 -33
  56. package/components/ListItem/ListItem.tsx +3 -1
  57. package/components/ListItem/utils.ts +1 -1
  58. package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
  59. package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
  60. package/components/LoadingIndicator/index.tsx +13 -0
  61. package/components/LoadingIndicator/utils.ts +117 -0
  62. package/components/Menu/Menu.tsx +3 -18
  63. package/components/NavigationRail/NavigationRail.tsx +15 -9
  64. package/components/Popover/Popover.tsx +122 -145
  65. package/components/Popover/PopoverRoot.tsx +74 -0
  66. package/components/Popover/common.ts +50 -34
  67. package/components/Popover/index.ts +18 -1
  68. package/components/Popover/usePlatformMeasure.native.ts +90 -0
  69. package/components/Popover/usePlatformMeasure.ts +118 -0
  70. package/components/Popover/utils.ts +34 -0
  71. package/components/Select/Select.tsx +368 -507
  72. package/components/Select/context.tsx +72 -0
  73. package/components/Select/index.ts +8 -14
  74. package/components/Select/types.ts +2 -4
  75. package/components/Select/utils.ts +144 -0
  76. package/components/Slot/Slot.tsx +244 -0
  77. package/components/Slot/compose-refs.tsx +62 -0
  78. package/components/Slot/index.tsx +8 -0
  79. package/components/Surface/Surface.android.tsx +34 -8
  80. package/components/Surface/Surface.ios.tsx +36 -29
  81. package/components/Surface/Surface.tsx +31 -4
  82. package/components/Surface/utils.ts +44 -30
  83. package/components/Switch/Switch.tsx +8 -2
  84. package/components/Tabs/TabItem.tsx +35 -58
  85. package/components/Tabs/TabLabel.tsx +5 -9
  86. package/components/Tabs/Tabs.tsx +154 -148
  87. package/components/Tabs/utils.ts +15 -2
  88. package/components/TextInput/TextInput.tsx +658 -575
  89. package/components/TextInput/index.tsx +19 -3
  90. package/components/TextInput/types.ts +76 -27
  91. package/components/TextInput/utils.ts +225 -145
  92. package/components/TimeField/TimeField.tsx +75 -0
  93. package/components/TimeField/index.tsx +6 -0
  94. package/components/TimeField/useTimeFieldState.ts +70 -0
  95. package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
  96. package/components/TimePicker/TimeInput.tsx +87 -37
  97. package/components/TimePicker/TimeInputs.tsx +137 -49
  98. package/components/TimePicker/TimePicker.tsx +73 -10
  99. package/components/TimePicker/TimePickerModal.tsx +186 -0
  100. package/components/TimePicker/context.tsx +17 -0
  101. package/components/TimePicker/index.tsx +15 -3
  102. package/components/TimePicker/utils.ts +93 -0
  103. package/components/Tooltip/Tooltip.tsx +42 -67
  104. package/components/Tooltip/TooltipContent.tsx +32 -5
  105. package/components/Tooltip/TooltipTrigger.tsx +20 -20
  106. package/components/Tooltip/index.tsx +1 -1
  107. package/components/TouchableRipple/TouchableRipple.native.tsx +50 -14
  108. package/components/TouchableRipple/TouchableRipple.tsx +137 -47
  109. package/hocs/withPortal.tsx +1 -1
  110. package/hooks/index.tsx +0 -6
  111. package/hooks/useActionState.tsx +19 -8
  112. package/hooks/useControlledValue.tsx +20 -4
  113. package/hooks/useFilePicker.tsx +6 -16
  114. package/hooks/useWhatHasUpdated.tsx +48 -0
  115. package/package.json +17 -13
  116. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +5 -2
  117. package/styles/shadow.ts +2 -1
  118. package/styles/themes/LightTheme.tsx +1 -1
  119. package/utils/DocumentPicker/documentPicker.ts +78 -27
  120. package/utils/DocumentPicker/types.ts +0 -1
  121. package/utils/extractPropertiesFromStyles.ts +25 -0
  122. package/utils/extractSubcomponents.ts +89 -0
  123. package/utils/lodash.ts +77 -5
  124. package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
  125. package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
  126. package/components/DatePickerDocked/index.tsx +0 -17
  127. package/components/DatePickerDocked/types.ts +0 -11
  128. package/components/DatePickerDocked/utils.ts +0 -157
  129. package/components/DatePickerInput/DatePickerInput.tsx +0 -139
  130. package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
  131. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -77
  132. package/components/DatePickerInput/DateRangeInput.tsx +0 -88
  133. package/components/DatePickerInput/index.tsx +0 -10
  134. package/components/DatePickerInput/types.ts +0 -28
  135. package/components/DatePickerInput/utils.ts +0 -15
  136. package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
  137. package/components/DatePickerModal/CalendarEdit.tsx +0 -139
  138. package/components/DatePickerModal/DatePickerModal.tsx +0 -85
  139. package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
  140. package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
  141. package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
  142. package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
  143. package/components/DatePickerModal/index.tsx +0 -16
  144. package/components/DatePickerModal/types.ts +0 -92
  145. package/components/DatePickerModal/utils.ts +0 -122
  146. package/components/DateTimePicker/DateTimePicker.tsx +0 -172
  147. package/components/DateTimePicker/index.tsx +0 -10
  148. package/components/DateTimePicker/utils.ts +0 -12
  149. package/components/Popover/Popover.native.tsx +0 -185
  150. package/components/TimePickerField/TimePickerField.tsx +0 -152
  151. package/components/TimePickerField/index.tsx +0 -10
  152. package/components/TimePickerField/utils.ts +0 -94
  153. package/components/TimePickerModal/TimePickerModal.tsx +0 -115
  154. package/components/TimePickerModal/index.tsx +0 -10
  155. package/components/TimePickerModal/utils.ts +0 -47
  156. package/hooks/useSearchable.tsx +0 -74
  157. package/hooks/useSubcomponents.tsx +0 -59
@@ -1,13 +1,4 @@
1
- import {
2
- createContext,
3
- memo,
4
- useCallback,
5
- useContext,
6
- useEffect,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from 'react';
1
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
11
2
  import {
12
3
  type AccessibilityRole,
13
4
  type GestureResponderEvent,
@@ -17,281 +8,235 @@ import {
17
8
  ScrollView,
18
9
  View,
19
10
  } from 'react-native';
20
- import { StyleSheet } from 'react-native-unistyles';
21
11
 
22
- import { useActionState, useControlledValue } from '../../hooks';
12
+ import { typedMemo } from '../../hocs';
13
+ import { useActionState, useControlledValue, useLatest } from '../../hooks';
23
14
  import { useToggle } from '../../hooks';
24
15
  import { resolveStateVariant } from '../../utils';
25
16
  import { Chip } from '../Chip';
26
17
  import { Icon } from '../Icon';
27
18
  import { IconButton } from '../IconButton';
28
19
  import { Popover } from '../Popover';
29
- import { registerPortalContext } from '../Portal';
30
20
  import { Text } from '../Text';
31
21
  import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
22
+ import {
23
+ SelectContextProvider,
24
+ SelectDropdownContextProvider,
25
+ useSelectContextValue,
26
+ useSelectDropdownContextValue,
27
+ } from './context';
32
28
  import type {
33
29
  DefaultItemT,
34
30
  SelectContentProps,
35
31
  SelectContextValue,
36
- SelectDropdownContextValue,
37
32
  SelectDropdownProps,
38
33
  SelectGroupProps,
39
34
  SelectOptionProps,
40
- SelectProviderProps,
35
+ SelectProps,
41
36
  SelectSearchInputProps,
42
37
  SelectTriggerProps,
43
38
  SelectValueProps,
44
39
  } from './types';
40
+ import { styles, triggerStyles } from './utils';
45
41
 
46
- // SelectContext - holds value, onAdd, onRemove
47
- export const SelectContext = createContext<SelectContextValue<DefaultItemT>>({
48
- value: null,
49
- multiple: false,
50
- onAdd: () => {},
51
- onRemove: () => {},
52
- disabled: false,
53
- error: false,
54
- labelKey: 'label',
55
- options: [],
56
- searchQuery: '',
57
- setSearchQuery: () => {},
58
- filteredOptions: [],
59
- });
60
-
61
- export const useSelectContext = <Option extends DefaultItemT = DefaultItemT>() => {
62
- return useContext(SelectContext) as unknown as SelectContextValue<Option>;
63
- };
64
-
65
- export const useSelectContextValue = <Option extends DefaultItemT = DefaultItemT, T = any>(
66
- selector: (state: SelectContextValue<Option>) => T,
67
- ): T => {
68
- const context = useContext(SelectContext) as unknown as SelectContextValue<Option>;
69
- return selector(context);
70
- };
42
+ const emptyArr: unknown[] = [];
71
43
 
72
- // SelectDropdownContext - holds isOpen, onClose, triggerRef
73
- export type SelectDropdownContextType = SelectDropdownContextValue & {
74
- triggerRef: React.RefObject<View> | null;
75
- contentRef: React.RefObject<any> | null;
76
- triggerLayout: { width: number; height: number } | null;
77
- setTriggerLayout: (layout: { width: number; height: number }) => void;
78
- };
79
-
80
- export const SelectDropdownContext = createContext<SelectDropdownContextType>({
81
- isOpen: false,
82
- onClose: () => {},
83
- onOpen: () => {},
84
- triggerRef: null,
85
- contentRef: null,
86
- triggerLayout: null,
87
- setTriggerLayout: () => {},
88
- });
44
+ // SelectProvider - manages controlled/uncontrolled state
45
+ const SelectProvider = typedMemo(
46
+ <Option extends DefaultItemT = DefaultItemT>({
47
+ children,
48
+ value: valueProp,
49
+ defaultValue,
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);
89
66
 
90
- registerPortalContext([SelectContext, SelectDropdownContext]);
67
+ const [searchQuery, setSearchQuery] = useState('');
91
68
 
92
- export const useSelectDropdownContext = () => {
93
- return useContext(SelectDropdownContext);
94
- };
69
+ const handleSearchQueryChange = useCallback(
70
+ (query: string) => {
71
+ setSearchQuery(query);
72
+ onSearchChange?.(query);
73
+ },
74
+ [onSearchChange],
75
+ );
95
76
 
96
- export const useSelectDropdownContextValue = <T,>(
97
- selector: (state: SelectDropdownContextType) => T,
98
- ): T => {
99
- const context = useContext(SelectDropdownContext);
100
- return selector(context);
101
- };
77
+ // Default hideSelected to multiple (true for multi-select, false for single select)
78
+ const hideSelected = hideSelectedProp !== undefined ? hideSelectedProp : multiple;
102
79
 
103
- // SelectProvider - manages controlled/uncontrolled state
104
- const SelectProvider = <Option extends DefaultItemT = DefaultItemT>({
105
- children,
106
- value: valueProp,
107
- defaultValue,
108
- onChange,
109
- multiple = false,
110
- disabled = false,
111
- error = false,
112
- labelKey = 'label',
113
- options = [],
114
- searchKey,
115
- onSearchChange,
116
- hideSelected: hideSelectedProp,
117
- }: SelectProviderProps<Option>) => {
118
- const [value, onValueChange] = useControlledValue<Option['id'] | Option['id'][] | null>({
119
- value: valueProp,
120
- defaultValue: defaultValue ?? (multiple ? [] : null),
121
- onChange: (newValue, item, event) => {
122
- onChange?.(newValue, item as Option, event);
123
- },
124
- });
80
+ const filteredOptions = useMemo(() => {
81
+ let result = options;
125
82
 
126
- const [searchQuery, setSearchQuery] = useState('');
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
+ }
127
95
 
128
- const handleSearchQueryChange = useCallback(
129
- (query: string) => {
130
- setSearchQuery(query);
131
- onSearchChange?.(query);
132
- },
133
- [onSearchChange],
134
- );
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
+ }
135
107
 
136
- // Default hideSelected to multiple (true for multi-select, false for single select)
137
- const hideSelected = hideSelectedProp !== undefined ? hideSelectedProp : multiple;
108
+ return result;
109
+ }, [options, searchQuery, searchKey, labelKey, hideSelected, multiple, value]);
138
110
 
139
- const filteredOptions = useMemo(() => {
140
- let result = options;
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
+ );
141
124
 
142
- // Filter out selected items if hideSelected is true
143
- if (hideSelected) {
144
- result = result.filter(item => {
125
+ const onRemove = useCallback(
126
+ (item: Option) => {
145
127
  if (multiple) {
146
- const values = (value as Option['id'][]) || [];
147
- return !values.some(v => v === item.id);
128
+ const currentValue = (valueRef.current as Option['id'][]) || [];
129
+ onValueChange(currentValue.filter(v => v !== item.id) as Option['id'][], item);
148
130
  } else {
149
- const singleValue = value as Option['id'] | null;
150
- return singleValue !== item.id;
131
+ onValueChange(null, item);
151
132
  }
152
- });
153
- }
133
+ },
134
+ [multiple, valueRef, onValueChange],
135
+ );
154
136
 
155
- // Apply search filter if there's a search query
156
- if (searchQuery) {
157
- const key = searchKey || labelKey || 'label';
158
- const lowerQuery = searchQuery.toLowerCase();
159
- result = result.filter(item => {
160
- const itemValue = item[key];
161
- return String(itemValue || '')
162
- .toLowerCase()
163
- .includes(lowerQuery);
164
- });
165
- }
137
+ const contextValue = useMemo(
138
+ () => ({
139
+ value: value,
140
+ multiple,
141
+ onAdd: onAdd as (item: DefaultItemT) => void,
142
+ onRemove: onRemove as (item: DefaultItemT) => void,
143
+ disabled,
144
+ error,
145
+ labelKey,
146
+ options,
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
+ );
166
165
 
167
- return result;
168
- }, [options, searchQuery, searchKey, labelKey, hideSelected, multiple, value]);
166
+ return (
167
+ <SelectContextProvider
168
+ value={contextValue as unknown as SelectContextValue<DefaultItemT>}>
169
+ {children}
170
+ </SelectContextProvider>
171
+ );
172
+ },
173
+ );
169
174
 
170
- const onAdd = useCallback(
171
- (item: Option) => {
172
- if (multiple) {
173
- const currentValue = (value as Option['id'][]) || [];
174
- if (!currentValue.find(v => v === item.id)) {
175
- onValueChange([...currentValue, item.id] as Option['id'][], item);
176
- }
175
+ // SelectDropdownProvider - manages dropdown state
176
+ const SelectDropdownProvider = memo(
177
+ ({
178
+ children,
179
+ isOpen: isOpenProp,
180
+ onClose: onCloseProp,
181
+ }: {
182
+ children: React.ReactNode;
183
+ isOpen?: boolean;
184
+ onClose?: () => void;
185
+ }) => {
186
+ const { state: isOpen, handleOpen, handleClose } = useToggle(false);
187
+ const triggerRef = useRef<View>(null);
188
+ const contentRef = useRef<any>(null);
189
+ const [triggerLayout, setTriggerLayout] = useState<{
190
+ width: number;
191
+ height: number;
192
+ } | null>(null);
193
+ const isControlled = isOpenProp !== undefined;
194
+
195
+ const onClose = useCallback(() => {
196
+ if (isControlled) {
197
+ onCloseProp?.();
177
198
  } else {
178
- onValueChange(item.id, item);
199
+ handleClose();
179
200
  }
180
- },
181
- [multiple, value, onValueChange],
182
- );
201
+ }, [isControlled, onCloseProp, handleClose]);
183
202
 
184
- const onRemove = useCallback(
185
- (item: Option) => {
186
- if (multiple) {
187
- const currentValue = (value as Option['id'][]) || [];
188
- onValueChange(currentValue.filter(v => v !== item.id) as Option['id'][], item);
189
- } else {
190
- onValueChange(null, item);
203
+ const onOpen = useCallback(() => {
204
+ if (!isControlled) {
205
+ handleOpen();
191
206
  }
192
- },
193
- [multiple, value, onValueChange],
194
- );
195
-
196
- const contextValue = useMemo(
197
- () => ({
198
- value: value,
199
- multiple,
200
- onAdd: onAdd as (item: DefaultItemT) => void,
201
- onRemove: onRemove as (item: DefaultItemT) => void,
202
- disabled,
203
- error,
204
- labelKey,
205
- options,
206
- searchQuery,
207
- setSearchQuery: handleSearchQueryChange,
208
- filteredOptions,
209
- }),
210
- [
211
- value,
212
- multiple,
213
- onAdd,
214
- onRemove,
215
- disabled,
216
- error,
217
- labelKey,
218
- options,
219
- searchQuery,
220
- handleSearchQueryChange,
221
- filteredOptions,
222
- ],
223
- );
224
-
225
- return (
226
- <SelectContext.Provider value={contextValue as unknown as SelectContextValue<DefaultItemT>}>
227
- {children}
228
- </SelectContext.Provider>
229
- );
230
- };
231
-
232
- // SelectDropdownProvider - manages dropdown state
233
- const SelectDropdownProvider = ({
234
- children,
235
- isOpen: isOpenProp,
236
- onClose: onCloseProp,
237
- }: {
238
- children: React.ReactNode;
239
- isOpen?: boolean;
240
- onClose?: () => void;
241
- }) => {
242
- const { state: isOpen, handleOpen, handleClose } = useToggle(false);
243
- const triggerRef = useRef<View>(null);
244
- const contentRef = useRef<any>(null);
245
- const [triggerLayout, setTriggerLayout] = useState<{ width: number; height: number } | null>(
246
- null,
247
- );
248
- const isControlled = isOpenProp !== undefined;
249
-
250
- const onClose = useCallback(() => {
251
- if (isControlled) {
252
- onCloseProp?.();
253
- } else {
254
- handleClose();
255
- }
256
- }, [isControlled, onCloseProp, handleClose]);
257
-
258
- const onOpen = useCallback(() => {
259
- if (!isControlled) {
260
- handleOpen();
261
- }
262
- }, [handleOpen, isControlled]);
263
-
264
- const contextValue = useMemo(
265
- () => ({
266
- isOpen: isControlled ? isOpenProp! : isOpen,
267
- onClose,
268
- onOpen,
269
- triggerRef: triggerRef as React.RefObject<View>,
270
- contentRef,
271
- triggerLayout,
272
- setTriggerLayout,
273
- }),
274
- [isControlled, isOpenProp, isOpen, onClose, onOpen, triggerLayout],
275
- );
207
+ }, [handleOpen, isControlled]);
208
+
209
+ const contextValue = useMemo(
210
+ () => ({
211
+ isOpen: isControlled ? isOpenProp! : isOpen,
212
+ onClose,
213
+ onOpen,
214
+ triggerRef: triggerRef as React.RefObject<View>,
215
+ contentRef,
216
+ triggerLayout,
217
+ setTriggerLayout,
218
+ }),
219
+ [isControlled, isOpenProp, isOpen, onClose, onOpen, triggerLayout],
220
+ );
276
221
 
277
- return (
278
- <SelectDropdownContext.Provider value={contextValue}>
279
- {children}
280
- </SelectDropdownContext.Provider>
281
- );
282
- };
222
+ return (
223
+ <SelectDropdownContextProvider value={contextValue}>
224
+ {children}
225
+ </SelectDropdownContextProvider>
226
+ );
227
+ },
228
+ );
283
229
 
284
230
  // Select - wrapper component
285
- const Select = <Option extends DefaultItemT = DefaultItemT>({
286
- children,
287
- ...props
288
- }: SelectProviderProps<Option>) => {
289
- return (
290
- <SelectProvider<Option> {...props}>
291
- <SelectDropdownProvider>{children}</SelectDropdownProvider>
292
- </SelectProvider>
293
- );
294
- };
231
+ const Select = typedMemo(
232
+ <Option extends DefaultItemT = DefaultItemT>({ children, ...props }: SelectProps<Option>) => {
233
+ return (
234
+ <SelectProvider<Option> {...props}>
235
+ <SelectDropdownProvider>{children}</SelectDropdownProvider>
236
+ </SelectProvider>
237
+ );
238
+ },
239
+ );
295
240
 
296
241
  // Select.Trigger - opens the dropdown
297
242
  const SelectTrigger = ({ children, style, ...rest }: SelectTriggerProps) => {
@@ -363,7 +308,7 @@ const SelectTrigger = ({ children, style, ...rest }: SelectTriggerProps) => {
363
308
  SelectTrigger.displayName = 'Select_Trigger';
364
309
 
365
310
  // Select.Value - displays the value
366
- const SelectValue = ({ placeholder, renderValue, style, ...rest }: SelectValueProps) => {
311
+ const SelectValue = memo(({ placeholder, renderValue, style, ...rest }: SelectValueProps) => {
367
312
  const { value, multiple, labelKey, onRemove, options } = useSelectContextValue(state => ({
368
313
  value: state.value,
369
314
  multiple: state.multiple,
@@ -409,13 +354,10 @@ const SelectValue = ({ placeholder, renderValue, style, ...rest }: SelectValuePr
409
354
  return (
410
355
  <View style={[styles.chipContainer, style]} {...rest}>
411
356
  {(resolvedValue as DefaultItemT[]).map(item => (
412
- <Chip.Input
357
+ <SelectValueItem
413
358
  key={item.id || String(item)}
414
- label={item[labelKey || 'label'] || String(item.id || item)}
415
- size="sm"
416
- selected
417
- left={<></>}
418
- onClose={() => onRemove(item)}
359
+ item={item}
360
+ onRemoveItem={onRemove}
419
361
  />
420
362
  ))}
421
363
  </View>
@@ -427,67 +369,95 @@ const SelectValue = ({ placeholder, renderValue, style, ...rest }: SelectValuePr
427
369
  {displayValue}
428
370
  </Text>
429
371
  );
430
- };
372
+ });
373
+
374
+ const SelectValueItem = typedMemo(
375
+ ({
376
+ item,
377
+ onRemoveItem,
378
+ }: {
379
+ item: DefaultItemT;
380
+ onRemoveItem: (item: DefaultItemT) => void;
381
+ }) => {
382
+ const onRemove = useCallback(() => {
383
+ onRemoveItem(item);
384
+ }, [item, onRemoveItem]);
385
+
386
+ return (
387
+ <Chip.Input
388
+ label={item[item.labelKey || 'label'] || String(item.id || item)}
389
+ size="sm"
390
+ selected
391
+ left={<></>}
392
+ onClose={onRemove}
393
+ />
394
+ );
395
+ },
396
+ );
431
397
 
432
398
  SelectValue.displayName = 'Select_Value';
433
399
 
434
400
  // Select.Dropdown - popover with keyboard navigation
435
- const SelectDropdown = ({
436
- children,
437
- WrapperComponent,
438
- wrapperComponentProps,
439
- enableKeyboardNavigation = true,
440
- style: popoverStyleProp,
441
- ...popoverProps
442
- }: SelectDropdownProps & { enableKeyboardNavigation?: boolean }) => {
443
- const { isOpen, onClose, triggerRef, triggerLayout } = useSelectDropdownContextValue(state => ({
444
- isOpen: state.isOpen,
445
- onClose: state.onClose,
446
- triggerRef: state.triggerRef,
447
- triggerLayout: state.triggerLayout,
448
- }));
401
+ const SelectDropdown = memo(
402
+ ({
403
+ children,
404
+ WrapperComponent,
405
+ wrapperComponentProps,
406
+ enableKeyboardNavigation = true,
407
+ style: popoverStyleProp,
408
+ ...popoverProps
409
+ }: SelectDropdownProps & { enableKeyboardNavigation?: boolean }) => {
410
+ const { isOpen, onClose, triggerRef, triggerLayout } = useSelectDropdownContextValue(
411
+ state => ({
412
+ isOpen: state.isOpen,
413
+ onClose: state.onClose,
414
+ triggerRef: state.triggerRef,
415
+ triggerLayout: state.triggerLayout,
416
+ }),
417
+ );
449
418
 
450
- const popoverStyle = useMemo(() => {
451
- const baseStyle = popoverStyleProp ? [popoverStyleProp] : [];
452
- if (triggerLayout) {
453
- return [{ width: triggerLayout.width }, ...baseStyle];
454
- }
455
- return baseStyle;
456
- }, [triggerLayout, popoverStyleProp]);
419
+ const popoverStyle = useMemo(() => {
420
+ const baseStyle = popoverStyleProp ? [popoverStyleProp] : [];
421
+ if (triggerLayout) {
422
+ return [{ width: triggerLayout.width }, ...baseStyle];
423
+ }
424
+ return baseStyle;
425
+ }, [triggerLayout, popoverStyleProp]);
457
426
 
458
- if (!triggerLayout) return null;
427
+ if (!triggerLayout) return null;
428
+
429
+ if (WrapperComponent) {
430
+ return (
431
+ <WrapperComponent isOpen={isOpen} onClose={onClose} {...wrapperComponentProps}>
432
+ {enableKeyboardNavigation && Platform.OS === 'web' ? (
433
+ <KeyboardNavigationWrapper>{children}</KeyboardNavigationWrapper>
434
+ ) : (
435
+ children
436
+ )}
437
+ </WrapperComponent>
438
+ );
439
+ }
459
440
 
460
- if (WrapperComponent) {
461
441
  return (
462
- <WrapperComponent isOpen={isOpen} onClose={onClose} {...wrapperComponentProps}>
442
+ <Popover
443
+ triggerRef={triggerRef as React.RefObject<View>}
444
+ isOpen={isOpen}
445
+ onClose={onClose}
446
+ style={popoverStyle}
447
+ triggerDimensions={triggerLayout}
448
+ {...popoverProps}>
463
449
  {enableKeyboardNavigation && Platform.OS === 'web' ? (
464
450
  <KeyboardNavigationWrapper>{children}</KeyboardNavigationWrapper>
465
451
  ) : (
466
452
  children
467
453
  )}
468
- </WrapperComponent>
454
+ </Popover>
469
455
  );
470
- }
471
-
472
- return (
473
- <Popover
474
- triggerRef={triggerRef as React.RefObject<View>}
475
- isOpen={isOpen}
476
- onClose={onClose}
477
- style={popoverStyle}
478
- triggerDimensions={triggerLayout}
479
- {...popoverProps}>
480
- {enableKeyboardNavigation && Platform.OS === 'web' ? (
481
- <KeyboardNavigationWrapper>{children}</KeyboardNavigationWrapper>
482
- ) : (
483
- children
484
- )}
485
- </Popover>
486
- );
487
- };
456
+ },
457
+ );
488
458
 
489
459
  // Keyboard navigation wrapper for web
490
- const KeyboardNavigationWrapper = ({ children }: { children: React.ReactNode }) => {
460
+ const KeyboardNavigationWrapper = memo(({ children }: { children: React.ReactNode }) => {
491
461
  const { onClose, contentRef, isOpen } = useSelectDropdownContextValue(state => ({
492
462
  onClose: state.onClose,
493
463
  contentRef: state.contentRef,
@@ -533,8 +503,14 @@ const KeyboardNavigationWrapper = ({ children }: { children: React.ReactNode })
533
503
  break;
534
504
  case 'Enter':
535
505
  e.preventDefault();
506
+ e.stopImmediatePropagation();
536
507
  if (currentIndex !== -1) {
537
- options[currentIndex]?.click();
508
+ // Store reference to the focused element before triggering click
509
+ // to prevent issues with DOM updates during the click handler
510
+ const focusedOption = options[currentIndex];
511
+ if (focusedOption) {
512
+ focusedOption.click();
513
+ }
538
514
  }
539
515
  break;
540
516
  case 'Escape':
@@ -599,75 +575,81 @@ const KeyboardNavigationWrapper = ({ children }: { children: React.ReactNode })
599
575
  }, [handleKeyDown, contentRef, isOpen]);
600
576
 
601
577
  return <>{children}</>;
602
- };
578
+ });
603
579
 
604
580
  SelectDropdown.displayName = 'Select_Dropdown';
605
581
 
606
582
  // Select.Content - ScrollView that renders children
607
- const SelectContent = ({
608
- children,
609
- ContainerComponent = ScrollView,
610
- style,
611
- emptyState,
612
- ...rest
613
- }: SelectContentProps) => {
614
- const { contentRef } = useSelectDropdownContextValue(state => ({
615
- contentRef: state.contentRef,
616
- }));
617
-
618
- const { filteredOptions, value, multiple, searchQuery, options } = useSelectContextValue(
619
- state => ({
620
- filteredOptions: state.filteredOptions,
621
- value: state.value,
622
- multiple: state.multiple,
623
- searchQuery: state.searchQuery,
624
- options: state.options,
625
- }),
626
- );
583
+ const SelectContent = memo(
584
+ ({
585
+ children,
586
+ ContainerComponent = ScrollView,
587
+ style,
588
+ emptyState,
589
+ ...rest
590
+ }: SelectContentProps) => {
591
+ const { contentRef } = useSelectDropdownContextValue(state => ({
592
+ contentRef: state.contentRef,
593
+ }));
627
594
 
628
- const content = useMemo(() => {
629
- return filteredOptions.map(option => {
630
- const isSelected = multiple
631
- ? (value as any[])?.some(v => (v?.id ?? v) === option.id)
632
- : (value as any)?.id === option.id || (value as any) === option.id;
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
+ );
633
604
 
634
- return children(option, !!isSelected);
635
- });
636
- }, [filteredOptions, value, multiple, children]);
605
+ const content = useMemo(() => {
606
+ return filteredOptions.map(option => {
607
+ const isSelected = multiple
608
+ ? (value as any[])?.some(v => (v?.id ?? v) === option.id)
609
+ : (value as any)?.id === option.id || (value as any) === option.id;
637
610
 
638
- const defaultEmptyState = useMemo(() => {
639
- const hasSearchQuery = searchQuery && searchQuery.trim().length > 0;
640
- const hasNoOptions = options.length === 0;
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
+ );
625
+ }
641
626
 
642
- if (hasNoOptions) {
643
- return (
644
- <View style={styles.emptyState}>
645
- <Text style={styles.emptyStateText}>No options available</Text>
646
- </View>
647
- );
648
- }
627
+ if (hasSearchQuery) {
628
+ return (
629
+ <View style={styles.emptyState}>
630
+ <Text style={styles.emptyStateText}>No results found</Text>
631
+ </View>
632
+ );
633
+ }
649
634
 
650
- if (hasSearchQuery) {
651
635
  return (
652
636
  <View style={styles.emptyState}>
653
- <Text style={styles.emptyStateText}>No results found</Text>
637
+ <Text style={styles.emptyStateText}>No options</Text>
654
638
  </View>
655
639
  );
656
- }
640
+ }, [searchQuery, options.length]);
657
641
 
658
642
  return (
659
- <View style={styles.emptyState}>
660
- <Text style={styles.emptyStateText}>No options</Text>
661
- </View>
643
+ <ContainerComponent
644
+ ref={contentRef}
645
+ style={style}
646
+ {...rest}
647
+ accessibilityRole="listbox">
648
+ {filteredOptions.length === 0 ? emptyState ?? defaultEmptyState : content}
649
+ </ContainerComponent>
662
650
  );
663
- }, [searchQuery, options.length]);
664
-
665
- return (
666
- <ContainerComponent ref={contentRef} style={style} {...rest} accessibilityRole="listbox">
667
- {filteredOptions.length === 0 ? emptyState ?? defaultEmptyState : content}
668
- </ContainerComponent>
669
- );
670
- };
651
+ },
652
+ );
671
653
 
672
654
  SelectContent.displayName = 'Select_Content';
673
655
 
@@ -695,23 +677,17 @@ const SelectOption = memo(
695
677
  ...rest
696
678
  }: SelectOptionProps<Option>) => {
697
679
  const {
698
- value: selectionValue,
699
680
  multiple,
700
681
  onAdd,
701
682
  onRemove,
702
683
  disabled: selectDisabled,
703
- } = useSelectContextValue<Option>(state => ({
704
- value: state.value,
684
+ } = useSelectContextValue(state => ({
705
685
  multiple: state.multiple,
706
686
  onAdd: state.onAdd,
707
687
  onRemove: state.onRemove,
708
688
  disabled: state.disabled,
709
689
  }));
710
690
 
711
- const { onClose } = useSelectDropdownContextValue(state => ({
712
- onClose: state.onClose,
713
- }));
714
-
715
691
  const option = useMemo(() => {
716
692
  return {
717
693
  id: value,
@@ -720,15 +696,19 @@ const SelectOption = memo(
720
696
  } as Option;
721
697
  }, [children, optionDisabledProp, value]);
722
698
 
723
- const isSelected = useMemo(() => {
699
+ const isSelected = useSelectContextValue(state => {
724
700
  if (multiple) {
725
- const values = selectionValue as any[];
701
+ const values = state.value as any[];
726
702
  return values?.some(v => (v?.id ?? v) === option.id) || false;
727
703
  } else {
728
- const singleValue = selectionValue as any;
704
+ const singleValue = state.value as any;
729
705
  return (singleValue?.id ?? singleValue) === option.id || false;
730
706
  }
731
- }, [selectionValue, multiple, option.id]);
707
+ });
708
+
709
+ const { onClose } = useSelectDropdownContextValue(state => ({
710
+ onClose: state.onClose,
711
+ }));
732
712
 
733
713
  const isOptionDisabled = Boolean(
734
714
  selectDisabled || optionDisabledProp || option.selectable === false,
@@ -779,6 +759,14 @@ const SelectOption = memo(
779
759
  tabIndex: -1 as 0 | -1 | undefined,
780
760
  // Use a dataset attribute to help the keyboard navigator find this
781
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
+ },
782
770
  },
783
771
  }),
784
772
  };
@@ -818,7 +806,7 @@ SelectOption.displayName = 'Select_Option';
818
806
 
819
807
  // Select.SearchInput - handles search
820
808
  const SelectSearchInput = memo(
821
- ({ onQueryChange, autoFocus = true, ...textInputProps }: SelectSearchInputProps) => {
809
+ ({ autoFocus = true, children, ...textInputProps }: SelectSearchInputProps) => {
822
810
  const { searchQuery, setSearchQuery } = useSelectContextValue(state => ({
823
811
  searchQuery: state.searchQuery,
824
812
  setSearchQuery: state.setSearchQuery,
@@ -828,15 +816,13 @@ const SelectSearchInput = memo(
828
816
  const handleChangeText = useCallback(
829
817
  (text: string) => {
830
818
  setSearchQuery(text);
831
- onQueryChange?.(text);
832
- textInputProps.onChangeText?.(text);
833
819
  },
834
- [onQueryChange, setSearchQuery, textInputProps],
820
+ [setSearchQuery],
835
821
  );
836
822
 
837
823
  const inputProps = {
838
824
  ...textInputProps,
839
- value: textInputProps.value !== undefined ? textInputProps.value : searchQuery,
825
+ value: searchQuery,
840
826
  onChangeText: handleChangeText,
841
827
  placeholder: textInputProps.placeholder || 'Search...',
842
828
  inputStyle: styles.searchInputInput,
@@ -866,23 +852,32 @@ const SelectSearchInput = memo(
866
852
  requestAnimationFrame(focusField);
867
853
  }, [autoFocus]);
868
854
 
855
+ const onPressLeftIcon = useCallback(() => {
856
+ textInputRef.current?.focus();
857
+ }, []);
858
+
859
+ const onClearSearchQuery = useCallback(() => {
860
+ handleChangeText('');
861
+ }, [handleChangeText]);
862
+
869
863
  return (
870
864
  <TextInput
871
865
  ref={textInputRef}
872
866
  autoFocus={Platform.OS !== 'web' && autoFocus}
873
867
  style={styles.searchInput}
874
- left={
875
- <Icon onPress={() => textInputRef.current?.focus()} name="magnify" size={20} />
876
- }
877
- right={
878
- searchQuery ? (
879
- <IconButton name="close" size={20} onPress={() => setSearchQuery('')} />
880
- ) : undefined
881
- }
882
868
  size="sm"
883
869
  variant="outlined"
884
- {...inputProps}
885
- />
870
+ {...inputProps}>
871
+ <TextInput.Left>
872
+ <Icon onPress={onPressLeftIcon} name="magnify" size={20} />
873
+ </TextInput.Left>
874
+ {searchQuery && (
875
+ <TextInput.Right>
876
+ <IconButton name="close" size={20} onPress={onClearSearchQuery} />
877
+ </TextInput.Right>
878
+ )}
879
+ {children}
880
+ </TextInput>
886
881
  );
887
882
  },
888
883
  );
@@ -900,139 +895,5 @@ const SelectWithSubcomponents = Object.assign(Select, {
900
895
  SearchInput: SelectSearchInput,
901
896
  });
902
897
 
903
- const triggerStyles = StyleSheet.create(theme => ({
904
- trigger: {
905
- borderRadius: theme.shapes.corner.extraSmall,
906
- paddingHorizontal: theme.spacings['3'],
907
- paddingVertical: theme.spacings['2'],
908
- minHeight: 56,
909
- flexDirection: 'row',
910
- alignItems: 'center',
911
- justifyContent: 'space-between',
912
- width: '100%',
913
- variants: {
914
- state: {
915
- disabled: {
916
- opacity: 0.38,
917
- backgroundColor: theme.colors.surfaceVariant,
918
- },
919
- errorDisabled: {
920
- opacity: 0.38,
921
- },
922
- },
923
- },
924
- },
925
- outline: {
926
- position: 'absolute',
927
- top: 0,
928
- left: 0,
929
- right: 0,
930
- bottom: 0,
931
- borderRadius: theme.shapes.corner.extraSmall,
932
- borderWidth: 1,
933
- borderColor: theme.colors.outline,
934
- pointerEvents: 'none',
935
- variants: {
936
- state: {
937
- focused: {
938
- borderWidth: 2,
939
- borderColor: theme.colors.primary,
940
- },
941
- hovered: {
942
- borderColor: theme.colors.onSurface,
943
- },
944
- hoveredAndFocused: {
945
- borderWidth: 2,
946
- borderColor: theme.colors.primary,
947
- },
948
- disabled: {
949
- borderColor: theme.colors.onSurface,
950
- },
951
- error: {
952
- borderColor: theme.colors.error,
953
- },
954
- errorFocused: {
955
- borderWidth: 2,
956
- borderColor: theme.colors.error,
957
- },
958
- errorHovered: {
959
- borderColor: theme.colors.onErrorContainer,
960
- },
961
- errorFocusedAndHovered: {
962
- borderWidth: 2,
963
- borderColor: theme.colors.error,
964
- },
965
- errorDisabled: {
966
- borderColor: theme.colors.error,
967
- },
968
- },
969
- },
970
- },
971
- triggerIcon: {
972
- marginLeft: theme.spacings['2'],
973
- color: theme.colors.onSurfaceVariant,
974
- },
975
- }));
976
-
977
- const styles = StyleSheet.create(theme => ({
978
- chipContainer: {
979
- flexDirection: 'row',
980
- flexWrap: 'wrap',
981
- gap: 6,
982
- maxWidth: '90%',
983
- },
984
- groupLabel: {
985
- paddingHorizontal: theme.spacings['4'],
986
- paddingVertical: theme.spacings['2'],
987
- fontWeight: '600',
988
- color: theme.colors.onSurface,
989
- },
990
- item: {
991
- paddingHorizontal: theme.spacings['4'],
992
- paddingVertical: theme.spacings['3'],
993
- backgroundColor: 'transparent',
994
-
995
- _web: {
996
- cursor: 'pointer',
997
- outlineStyle: 'none',
998
- _hover: {
999
- backgroundColor: theme.colors.stateLayer.hover.primary,
1000
- },
1001
- _focus: {
1002
- backgroundColor: theme.colors.stateLayer.hover.primary,
1003
- },
1004
- },
1005
- },
1006
- itemSelected: {
1007
- backgroundColor: theme.colors.stateLayer.hover.primary,
1008
- },
1009
- itemDisabled: {
1010
- opacity: 0.38,
1011
- _web: {
1012
- cursor: 'not-allowed',
1013
- },
1014
- },
1015
- itemDisabledText: {
1016
- color: theme.colors.onSurfaceVariant,
1017
- },
1018
- searchInput: {
1019
- marginHorizontal: theme.spacings['2'],
1020
- marginVertical: theme.spacings['3'],
1021
- },
1022
- searchInputInput: {
1023
- height: 42,
1024
- },
1025
- emptyState: {
1026
- paddingHorizontal: theme.spacings['4'],
1027
- paddingVertical: theme.spacings['6'],
1028
- alignItems: 'center',
1029
- justifyContent: 'center',
1030
- },
1031
- emptyStateText: {
1032
- color: theme.colors.onSurfaceVariant,
1033
- fontSize: 14,
1034
- },
1035
- }));
1036
-
1037
898
  export default SelectWithSubcomponents;
1038
899
  export { SelectDropdownProvider, SelectProvider };