@primitiv-ui/react 0.1.27 → 0.1.29

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 (307) hide show
  1. package/README.md +4 -0
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +25 -4
  7. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  8. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  9. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  10. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  11. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  12. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  13. package/src/Accordion/types.ts +72 -21
  14. package/src/Alert/Alert.tsx +36 -7
  15. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  16. package/src/Alert/types.ts +18 -2
  17. package/src/Avatar/Avatar.tsx +99 -15
  18. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  19. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  20. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  21. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  22. package/src/Avatar/types.ts +15 -3
  23. package/src/Breadcrumb/Breadcrumb.tsx +136 -18
  24. package/src/Breadcrumb/README.md +47 -0
  25. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  27. package/src/Breadcrumb/types.ts +73 -7
  28. package/src/Button/Button.tsx +2 -0
  29. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  30. package/src/Button/types.ts +21 -0
  31. package/src/Carousel/Carousel.tsx +263 -15
  32. package/src/Carousel/README.md +749 -72
  33. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  34. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  35. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  36. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  37. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  38. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  39. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
  40. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  41. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  42. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  43. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  44. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  45. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  46. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  47. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  48. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  49. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  50. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  51. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  52. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  53. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  54. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  55. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  56. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  57. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  58. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  59. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  60. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  61. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  62. package/src/Carousel/hooks/index.ts +1 -0
  63. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  64. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  65. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  66. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  67. package/src/Carousel/loopEngine.ts +54 -0
  68. package/src/Carousel/types.ts +457 -35
  69. package/src/Checkbox/Checkbox.tsx +9 -1
  70. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  71. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  72. package/src/Checkbox/types.ts +19 -0
  73. package/src/CheckboxCard/CheckboxCard.tsx +13 -4
  74. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  75. package/src/CheckboxCard/types.ts +65 -17
  76. package/src/Collapsible/Collapsible.tsx +49 -3
  77. package/src/Collapsible/README.md +40 -0
  78. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  79. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  80. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  81. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  82. package/src/Collapsible/types.ts +89 -22
  83. package/src/ContextMenu/ContextMenu.tsx +270 -4
  84. package/src/ContextMenu/types.ts +148 -6
  85. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  86. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  87. package/src/DirectionProvider/types.ts +28 -3
  88. package/src/Divider/Divider.tsx +24 -10
  89. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  90. package/src/Divider/types.ts +10 -3
  91. package/src/Drawer/Drawer.tsx +315 -0
  92. package/src/Drawer/README.md +124 -0
  93. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  94. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  95. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  96. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  97. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  98. package/src/Drawer/index.ts +2 -0
  99. package/src/Drawer/types.ts +66 -0
  100. package/src/Dropdown/Dropdown.tsx +117 -0
  101. package/src/Dropdown/types.ts +241 -11
  102. package/src/EmptyState/EmptyState.tsx +74 -4
  103. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  104. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  105. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  106. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  107. package/src/EmptyState/types.ts +49 -6
  108. package/src/Field/Field.tsx +23 -0
  109. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  110. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  111. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  112. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  113. package/src/Field/types.ts +60 -12
  114. package/src/Fieldset/Fieldset.tsx +54 -18
  115. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  116. package/src/Fieldset/types.ts +20 -2
  117. package/src/Input/Input.tsx +14 -9
  118. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  119. package/src/Input/types.ts +19 -2
  120. package/src/InputGroup/InputGroup.tsx +11 -3
  121. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  122. package/src/InputGroup/types.ts +26 -5
  123. package/src/MillerColumns/MillerColumns.tsx +86 -19
  124. package/src/MillerColumns/types.ts +83 -27
  125. package/src/Modal/Modal.tsx +31 -4
  126. package/src/Modal/README.md +19 -10
  127. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  128. package/src/Modal/hooks/useModalContent.ts +32 -0
  129. package/src/Modal/types.ts +110 -29
  130. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  131. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  132. package/src/NavigationMenu/README.md +355 -0
  133. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  134. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  135. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  136. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  137. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  138. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  139. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  140. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  141. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  142. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  143. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  144. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  145. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  146. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  147. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  148. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  149. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  150. package/src/NavigationMenu/constants.ts +5 -0
  151. package/src/NavigationMenu/hooks/index.ts +7 -0
  152. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  153. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  154. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  155. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  156. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  157. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  158. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  159. package/src/NavigationMenu/index.ts +3 -0
  160. package/src/NavigationMenu/types.ts +259 -0
  161. package/src/NavigationMenu/utils.ts +89 -0
  162. package/src/Popover/Popover.tsx +427 -0
  163. package/src/Popover/PopoverContext.ts +13 -0
  164. package/src/Popover/README.md +106 -0
  165. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  166. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  167. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  168. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  169. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  170. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  171. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  172. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  173. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  174. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  175. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  176. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  177. package/src/Popover/hooks/index.ts +3 -0
  178. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  179. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  180. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  181. package/src/Popover/index.ts +2 -0
  182. package/src/Popover/types.ts +197 -0
  183. package/src/Portal/Portal.tsx +50 -10
  184. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  185. package/src/Portal/types.ts +5 -0
  186. package/src/Progress/Progress.tsx +33 -1
  187. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  188. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  189. package/src/Progress/types.ts +20 -4
  190. package/src/Radio/Radio.tsx +9 -1
  191. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  192. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  193. package/src/Radio/types.ts +20 -0
  194. package/src/RadioCard/RadioCard.tsx +11 -0
  195. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  196. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  197. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  198. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  199. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  200. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  201. package/src/RadioCard/types.ts +95 -22
  202. package/src/RadioGroup/RadioGroup.tsx +11 -0
  203. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  204. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  205. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  206. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  207. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  208. package/src/RadioGroup/types.ts +73 -9
  209. package/src/SegmentedControl/README.md +154 -0
  210. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  211. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  212. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  213. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  214. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  215. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  216. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  217. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  218. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  219. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  220. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  221. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  222. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  223. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  224. package/src/SegmentedControl/hooks/index.ts +2 -0
  225. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  226. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
  227. package/src/SegmentedControl/index.ts +2 -0
  228. package/src/SegmentedControl/types.ts +138 -0
  229. package/src/Select/README.md +122 -157
  230. package/src/Select/Select.tsx +595 -68
  231. package/src/Select/SelectContext.ts +38 -0
  232. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  233. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  234. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  235. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  236. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  237. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  238. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  239. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  240. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  241. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  242. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  243. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  244. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  245. package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
  246. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  247. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  248. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  249. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  250. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  251. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  252. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  253. package/src/Select/constants.ts +8 -0
  254. package/src/Select/hooks/index.ts +2 -0
  255. package/src/Select/hooks/useSelectContent.ts +149 -0
  256. package/src/Select/hooks/useSelectRoot.ts +149 -0
  257. package/src/Select/types.ts +199 -31
  258. package/src/SkipNav/SkipNav.tsx +69 -29
  259. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  260. package/src/SkipNav/types.ts +17 -9
  261. package/src/Slider/Slider.tsx +33 -1
  262. package/src/Slider/SliderContext.ts +28 -1
  263. package/src/Slider/types.ts +157 -9
  264. package/src/Slot/Slot.tsx +94 -19
  265. package/src/Slot/__tests__/Slot.test.tsx +110 -0
  266. package/src/Slot/composeEventHandlers.ts +26 -2
  267. package/src/Slot/types.ts +16 -0
  268. package/src/Status/Status.tsx +36 -15
  269. package/src/Status/__tests__/Status.test.tsx +5 -0
  270. package/src/Status/types.ts +12 -1
  271. package/src/Switch/Switch.tsx +15 -2
  272. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  273. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  274. package/src/Switch/types.ts +49 -22
  275. package/src/Table/Table.tsx +44 -10
  276. package/src/Table/types.ts +66 -17
  277. package/src/Tabs/Tabs.tsx +8 -0
  278. package/src/Tabs/types.ts +29 -4
  279. package/src/Textarea/Textarea.tsx +9 -1
  280. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  281. package/src/Textarea/types.ts +20 -3
  282. package/src/Toggle/Toggle.tsx +51 -13
  283. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  284. package/src/Toggle/types.ts +54 -4
  285. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  286. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  287. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  288. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  289. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  290. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  291. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  292. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  293. package/src/ToggleGroup/types.ts +55 -2
  294. package/src/Tooltip/Tooltip.tsx +98 -20
  295. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  296. package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
  297. package/src/Tooltip/types.ts +89 -8
  298. package/src/Tree/Tree.tsx +187 -33
  299. package/src/Tree/types.ts +54 -12
  300. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  301. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  302. package/src/VisuallyHidden/types.ts +11 -1
  303. package/src/index.ts +4 -0
  304. package/src/test/intersectionObserverPolyfill.ts +7 -0
  305. package/src/test/resizeObserverPolyfill.ts +80 -0
  306. package/src/utils/focusable.ts +15 -0
  307. package/src/utils/index.ts +1 -0
@@ -13,7 +13,7 @@ describe("EmptyState.Title component", () => {
13
13
 
14
14
  it("should render the consumer element with asChild", () => {
15
15
  // Arrange
16
- render(
16
+ const { container } = render(
17
17
  <EmptyState.Title asChild>
18
18
  <h2>No results found</h2>
19
19
  </EmptyState.Title>,
@@ -22,5 +22,7 @@ describe("EmptyState.Title component", () => {
22
22
  // Assert
23
23
  const title = screen.getByRole("heading", { name: "No results found" });
24
24
  expect(title.tagName).toBe("H2");
25
+ // Slot renders the child in place — no default <p> wrapper around it.
26
+ expect(container.querySelector("p")).toBeNull();
25
27
  });
26
28
  });
@@ -2,21 +2,64 @@ import { ComponentProps } from "react";
2
2
 
3
3
  /** Mixin adding the `asChild` opt-in to a sub-component's props. */
4
4
  export type WithAsChild = {
5
- /** Render the consumer's own element instead of the default, via `Slot`. */
5
+ /**
6
+ * When `true`, renders the consumer's own element instead of the
7
+ * sub-component's default host element, merging all props (aria-*, data-*,
8
+ * event handlers) onto it via the `Slot` utility. The child must be a single
9
+ * React element. For `EmptyState.Root` and `EmptyState.Media`, the implicit
10
+ * `role="status"` / `aria-hidden="true"` are also forwarded through `Slot`.
11
+ * @default false
12
+ */
6
13
  asChild?: boolean;
7
14
  };
8
15
 
9
- /** Props for {@link EmptyState.Root} — all `<div>` props plus `asChild`. */
16
+ /**
17
+ * Props for {@link EmptyStateRoot | `EmptyState.Root`} — all native `<div>`
18
+ * attributes plus the `asChild` escape hatch.
19
+ *
20
+ * The root renders `role="status"` by default, creating a polite ARIA live
21
+ * region. Override with `role={undefined}` for a static empty state that has
22
+ * nothing to announce on appearance.
23
+ */
10
24
  export type EmptyStateRootProps = ComponentProps<"div"> & WithAsChild;
11
25
 
12
- /** Props for {@link EmptyState.Media} — all `<div>` props plus `asChild`. */
26
+ /**
27
+ * Props for {@link EmptyStateMedia | `EmptyState.Media`} — all native `<div>`
28
+ * attributes plus the `asChild` escape hatch.
29
+ *
30
+ * The media wrapper renders `aria-hidden="true"` by default, hiding decorative
31
+ * artwork from assistive technology. Pass `aria-hidden={false}` and provide an
32
+ * accessible name when the artwork is genuinely informative.
33
+ */
13
34
  export type EmptyStateMediaProps = ComponentProps<"div"> & WithAsChild;
14
35
 
15
- /** Props for {@link EmptyState.Title} — all `<p>` props plus `asChild`. */
36
+ /**
37
+ * Props for {@link EmptyStateTitle | `EmptyState.Title`} — all native `<p>`
38
+ * attributes plus the `asChild` escape hatch.
39
+ *
40
+ * Renders a `<p>` by default. Use `asChild` to substitute a heading element
41
+ * (`<h2>`, `<h3>`, etc.) when the empty state stands in for a titled section
42
+ * and needs to participate in the document's heading hierarchy.
43
+ */
16
44
  export type EmptyStateTitleProps = ComponentProps<"p"> & WithAsChild;
17
45
 
18
- /** Props for {@link EmptyState.Description} — all `<p>` props plus `asChild`. */
46
+ /**
47
+ * Props for {@link EmptyStateDescription | `EmptyState.Description`} — all
48
+ * native `<p>` attributes plus the `asChild` escape hatch.
49
+ *
50
+ * Renders a `<p>` for secondary supporting copy that explains the situation or
51
+ * suggests a next step. Actionable controls belong in
52
+ * {@link EmptyStateActionsProps | `EmptyState.Actions`} rather than here.
53
+ */
19
54
  export type EmptyStateDescriptionProps = ComponentProps<"p"> & WithAsChild;
20
55
 
21
- /** Props for {@link EmptyState.Actions} — all `<div>` props plus `asChild`. */
56
+ /**
57
+ * Props for {@link EmptyStateActions | `EmptyState.Actions`} — all native
58
+ * `<div>` attributes plus the `asChild` escape hatch.
59
+ *
60
+ * Renders a plain `<div>` with no ARIA role of its own, so buttons and links
61
+ * placed inside it retain their native semantics. The element is a child of the
62
+ * `role="status"` live region, so its text content is included in the
63
+ * announcement.
64
+ */
22
65
  export type EmptyStateActionsProps = ComponentProps<"div"> & WithAsChild;
@@ -36,6 +36,12 @@ import {
36
36
  * `<section>`. The `data-field-*` hooks and context provider stay
37
37
  * intact.
38
38
  *
39
+ * **Styling hooks.** `data-field=""` always; `data-field-invalid` /
40
+ * `data-field-disabled` / `data-field-required` present (empty string)
41
+ * only when the corresponding flag is truthy.
42
+ *
43
+ * @extends HTMLDivElement
44
+ *
39
45
  * @example
40
46
  * ```tsx
41
47
  * <Field.Root id="email" invalid={!!errors.email}>
@@ -85,6 +91,7 @@ export function FieldRoot({
85
91
  }
86
92
 
87
93
  /** @internal */
94
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
88
95
  FieldRoot.displayName = "FieldRoot";
89
96
 
90
97
  /**
@@ -95,6 +102,13 @@ FieldRoot.displayName = "FieldRoot";
95
102
  * element with the `htmlFor` attribute merged on.
96
103
  *
97
104
  * @throws If rendered outside a `<Field.Root>`.
105
+ *
106
+ * @extends HTMLLabelElement
107
+ *
108
+ * @example
109
+ * ```tsx
110
+ * <Field.Label>Email address</Field.Label>
111
+ * ```
98
112
  */
99
113
  export function FieldLabel({
100
114
  asChild = false,
@@ -123,6 +137,13 @@ FieldLabel.displayName = "FieldLabel";
123
137
  * `<span>`, or any other element with the `id` merged on.
124
138
  *
125
139
  * @throws If rendered outside a `<Field.Root>`.
140
+ *
141
+ * @extends HTMLDivElement
142
+ *
143
+ * @example
144
+ * ```tsx
145
+ * <Field.Description>We'll only use this to sign you in.</Field.Description>
146
+ * ```
126
147
  */
127
148
  export function FieldDescription({
128
149
  asChild = false,
@@ -150,6 +171,8 @@ FieldDescription.displayName = "FieldDescription";
150
171
  * **`asChild` composition.** Pass `asChild` to render the consumer's
151
172
  * element with the `id` and `role="alert"` merged on.
152
173
  *
174
+ * @extends HTMLDivElement
175
+ *
153
176
  * @example
154
177
  * ```tsx
155
178
  * <Field.Root invalid={!!errors.email}>
@@ -101,4 +101,15 @@ describe("Field basic rendering", () => {
101
101
  // Assert
102
102
  expect(screen.getByTestId("field")).toHaveClass("form-field");
103
103
  });
104
+
105
+ it("sets a displayName on the compound and each sub-component", () => {
106
+ // Assert — empty displayNames would render each as anonymous in DevTools.
107
+ // `Field`, `Field.Root`, and the underlying function are one object
108
+ // (Object.assign compound), so the compound's "Field" alias is the
109
+ // observable Root displayName; the sub-components are distinct objects.
110
+ expect(Field.displayName).toBe("Field");
111
+ expect(Field.Label.displayName).toBe("FieldLabel");
112
+ expect(Field.Description.displayName).toBe("FieldDescription");
113
+ expect(Field.ErrorText.displayName).toBe("FieldErrorText");
114
+ });
104
115
  });
@@ -0,0 +1,23 @@
1
+ import { render } from "@testing-library/react";
2
+
3
+ import { Field } from "../Field";
4
+ import { FieldContext } from "../FieldContext";
5
+
6
+ describe("Field error handling", () => {
7
+ it("throws a helpful error when a sub-component is rendered outside Root", () => {
8
+ // Arrange
9
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
10
+
11
+ // Assert
12
+ expect(() => render(<Field.Label>Email</Field.Label>)).toThrow(
13
+ /Field sub-components must be rendered inside a <Field\.Root>/,
14
+ );
15
+
16
+ error.mockRestore();
17
+ });
18
+
19
+ it("names the context for React DevTools", () => {
20
+ // Assert — an empty displayName would render the provider as anonymous.
21
+ expect(FieldContext.displayName).toBe("FieldContext");
22
+ });
23
+ });
@@ -72,4 +72,25 @@ describe("Field state cascade", () => {
72
72
  // Assert
73
73
  expect(screen.getByText("Required.")).toBeInTheDocument();
74
74
  });
75
+
76
+ it("re-derives the cascaded context when a flag changes after mount", () => {
77
+ // Arrange — a valid field: the error is gated off.
78
+ const { rerender } = render(
79
+ <Field.Root>
80
+ <Field.ErrorText>Required.</Field.ErrorText>
81
+ </Field.Root>,
82
+ );
83
+ expect(screen.queryByText("Required.")).not.toBeInTheDocument();
84
+
85
+ // Act — flip to invalid; the memoised context value must recompute.
86
+ rerender(
87
+ <Field.Root invalid>
88
+ <Field.ErrorText>Required.</Field.ErrorText>
89
+ </Field.Root>,
90
+ );
91
+
92
+ // Assert — a frozen memo (empty dependency array) would leave the error
93
+ // gated off; the recomputed context reveals it.
94
+ expect(screen.getByText("Required.")).toBeInTheDocument();
95
+ });
75
96
  });
@@ -5,20 +5,26 @@ import { useFieldProps } from "../useFieldProps";
5
5
 
6
6
  /**
7
7
  * `useFieldProps` is a reusable hook that reads {@link FieldContext} directly,
8
- * so it is exercised here against a hand-built provider value rather than the
9
- * full `Field.Root`. `Field.Root` always supplies a truthy `descriptionId`,
10
- * which means its defensive "nothing to describe" fallback is only reachable
11
- * through a custom provider like the one below.
8
+ * so it is exercised here against a hand-built provider value (and, for the
9
+ * no-context path, no provider at all) rather than the full `Field.Root` —
10
+ * always through a real component that consumes it. `Field.Root` always
11
+ * supplies a truthy `descriptionId`, so its defensive "nothing to describe"
12
+ * fallback is only reachable through a custom provider like the one below.
12
13
  */
13
- function Probe(props: { "aria-describedby"?: string }) {
14
+ type ProbeProps = {
15
+ id?: string;
16
+ disabled?: boolean;
17
+ required?: boolean;
18
+ "aria-invalid"?: boolean | "true" | "false";
19
+ "aria-describedby"?: string;
20
+ };
21
+
22
+ function Probe(props: ProbeProps) {
14
23
  const merged = useFieldProps(props);
15
24
  return <input data-testid="probe" {...merged} />;
16
25
  }
17
26
 
18
- function renderInField(
19
- field: Partial<FieldContextValue>,
20
- props: { "aria-describedby"?: string } = {},
21
- ) {
27
+ function renderInField(field: Partial<FieldContextValue>, props: ProbeProps = {}) {
22
28
  const value: FieldContextValue = {
23
29
  id: "f",
24
30
  descriptionId: "",
@@ -35,6 +41,80 @@ function renderInField(
35
41
  );
36
42
  }
37
43
 
44
+ describe("useFieldProps without a FieldContext", () => {
45
+ it("returns the consumer props unchanged when no provider is in scope", () => {
46
+ // Arrange & Act — no <FieldContext.Provider>: the early guard must fire.
47
+ render(<Probe id="consumer-id" aria-invalid="true" />);
48
+ const probe = screen.getByTestId("probe");
49
+
50
+ // Assert — consumer props survive untouched and no field wiring is added.
51
+ expect(probe).toHaveAttribute("id", "consumer-id");
52
+ expect(probe).toHaveAttribute("aria-invalid", "true");
53
+ expect(probe).not.toHaveAttribute("aria-describedby");
54
+ });
55
+ });
56
+
57
+ describe("useFieldProps inheriting from the field", () => {
58
+ it("adopts the field's id, disabled, required, and invalid state when the consumer omits them", () => {
59
+ // Arrange & Act
60
+ renderInField(
61
+ {
62
+ id: "field-id",
63
+ descriptionId: "field-desc",
64
+ errorId: "field-err",
65
+ invalid: true,
66
+ disabled: true,
67
+ required: true,
68
+ },
69
+ {},
70
+ );
71
+ const probe = screen.getByTestId("probe");
72
+
73
+ // Assert — every value falls back to the field.
74
+ expect(probe).toHaveAttribute("id", "field-id");
75
+ expect(probe).toBeDisabled();
76
+ expect(probe).toBeRequired();
77
+ expect(probe).toHaveAttribute("aria-invalid", "true");
78
+ expect(probe).toHaveAttribute("aria-describedby", "field-desc field-err");
79
+ });
80
+
81
+ it("omits aria-invalid entirely when the field is valid and the consumer is silent", () => {
82
+ // Arrange & Act
83
+ renderInField({ invalid: false }, {});
84
+
85
+ // Assert — a valid field contributes no aria-invalid, so the attribute is
86
+ // absent rather than rendered as aria-invalid="false".
87
+ expect(screen.getByTestId("probe")).not.toHaveAttribute("aria-invalid");
88
+ });
89
+ });
90
+
91
+ describe("useFieldProps with consumer overrides", () => {
92
+ it("lets consumer id, disabled, required, and aria-invalid win over the field", () => {
93
+ // Arrange & Act — field would contribute the opposite of each override.
94
+ renderInField(
95
+ {
96
+ id: "field-id",
97
+ invalid: false,
98
+ disabled: false,
99
+ required: false,
100
+ },
101
+ {
102
+ id: "consumer-id",
103
+ disabled: true,
104
+ required: true,
105
+ "aria-invalid": "true",
106
+ },
107
+ );
108
+ const probe = screen.getByTestId("probe");
109
+
110
+ // Assert — the consumer's explicit values are preserved.
111
+ expect(probe).toHaveAttribute("id", "consumer-id");
112
+ expect(probe).toBeDisabled();
113
+ expect(probe).toBeRequired();
114
+ expect(probe).toHaveAttribute("aria-invalid", "true");
115
+ });
116
+ });
117
+
38
118
  describe("useFieldProps aria-describedby composition", () => {
39
119
  it("omits aria-describedby entirely when nothing contributes an id", () => {
40
120
  // Arrange & Act — empty description/error ids, not invalid, no consumer id.
@@ -44,4 +124,19 @@ describe("useFieldProps aria-describedby composition", () => {
44
124
  // rather than rendered as aria-describedby="".
45
125
  expect(screen.getByTestId("probe")).not.toHaveAttribute("aria-describedby");
46
126
  });
127
+
128
+ it("prepends the consumer's ids ahead of the field's, space-separated", () => {
129
+ // Arrange & Act — a valid field so the errorId does not join in.
130
+ renderInField(
131
+ { descriptionId: "field-desc", errorId: "field-err", invalid: false },
132
+ { "aria-describedby": "consumer-desc" },
133
+ );
134
+
135
+ // Assert — consumer id first, then the field's descriptionId; the errorId is
136
+ // excluded while valid.
137
+ expect(screen.getByTestId("probe")).toHaveAttribute(
138
+ "aria-describedby",
139
+ "consumer-desc field-desc",
140
+ );
141
+ });
47
142
  });
@@ -1,37 +1,85 @@
1
1
  import { ComponentProps, ReactNode } from "react";
2
2
 
3
- /** Props for `Field.Root`, the wrapper that owns shared field state. */
3
+ /**
4
+ * Props for {@link FieldRoot | `Field.Root`} — the wrapper that owns the
5
+ * shared field id, derived `descriptionId` / `errorId`, and the
6
+ * `invalid` / `disabled` / `required` cascade. Extends the native
7
+ * `<div>` props; `id` is redeclared purely to document its role as the
8
+ * field-wide id (its type is unchanged from the native attribute).
9
+ */
4
10
  export type FieldRootProps = ComponentProps<"div"> & {
5
- /** Stable id wired to the control via {@link FieldLabel}'s `htmlFor`. Auto-generated via `useId` when omitted. */
11
+ /** Stable id for the field. Wired to the control via
12
+ * {@link FieldLabel | `Field.Label`}'s `htmlFor`, and used to derive
13
+ * the `descriptionId` (`<id>-description`) and `errorId`
14
+ * (`<id>-error`) exposed through {@link FieldContext}. Auto-generated
15
+ * via React's `useId` when omitted. */
6
16
  id?: string;
7
- /** Marks the field as invalid; cascades to the control via context. */
17
+ /** Marks the field invalid. Cascades to a context-aware control (e.g.
18
+ * `Input`) as `aria-invalid`, sets `data-field-invalid` on the
19
+ * wrapper, and gates {@link FieldErrorText | `Field.ErrorText`}
20
+ * rendering.
21
+ * @default false */
8
22
  invalid?: boolean;
9
- /** Disables the field; cascades to the control via context. */
23
+ /** Disables the field. Cascades to the control's `disabled` prop and
24
+ * sets `data-field-disabled` on the wrapper.
25
+ * @default false */
10
26
  disabled?: boolean;
11
- /** Marks the field as required; cascades to the control via context. */
27
+ /** Marks the field required. Cascades to the control's `required` prop
28
+ * and sets `data-field-required` on the wrapper.
29
+ * @default false */
12
30
  required?: boolean;
13
- /** Renders the consumer element instead of `<div>` via Slot. */
31
+ /** Render the consumer's element instead of `<div>` via the
32
+ * {@link Slot} pattern — e.g. a semantic `<fieldset>`. The
33
+ * `data-field-*` hooks and context provider are preserved.
34
+ * @default false */
14
35
  asChild?: boolean;
36
+ /** The label, control, description, and error sub-components composed
37
+ * inside the field. */
15
38
  children?: ReactNode;
16
39
  };
17
40
 
18
- /** Props for `Field.Label`, the field's `<label>`. */
41
+ /**
42
+ * Props for {@link FieldLabel | `Field.Label`} — the field's `<label>`,
43
+ * wired to the field id via `htmlFor`. Extends the native `<label>`
44
+ * props.
45
+ */
19
46
  export type FieldLabelProps = ComponentProps<"label"> & {
20
- /** Renders the consumer element instead of `<label>` via Slot. */
47
+ /** Render the consumer's element instead of `<label>` via the
48
+ * {@link Slot} pattern, with `htmlFor` merged on.
49
+ * @default false */
21
50
  asChild?: boolean;
51
+ /** The label text. */
22
52
  children?: ReactNode;
23
53
  };
24
54
 
25
- /** Props for `Field.Description`, the field's supporting helper text. */
55
+ /**
56
+ * Props for {@link FieldDescription | `Field.Description`} — the field's
57
+ * supporting helper text, carrying the field's `descriptionId` so a
58
+ * context-aware control can reference it via `aria-describedby`. Extends
59
+ * the native `<div>` props.
60
+ */
26
61
  export type FieldDescriptionProps = ComponentProps<"div"> & {
27
- /** Renders the consumer element instead of `<div>` via Slot. */
62
+ /** Render the consumer's element instead of `<div>` via the
63
+ * {@link Slot} pattern (e.g. a `<p>` or `<span>`), with the derived
64
+ * `id` merged on.
65
+ * @default false */
28
66
  asChild?: boolean;
67
+ /** The helper text. */
29
68
  children?: ReactNode;
30
69
  };
31
70
 
32
- /** Props for `Field.ErrorText`, the error message shown when invalid. */
71
+ /**
72
+ * Props for {@link FieldErrorText | `Field.ErrorText`} — the error
73
+ * message shown only when the field is
74
+ * {@link FieldRootProps.invalid | invalid}. Renders `role="alert"` and
75
+ * carries the field's `errorId`. Extends the native `<div>` props.
76
+ */
33
77
  export type FieldErrorTextProps = ComponentProps<"div"> & {
34
- /** Renders the consumer element instead of `<div>` via Slot. */
78
+ /** Render the consumer's element instead of `<div>` via the
79
+ * {@link Slot} pattern, with the derived `id` and `role="alert"`
80
+ * merged on.
81
+ * @default false */
35
82
  asChild?: boolean;
83
+ /** The error message. */
36
84
  children?: ReactNode;
37
85
  };
@@ -3,24 +3,37 @@ import type { ReactElement } from "react";
3
3
  import { FieldsetLegendProps, FieldsetProps } from "./types";
4
4
 
5
5
  /**
6
- * The root of a Fieldset — renders a native `<fieldset>` element.
6
+ * The root of a Fieldset — renders a native `<fieldset>` element with an
7
+ * implicit `role="group"`.
7
8
  *
8
- * A `<fieldset>` carries an implicit `role="group"` and groups a set of
9
- * related form controls (radios, checkboxes, inputs). Pair it with a
10
- * {@link FieldsetLegend} so the group is announced with an accessible
11
- * name when the user enters any control inside it.
9
+ * Groups a set of related form controls (radios, checkboxes, inputs). Pair
10
+ * it with a {@link FieldsetLegend} as the first child so the group is
11
+ * announced with an accessible name when the user moves focus into any
12
+ * control inside it.
12
13
  *
13
- * **Disabled.** Setting `disabled` forwards the native attribute, which
14
- * disables *every* form control nested inside the fieldset at once — the
15
- * standard way to disable a whole section of a form. It also sets
16
- * `data-disabled=""` so CSS can target the state without the `:disabled`
17
- * pseudo-class:
14
+ * **Styling hooks.** `data-disabled=""` is set when the `disabled` prop is
15
+ * truthy (omitted otherwise), so CSS can target the state without relying
16
+ * on the `:disabled` pseudo-class:
18
17
  *
19
18
  * ```css
20
- * fieldset[data-disabled] { opacity: 0.5; }
19
+ * fieldset[data-disabled] { opacity: 0.5; cursor: not-allowed; }
21
20
  * ```
22
21
  *
23
- * @example
22
+ * **Disabled.** Forwarding the native `disabled` attribute disables *every*
23
+ * form control nested inside the fieldset at once — the standard way to
24
+ * disable a whole section of a form.
25
+ *
26
+ * **Ref forwarding.** Pass a `ref` prop to access the underlying
27
+ * `HTMLFieldSetElement` directly:
28
+ *
29
+ * ```tsx
30
+ * const ref = useRef<HTMLFieldSetElement>(null);
31
+ * <Fieldset.Root ref={ref}>…</Fieldset.Root>
32
+ * ```
33
+ *
34
+ * @extends HTMLFieldSetElement
35
+ *
36
+ * @example Basic grouping
24
37
  * ```tsx
25
38
  * <Fieldset.Root>
26
39
  * <Fieldset.Legend>Notifications</Fieldset.Legend>
@@ -28,6 +41,14 @@ import { FieldsetLegendProps, FieldsetProps } from "./types";
28
41
  * <label><input type="checkbox" name="sms" /> SMS</label>
29
42
  * </Fieldset.Root>
30
43
  * ```
44
+ *
45
+ * @example Disabled section
46
+ * ```tsx
47
+ * <Fieldset.Root disabled>
48
+ * <Fieldset.Legend>Billing</Fieldset.Legend>
49
+ * …every nested control is disabled…
50
+ * </Fieldset.Root>
51
+ * ```
31
52
  */
32
53
  export function FieldsetRoot({
33
54
  disabled,
@@ -46,14 +67,26 @@ export function FieldsetRoot({
46
67
  }
47
68
 
48
69
  /** @internal */
70
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
49
71
  FieldsetRoot.displayName = "Fieldset";
50
72
 
51
73
  /**
52
- * The caption for a {@link Fieldset} — renders a native `<legend>`.
74
+ * The caption for a {@link FieldsetRoot | `Fieldset.Root`} — renders a
75
+ * native `<legend>`.
76
+ *
77
+ * Must be the **first child** of the `<fieldset>`. It supplies the group's
78
+ * accessible name; assistive technology announces it when the user moves
79
+ * focus into any control within the group.
53
80
  *
54
- * The `<legend>` must be the first child of the `<fieldset>`. It supplies
55
- * the group's accessible name, so assistive technology announces it when
56
- * the user moves focus into any control within the group.
81
+ * **Ref forwarding.** Pass a `ref` prop to access the underlying
82
+ * `HTMLLegendElement` directly:
83
+ *
84
+ * ```tsx
85
+ * const ref = useRef<HTMLLegendElement>(null);
86
+ * <Fieldset.Legend ref={ref}>Address</Fieldset.Legend>
87
+ * ```
88
+ *
89
+ * @extends HTMLLegendElement
57
90
  *
58
91
  * @example
59
92
  * ```tsx
@@ -83,11 +116,11 @@ export type FieldsetCompound = typeof FieldsetRoot & {
83
116
  * Headless, accessible **Fieldset** — a stateless compound component that
84
117
  * groups related form controls, with zero styles.
85
118
  *
86
- * `Fieldset` is both callable (an alias of {@link Fieldset | `Fieldset.Root`})
119
+ * `Fieldset` is both callable (an alias of {@link FieldsetRoot | `Fieldset.Root`})
87
120
  * and carries its sub-component as a static property. Prefer the
88
121
  * namespaced form in application code:
89
122
  *
90
- * - {@link Fieldset | `Fieldset.Root`} — `<fieldset>`, implicit `role="group"`.
123
+ * - {@link FieldsetRoot | `Fieldset.Root`} — `<fieldset>`, implicit `role="group"`.
91
124
  * - {@link FieldsetLegend | `Fieldset.Legend`} — `<legend>`, the group's accessible name.
92
125
  *
93
126
  * @example Grouping a set of radios
@@ -108,6 +141,9 @@ export type FieldsetCompound = typeof FieldsetRoot & {
108
141
  * …every nested control is disabled…
109
142
  * </Fieldset.Root>
110
143
  * ```
144
+ *
145
+ * @see {@link FieldsetRoot} for the `disabled` prop and `data-disabled` styling hook.
146
+ * @see {@link FieldsetLegend} for accessible name requirements.
111
147
  */
112
148
  const FieldsetCompound: FieldsetCompound = Object.assign(FieldsetRoot, {
113
149
  Root: FieldsetRoot,
@@ -78,4 +78,13 @@ describe("Fieldset basic rendering", () => {
78
78
  // Assert
79
79
  expect(screen.getByText("Billing")).toHaveClass("group__legend");
80
80
  });
81
+
82
+ it("sets a displayName on the compound and Legend", () => {
83
+ // Assert — empty displayNames would render each as anonymous in DevTools.
84
+ // `Fieldset`, `Fieldset.Root`, and the underlying function are one object
85
+ // (Object.assign compound), so the compound's "Fieldset" alias is the
86
+ // observable Root displayName; Legend is a distinct object.
87
+ expect(Fieldset.displayName).toBe("Fieldset");
88
+ expect(Fieldset.Legend.displayName).toBe("FieldsetLegend");
89
+ });
81
90
  });
@@ -1,7 +1,25 @@
1
1
  import { ComponentProps } from "react";
2
2
 
3
- /** Props for `Fieldset.Root` — the native `<fieldset>` attributes. */
3
+ /**
4
+ * Props for {@link FieldsetRoot | `Fieldset.Root`} — all native `<fieldset>`
5
+ * attributes plus the `data-disabled` styling hook.
6
+ *
7
+ * No custom props narrow any native attribute, so the full `ComponentProps<"fieldset">`
8
+ * surface is passed through unchanged, including `ref` (forwarded to the
9
+ * underlying `HTMLFieldSetElement`).
10
+ *
11
+ * The one behaviour addition over a bare `<fieldset>` is `data-disabled=""`:
12
+ * when `disabled` is truthy the component sets this attribute alongside the
13
+ * native `disabled`, so CSS can target `[data-disabled]` without relying on
14
+ * the `:disabled` pseudo-class.
15
+ */
4
16
  export type FieldsetProps = ComponentProps<"fieldset">;
5
17
 
6
- /** Props for `Fieldset.Legend` — the native `<legend>` attributes. */
18
+ /**
19
+ * Props for {@link FieldsetLegend | `Fieldset.Legend`} — all native `<legend>`
20
+ * attributes, including `ref` (forwarded to the underlying `HTMLLegendElement`).
21
+ *
22
+ * `<legend>` has no interactive state, so no additional `data-*` attributes
23
+ * are emitted beyond what is passed explicitly by the consumer.
24
+ */
7
25
  export type FieldsetLegendProps = ComponentProps<"legend">;
@@ -5,12 +5,9 @@ import { Slot } from "../Slot/index.ts";
5
5
  import { InputProps } from "./types";
6
6
 
7
7
  /**
8
- * A headless, accessible single-line text input.
9
- *
10
- * Renders a native `<input>` and passes every standard input attribute
11
- * straight through to the DOM — `value` / `defaultValue`, `placeholder`,
12
- * `type`, `name`, `required`, `pattern`, `min`, `max`, `step`,
13
- * `maxLength`, and so on. No styles ship with the component.
8
+ * A headless, accessible single-line text input. Renders a native
9
+ * `<input>` element and passes every standard attribute straight through
10
+ * to the DOM. No styles ship with the component.
14
11
  *
15
12
  * **Default type.** `type="text"` is set by default. Override with the
16
13
  * `type` prop for any native variant (`"email"`, `"password"`,
@@ -45,6 +42,11 @@ import { InputProps } from "./types";
45
42
  * ids first, then field-supplied description / error ids). Outside a
46
43
  * `<Field.Root>`, behaviour is unchanged.
47
44
  *
45
+ * **Styling hooks.**
46
+ * - `data-disabled=""` — present when `disabled` is `true`; absent
47
+ * otherwise. Lets CSS target `[data-disabled]` without depending on
48
+ * the `:disabled` pseudo-class.
49
+ *
48
50
  * **Ref forwarding.** Pass a `ref` prop to access the underlying
49
51
  * `HTMLInputElement`:
50
52
  *
@@ -62,14 +64,17 @@ import { InputProps } from "./types";
62
64
  * ```
63
65
  *
64
66
  * **`asChild` composition.** Renders the consumer's element instead of
65
- * `<input>`, merging all props (aria-*, data-*, event handlers, ref)
66
- * via the {@link Slot} utility. `type` is **not** forwarded in this
67
- * mode — the child element owns its own type semantics.
67
+ * `<input>`, merging all props (`aria-*`, `data-*`, event handlers,
68
+ * `ref`) via the {@link Slot} utility. Event handlers compose with the
69
+ * child's own handlers (child runs first). `type` is **not** forwarded
70
+ * in this mode — the child element owns its own type semantics.
68
71
  *
69
72
  * **Adornments** live in the separate `InputGroup` primitive — leading
70
73
  * / trailing icons, currency symbols, clear buttons, password-reveal
71
74
  * toggles.
72
75
  *
76
+ * @extends HTMLInputElement
77
+ *
73
78
  * @example Basic usage
74
79
  * ```tsx
75
80
  * <label htmlFor="email">Email</label>