@cdx-ui/primitives 0.0.1-beta.3 → 0.0.1-beta.30

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 (251) hide show
  1. package/README.md +49 -24
  2. package/lib/commonjs/checkbox/createCheckboxRoot.web.js +8 -3
  3. package/lib/commonjs/checkbox/createCheckboxRoot.web.js.map +1 -1
  4. package/lib/commonjs/field/createFieldLabel.js.map +1 -1
  5. package/lib/commonjs/index.js +36 -0
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/input/createInputField.js +5 -0
  8. package/lib/commonjs/input/createInputField.js.map +1 -1
  9. package/lib/commonjs/list-item/context.js +11 -0
  10. package/lib/commonjs/list-item/context.js.map +1 -0
  11. package/lib/commonjs/list-item/createListItemContent.js +30 -0
  12. package/lib/commonjs/list-item/createListItemContent.js.map +1 -0
  13. package/lib/commonjs/list-item/createListItemDescription.js +25 -0
  14. package/lib/commonjs/list-item/createListItemDescription.js.map +1 -0
  15. package/lib/commonjs/list-item/createListItemLeadingSlot.js +34 -0
  16. package/lib/commonjs/list-item/createListItemLeadingSlot.js.map +1 -0
  17. package/lib/commonjs/list-item/createListItemMeta.js +25 -0
  18. package/lib/commonjs/list-item/createListItemMeta.js.map +1 -0
  19. package/lib/commonjs/list-item/createListItemRoot.js +157 -0
  20. package/lib/commonjs/list-item/createListItemRoot.js.map +1 -0
  21. package/lib/commonjs/list-item/createListItemSectionHeader.js +54 -0
  22. package/lib/commonjs/list-item/createListItemSectionHeader.js.map +1 -0
  23. package/lib/commonjs/list-item/createListItemTitle.js +25 -0
  24. package/lib/commonjs/list-item/createListItemTitle.js.map +1 -0
  25. package/lib/commonjs/list-item/createListItemTrailingSlot.js +28 -0
  26. package/lib/commonjs/list-item/createListItemTrailingSlot.js.map +1 -0
  27. package/lib/commonjs/list-item/index.js +55 -0
  28. package/lib/commonjs/list-item/index.js.map +1 -0
  29. package/lib/commonjs/list-item/types.js +6 -0
  30. package/lib/commonjs/list-item/types.js.map +1 -0
  31. package/lib/commonjs/radio/context.js +14 -0
  32. package/lib/commonjs/radio/context.js.map +1 -0
  33. package/lib/commonjs/radio/createRadioGroup.js +66 -0
  34. package/lib/commonjs/radio/createRadioGroup.js.map +1 -0
  35. package/lib/commonjs/radio/createRadioIndicator.js +43 -0
  36. package/lib/commonjs/radio/createRadioIndicator.js.map +1 -0
  37. package/lib/commonjs/radio/createRadioLabel.js +38 -0
  38. package/lib/commonjs/radio/createRadioLabel.js.map +1 -0
  39. package/lib/commonjs/radio/createRadioRoot.js +95 -0
  40. package/lib/commonjs/radio/createRadioRoot.js.map +1 -0
  41. package/lib/commonjs/radio/createRadioRoot.web.js +87 -0
  42. package/lib/commonjs/radio/createRadioRoot.web.js.map +1 -0
  43. package/lib/commonjs/radio/index.js +26 -0
  44. package/lib/commonjs/radio/index.js.map +1 -0
  45. package/lib/commonjs/radio/types.js +6 -0
  46. package/lib/commonjs/radio/types.js.map +1 -0
  47. package/lib/commonjs/radio/useRadioRoot.js +64 -0
  48. package/lib/commonjs/radio/useRadioRoot.js.map +1 -0
  49. package/lib/commonjs/select/createSelectTrigger.js +17 -4
  50. package/lib/commonjs/select/createSelectTrigger.js.map +1 -1
  51. package/lib/commonjs/tile/context.js +30 -0
  52. package/lib/commonjs/tile/context.js.map +1 -0
  53. package/lib/commonjs/tile/createTileContent.js +30 -0
  54. package/lib/commonjs/tile/createTileContent.js.map +1 -0
  55. package/lib/commonjs/tile/createTileDescription.js +28 -0
  56. package/lib/commonjs/tile/createTileDescription.js.map +1 -0
  57. package/lib/commonjs/tile/createTileGroup.js +112 -0
  58. package/lib/commonjs/tile/createTileGroup.js.map +1 -0
  59. package/lib/commonjs/tile/createTileIndicator.js +46 -0
  60. package/lib/commonjs/tile/createTileIndicator.js.map +1 -0
  61. package/lib/commonjs/tile/createTileLeadingSlot.js +34 -0
  62. package/lib/commonjs/tile/createTileLeadingSlot.js.map +1 -0
  63. package/lib/commonjs/tile/createTileRoot.js +133 -0
  64. package/lib/commonjs/tile/createTileRoot.js.map +1 -0
  65. package/lib/commonjs/tile/createTileTitle.js +28 -0
  66. package/lib/commonjs/tile/createTileTitle.js.map +1 -0
  67. package/lib/commonjs/tile/createTileTrailingSlot.js +35 -0
  68. package/lib/commonjs/tile/createTileTrailingSlot.js.map +1 -0
  69. package/lib/commonjs/tile/index.js +55 -0
  70. package/lib/commonjs/tile/index.js.map +1 -0
  71. package/lib/commonjs/tile/types.js +6 -0
  72. package/lib/commonjs/tile/types.js.map +1 -0
  73. package/lib/module/checkbox/createCheckboxRoot.web.js +8 -2
  74. package/lib/module/checkbox/createCheckboxRoot.web.js.map +1 -1
  75. package/lib/module/field/createFieldLabel.js.map +1 -1
  76. package/lib/module/index.js +3 -0
  77. package/lib/module/index.js.map +1 -1
  78. package/lib/module/input/createInputField.js +5 -0
  79. package/lib/module/input/createInputField.js.map +1 -1
  80. package/lib/module/list-item/context.js +5 -0
  81. package/lib/module/list-item/context.js.map +1 -0
  82. package/lib/module/list-item/createListItemContent.js +24 -0
  83. package/lib/module/list-item/createListItemContent.js.map +1 -0
  84. package/lib/module/list-item/createListItemDescription.js +19 -0
  85. package/lib/module/list-item/createListItemDescription.js.map +1 -0
  86. package/lib/module/list-item/createListItemLeadingSlot.js +28 -0
  87. package/lib/module/list-item/createListItemLeadingSlot.js.map +1 -0
  88. package/lib/module/list-item/createListItemMeta.js +19 -0
  89. package/lib/module/list-item/createListItemMeta.js.map +1 -0
  90. package/lib/module/list-item/createListItemRoot.js +151 -0
  91. package/lib/module/list-item/createListItemRoot.js.map +1 -0
  92. package/lib/module/list-item/createListItemSectionHeader.js +48 -0
  93. package/lib/module/list-item/createListItemSectionHeader.js.map +1 -0
  94. package/lib/module/list-item/createListItemTitle.js +19 -0
  95. package/lib/module/list-item/createListItemTitle.js.map +1 -0
  96. package/lib/module/list-item/createListItemTrailingSlot.js +22 -0
  97. package/lib/module/list-item/createListItemTrailingSlot.js.map +1 -0
  98. package/lib/module/list-item/index.js +39 -0
  99. package/lib/module/list-item/index.js.map +1 -0
  100. package/lib/module/list-item/types.js +4 -0
  101. package/lib/module/list-item/types.js.map +1 -0
  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/createSelectTrigger.js +19 -6
  121. package/lib/module/select/createSelectTrigger.js.map +1 -1
  122. package/lib/module/tile/context.js +21 -0
  123. package/lib/module/tile/context.js.map +1 -0
  124. package/lib/module/tile/createTileContent.js +24 -0
  125. package/lib/module/tile/createTileContent.js.map +1 -0
  126. package/lib/module/tile/createTileDescription.js +22 -0
  127. package/lib/module/tile/createTileDescription.js.map +1 -0
  128. package/lib/module/tile/createTileGroup.js +106 -0
  129. package/lib/module/tile/createTileGroup.js.map +1 -0
  130. package/lib/module/tile/createTileIndicator.js +40 -0
  131. package/lib/module/tile/createTileIndicator.js.map +1 -0
  132. package/lib/module/tile/createTileLeadingSlot.js +28 -0
  133. package/lib/module/tile/createTileLeadingSlot.js.map +1 -0
  134. package/lib/module/tile/createTileRoot.js +127 -0
  135. package/lib/module/tile/createTileRoot.js.map +1 -0
  136. package/lib/module/tile/createTileTitle.js +22 -0
  137. package/lib/module/tile/createTileTitle.js.map +1 -0
  138. package/lib/module/tile/createTileTrailingSlot.js +29 -0
  139. package/lib/module/tile/createTileTrailingSlot.js.map +1 -0
  140. package/lib/module/tile/index.js +39 -0
  141. package/lib/module/tile/index.js.map +1 -0
  142. package/lib/module/tile/types.js +4 -0
  143. package/lib/module/tile/types.js.map +1 -0
  144. package/lib/typescript/checkbox/createCheckboxRoot.web.d.ts +4 -0
  145. package/lib/typescript/checkbox/createCheckboxRoot.web.d.ts.map +1 -1
  146. package/lib/typescript/checkbox/useCheckboxRoot.d.ts +2 -0
  147. package/lib/typescript/checkbox/useCheckboxRoot.d.ts.map +1 -1
  148. package/lib/typescript/field/createFieldLabel.d.ts.map +1 -1
  149. package/lib/typescript/index.d.ts +3 -0
  150. package/lib/typescript/index.d.ts.map +1 -1
  151. package/lib/typescript/input/createInputField.d.ts.map +1 -1
  152. package/lib/typescript/list-item/context.d.ts +6 -0
  153. package/lib/typescript/list-item/context.d.ts.map +1 -0
  154. package/lib/typescript/list-item/createListItemContent.d.ts +3 -0
  155. package/lib/typescript/list-item/createListItemContent.d.ts.map +1 -0
  156. package/lib/typescript/list-item/createListItemDescription.d.ts +3 -0
  157. package/lib/typescript/list-item/createListItemDescription.d.ts.map +1 -0
  158. package/lib/typescript/list-item/createListItemLeadingSlot.d.ts +4 -0
  159. package/lib/typescript/list-item/createListItemLeadingSlot.d.ts.map +1 -0
  160. package/lib/typescript/list-item/createListItemMeta.d.ts +3 -0
  161. package/lib/typescript/list-item/createListItemMeta.d.ts.map +1 -0
  162. package/lib/typescript/list-item/createListItemRoot.d.ts +4 -0
  163. package/lib/typescript/list-item/createListItemRoot.d.ts.map +1 -0
  164. package/lib/typescript/list-item/createListItemSectionHeader.d.ts +4 -0
  165. package/lib/typescript/list-item/createListItemSectionHeader.d.ts.map +1 -0
  166. package/lib/typescript/list-item/createListItemTitle.d.ts +3 -0
  167. package/lib/typescript/list-item/createListItemTitle.d.ts.map +1 -0
  168. package/lib/typescript/list-item/createListItemTrailingSlot.d.ts +3 -0
  169. package/lib/typescript/list-item/createListItemTrailingSlot.d.ts.map +1 -0
  170. package/lib/typescript/list-item/index.d.ts +16 -0
  171. package/lib/typescript/list-item/index.d.ts.map +1 -0
  172. package/lib/typescript/list-item/types.d.ts +86 -0
  173. package/lib/typescript/list-item/types.d.ts.map +1 -0
  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/createSelectTrigger.d.ts.map +1 -1
  193. package/lib/typescript/tile/context.d.ts +9 -0
  194. package/lib/typescript/tile/context.d.ts.map +1 -0
  195. package/lib/typescript/tile/createTileContent.d.ts +3 -0
  196. package/lib/typescript/tile/createTileContent.d.ts.map +1 -0
  197. package/lib/typescript/tile/createTileDescription.d.ts +3 -0
  198. package/lib/typescript/tile/createTileDescription.d.ts.map +1 -0
  199. package/lib/typescript/tile/createTileGroup.d.ts +4 -0
  200. package/lib/typescript/tile/createTileGroup.d.ts.map +1 -0
  201. package/lib/typescript/tile/createTileIndicator.d.ts +4 -0
  202. package/lib/typescript/tile/createTileIndicator.d.ts.map +1 -0
  203. package/lib/typescript/tile/createTileLeadingSlot.d.ts +4 -0
  204. package/lib/typescript/tile/createTileLeadingSlot.d.ts.map +1 -0
  205. package/lib/typescript/tile/createTileRoot.d.ts +4 -0
  206. package/lib/typescript/tile/createTileRoot.d.ts.map +1 -0
  207. package/lib/typescript/tile/createTileTitle.d.ts +3 -0
  208. package/lib/typescript/tile/createTileTitle.d.ts.map +1 -0
  209. package/lib/typescript/tile/createTileTrailingSlot.d.ts +9 -0
  210. package/lib/typescript/tile/createTileTrailingSlot.d.ts.map +1 -0
  211. package/lib/typescript/tile/index.d.ts +15 -0
  212. package/lib/typescript/tile/index.d.ts.map +1 -0
  213. package/lib/typescript/tile/types.d.ts +119 -0
  214. package/lib/typescript/tile/types.d.ts.map +1 -0
  215. package/package.json +5 -2
  216. package/src/checkbox/createCheckboxRoot.web.tsx +6 -2
  217. package/src/field/createFieldLabel.tsx +4 -1
  218. package/src/index.ts +3 -0
  219. package/src/input/createInputField.tsx +6 -0
  220. package/src/list-item/context.tsx +5 -0
  221. package/src/list-item/createListItemContent.tsx +23 -0
  222. package/src/list-item/createListItemDescription.tsx +19 -0
  223. package/src/list-item/createListItemLeadingSlot.tsx +30 -0
  224. package/src/list-item/createListItemMeta.tsx +17 -0
  225. package/src/list-item/createListItemRoot.tsx +178 -0
  226. package/src/list-item/createListItemSectionHeader.tsx +53 -0
  227. package/src/list-item/createListItemTitle.tsx +17 -0
  228. package/src/list-item/createListItemTrailingSlot.tsx +21 -0
  229. package/src/list-item/index.ts +88 -0
  230. package/src/list-item/types.ts +122 -0
  231. package/src/radio/context.tsx +21 -0
  232. package/src/radio/createRadioGroup.tsx +67 -0
  233. package/src/radio/createRadioIndicator.tsx +32 -0
  234. package/src/radio/createRadioLabel.tsx +28 -0
  235. package/src/radio/createRadioRoot.tsx +100 -0
  236. package/src/radio/createRadioRoot.web.tsx +81 -0
  237. package/src/radio/index.ts +37 -0
  238. package/src/radio/types.ts +67 -0
  239. package/src/radio/useRadioRoot.ts +69 -0
  240. package/src/select/createSelectTrigger.tsx +26 -3
  241. package/src/tile/context.tsx +23 -0
  242. package/src/tile/createTileContent.tsx +23 -0
  243. package/src/tile/createTileDescription.tsx +19 -0
  244. package/src/tile/createTileGroup.tsx +134 -0
  245. package/src/tile/createTileIndicator.tsx +38 -0
  246. package/src/tile/createTileLeadingSlot.tsx +30 -0
  247. package/src/tile/createTileRoot.tsx +124 -0
  248. package/src/tile/createTileTitle.tsx +19 -0
  249. package/src/tile/createTileTrailingSlot.tsx +25 -0
  250. package/src/tile/index.ts +88 -0
  251. package/src/tile/types.ts +153 -0
@@ -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
+ }
@@ -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
+ );
@@ -0,0 +1,30 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileLeadingSlotProps } from './types';
4
+
5
+ const shrinkZero = { flexShrink: 0 as const };
6
+
7
+ export const createTileLeadingSlot = <T,>(Base: React.ComponentType<T>) =>
8
+ forwardRef(
9
+ (
10
+ { 'aria-hidden': ariaHidden, style, children, ...props }: ITileLeadingSlotProps,
11
+ ref: React.Ref<unknown>,
12
+ ) => {
13
+ const accessibilityElementsHidden = ariaHidden !== false;
14
+
15
+ return (
16
+ <Base
17
+ {...(props as T)}
18
+ {...dataAttributes({
19
+ slot: 'tile-leading',
20
+ })}
21
+ accessibilityElementsHidden={accessibilityElementsHidden}
22
+ aria-hidden={ariaHidden}
23
+ ref={ref as React.Ref<T>}
24
+ style={[shrinkZero, style]}
25
+ >
26
+ {children}
27
+ </Base>
28
+ );
29
+ },
30
+ );
@@ -0,0 +1,124 @@
1
+ import React, { forwardRef, useMemo } from 'react';
2
+ import { composeEventHandlers, mergeRefs, useControllableState } from '@cdx-ui/utils';
3
+ import { useFocus } from '@react-native-aria/focus';
4
+ import { useHover, usePress } from '@react-native-aria/interactions';
5
+ import { dataAttributes } from '../utils/dataAttributes';
6
+ import { TileProvider, useOptionalTileGroupContext } from './context';
7
+ import type { ITileProps, ITilePressablePassthrough } from './types';
8
+
9
+ const rowStyle = {
10
+ flexDirection: 'row' as const,
11
+ alignSelf: 'stretch' as const,
12
+ alignItems: 'center' as const,
13
+ };
14
+
15
+ export const createTileRoot = <T,>(BasePressable: React.ComponentType<T>) =>
16
+ forwardRef((props: ITileProps, ref: React.Ref<unknown>) => {
17
+ const {
18
+ value,
19
+ disabled: disabledProp = false,
20
+ isSelected: controlledSelected,
21
+ defaultSelected,
22
+ onSelectedChange,
23
+ children,
24
+ onPress,
25
+ onFocus,
26
+ onBlur,
27
+ style,
28
+ /** Consumed by styled `withStyleContext` root. */
29
+ context: _styleContext,
30
+ ...rest
31
+ } = props;
32
+
33
+ const group = useOptionalTileGroupContext();
34
+
35
+ // Standalone selection state — only meaningful when no group owns selection.
36
+ const [standaloneSelected = false, setStandaloneSelected] = useControllableState<boolean>({
37
+ prop: controlledSelected,
38
+ defaultProp: defaultSelected ?? false,
39
+ onChange: (next) => {
40
+ onSelectedChange?.(next);
41
+ },
42
+ });
43
+
44
+ const isSelected = group ? group.isSelected(value) : standaloneSelected;
45
+ const disabledByGroup = group ? group.isTileDisabledByGroup(value) : false;
46
+ const isDisabled = disabledByGroup || disabledProp;
47
+ const cannotToggle = isDisabled;
48
+
49
+ const { focusProps, isFocused } = useFocus();
50
+ const { pressProps, isPressed } = usePress({ isDisabled: cannotToggle });
51
+ const { hoverProps, isHovered } = useHover();
52
+
53
+ // Standalone tiles use checkbox semantics (independent on/off toggle).
54
+ const selectionType = group ? group.type : 'multiple';
55
+
56
+ const accessibilityState = useMemo(
57
+ () => ({
58
+ disabled: isDisabled,
59
+ ...(selectionType === 'single' ? { selected: isSelected } : { checked: isSelected }),
60
+ }),
61
+ [isDisabled, isSelected, selectionType],
62
+ );
63
+
64
+ const tileContext = useMemo(
65
+ () => ({ value, isSelected, isDisabled, selectionType }),
66
+ [value, isSelected, isDisabled, selectionType],
67
+ );
68
+
69
+ const passthrough = rest as ITilePressablePassthrough;
70
+
71
+ const composedOnPress = composeEventHandlers(onPress, () => {
72
+ if (cannotToggle) {
73
+ return;
74
+ }
75
+ if (group) {
76
+ group.toggleValue(value);
77
+ } else {
78
+ setStandaloneSelected(!standaloneSelected);
79
+ }
80
+ });
81
+
82
+ const sharedHandlers = {
83
+ onPress: isDisabled ? undefined : composedOnPress,
84
+ onPressIn: composeEventHandlers(passthrough.onPressIn, pressProps.onPressIn),
85
+ onPressOut: composeEventHandlers(passthrough.onPressOut, pressProps.onPressOut),
86
+ onHoverIn: composeEventHandlers(passthrough.onHoverIn, hoverProps.onHoverIn),
87
+ onHoverOut: composeEventHandlers(passthrough.onHoverOut, hoverProps.onHoverOut),
88
+ onFocus: composeEventHandlers(onFocus, focusProps.onFocus),
89
+ onBlur: composeEventHandlers(onBlur, focusProps.onBlur),
90
+ } as const;
91
+
92
+ const role = selectionType === 'single' ? 'radio' : 'checkbox';
93
+
94
+ const sharedAttrs = {
95
+ accessibilityRole: role,
96
+ role,
97
+ accessibilityState,
98
+ 'aria-checked': isSelected,
99
+ ...(isDisabled ? { 'aria-disabled': true as const } : {}),
100
+ ...dataAttributes({
101
+ slot: 'tile',
102
+ state: isSelected ? 'selected' : 'unselected',
103
+ disabled: isDisabled,
104
+ active: isPressed,
105
+ hover: isHovered,
106
+ focused: isFocused,
107
+ }),
108
+ };
109
+
110
+ return (
111
+ <TileProvider value={tileContext}>
112
+ <BasePressable
113
+ {...(rest as T)}
114
+ context={_styleContext}
115
+ ref={mergeRefs(ref) as React.Ref<T>}
116
+ {...sharedAttrs}
117
+ {...sharedHandlers}
118
+ style={[rowStyle, style]}
119
+ >
120
+ {children}
121
+ </BasePressable>
122
+ </TileProvider>
123
+ );
124
+ });
@@ -0,0 +1,19 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileTitleProps } from './types';
4
+
5
+ const noUnderline = { textDecorationLine: 'none' as const };
6
+
7
+ export const createTileTitle = <T,>(Base: React.ComponentType<T>) =>
8
+ forwardRef(({ children, style, ...props }: ITileTitleProps, ref: React.Ref<unknown>) => (
9
+ <Base
10
+ {...(props as T)}
11
+ {...dataAttributes({
12
+ slot: 'tile-title',
13
+ })}
14
+ ref={ref as React.Ref<T>}
15
+ style={[noUnderline, style]}
16
+ >
17
+ {children}
18
+ </Base>
19
+ ));
@@ -0,0 +1,25 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import type { ITileTrailingSlotProps } from './types';
4
+
5
+ const shrinkZero = { flexShrink: 0 as const };
6
+
7
+ /**
8
+ * Generic trailing content (chevron, amount, chip, status). Distinct from `Tile.Indicator`,
9
+ * which is purpose-built for the radio/checkbox selection affordance.
10
+ *
11
+ * Trailing content is meaningful by default (`aria-hidden` falsy), unlike the indicator.
12
+ */
13
+ export const createTileTrailingSlot = <T,>(Base: React.ComponentType<T>) =>
14
+ forwardRef(({ children, style, ...props }: ITileTrailingSlotProps, ref: React.Ref<unknown>) => (
15
+ <Base
16
+ {...(props as T)}
17
+ {...dataAttributes({
18
+ slot: 'tile-trailing',
19
+ })}
20
+ ref={ref as React.Ref<T>}
21
+ style={[shrinkZero, style]}
22
+ >
23
+ {children}
24
+ </Base>
25
+ ));
@@ -0,0 +1,88 @@
1
+ import type React from 'react';
2
+ import { createTileContent } from './createTileContent';
3
+ import { createTileDescription } from './createTileDescription';
4
+ import { createTileGroup } from './createTileGroup';
5
+ import { createTileIndicator } from './createTileIndicator';
6
+ import { createTileLeadingSlot } from './createTileLeadingSlot';
7
+ import { createTileRoot } from './createTileRoot';
8
+ import { createTileTitle } from './createTileTitle';
9
+ import { createTileTrailingSlot } from './createTileTrailingSlot';
10
+ import type { ITileComponentType } from './types';
11
+
12
+ export type {
13
+ ITileComponentType,
14
+ ITileContentProps,
15
+ ITileContextValue,
16
+ ITileDescriptionProps,
17
+ ITileGroupMultipleProps,
18
+ ITileGroupProps,
19
+ ITileGroupSingleProps,
20
+ ITileGroupContextValue,
21
+ ITileIndicatorProps,
22
+ ITileLeadingSlotProps,
23
+ ITilePressablePassthrough,
24
+ ITileProps,
25
+ ITileTitleProps,
26
+ ITileTrailingSlotProps,
27
+ TileGroupType,
28
+ TileGroupValue,
29
+ } from './types';
30
+
31
+ export { TileProvider, useTileContext } from './context';
32
+
33
+ export function createTile<
34
+ Pressable,
35
+ Leading,
36
+ Content,
37
+ Title,
38
+ Description,
39
+ Indicator,
40
+ TrailingSlot,
41
+ Group,
42
+ >(BaseComponents: {
43
+ Pressable: React.ComponentType<Pressable>;
44
+ LeadingSlot: React.ComponentType<Leading>;
45
+ Content: React.ComponentType<Content>;
46
+ Title: React.ComponentType<Title>;
47
+ Description: React.ComponentType<Description>;
48
+ Indicator: React.ComponentType<Indicator>;
49
+ TrailingSlot: React.ComponentType<TrailingSlot>;
50
+ Group: React.ComponentType<Group>;
51
+ }) {
52
+ const Tile = createTileRoot(BaseComponents.Pressable);
53
+ const Group = createTileGroup(BaseComponents.Group);
54
+ const LeadingSlot = createTileLeadingSlot(BaseComponents.LeadingSlot);
55
+ const Content = createTileContent(BaseComponents.Content);
56
+ const Title = createTileTitle(BaseComponents.Title);
57
+ const Description = createTileDescription(BaseComponents.Description);
58
+ const Indicator = createTileIndicator(BaseComponents.Indicator);
59
+ const TrailingSlot = createTileTrailingSlot(BaseComponents.TrailingSlot);
60
+
61
+ Tile.displayName = 'TilePrimitive';
62
+ Group.displayName = 'TilePrimitive.Group';
63
+ LeadingSlot.displayName = 'TilePrimitive.LeadingSlot';
64
+ Content.displayName = 'TilePrimitive.Content';
65
+ Title.displayName = 'TilePrimitive.Title';
66
+ Description.displayName = 'TilePrimitive.Description';
67
+ Indicator.displayName = 'TilePrimitive.Indicator';
68
+ TrailingSlot.displayName = 'TilePrimitive.TrailingSlot';
69
+
70
+ return Object.assign(Tile, {
71
+ Group,
72
+ LeadingSlot,
73
+ Content,
74
+ Title,
75
+ Description,
76
+ Indicator,
77
+ TrailingSlot,
78
+ }) as ITileComponentType<
79
+ Pressable,
80
+ Leading,
81
+ Content,
82
+ Title,
83
+ Description,
84
+ Indicator,
85
+ TrailingSlot,
86
+ Group
87
+ >;
88
+ }