@cdx-ui/primitives 0.0.1-beta.5 → 0.0.1-beta.50

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 (259) hide show
  1. package/README.md +51 -25
  2. package/lib/commonjs/CLAUDE.md +32 -0
  3. package/lib/commonjs/avatar/createAvatarText.js +9 -1
  4. package/lib/commonjs/avatar/createAvatarText.js.map +1 -1
  5. package/lib/commonjs/button/createButtonText.js +1 -0
  6. package/lib/commonjs/button/createButtonText.js.map +1 -1
  7. package/lib/commonjs/checkbox/createCheckboxRoot.web.js +8 -3
  8. package/lib/commonjs/checkbox/createCheckboxRoot.web.js.map +1 -1
  9. package/lib/commonjs/chip/createChipRoot.js +132 -0
  10. package/lib/commonjs/chip/createChipRoot.js.map +1 -0
  11. package/lib/commonjs/chip/index.js +16 -0
  12. package/lib/commonjs/chip/index.js.map +1 -0
  13. package/lib/commonjs/chip/types.js +6 -0
  14. package/lib/commonjs/chip/types.js.map +1 -0
  15. package/lib/commonjs/field/createFieldLabel.js.map +1 -1
  16. package/lib/commonjs/index.js +45 -1
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/input/createInputField.js +5 -0
  19. package/lib/commonjs/input/createInputField.js.map +1 -1
  20. package/lib/commonjs/link/context.js +11 -0
  21. package/lib/commonjs/link/context.js.map +1 -0
  22. package/lib/commonjs/link/createLink.js +28 -14
  23. package/lib/commonjs/link/createLink.js.map +1 -1
  24. package/lib/commonjs/link/index.js +13 -0
  25. package/lib/commonjs/link/index.js.map +1 -1
  26. package/lib/commonjs/list-item/createListItemRoot.js +36 -21
  27. package/lib/commonjs/list-item/createListItemRoot.js.map +1 -1
  28. package/lib/commonjs/radio/context.js +14 -0
  29. package/lib/commonjs/radio/context.js.map +1 -0
  30. package/lib/commonjs/radio/createRadioGroup.js +66 -0
  31. package/lib/commonjs/radio/createRadioGroup.js.map +1 -0
  32. package/lib/commonjs/radio/createRadioIndicator.js +43 -0
  33. package/lib/commonjs/radio/createRadioIndicator.js.map +1 -0
  34. package/lib/commonjs/radio/createRadioLabel.js +38 -0
  35. package/lib/commonjs/radio/createRadioLabel.js.map +1 -0
  36. package/lib/commonjs/radio/createRadioRoot.js +95 -0
  37. package/lib/commonjs/radio/createRadioRoot.js.map +1 -0
  38. package/lib/commonjs/radio/createRadioRoot.web.js +87 -0
  39. package/lib/commonjs/radio/createRadioRoot.web.js.map +1 -0
  40. package/lib/commonjs/radio/index.js +26 -0
  41. package/lib/commonjs/radio/index.js.map +1 -0
  42. package/lib/commonjs/radio/types.js +6 -0
  43. package/lib/commonjs/radio/types.js.map +1 -0
  44. package/lib/commonjs/radio/useRadioRoot.js +64 -0
  45. package/lib/commonjs/radio/useRadioRoot.js.map +1 -0
  46. package/lib/commonjs/select/createSelectIcon.js +3 -1
  47. package/lib/commonjs/select/createSelectIcon.js.map +1 -1
  48. package/lib/commonjs/select/createSelectTrigger.js +17 -4
  49. package/lib/commonjs/select/createSelectTrigger.js.map +1 -1
  50. package/lib/commonjs/tile/context.js +30 -0
  51. package/lib/commonjs/tile/context.js.map +1 -0
  52. package/lib/commonjs/tile/createTileContent.js +30 -0
  53. package/lib/commonjs/tile/createTileContent.js.map +1 -0
  54. package/lib/commonjs/tile/createTileDescription.js +28 -0
  55. package/lib/commonjs/tile/createTileDescription.js.map +1 -0
  56. package/lib/commonjs/tile/createTileGroup.js +112 -0
  57. package/lib/commonjs/tile/createTileGroup.js.map +1 -0
  58. package/lib/commonjs/tile/createTileIndicator.js +46 -0
  59. package/lib/commonjs/tile/createTileIndicator.js.map +1 -0
  60. package/lib/commonjs/tile/createTileLeadingSlot.js +34 -0
  61. package/lib/commonjs/tile/createTileLeadingSlot.js.map +1 -0
  62. package/lib/commonjs/tile/createTileRoot.js +133 -0
  63. package/lib/commonjs/tile/createTileRoot.js.map +1 -0
  64. package/lib/commonjs/tile/createTileTitle.js +28 -0
  65. package/lib/commonjs/tile/createTileTitle.js.map +1 -0
  66. package/lib/commonjs/tile/createTileTrailingSlot.js +35 -0
  67. package/lib/commonjs/tile/createTileTrailingSlot.js.map +1 -0
  68. package/lib/commonjs/tile/index.js +55 -0
  69. package/lib/commonjs/tile/index.js.map +1 -0
  70. package/lib/commonjs/tile/types.js +6 -0
  71. package/lib/commonjs/tile/types.js.map +1 -0
  72. package/lib/commonjs/utils/dataAttributes.js +6 -25
  73. package/lib/commonjs/utils/dataAttributes.js.map +1 -1
  74. package/lib/commonjs/utils/domDataAttributes.js +34 -0
  75. package/lib/commonjs/utils/domDataAttributes.js.map +1 -0
  76. package/lib/module/CLAUDE.md +32 -0
  77. package/lib/module/avatar/createAvatarText.js +9 -1
  78. package/lib/module/avatar/createAvatarText.js.map +1 -1
  79. package/lib/module/button/createButtonText.js +1 -0
  80. package/lib/module/button/createButtonText.js.map +1 -1
  81. package/lib/module/checkbox/createCheckboxRoot.web.js +8 -2
  82. package/lib/module/checkbox/createCheckboxRoot.web.js.map +1 -1
  83. package/lib/module/chip/createChipRoot.js +126 -0
  84. package/lib/module/chip/createChipRoot.js.map +1 -0
  85. package/lib/module/chip/index.js +12 -0
  86. package/lib/module/chip/index.js.map +1 -0
  87. package/lib/module/chip/types.js +4 -0
  88. package/lib/module/chip/types.js.map +1 -0
  89. package/lib/module/field/createFieldLabel.js.map +1 -1
  90. package/lib/module/index.js +4 -0
  91. package/lib/module/index.js.map +1 -1
  92. package/lib/module/input/createInputField.js +5 -0
  93. package/lib/module/input/createInputField.js.map +1 -1
  94. package/lib/module/link/context.js +5 -0
  95. package/lib/module/link/context.js.map +1 -0
  96. package/lib/module/link/createLink.js +29 -15
  97. package/lib/module/link/createLink.js.map +1 -1
  98. package/lib/module/link/index.js +1 -0
  99. package/lib/module/link/index.js.map +1 -1
  100. package/lib/module/list-item/createListItemRoot.js +36 -21
  101. package/lib/module/list-item/createListItemRoot.js.map +1 -1
  102. package/lib/module/radio/context.js +7 -0
  103. package/lib/module/radio/context.js.map +1 -0
  104. package/lib/module/radio/createRadioGroup.js +61 -0
  105. package/lib/module/radio/createRadioGroup.js.map +1 -0
  106. package/lib/module/radio/createRadioIndicator.js +38 -0
  107. package/lib/module/radio/createRadioIndicator.js.map +1 -0
  108. package/lib/module/radio/createRadioLabel.js +33 -0
  109. package/lib/module/radio/createRadioLabel.js.map +1 -0
  110. package/lib/module/radio/createRadioRoot.js +90 -0
  111. package/lib/module/radio/createRadioRoot.js.map +1 -0
  112. package/lib/module/radio/createRadioRoot.web.js +82 -0
  113. package/lib/module/radio/createRadioRoot.web.js.map +1 -0
  114. package/lib/module/radio/index.js +22 -0
  115. package/lib/module/radio/index.js.map +1 -0
  116. package/lib/module/radio/types.js +4 -0
  117. package/lib/module/radio/types.js.map +1 -0
  118. package/lib/module/radio/useRadioRoot.js +60 -0
  119. package/lib/module/radio/useRadioRoot.js.map +1 -0
  120. package/lib/module/select/createSelectIcon.js +3 -1
  121. package/lib/module/select/createSelectIcon.js.map +1 -1
  122. package/lib/module/select/createSelectTrigger.js +19 -6
  123. package/lib/module/select/createSelectTrigger.js.map +1 -1
  124. package/lib/module/tile/context.js +21 -0
  125. package/lib/module/tile/context.js.map +1 -0
  126. package/lib/module/tile/createTileContent.js +24 -0
  127. package/lib/module/tile/createTileContent.js.map +1 -0
  128. package/lib/module/tile/createTileDescription.js +22 -0
  129. package/lib/module/tile/createTileDescription.js.map +1 -0
  130. package/lib/module/tile/createTileGroup.js +106 -0
  131. package/lib/module/tile/createTileGroup.js.map +1 -0
  132. package/lib/module/tile/createTileIndicator.js +40 -0
  133. package/lib/module/tile/createTileIndicator.js.map +1 -0
  134. package/lib/module/tile/createTileLeadingSlot.js +28 -0
  135. package/lib/module/tile/createTileLeadingSlot.js.map +1 -0
  136. package/lib/module/tile/createTileRoot.js +127 -0
  137. package/lib/module/tile/createTileRoot.js.map +1 -0
  138. package/lib/module/tile/createTileTitle.js +22 -0
  139. package/lib/module/tile/createTileTitle.js.map +1 -0
  140. package/lib/module/tile/createTileTrailingSlot.js +29 -0
  141. package/lib/module/tile/createTileTrailingSlot.js.map +1 -0
  142. package/lib/module/tile/index.js +39 -0
  143. package/lib/module/tile/index.js.map +1 -0
  144. package/lib/module/tile/types.js +4 -0
  145. package/lib/module/tile/types.js.map +1 -0
  146. package/lib/module/utils/dataAttributes.js +4 -22
  147. package/lib/module/utils/dataAttributes.js.map +1 -1
  148. package/lib/module/utils/domDataAttributes.js +30 -0
  149. package/lib/module/utils/domDataAttributes.js.map +1 -0
  150. package/lib/typescript/avatar/createAvatarText.d.ts.map +1 -1
  151. package/lib/typescript/button/createButtonText.d.ts.map +1 -1
  152. package/lib/typescript/checkbox/createCheckboxRoot.web.d.ts +4 -0
  153. package/lib/typescript/checkbox/createCheckboxRoot.web.d.ts.map +1 -1
  154. package/lib/typescript/checkbox/useCheckboxRoot.d.ts +2 -0
  155. package/lib/typescript/checkbox/useCheckboxRoot.d.ts.map +1 -1
  156. package/lib/typescript/chip/createChipRoot.d.ts +4 -0
  157. package/lib/typescript/chip/createChipRoot.d.ts.map +1 -0
  158. package/lib/typescript/chip/index.d.ts +9 -0
  159. package/lib/typescript/chip/index.d.ts.map +1 -0
  160. package/lib/typescript/chip/types.d.ts +17 -0
  161. package/lib/typescript/chip/types.d.ts.map +1 -0
  162. package/lib/typescript/field/createFieldLabel.d.ts.map +1 -1
  163. package/lib/typescript/index.d.ts +4 -0
  164. package/lib/typescript/index.d.ts.map +1 -1
  165. package/lib/typescript/input/createInputField.d.ts.map +1 -1
  166. package/lib/typescript/link/context.d.ts +10 -0
  167. package/lib/typescript/link/context.d.ts.map +1 -0
  168. package/lib/typescript/link/createLink.d.ts.map +1 -1
  169. package/lib/typescript/link/index.d.ts +2 -0
  170. package/lib/typescript/link/index.d.ts.map +1 -1
  171. package/lib/typescript/link/types.d.ts +1 -0
  172. package/lib/typescript/link/types.d.ts.map +1 -1
  173. package/lib/typescript/list-item/createListItemRoot.d.ts.map +1 -1
  174. package/lib/typescript/radio/context.d.ts +21 -0
  175. package/lib/typescript/radio/context.d.ts.map +1 -0
  176. package/lib/typescript/radio/createRadioGroup.d.ts +3 -0
  177. package/lib/typescript/radio/createRadioGroup.d.ts.map +1 -0
  178. package/lib/typescript/radio/createRadioIndicator.d.ts +5 -0
  179. package/lib/typescript/radio/createRadioIndicator.d.ts.map +1 -0
  180. package/lib/typescript/radio/createRadioLabel.d.ts +5 -0
  181. package/lib/typescript/radio/createRadioLabel.d.ts.map +1 -0
  182. package/lib/typescript/radio/createRadioRoot.d.ts +3 -0
  183. package/lib/typescript/radio/createRadioRoot.d.ts.map +1 -0
  184. package/lib/typescript/radio/createRadioRoot.web.d.ts +3 -0
  185. package/lib/typescript/radio/createRadioRoot.web.d.ts.map +1 -0
  186. package/lib/typescript/radio/index.d.ts +10 -0
  187. package/lib/typescript/radio/index.d.ts.map +1 -0
  188. package/lib/typescript/radio/types.d.ts +54 -0
  189. package/lib/typescript/radio/types.d.ts.map +1 -0
  190. package/lib/typescript/radio/useRadioRoot.d.ts +151 -0
  191. package/lib/typescript/radio/useRadioRoot.d.ts.map +1 -0
  192. package/lib/typescript/select/createSelectIcon.d.ts.map +1 -1
  193. package/lib/typescript/select/createSelectTrigger.d.ts.map +1 -1
  194. package/lib/typescript/tile/context.d.ts +9 -0
  195. package/lib/typescript/tile/context.d.ts.map +1 -0
  196. package/lib/typescript/tile/createTileContent.d.ts +3 -0
  197. package/lib/typescript/tile/createTileContent.d.ts.map +1 -0
  198. package/lib/typescript/tile/createTileDescription.d.ts +3 -0
  199. package/lib/typescript/tile/createTileDescription.d.ts.map +1 -0
  200. package/lib/typescript/tile/createTileGroup.d.ts +4 -0
  201. package/lib/typescript/tile/createTileGroup.d.ts.map +1 -0
  202. package/lib/typescript/tile/createTileIndicator.d.ts +4 -0
  203. package/lib/typescript/tile/createTileIndicator.d.ts.map +1 -0
  204. package/lib/typescript/tile/createTileLeadingSlot.d.ts +4 -0
  205. package/lib/typescript/tile/createTileLeadingSlot.d.ts.map +1 -0
  206. package/lib/typescript/tile/createTileRoot.d.ts +4 -0
  207. package/lib/typescript/tile/createTileRoot.d.ts.map +1 -0
  208. package/lib/typescript/tile/createTileTitle.d.ts +3 -0
  209. package/lib/typescript/tile/createTileTitle.d.ts.map +1 -0
  210. package/lib/typescript/tile/createTileTrailingSlot.d.ts +9 -0
  211. package/lib/typescript/tile/createTileTrailingSlot.d.ts.map +1 -0
  212. package/lib/typescript/tile/index.d.ts +15 -0
  213. package/lib/typescript/tile/index.d.ts.map +1 -0
  214. package/lib/typescript/tile/types.d.ts +119 -0
  215. package/lib/typescript/tile/types.d.ts.map +1 -0
  216. package/lib/typescript/utils/dataAttributes.d.ts +4 -10
  217. package/lib/typescript/utils/dataAttributes.d.ts.map +1 -1
  218. package/lib/typescript/utils/domDataAttributes.d.ts +15 -0
  219. package/lib/typescript/utils/domDataAttributes.d.ts.map +1 -0
  220. package/package.json +5 -2
  221. package/src/CLAUDE.md +32 -0
  222. package/src/avatar/createAvatarText.tsx +10 -1
  223. package/src/button/createButtonText.tsx +1 -0
  224. package/src/checkbox/createCheckboxRoot.web.tsx +6 -2
  225. package/src/chip/createChipRoot.tsx +144 -0
  226. package/src/chip/index.ts +18 -0
  227. package/src/chip/types.ts +23 -0
  228. package/src/field/createFieldLabel.tsx +4 -1
  229. package/src/index.ts +4 -0
  230. package/src/input/createInputField.tsx +6 -0
  231. package/src/link/context.tsx +10 -0
  232. package/src/link/createLink.tsx +41 -23
  233. package/src/link/index.tsx +2 -0
  234. package/src/link/types.ts +1 -0
  235. package/src/list-item/createListItemRoot.tsx +37 -22
  236. package/src/radio/context.tsx +21 -0
  237. package/src/radio/createRadioGroup.tsx +67 -0
  238. package/src/radio/createRadioIndicator.tsx +32 -0
  239. package/src/radio/createRadioLabel.tsx +28 -0
  240. package/src/radio/createRadioRoot.tsx +100 -0
  241. package/src/radio/createRadioRoot.web.tsx +81 -0
  242. package/src/radio/index.ts +37 -0
  243. package/src/radio/types.ts +67 -0
  244. package/src/radio/useRadioRoot.ts +69 -0
  245. package/src/select/createSelectIcon.tsx +2 -1
  246. package/src/select/createSelectTrigger.tsx +26 -3
  247. package/src/tile/context.tsx +23 -0
  248. package/src/tile/createTileContent.tsx +23 -0
  249. package/src/tile/createTileDescription.tsx +19 -0
  250. package/src/tile/createTileGroup.tsx +134 -0
  251. package/src/tile/createTileIndicator.tsx +38 -0
  252. package/src/tile/createTileLeadingSlot.tsx +30 -0
  253. package/src/tile/createTileRoot.tsx +124 -0
  254. package/src/tile/createTileTitle.tsx +19 -0
  255. package/src/tile/createTileTrailingSlot.tsx +25 -0
  256. package/src/tile/index.ts +88 -0
  257. package/src/tile/types.ts +153 -0
  258. package/src/utils/dataAttributes.ts +4 -25
  259. package/src/utils/domDataAttributes.ts +32 -0
@@ -0,0 +1,100 @@
1
+ import { forwardRef } from 'react';
2
+ import { composeEventHandlers } from '@cdx-ui/utils';
3
+ import { useFocus } from '@react-native-aria/focus';
4
+ import { usePress } from '@react-native-aria/interactions';
5
+ import { dataAttributes } from '../utils/dataAttributes';
6
+ import { RadioProvider } from './context';
7
+ import type { IRadioProps } from './types';
8
+ import { useRadioRoot } from './useRadioRoot';
9
+
10
+ export const createRadioRoot = <T,>(
11
+ BaseRadio: React.ComponentType<T>,
12
+ RadioIndicator: React.ComponentType<any>,
13
+ ) =>
14
+ forwardRef(
15
+ (
16
+ {
17
+ onPressIn,
18
+ onPressOut,
19
+ onHoverIn,
20
+ onHoverOut,
21
+ onFocus,
22
+ onBlur,
23
+ children,
24
+ ...props
25
+ }: IRadioProps,
26
+ ref?: React.Ref<T>,
27
+ ) => {
28
+ const {
29
+ isHovered: isHoveredProp,
30
+ isDisabled: isDisabledProp,
31
+ isInvalid: isInvalidProp,
32
+ isReadOnly: isReadOnlyProp,
33
+ isPressed: isPressedProp,
34
+ isFocused: isFocusedProp,
35
+ isFocusVisible,
36
+ } = props;
37
+
38
+ const {
39
+ combinedProps,
40
+ isInvalid,
41
+ isReadOnly,
42
+ inputProps,
43
+ isChecked,
44
+ isDisabled,
45
+ isHovered,
46
+ hoverProps,
47
+ mergedRef,
48
+ } = useRadioRoot(props, ref);
49
+
50
+ const { focusProps, isFocused } = useFocus();
51
+
52
+ const { pressProps, isPressed } = usePress({
53
+ isDisabled: isDisabled || isDisabledProp,
54
+ });
55
+
56
+ return (
57
+ <BaseRadio
58
+ disabled={isDisabled || isDisabledProp}
59
+ {...pressProps}
60
+ {...(combinedProps as T)}
61
+ {...inputProps}
62
+ ref={mergedRef}
63
+ role="radio"
64
+ onPressIn={composeEventHandlers(onPressIn, pressProps.onPressIn)}
65
+ onPressOut={composeEventHandlers(onPressOut, pressProps.onPressOut)}
66
+ onHoverIn={composeEventHandlers(onHoverIn, hoverProps.onHoverIn)}
67
+ onHoverOut={composeEventHandlers(onHoverOut, hoverProps.onHoverOut)}
68
+ onFocus={composeEventHandlers(onFocus, focusProps.onFocus)}
69
+ onBlur={composeEventHandlers(onBlur, focusProps.onBlur)}
70
+ {...dataAttributes({
71
+ slot: 'radio',
72
+ checked: isChecked,
73
+ disabled: isDisabled || isDisabledProp,
74
+ hover: isHovered || isHoveredProp,
75
+ invalid: isInvalid || isInvalidProp,
76
+ readonly: isReadOnly || isReadOnlyProp,
77
+ active: isPressed,
78
+ focus: isFocused,
79
+ focusVisible: isFocusVisible,
80
+ })}
81
+ >
82
+ <RadioProvider
83
+ value={{
84
+ isChecked,
85
+ isDisabled: isDisabled || isDisabledProp,
86
+ isHovered: isHovered || isHoveredProp,
87
+ isInvalid: isInvalid || isInvalidProp,
88
+ isReadOnly: isReadOnly || isReadOnlyProp,
89
+ isPressed: isPressed || isPressedProp,
90
+ isFocused: isFocused || isFocusedProp,
91
+ isFocusVisible,
92
+ }}
93
+ >
94
+ <RadioIndicator />
95
+ {children}
96
+ </RadioProvider>
97
+ </BaseRadio>
98
+ );
99
+ },
100
+ );
@@ -0,0 +1,81 @@
1
+ import { forwardRef } from 'react';
2
+ import { VisuallyHidden } from '@react-aria/visually-hidden';
3
+ import { mergeProps } from '@react-aria/utils';
4
+ import { useFocusRing } from '@react-native-aria/focus';
5
+ import { dataAttributes } from '../utils/dataAttributes';
6
+ import { RadioProvider } from './context';
7
+ import type { IRadioProps } from './types';
8
+ import { useRadioRoot } from './useRadioRoot';
9
+
10
+ // TODO: Keyboard focus is not working as expected
11
+
12
+ export const createRadioRoot = <T,>(
13
+ BaseRadio: React.ComponentType<T>,
14
+ RadioIndicator: React.ComponentType<any>,
15
+ ) =>
16
+ forwardRef(({ children, ...props }: IRadioProps, ref?: React.Ref<T>) => {
17
+ const {
18
+ isHovered: isHoveredProp,
19
+ isFocusVisible: isFocusVisibleProp,
20
+ isDisabled: isDisabledProp,
21
+ isInvalid: isInvalidProp,
22
+ isReadOnly: isReadOnlyProp,
23
+ isFocused,
24
+ isPressed,
25
+ } = props;
26
+
27
+ const {
28
+ combinedProps,
29
+ isInvalid,
30
+ isReadOnly,
31
+ inputProps,
32
+ labelProps,
33
+ isChecked,
34
+ isDisabled,
35
+ isHovered,
36
+ mergedRef,
37
+ inputRef,
38
+ } = useRadioRoot(props, ref, { useInputRefForAria: true });
39
+
40
+ const { focusProps, isFocusVisible } = useFocusRing();
41
+
42
+ return (
43
+ <BaseRadio
44
+ {...mergeProps(combinedProps as any, labelProps as any, focusProps as any)}
45
+ ref={mergedRef}
46
+ role="label"
47
+ // eslint-disable-next-line react-native-a11y/has-valid-accessibility-role
48
+ accessibilityRole="label"
49
+ {...dataAttributes({
50
+ slot: 'radio',
51
+ checked: isChecked,
52
+ disabled: isDisabled || isDisabledProp,
53
+ hover: isHovered || isHoveredProp,
54
+ invalid: isInvalid || isInvalidProp,
55
+ readonly: isReadOnly || isReadOnlyProp,
56
+ active: isPressed,
57
+ focus: isFocused,
58
+ focusVisible: isFocusVisible || isFocusVisibleProp,
59
+ })}
60
+ >
61
+ <RadioProvider
62
+ value={{
63
+ isChecked,
64
+ isDisabled: isDisabled || isDisabledProp,
65
+ isFocusVisible: isFocusVisible || isFocusVisibleProp,
66
+ isHovered: isHovered || isHoveredProp,
67
+ isInvalid: isInvalid || isInvalidProp,
68
+ isReadOnly: isReadOnly || isReadOnlyProp,
69
+ isPressed,
70
+ isFocused,
71
+ }}
72
+ >
73
+ <VisuallyHidden>
74
+ <input {...inputProps} ref={inputRef} />
75
+ </VisuallyHidden>
76
+ <RadioIndicator />
77
+ {children}
78
+ </RadioProvider>
79
+ </BaseRadio>
80
+ );
81
+ });
@@ -0,0 +1,37 @@
1
+ import type React from 'react';
2
+ import { createRadioGroup } from './createRadioGroup';
3
+ import { createRadioIndicator } from './createRadioIndicator';
4
+ import { createRadioLabel } from './createRadioLabel';
5
+ import { createRadioRoot } from './createRadioRoot';
6
+ import type { IRadioComponentType } from './types';
7
+
8
+ export type {
9
+ IRadioComponentType,
10
+ IRadioGroupProps,
11
+ IRadioIndicatorProps,
12
+ IRadioLabelProps,
13
+ IRadioProps,
14
+ } from './types';
15
+
16
+ export function createRadio<Root, Indicator, Label, Group>(BaseComponents: {
17
+ Root: React.ComponentType<Root>;
18
+ Indicator: React.ComponentType<Indicator>;
19
+ Label: React.ComponentType<Label>;
20
+ Group: React.ComponentType<Group>;
21
+ }) {
22
+ const Indicator = createRadioIndicator(BaseComponents.Indicator);
23
+ const Radio = createRadioRoot(BaseComponents.Root, Indicator);
24
+ const Label = createRadioLabel(BaseComponents.Label);
25
+ const Group = createRadioGroup(BaseComponents.Group);
26
+
27
+ Radio.displayName = 'RadioPrimitive';
28
+ Indicator.displayName = 'RadioPrimitive.Indicator';
29
+ Label.displayName = 'RadioPrimitive.Label';
30
+ Group.displayName = 'RadioPrimitive.Group';
31
+
32
+ return Object.assign(Radio, {
33
+ Indicator,
34
+ Label,
35
+ Group,
36
+ }) as IRadioComponentType<Root, Indicator, Label, Group>;
37
+ }
@@ -0,0 +1,67 @@
1
+ import type { PressableProps } from 'react-native';
2
+
3
+ export interface IRadioProps extends PressableProps {
4
+ /** Required — identifies this item's value within a Radio.Group. */
5
+ value: string;
6
+ isDisabled?: boolean;
7
+ isInvalid?: boolean;
8
+ isReadOnly?: boolean;
9
+ isHovered?: boolean;
10
+ isFocused?: boolean;
11
+ isPressed?: boolean;
12
+ isFocusVisible?: boolean;
13
+ children?: React.ReactNode;
14
+ }
15
+
16
+ export interface IRadioGroupProps {
17
+ /** Controlled selected value. */
18
+ value?: string;
19
+ /** Default selected value for uncontrolled usage. */
20
+ defaultValue?: string;
21
+ /** Called when the selected value changes. */
22
+ onChange?: (value: string) => void;
23
+ /** Layout direction of the radio items. */
24
+ direction?: 'column' | 'row';
25
+ /** Form field name for web form integration. */
26
+ name?: string;
27
+ isDisabled?: boolean;
28
+ isInvalid?: boolean;
29
+ isRequired?: boolean;
30
+ isReadOnly?: boolean;
31
+ 'aria-label'?: string;
32
+ 'aria-labelledby'?: string;
33
+ children?: React.ReactNode;
34
+ }
35
+
36
+ export interface IRadioIndicatorProps {
37
+ children?: React.ReactNode;
38
+ }
39
+
40
+ export interface IRadioLabelProps {
41
+ children?: React.ReactNode;
42
+ }
43
+
44
+ export type IRadioComponentType<Root, Indicator, Label, Group> = React.ForwardRefExoticComponent<
45
+ React.RefAttributes<Root> & React.PropsWithoutRef<Root> & IRadioProps
46
+ > & {
47
+ Indicator: React.ForwardRefExoticComponent<
48
+ React.RefAttributes<Indicator> & React.PropsWithoutRef<Indicator> & IRadioIndicatorProps
49
+ >;
50
+ Label: React.ForwardRefExoticComponent<
51
+ React.RefAttributes<Label> & React.PropsWithoutRef<Label> & IRadioLabelProps
52
+ >;
53
+ Group: React.ForwardRefExoticComponent<
54
+ React.RefAttributes<Group> & React.PropsWithoutRef<Group> & IRadioGroupProps
55
+ >;
56
+ };
57
+
58
+ export interface IRadioContextValue {
59
+ isChecked?: boolean;
60
+ isDisabled?: boolean;
61
+ isInvalid?: boolean;
62
+ isReadOnly?: boolean;
63
+ isFocused?: boolean;
64
+ isFocusVisible?: boolean;
65
+ isHovered?: boolean;
66
+ isPressed?: boolean;
67
+ }
@@ -0,0 +1,69 @@
1
+ import { useContext, useRef } from 'react';
2
+ import { mergeRefs, useFormControlContext } from '@cdx-ui/utils';
3
+ import { useRadio } from '@react-native-aria/radio';
4
+ import { useHover } from '@react-native-aria/interactions';
5
+ import { RadioGroupContext } from './context';
6
+ import type { IRadioProps } from './types';
7
+
8
+ interface UseRadioRootOptions {
9
+ useInputRefForAria?: boolean;
10
+ }
11
+
12
+ export function useRadioRoot(
13
+ props: IRadioProps,
14
+ ref?: React.Ref<unknown>,
15
+ { useInputRefForAria = false }: UseRadioRootOptions = {},
16
+ ) {
17
+ const formControlContext = useFormControlContext();
18
+
19
+ const { isInvalid, isReadOnly, ...combinedProps } = {
20
+ ...formControlContext,
21
+ ...props,
22
+ };
23
+
24
+ const radioGroupContext = useContext(RadioGroupContext);
25
+
26
+ if (!radioGroupContext) {
27
+ throw new Error(
28
+ 'Radio must be rendered inside a Radio.Group. Standalone Radio is not supported.',
29
+ );
30
+ }
31
+
32
+ const rootRef = useRef(null);
33
+ const inputRef = useRef<HTMLInputElement | null>(null);
34
+ const mergedRootRef = mergeRefs(ref as any, rootRef as any);
35
+ const ariaLabel = combinedProps['aria-label'] || combinedProps.value || 'Radio';
36
+
37
+ const radio = useRadio(
38
+ {
39
+ ...combinedProps,
40
+ 'aria-label': ariaLabel,
41
+ isReadOnly: isReadOnly || radioGroupContext.state.isReadOnly,
42
+ isDisabled: combinedProps.isDisabled || radioGroupContext.state.isDisabled,
43
+ } as any,
44
+ radioGroupContext.radioGroupState,
45
+ (useInputRefForAria ? inputRef : rootRef) as any,
46
+ );
47
+
48
+ const { inputProps, labelProps } = radio as typeof radio & {
49
+ labelProps?: Record<string, unknown>;
50
+ };
51
+
52
+ const { checked: isChecked, disabled: isDisabled } = inputProps;
53
+
54
+ const { hoverProps, isHovered } = useHover({}, rootRef);
55
+
56
+ return {
57
+ combinedProps,
58
+ isInvalid: isInvalid || radioGroupContext.state.isInvalid,
59
+ isReadOnly: isReadOnly || radioGroupContext.state.isReadOnly,
60
+ inputProps,
61
+ labelProps: labelProps ?? {},
62
+ isChecked,
63
+ isDisabled,
64
+ isHovered,
65
+ hoverProps,
66
+ mergedRef: mergedRootRef,
67
+ inputRef,
68
+ };
69
+ }
@@ -5,7 +5,7 @@ import { useSelectContext, useSelectTriggerContext } from './context';
5
5
  export const createSelectIcon = <T,>(BaseIcon: React.ComponentType<T>) =>
6
6
  forwardRef((props: any, ref?: any) => {
7
7
  const { hover, focus, active, disabled, focusVisible } = useSelectTriggerContext();
8
- const { required, invalid, readOnly } = useSelectContext();
8
+ const { required, invalid, readOnly, open } = useSelectContext();
9
9
 
10
10
  return (
11
11
  <BaseIcon
@@ -19,6 +19,7 @@ export const createSelectIcon = <T,>(BaseIcon: React.ComponentType<T>) =>
19
19
  required,
20
20
  invalid,
21
21
  readonly: readOnly,
22
+ state: open ? 'open' : 'closed',
22
23
  slot: 'select-icon',
23
24
  })}
24
25
  {...(props as T)}
@@ -1,7 +1,12 @@
1
1
  import type React from 'react';
2
- import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef } from 'react';
2
+ import { forwardRef, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
3
  import { type GestureResponderEvent, Platform } from 'react-native';
4
- import { composeEventHandlers, mergeRefs, useFormControlContext } from '@cdx-ui/utils';
4
+ import {
5
+ composeEventHandlers,
6
+ mergeRefs,
7
+ useFormControlContext,
8
+ useReportFormControlLabelFocus,
9
+ } from '@cdx-ui/utils';
5
10
  import { useFocus, useFocusRing } from '@react-native-aria/focus';
6
11
  import { useHover, usePress } from '@react-native-aria/interactions';
7
12
  import type { InteractionState } from '../types';
@@ -51,6 +56,7 @@ export const createSelectTrigger = <T,>(BaseTrigger: React.ComponentType<T>) =>
51
56
  });
52
57
  const { isFocused, focusProps }: any = useFocus();
53
58
  const { isHovered, hoverProps } = useHover();
59
+ const [triggerFocused, setTriggerFocused] = useState(false);
54
60
 
55
61
  const handlePress = useCallback(
56
62
  (e: GestureResponderEvent | KeyboardEvent) => {
@@ -84,10 +90,24 @@ export const createSelectTrigger = <T,>(BaseTrigger: React.ComponentType<T>) =>
84
90
  [disabled, setOpen],
85
91
  );
86
92
 
93
+ // `useFocus()` on Pressable can stay true on native after blur; use explicit focus/blur handlers.
94
+ const isLabelActive = Platform.select({
95
+ web: Boolean((isFocusedProp || isFocused || open) && !disabled),
96
+ default: Boolean((open || triggerFocused) && !disabled),
97
+ });
98
+
99
+ useReportFormControlLabelFocus(isLabelActive);
100
+
101
+ useLayoutEffect(() => {
102
+ if (!open) {
103
+ setTriggerFocused(false);
104
+ }
105
+ }, [open]);
106
+
87
107
  const interactionState = useMemo(
88
108
  () => ({
89
109
  hover: isHoveredProp || isHovered,
90
- focus: isFocusedProp || isFocused,
110
+ focus: isFocusedProp || isFocused || open,
91
111
  active: isActiveProp || isActive,
92
112
  disabled: !!disabled,
93
113
  focusVisible: isFocusVisibleProp || isFocusVisible,
@@ -102,6 +122,7 @@ export const createSelectTrigger = <T,>(BaseTrigger: React.ComponentType<T>) =>
102
122
  disabled,
103
123
  isFocusVisibleProp,
104
124
  isFocusVisible,
125
+ open,
105
126
  ],
106
127
  );
107
128
 
@@ -200,6 +221,7 @@ export const createSelectTrigger = <T,>(BaseTrigger: React.ComponentType<T>) =>
200
221
  focusProps.onFocus,
201
222
  ),
202
223
  focusRingProps.onFocus,
224
+ () => setTriggerFocused(true),
203
225
  )}
204
226
  onBlur={composeEventHandlers(
205
227
  composeEventHandlers(
@@ -207,6 +229,7 @@ export const createSelectTrigger = <T,>(BaseTrigger: React.ComponentType<T>) =>
207
229
  focusProps.onBlur,
208
230
  ),
209
231
  focusRingProps.onBlur,
232
+ () => setTriggerFocused(false),
210
233
  )}
211
234
  {...webKeyboardProps}
212
235
  >
@@ -0,0 +1,23 @@
1
+ import { createContext as createReactContext, useContext } from 'react';
2
+ import { createContext } from '@cdx-ui/utils';
3
+ import type { ITileContextValue, ITileGroupContextValue } from './types';
4
+
5
+ // Per-tile subtree (selection-aware slots, indicator, etc.)
6
+ export const [TileProvider, useTileContext] = createContext<ITileContextValue>('TileContext');
7
+
8
+ // Per-group subtree (optional — standalone tiles have no provider)
9
+ const TileGroupContext = createReactContext<ITileGroupContextValue | null>(null);
10
+
11
+ export const TileGroupContextProvider = TileGroupContext.Provider;
12
+
13
+ export function useTileGroupContext(): ITileGroupContextValue {
14
+ const ctx = useContext(TileGroupContext);
15
+ if (!ctx) {
16
+ throw new Error('Tile must be used within Tile.Group');
17
+ }
18
+ return ctx;
19
+ }
20
+
21
+ export function useOptionalTileGroupContext(): ITileGroupContextValue | null {
22
+ return useContext(TileGroupContext);
23
+ }
@@ -0,0 +1,23 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileContentProps } from './types';
4
+
5
+ const contentStyle = {
6
+ flex: 1,
7
+ flexDirection: 'column' as const,
8
+ minWidth: 0,
9
+ };
10
+
11
+ export const createTileContent = <T,>(Base: React.ComponentType<T>) =>
12
+ forwardRef(({ children, style, ...props }: ITileContentProps, ref: React.Ref<unknown>) => (
13
+ <Base
14
+ {...(props as T)}
15
+ {...dataAttributes({
16
+ slot: 'tile-content',
17
+ })}
18
+ ref={ref as React.Ref<T>}
19
+ style={[contentStyle, style]}
20
+ >
21
+ {children}
22
+ </Base>
23
+ ));
@@ -0,0 +1,19 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileDescriptionProps } from './types';
4
+
5
+ const noUnderline = { textDecorationLine: 'none' as const };
6
+
7
+ export const createTileDescription = <T,>(Base: React.ComponentType<T>) =>
8
+ forwardRef(({ children, style, ...props }: ITileDescriptionProps, ref: React.Ref<unknown>) => (
9
+ <Base
10
+ {...(props as T)}
11
+ {...dataAttributes({
12
+ slot: 'tile-description',
13
+ })}
14
+ ref={ref as React.Ref<T>}
15
+ style={[noUnderline, style]}
16
+ >
17
+ {children}
18
+ </Base>
19
+ ));
@@ -0,0 +1,134 @@
1
+ import React, { forwardRef, useCallback, useMemo, type Ref } from 'react';
2
+ import type { ViewProps } from 'react-native';
3
+ import { useControllableState } from '@cdx-ui/utils';
4
+ import { dataAttributes } from '../utils/dataAttributes';
5
+ import { TileGroupContextProvider } from './context';
6
+ import type { ITileGroupProps, TileGroupType } from './types';
7
+
8
+ /**
9
+ * Internal widening of the public discriminated union: the runtime accepts any value
10
+ * shape and reads `type` to decide branch behavior. The public API stays narrow.
11
+ */
12
+ type WidenedTileGroupProps = ViewProps & {
13
+ type: TileGroupType;
14
+ value?: string | string[];
15
+ defaultValue?: string | string[];
16
+ onValueChange?: (value: string | string[]) => void;
17
+ max?: number;
18
+ isDisabled?: boolean;
19
+ 'aria-label'?: string;
20
+ };
21
+
22
+ export const createTileGroup = <T,>(Base: React.ComponentType<T>) =>
23
+ forwardRef((props: ITileGroupProps, ref: Ref<T>) => {
24
+ const {
25
+ children,
26
+ type,
27
+ value: valueProp,
28
+ defaultValue,
29
+ onValueChange,
30
+ max: maxProp,
31
+ isDisabled = false,
32
+ 'aria-label': ariaLabel,
33
+ ...rest
34
+ } = props as WidenedTileGroupProps;
35
+
36
+ const max = maxProp ?? Number.POSITIVE_INFINITY;
37
+
38
+ const defaultProp =
39
+ defaultValue !== undefined ? defaultValue : type === 'multiple' ? [] : undefined;
40
+
41
+ const [state, setState] = useControllableState<string | string[] | undefined>({
42
+ prop: valueProp,
43
+ defaultProp,
44
+ onChange: (next) => {
45
+ if (next !== undefined) {
46
+ onValueChange?.(next);
47
+ }
48
+ },
49
+ });
50
+
51
+ const isSelected = useCallback(
52
+ (tileValue: string) => {
53
+ if (type === 'single') {
54
+ return state === tileValue;
55
+ }
56
+ return Array.isArray(state) && state.includes(tileValue);
57
+ },
58
+ [type, state],
59
+ );
60
+
61
+ const isTileDisabledByGroup = useCallback(
62
+ (tileValue: string) => {
63
+ if (isDisabled) {
64
+ return true;
65
+ }
66
+ if (type !== 'multiple') {
67
+ return false;
68
+ }
69
+ const arr = Array.isArray(state) ? state : [];
70
+ if (arr.length < max) {
71
+ return false;
72
+ }
73
+ return !arr.includes(tileValue);
74
+ },
75
+ [isDisabled, type, state, max],
76
+ );
77
+
78
+ const toggleValue = useCallback(
79
+ (tileValue: string) => {
80
+ if (isDisabled) {
81
+ return;
82
+ }
83
+ if (type === 'single') {
84
+ if (state === tileValue) {
85
+ return;
86
+ }
87
+ setState(tileValue);
88
+ return;
89
+ }
90
+ const arr = Array.isArray(state) ? [...state] : [];
91
+ const idx = arr.indexOf(tileValue);
92
+ if (idx >= 0) {
93
+ arr.splice(idx, 1);
94
+ setState(arr);
95
+ } else if (arr.length < max) {
96
+ arr.push(tileValue);
97
+ setState(arr);
98
+ }
99
+ },
100
+ [isDisabled, type, state, setState, max],
101
+ );
102
+
103
+ const contextValue = useMemo(
104
+ () => ({
105
+ type,
106
+ value: state,
107
+ toggleValue,
108
+ isSelected,
109
+ isTileDisabledByGroup,
110
+ isGroupDisabled: isDisabled,
111
+ max,
112
+ }),
113
+ [type, state, toggleValue, isSelected, isTileDisabledByGroup, isDisabled, max],
114
+ );
115
+
116
+ const role = type === 'single' ? 'radiogroup' : 'group';
117
+
118
+ return (
119
+ <TileGroupContextProvider value={contextValue}>
120
+ <Base
121
+ {...(rest as T)}
122
+ ref={ref}
123
+ role={role}
124
+ {...(type === 'single' ? { accessibilityRole: 'radiogroup' as const } : {})}
125
+ {...(ariaLabel ? { 'aria-label': ariaLabel, accessibilityLabel: ariaLabel } : {})}
126
+ {...dataAttributes({
127
+ slot: 'tile-group',
128
+ })}
129
+ >
130
+ {children}
131
+ </Base>
132
+ </TileGroupContextProvider>
133
+ );
134
+ });
@@ -0,0 +1,38 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import { useTileContext } from './context';
4
+ import type { ITileIndicatorProps } from './types';
5
+
6
+ const shrinkZero = { flexShrink: 0 as const, pointerEvents: 'none' as const };
7
+
8
+ export const createTileIndicator = <T,>(Base: React.ComponentType<T>) =>
9
+ forwardRef(
10
+ (
11
+ { children, style, indicatorType, ...props }: ITileIndicatorProps,
12
+ ref: React.Ref<unknown>,
13
+ ) => {
14
+ const { isSelected, isDisabled, selectionType } = useTileContext();
15
+
16
+ // Effective visual type: explicit prop wins; otherwise infer from group/standalone context.
17
+ const effectiveType: 'radio' | 'checkbox' =
18
+ indicatorType ?? (selectionType === 'single' ? 'radio' : 'checkbox');
19
+
20
+ return (
21
+ <Base
22
+ {...(props as T)}
23
+ accessibilityElementsHidden
24
+ aria-hidden
25
+ {...dataAttributes({
26
+ slot: 'tile-indicator',
27
+ checked: isSelected,
28
+ selectionType: effectiveType === 'radio' ? 'single' : 'multiple',
29
+ disabled: isDisabled,
30
+ })}
31
+ ref={ref as React.Ref<T>}
32
+ style={[shrinkZero, style]}
33
+ >
34
+ {children}
35
+ </Base>
36
+ );
37
+ },
38
+ );