@cdx-ui/primitives 0.0.1-alpha.1

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 (260) hide show
  1. package/README.md +17 -0
  2. package/lib/commonjs/button/context.js +11 -0
  3. package/lib/commonjs/button/context.js.map +1 -0
  4. package/lib/commonjs/button/createButtonGroup.js +80 -0
  5. package/lib/commonjs/button/createButtonGroup.js.map +1 -0
  6. package/lib/commonjs/button/createButtonIcon.js +18 -0
  7. package/lib/commonjs/button/createButtonIcon.js.map +1 -0
  8. package/lib/commonjs/button/createButtonRoot.js +81 -0
  9. package/lib/commonjs/button/createButtonRoot.js.map +1 -0
  10. package/lib/commonjs/button/createButtonSpinner.js +34 -0
  11. package/lib/commonjs/button/createButtonSpinner.js.map +1 -0
  12. package/lib/commonjs/button/createButtonText.js +38 -0
  13. package/lib/commonjs/button/createButtonText.js.map +1 -0
  14. package/lib/commonjs/button/index.js +31 -0
  15. package/lib/commonjs/button/index.js.map +1 -0
  16. package/lib/commonjs/button/types.js +6 -0
  17. package/lib/commonjs/button/types.js.map +1 -0
  18. package/lib/commonjs/index.js +52 -0
  19. package/lib/commonjs/index.js.map +1 -0
  20. package/lib/commonjs/input/context.js +11 -0
  21. package/lib/commonjs/input/context.js.map +1 -0
  22. package/lib/commonjs/input/createInputField.js +90 -0
  23. package/lib/commonjs/input/createInputField.js.map +1 -0
  24. package/lib/commonjs/input/createInputIcon.js +20 -0
  25. package/lib/commonjs/input/createInputIcon.js.map +1 -0
  26. package/lib/commonjs/input/createInputRoot.js +82 -0
  27. package/lib/commonjs/input/createInputRoot.js.map +1 -0
  28. package/lib/commonjs/input/createInputSlot.js +44 -0
  29. package/lib/commonjs/input/createInputSlot.js.map +1 -0
  30. package/lib/commonjs/input/index.js +28 -0
  31. package/lib/commonjs/input/index.js.map +1 -0
  32. package/lib/commonjs/input/types.js +6 -0
  33. package/lib/commonjs/input/types.js.map +1 -0
  34. package/lib/commonjs/overlay/OverlayContainer.js +67 -0
  35. package/lib/commonjs/overlay/OverlayContainer.js.map +1 -0
  36. package/lib/commonjs/overlay/index.js +40 -0
  37. package/lib/commonjs/overlay/index.js.map +1 -0
  38. package/lib/commonjs/overlay/useAnchorPosition.js +68 -0
  39. package/lib/commonjs/overlay/useAnchorPosition.js.map +1 -0
  40. package/lib/commonjs/overlay/useDismissOverlay.js +14 -0
  41. package/lib/commonjs/overlay/useDismissOverlay.js.map +1 -0
  42. package/lib/commonjs/overlay/useDismissOverlay.web.js +46 -0
  43. package/lib/commonjs/overlay/useDismissOverlay.web.js.map +1 -0
  44. package/lib/commonjs/overlay/useOverlayPosition.js +93 -0
  45. package/lib/commonjs/overlay/useOverlayPosition.js.map +1 -0
  46. package/lib/commonjs/package.json +1 -0
  47. package/lib/commonjs/select/context.js +63 -0
  48. package/lib/commonjs/select/context.js.map +1 -0
  49. package/lib/commonjs/select/createSelectContent.js +102 -0
  50. package/lib/commonjs/select/createSelectContent.js.map +1 -0
  51. package/lib/commonjs/select/createSelectIcon.js +41 -0
  52. package/lib/commonjs/select/createSelectIcon.js.map +1 -0
  53. package/lib/commonjs/select/createSelectItem.js +101 -0
  54. package/lib/commonjs/select/createSelectItem.js.map +1 -0
  55. package/lib/commonjs/select/createSelectItemLabel.js +41 -0
  56. package/lib/commonjs/select/createSelectItemLabel.js.map +1 -0
  57. package/lib/commonjs/select/createSelectRoot.js +99 -0
  58. package/lib/commonjs/select/createSelectRoot.js.map +1 -0
  59. package/lib/commonjs/select/createSelectTrigger.js +138 -0
  60. package/lib/commonjs/select/createSelectTrigger.js.map +1 -0
  61. package/lib/commonjs/select/createSelectValue.js +53 -0
  62. package/lib/commonjs/select/createSelectValue.js.map +1 -0
  63. package/lib/commonjs/select/index.js +38 -0
  64. package/lib/commonjs/select/index.js.map +1 -0
  65. package/lib/commonjs/select/types.js +6 -0
  66. package/lib/commonjs/select/types.js.map +1 -0
  67. package/lib/commonjs/select/useContentFocus.js +47 -0
  68. package/lib/commonjs/select/useContentFocus.js.map +1 -0
  69. package/lib/commonjs/select/useListboxNavigation.js +83 -0
  70. package/lib/commonjs/select/useListboxNavigation.js.map +1 -0
  71. package/lib/commonjs/uniwind.d.js +2 -0
  72. package/lib/commonjs/uniwind.d.js.map +1 -0
  73. package/lib/commonjs/utils/dataAttributes.js +32 -0
  74. package/lib/commonjs/utils/dataAttributes.js.map +1 -0
  75. package/lib/commonjs/utils/dataAttributes.web.js +31 -0
  76. package/lib/commonjs/utils/dataAttributes.web.js.map +1 -0
  77. package/lib/module/button/context.js +5 -0
  78. package/lib/module/button/context.js.map +1 -0
  79. package/lib/module/button/createButtonGroup.js +74 -0
  80. package/lib/module/button/createButtonGroup.js.map +1 -0
  81. package/lib/module/button/createButtonIcon.js +13 -0
  82. package/lib/module/button/createButtonIcon.js.map +1 -0
  83. package/lib/module/button/createButtonRoot.js +76 -0
  84. package/lib/module/button/createButtonRoot.js.map +1 -0
  85. package/lib/module/button/createButtonSpinner.js +29 -0
  86. package/lib/module/button/createButtonSpinner.js.map +1 -0
  87. package/lib/module/button/createButtonText.js +33 -0
  88. package/lib/module/button/createButtonText.js.map +1 -0
  89. package/lib/module/button/index.js +27 -0
  90. package/lib/module/button/index.js.map +1 -0
  91. package/lib/module/button/types.js +4 -0
  92. package/lib/module/button/types.js.map +1 -0
  93. package/lib/module/index.js +7 -0
  94. package/lib/module/index.js.map +1 -0
  95. package/lib/module/input/context.js +5 -0
  96. package/lib/module/input/context.js.map +1 -0
  97. package/lib/module/input/createInputField.js +85 -0
  98. package/lib/module/input/createInputField.js.map +1 -0
  99. package/lib/module/input/createInputIcon.js +15 -0
  100. package/lib/module/input/createInputIcon.js.map +1 -0
  101. package/lib/module/input/createInputRoot.js +76 -0
  102. package/lib/module/input/createInputRoot.js.map +1 -0
  103. package/lib/module/input/createInputSlot.js +39 -0
  104. package/lib/module/input/createInputSlot.js.map +1 -0
  105. package/lib/module/input/index.js +23 -0
  106. package/lib/module/input/index.js.map +1 -0
  107. package/lib/module/input/types.js +4 -0
  108. package/lib/module/input/types.js.map +1 -0
  109. package/lib/module/overlay/OverlayContainer.js +62 -0
  110. package/lib/module/overlay/OverlayContainer.js.map +1 -0
  111. package/lib/module/overlay/index.js +7 -0
  112. package/lib/module/overlay/index.js.map +1 -0
  113. package/lib/module/overlay/useAnchorPosition.js +64 -0
  114. package/lib/module/overlay/useAnchorPosition.js.map +1 -0
  115. package/lib/module/overlay/useDismissOverlay.js +10 -0
  116. package/lib/module/overlay/useDismissOverlay.js.map +1 -0
  117. package/lib/module/overlay/useDismissOverlay.web.js +43 -0
  118. package/lib/module/overlay/useDismissOverlay.web.js.map +1 -0
  119. package/lib/module/overlay/useOverlayPosition.js +88 -0
  120. package/lib/module/overlay/useOverlayPosition.js.map +1 -0
  121. package/lib/module/select/context.js +56 -0
  122. package/lib/module/select/context.js.map +1 -0
  123. package/lib/module/select/createSelectContent.js +97 -0
  124. package/lib/module/select/createSelectContent.js.map +1 -0
  125. package/lib/module/select/createSelectIcon.js +36 -0
  126. package/lib/module/select/createSelectIcon.js.map +1 -0
  127. package/lib/module/select/createSelectItem.js +96 -0
  128. package/lib/module/select/createSelectItem.js.map +1 -0
  129. package/lib/module/select/createSelectItemLabel.js +36 -0
  130. package/lib/module/select/createSelectItemLabel.js.map +1 -0
  131. package/lib/module/select/createSelectRoot.js +94 -0
  132. package/lib/module/select/createSelectRoot.js.map +1 -0
  133. package/lib/module/select/createSelectTrigger.js +133 -0
  134. package/lib/module/select/createSelectTrigger.js.map +1 -0
  135. package/lib/module/select/createSelectValue.js +48 -0
  136. package/lib/module/select/createSelectValue.js.map +1 -0
  137. package/lib/module/select/index.js +34 -0
  138. package/lib/module/select/index.js.map +1 -0
  139. package/lib/module/select/types.js +4 -0
  140. package/lib/module/select/types.js.map +1 -0
  141. package/lib/module/select/useContentFocus.js +44 -0
  142. package/lib/module/select/useContentFocus.js.map +1 -0
  143. package/lib/module/select/useListboxNavigation.js +80 -0
  144. package/lib/module/select/useListboxNavigation.js.map +1 -0
  145. package/lib/module/uniwind.d.js +2 -0
  146. package/lib/module/uniwind.d.js.map +1 -0
  147. package/lib/module/utils/dataAttributes.js +28 -0
  148. package/lib/module/utils/dataAttributes.js.map +1 -0
  149. package/lib/module/utils/dataAttributes.web.js +27 -0
  150. package/lib/module/utils/dataAttributes.web.js.map +1 -0
  151. package/lib/typescript/button/context.d.ts +6 -0
  152. package/lib/typescript/button/context.d.ts.map +1 -0
  153. package/lib/typescript/button/createButtonGroup.d.ts +4 -0
  154. package/lib/typescript/button/createButtonGroup.d.ts.map +1 -0
  155. package/lib/typescript/button/createButtonIcon.d.ts +2 -0
  156. package/lib/typescript/button/createButtonIcon.d.ts.map +1 -0
  157. package/lib/typescript/button/createButtonRoot.d.ts +6 -0
  158. package/lib/typescript/button/createButtonRoot.d.ts.map +1 -0
  159. package/lib/typescript/button/createButtonSpinner.d.ts +4 -0
  160. package/lib/typescript/button/createButtonSpinner.d.ts.map +1 -0
  161. package/lib/typescript/button/createButtonText.d.ts +4 -0
  162. package/lib/typescript/button/createButtonText.d.ts.map +1 -0
  163. package/lib/typescript/button/index.d.ts +11 -0
  164. package/lib/typescript/button/index.d.ts.map +1 -0
  165. package/lib/typescript/button/types.d.ts +65 -0
  166. package/lib/typescript/button/types.d.ts.map +1 -0
  167. package/lib/typescript/index.d.ts +5 -0
  168. package/lib/typescript/index.d.ts.map +1 -0
  169. package/lib/typescript/input/context.d.ts +6 -0
  170. package/lib/typescript/input/context.d.ts.map +1 -0
  171. package/lib/typescript/input/createInputField.d.ts +4 -0
  172. package/lib/typescript/input/createInputField.d.ts.map +1 -0
  173. package/lib/typescript/input/createInputIcon.d.ts +4 -0
  174. package/lib/typescript/input/createInputIcon.d.ts.map +1 -0
  175. package/lib/typescript/input/createInputRoot.d.ts +4 -0
  176. package/lib/typescript/input/createInputRoot.d.ts.map +1 -0
  177. package/lib/typescript/input/createInputSlot.d.ts +4 -0
  178. package/lib/typescript/input/createInputSlot.d.ts.map +1 -0
  179. package/lib/typescript/input/index.d.ts +9 -0
  180. package/lib/typescript/input/index.d.ts.map +1 -0
  181. package/lib/typescript/input/types.d.ts +91 -0
  182. package/lib/typescript/input/types.d.ts.map +1 -0
  183. package/lib/typescript/overlay/OverlayContainer.d.ts +23 -0
  184. package/lib/typescript/overlay/OverlayContainer.d.ts.map +1 -0
  185. package/lib/typescript/overlay/index.d.ts +6 -0
  186. package/lib/typescript/overlay/index.d.ts.map +1 -0
  187. package/lib/typescript/overlay/useAnchorPosition.d.ts +15 -0
  188. package/lib/typescript/overlay/useAnchorPosition.d.ts.map +1 -0
  189. package/lib/typescript/overlay/useDismissOverlay.d.ts +7 -0
  190. package/lib/typescript/overlay/useDismissOverlay.d.ts.map +1 -0
  191. package/lib/typescript/overlay/useDismissOverlay.web.d.ts +8 -0
  192. package/lib/typescript/overlay/useDismissOverlay.web.d.ts.map +1 -0
  193. package/lib/typescript/overlay/useOverlayPosition.d.ts +45 -0
  194. package/lib/typescript/overlay/useOverlayPosition.d.ts.map +1 -0
  195. package/lib/typescript/select/context.d.ts +17 -0
  196. package/lib/typescript/select/context.d.ts.map +1 -0
  197. package/lib/typescript/select/createSelectContent.d.ts +4 -0
  198. package/lib/typescript/select/createSelectContent.d.ts.map +1 -0
  199. package/lib/typescript/select/createSelectIcon.d.ts +2 -0
  200. package/lib/typescript/select/createSelectIcon.d.ts.map +1 -0
  201. package/lib/typescript/select/createSelectItem.d.ts +6 -0
  202. package/lib/typescript/select/createSelectItem.d.ts.map +1 -0
  203. package/lib/typescript/select/createSelectItemLabel.d.ts +4 -0
  204. package/lib/typescript/select/createSelectItemLabel.d.ts.map +1 -0
  205. package/lib/typescript/select/createSelectRoot.d.ts +4 -0
  206. package/lib/typescript/select/createSelectRoot.d.ts.map +1 -0
  207. package/lib/typescript/select/createSelectTrigger.d.ts +12 -0
  208. package/lib/typescript/select/createSelectTrigger.d.ts.map +1 -0
  209. package/lib/typescript/select/createSelectValue.d.ts +4 -0
  210. package/lib/typescript/select/createSelectValue.d.ts.map +1 -0
  211. package/lib/typescript/select/index.d.ts +13 -0
  212. package/lib/typescript/select/index.d.ts.map +1 -0
  213. package/lib/typescript/select/types.d.ts +96 -0
  214. package/lib/typescript/select/types.d.ts.map +1 -0
  215. package/lib/typescript/select/useContentFocus.d.ts +19 -0
  216. package/lib/typescript/select/useContentFocus.d.ts.map +1 -0
  217. package/lib/typescript/select/useListboxNavigation.d.ts +13 -0
  218. package/lib/typescript/select/useListboxNavigation.d.ts.map +1 -0
  219. package/lib/typescript/utils/dataAttributes.d.ts +14 -0
  220. package/lib/typescript/utils/dataAttributes.d.ts.map +1 -0
  221. package/lib/typescript/utils/dataAttributes.web.d.ts +16 -0
  222. package/lib/typescript/utils/dataAttributes.web.d.ts.map +1 -0
  223. package/package.json +78 -0
  224. package/src/button/context.tsx +4 -0
  225. package/src/button/createButtonGroup.tsx +88 -0
  226. package/src/button/createButtonIcon.tsx +8 -0
  227. package/src/button/createButtonRoot.tsx +101 -0
  228. package/src/button/createButtonSpinner.tsx +20 -0
  229. package/src/button/createButtonText.tsx +22 -0
  230. package/src/button/index.tsx +53 -0
  231. package/src/button/types.ts +85 -0
  232. package/src/index.ts +4 -0
  233. package/src/input/context.tsx +4 -0
  234. package/src/input/createInputField.tsx +104 -0
  235. package/src/input/createInputIcon.tsx +12 -0
  236. package/src/input/createInputRoot.tsx +92 -0
  237. package/src/input/createInputSlot.tsx +39 -0
  238. package/src/input/index.tsx +51 -0
  239. package/src/input/types.ts +113 -0
  240. package/src/overlay/OverlayContainer.tsx +77 -0
  241. package/src/overlay/index.ts +10 -0
  242. package/src/overlay/useAnchorPosition.ts +72 -0
  243. package/src/overlay/useDismissOverlay.ts +14 -0
  244. package/src/overlay/useDismissOverlay.web.ts +51 -0
  245. package/src/overlay/useOverlayPosition.ts +96 -0
  246. package/src/select/context.tsx +56 -0
  247. package/src/select/createSelectContent.tsx +115 -0
  248. package/src/select/createSelectIcon.tsx +27 -0
  249. package/src/select/createSelectItem.tsx +121 -0
  250. package/src/select/createSelectItemLabel.tsx +30 -0
  251. package/src/select/createSelectRoot.tsx +130 -0
  252. package/src/select/createSelectTrigger.tsx +192 -0
  253. package/src/select/createSelectValue.tsx +38 -0
  254. package/src/select/index.tsx +73 -0
  255. package/src/select/types.ts +131 -0
  256. package/src/select/useContentFocus.ts +54 -0
  257. package/src/select/useListboxNavigation.ts +85 -0
  258. package/src/uniwind.d.ts +3 -0
  259. package/src/utils/dataAttributes.ts +28 -0
  260. package/src/utils/dataAttributes.web.ts +26 -0
@@ -0,0 +1,130 @@
1
+ import type React from 'react';
2
+ import { forwardRef, useCallback, useId, useMemo, useRef, useState } from 'react';
3
+ import { useControllableState, useFormControl } from '@cdx-ui/utils';
4
+ import { dataAttributes } from '../utils/dataAttributes';
5
+ import { SelectContext } from './context';
6
+ import type { ISelectProps } from './types';
7
+
8
+ export const createSelectRoot = <T,>(BaseRoot: React.ComponentType<T>) =>
9
+ forwardRef(
10
+ (
11
+ {
12
+ children,
13
+ items,
14
+ value: valueProp,
15
+ defaultValue,
16
+ onValueChange,
17
+ open: openProp,
18
+ defaultOpen = false,
19
+ onOpenChange,
20
+ disabled = false,
21
+ isRequired,
22
+ isInvalid,
23
+ isReadOnly,
24
+ native = false,
25
+ accessibilityLabel,
26
+ ...props
27
+ }: ISelectProps,
28
+ ref?: any,
29
+ ) => {
30
+ const formControlProps = useFormControl({
31
+ isDisabled: disabled,
32
+ isRequired,
33
+ isInvalid,
34
+ isReadOnly,
35
+ });
36
+
37
+ const isEffectivelyDisabled = !!formControlProps.disabled;
38
+ const isEffectivelyRequired = !!formControlProps.required;
39
+ const isEffectivelyInvalid = !!formControlProps['aria-invalid'];
40
+ const isEffectivelyReadOnly = !!formControlProps.readOnly;
41
+
42
+ const [value, setValue] = useControllableState<string | undefined>({
43
+ prop: valueProp,
44
+ defaultProp: defaultValue,
45
+ onChange: (newValue) => {
46
+ if (newValue !== undefined) {
47
+ onValueChange?.(newValue);
48
+ }
49
+ },
50
+ });
51
+
52
+ const [open, setOpenState] = useControllableState<boolean>({
53
+ prop: openProp,
54
+ defaultProp: defaultOpen,
55
+ onChange: onOpenChange,
56
+ });
57
+
58
+ const setOpen = useCallback(
59
+ (newOpen: boolean) => {
60
+ if ((isEffectivelyDisabled || isEffectivelyReadOnly) && newOpen) {
61
+ return;
62
+ }
63
+ setOpenState(newOpen);
64
+ },
65
+ [isEffectivelyDisabled, isEffectivelyReadOnly, setOpenState],
66
+ );
67
+
68
+ const [activeValue, setActiveValue] = useState<string | undefined>(undefined);
69
+
70
+ const triggerRef = useRef<any>(null);
71
+ const id = useId();
72
+ const triggerId = `select-trigger-${id}`;
73
+ const contentId = `select-content-${id}`;
74
+
75
+ const contextValue = useMemo(
76
+ () => ({
77
+ open: open ?? false,
78
+ items,
79
+ setOpen,
80
+ value,
81
+ onValueChange: setValue,
82
+ disabled: isEffectivelyDisabled,
83
+ required: isEffectivelyRequired,
84
+ invalid: isEffectivelyInvalid,
85
+ readOnly: isEffectivelyReadOnly,
86
+ native,
87
+ triggerRef,
88
+ contentId,
89
+ triggerId,
90
+ activeValue,
91
+ setActiveValue,
92
+ accessibilityLabel,
93
+ }),
94
+ [
95
+ open,
96
+ items,
97
+ setOpen,
98
+ value,
99
+ setValue,
100
+ isEffectivelyDisabled,
101
+ isEffectivelyRequired,
102
+ isEffectivelyInvalid,
103
+ isEffectivelyReadOnly,
104
+ native,
105
+ contentId,
106
+ triggerId,
107
+ activeValue,
108
+ accessibilityLabel,
109
+ ],
110
+ );
111
+
112
+ return (
113
+ <SelectContext.Provider value={contextValue}>
114
+ <BaseRoot
115
+ ref={ref}
116
+ {...(props as T)}
117
+ {...dataAttributes({
118
+ slot: 'select',
119
+ disabled: isEffectivelyDisabled,
120
+ required: isEffectivelyRequired,
121
+ invalid: isEffectivelyInvalid,
122
+ readonly: isEffectivelyReadOnly,
123
+ })}
124
+ >
125
+ {children}
126
+ </BaseRoot>
127
+ </SelectContext.Provider>
128
+ );
129
+ },
130
+ );
@@ -0,0 +1,192 @@
1
+ import type React from 'react';
2
+ import { forwardRef, useCallback, useMemo } from 'react';
3
+ import { type GestureResponderEvent, Platform } from 'react-native';
4
+ import { composeEventHandlers, mergeRefs } from '@cdx-ui/utils';
5
+ import { useFocus, useFocusRing } from '@react-native-aria/focus';
6
+ import { useHover, usePress } from '@react-native-aria/interactions';
7
+ import { dataAttributes } from '../utils/dataAttributes';
8
+ import { SelectTriggerContext, useSelectContext } from './context';
9
+ import type { ISelectTriggerProps } from './types';
10
+
11
+ export const createSelectTrigger = <T,>(BaseTrigger: React.ComponentType<T>) =>
12
+ forwardRef(
13
+ (
14
+ {
15
+ children,
16
+ isHovered: isHoveredProp,
17
+ isActive: isActiveProp,
18
+ isFocused: isFocusedProp,
19
+ isFocusVisible: isFocusVisibleProp,
20
+ isDisabled: isDisabledProp,
21
+ ...props
22
+ }: Omit<ISelectTriggerProps, 'children'> & {
23
+ children?:
24
+ | (({
25
+ hovered,
26
+ pressed,
27
+ focused,
28
+ focusVisible,
29
+ disabled,
30
+ }: {
31
+ hovered?: boolean;
32
+ pressed?: boolean;
33
+ focused?: boolean;
34
+ focusVisible?: boolean;
35
+ disabled?: boolean;
36
+ }) => React.ReactNode)
37
+ | React.ReactNode;
38
+ },
39
+ ref?: any,
40
+ ) => {
41
+ const {
42
+ open,
43
+ setOpen,
44
+ disabled: contextDisabled,
45
+ required: contextRequired,
46
+ invalid: contextInvalid,
47
+ readOnly: contextReadOnly,
48
+ triggerId,
49
+ contentId,
50
+ triggerRef,
51
+ activeValue,
52
+ accessibilityLabel,
53
+ } = useSelectContext();
54
+
55
+ const disabled = contextDisabled || !!isDisabledProp;
56
+
57
+ const { isFocusVisible, focusProps: focusRingProps }: any = useFocusRing();
58
+ const { pressProps, isPressed: isActive } = usePress({
59
+ isDisabled: disabled,
60
+ });
61
+ const { isFocused, focusProps }: any = useFocus();
62
+ const { isHovered, hoverProps } = useHover();
63
+
64
+ const handlePress = useCallback(
65
+ (e: GestureResponderEvent | KeyboardEvent) => {
66
+ if (e.type === 'keyup' && (e as KeyboardEvent).key === 'Enter') {
67
+ return;
68
+ }
69
+ if (!disabled) {
70
+ setOpen(!open);
71
+ }
72
+ },
73
+ [disabled, open, setOpen],
74
+ );
75
+
76
+ const handleKeyDown = useCallback(
77
+ (e: KeyboardEvent) => {
78
+ if (disabled) {
79
+ return;
80
+ }
81
+
82
+ switch (e.key) {
83
+ case 'Enter': // TODO: Only stays open if the key is held down
84
+ case ' ':
85
+ case 'ArrowDown':
86
+ case 'ArrowUp': {
87
+ e.preventDefault();
88
+ setOpen(true);
89
+ break;
90
+ }
91
+ }
92
+ },
93
+ [disabled, setOpen],
94
+ );
95
+
96
+ const interactionState = useMemo(
97
+ () => ({
98
+ hover: isHoveredProp || isHovered,
99
+ focus: isFocusedProp || isFocused,
100
+ active: isActiveProp || isActive,
101
+ disabled: !!disabled,
102
+ focusVisible: isFocusVisibleProp || isFocusVisible,
103
+ }),
104
+ [
105
+ isHoveredProp,
106
+ isHovered,
107
+ isFocusedProp,
108
+ isFocused,
109
+ isActiveProp,
110
+ isActive,
111
+ disabled,
112
+ isFocusVisibleProp,
113
+ isFocusVisible,
114
+ ],
115
+ );
116
+
117
+ const mergedRef = mergeRefs(ref, triggerRef);
118
+
119
+ const webKeyboardProps =
120
+ Platform.OS === 'web'
121
+ ? { onKeyDown: composeEventHandlers((props as any).onKeyDown, handleKeyDown) }
122
+ : {};
123
+
124
+ return (
125
+ <SelectTriggerContext.Provider value={interactionState}>
126
+ <BaseTrigger
127
+ ref={mergedRef}
128
+ role="combobox"
129
+ aria-label={accessibilityLabel}
130
+ aria-haspopup="listbox"
131
+ aria-expanded={open}
132
+ aria-controls={contentId}
133
+ aria-activedescendant={
134
+ open && activeValue ? `${contentId}-option-${activeValue}` : undefined
135
+ }
136
+ aria-required={contextRequired || undefined}
137
+ aria-invalid={contextInvalid || undefined}
138
+ aria-readonly={contextReadOnly || undefined}
139
+ id={triggerId}
140
+ {...dataAttributes({
141
+ hover: interactionState.hover,
142
+ focus: interactionState.focus,
143
+ active: interactionState.active,
144
+ disabled: interactionState.disabled,
145
+ focusVisible: interactionState.focusVisible,
146
+ required: contextRequired,
147
+ invalid: contextInvalid,
148
+ readonly: contextReadOnly,
149
+ state: open ? 'open' : 'closed',
150
+ slot: 'select-trigger',
151
+ })}
152
+ disabled={disabled}
153
+ {...(props as T)}
154
+ onPress={composeEventHandlers(props?.onPress, handlePress)}
155
+ onPressIn={composeEventHandlers(props?.onPressIn, pressProps.onPressIn)}
156
+ onPressOut={composeEventHandlers(props?.onPressOut, pressProps.onPressOut)}
157
+ onHoverIn={composeEventHandlers(props?.onHoverIn, hoverProps.onHoverIn)}
158
+ onHoverOut={composeEventHandlers(props?.onHoverOut, hoverProps.onHoverOut)}
159
+ onFocus={composeEventHandlers(
160
+ composeEventHandlers(
161
+ props?.onFocus as (...args: unknown[]) => unknown,
162
+ focusProps.onFocus,
163
+ ),
164
+ focusRingProps.onFocus,
165
+ )}
166
+ onBlur={composeEventHandlers(
167
+ composeEventHandlers(
168
+ props?.onBlur as (...args: unknown[]) => unknown,
169
+ focusProps.onBlur,
170
+ ),
171
+ focusRingProps.onBlur,
172
+ )}
173
+ {...webKeyboardProps}
174
+ >
175
+ {/* TODO: Align render-function children convention with Button — Button
176
+ passes raw hook values (isHovered, isFocused, etc.) while this
177
+ passes merged interaction state (isHoveredProp || isHovered). Pick
178
+ one convention and apply to both. */}
179
+ {typeof children === 'function'
180
+ ? children({
181
+ hovered: interactionState.hover,
182
+ focused: interactionState.focus,
183
+ pressed: interactionState.active,
184
+ disabled: interactionState.disabled,
185
+ focusVisible: interactionState.focusVisible,
186
+ })
187
+ : children}
188
+ </BaseTrigger>
189
+ </SelectTriggerContext.Provider>
190
+ );
191
+ },
192
+ );
@@ -0,0 +1,38 @@
1
+ import type React from 'react';
2
+ import { forwardRef } from 'react';
3
+ import { dataAttributes } from '../utils/dataAttributes';
4
+ import { useSelectContext, useSelectTriggerContext } from './context';
5
+ import type { ISelectValueProps } from './types';
6
+
7
+ // TODO: Support custom display text
8
+
9
+ export const createSelectValue = <T,>(BaseValue: React.ComponentType<T>) =>
10
+ forwardRef(({ placeholder, ...props }: ISelectValueProps, ref?: any) => {
11
+ const { value, items, required, invalid, readOnly } = useSelectContext();
12
+ const { hover, focus, active, disabled, focusVisible } = useSelectTriggerContext();
13
+
14
+ const hasValue = value !== undefined && value !== '';
15
+ const showPlaceholder = !hasValue;
16
+ const displayText = hasValue ? items.find((item) => item.value === value)?.label : placeholder;
17
+
18
+ return (
19
+ <BaseValue
20
+ ref={ref}
21
+ {...(props as T)}
22
+ {...dataAttributes({
23
+ hover,
24
+ focus,
25
+ active,
26
+ disabled,
27
+ focusVisible,
28
+ required,
29
+ invalid,
30
+ readonly: readOnly,
31
+ placeholder: showPlaceholder,
32
+ slot: 'select-value',
33
+ })}
34
+ >
35
+ {displayText}
36
+ </BaseValue>
37
+ );
38
+ });
@@ -0,0 +1,73 @@
1
+ import type React from 'react';
2
+ import { createSelectContent } from './createSelectContent';
3
+ import { createSelectIcon } from './createSelectIcon';
4
+ import { createSelectItem } from './createSelectItem';
5
+ import { createSelectItemLabel } from './createSelectItemLabel';
6
+ import { createSelectRoot } from './createSelectRoot';
7
+ import { createSelectTrigger } from './createSelectTrigger';
8
+ import { createSelectValue } from './createSelectValue';
9
+ import type { ISelectComponentType } from './types';
10
+
11
+ export type {
12
+ ISelectContentProps,
13
+ ISelectItemLabelProps,
14
+ ISelectItemProps,
15
+ ISelectProps,
16
+ ISelectTriggerProps,
17
+ ISelectValueProps,
18
+ } from './types';
19
+
20
+ export function createSelect<
21
+ RootProps,
22
+ TriggerProps,
23
+ ValueProps,
24
+ IconProps,
25
+ ContentProps,
26
+ ItemProps,
27
+ ItemLabelProps,
28
+ TriggerRef = unknown,
29
+ ValueRef = unknown,
30
+ >(BaseComponents: {
31
+ Root: React.ComponentType<RootProps>;
32
+ Trigger: React.ComponentType<TriggerProps>;
33
+ Value: React.ComponentType<ValueProps>;
34
+ Icon: React.ComponentType<IconProps>;
35
+ Content: React.ComponentType<ContentProps>;
36
+ Item: React.ComponentType<ItemProps>;
37
+ ItemLabel: React.ComponentType<ItemLabelProps>;
38
+ }) {
39
+ const Select = createSelectRoot(BaseComponents.Root);
40
+ const Trigger = createSelectTrigger(BaseComponents.Trigger);
41
+ const Value = createSelectValue(BaseComponents.Value);
42
+ const Icon = createSelectIcon(BaseComponents.Icon);
43
+ const Content = createSelectContent(BaseComponents.Content);
44
+ const Item = createSelectItem(BaseComponents.Item);
45
+ const ItemLabel = createSelectItemLabel(BaseComponents.ItemLabel);
46
+
47
+ Select.displayName = 'SelectPrimitive';
48
+ Trigger.displayName = 'SelectPrimitive.Trigger';
49
+ Value.displayName = 'SelectPrimitive.Value';
50
+ Icon.displayName = 'SelectPrimitive.Icon';
51
+ Content.displayName = 'SelectPrimitive.Content';
52
+ Item.displayName = 'SelectPrimitive.Item';
53
+ ItemLabel.displayName = 'SelectPrimitive.ItemLabel';
54
+
55
+ return Object.assign(Select, {
56
+ Trigger,
57
+ Value,
58
+ Icon,
59
+ Content,
60
+ Item,
61
+ ItemLabel,
62
+ }) as ISelectComponentType<
63
+ RootProps,
64
+ TriggerProps,
65
+ ValueProps,
66
+ IconProps,
67
+ ContentProps,
68
+ ItemProps,
69
+ ItemLabelProps,
70
+ TriggerRef,
71
+ ValueRef
72
+ >;
73
+ }
@@ -0,0 +1,131 @@
1
+ import type { PropsWithoutRef, ReactNode, RefAttributes, RefObject } from 'react';
2
+ import type { PressableProps, ViewProps } from 'react-native';
3
+ import type { EntryOrExitLayoutType } from 'react-native-reanimated';
4
+
5
+ // TODO: Consolidate with button/types.ts
6
+
7
+ export interface InteractionState {
8
+ hover: boolean;
9
+ focus: boolean;
10
+ active: boolean;
11
+ disabled: boolean;
12
+ focusVisible: boolean;
13
+ }
14
+
15
+ export interface SelectItemInteractionState extends InteractionState {
16
+ highlighted: boolean;
17
+ checked: boolean;
18
+ }
19
+
20
+ export interface ISelectProps {
21
+ items: SelectItemData[];
22
+ value?: string;
23
+ defaultValue?: string;
24
+ onValueChange?: (value: string) => void;
25
+ open?: boolean;
26
+ defaultOpen?: boolean;
27
+ onOpenChange?: (open: boolean) => void;
28
+ disabled?: boolean;
29
+ isRequired?: boolean;
30
+ isInvalid?: boolean;
31
+ isReadOnly?: boolean;
32
+ native?: boolean;
33
+ accessibilityLabel?: string;
34
+ children?: ReactNode;
35
+ }
36
+
37
+ export interface ISelectTriggerProps extends PressableProps {
38
+ isHovered?: boolean;
39
+ isActive?: boolean;
40
+ isFocused?: boolean;
41
+ isFocusVisible?: boolean;
42
+ isDisabled?: boolean;
43
+ }
44
+
45
+ export interface ISelectValueProps {
46
+ placeholder?: string;
47
+ children?: ReactNode;
48
+ }
49
+
50
+ export interface ISelectContentProps extends ViewProps {
51
+ enteringAnimation?: EntryOrExitLayoutType;
52
+ exitingAnimation?: EntryOrExitLayoutType;
53
+ children?: ReactNode;
54
+ }
55
+
56
+ export interface ISelectItemProps extends PressableProps {
57
+ value: string;
58
+ disabled?: boolean;
59
+ isHovered?: boolean;
60
+ isActive?: boolean;
61
+ isFocused?: boolean;
62
+ isFocusVisible?: boolean;
63
+ isDisabled?: boolean;
64
+ }
65
+
66
+ export interface ISelectItemLabelProps {
67
+ children?: ReactNode;
68
+ }
69
+
70
+ export interface SelectItemData {
71
+ value: string;
72
+ label: string;
73
+ disabled?: boolean; // TODO: This isn't working
74
+ }
75
+
76
+ export interface SelectItemCollectionContextValue {
77
+ registerItem: (item: SelectItemData) => void;
78
+ unregisterItem: (value: string) => void;
79
+ getItems: () => SelectItemData[];
80
+ }
81
+
82
+ export interface SelectContextValue {
83
+ open: boolean;
84
+ items: SelectItemData[];
85
+ setOpen: (open: boolean) => void;
86
+ value: string | undefined;
87
+ onValueChange: (value: string) => void;
88
+ disabled: boolean;
89
+ required: boolean;
90
+ invalid: boolean;
91
+ readOnly: boolean;
92
+ native: boolean;
93
+ triggerRef: RefObject<any>;
94
+ contentId: string;
95
+ triggerId: string;
96
+ /** Currently highlighted value during keyboard navigation */
97
+ activeValue: string | undefined;
98
+ setActiveValue: (value: string | undefined) => void;
99
+ accessibilityLabel: string | undefined;
100
+ }
101
+
102
+ export type ISelectComponentType<
103
+ RootProps,
104
+ TriggerProps,
105
+ ValueProps,
106
+ IconProps,
107
+ ContentProps,
108
+ ItemProps,
109
+ ItemLabelProps,
110
+ TriggerRef = unknown,
111
+ ValueRef = unknown,
112
+ > = React.ForwardRefExoticComponent<
113
+ PropsWithoutRef<RootProps & ISelectProps> & RefAttributes<unknown>
114
+ > & {
115
+ Trigger: React.ForwardRefExoticComponent<
116
+ PropsWithoutRef<TriggerProps & ISelectTriggerProps> & RefAttributes<TriggerRef>
117
+ >;
118
+ Value: React.ForwardRefExoticComponent<
119
+ PropsWithoutRef<ValueProps & ISelectValueProps> & RefAttributes<ValueRef>
120
+ >;
121
+ Icon: React.ForwardRefExoticComponent<PropsWithoutRef<IconProps> & RefAttributes<unknown>>;
122
+ Content: React.ForwardRefExoticComponent<
123
+ PropsWithoutRef<ContentProps & ISelectContentProps> & RefAttributes<unknown>
124
+ >;
125
+ Item: React.ForwardRefExoticComponent<
126
+ PropsWithoutRef<ItemProps & ISelectItemProps> & RefAttributes<unknown>
127
+ >;
128
+ ItemLabel: React.ForwardRefExoticComponent<
129
+ PropsWithoutRef<ItemLabelProps & ISelectItemLabelProps> & RefAttributes<unknown>
130
+ >;
131
+ };
@@ -0,0 +1,54 @@
1
+ import type { RefObject } from 'react';
2
+ import { useCallback, useEffect, useRef } from 'react';
3
+ import { Platform } from 'react-native';
4
+ import type { SelectItemData } from './types';
5
+
6
+ // TODO: Split into web and native versions?
7
+
8
+ export interface UseContentFocusOptions {
9
+ open: boolean;
10
+ triggerRef: RefObject<any>;
11
+ items: SelectItemData[];
12
+ value: string | undefined;
13
+ setActiveValue: (value: string | undefined) => void;
14
+ }
15
+
16
+ /**
17
+ * Manages focus lifecycle for overlay content:
18
+ * - Restores focus to the trigger when the overlay closes (web)
19
+ * - Initializes `activeValue` to the current value or first enabled item
20
+ *
21
+ * Initial focus on open is handled via a ref callback in createSelectContent
22
+ * to avoid timing issues with deferred portal rendering.
23
+ */
24
+ export function useContentFocus({
25
+ open,
26
+ triggerRef,
27
+ items,
28
+ value,
29
+ setActiveValue,
30
+ }: UseContentFocusOptions) {
31
+ const prevOpenRef = useRef(open);
32
+
33
+ const getEnabledItems = useCallback(() => items.filter((item) => !item.disabled), [items]);
34
+
35
+ useEffect(() => {
36
+ if (prevOpenRef.current && !open && Platform.OS === 'web') {
37
+ triggerRef.current?.focus?.();
38
+ }
39
+ prevOpenRef.current = open;
40
+ }, [open, triggerRef]);
41
+
42
+ useEffect(() => {
43
+ if (open) {
44
+ const enabled = getEnabledItems();
45
+ if (value && enabled.some((i) => i.value === value)) {
46
+ setActiveValue(value);
47
+ } else if (enabled.length > 0) {
48
+ setActiveValue(enabled[0].value);
49
+ }
50
+ } else {
51
+ setActiveValue(undefined);
52
+ }
53
+ }, [open, getEnabledItems, setActiveValue, value]);
54
+ }
@@ -0,0 +1,85 @@
1
+ import { useCallback, useRef } from 'react';
2
+ import type { SelectItemData } from './types';
3
+
4
+ // TODO: Split into web and native versions?
5
+
6
+ export interface UseListboxNavigationOptions {
7
+ items: SelectItemData[];
8
+ activeValue: string | undefined;
9
+ setActiveValue: (value: string | undefined) => void;
10
+ onSelect: (value: string) => void;
11
+ onDismiss: () => void;
12
+ }
13
+
14
+ export function useListboxNavigation({
15
+ items,
16
+ activeValue,
17
+ setActiveValue,
18
+ onSelect,
19
+ onDismiss,
20
+ }: UseListboxNavigationOptions) {
21
+ const onSelectRef = useRef(onSelect);
22
+ onSelectRef.current = onSelect;
23
+
24
+ const onDismissRef = useRef(onDismiss);
25
+ onDismissRef.current = onDismiss;
26
+
27
+ const getEnabledItems = useCallback(() => items.filter((item) => !item.disabled), [items]);
28
+
29
+ const handleKeyDown = useCallback(
30
+ (e: any) => {
31
+ const enabledItems = getEnabledItems();
32
+ if (enabledItems.length === 0) {
33
+ return;
34
+ }
35
+
36
+ const idx = enabledItems.findIndex((i) => i.value === activeValue);
37
+
38
+ switch (e.key) {
39
+ case 'ArrowDown': {
40
+ e.preventDefault();
41
+ setActiveValue(enabledItems[idx < enabledItems.length - 1 ? idx + 1 : 0].value);
42
+ break;
43
+ }
44
+ case 'ArrowUp': {
45
+ e.preventDefault();
46
+ setActiveValue(enabledItems[idx > 0 ? idx - 1 : enabledItems.length - 1].value);
47
+ break;
48
+ }
49
+ case 'Home': {
50
+ e.preventDefault();
51
+ setActiveValue(enabledItems[0].value);
52
+ break;
53
+ }
54
+ case 'End': {
55
+ e.preventDefault();
56
+ const last = enabledItems.at(-1);
57
+ if (last) {
58
+ setActiveValue(last.value);
59
+ }
60
+ break;
61
+ }
62
+ case 'Enter':
63
+ case ' ': {
64
+ e.preventDefault();
65
+ if (activeValue) {
66
+ onSelectRef.current(activeValue);
67
+ }
68
+ break;
69
+ }
70
+ case 'Escape': {
71
+ e.preventDefault();
72
+ onDismissRef.current();
73
+ break;
74
+ }
75
+ case 'Tab': {
76
+ onDismissRef.current();
77
+ break;
78
+ }
79
+ }
80
+ },
81
+ [activeValue, getEnabledItems, setActiveValue],
82
+ );
83
+
84
+ return { handleKeyDown, getEnabledItems };
85
+ }