@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,72 @@
1
+ import type { RefObject } from 'react';
2
+ import { useEffect, useLayoutEffect, useState } from 'react';
3
+ import { Platform } from 'react-native';
4
+
5
+ const useIsomorphicLayoutEffect = Platform.OS === 'web' ? useLayoutEffect : useEffect;
6
+
7
+ export interface AnchorLayout {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+
14
+ function measureWeb(element: any): AnchorLayout {
15
+ const rect = (element as unknown as HTMLElement).getBoundingClientRect();
16
+ return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
17
+ }
18
+
19
+ /**
20
+ * Measures an anchor element's position and size, re-measuring on scroll/resize
21
+ * (web) so a positioned overlay can track its anchor across layout changes.
22
+ *
23
+ * Returns `null` until the first measurement completes.
24
+ */
25
+ export function useAnchorPosition(anchorRef: RefObject<any>, open: boolean): AnchorLayout | null {
26
+ const [layout, setLayout] = useState<AnchorLayout | null>(null);
27
+
28
+ useIsomorphicLayoutEffect(() => {
29
+ if (!open) {
30
+ setLayout(null);
31
+ return;
32
+ }
33
+
34
+ const anchor = anchorRef.current;
35
+ if (!anchor) return;
36
+
37
+ let cancelled = false;
38
+
39
+ if (Platform.OS === 'web') {
40
+ setLayout(measureWeb(anchor));
41
+ } else {
42
+ anchor.measureInWindow((x: number, y: number, width: number, height: number) => {
43
+ if (!cancelled) {
44
+ setLayout({ x, y, width, height });
45
+ }
46
+ });
47
+ }
48
+
49
+ return () => {
50
+ cancelled = true;
51
+ };
52
+ }, [open, anchorRef]);
53
+
54
+ useEffect(() => {
55
+ if (!open || Platform.OS !== 'web') return;
56
+
57
+ const anchor = anchorRef.current;
58
+ if (!anchor) return;
59
+
60
+ const update = () => setLayout(measureWeb(anchor));
61
+
62
+ window.addEventListener('scroll', update, true);
63
+ window.addEventListener('resize', update);
64
+
65
+ return () => {
66
+ window.removeEventListener('scroll', update, true);
67
+ window.removeEventListener('resize', update);
68
+ };
69
+ }, [open, anchorRef]);
70
+
71
+ return layout;
72
+ }
@@ -0,0 +1,14 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+
3
+ import type { RefObject } from 'react';
4
+
5
+ /**
6
+ * No-op on native — overlay dismissal is handled by backdrop press
7
+ * or the system back gesture / hardware button via Modal.
8
+ */
9
+ export function useDismissOverlay(
10
+ _open: boolean,
11
+ _contentRef: RefObject<any>,
12
+ _excludeRefs: RefObject<any>[],
13
+ _onDismiss: () => void,
14
+ ) {}
@@ -0,0 +1,51 @@
1
+ import type { RefObject } from 'react';
2
+ import { useEffect, useRef } from 'react';
3
+
4
+ /**
5
+ * Dismisses an overlay when the user clicks/presses outside of it or when
6
+ * focus moves to an element outside the overlay via keyboard.
7
+ * Events targeting `contentRef` or any of `excludeRefs` are ignored.
8
+ */
9
+ export function useDismissOverlay(
10
+ open: boolean,
11
+ contentRef: RefObject<any>,
12
+ excludeRefs: RefObject<any>[],
13
+ onDismiss: () => void,
14
+ ) {
15
+ const onDismissRef = useRef(onDismiss);
16
+ onDismissRef.current = onDismiss;
17
+
18
+ const excludeRefsRef = useRef(excludeRefs);
19
+ excludeRefsRef.current = excludeRefs;
20
+
21
+ useEffect(() => {
22
+ if (!open) {
23
+ return;
24
+ }
25
+
26
+ const isInsideOverlay = (target: Node) => {
27
+ if (contentRef.current?.contains?.(target)) {
28
+ return true;
29
+ }
30
+ for (const ref of excludeRefsRef.current) {
31
+ if (ref.current?.contains?.(target)) {
32
+ return true;
33
+ }
34
+ }
35
+ return false;
36
+ };
37
+
38
+ const dismissIfOutside = (e: Event) => {
39
+ if (!isInsideOverlay(e.target as Node)) {
40
+ onDismissRef.current();
41
+ }
42
+ };
43
+
44
+ document.addEventListener('pointerdown', dismissIfOutside);
45
+ document.addEventListener('focusin', dismissIfOutside);
46
+ return () => {
47
+ document.removeEventListener('pointerdown', dismissIfOutside);
48
+ document.removeEventListener('focusin', dismissIfOutside);
49
+ };
50
+ }, [open, contentRef]);
51
+ }
@@ -0,0 +1,96 @@
1
+ import { createContext, useContext, useMemo } from 'react';
2
+ import { useWindowDimensions, type ViewStyle } from 'react-native';
3
+ import type { AnchorLayout } from './useAnchorPosition';
4
+
5
+ export interface EdgeInsets {
6
+ top: number;
7
+ right: number;
8
+ bottom: number;
9
+ left: number;
10
+ }
11
+
12
+ const DEFAULT_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
13
+
14
+ /**
15
+ * Provides safe-area insets to overlay positioning hooks. Wrap your app
16
+ * root with this provider and pass values from `useSafeAreaInsets()` to
17
+ * enable safe-area-aware overlay placement.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * import { useSafeAreaInsets } from 'react-native-safe-area-context';
22
+ * import { OverlayInsetsProvider } from '@cdx-ui/primitives';
23
+ *
24
+ * function App() {
25
+ * const insets = useSafeAreaInsets();
26
+ * return (
27
+ * <OverlayInsetsProvider value={insets}>
28
+ * {children}
29
+ * </OverlayInsetsProvider>
30
+ * );
31
+ * }
32
+ * ```
33
+ */
34
+ export const OverlayInsetsContext = createContext<EdgeInsets>(DEFAULT_INSETS);
35
+ export const OverlayInsetsProvider = OverlayInsetsContext.Provider;
36
+
37
+ /** Minimum gap (px) between the overlay edge and the viewport/safe-area boundary. */
38
+ const SCREEN_EDGE_PADDING = 8;
39
+
40
+ /** Minimum usable space (px) below the anchor before the overlay flips above. */
41
+ const MIN_CONTENT_HEIGHT = 200;
42
+
43
+ export interface OverlayPosition {
44
+ style: ViewStyle;
45
+ maxHeight: number;
46
+ placement: 'top' | 'bottom';
47
+ }
48
+
49
+ /**
50
+ * Computes viewport-aware overlay positioning relative to an anchor element.
51
+ * Automatically flips placement from bottom to top when insufficient space
52
+ * exists below the anchor. Constrains `maxHeight` to available space so
53
+ * content never extends beyond the screen edge.
54
+ *
55
+ * Respects safe-area insets provided via `OverlayInsetsProvider`.
56
+ */
57
+ export function useOverlayPosition(anchorLayout: AnchorLayout | null): OverlayPosition | null {
58
+ const { height: viewportHeight } = useWindowDimensions();
59
+ const { top: insetTop, bottom: insetBottom } = useContext(OverlayInsetsContext);
60
+
61
+ return useMemo(() => {
62
+ if (!anchorLayout) return null;
63
+
64
+ const anchorBottom = anchorLayout.y + anchorLayout.height;
65
+ const spaceBelow = viewportHeight - anchorBottom - insetBottom - SCREEN_EDGE_PADDING;
66
+ const spaceAbove = anchorLayout.y - insetTop - SCREEN_EDGE_PADDING;
67
+
68
+ const preferBelow = spaceBelow >= MIN_CONTENT_HEIGHT || spaceBelow >= spaceAbove;
69
+
70
+ if (preferBelow) {
71
+ const maxHeight = Math.max(0, spaceBelow);
72
+ return {
73
+ style: {
74
+ top: anchorBottom,
75
+ left: anchorLayout.x,
76
+ width: anchorLayout.width,
77
+ maxHeight,
78
+ },
79
+ maxHeight,
80
+ placement: 'bottom' as const,
81
+ };
82
+ }
83
+
84
+ const maxHeight = Math.max(0, spaceAbove);
85
+ return {
86
+ style: {
87
+ bottom: viewportHeight - anchorLayout.y,
88
+ left: anchorLayout.x,
89
+ width: anchorLayout.width,
90
+ maxHeight,
91
+ },
92
+ maxHeight,
93
+ placement: 'top' as const,
94
+ };
95
+ }, [anchorLayout, viewportHeight, insetTop, insetBottom]);
96
+ }
@@ -0,0 +1,56 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { InteractionState, SelectContextValue, SelectItemInteractionState } from './types';
3
+
4
+ const defaultContextValue: SelectContextValue = {
5
+ open: false,
6
+ items: [],
7
+ setOpen: () => {},
8
+ value: undefined,
9
+ onValueChange: () => {},
10
+ disabled: false,
11
+ required: false,
12
+ invalid: false,
13
+ readOnly: false,
14
+ native: false,
15
+ triggerRef: { current: null },
16
+ contentId: '',
17
+ triggerId: '',
18
+ activeValue: undefined,
19
+ setActiveValue: () => {}, // TODO: Is this needed? At the very least it could be moved to SelectContent
20
+ accessibilityLabel: undefined,
21
+ };
22
+
23
+ export const SelectContext = createContext<SelectContextValue>(defaultContextValue);
24
+
25
+ export const useSelectContext = () => useContext(SelectContext);
26
+
27
+ /**
28
+ * Propagates the trigger's interaction state (hover, focus, active, etc.) to
29
+ * child components like SelectValue and SelectIcon so they can apply matching
30
+ * data-attribute styles.
31
+ */
32
+ export const SelectTriggerContext = createContext<InteractionState>({
33
+ hover: false,
34
+ focus: false,
35
+ active: false,
36
+ disabled: false,
37
+ focusVisible: false,
38
+ });
39
+
40
+ export const useSelectTriggerContext = () => useContext(SelectTriggerContext);
41
+
42
+ /**
43
+ * Propagates the item's interaction and selection state (hover, focus, active,
44
+ * highlighted, checked, etc.) to child components like SelectItemLabel.
45
+ */
46
+ export const SelectItemContext = createContext<SelectItemInteractionState>({
47
+ hover: false,
48
+ focus: false,
49
+ active: false,
50
+ disabled: false,
51
+ focusVisible: false,
52
+ highlighted: false,
53
+ checked: false,
54
+ });
55
+
56
+ export const useSelectItemContext = () => useContext(SelectItemContext);
@@ -0,0 +1,115 @@
1
+ import type React from 'react';
2
+ import { forwardRef, useCallback, useRef } from 'react';
3
+ import { Platform, ScrollView } from 'react-native';
4
+ import { FadeIn, FadeOut } from 'react-native-reanimated';
5
+ import { composeEventHandlers, mergeRefs } from '@cdx-ui/utils';
6
+ import {
7
+ OverlayContainer,
8
+ useAnchorPosition,
9
+ useDismissOverlay,
10
+ useOverlayPosition,
11
+ } from '../overlay';
12
+ import { dataAttributes } from '../utils/dataAttributes';
13
+ import { useSelectContext } from './context';
14
+ import type { ISelectContentProps } from './types';
15
+ import { useContentFocus } from './useContentFocus';
16
+ import { useListboxNavigation } from './useListboxNavigation';
17
+
18
+ export const createSelectContent = <T,>(BaseContent: React.ComponentType<T>) =>
19
+ forwardRef(
20
+ (
21
+ { children, enteringAnimation, exitingAnimation, ...props }: ISelectContentProps,
22
+ ref?: any,
23
+ ) => {
24
+ const {
25
+ open,
26
+ setOpen,
27
+ contentId,
28
+ triggerId,
29
+ activeValue,
30
+ setActiveValue,
31
+ value,
32
+ onValueChange,
33
+ native,
34
+ triggerRef,
35
+ items,
36
+ } = useSelectContext();
37
+
38
+ const contentRef = useRef<HTMLElement | null>(null);
39
+
40
+ const focusOnMount = useCallback((node: HTMLElement) => {
41
+ contentRef.current = node;
42
+ if (node && Platform.OS === 'web') {
43
+ requestAnimationFrame(() => {
44
+ setTimeout(() => {
45
+ node.focus?.();
46
+ }, 100);
47
+ });
48
+ }
49
+ }, []);
50
+
51
+ const mergedRef = mergeRefs(ref, focusOnMount);
52
+
53
+ const dismiss = () => setOpen(false);
54
+
55
+ const anchorLayout = useAnchorPosition(triggerRef, open);
56
+ const position = useOverlayPosition(anchorLayout);
57
+
58
+ useDismissOverlay(open, contentRef, [triggerRef], dismiss);
59
+
60
+ const { handleKeyDown } = useListboxNavigation({
61
+ items,
62
+ activeValue,
63
+ setActiveValue,
64
+ onSelect: (val) => {
65
+ onValueChange(val);
66
+ setOpen(false);
67
+ },
68
+ onDismiss: dismiss,
69
+ });
70
+
71
+ useContentFocus({
72
+ open,
73
+ triggerRef,
74
+ items,
75
+ value,
76
+ setActiveValue,
77
+ });
78
+
79
+ if (native || !open || !position) {
80
+ return null;
81
+ }
82
+
83
+ const webProps =
84
+ Platform.OS === 'web'
85
+ ? {
86
+ onKeyDown: composeEventHandlers((props as any).onKeyDown, handleKeyDown),
87
+ tabIndex: -1,
88
+ }
89
+ : {};
90
+
91
+ return (
92
+ <OverlayContainer
93
+ onDismiss={dismiss}
94
+ entering={enteringAnimation ?? FadeIn.duration(150)}
95
+ exiting={exitingAnimation ?? FadeOut.duration(100)}
96
+ style={position.style}
97
+ >
98
+ <BaseContent
99
+ ref={mergedRef}
100
+ role="listbox"
101
+ id={contentId}
102
+ aria-labelledby={triggerId}
103
+ {...dataAttributes({
104
+ state: 'open',
105
+ slot: 'select-content',
106
+ })}
107
+ {...(props as T)}
108
+ {...webProps}
109
+ >
110
+ <ScrollView scrollEventThrottle={16}>{children}</ScrollView>
111
+ </BaseContent>
112
+ </OverlayContainer>
113
+ );
114
+ },
115
+ );
@@ -0,0 +1,27 @@
1
+ import { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import { useSelectContext, useSelectTriggerContext } from './context';
4
+
5
+ export const createSelectIcon = <T,>(BaseIcon: React.ComponentType<T>) =>
6
+ forwardRef((props: any, ref?: any) => {
7
+ const { hover, focus, active, disabled, focusVisible } = useSelectTriggerContext();
8
+ const { required, invalid, readOnly } = useSelectContext();
9
+
10
+ return (
11
+ <BaseIcon
12
+ ref={ref}
13
+ {...dataAttributes({
14
+ hover,
15
+ focus,
16
+ active,
17
+ disabled,
18
+ focusVisible,
19
+ required,
20
+ invalid,
21
+ readonly: readOnly,
22
+ slot: 'select-icon',
23
+ })}
24
+ {...(props as T)}
25
+ />
26
+ );
27
+ });
@@ -0,0 +1,121 @@
1
+ import type React from 'react';
2
+ import { forwardRef, useCallback, useMemo } from 'react';
3
+ import { composeEventHandlers } from '@cdx-ui/utils';
4
+ import { useFocus, useFocusRing } from '@react-native-aria/focus';
5
+ import { useHover, usePress } from '@react-native-aria/interactions';
6
+ import { dataAttributes } from '../utils/dataAttributes';
7
+ import { SelectItemContext, useSelectContext } from './context';
8
+ import type { ISelectItemProps } from './types';
9
+
10
+ export const createSelectItem = <T,>(BaseItem: React.ComponentType<T>) =>
11
+ forwardRef(
12
+ (
13
+ {
14
+ children,
15
+ value: itemValue,
16
+ disabled: disabledProp = false,
17
+ isHovered: isHoveredProp,
18
+ isActive: isActiveProp,
19
+ isFocused: isFocusedProp,
20
+ isFocusVisible: isFocusVisibleProp,
21
+ isDisabled: isDisabledProp,
22
+ ...props
23
+ }: Omit<ISelectItemProps, 'children'> & { children?: React.ReactNode },
24
+ ref?: any,
25
+ ) => {
26
+ const {
27
+ value: selectedValue,
28
+ onValueChange,
29
+ setOpen,
30
+ activeValue,
31
+ contentId,
32
+ disabled: contextDisabled,
33
+ } = useSelectContext();
34
+
35
+ const disabled = contextDisabled || !!disabledProp || !!isDisabledProp;
36
+ const checked = selectedValue === itemValue;
37
+ const highlighted = activeValue === itemValue;
38
+
39
+ const { isFocusVisible, focusProps: focusRingProps }: any = useFocusRing();
40
+ const { pressProps, isPressed: isActive } = usePress({ isDisabled: disabled });
41
+ const { isFocused, focusProps } = useFocus();
42
+ const { isHovered, hoverProps }: any = useHover();
43
+
44
+ const handlePress = useCallback(() => {
45
+ if (!disabled) {
46
+ onValueChange(itemValue);
47
+ setOpen(false);
48
+ }
49
+ }, [disabled, itemValue, onValueChange, setOpen]);
50
+
51
+ const interactionState = useMemo(
52
+ () => ({
53
+ hover: isHoveredProp || isHovered,
54
+ focus: isFocusedProp || isFocused,
55
+ active: isActiveProp || isActive,
56
+ disabled: !!disabled,
57
+ focusVisible: isFocusVisibleProp || isFocusVisible,
58
+ highlighted,
59
+ checked,
60
+ }),
61
+ [
62
+ isHoveredProp,
63
+ isHovered,
64
+ isFocusedProp,
65
+ isFocused,
66
+ isActiveProp,
67
+ isActive,
68
+ disabled,
69
+ isFocusVisibleProp,
70
+ isFocusVisible,
71
+ highlighted,
72
+ checked,
73
+ ],
74
+ );
75
+
76
+ return (
77
+ <SelectItemContext.Provider value={interactionState}>
78
+ <BaseItem
79
+ ref={ref}
80
+ role="option"
81
+ id={`${contentId}-option-${itemValue}`}
82
+ aria-selected={checked}
83
+ aria-disabled={disabled}
84
+ {...dataAttributes({
85
+ hover: interactionState.hover,
86
+ focus: interactionState.focus,
87
+ active: interactionState.active,
88
+ disabled: interactionState.disabled,
89
+ focusVisible: interactionState.focusVisible,
90
+ highlighted: interactionState.highlighted,
91
+ state: checked ? 'checked' : 'unchecked',
92
+ slot: 'select-item',
93
+ })}
94
+ disabled={disabled}
95
+ {...(props as T)}
96
+ onPress={composeEventHandlers(props?.onPress, handlePress)}
97
+ onPressIn={composeEventHandlers(props?.onPressIn, pressProps.onPressIn)}
98
+ onPressOut={composeEventHandlers(props?.onPressOut, pressProps.onPressOut)}
99
+ onHoverIn={composeEventHandlers(props?.onHoverIn, hoverProps.onHoverIn)}
100
+ onHoverOut={composeEventHandlers(props?.onHoverOut, hoverProps.onHoverOut)}
101
+ onFocus={composeEventHandlers(
102
+ composeEventHandlers(
103
+ props?.onFocus as (...args: unknown[]) => unknown,
104
+ focusProps.onFocus,
105
+ ),
106
+ focusRingProps.onFocus,
107
+ )}
108
+ onBlur={composeEventHandlers(
109
+ composeEventHandlers(
110
+ props?.onBlur as (...args: unknown[]) => unknown,
111
+ focusProps.onBlur,
112
+ ),
113
+ focusRingProps.onBlur,
114
+ )}
115
+ >
116
+ {children}
117
+ </BaseItem>
118
+ </SelectItemContext.Provider>
119
+ );
120
+ },
121
+ );
@@ -0,0 +1,30 @@
1
+ import type React from 'react';
2
+ import { forwardRef } from 'react';
3
+ import { dataAttributes } from '../utils/dataAttributes';
4
+ import { useSelectItemContext } from './context';
5
+ import type { ISelectItemLabelProps } from './types';
6
+
7
+ export const createSelectItemLabel = <T,>(BaseItemLabel: React.ComponentType<T>) =>
8
+ forwardRef(({ children, ...props }: ISelectItemLabelProps, ref?: any) => {
9
+ const { hover, focus, active, disabled, focusVisible, highlighted, checked } =
10
+ useSelectItemContext();
11
+
12
+ return (
13
+ <BaseItemLabel
14
+ ref={ref}
15
+ {...(props as T)}
16
+ {...dataAttributes({
17
+ hover,
18
+ focus,
19
+ active,
20
+ disabled,
21
+ focusVisible,
22
+ highlighted,
23
+ state: checked ? 'checked' : 'unchecked',
24
+ slot: 'select-item-label',
25
+ })}
26
+ >
27
+ {children}
28
+ </BaseItemLabel>
29
+ );
30
+ });