@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
@@ -6,8 +6,10 @@ import { CheckboxProvider } from './context';
6
6
  import type { ICheckboxProps } from './types';
7
7
  import { useCheckboxRoot } from './useCheckboxRoot';
8
8
 
9
- // TODO: Label is taking focus on keyboard navigation
10
-
9
+ /**
10
+ * Web uses a native checkbox input (VisuallyHidden) for ARIA + focus management. The outer
11
+ * `Pressable` must not be in the tab order or each control consumes two Tab stops (wrapper + input).
12
+ */
11
13
  export const createCheckboxRoot = <T,>(BaseCheckbox: React.ComponentType<T>) =>
12
14
  forwardRef(({ children, ...props }: ICheckboxProps, ref?: React.Ref<T>) => {
13
15
  const {
@@ -41,6 +43,8 @@ export const createCheckboxRoot = <T,>(BaseCheckbox: React.ComponentType<T>) =>
41
43
  <BaseCheckbox
42
44
  {...(combinedProps as T)}
43
45
  ref={mergedRef}
46
+ // Native input is the only tab stop; Pressable is still clickable (mouse / screen reader).
47
+ tabIndex={-1}
44
48
  role="label"
45
49
  // eslint-disable-next-line react-native-a11y/has-valid-accessibility-role
46
50
  accessibilityRole="label"
@@ -0,0 +1,144 @@
1
+ import React, { forwardRef } from 'react';
2
+ import type { PressableProps } from 'react-native';
3
+ import { composeEventHandlers, mergeRefs } from '@cdx-ui/utils';
4
+ import { useFocus, useFocusRing } from '@react-native-aria/focus';
5
+ import { useHover, usePress } from '@react-native-aria/interactions';
6
+ import { dataAttributes } from '../utils/dataAttributes';
7
+ import type { IChipPressablePassthrough, IChipProps } from './types';
8
+
9
+ const rowStyle = {
10
+ flexDirection: 'row' as const,
11
+ alignItems: 'center' as const,
12
+ alignSelf: 'flex-start' as const,
13
+ };
14
+
15
+ const chipSlotAttrs = dataAttributes({ slot: 'chip' });
16
+
17
+ export const createChipRoot = <V, P>(
18
+ BaseView: React.ComponentType<V>,
19
+ BasePressable: React.ComponentType<P>,
20
+ ) =>
21
+ forwardRef(
22
+ (
23
+ {
24
+ asChild = false,
25
+ children,
26
+ onPress,
27
+ disabled = false,
28
+ accessibilityRole,
29
+ style,
30
+ ...restProps
31
+ }: IChipProps,
32
+ ref: React.Ref<unknown>,
33
+ ) => {
34
+ const childOnPress = React.isValidElement(children)
35
+ ? ((children.props as { onPress?: unknown }).onPress as IChipProps['onPress'] | undefined)
36
+ : undefined;
37
+
38
+ const asChildInteractive = asChild && React.isValidElement(children);
39
+ const isPressableRoot = !!onPress && !asChildInteractive;
40
+
41
+ const pressState = usePress({
42
+ isDisabled: !isPressableRoot || disabled,
43
+ });
44
+ const isPressed = pressState.isPressed;
45
+ const pressProps = pressState.pressProps as Pick<PressableProps, 'onPressIn' | 'onPressOut'>;
46
+
47
+ const { hoverProps, isHovered } = useHover();
48
+ const { isFocused, focusProps } = useFocus();
49
+ const { isFocusVisible, focusProps: focusRingProps } = useFocusRing() as {
50
+ isFocusVisible: boolean;
51
+ focusProps: typeof focusProps;
52
+ };
53
+
54
+ if (asChildInteractive) {
55
+ const child = children as React.ReactElement<Record<string, unknown>>;
56
+
57
+ const childDisabled = !!(child.props as { disabled?: boolean }).disabled;
58
+ const resolvedDisabled = disabled || childDisabled;
59
+
60
+ const cloneProps: Record<string, unknown> = {
61
+ ...restProps,
62
+ ...chipSlotAttrs,
63
+ ...dataAttributes({
64
+ active: false,
65
+ hover: false,
66
+ disabled: resolvedDisabled,
67
+ focused: false,
68
+ focusVisible: false,
69
+ }),
70
+ ...(resolvedDisabled && { accessibilityState: { disabled: true } }),
71
+ disabled: resolvedDisabled,
72
+ ref: mergeRefs(ref, child.props.ref as React.Ref<unknown>),
73
+ style: [rowStyle, style, child.props.style],
74
+ };
75
+
76
+ if (resolvedDisabled) {
77
+ cloneProps.onPress = undefined;
78
+ } else if (onPress) {
79
+ cloneProps.onPress = composeEventHandlers(childOnPress, onPress);
80
+ }
81
+
82
+ return React.cloneElement(child, cloneProps);
83
+ }
84
+
85
+ if (isPressableRoot) {
86
+ const interactionAttrs = dataAttributes({
87
+ active: isPressed,
88
+ hover: isHovered,
89
+ disabled,
90
+ focused: isFocused,
91
+ focusVisible: isFocusVisible,
92
+ });
93
+
94
+ const {
95
+ onPressIn: onPressInProp,
96
+ onPressOut: onPressOutProp,
97
+ onHoverIn: onHoverInProp,
98
+ onHoverOut: onHoverOutProp,
99
+ onFocus: onFocusProp,
100
+ onBlur: onBlurProp,
101
+ } = restProps as IChipPressablePassthrough & Pick<PressableProps, 'onFocus' | 'onBlur'>;
102
+
103
+ return (
104
+ <BasePressable
105
+ {...(restProps as P)}
106
+ {...chipSlotAttrs}
107
+ {...interactionAttrs}
108
+ accessibilityRole={accessibilityRole ?? 'button'}
109
+ {...(disabled && { accessibilityState: { disabled: true } })}
110
+ disabled={disabled}
111
+ ref={ref as React.Ref<P>}
112
+ style={[rowStyle, style]}
113
+ onPress={disabled ? undefined : onPress}
114
+ onPressIn={composeEventHandlers(onPressInProp, pressProps.onPressIn)}
115
+ onPressOut={composeEventHandlers(onPressOutProp, pressProps.onPressOut)}
116
+ onHoverIn={composeEventHandlers(onHoverInProp, hoverProps.onHoverIn)}
117
+ onHoverOut={composeEventHandlers(onHoverOutProp, hoverProps.onHoverOut)}
118
+ onFocus={composeEventHandlers(
119
+ composeEventHandlers(onFocusProp, focusProps.onFocus),
120
+ focusRingProps.onFocus,
121
+ )}
122
+ onBlur={composeEventHandlers(
123
+ composeEventHandlers(onBlurProp, focusProps.onBlur),
124
+ focusRingProps.onBlur,
125
+ )}
126
+ >
127
+ {children}
128
+ </BasePressable>
129
+ );
130
+ }
131
+
132
+ return (
133
+ <BaseView
134
+ {...(restProps as V)}
135
+ {...chipSlotAttrs}
136
+ {...(disabled ? dataAttributes({ disabled: true }) : undefined)}
137
+ ref={ref as React.Ref<V>}
138
+ style={[rowStyle, style]}
139
+ >
140
+ {children}
141
+ </BaseView>
142
+ );
143
+ },
144
+ );
@@ -0,0 +1,18 @@
1
+ import type React from 'react';
2
+ import type { PressableProps, ViewProps } from 'react-native';
3
+ import { createChipRoot } from './createChipRoot';
4
+ import type { IChipComponentType } from './types';
5
+
6
+ export type { IChipComponentType, IChipPressablePassthrough, IChipProps } from './types';
7
+
8
+ export function createChip({
9
+ View,
10
+ Pressable,
11
+ }: {
12
+ View: React.ComponentType<ViewProps>;
13
+ Pressable: React.ComponentType<PressableProps>;
14
+ }) {
15
+ const Chip = createChipRoot(View, Pressable);
16
+ Chip.displayName = 'ChipPrimitive';
17
+ return Chip as IChipComponentType<ViewProps | PressableProps>;
18
+ }
@@ -0,0 +1,23 @@
1
+ import type { ForwardRefExoticComponent, RefAttributes } from 'react';
2
+ import type { PressableProps, ViewProps } from 'react-native';
3
+
4
+ export type IChipPressablePassthrough = Partial<
5
+ Pick<PressableProps, 'onPressIn' | 'onPressOut' | 'onHoverIn' | 'onHoverOut'>
6
+ >;
7
+
8
+ export interface IChipProps extends ViewProps, IChipPressablePassthrough {
9
+ /**
10
+ * When set (and `asChild` is not used with a valid element), root renders as pressable.
11
+ */
12
+ onPress?: PressableProps['onPress'];
13
+ /** Disables press handling when the root is pressable. */
14
+ disabled?: boolean;
15
+ /**
16
+ * Merge slot + interaction props onto a single child instead of rendering the default pressable root.
17
+ */
18
+ asChild?: boolean;
19
+ }
20
+
21
+ export type IChipComponentType<RootRef> = ForwardRefExoticComponent<
22
+ RefAttributes<RootRef> & IChipProps
23
+ >;
@@ -20,7 +20,10 @@ export const createFieldLabel = <T,>(BaseFormLabel: React.ComponentType<T>) =>
20
20
  {...(props as T)}
21
21
  id={labelId}
22
22
  htmlFor={htmlFor}
23
- {...dataAttributes({ invalid: field.isInvalid, required: field.isRequired })}
23
+ {...dataAttributes({
24
+ invalid: field.isInvalid,
25
+ required: field.isRequired,
26
+ })}
24
27
  >
25
28
  {children}
26
29
  {field.isRequired ? requiredIndicator : null}
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './avatar';
2
2
  export * from './button';
3
+ export * from './chip';
3
4
  export * from './checkbox';
4
5
  export * from './dialog';
5
6
  export * from './field';
@@ -11,6 +12,9 @@ export * from './list-item';
11
12
  export { type EdgeInsets, OverlayInsetsProvider } from './overlay';
12
13
  export * from './select';
13
14
  export * from './switch';
15
+ export * from './tile';
14
16
  export * from './progress';
17
+ export * from './radio';
15
18
  export type { InteractionState } from './types';
16
19
  export { dataAttributes } from './utils/dataAttributes';
20
+ export { domDataAttributes, type DomDataAttributeValue } from './utils/domDataAttributes';
@@ -49,8 +49,14 @@ export const createInputField = <T,>(BaseInputField: React.ComponentType<T>) =>
49
49
 
50
50
  const field = useFormControlContext();
51
51
 
52
+ const reportLabelFocus = (focused: boolean) => {
53
+ const active = focused && !(isDisabled || inputProps.disabled);
54
+ field.setIsLabelFocused?.(active);
55
+ };
56
+
52
57
  const handleFocus = (focusState: boolean, callback: any) => {
53
58
  setIsFocused(focusState);
59
+ reportLabelFocus(focusState);
54
60
  callback();
55
61
  };
56
62
 
@@ -0,0 +1,10 @@
1
+ import { createContext } from '@cdx-ui/utils';
2
+
3
+ export interface LinkInteractionState {
4
+ hover: boolean;
5
+ visited: boolean;
6
+ focusVisible: boolean;
7
+ }
8
+
9
+ export const [LinkInteractionProvider, useLinkInteractionContext] =
10
+ createContext<LinkInteractionState>('LinkInteractionContext');
@@ -1,8 +1,9 @@
1
- import React, { forwardRef } from 'react';
1
+ import React, { forwardRef, useMemo } from 'react';
2
2
  import { composeEventHandlers } from '@cdx-ui/utils';
3
3
  import { useFocusRing, useFocus } from '@react-native-aria/focus';
4
4
  import { useHover, usePress } from '@react-native-aria/interactions';
5
5
  import { dataAttributes } from '../utils/dataAttributes';
6
+ import { LinkInteractionProvider } from './context';
6
7
  import { useLink } from './useLink';
7
8
  import type { ILinkProps } from './types';
8
9
 
@@ -15,6 +16,7 @@ export const createLink = <RootT, R = unknown>({ Root }: { Root: React.Component
15
16
  isPressed: isPressedProp,
16
17
  isFocused: isFocusedProp,
17
18
  isFocusVisible: isFocusVisibleProp,
19
+ isVisited: isVisitedProp,
18
20
  href,
19
21
  onPress,
20
22
  action,
@@ -38,34 +40,50 @@ export const createLink = <RootT, R = unknown>({ Root }: { Root: React.Component
38
40
  webProps,
39
41
  });
40
42
 
43
+ const hover = isHoveredProp || isHovered;
44
+ const visited = isVisitedProp ?? false;
45
+ const focusVisible = isFocusVisibleProp || isFocusVisible;
46
+
47
+ const interactionState = useMemo(
48
+ () => ({
49
+ hover,
50
+ visited,
51
+ focusVisible,
52
+ }),
53
+ [hover, visited, focusVisible],
54
+ );
55
+
41
56
  const interactionAttrs = dataAttributes({
42
- hover: isHoveredProp || isHovered,
57
+ hover,
43
58
  focus: isFocusedProp || isFocused,
44
59
  active: isPressedProp || isPressed,
45
- focusVisible: isFocusVisibleProp || isFocusVisible,
60
+ focusVisible,
61
+ visited,
46
62
  });
47
63
 
48
64
  return (
49
- <Root
50
- ref={ref}
51
- {...interactionAttrs}
52
- {...linkProps}
53
- {...(props as RootT)}
54
- onPressIn={composeEventHandlers(props?.onPressIn, pressProps.onPressIn)}
55
- onPressOut={composeEventHandlers(props?.onPressOut, pressProps.onPressOut)}
56
- onHoverIn={composeEventHandlers(props?.onHoverIn, hoverProps.onHoverIn)}
57
- onHoverOut={composeEventHandlers(props?.onHoverOut, hoverProps.onHoverOut)}
58
- onFocus={composeEventHandlers(
59
- composeEventHandlers(props?.onFocus, focusProps.onFocus),
60
- focusRingProps.onFocus,
61
- )}
62
- onBlur={composeEventHandlers(
63
- composeEventHandlers(props?.onBlur, focusProps.onBlur),
64
- focusRingProps.onBlur,
65
- )}
66
- >
67
- {children}
68
- </Root>
65
+ <LinkInteractionProvider value={interactionState}>
66
+ <Root
67
+ ref={ref}
68
+ {...interactionAttrs}
69
+ {...linkProps}
70
+ {...(props as RootT)}
71
+ onPressIn={composeEventHandlers(props?.onPressIn, pressProps.onPressIn)}
72
+ onPressOut={composeEventHandlers(props?.onPressOut, pressProps.onPressOut)}
73
+ onHoverIn={composeEventHandlers(props?.onHoverIn, hoverProps.onHoverIn)}
74
+ onHoverOut={composeEventHandlers(props?.onHoverOut, hoverProps.onHoverOut)}
75
+ onFocus={composeEventHandlers(
76
+ composeEventHandlers(props?.onFocus, focusProps.onFocus),
77
+ focusRingProps.onFocus,
78
+ )}
79
+ onBlur={composeEventHandlers(
80
+ composeEventHandlers(props?.onBlur, focusProps.onBlur),
81
+ focusRingProps.onBlur,
82
+ )}
83
+ >
84
+ {children}
85
+ </Root>
86
+ </LinkInteractionProvider>
69
87
  );
70
88
  },
71
89
  );
@@ -1,4 +1,6 @@
1
1
  export { createLink } from './createLink';
2
+ export { LinkInteractionProvider, useLinkInteractionContext } from './context';
3
+ export type { LinkInteractionState } from './context';
2
4
  export { LinkProvider } from './LinkProvider';
3
5
  export type { LinkConfig } from './LinkProvider';
4
6
  export { useLink } from './useLink';
package/src/link/types.ts CHANGED
@@ -32,6 +32,7 @@ export interface ILinkProps extends PressableProps {
32
32
  isHovered?: boolean;
33
33
  isFocused?: boolean;
34
34
  isFocusVisible?: boolean;
35
+ isVisited?: boolean;
35
36
  /**
36
37
  * HTML anchor attributes applied only on web. Ignored on native platforms.
37
38
  * `target`, `rel`, and `download` are forwarded via react-native-web's `hrefAttrs`.
@@ -49,7 +49,17 @@ export const createListItemRoot = <V, P>(
49
49
  }: IListItemProps,
50
50
  ref: React.Ref<unknown>,
51
51
  ) => {
52
- const asChildInteractive = asChild && !!onPress && React.isValidElement(children);
52
+ const childOnPress = React.isValidElement(children)
53
+ ? ((children.props as { onPress?: unknown }).onPress as
54
+ | IListItemProps['onPress']
55
+ | undefined)
56
+ : undefined;
57
+
58
+ // asChild always wins when explicitly set with a valid element — even when the press
59
+ // surface is built internally by the child (e.g. `Link` adds its onPress via `useLink`,
60
+ // not as a JSX-level prop). The clone path below preserves the child's own press by
61
+ // *omitting* `onPress` when there is nothing to compose.
62
+ const asChildInteractive = asChild && React.isValidElement(children);
53
63
  const isPressableRoot = !!onPress && !asChildInteractive;
54
64
 
55
65
  const pressState = usePress({
@@ -74,15 +84,33 @@ export const createListItemRoot = <V, P>(
74
84
  if (asChildInteractive) {
75
85
  const child = children as React.ReactElement<Record<string, unknown>>;
76
86
 
77
- const mergedOnPress = composeEventHandlers(
78
- child.props.onPress as IListItemProps['onPress'],
79
- onPress,
80
- );
81
-
82
87
  const childDisabled = !!(child.props as { disabled?: boolean }).disabled;
83
-
84
88
  const resolvedDisabled = disabled || childDisabled;
85
89
 
90
+ const cloneProps: Record<string, unknown> = {
91
+ ...restProps,
92
+ ...slotAttrs,
93
+ ...dataAttributes({
94
+ active: false,
95
+ hover: false,
96
+ disabled: resolvedDisabled,
97
+ }),
98
+ ...(resolvedDisabled && { accessibilityState: { disabled: true } }),
99
+ disabled: resolvedDisabled,
100
+ ref: mergeRefs(ref, child.props.ref as React.Ref<unknown>),
101
+ style: [rowStyleForCrossAlign(crossAlign), style, child.props.style],
102
+ };
103
+
104
+ // Only override the child's `onPress` when we have something to add (a parent handler)
105
+ // or need to suppress (disabled). Otherwise leave the child's own onPress alone — e.g.
106
+ // `Link` builds its navigation handler inside `useLink`, not as a JSX-level prop, and
107
+ // overriding here would shadow it.
108
+ if (resolvedDisabled) {
109
+ cloneProps.onPress = undefined;
110
+ } else if (onPress) {
111
+ cloneProps.onPress = composeEventHandlers(childOnPress, onPress);
112
+ }
113
+
86
114
  return (
87
115
  <ListItemProvider
88
116
  value={{
@@ -92,20 +120,7 @@ export const createListItemRoot = <V, P>(
92
120
  crossAlign: crossAlign ?? 'center',
93
121
  }}
94
122
  >
95
- {React.cloneElement(child, {
96
- ...restProps,
97
- ...slotAttrs,
98
- ...dataAttributes({
99
- active: false,
100
- hovered: false,
101
- disabled: resolvedDisabled,
102
- }),
103
- ...(resolvedDisabled && { accessibilityState: { disabled: true } }),
104
- disabled: resolvedDisabled,
105
- onPress: resolvedDisabled ? undefined : mergedOnPress,
106
- ref: mergeRefs(ref, child.props.ref as React.Ref<unknown>),
107
- style: [rowStyleForCrossAlign(crossAlign), style, child.props.style],
108
- })}
123
+ {React.cloneElement(child, cloneProps)}
109
124
  </ListItemProvider>
110
125
  );
111
126
  }
@@ -113,7 +128,7 @@ export const createListItemRoot = <V, P>(
113
128
  if (isPressableRoot) {
114
129
  const interactionAttrs = dataAttributes({
115
130
  active: isPressed,
116
- hovered: isHovered,
131
+ hover: isHovered,
117
132
  disabled,
118
133
  });
119
134
 
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { createContext } from '@cdx-ui/utils';
3
+ import type { RadioGroupState } from '@react-stately/radio';
4
+ import type { IRadioContextValue } from './types';
5
+
6
+ export const [RadioProvider, useRadioContext] = createContext<IRadioContextValue>('RadioContext');
7
+
8
+ export interface IRadioGroupState {
9
+ isReadOnly: boolean;
10
+ isDisabled: boolean;
11
+ isInvalid: boolean;
12
+ isRequired: boolean;
13
+ isSelected: (value: string) => boolean;
14
+ selectValue: (value: string) => void;
15
+ }
16
+
17
+ export const RadioGroupContext = React.createContext<{
18
+ state: IRadioGroupState;
19
+ radioGroupState: RadioGroupState;
20
+ name?: string;
21
+ } | null>(null);
@@ -0,0 +1,67 @@
1
+ import { forwardRef } from 'react';
2
+ import { useFormControlContext } from '@cdx-ui/utils';
3
+ import { useRadioGroup } from '@react-native-aria/radio';
4
+ import { useRadioGroupState } from '@react-stately/radio';
5
+ import { dataAttributes } from '../utils/dataAttributes';
6
+ import { RadioGroupContext } from './context';
7
+ import type { IRadioGroupProps } from './types';
8
+
9
+ export const createRadioGroup = <T,>(BaseRadioGroup: React.ComponentType<T>) =>
10
+ forwardRef(({ children, ...props }: IRadioGroupProps, ref?: React.Ref<T>) => {
11
+ const formControlContext = useFormControlContext();
12
+
13
+ const combinedProps = {
14
+ ...formControlContext,
15
+ ...props,
16
+ };
17
+
18
+ const radioGroupState = useRadioGroupState({
19
+ ...combinedProps,
20
+ validationState: combinedProps.isInvalid ? 'invalid' : 'valid',
21
+ });
22
+
23
+ const { radioGroupProps } = useRadioGroup(
24
+ {
25
+ ...combinedProps,
26
+ 'aria-label': combinedProps['aria-label'],
27
+ },
28
+ radioGroupState,
29
+ );
30
+
31
+ const isDisabled = combinedProps.isDisabled ?? false;
32
+ const isInvalid = combinedProps.isInvalid ?? false;
33
+ const isRequired = combinedProps.isRequired ?? false;
34
+ const isReadOnly = combinedProps.isReadOnly ?? false;
35
+
36
+ return (
37
+ <RadioGroupContext.Provider
38
+ value={{
39
+ state: {
40
+ isDisabled,
41
+ isInvalid,
42
+ isRequired,
43
+ isReadOnly,
44
+ isSelected: (value: string) => radioGroupState.selectedValue === value,
45
+ selectValue: (value: string) => radioGroupState.setSelectedValue(value),
46
+ },
47
+ radioGroupState,
48
+ name: combinedProps.name,
49
+ }}
50
+ >
51
+ <BaseRadioGroup
52
+ {...radioGroupProps}
53
+ {...(combinedProps as unknown as T)}
54
+ ref={ref}
55
+ aria-required={isRequired || undefined}
56
+ aria-readonly={isReadOnly || undefined}
57
+ {...dataAttributes({
58
+ slot: 'radio-group',
59
+ disabled: isDisabled,
60
+ invalid: isInvalid,
61
+ })}
62
+ >
63
+ {children}
64
+ </BaseRadioGroup>
65
+ </RadioGroupContext.Provider>
66
+ );
67
+ });
@@ -0,0 +1,32 @@
1
+ import { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import { useRadioContext } from './context';
4
+ import type { IRadioIndicatorProps } from './types';
5
+
6
+ export const createRadioIndicator = <T,>(BaseRadioIndicator: React.ComponentType<T>) =>
7
+ forwardRef<unknown, IRadioIndicatorProps & { className?: string }>(
8
+ ({ children, className, ...props }, ref) => {
9
+ const { isChecked, isDisabled, isHovered, isInvalid, isReadOnly, isPressed, isFocusVisible } =
10
+ useRadioContext();
11
+
12
+ return (
13
+ <BaseRadioIndicator
14
+ className={className}
15
+ {...dataAttributes({
16
+ slot: 'radio-indicator',
17
+ hover: isHovered,
18
+ checked: isChecked,
19
+ disabled: isDisabled,
20
+ focusVisible: isFocusVisible,
21
+ invalid: isInvalid,
22
+ readonly: isReadOnly,
23
+ active: isPressed,
24
+ })}
25
+ {...(props as T)}
26
+ ref={ref}
27
+ >
28
+ {children}
29
+ </BaseRadioIndicator>
30
+ );
31
+ },
32
+ );
@@ -0,0 +1,28 @@
1
+ import { forwardRef } from 'react';
2
+ import { dataAttributes } from '../utils/dataAttributes';
3
+ import { useRadioContext } from './context';
4
+ import type { IRadioLabelProps } from './types';
5
+
6
+ export const createRadioLabel = <T,>(BaseRadioLabel: React.ComponentType<T>) =>
7
+ forwardRef<unknown, IRadioLabelProps & { className?: string }>(
8
+ ({ children, className, ...props }, ref) => {
9
+ const { isChecked, isDisabled, isHovered, isInvalid, isReadOnly } = useRadioContext();
10
+
11
+ return (
12
+ <BaseRadioLabel
13
+ className={className}
14
+ {...dataAttributes({
15
+ hover: isHovered,
16
+ checked: isChecked,
17
+ disabled: isDisabled,
18
+ invalid: isInvalid,
19
+ readonly: isReadOnly,
20
+ })}
21
+ {...(props as T)}
22
+ ref={ref}
23
+ >
24
+ {children}
25
+ </BaseRadioLabel>
26
+ );
27
+ },
28
+ );