@cdx-ui/primitives 0.0.1-beta.159 → 0.0.1-beta.160

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 (233) hide show
  1. package/lib/commonjs/dialog/DialogContent.js +17 -3
  2. package/lib/commonjs/dialog/DialogContent.js.map +1 -1
  3. package/lib/commonjs/dialog/DialogPopup.js +5 -1
  4. package/lib/commonjs/dialog/DialogPopup.js.map +1 -1
  5. package/lib/commonjs/index.js +54 -0
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/menu/MenuCheckboxItem.js +42 -0
  8. package/lib/commonjs/menu/MenuCheckboxItem.js.map +1 -0
  9. package/lib/commonjs/menu/MenuItem.js +142 -0
  10. package/lib/commonjs/menu/MenuItem.js.map +1 -0
  11. package/lib/commonjs/menu/MenuOverlayRoot.js +80 -0
  12. package/lib/commonjs/menu/MenuOverlayRoot.js.map +1 -0
  13. package/lib/commonjs/menu/MenuPopup.js +78 -0
  14. package/lib/commonjs/menu/MenuPopup.js.map +1 -0
  15. package/lib/commonjs/menu/MenuRadioGroup.js +48 -0
  16. package/lib/commonjs/menu/MenuRadioGroup.js.map +1 -0
  17. package/lib/commonjs/menu/MenuRadioItem.js +32 -0
  18. package/lib/commonjs/menu/MenuRadioItem.js.map +1 -0
  19. package/lib/commonjs/menu/MenuRoot.js +139 -0
  20. package/lib/commonjs/menu/MenuRoot.js.map +1 -0
  21. package/lib/commonjs/menu/MenuTrigger.js +59 -0
  22. package/lib/commonjs/menu/MenuTrigger.js.map +1 -0
  23. package/lib/commonjs/menu/context.js +36 -0
  24. package/lib/commonjs/menu/context.js.map +1 -0
  25. package/lib/commonjs/menu/index.js +118 -0
  26. package/lib/commonjs/menu/index.js.map +1 -0
  27. package/lib/commonjs/menu/overlayContext.js +30 -0
  28. package/lib/commonjs/menu/overlayContext.js.map +1 -0
  29. package/lib/commonjs/menu/types.js +6 -0
  30. package/lib/commonjs/menu/types.js.map +1 -0
  31. package/lib/commonjs/menu/useMenuNavigation.js +87 -0
  32. package/lib/commonjs/menu/useMenuNavigation.js.map +1 -0
  33. package/lib/commonjs/menu/useMenuOverlayLifecycle.js +68 -0
  34. package/lib/commonjs/menu/useMenuOverlayLifecycle.js.map +1 -0
  35. package/lib/commonjs/menu/useMenuTrigger.js +115 -0
  36. package/lib/commonjs/menu/useMenuTrigger.js.map +1 -0
  37. package/lib/commonjs/overlay/OverlayContainer.js +2 -3
  38. package/lib/commonjs/overlay/OverlayContainer.js.map +1 -1
  39. package/lib/commonjs/overlay/OverlayStack.js +125 -0
  40. package/lib/commonjs/overlay/OverlayStack.js.map +1 -0
  41. package/lib/commonjs/overlay/constants.js +7 -1
  42. package/lib/commonjs/overlay/constants.js.map +1 -1
  43. package/lib/commonjs/overlay/getAnchoredOverlayPosition.js +188 -0
  44. package/lib/commonjs/overlay/getAnchoredOverlayPosition.js.map +1 -0
  45. package/lib/commonjs/overlay/index.js +39 -0
  46. package/lib/commonjs/overlay/index.js.map +1 -1
  47. package/lib/commonjs/overlay/useAnchoredOverlay.js +127 -0
  48. package/lib/commonjs/overlay/useAnchoredOverlay.js.map +1 -0
  49. package/lib/commonjs/overlay/useDismissOverlay.js +8 -1
  50. package/lib/commonjs/overlay/useDismissOverlay.js.map +1 -1
  51. package/lib/commonjs/overlay/useDismissOverlay.web.js +11 -3
  52. package/lib/commonjs/overlay/useDismissOverlay.web.js.map +1 -1
  53. package/lib/commonjs/overlay/useEscapeKey.js +8 -1
  54. package/lib/commonjs/overlay/useEscapeKey.js.map +1 -1
  55. package/lib/commonjs/overlay/useEscapeKey.web.js +11 -3
  56. package/lib/commonjs/overlay/useEscapeKey.web.js.map +1 -1
  57. package/lib/commonjs/overlay/useOverlayPosition.js +16 -7
  58. package/lib/commonjs/overlay/useOverlayPosition.js.map +1 -1
  59. package/lib/commonjs/portal/portal.js +86 -63
  60. package/lib/commonjs/portal/portal.js.map +1 -1
  61. package/lib/commonjs/select/SelectContent.js +23 -11
  62. package/lib/commonjs/select/SelectContent.js.map +1 -1
  63. package/lib/commonjs/select/useListboxNavigation.js +10 -11
  64. package/lib/commonjs/select/useListboxNavigation.js.map +1 -1
  65. package/lib/commonjs/tooltip/TooltipContent.js +25 -24
  66. package/lib/commonjs/tooltip/TooltipContent.js.map +1 -1
  67. package/lib/commonjs/tooltip/getTooltipPosition.js +37 -84
  68. package/lib/commonjs/tooltip/getTooltipPosition.js.map +1 -1
  69. package/lib/module/dialog/DialogContent.js +18 -4
  70. package/lib/module/dialog/DialogContent.js.map +1 -1
  71. package/lib/module/dialog/DialogPopup.js +5 -1
  72. package/lib/module/dialog/DialogPopup.js.map +1 -1
  73. package/lib/module/index.js +2 -1
  74. package/lib/module/index.js.map +1 -1
  75. package/lib/module/menu/MenuCheckboxItem.js +38 -0
  76. package/lib/module/menu/MenuCheckboxItem.js.map +1 -0
  77. package/lib/module/menu/MenuItem.js +138 -0
  78. package/lib/module/menu/MenuItem.js.map +1 -0
  79. package/lib/module/menu/MenuOverlayRoot.js +76 -0
  80. package/lib/module/menu/MenuOverlayRoot.js.map +1 -0
  81. package/lib/module/menu/MenuPopup.js +74 -0
  82. package/lib/module/menu/MenuPopup.js.map +1 -0
  83. package/lib/module/menu/MenuRadioGroup.js +44 -0
  84. package/lib/module/menu/MenuRadioGroup.js.map +1 -0
  85. package/lib/module/menu/MenuRadioItem.js +28 -0
  86. package/lib/module/menu/MenuRadioItem.js.map +1 -0
  87. package/lib/module/menu/MenuRoot.js +135 -0
  88. package/lib/module/menu/MenuRoot.js.map +1 -0
  89. package/lib/module/menu/MenuTrigger.js +55 -0
  90. package/lib/module/menu/MenuTrigger.js.map +1 -0
  91. package/lib/module/menu/context.js +27 -0
  92. package/lib/module/menu/context.js.map +1 -0
  93. package/lib/module/menu/index.js +13 -0
  94. package/lib/module/menu/index.js.map +1 -0
  95. package/lib/module/menu/overlayContext.js +24 -0
  96. package/lib/module/menu/overlayContext.js.map +1 -0
  97. package/lib/module/menu/types.js +4 -0
  98. package/lib/module/menu/types.js.map +1 -0
  99. package/lib/module/menu/useMenuNavigation.js +84 -0
  100. package/lib/module/menu/useMenuNavigation.js.map +1 -0
  101. package/lib/module/menu/useMenuOverlayLifecycle.js +63 -0
  102. package/lib/module/menu/useMenuOverlayLifecycle.js.map +1 -0
  103. package/lib/module/menu/useMenuTrigger.js +111 -0
  104. package/lib/module/menu/useMenuTrigger.js.map +1 -0
  105. package/lib/module/overlay/OverlayContainer.js +2 -3
  106. package/lib/module/overlay/OverlayContainer.js.map +1 -1
  107. package/lib/module/overlay/OverlayStack.js +119 -0
  108. package/lib/module/overlay/OverlayStack.js.map +1 -0
  109. package/lib/module/overlay/constants.js +6 -0
  110. package/lib/module/overlay/constants.js.map +1 -1
  111. package/lib/module/overlay/getAnchoredOverlayPosition.js +183 -0
  112. package/lib/module/overlay/getAnchoredOverlayPosition.js.map +1 -0
  113. package/lib/module/overlay/index.js +4 -1
  114. package/lib/module/overlay/index.js.map +1 -1
  115. package/lib/module/overlay/useAnchoredOverlay.js +123 -0
  116. package/lib/module/overlay/useAnchoredOverlay.js.map +1 -0
  117. package/lib/module/overlay/useDismissOverlay.js +9 -1
  118. package/lib/module/overlay/useDismissOverlay.js.map +1 -1
  119. package/lib/module/overlay/useDismissOverlay.web.js +11 -3
  120. package/lib/module/overlay/useDismissOverlay.web.js.map +1 -1
  121. package/lib/module/overlay/useEscapeKey.js +9 -1
  122. package/lib/module/overlay/useEscapeKey.js.map +1 -1
  123. package/lib/module/overlay/useEscapeKey.web.js +11 -3
  124. package/lib/module/overlay/useEscapeKey.web.js.map +1 -1
  125. package/lib/module/overlay/useOverlayPosition.js +16 -7
  126. package/lib/module/overlay/useOverlayPosition.js.map +1 -1
  127. package/lib/module/portal/portal.js +86 -64
  128. package/lib/module/portal/portal.js.map +1 -1
  129. package/lib/module/select/SelectContent.js +25 -13
  130. package/lib/module/select/SelectContent.js.map +1 -1
  131. package/lib/module/select/useListboxNavigation.js +10 -11
  132. package/lib/module/select/useListboxNavigation.js.map +1 -1
  133. package/lib/module/tooltip/TooltipContent.js +30 -29
  134. package/lib/module/tooltip/TooltipContent.js.map +1 -1
  135. package/lib/module/tooltip/getTooltipPosition.js +37 -84
  136. package/lib/module/tooltip/getTooltipPosition.js.map +1 -1
  137. package/lib/typescript/dialog/DialogContent.d.ts.map +1 -1
  138. package/lib/typescript/dialog/DialogPopup.d.ts.map +1 -1
  139. package/lib/typescript/index.d.ts +2 -1
  140. package/lib/typescript/index.d.ts.map +1 -1
  141. package/lib/typescript/menu/MenuCheckboxItem.d.ts +6 -0
  142. package/lib/typescript/menu/MenuCheckboxItem.d.ts.map +1 -0
  143. package/lib/typescript/menu/MenuItem.d.ts +8 -0
  144. package/lib/typescript/menu/MenuItem.d.ts.map +1 -0
  145. package/lib/typescript/menu/MenuOverlayRoot.d.ts +8 -0
  146. package/lib/typescript/menu/MenuOverlayRoot.d.ts.map +1 -0
  147. package/lib/typescript/menu/MenuPopup.d.ts +8 -0
  148. package/lib/typescript/menu/MenuPopup.d.ts.map +1 -0
  149. package/lib/typescript/menu/MenuRadioGroup.d.ts +8 -0
  150. package/lib/typescript/menu/MenuRadioGroup.d.ts.map +1 -0
  151. package/lib/typescript/menu/MenuRadioItem.d.ts +6 -0
  152. package/lib/typescript/menu/MenuRadioItem.d.ts.map +1 -0
  153. package/lib/typescript/menu/MenuRoot.d.ts +8 -0
  154. package/lib/typescript/menu/MenuRoot.d.ts.map +1 -0
  155. package/lib/typescript/menu/MenuTrigger.d.ts +7 -0
  156. package/lib/typescript/menu/MenuTrigger.d.ts.map +1 -0
  157. package/lib/typescript/menu/context.d.ts +15 -0
  158. package/lib/typescript/menu/context.d.ts.map +1 -0
  159. package/lib/typescript/menu/index.d.ts +12 -0
  160. package/lib/typescript/menu/index.d.ts.map +1 -0
  161. package/lib/typescript/menu/overlayContext.d.ts +35 -0
  162. package/lib/typescript/menu/overlayContext.d.ts.map +1 -0
  163. package/lib/typescript/menu/types.d.ts +104 -0
  164. package/lib/typescript/menu/types.d.ts.map +1 -0
  165. package/lib/typescript/menu/useMenuNavigation.d.ts +16 -0
  166. package/lib/typescript/menu/useMenuNavigation.d.ts.map +1 -0
  167. package/lib/typescript/menu/useMenuOverlayLifecycle.d.ts +15 -0
  168. package/lib/typescript/menu/useMenuOverlayLifecycle.d.ts.map +1 -0
  169. package/lib/typescript/menu/useMenuTrigger.d.ts +35 -0
  170. package/lib/typescript/menu/useMenuTrigger.d.ts.map +1 -0
  171. package/lib/typescript/overlay/OverlayContainer.d.ts.map +1 -1
  172. package/lib/typescript/overlay/OverlayStack.d.ts +46 -0
  173. package/lib/typescript/overlay/OverlayStack.d.ts.map +1 -0
  174. package/lib/typescript/overlay/constants.d.ts +5 -0
  175. package/lib/typescript/overlay/constants.d.ts.map +1 -1
  176. package/lib/typescript/overlay/getAnchoredOverlayPosition.d.ts +45 -0
  177. package/lib/typescript/overlay/getAnchoredOverlayPosition.d.ts.map +1 -0
  178. package/lib/typescript/overlay/index.d.ts +5 -2
  179. package/lib/typescript/overlay/index.d.ts.map +1 -1
  180. package/lib/typescript/overlay/useAnchoredOverlay.d.ts +15 -0
  181. package/lib/typescript/overlay/useAnchoredOverlay.d.ts.map +1 -0
  182. package/lib/typescript/overlay/useDismissOverlay.d.ts +2 -1
  183. package/lib/typescript/overlay/useDismissOverlay.d.ts.map +1 -1
  184. package/lib/typescript/overlay/useDismissOverlay.web.d.ts +2 -1
  185. package/lib/typescript/overlay/useDismissOverlay.web.d.ts.map +1 -1
  186. package/lib/typescript/overlay/useEscapeKey.d.ts +2 -1
  187. package/lib/typescript/overlay/useEscapeKey.d.ts.map +1 -1
  188. package/lib/typescript/overlay/useEscapeKey.web.d.ts +2 -1
  189. package/lib/typescript/overlay/useEscapeKey.web.d.ts.map +1 -1
  190. package/lib/typescript/overlay/useOverlayPosition.d.ts +5 -1
  191. package/lib/typescript/overlay/useOverlayPosition.d.ts.map +1 -1
  192. package/lib/typescript/portal/portal.d.ts +3 -0
  193. package/lib/typescript/portal/portal.d.ts.map +1 -1
  194. package/lib/typescript/select/SelectContent.d.ts.map +1 -1
  195. package/lib/typescript/select/useListboxNavigation.d.ts.map +1 -1
  196. package/lib/typescript/tooltip/TooltipContent.d.ts.map +1 -1
  197. package/lib/typescript/tooltip/getTooltipPosition.d.ts +6 -8
  198. package/lib/typescript/tooltip/getTooltipPosition.d.ts.map +1 -1
  199. package/package.json +3 -3
  200. package/src/dialog/DialogContent.tsx +16 -4
  201. package/src/dialog/DialogPopup.tsx +5 -1
  202. package/src/index.ts +23 -0
  203. package/src/menu/MenuCheckboxItem.tsx +51 -0
  204. package/src/menu/MenuItem.tsx +197 -0
  205. package/src/menu/MenuOverlayRoot.tsx +100 -0
  206. package/src/menu/MenuPopup.tsx +81 -0
  207. package/src/menu/MenuRadioGroup.tsx +48 -0
  208. package/src/menu/MenuRadioItem.tsx +32 -0
  209. package/src/menu/MenuRoot.tsx +190 -0
  210. package/src/menu/MenuTrigger.tsx +57 -0
  211. package/src/menu/context.tsx +38 -0
  212. package/src/menu/index.ts +36 -0
  213. package/src/menu/overlayContext.tsx +61 -0
  214. package/src/menu/types.ts +119 -0
  215. package/src/menu/useMenuNavigation.ts +102 -0
  216. package/src/menu/useMenuOverlayLifecycle.ts +77 -0
  217. package/src/menu/useMenuTrigger.ts +161 -0
  218. package/src/overlay/OverlayContainer.tsx +3 -1
  219. package/src/overlay/OverlayStack.tsx +202 -0
  220. package/src/overlay/constants.ts +6 -0
  221. package/src/overlay/getAnchoredOverlayPosition.ts +326 -0
  222. package/src/overlay/index.ts +29 -1
  223. package/src/overlay/useAnchoredOverlay.ts +153 -0
  224. package/src/overlay/useDismissOverlay.ts +10 -2
  225. package/src/overlay/useDismissOverlay.web.ts +9 -2
  226. package/src/overlay/useEscapeKey.ts +13 -1
  227. package/src/overlay/useEscapeKey.web.ts +13 -3
  228. package/src/overlay/useOverlayPosition.ts +35 -8
  229. package/src/portal/portal.tsx +109 -69
  230. package/src/select/SelectContent.tsx +29 -17
  231. package/src/select/useListboxNavigation.ts +12 -9
  232. package/src/tooltip/TooltipContent.tsx +32 -34
  233. package/src/tooltip/getTooltipPosition.ts +58 -125
package/src/index.ts CHANGED
@@ -21,17 +21,40 @@ export * from './date-time-list';
21
21
  export * from './date-time-picker';
22
22
  export * from './link';
23
23
  export * from './list-item';
24
+ export * from './menu';
24
25
  export {
25
26
  MODAL_OVERLAY_Z_INDEX,
27
+ NOTIFICATION_OVERLAY_Z_INDEX,
28
+ OVERLAY_PLACEMENTS,
26
29
  type EdgeInsets,
30
+ getAnchoredOverlayPosition,
27
31
  OverlayContainer,
28
32
  OverlayInsetsProvider,
33
+ OverlayStackProvider,
29
34
  useAnchorPosition,
35
+ useAnchoredOverlay,
30
36
  useDismissOverlay,
31
37
  useEscapeKey,
38
+ useOverlayStackRegistration,
32
39
  useOverlayPosition,
33
40
  type AnchorLayout,
41
+ type AnchoredOverlayPosition,
42
+ type AnchoredOverlayPositionOptions,
43
+ type FloatingLayout,
44
+ type OverlayAlign,
45
+ type OverlayCollisionBehavior,
46
+ type OverlayPlacement,
34
47
  type OverlayPosition,
48
+ type OverlaySide,
49
+ type OverlayStackCapability,
50
+ type OverlayStackLayer,
51
+ type OverlayStackMetadata,
52
+ type OverlayStackRegistrationOptions,
53
+ type OverlayStackRegistrationResult,
54
+ type OverlayWidthPolicy,
55
+ type ResolvedOverlayPlacement,
56
+ type UseAnchoredOverlayOptions,
57
+ type UseAnchoredOverlayResult,
35
58
  } from './overlay';
36
59
  export * from './select';
37
60
  export * from './switch';
@@ -0,0 +1,51 @@
1
+ import { forwardRef, type ComponentRef } from 'react';
2
+ import { Pressable } from 'react-native';
3
+ import { composeEventHandlers, useControllableState } from '@cdx-ui/utils';
4
+ import { MenuItem } from './MenuItem';
5
+ import type { IMenuCheckboxItemProps } from './types';
6
+
7
+ export interface MenuCheckboxItemProps extends IMenuCheckboxItemProps {
8
+ asChild?: boolean;
9
+ }
10
+
11
+ export const MenuCheckboxItem = forwardRef<ComponentRef<typeof Pressable>, MenuCheckboxItemProps>(
12
+ (
13
+ {
14
+ asChild,
15
+ children,
16
+ isChecked: checkedProp,
17
+ defaultIsChecked = false,
18
+ onChange,
19
+ onPress,
20
+ closeOnSelect = false,
21
+ ...props
22
+ },
23
+ ref,
24
+ ) => {
25
+ const [checked = false, setCheckedState] = useControllableState<boolean>({
26
+ prop: checkedProp,
27
+ defaultProp: defaultIsChecked,
28
+ });
29
+ const toggleChecked = () => {
30
+ const nextChecked = !checked;
31
+ setCheckedState(nextChecked);
32
+ onChange?.(nextChecked);
33
+ };
34
+
35
+ return (
36
+ <MenuItem
37
+ ref={ref}
38
+ asChild={asChild}
39
+ itemRole="menuitemcheckbox"
40
+ isChecked={checked}
41
+ closeOnSelect={closeOnSelect}
42
+ onPress={composeEventHandlers(onPress, toggleChecked)}
43
+ {...props}
44
+ >
45
+ {children}
46
+ </MenuItem>
47
+ );
48
+ },
49
+ );
50
+
51
+ MenuCheckboxItem.displayName = 'MenuCheckboxItem';
@@ -0,0 +1,197 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useId,
6
+ useMemo,
7
+ useRef,
8
+ type ComponentRef,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import { Platform, Pressable, type GestureResponderEvent, type PressableProps } from 'react-native';
12
+ import { composeEventHandlers, mergeRefs } from '@cdx-ui/utils';
13
+ import { useFocus, useFocusRing } from '@react-native-aria/focus';
14
+ import { useHover, usePress } from '@react-native-aria/interactions';
15
+ import { Slot } from '../slot';
16
+ import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
17
+ import { MenuItemProvider, useMenuContext } from './context';
18
+ import type { IMenuItemProps } from './types';
19
+
20
+ export interface MenuItemProps extends Omit<IMenuItemProps, 'children'> {
21
+ asChild?: boolean;
22
+ children?: ReactNode;
23
+ }
24
+
25
+ export const MenuItem = forwardRef<ComponentRef<typeof Pressable>, MenuItemProps>(
26
+ (
27
+ {
28
+ asChild,
29
+ children,
30
+ itemRole = 'menuitem',
31
+ isChecked = false,
32
+ disabled: disabledProp = false,
33
+ closeOnSelect: closeOnSelectProp,
34
+ isHovered: isHoveredProp,
35
+ isActive: isActiveProp,
36
+ isFocused: isFocusedProp,
37
+ isFocusVisible: isFocusVisibleProp,
38
+ isDisabled: isDisabledProp,
39
+ onPress,
40
+ ...props
41
+ },
42
+ ref,
43
+ ) => {
44
+ const {
45
+ menuId,
46
+ registerItem,
47
+ unregisterItem,
48
+ focusedItemId,
49
+ setFocusedItemId,
50
+ closeOnSelect: closeOverlayOnSelect,
51
+ dismissOverlay,
52
+ } = useMenuContext();
53
+
54
+ const reactId = useId();
55
+ const itemId = `menu-item-${reactId}`;
56
+ const domId = props.id ?? `${menuId}-item-${reactId}`;
57
+ const disabled = !!disabledProp || !!isDisabledProp;
58
+ const highlighted = focusedItemId === itemId;
59
+ const isSelectable = itemRole === 'menuitemcheckbox' || itemRole === 'menuitemradio';
60
+ const closeOnSelect = closeOnSelectProp ?? closeOverlayOnSelect;
61
+ const resolvedRole: PressableProps['role'] =
62
+ Platform.OS === 'web'
63
+ ? (itemRole as PressableProps['role'])
64
+ : itemRole === 'menuitemcheckbox'
65
+ ? 'checkbox'
66
+ : itemRole === 'menuitemradio'
67
+ ? 'radio'
68
+ : 'menuitem';
69
+
70
+ const nodeRef = useRef<ComponentRef<typeof Pressable> | null>(null);
71
+ const mergedRef = mergeRefs(ref, nodeRef);
72
+ const onPressRef = useRef(onPress);
73
+ onPressRef.current = onPress;
74
+
75
+ const invokePress = useCallback(
76
+ (event: GestureResponderEvent) => {
77
+ if (!disabled) {
78
+ onPressRef.current?.(event);
79
+ if (closeOnSelect) {
80
+ dismissOverlay?.('select');
81
+ }
82
+ }
83
+ },
84
+ [disabled, closeOnSelect, dismissOverlay],
85
+ );
86
+
87
+ useEffect(() => {
88
+ registerItem(itemId, {
89
+ disabled,
90
+ node: nodeRef.current as { focus?: () => void } | null,
91
+ });
92
+ return () => unregisterItem(itemId);
93
+ }, [itemId, disabled, registerItem, unregisterItem]);
94
+
95
+ const { isFocusVisible, focusProps: focusRingProps } = useFocusRing() as {
96
+ isFocusVisible: boolean;
97
+ focusProps: {
98
+ onFocus?: (...args: unknown[]) => unknown;
99
+ onBlur?: (...args: unknown[]) => unknown;
100
+ };
101
+ };
102
+ const { pressProps, isPressed: isActive } = usePress({ isDisabled: disabled });
103
+ const { isFocused, focusProps } = useFocus();
104
+ const { isHovered, hoverProps } = useHover();
105
+
106
+ const interactionState = useMemo(
107
+ () => ({
108
+ hover: !!(isHoveredProp || isHovered),
109
+ focus: !!(isFocusedProp || isFocused),
110
+ active: !!(isActiveProp || isActive),
111
+ disabled: !!disabled,
112
+ focusVisible: !!(isFocusVisibleProp || isFocusVisible),
113
+ highlighted,
114
+ checked: isChecked,
115
+ }),
116
+ [
117
+ isHoveredProp,
118
+ isHovered,
119
+ isFocusedProp,
120
+ isFocused,
121
+ isActiveProp,
122
+ isActive,
123
+ disabled,
124
+ isFocusVisibleProp,
125
+ isFocusVisible,
126
+ highlighted,
127
+ isChecked,
128
+ ],
129
+ );
130
+
131
+ const isRovingTabStop = highlighted;
132
+ const webProps = Platform.OS === 'web' ? { tabIndex: isRovingTabStop ? 0 : -1 } : {};
133
+
134
+ const handlePressWithFocus = useCallback(
135
+ (_: GestureResponderEvent) => {
136
+ if (!disabled) {
137
+ setFocusedItemId(itemId);
138
+ }
139
+ },
140
+ [disabled, itemId, setFocusedItemId],
141
+ );
142
+
143
+ const Comp = asChild ? Slot : Pressable;
144
+
145
+ return (
146
+ <MenuItemProvider value={interactionState}>
147
+ <Comp
148
+ ref={mergedRef}
149
+ role={resolvedRole}
150
+ id={domId}
151
+ aria-checked={isSelectable ? isChecked : undefined}
152
+ aria-disabled={disabled}
153
+ disabled={disabled}
154
+ accessibilityState={{ disabled, ...(isSelectable ? { checked: isChecked } : {}) }}
155
+ {...webProps}
156
+ {...props}
157
+ {...mergeDataAttributes(
158
+ props,
159
+ dataAttributes({
160
+ hover: interactionState.hover,
161
+ focus: interactionState.focus,
162
+ active: interactionState.active,
163
+ disabled: interactionState.disabled,
164
+ focusVisible: interactionState.focusVisible,
165
+ highlighted: interactionState.highlighted,
166
+ state: isSelectable ? (isChecked ? 'checked' : 'unchecked') : undefined,
167
+ }),
168
+ )}
169
+ onPress={composeEventHandlers(invokePress, handlePressWithFocus)}
170
+ onPressIn={composeEventHandlers(props?.onPressIn, pressProps.onPressIn)}
171
+ onPressOut={composeEventHandlers(props?.onPressOut, pressProps.onPressOut)}
172
+ onHoverIn={composeEventHandlers(props?.onHoverIn, hoverProps.onHoverIn)}
173
+ onHoverOut={composeEventHandlers(props?.onHoverOut, hoverProps.onHoverOut)}
174
+ onFocus={composeEventHandlers(
175
+ composeEventHandlers(
176
+ props?.onFocus as (...args: unknown[]) => unknown,
177
+ focusProps.onFocus,
178
+ ),
179
+ focusRingProps.onFocus,
180
+ () => setFocusedItemId(itemId),
181
+ )}
182
+ onBlur={composeEventHandlers(
183
+ composeEventHandlers(
184
+ props?.onBlur as (...args: unknown[]) => unknown,
185
+ focusProps.onBlur,
186
+ ),
187
+ focusRingProps.onBlur,
188
+ )}
189
+ >
190
+ {children}
191
+ </Comp>
192
+ </MenuItemProvider>
193
+ );
194
+ },
195
+ );
196
+
197
+ MenuItem.displayName = 'MenuItem';
@@ -0,0 +1,100 @@
1
+ import { forwardRef, useCallback, useId, useMemo, useRef, type ComponentRef } from 'react';
2
+ import { View } from 'react-native';
3
+ import { useControllableState } from '@cdx-ui/utils';
4
+ import { Slot } from '../slot';
5
+ import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
6
+ import { MenuOverlayProvider, type MenuFocusOnOpen } from './overlayContext';
7
+ import type { IMenuOverlayRootProps, MenuCloseReason } from './types';
8
+
9
+ export interface MenuOverlayRootProps extends IMenuOverlayRootProps {
10
+ asChild?: boolean;
11
+ className?: string;
12
+ }
13
+
14
+ export const MenuOverlayRoot = forwardRef<ComponentRef<typeof View>, MenuOverlayRootProps>(
15
+ (
16
+ {
17
+ asChild,
18
+ children,
19
+ isOpen: openProp,
20
+ defaultOpen = false,
21
+ onOpenChange,
22
+ disabled = false,
23
+ placement = 'bottom-start',
24
+ offset = 4,
25
+ crossOffset = 0,
26
+ ...props
27
+ },
28
+ ref,
29
+ ) => {
30
+ const [open, setOpenState] = useControllableState<boolean>({
31
+ prop: openProp,
32
+ defaultProp: defaultOpen,
33
+ onChange: onOpenChange,
34
+ });
35
+ const closeReasonRef = useRef<MenuCloseReason>('programmatic');
36
+
37
+ const setOpen = useCallback(
38
+ (nextOpen: boolean) => {
39
+ if (disabled && nextOpen) {
40
+ return;
41
+ }
42
+ if (nextOpen) {
43
+ closeReasonRef.current = 'programmatic';
44
+ }
45
+ setOpenState(nextOpen);
46
+ },
47
+ [disabled, setOpenState],
48
+ );
49
+
50
+ const triggerRef = useRef<ComponentRef<typeof View> | null>(null);
51
+ const contentRef = useRef<ComponentRef<typeof View> | null>(null);
52
+ const focusOnOpenRef = useRef<MenuFocusOnOpen>('menu');
53
+ const reactId = useId();
54
+ const triggerId = `menu-trigger-${reactId}`;
55
+ const menuId = `menu-content-${reactId}`;
56
+
57
+ const closeMenu = useCallback(
58
+ (reason: MenuCloseReason = 'programmatic') => {
59
+ closeReasonRef.current = reason;
60
+ setOpen(false);
61
+ },
62
+ [setOpen],
63
+ );
64
+
65
+ const contextValue = useMemo(
66
+ () => ({
67
+ open: open ?? false,
68
+ setOpen,
69
+ closeMenu,
70
+ disabled,
71
+ placement,
72
+ offset,
73
+ crossOffset,
74
+ triggerRef,
75
+ triggerId,
76
+ menuId,
77
+ contentRef,
78
+ focusOnOpenRef,
79
+ closeReasonRef,
80
+ }),
81
+ [open, setOpen, closeMenu, disabled, placement, offset, crossOffset, triggerId, menuId],
82
+ );
83
+
84
+ const Comp = asChild ? Slot : View;
85
+
86
+ return (
87
+ <MenuOverlayProvider value={contextValue}>
88
+ <Comp
89
+ ref={ref}
90
+ {...props}
91
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'menu' }))}
92
+ >
93
+ {children}
94
+ </Comp>
95
+ </MenuOverlayProvider>
96
+ );
97
+ },
98
+ );
99
+
100
+ MenuOverlayRoot.displayName = 'MenuOverlayRoot';
@@ -0,0 +1,81 @@
1
+ import { forwardRef, type ComponentRef } from 'react';
2
+ import { View } from 'react-native';
3
+ import { FadeIn, FadeOut } from 'react-native-reanimated';
4
+ import { composeEventHandlers, mergeRefs } from '@cdx-ui/utils';
5
+ import { OverlayContainer, useAnchoredOverlay, useDismissOverlay, useEscapeKey } from '../overlay';
6
+ import { Slot } from '../slot';
7
+ import { dataAttributes, mergeDataAttributes } from '../utils/dataAttributes';
8
+ import { MenuPopupProvider, useMenuOverlayContext } from './overlayContext';
9
+ import type { IMenuPopupProps } from './types';
10
+ import { useMenuOverlayCloseFocus } from './useMenuOverlayLifecycle';
11
+
12
+ export interface MenuPopupProps extends IMenuPopupProps {
13
+ asChild?: boolean;
14
+ className?: string;
15
+ }
16
+
17
+ export const MenuPopup = forwardRef<ComponentRef<typeof View>, MenuPopupProps>(
18
+ ({ asChild, children, onLayout, style, ...props }, ref) => {
19
+ const overlay = useMenuOverlayContext();
20
+ const { open, closeMenu, triggerRef, placement, offset, crossOffset } = overlay;
21
+
22
+ const dismiss = () => closeMenu('outside');
23
+
24
+ const {
25
+ floatingRef,
26
+ onFloatingLayout,
27
+ style: overlayStyle,
28
+ position,
29
+ } = useAnchoredOverlay(triggerRef, open, {
30
+ placement,
31
+ offset,
32
+ crossOffset,
33
+ collisionBehavior: 'flip-shift',
34
+ width: 'content',
35
+ });
36
+
37
+ useDismissOverlay(open, floatingRef, [triggerRef], dismiss);
38
+ useEscapeKey(open, () => closeMenu('escape'));
39
+ useMenuOverlayCloseFocus(overlay);
40
+
41
+ if (!open) {
42
+ return null;
43
+ }
44
+
45
+ const Comp = asChild ? Slot : View;
46
+
47
+ return (
48
+ <MenuPopupProvider
49
+ value={{
50
+ availableHeight: position?.availableHeight,
51
+ availableWidth: position?.availableWidth,
52
+ }}
53
+ >
54
+ <OverlayContainer
55
+ onDismiss={dismiss}
56
+ entering={FadeIn.duration(150)}
57
+ exiting={FadeOut.duration(100)}
58
+ >
59
+ <Comp
60
+ ref={mergeRefs(ref, floatingRef)}
61
+ onLayout={composeEventHandlers(onLayout, onFloatingLayout)}
62
+ style={[style, overlayStyle]}
63
+ {...props}
64
+ {...mergeDataAttributes(
65
+ props,
66
+ dataAttributes({
67
+ slot: 'menu-popup',
68
+ side: position?.resolvedPlacement.side,
69
+ align: position?.resolvedPlacement.align,
70
+ }),
71
+ )}
72
+ >
73
+ {children}
74
+ </Comp>
75
+ </OverlayContainer>
76
+ </MenuPopupProvider>
77
+ );
78
+ },
79
+ );
80
+
81
+ MenuPopup.displayName = 'MenuPopup';
@@ -0,0 +1,48 @@
1
+ import { forwardRef, useCallback, useMemo, type ComponentRef } from 'react';
2
+ import { View } from 'react-native';
3
+ import { useControllableState } from '@cdx-ui/utils';
4
+ import { Slot } from '../slot';
5
+ import { MenuRadioGroupProvider } from './context';
6
+ import type { IMenuRadioGroupProps } from './types';
7
+
8
+ export interface MenuRadioGroupProps extends IMenuRadioGroupProps {
9
+ asChild?: boolean;
10
+ className?: string;
11
+ }
12
+
13
+ export const MenuRadioGroup = forwardRef<ComponentRef<typeof View>, MenuRadioGroupProps>(
14
+ ({ asChild, children, value: valueProp, defaultValue, onChange, ...props }, ref) => {
15
+ const [value, setValueState] = useControllableState<string>({
16
+ prop: valueProp,
17
+ defaultProp: defaultValue,
18
+ });
19
+ const setValue = useCallback(
20
+ (nextValue: string) => {
21
+ if (nextValue === value) {
22
+ return;
23
+ }
24
+ setValueState(nextValue);
25
+ onChange?.(nextValue);
26
+ },
27
+ [onChange, setValueState, value],
28
+ );
29
+ const contextValue = useMemo(
30
+ () => ({
31
+ value,
32
+ setValue,
33
+ }),
34
+ [setValue, value],
35
+ );
36
+ const Comp = asChild ? Slot : View;
37
+
38
+ return (
39
+ <MenuRadioGroupProvider value={contextValue}>
40
+ <Comp ref={ref} role="group" {...props}>
41
+ {children}
42
+ </Comp>
43
+ </MenuRadioGroupProvider>
44
+ );
45
+ },
46
+ );
47
+
48
+ MenuRadioGroup.displayName = 'MenuRadioGroup';
@@ -0,0 +1,32 @@
1
+ import { forwardRef, type ComponentRef } from 'react';
2
+ import { Pressable } from 'react-native';
3
+ import { composeEventHandlers } from '@cdx-ui/utils';
4
+ import { useMenuRadioGroupContext } from './context';
5
+ import { MenuItem } from './MenuItem';
6
+ import type { IMenuRadioItemProps } from './types';
7
+
8
+ export interface MenuRadioItemProps extends IMenuRadioItemProps {
9
+ asChild?: boolean;
10
+ }
11
+
12
+ export const MenuRadioItem = forwardRef<ComponentRef<typeof Pressable>, MenuRadioItemProps>(
13
+ ({ asChild, children, value, onPress, ...props }, ref) => {
14
+ const radioGroup = useMenuRadioGroupContext();
15
+ const checked = radioGroup.value === value;
16
+
17
+ return (
18
+ <MenuItem
19
+ ref={ref}
20
+ asChild={asChild}
21
+ itemRole="menuitemradio"
22
+ isChecked={checked}
23
+ onPress={composeEventHandlers(onPress, () => radioGroup.setValue(value))}
24
+ {...props}
25
+ >
26
+ {children}
27
+ </MenuItem>
28
+ );
29
+ },
30
+ );
31
+
32
+ MenuRadioItem.displayName = 'MenuRadioItem';