react-native-molecules 0.5.0-beta.0

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 (355) hide show
  1. package/components/Accordion/Accordion.tsx +89 -0
  2. package/components/Accordion/AccordionItem.tsx +94 -0
  3. package/components/Accordion/AccordionItemContent.tsx +17 -0
  4. package/components/Accordion/AccordionItemHeader.tsx +122 -0
  5. package/components/Accordion/index.tsx +36 -0
  6. package/components/Accordion/utils.ts +62 -0
  7. package/components/ActivityIndicator/ActivityIndicator.ios.tsx +1 -0
  8. package/components/ActivityIndicator/ActivityIndicator.tsx +200 -0
  9. package/components/ActivityIndicator/AnimatedSpinner.tsx +121 -0
  10. package/components/ActivityIndicator/index.tsx +17 -0
  11. package/components/Appbar/AppbarActions.tsx +13 -0
  12. package/components/Appbar/AppbarBase.tsx +60 -0
  13. package/components/Appbar/AppbarCenterAligned.tsx +13 -0
  14. package/components/Appbar/AppbarLarge.tsx +13 -0
  15. package/components/Appbar/AppbarLeft.tsx +26 -0
  16. package/components/Appbar/AppbarMedium.tsx +13 -0
  17. package/components/Appbar/AppbarRight.tsx +40 -0
  18. package/components/Appbar/AppbarSmall.tsx +13 -0
  19. package/components/Appbar/AppbarTitle.tsx +49 -0
  20. package/components/Appbar/index.tsx +46 -0
  21. package/components/Appbar/types.ts +19 -0
  22. package/components/Appbar/utils.ts +127 -0
  23. package/components/Avatar/Avatar.tsx +189 -0
  24. package/components/Avatar/index.tsx +11 -0
  25. package/components/Avatar/utils.ts +35 -0
  26. package/components/Backdrop/Backdrop.tsx +18 -0
  27. package/components/Backdrop/index.tsx +11 -0
  28. package/components/Backdrop/types.ts +3 -0
  29. package/components/Backdrop/utils.ts +21 -0
  30. package/components/Badge/Badge.tsx +29 -0
  31. package/components/Badge/index.tsx +11 -0
  32. package/components/Badge/utils.ts +38 -0
  33. package/components/Button/Button.tsx +349 -0
  34. package/components/Button/index.tsx +11 -0
  35. package/components/Button/types.ts +5 -0
  36. package/components/Button/utils.ts +416 -0
  37. package/components/Card/Card.tsx +160 -0
  38. package/components/Card/CardActions.tsx +18 -0
  39. package/components/Card/CardContent.tsx +29 -0
  40. package/components/Card/CardHeader.tsx +31 -0
  41. package/components/Card/CardHeadline.tsx +15 -0
  42. package/components/Card/CardMedia.tsx +36 -0
  43. package/components/Card/CardSubhead.tsx +15 -0
  44. package/components/Card/CardText.tsx +13 -0
  45. package/components/Card/CardTypography.tsx +113 -0
  46. package/components/Card/index.tsx +38 -0
  47. package/components/Card/types.ts +1 -0
  48. package/components/Card/utils.ts +23 -0
  49. package/components/Checkbox/Checkbox.tsx +123 -0
  50. package/components/Checkbox/CheckboxBase.ios.tsx +86 -0
  51. package/components/Checkbox/CheckboxBase.tsx +216 -0
  52. package/components/Checkbox/index.tsx +11 -0
  53. package/components/Checkbox/types.ts +72 -0
  54. package/components/Checkbox/utils.ts +210 -0
  55. package/components/Chip/Chip.tsx +416 -0
  56. package/components/Chip/index.tsx +51 -0
  57. package/components/Chip/utils.ts +100 -0
  58. package/components/DatePickerDocked/DatePickerDocked.tsx +30 -0
  59. package/components/DatePickerDocked/DatePickerDockedHeader.tsx +129 -0
  60. package/components/DatePickerDocked/index.tsx +21 -0
  61. package/components/DatePickerDocked/types.ts +11 -0
  62. package/components/DatePickerDocked/utils.ts +155 -0
  63. package/components/DatePickerInline/AutoSizer.tsx +46 -0
  64. package/components/DatePickerInline/DatePickerContext.tsx +21 -0
  65. package/components/DatePickerInline/DatePickerInline.tsx +82 -0
  66. package/components/DatePickerInline/DatePickerInlineBase.tsx +181 -0
  67. package/components/DatePickerInline/DatePickerInlineHeader.tsx +108 -0
  68. package/components/DatePickerInline/Day.tsx +88 -0
  69. package/components/DatePickerInline/DayName.tsx +17 -0
  70. package/components/DatePickerInline/DayNames.tsx +32 -0
  71. package/components/DatePickerInline/DayRange.tsx +48 -0
  72. package/components/DatePickerInline/HeaderItem.tsx +111 -0
  73. package/components/DatePickerInline/Month.tsx +233 -0
  74. package/components/DatePickerInline/MonthPicker.tsx +174 -0
  75. package/components/DatePickerInline/Swiper.native.tsx +172 -0
  76. package/components/DatePickerInline/Swiper.tsx +172 -0
  77. package/components/DatePickerInline/SwiperUtils.ts +40 -0
  78. package/components/DatePickerInline/Week.tsx +67 -0
  79. package/components/DatePickerInline/YearPicker.tsx +139 -0
  80. package/components/DatePickerInline/dateUtils.tsx +334 -0
  81. package/components/DatePickerInline/index.tsx +41 -0
  82. package/components/DatePickerInline/types.ts +104 -0
  83. package/components/DatePickerInline/utils.ts +367 -0
  84. package/components/DatePickerInput/DatePickerInput.tsx +139 -0
  85. package/components/DatePickerInput/DatePickerInputModal.tsx +48 -0
  86. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +77 -0
  87. package/components/DatePickerInput/DateRangeInput.tsx +88 -0
  88. package/components/DatePickerInput/index.tsx +14 -0
  89. package/components/DatePickerInput/inputUtils.ts +138 -0
  90. package/components/DatePickerInput/types.ts +28 -0
  91. package/components/DatePickerInput/utils.ts +16 -0
  92. package/components/DatePickerModal/AnimatedCrossView.tsx +94 -0
  93. package/components/DatePickerModal/CalendarEdit.tsx +139 -0
  94. package/components/DatePickerModal/DatePickerModal.tsx +85 -0
  95. package/components/DatePickerModal/DatePickerModalContent.tsx +155 -0
  96. package/components/DatePickerModal/DatePickerModalContentHeader.tsx +213 -0
  97. package/components/DatePickerModal/DatePickerModalHeader.tsx +74 -0
  98. package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +13 -0
  99. package/components/DatePickerModal/index.tsx +20 -0
  100. package/components/DatePickerModal/types.ts +92 -0
  101. package/components/DatePickerModal/utils.ts +121 -0
  102. package/components/DateTimePicker/DateTimePicker.tsx +172 -0
  103. package/components/DateTimePicker/index.tsx +14 -0
  104. package/components/DateTimePicker/utils.ts +13 -0
  105. package/components/Dialog/Dialog.tsx +66 -0
  106. package/components/Dialog/DialogActions.tsx +89 -0
  107. package/components/Dialog/DialogContent.tsx +37 -0
  108. package/components/Dialog/DialogIcon.tsx +69 -0
  109. package/components/Dialog/DialogTitle.tsx +68 -0
  110. package/components/Dialog/index.tsx +37 -0
  111. package/components/Dialog/utils.ts +80 -0
  112. package/components/Drawer/Collapsible/DrawerCollapsible.tsx +22 -0
  113. package/components/Drawer/Collapsible/DrawerCollapsibleItem.tsx +38 -0
  114. package/components/Drawer/Collapsible/DrawerCollapsibleItemContent.tsx +16 -0
  115. package/components/Drawer/Collapsible/DrawerCollapsibleItemHeader.tsx +48 -0
  116. package/components/Drawer/Collapsible/index.tsx +23 -0
  117. package/components/Drawer/Collapsible/utils.ts +49 -0
  118. package/components/Drawer/Drawer.tsx +43 -0
  119. package/components/Drawer/DrawerContent.tsx +35 -0
  120. package/components/Drawer/DrawerFooter.tsx +27 -0
  121. package/components/Drawer/DrawerHeader.tsx +27 -0
  122. package/components/Drawer/DrawerItem.tsx +206 -0
  123. package/components/Drawer/DrawerItemGroup.tsx +82 -0
  124. package/components/Drawer/index.tsx +47 -0
  125. package/components/Drawer/types.ts +3 -0
  126. package/components/Drawer/utils.ts +8 -0
  127. package/components/ElementGroup/ElementGroup.tsx +139 -0
  128. package/components/ElementGroup/index.tsx +11 -0
  129. package/components/ElementGroup/utils.ts +25 -0
  130. package/components/FAB/FAB.tsx +176 -0
  131. package/components/FAB/index.tsx +12 -0
  132. package/components/FAB/types.ts +1 -0
  133. package/components/FAB/utils.ts +221 -0
  134. package/components/FilePicker/FilePicker.tsx +133 -0
  135. package/components/FilePicker/index.tsx +11 -0
  136. package/components/FilePicker/utils.ts +13 -0
  137. package/components/HelperText/HelperText.tsx +139 -0
  138. package/components/HelperText/index.tsx +11 -0
  139. package/components/HelperText/utils.ts +29 -0
  140. package/components/HorizontalDivider/HorizontalDivider.tsx +101 -0
  141. package/components/HorizontalDivider/index.tsx +13 -0
  142. package/components/Icon/CrossFadeIcon.tsx +116 -0
  143. package/components/Icon/Icon.tsx +41 -0
  144. package/components/Icon/iconFactory.tsx +23 -0
  145. package/components/Icon/index.tsx +11 -0
  146. package/components/Icon/types.ts +35 -0
  147. package/components/IconButton/IconButton.tsx +218 -0
  148. package/components/IconButton/index.tsx +11 -0
  149. package/components/IconButton/types.ts +1 -0
  150. package/components/IconButton/utils.ts +325 -0
  151. package/components/If/index.tsx +13 -0
  152. package/components/InputAddon/InputAddon.tsx +27 -0
  153. package/components/InputAddon/index.tsx +11 -0
  154. package/components/InputAddon/utils.ts +33 -0
  155. package/components/Link/Link.tsx +48 -0
  156. package/components/Link/index.tsx +11 -0
  157. package/components/Link/utils.ts +37 -0
  158. package/components/ListItem/ListItem.tsx +136 -0
  159. package/components/ListItem/ListItemDescription.tsx +25 -0
  160. package/components/ListItem/ListItemTitle.tsx +25 -0
  161. package/components/ListItem/index.tsx +18 -0
  162. package/components/ListItem/utils.ts +113 -0
  163. package/components/Menu/Menu.tsx +69 -0
  164. package/components/Menu/MenuDivider.tsx +13 -0
  165. package/components/Menu/MenuItem.tsx +128 -0
  166. package/components/Menu/index.tsx +19 -0
  167. package/components/Menu/utils.ts +92 -0
  168. package/components/Modal/Modal.tsx +261 -0
  169. package/components/Modal/index.tsx +11 -0
  170. package/components/Modal/utils.ts +45 -0
  171. package/components/NavigationRail/NavigationRail.tsx +32 -0
  172. package/components/NavigationRail/NavigationRailContent.tsx +25 -0
  173. package/components/NavigationRail/NavigationRailFooter.tsx +18 -0
  174. package/components/NavigationRail/NavigationRailHeader.tsx +18 -0
  175. package/components/NavigationRail/NavigationRailItem.tsx +226 -0
  176. package/components/NavigationRail/index.tsx +35 -0
  177. package/components/NavigationRail/utils.ts +170 -0
  178. package/components/NavigationStack/NavigationStack.tsx +85 -0
  179. package/components/NavigationStack/NavigationStackItem.tsx +60 -0
  180. package/components/NavigationStack/index.tsx +20 -0
  181. package/components/NavigationStack/utils.tsx +16 -0
  182. package/components/Popover/Popover.native.tsx +185 -0
  183. package/components/Popover/Popover.tsx +198 -0
  184. package/components/Popover/common.ts +459 -0
  185. package/components/Popover/index.ts +2 -0
  186. package/components/Portal/Portal.tsx +13 -0
  187. package/components/Portal/index.tsx +12 -0
  188. package/components/RadioButton/RadioButton.tsx +138 -0
  189. package/components/RadioButton/RadioButtonAndroid.tsx +188 -0
  190. package/components/RadioButton/RadioButtonGroup.tsx +98 -0
  191. package/components/RadioButton/RadioButtonIOS.tsx +106 -0
  192. package/components/RadioButton/RadioButtonItem.tsx +232 -0
  193. package/components/RadioButton/index.ts +27 -0
  194. package/components/RadioButton/utils.ts +164 -0
  195. package/components/Rating/Rating.tsx +149 -0
  196. package/components/Rating/RatingItem.tsx +125 -0
  197. package/components/Rating/index.tsx +13 -0
  198. package/components/Rating/utils.ts +38 -0
  199. package/components/Select/Select.tsx +1038 -0
  200. package/components/Select/index.ts +14 -0
  201. package/components/Select/types.ts +115 -0
  202. package/components/StateLayer/StateLayer.tsx +12 -0
  203. package/components/StateLayer/index.tsx +11 -0
  204. package/components/StateLayer/utils.ts +17 -0
  205. package/components/Surface/BackgroundContextWrapper.tsx +27 -0
  206. package/components/Surface/Surface.android.tsx +62 -0
  207. package/components/Surface/Surface.ios.tsx +123 -0
  208. package/components/Surface/Surface.tsx +48 -0
  209. package/components/Surface/index.tsx +12 -0
  210. package/components/Surface/utils.ts +106 -0
  211. package/components/Switch/Switch.ios.tsx +67 -0
  212. package/components/Switch/Switch.tsx +278 -0
  213. package/components/Switch/index.tsx +11 -0
  214. package/components/Switch/utils.ts +283 -0
  215. package/components/Tabs/TabItem.tsx +150 -0
  216. package/components/Tabs/TabLabel.tsx +84 -0
  217. package/components/Tabs/Tabs.tsx +398 -0
  218. package/components/Tabs/index.tsx +21 -0
  219. package/components/Tabs/utils.ts +126 -0
  220. package/components/Text/Text.tsx +23 -0
  221. package/components/Text/index.tsx +2 -0
  222. package/components/Text/textFactory.tsx +33 -0
  223. package/components/TextInput/InputLabel.tsx +181 -0
  224. package/components/TextInput/TextInput.tsx +693 -0
  225. package/components/TextInput/index.tsx +16 -0
  226. package/components/TextInput/types.ts +96 -0
  227. package/components/TextInput/utils.ts +544 -0
  228. package/components/TextInputWithMask/TextInputMask.tsx +57 -0
  229. package/components/TextInputWithMask/index.tsx +11 -0
  230. package/components/TextInputWithMask/utils.ts +56 -0
  231. package/components/TimePicker/AmPmSwitcher.tsx +99 -0
  232. package/components/TimePicker/AnalogClock.tsx +165 -0
  233. package/components/TimePicker/AnalogClockHours.tsx +163 -0
  234. package/components/TimePicker/AnalogClockMinutes.tsx +68 -0
  235. package/components/TimePicker/AnimatedClockSwitcher.tsx +72 -0
  236. package/components/TimePicker/DisplayModeContext.tsx +6 -0
  237. package/components/TimePicker/TimeInput.tsx +112 -0
  238. package/components/TimePicker/TimeInputs.tsx +148 -0
  239. package/components/TimePicker/TimePicker.tsx +130 -0
  240. package/components/TimePicker/index.tsx +19 -0
  241. package/components/TimePicker/timeUtils.ts +159 -0
  242. package/components/TimePicker/utils.ts +285 -0
  243. package/components/TimePickerField/TimePickerField.tsx +152 -0
  244. package/components/TimePickerField/index.tsx +14 -0
  245. package/components/TimePickerField/sanitizeTime.ts +85 -0
  246. package/components/TimePickerField/utils.ts +95 -0
  247. package/components/TimePickerModal/TimePickerModal.tsx +115 -0
  248. package/components/TimePickerModal/index.tsx +14 -0
  249. package/components/TimePickerModal/utils.ts +48 -0
  250. package/components/Tooltip/Tooltip.tsx +137 -0
  251. package/components/Tooltip/TooltipContent.tsx +12 -0
  252. package/components/Tooltip/TooltipTrigger.tsx +94 -0
  253. package/components/Tooltip/index.tsx +20 -0
  254. package/components/Tooltip/utils.ts +21 -0
  255. package/components/TouchableRipple/TouchableRipple.native.tsx +105 -0
  256. package/components/TouchableRipple/TouchableRipple.tsx +286 -0
  257. package/components/TouchableRipple/index.tsx +14 -0
  258. package/components/TouchableRipple/utils.ts +15 -0
  259. package/components/VerticalDivider/VerticalDivider.tsx +99 -0
  260. package/components/VerticalDivider/index.tsx +13 -0
  261. package/context-bridge/index.tsx +87 -0
  262. package/core/componentsRegistry.ts +164 -0
  263. package/core/index.tsx +2 -0
  264. package/fast-context/index.tsx +190 -0
  265. package/hocs/index.tsx +5 -0
  266. package/hocs/typedMemo.tsx +5 -0
  267. package/hocs/withKeyboardAccessibility.tsx +231 -0
  268. package/hocs/withPortal.tsx +16 -0
  269. package/hooks/createPsuedoHook.tsx +50 -0
  270. package/hooks/index.tsx +29 -0
  271. package/hooks/useActionState.native.tsx +22 -0
  272. package/hooks/useActionState.tsx +34 -0
  273. package/hooks/useActive.tsx +5 -0
  274. package/hooks/useBreakpoints.tsx +7 -0
  275. package/hooks/useColorMode.tsx +17 -0
  276. package/hooks/useContrastColor.ts +15 -0
  277. package/hooks/useControlledValue.tsx +68 -0
  278. package/hooks/useFilePicker.tsx +48 -0
  279. package/hooks/useFocus.tsx +5 -0
  280. package/hooks/useHandleNumberFormat.tsx +106 -0
  281. package/hooks/useHover.tsx +5 -0
  282. package/hooks/useKeyboardDismissable.ts +66 -0
  283. package/hooks/useLatest.tsx +9 -0
  284. package/hooks/useMediaQuery.tsx +64 -0
  285. package/hooks/useMergedRefs.ts +14 -0
  286. package/hooks/usePrevious.ts +13 -0
  287. package/hooks/useQueryFilter.tsx +35 -0
  288. package/hooks/useSearchable.tsx +74 -0
  289. package/hooks/useSubcomponents.tsx +59 -0
  290. package/hooks/useTheme.ts +3 -0
  291. package/hooks/useToggle.tsx +24 -0
  292. package/package.json +114 -0
  293. package/shortcuts-manager/EventsManager.tsx +121 -0
  294. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.native.tsx +9 -0
  295. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +58 -0
  296. package/shortcuts-manager/ShortcutsManager/index.tsx +2 -0
  297. package/shortcuts-manager/ShortcutsManager/utils.tsx +30 -0
  298. package/shortcuts-manager/index.tsx +6 -0
  299. package/shortcuts-manager/types.ts +24 -0
  300. package/shortcuts-manager/useIsKeyPressed/index.tsx +1 -0
  301. package/shortcuts-manager/useIsKeyPressed/useIsKeyPress.tsx +9 -0
  302. package/shortcuts-manager/useSetScopes/index.tsx +1 -0
  303. package/shortcuts-manager/useSetScopes/useSetScopes.native.tsx +9 -0
  304. package/shortcuts-manager/useSetScopes/useSetScopes.tsx +25 -0
  305. package/shortcuts-manager/useShortcut/index.tsx +2 -0
  306. package/shortcuts-manager/useShortcut/types.ts +3 -0
  307. package/shortcuts-manager/useShortcut/useShortcut.native.tsx +9 -0
  308. package/shortcuts-manager/useShortcut/useShortcut.tsx +61 -0
  309. package/shortcuts-manager/utils.ts +105 -0
  310. package/styles/index.ts +4 -0
  311. package/styles/overlay.ts +69 -0
  312. package/styles/shadow.ts +21 -0
  313. package/styles/themes/DarkTheme.tsx +98 -0
  314. package/styles/themes/LightTheme.tsx +212 -0
  315. package/styles/themes/tokens.ts +248 -0
  316. package/styles/utils.ts +11 -0
  317. package/types/index.ts +129 -0
  318. package/types/theme.ts +159 -0
  319. package/utils/DocumentPicker/documentPicker.native.ts +10 -0
  320. package/utils/DocumentPicker/documentPicker.ts +76 -0
  321. package/utils/DocumentPicker/index.ts +2 -0
  322. package/utils/DocumentPicker/types.ts +28 -0
  323. package/utils/addEventListener.tsx +51 -0
  324. package/utils/backgroundContext.ts +9 -0
  325. package/utils/color.ts +22 -0
  326. package/utils/compare/index.ts +54 -0
  327. package/utils/composeEventHandlers.ts +9 -0
  328. package/utils/createNumberMask/createNumberMask.ts +98 -0
  329. package/utils/createNumberMask/index.ts +5 -0
  330. package/utils/createSyntheticEvent.ts +31 -0
  331. package/utils/date-fns.ts +7 -0
  332. package/utils/dateTimePicker.ts +5 -0
  333. package/utils/escapeRegex.ts +9 -0
  334. package/utils/extractTextStyles.ts +52 -0
  335. package/utils/formatNumberWithMask/formatNumberWithMask.ts +26 -0
  336. package/utils/formatNumberWithMask/formatWithMask.ts +119 -0
  337. package/utils/formatNumberWithMask/index.ts +6 -0
  338. package/utils/getCursorStyle/getCursorStyle.native.ts +1 -0
  339. package/utils/getCursorStyle/getCursorStyle.ts +1 -0
  340. package/utils/getCursorStyle/index.ts +1 -0
  341. package/utils/getOS/getOS.native.ts +7 -0
  342. package/utils/getOS/getOS.ts +24 -0
  343. package/utils/getOS/index.ts +1 -0
  344. package/utils/getyearRange.ts +5 -0
  345. package/utils/index.ts +20 -0
  346. package/utils/lodash.ts +50 -0
  347. package/utils/mergeRefs.ts +13 -0
  348. package/utils/normalizeBorderRadiuses.ts +24 -0
  349. package/utils/normalizeSpacings.ts +110 -0
  350. package/utils/normalizeToNumberString/index.ts +4 -0
  351. package/utils/normalizeToNumberString/normalizeToNumberString.ts +48 -0
  352. package/utils/repository.ts +103 -0
  353. package/utils/resolveColorMode.ts +9 -0
  354. package/utils/resolveStateVariant.ts +26 -0
  355. package/utils/tokenStylesParser.ts +7 -0
@@ -0,0 +1,1038 @@
1
+ import {
2
+ createContext,
3
+ memo,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import {
12
+ type AccessibilityRole,
13
+ type GestureResponderEvent,
14
+ type LayoutChangeEvent,
15
+ Platform,
16
+ Pressable,
17
+ ScrollView,
18
+ View,
19
+ } from 'react-native';
20
+ import { StyleSheet } from 'react-native-unistyles';
21
+
22
+ import { useActionState, useControlledValue } from '../../hooks';
23
+ import { useToggle } from '../../hooks';
24
+ import { resolveStateVariant } from '../../utils';
25
+ import { Chip } from '../Chip';
26
+ import { Icon } from '../Icon';
27
+ import { IconButton } from '../IconButton';
28
+ import { Popover } from '../Popover';
29
+ import { registerPortalContext } from '../Portal';
30
+ import { Text } from '../Text';
31
+ import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
32
+ import type {
33
+ DefaultItemT,
34
+ SelectContentProps,
35
+ SelectContextValue,
36
+ SelectDropdownContextValue,
37
+ SelectDropdownProps,
38
+ SelectGroupProps,
39
+ SelectOptionProps,
40
+ SelectProviderProps,
41
+ SelectSearchInputProps,
42
+ SelectTriggerProps,
43
+ SelectValueProps,
44
+ } from './types';
45
+
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
+ };
71
+
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
+ });
89
+
90
+ registerPortalContext([SelectContext, SelectDropdownContext]);
91
+
92
+ export const useSelectDropdownContext = () => {
93
+ return useContext(SelectDropdownContext);
94
+ };
95
+
96
+ export const useSelectDropdownContextValue = <T,>(
97
+ selector: (state: SelectDropdownContextType) => T,
98
+ ): T => {
99
+ const context = useContext(SelectDropdownContext);
100
+ return selector(context);
101
+ };
102
+
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
+ });
125
+
126
+ const [searchQuery, setSearchQuery] = useState('');
127
+
128
+ const handleSearchQueryChange = useCallback(
129
+ (query: string) => {
130
+ setSearchQuery(query);
131
+ onSearchChange?.(query);
132
+ },
133
+ [onSearchChange],
134
+ );
135
+
136
+ // Default hideSelected to multiple (true for multi-select, false for single select)
137
+ const hideSelected = hideSelectedProp !== undefined ? hideSelectedProp : multiple;
138
+
139
+ const filteredOptions = useMemo(() => {
140
+ let result = options;
141
+
142
+ // Filter out selected items if hideSelected is true
143
+ if (hideSelected) {
144
+ result = result.filter(item => {
145
+ if (multiple) {
146
+ const values = (value as Option['id'][]) || [];
147
+ return !values.some(v => v === item.id);
148
+ } else {
149
+ const singleValue = value as Option['id'] | null;
150
+ return singleValue !== item.id;
151
+ }
152
+ });
153
+ }
154
+
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
+ }
166
+
167
+ return result;
168
+ }, [options, searchQuery, searchKey, labelKey, hideSelected, multiple, value]);
169
+
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
+ }
177
+ } else {
178
+ onValueChange(item.id, item);
179
+ }
180
+ },
181
+ [multiple, value, onValueChange],
182
+ );
183
+
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);
191
+ }
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
+ );
276
+
277
+ return (
278
+ <SelectDropdownContext.Provider value={contextValue}>
279
+ {children}
280
+ </SelectDropdownContext.Provider>
281
+ );
282
+ };
283
+
284
+ // 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
+ };
295
+
296
+ // Select.Trigger - opens the dropdown
297
+ const SelectTrigger = ({ children, style, ...rest }: SelectTriggerProps) => {
298
+ const { onOpen, isOpen, triggerRef, setTriggerLayout } = useSelectDropdownContextValue(
299
+ state => ({
300
+ onOpen: state.onOpen,
301
+ isOpen: state.isOpen,
302
+ triggerRef: state.triggerRef,
303
+ setTriggerLayout: state.setTriggerLayout,
304
+ }),
305
+ );
306
+
307
+ const { disabled, error } = useSelectContextValue(state => ({
308
+ disabled: state.disabled,
309
+ error: state.error,
310
+ }));
311
+
312
+ const { hovered } = useActionState({ ref: triggerRef, actionsToListen: ['hover'] });
313
+
314
+ triggerStyles.useVariants({
315
+ state: resolveStateVariant({
316
+ focused: isOpen,
317
+ hovered,
318
+ disabled: !!disabled,
319
+ error: !!error,
320
+ hoveredAndFocused: hovered && isOpen,
321
+ errorFocused: !!error && isOpen,
322
+ errorHovered: !!error && hovered,
323
+ errorFocusedAndHovered: !!error && isOpen && hovered,
324
+ errorDisabled: !!error && !!disabled,
325
+ }) as any,
326
+ });
327
+
328
+ const handleLayout = useCallback(
329
+ (event: LayoutChangeEvent) => {
330
+ const { width, height } = event.nativeEvent.layout;
331
+ setTriggerLayout({ width, height });
332
+ },
333
+ [setTriggerLayout],
334
+ );
335
+
336
+ const handlePress = useCallback(() => {
337
+ if (!isOpen && !disabled) {
338
+ onOpen();
339
+ }
340
+ }, [isOpen, onOpen, disabled]);
341
+
342
+ return (
343
+ <Pressable
344
+ ref={triggerRef}
345
+ onPress={handlePress}
346
+ onLayout={handleLayout}
347
+ style={[triggerStyles.trigger, style]}
348
+ accessibilityRole="combobox"
349
+ accessibilityState={{ expanded: isOpen, disabled: !!disabled }}
350
+ disabled={disabled}
351
+ {...rest}>
352
+ {children}
353
+ <Icon
354
+ name={isOpen ? 'chevron-up' : 'chevron-down'}
355
+ size={20}
356
+ style={triggerStyles.triggerIcon}
357
+ />
358
+ <View style={triggerStyles.outline} />
359
+ </Pressable>
360
+ );
361
+ };
362
+
363
+ SelectTrigger.displayName = 'Select_Trigger';
364
+
365
+ // Select.Value - displays the value
366
+ const SelectValue = ({ placeholder, renderValue, style, ...rest }: SelectValueProps) => {
367
+ const { value, multiple, labelKey, onRemove, options } = useSelectContextValue(state => ({
368
+ value: state.value,
369
+ multiple: state.multiple,
370
+ labelKey: state.labelKey,
371
+ onRemove: state.onRemove,
372
+ options: state.options,
373
+ }));
374
+
375
+ const resolvedValue = useMemo(() => {
376
+ const resolve = (item: any) => {
377
+ if (item === null || item === undefined) return null;
378
+ const id = typeof item === 'object' ? item.id : item;
379
+ const found = options.find(o => o.id === id);
380
+ return found || item;
381
+ };
382
+
383
+ if (multiple) {
384
+ return (Array.isArray(value) ? value : []).map(resolve).filter(Boolean);
385
+ }
386
+ return resolve(value);
387
+ }, [value, multiple, options]);
388
+
389
+ const displayValue = useMemo(() => {
390
+ if (!resolvedValue) return placeholder || '';
391
+ if (multiple && (resolvedValue as any[]).length === 0) return placeholder || '';
392
+
393
+ if (renderValue) {
394
+ return renderValue(resolvedValue as any);
395
+ }
396
+
397
+ if (multiple) {
398
+ const values = resolvedValue as DefaultItemT[];
399
+ // For multi-select, show chips
400
+ return values.map(item => item[labelKey || 'label'] || String(item.id)).join(', ');
401
+ } else {
402
+ const singleValue = resolvedValue as DefaultItemT;
403
+ return singleValue[labelKey || 'label'] || String(singleValue.id || singleValue);
404
+ }
405
+ }, [resolvedValue, multiple, labelKey, placeholder, renderValue]);
406
+
407
+ if (multiple && Array.isArray(resolvedValue) && resolvedValue.length > 0) {
408
+ // Render chips for multi-select
409
+ return (
410
+ <View style={[styles.chipContainer, style]} {...rest}>
411
+ {(resolvedValue as DefaultItemT[]).map(item => (
412
+ <Chip.Input
413
+ 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)}
419
+ />
420
+ ))}
421
+ </View>
422
+ );
423
+ }
424
+
425
+ return (
426
+ <Text style={style} {...rest}>
427
+ {displayValue}
428
+ </Text>
429
+ );
430
+ };
431
+
432
+ SelectValue.displayName = 'Select_Value';
433
+
434
+ // 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
+ }));
449
+
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]);
457
+
458
+ if (!triggerLayout) return null;
459
+
460
+ if (WrapperComponent) {
461
+ return (
462
+ <WrapperComponent isOpen={isOpen} onClose={onClose} {...wrapperComponentProps}>
463
+ {enableKeyboardNavigation && Platform.OS === 'web' ? (
464
+ <KeyboardNavigationWrapper>{children}</KeyboardNavigationWrapper>
465
+ ) : (
466
+ children
467
+ )}
468
+ </WrapperComponent>
469
+ );
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
+ };
488
+
489
+ // Keyboard navigation wrapper for web
490
+ const KeyboardNavigationWrapper = ({ children }: { children: React.ReactNode }) => {
491
+ const { onClose, contentRef, isOpen } = useSelectDropdownContextValue(state => ({
492
+ onClose: state.onClose,
493
+ contentRef: state.contentRef,
494
+ isOpen: state.isOpen,
495
+ }));
496
+
497
+ const handleKeyDown = useCallback(
498
+ (e: globalThis.KeyboardEvent) => {
499
+ if (!contentRef?.current) return;
500
+
501
+ // Find all focusable options
502
+ // We assume options have role="option" and are descendants of the contentRef
503
+ // On React Native Web, refs often point to the host node (div)
504
+ const container = contentRef.current as HTMLElement;
505
+ if (!container || !container.querySelectorAll) return;
506
+
507
+ const options = Array.from(
508
+ container.querySelectorAll('[role="option"]:not([disabled])'),
509
+ ) as HTMLElement[];
510
+
511
+ if (options.length === 0) return;
512
+
513
+ const currentIndex = options.findIndex(el => el === document.activeElement);
514
+
515
+ switch (e.key) {
516
+ case 'ArrowDown':
517
+ e.preventDefault();
518
+ if (currentIndex === -1) {
519
+ options[0]?.focus();
520
+ } else {
521
+ const nextIndex = (currentIndex + 1) % options.length;
522
+ options[nextIndex]?.focus();
523
+ }
524
+ break;
525
+ case 'ArrowUp':
526
+ e.preventDefault();
527
+ if (currentIndex === -1) {
528
+ options[options.length - 1]?.focus();
529
+ } else {
530
+ const prevIndex = (currentIndex - 1 + options.length) % options.length;
531
+ options[prevIndex]?.focus();
532
+ }
533
+ break;
534
+ case 'Enter':
535
+ e.preventDefault();
536
+ if (currentIndex !== -1) {
537
+ options[currentIndex]?.click();
538
+ }
539
+ break;
540
+ case 'Escape':
541
+ e.preventDefault();
542
+ onClose();
543
+ // Return focus to trigger? This should be handled by the caller/Popover usually.
544
+ break;
545
+ }
546
+ },
547
+ [contentRef, onClose],
548
+ );
549
+
550
+ useEffect(() => {
551
+ if (Platform.OS === 'web') {
552
+ const controller = new AbortController();
553
+ // We attach listener to the window or the container?
554
+ // If we attach to container, it needs focus to receive keys.
555
+ // Popovers usually trap focus.
556
+ // Let's attach to window to be safe, but only when open (which this component implies).
557
+ // Actually, best practice is to attach to the container if it captures focus.
558
+ // But SelectDropdown usually renders in a Portal.
559
+ // Let's attach to window but check if the event target is inside our content.
560
+ // Or rely on the fact that if an option is focused, the keydown bubbles up.
561
+ // If nothing is focused, where do keys go? Body.
562
+ const listener = (e: KeyboardEvent) => {
563
+ // Only handle navigation keys when dropdown is open
564
+ if (!isOpen) return;
565
+
566
+ // For arrow keys, Enter, and Escape, allow navigation regardless of focus location
567
+ // This ensures keyboard navigation works even when focus is still on the trigger
568
+ const isNavigationKey = ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key);
569
+
570
+ if (isNavigationKey) {
571
+ handleKeyDown(e);
572
+ return;
573
+ }
574
+
575
+ // For other keys, only handle if focus is within the dropdown
576
+ const contentEl = contentRef?.current as HTMLElement | null;
577
+ const dropdownContainer = contentEl?.parentElement ?? contentEl;
578
+ const targetNode = e.target as Node;
579
+
580
+ const isWithinDropdown =
581
+ !!dropdownContainer &&
582
+ (dropdownContainer === targetNode || dropdownContainer.contains(targetNode));
583
+
584
+ if (isWithinDropdown || e.target === document.body) {
585
+ handleKeyDown(e);
586
+ }
587
+ };
588
+
589
+ window.addEventListener('keydown', listener, {
590
+ capture: true,
591
+ signal: controller.signal,
592
+ });
593
+
594
+ return () => {
595
+ controller.abort();
596
+ };
597
+ }
598
+ return undefined;
599
+ }, [handleKeyDown, contentRef, isOpen]);
600
+
601
+ return <>{children}</>;
602
+ };
603
+
604
+ SelectDropdown.displayName = 'Select_Dropdown';
605
+
606
+ // 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
+ );
627
+
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;
633
+
634
+ return children(option, !!isSelected);
635
+ });
636
+ }, [filteredOptions, value, multiple, children]);
637
+
638
+ const defaultEmptyState = useMemo(() => {
639
+ const hasSearchQuery = searchQuery && searchQuery.trim().length > 0;
640
+ const hasNoOptions = options.length === 0;
641
+
642
+ if (hasNoOptions) {
643
+ return (
644
+ <View style={styles.emptyState}>
645
+ <Text style={styles.emptyStateText}>No options available</Text>
646
+ </View>
647
+ );
648
+ }
649
+
650
+ if (hasSearchQuery) {
651
+ return (
652
+ <View style={styles.emptyState}>
653
+ <Text style={styles.emptyStateText}>No results found</Text>
654
+ </View>
655
+ );
656
+ }
657
+
658
+ return (
659
+ <View style={styles.emptyState}>
660
+ <Text style={styles.emptyStateText}>No options</Text>
661
+ </View>
662
+ );
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
+ };
671
+
672
+ SelectContent.displayName = 'Select_Content';
673
+
674
+ // Select.Group - groups items with label
675
+ const SelectGroup = memo(({ children, label, style, ...rest }: SelectGroupProps) => {
676
+ return (
677
+ <View style={style} {...rest}>
678
+ {label && <Text style={styles.groupLabel}>{label}</Text>}
679
+ {children}
680
+ </View>
681
+ );
682
+ });
683
+
684
+ SelectGroup.displayName = 'Select_Group';
685
+
686
+ // Select.Item - select item that uses context
687
+ const SelectOption = memo(
688
+ <Option extends DefaultItemT = DefaultItemT>({
689
+ value,
690
+ children,
691
+ renderItem,
692
+ onPress,
693
+ style,
694
+ disabled: optionDisabledProp = false,
695
+ ...rest
696
+ }: SelectOptionProps<Option>) => {
697
+ const {
698
+ value: selectionValue,
699
+ multiple,
700
+ onAdd,
701
+ onRemove,
702
+ disabled: selectDisabled,
703
+ } = useSelectContextValue<Option>(state => ({
704
+ value: state.value,
705
+ multiple: state.multiple,
706
+ onAdd: state.onAdd,
707
+ onRemove: state.onRemove,
708
+ disabled: state.disabled,
709
+ }));
710
+
711
+ const { onClose } = useSelectDropdownContextValue(state => ({
712
+ onClose: state.onClose,
713
+ }));
714
+
715
+ const option = useMemo(() => {
716
+ return {
717
+ id: value,
718
+ ...(typeof children === 'string' ? { label: children } : {}),
719
+ ...(optionDisabledProp ? { selectable: false } : {}),
720
+ } as Option;
721
+ }, [children, optionDisabledProp, value]);
722
+
723
+ const isSelected = useMemo(() => {
724
+ if (multiple) {
725
+ const values = selectionValue as any[];
726
+ return values?.some(v => (v?.id ?? v) === option.id) || false;
727
+ } else {
728
+ const singleValue = selectionValue as any;
729
+ return (singleValue?.id ?? singleValue) === option.id || false;
730
+ }
731
+ }, [selectionValue, multiple, option.id]);
732
+
733
+ const isOptionDisabled = Boolean(
734
+ selectDisabled || optionDisabledProp || option.selectable === false,
735
+ );
736
+
737
+ const handlePress = useCallback(
738
+ (event: GestureResponderEvent) => {
739
+ if (isOptionDisabled) return;
740
+
741
+ if (onPress) {
742
+ onPress(option, event);
743
+ }
744
+
745
+ if (isSelected) {
746
+ onRemove(option);
747
+ } else {
748
+ onAdd(option);
749
+ }
750
+
751
+ // Close dropdown for single select
752
+ if (!multiple) {
753
+ onClose();
754
+ }
755
+ },
756
+ [isOptionDisabled, option, isSelected, onPress, onAdd, onRemove, multiple, onClose],
757
+ );
758
+
759
+ const content = useMemo(() => {
760
+ if (typeof children === 'string') {
761
+ return <Text style={isOptionDisabled && styles.itemDisabledText}>{children}</Text>;
762
+ }
763
+
764
+ if (children) return children;
765
+
766
+ return (
767
+ <Text style={isOptionDisabled && styles.itemDisabledText}>
768
+ {option.label || String(option.id)}
769
+ </Text>
770
+ );
771
+ }, [children, option.id, option.label, isOptionDisabled]);
772
+
773
+ const accessibilityProps = {
774
+ accessibilityRole: 'button' as AccessibilityRole, // Fallback for native
775
+ accessibilityState: { selected: isSelected, disabled: isOptionDisabled },
776
+ ...Platform.select({
777
+ web: {
778
+ accessibilityRole: 'option' as AccessibilityRole,
779
+ tabIndex: -1 as 0 | -1 | undefined,
780
+ // Use a dataset attribute to help the keyboard navigator find this
781
+ 'data-option-id': String(option.id),
782
+ },
783
+ }),
784
+ };
785
+
786
+ if (renderItem) {
787
+ return (
788
+ <Pressable
789
+ onPress={handlePress}
790
+ disabled={isOptionDisabled}
791
+ style={[isOptionDisabled && styles.itemDisabled, style]}
792
+ {...accessibilityProps}
793
+ {...rest}>
794
+ {renderItem(option, isSelected)}
795
+ </Pressable>
796
+ );
797
+ }
798
+
799
+ return (
800
+ <Pressable
801
+ onPress={handlePress}
802
+ disabled={isOptionDisabled}
803
+ style={[
804
+ styles.item,
805
+ isSelected && styles.itemSelected,
806
+ isOptionDisabled && styles.itemDisabled,
807
+ style,
808
+ ]}
809
+ {...accessibilityProps}
810
+ {...rest}>
811
+ {content}
812
+ </Pressable>
813
+ );
814
+ },
815
+ );
816
+
817
+ SelectOption.displayName = 'Select_Option';
818
+
819
+ // Select.SearchInput - handles search
820
+ const SelectSearchInput = memo(
821
+ ({ onQueryChange, autoFocus = true, ...textInputProps }: SelectSearchInputProps) => {
822
+ const { searchQuery, setSearchQuery } = useSelectContextValue(state => ({
823
+ searchQuery: state.searchQuery,
824
+ setSearchQuery: state.setSearchQuery,
825
+ }));
826
+ const textInputRef = useRef<TextInputHandles>(null);
827
+
828
+ const handleChangeText = useCallback(
829
+ (text: string) => {
830
+ setSearchQuery(text);
831
+ onQueryChange?.(text);
832
+ textInputProps.onChangeText?.(text);
833
+ },
834
+ [onQueryChange, setSearchQuery, textInputProps],
835
+ );
836
+
837
+ const inputProps = {
838
+ ...textInputProps,
839
+ value: textInputProps.value !== undefined ? textInputProps.value : searchQuery,
840
+ onChangeText: handleChangeText,
841
+ placeholder: textInputProps.placeholder || 'Search...',
842
+ inputStyle: styles.searchInputInput,
843
+ } as TextInputProps;
844
+
845
+ useEffect(() => {
846
+ if (Platform.OS !== 'web') return;
847
+ if (!autoFocus || !textInputRef.current) {
848
+ return;
849
+ }
850
+
851
+ const node = textInputRef.current as TextInputHandles & {
852
+ focus?: (options?: { preventScroll?: boolean }) => void;
853
+ };
854
+
855
+ const focusField = () => {
856
+ try {
857
+ node.focus?.({ preventScroll: true });
858
+ } catch {
859
+ const { scrollX, scrollY } = window;
860
+ node.focus?.();
861
+ window.scrollTo(scrollX, scrollY);
862
+ }
863
+ };
864
+
865
+ // Run after popover layout so positioning is stable before focus.
866
+ requestAnimationFrame(focusField);
867
+ }, [autoFocus]);
868
+
869
+ return (
870
+ <TextInput
871
+ ref={textInputRef}
872
+ autoFocus={Platform.OS !== 'web' && autoFocus}
873
+ 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
+ size="sm"
883
+ variant="outlined"
884
+ {...inputProps}
885
+ />
886
+ );
887
+ },
888
+ );
889
+
890
+ SelectSearchInput.displayName = 'Select_SearchInput';
891
+
892
+ // Attach subcomponents
893
+ const SelectWithSubcomponents = Object.assign(Select, {
894
+ Trigger: SelectTrigger,
895
+ Value: SelectValue,
896
+ Dropdown: SelectDropdown,
897
+ Content: SelectContent,
898
+ Group: SelectGroup,
899
+ Option: SelectOption,
900
+ SearchInput: SelectSearchInput,
901
+ });
902
+
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
+ export default SelectWithSubcomponents;
1038
+ export { SelectDropdownProvider, SelectProvider };