@primitiv-ui/react 0.1.28 → 0.1.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 (456) hide show
  1. package/README.md +19 -2
  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 +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -0,0 +1,315 @@
1
+ import type { ReactElement } from "react";
2
+
3
+ import { Modal } from "../Modal/index.ts";
4
+ import type {
5
+ DrawerCloseProps,
6
+ DrawerContentProps,
7
+ DrawerDescriptionProps,
8
+ DrawerOverlayProps,
9
+ DrawerPortalProps,
10
+ DrawerRootProps,
11
+ DrawerTitleProps,
12
+ DrawerTriggerProps,
13
+ } from "./types";
14
+
15
+ /**
16
+ * The root of a Drawer — owns open state, registers the dialog's id,
17
+ * title id, and description id, and exposes an imperative handle for
18
+ * opening / closing from outside the React subtree.
19
+ *
20
+ * A Drawer *is* a {@link Modal | Modal} that slides in from a screen edge, so
21
+ * `Drawer.Root` delegates its entire behaviour to `Modal.Root`: the two
22
+ * state modes (uncontrolled `defaultOpen` vs. controlled `open` +
23
+ * `onOpenChange`, statically discriminated at the type level) and the
24
+ * imperative open/close API are identical.
25
+ *
26
+ * @example Uncontrolled
27
+ * ```tsx
28
+ * <Drawer.Root defaultOpen>
29
+ * <Drawer.Content>...</Drawer.Content>
30
+ * </Drawer.Root>
31
+ * ```
32
+ *
33
+ * @example Controlled
34
+ * ```tsx
35
+ * const [open, setOpen] = useState(false);
36
+ *
37
+ * <Drawer.Root open={open} onOpenChange={setOpen}>
38
+ * <Drawer.Trigger>Open</Drawer.Trigger>
39
+ * <Drawer.Portal>
40
+ * <Drawer.Overlay />
41
+ * <Drawer.Content side="right">...</Drawer.Content>
42
+ * </Drawer.Portal>
43
+ * </Drawer.Root>
44
+ * ```
45
+ */
46
+ function DrawerRoot(props: DrawerRootProps): ReactElement {
47
+ return <Modal.Root {...props} />;
48
+ }
49
+
50
+ DrawerRoot.displayName = "DrawerRoot";
51
+
52
+ /**
53
+ * A button that toggles the drawer open. Renders a
54
+ * `<button type="button">` with full ARIA wiring inherited from
55
+ * `Modal.Trigger`:
56
+ *
57
+ * - `aria-haspopup="dialog"`
58
+ * - `aria-expanded` tracks open state
59
+ * - `aria-controls` points at the `Drawer.Content` dialog's id
60
+ *
61
+ * **`asChild` prop.** Pass `asChild` to render any consumer-supplied
62
+ * element (e.g. a router `<Link>`) with the trigger's ARIA attributes,
63
+ * composed event handlers, and ref merged in.
64
+ *
65
+ * @extends HTMLButtonElement
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * <Drawer.Trigger>Open menu</Drawer.Trigger>
70
+ *
71
+ * <Drawer.Trigger asChild>
72
+ * <Link to="/cart">Cart</Link>
73
+ * </Drawer.Trigger>
74
+ * ```
75
+ */
76
+ function DrawerTrigger(props: DrawerTriggerProps): ReactElement {
77
+ return <Modal.Trigger {...props} />;
78
+ }
79
+
80
+ DrawerTrigger.displayName = "DrawerTrigger";
81
+
82
+ /**
83
+ * Renders its children through `React.createPortal` so the drawer is
84
+ * detached from wherever `Drawer.Root` lives in the React tree and becomes
85
+ * a direct child of `container` (default `document.body`).
86
+ *
87
+ * By default the portal only renders while the drawer is open. Pass
88
+ * `forceMount` to keep the subtree in the DOM after `open` flips false —
89
+ * useful for the CSS slide-out animation driven by `data-state="closed"`.
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * <Drawer.Portal container={document.getElementById("drawer-root")!}>
94
+ * <Drawer.Overlay />
95
+ * <Drawer.Content>...</Drawer.Content>
96
+ * </Drawer.Portal>
97
+ * ```
98
+ */
99
+ function DrawerPortal(props: DrawerPortalProps): ReactElement {
100
+ return <Modal.Portal {...props} />;
101
+ }
102
+
103
+ DrawerPortal.displayName = "DrawerPortal";
104
+
105
+ /**
106
+ * A decorative, animation-friendly backdrop rendered as a **sibling** of
107
+ * `Drawer.Content`. Like `Modal.Overlay` it is **not** the click-outside
108
+ * event surface — opening the native `<dialog>` via `showModal()` promotes
109
+ * it to the top layer with a browser-painted `::backdrop` above this
110
+ * overlay, so click-outside-to-close is wired on the dialog itself via
111
+ * `onPointerDownOutside`.
112
+ *
113
+ * - `aria-hidden="true"` (the backdrop is decorative).
114
+ * - `data-state="open" | "closed"` follows the drawer's open state.
115
+ *
116
+ * **`asChild` prop.** Pass `asChild` to render a consumer-supplied element
117
+ * (e.g. a motion wrapper) with the overlay's ARIA and data-state merged in.
118
+ *
119
+ * **`forceMount` prop.** Pass `forceMount` to keep the overlay in the DOM
120
+ * while `open` is false so a CSS fade-out can play.
121
+ *
122
+ * @extends HTMLDivElement
123
+ *
124
+ * @example
125
+ * ```tsx
126
+ * <Drawer.Overlay />
127
+ * ```
128
+ */
129
+ function DrawerOverlay(props: DrawerOverlayProps): ReactElement {
130
+ return <Modal.Overlay {...props} />;
131
+ }
132
+
133
+ DrawerOverlay.displayName = "DrawerOverlay";
134
+
135
+ /**
136
+ * The native `<dialog>` element — the sliding panel. Behaviour is
137
+ * `Modal.Content`'s: React drives `showModal()` / `close()` in response to
138
+ * open state, so the browser owns the inert background, the top layer, and
139
+ * the `Esc` key, while the same JS `Tab`-wrap focus trap (last ⇄ first)
140
+ * applies — inherited from `Modal.Content`. `aria-labelledby` /
141
+ * `aria-describedby` are wired automatically from `Drawer.Title` /
142
+ * `Drawer.Description`, and the `onEscapeKeyDown` / `onPointerDownOutside`
143
+ * escape hatches pass straight through.
144
+ *
145
+ * **The one drawer-specific addition —
146
+ * {@link DrawerContentProps.side | `side`}.** Which edge the panel slides in
147
+ * from (`"top" | "right" | "bottom" | "left"`, default `"right"`). It is
148
+ * emitted verbatim as `data-side` on the dialog for the styled layer to
149
+ * position and animate against; it changes no behaviour, focus, or ARIA.
150
+ *
151
+ * **`asChild` is intentionally not supported** (as with `Modal.Content`) —
152
+ * the native dialog is what provides the inert background and top layer (and
153
+ * hosts the inherited Tab-wrap focus trap).
154
+ *
155
+ * **Styling hooks.** `data-state="open" | "closed"` and
156
+ * `data-side="top|right|bottom|left"` on the dialog.
157
+ *
158
+ * @extends HTMLDialogElement
159
+ *
160
+ * @example Right-hand drawer (default)
161
+ * ```tsx
162
+ * <Drawer.Content>...</Drawer.Content>
163
+ * ```
164
+ *
165
+ * @example Bottom sheet
166
+ * ```tsx
167
+ * <Drawer.Content side="bottom">...</Drawer.Content>
168
+ * ```
169
+ */
170
+ function DrawerContent({
171
+ side = "right",
172
+ ...rest
173
+ }: DrawerContentProps): ReactElement {
174
+ // `side` is stripped from the props and re-emitted as `data-side` on the
175
+ // native <dialog> (Modal.Content forwards unrecognised props through its own
176
+ // `...rest`), giving the styled layer its positioning hook.
177
+ return <Modal.Content {...rest} data-side={side} />;
178
+ }
179
+
180
+ DrawerContent.displayName = "DrawerContent";
181
+
182
+ /**
183
+ * The drawer's accessible name. Renders an `<h2>` by default and
184
+ * auto-registers its generated id so `Drawer.Content` can wire it up as
185
+ * `aria-labelledby` (via `Modal.Title`).
186
+ *
187
+ * Pass `asChild` to render the consumer's own heading element; the id is
188
+ * still registered.
189
+ *
190
+ * @extends HTMLHeadingElement
191
+ *
192
+ * @example
193
+ * ```tsx
194
+ * <Drawer.Title>Filters</Drawer.Title>
195
+ * ```
196
+ */
197
+ function DrawerTitle(props: DrawerTitleProps): ReactElement {
198
+ return <Modal.Title {...props} />;
199
+ }
200
+
201
+ DrawerTitle.displayName = "DrawerTitle";
202
+
203
+ /**
204
+ * The drawer's accessible description. Renders a `<p>` by default and
205
+ * auto-registers its generated id so `Drawer.Content` can wire it up as
206
+ * `aria-describedby` (via `Modal.Description`).
207
+ *
208
+ * Pass `asChild` to render any consumer-supplied element; the id is still
209
+ * registered.
210
+ *
211
+ * @extends HTMLParagraphElement
212
+ *
213
+ * @example
214
+ * ```tsx
215
+ * <Drawer.Description>Narrow the results below.</Drawer.Description>
216
+ * ```
217
+ */
218
+ function DrawerDescription(props: DrawerDescriptionProps): ReactElement {
219
+ return <Modal.Description {...props} />;
220
+ }
221
+
222
+ DrawerDescription.displayName = "DrawerDescription";
223
+
224
+ /**
225
+ * A button that closes the drawer. Renders a `<button type="button">`
226
+ * whose `onClick` is composed with the close action (via `Modal.Close`) —
227
+ * consumer handlers run first and can `event.preventDefault()` to veto.
228
+ *
229
+ * **`asChild` prop.** Pass `asChild` to render any element (e.g. an
230
+ * icon-only button of your own) with the close behaviour merged in.
231
+ *
232
+ * @extends HTMLButtonElement
233
+ *
234
+ * @example
235
+ * ```tsx
236
+ * <Drawer.Close>Done</Drawer.Close>
237
+ *
238
+ * <Drawer.Close asChild>
239
+ * <IconButton aria-label="Close" icon={<XIcon />} />
240
+ * </Drawer.Close>
241
+ * ```
242
+ */
243
+ function DrawerClose(props: DrawerCloseProps): ReactElement {
244
+ return <Modal.Close {...props} />;
245
+ }
246
+
247
+ DrawerClose.displayName = "DrawerClose";
248
+
249
+ /** @internal */
250
+ type DrawerCompound = typeof DrawerRoot & {
251
+ Root: typeof DrawerRoot;
252
+ Trigger: typeof DrawerTrigger;
253
+ Portal: typeof DrawerPortal;
254
+ Overlay: typeof DrawerOverlay;
255
+ Content: typeof DrawerContent;
256
+ Title: typeof DrawerTitle;
257
+ Description: typeof DrawerDescription;
258
+ Close: typeof DrawerClose;
259
+ };
260
+
261
+ /**
262
+ * Headless, accessible **Drawer** — a Modal dialog that slides in from a
263
+ * screen edge. It reuses {@link Modal | Modal}'s native-`<dialog>` machinery
264
+ * wholesale (focus trap, inert background, top layer, `Esc`, click-outside,
265
+ * controlled / uncontrolled state, the imperative API), adding a single
266
+ * presentational axis: {@link DrawerContentProps.side | `side`} on
267
+ * `Drawer.Content`, emitted as `data-side` for the styled layer to slide the
268
+ * panel from `top` / `right` / `bottom` / `left`.
269
+ *
270
+ * `Drawer` is both callable (an alias of {@link DrawerRoot | `Drawer.Root`})
271
+ * and carries its sub-components as static properties.
272
+ *
273
+ * - {@link DrawerRoot | `Drawer.Root`} — state owner, context provider, imperative API holder.
274
+ * - {@link DrawerTrigger | `Drawer.Trigger`} — `<button>` that opens the drawer.
275
+ * - {@link DrawerPortal | `Drawer.Portal`} — `createPortal` wrapper with `container` + `forceMount`.
276
+ * - {@link DrawerOverlay | `Drawer.Overlay`} — click-outside backdrop sibling of the dialog.
277
+ * - {@link DrawerContent | `Drawer.Content`} — native `<dialog>` with the `side` axis and auto-ARIA.
278
+ * - {@link DrawerTitle | `Drawer.Title`} — accessible name; auto-wires `aria-labelledby`.
279
+ * - {@link DrawerDescription | `Drawer.Description`} — auto-wires `aria-describedby`.
280
+ * - {@link DrawerClose | `Drawer.Close`} — `<button>` that closes the drawer.
281
+ *
282
+ * @example Minimal usage
283
+ * ```tsx
284
+ * import { Drawer } from "@primitiv-ui/react";
285
+ *
286
+ * <Drawer.Root>
287
+ * <Drawer.Trigger>Open</Drawer.Trigger>
288
+ * <Drawer.Portal>
289
+ * <Drawer.Overlay />
290
+ * <Drawer.Content side="right">
291
+ * <Drawer.Title>Filters</Drawer.Title>
292
+ * <Drawer.Description>Narrow the results below.</Drawer.Description>
293
+ * <Drawer.Close>Done</Drawer.Close>
294
+ * </Drawer.Content>
295
+ * </Drawer.Portal>
296
+ * </Drawer.Root>;
297
+ * ```
298
+ *
299
+ * @see {@link Modal} for the shared dialog behaviour, state modes, and imperative API.
300
+ * @see {@link DrawerContent} for the `side` axis and the styling hooks.
301
+ */
302
+ const DrawerCompound: DrawerCompound = Object.assign(DrawerRoot, {
303
+ Root: DrawerRoot,
304
+ Trigger: DrawerTrigger,
305
+ Portal: DrawerPortal,
306
+ Overlay: DrawerOverlay,
307
+ Content: DrawerContent,
308
+ Title: DrawerTitle,
309
+ Description: DrawerDescription,
310
+ Close: DrawerClose,
311
+ });
312
+
313
+ DrawerCompound.displayName = "Drawer";
314
+
315
+ export { DrawerCompound as Drawer };
@@ -0,0 +1,124 @@
1
+ # Drawer
2
+
3
+ A headless, accessible **dialog that slides in from a screen edge** — a
4
+ settings panel, a filter sheet, a navigation drawer, a cart. Behaviourally it
5
+ _is_ [Modal](../Modal/README.md): it reuses the same native
6
+ [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) +
7
+ `showModal()` machinery, so the inert background, top-layer stacking, and
8
+ `Esc`-to-close are handled by the browser, and the same JS `Tab`-wrap focus
9
+ trap (last ⇄ first) applies. The single addition is a **`side`** axis on
10
+ `Drawer.Content` that says which edge the panel enters from.
11
+
12
+ ```tsx
13
+ import { Drawer } from "@primitiv-ui/react";
14
+
15
+ <Drawer.Root>
16
+ <Drawer.Trigger>Filters</Drawer.Trigger>
17
+ <Drawer.Portal>
18
+ <Drawer.Overlay />
19
+ <Drawer.Content side="right">
20
+ <Drawer.Title>Filters</Drawer.Title>
21
+ <Drawer.Description>Narrow the results below.</Drawer.Description>
22
+ {/* body */}
23
+ <Drawer.Close>Done</Drawer.Close>
24
+ </Drawer.Content>
25
+ </Drawer.Portal>
26
+ </Drawer.Root>;
27
+ ```
28
+
29
+ ## Relationship to Modal
30
+
31
+ Drawer is a **thin composition over Modal** — every sub-component renders its
32
+ `Modal.*` counterpart and inherits its behaviour and props verbatim. There is no
33
+ separate state machine, context, or focus logic to learn or maintain: a fix to
34
+ Modal is a fix to Drawer. The _only_ behavioural surface that differs is
35
+ `Drawer.Content`'s `side` prop. If you need a centred dialog rather than an
36
+ edge-anchored one, use [Modal](../Modal/README.md) directly.
37
+
38
+ The visual difference — the panel spanning an edge and sliding in — is a
39
+ **styling concern**, driven by the `data-side` hook below, not by this headless
40
+ layer. Panel size is likewise a styling concern (a `size` variant on the styled
41
+ distribution), not a headless prop.
42
+
43
+ ## Sub-components
44
+
45
+ | Export | Element / role | Notes |
46
+ | --------------------- | -------------------- | ------------------------------------------------------------------------------ |
47
+ | `Drawer.Root` | State owner | Uncontrolled (`defaultOpen`) or controlled (`open` + `onOpenChange`); exposes the imperative API via `ref` |
48
+ | `Drawer.Trigger` | `<button>` | `aria-haspopup="dialog"` / `aria-expanded` / `aria-controls`; `asChild` |
49
+ | `Drawer.Portal` | `createPortal` | `container?` (default `document.body`); `forceMount` |
50
+ | `Drawer.Overlay` | `<div>` (sibling) | Decorative backdrop — **not** the click-outside surface (see Modal); `asChild`, `forceMount` |
51
+ | `Drawer.Content` | `<dialog>` | The sliding panel; adds the `side` axis; escape hatches + auto-ARIA. `asChild` **not** supported |
52
+ | `Drawer.Title` | `<h2>` | Accessible name; auto-wires `aria-labelledby`; `asChild` |
53
+ | `Drawer.Description` | `<p>` | Accessible description; auto-wires `aria-describedby`; `asChild` |
54
+ | `Drawer.Close` | `<button>` | Composes `onClick` with close; `asChild` |
55
+
56
+ ## The `side` axis
57
+
58
+ `Drawer.Content` accepts **`side`** — `"top" | "right" | "bottom" | "left"`,
59
+ default `"right"`. It is emitted verbatim as **`data-side`** on the `<dialog>`
60
+ and changes no behaviour, focus, or ARIA — it exists purely so the styling layer
61
+ can position and animate the panel against the right edge:
62
+
63
+ ```tsx
64
+ <Drawer.Content side="bottom">...</Drawer.Content>
65
+ // → <dialog data-side="bottom" data-state="open" ...>
66
+ ```
67
+
68
+ ```css
69
+ dialog[data-side="right"] {
70
+ inset: 0 0 0 auto;
71
+ block-size: 100dvh;
72
+ }
73
+ dialog[data-side="bottom"] {
74
+ inset: auto 0 0 0;
75
+ inline-size: 100vw;
76
+ }
77
+ ```
78
+
79
+ ## State, focus, keyboard, escape hatches
80
+
81
+ All identical to [Modal](../Modal/README.md) — see its README for the full
82
+ detail:
83
+
84
+ - **State modes** — uncontrolled `defaultOpen`, or controlled `open` +
85
+ `onOpenChange` (statically discriminated; mixing them is a type error).
86
+ - **Imperative API** — `ref.current?.open()` / `.close()` on `Drawer.Root`
87
+ (`DrawerImperativeApi`). In controlled mode these delegate to `onOpenChange`.
88
+ - **Keyboard** — `Esc` closes (native `cancel`); `Tab` / `Shift+Tab` wrap
89
+ focus at the dialog's boundaries (the JS trap inherited from `Modal.Content`,
90
+ since native `showModal()` doesn't cycle focus).
91
+ - **Escape hatches** — `onEscapeKeyDown` and `onPointerDownOutside` on
92
+ `Drawer.Content`; call `event.preventDefault()` to keep the drawer open.
93
+ - **Click-outside** — detected on the dialog itself (the `::backdrop` sits above
94
+ a sibling `Drawer.Overlay`), exactly as in Modal.
95
+
96
+ ## Animation hooks
97
+
98
+ `Drawer.Portal` and `Drawer.Overlay` accept `forceMount` to keep the subtree in
99
+ the DOM while `open` is false, so a CSS slide-out can play against
100
+ `data-state="closed"` before unmount. Combine `data-state` with `data-side` to
101
+ drive direction-aware transforms:
102
+
103
+ ```css
104
+ dialog[data-side="right"][data-state="closed"] {
105
+ translate: 100% 0;
106
+ }
107
+ dialog[data-side="right"][data-state="open"] {
108
+ translate: 0 0;
109
+ }
110
+ ```
111
+
112
+ ## Styling hooks
113
+
114
+ - `data-state="open" | "closed"` on `Drawer.Content` (and `Drawer.Overlay`).
115
+ - `data-side="top" | "right" | "bottom" | "left"` on `Drawer.Content`.
116
+ - `aria-expanded` on the trigger tracks the same open state.
117
+
118
+ ## `asChild` composition
119
+
120
+ Every rendered sub-component except `Drawer.Content` accepts `asChild` to project
121
+ its behaviour onto a consumer element via [Slot](../Slot.tsx), exactly as in
122
+ Modal. `Drawer.Content` is intentionally not slot-able — its native `<dialog>` is
123
+ what provides the inert background and top layer (and hosts the inherited
124
+ Tab-wrap focus trap).
@@ -0,0 +1,100 @@
1
+ import "./dialog-polyfill";
2
+
3
+ import { act, render, screen } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { createRef } from "react";
6
+
7
+ import { Drawer } from "../Drawer";
8
+ import type { DrawerImperativeApi } from "../types";
9
+
10
+ describe("Drawer — behaviour (delegated to Modal)", () => {
11
+ it("opens on trigger click and closes on Close click", async () => {
12
+ // Arrange
13
+ const user = userEvent.setup();
14
+ render(
15
+ <Drawer.Root>
16
+ <Drawer.Trigger>Open</Drawer.Trigger>
17
+ <Drawer.Portal>
18
+ <Drawer.Content data-testid="drawer">
19
+ <Drawer.Close>Done</Drawer.Close>
20
+ </Drawer.Content>
21
+ </Drawer.Portal>
22
+ </Drawer.Root>,
23
+ );
24
+ // closed → the portal renders nothing
25
+ expect(screen.queryByTestId("drawer")).toBeNull();
26
+
27
+ // Act — open
28
+ await user.click(screen.getByRole("button", { name: "Open" }));
29
+ // Assert
30
+ expect(screen.getByTestId("drawer")).toHaveAttribute("open");
31
+
32
+ // Act — close
33
+ await user.click(screen.getByRole("button", { name: "Done" }));
34
+ // Assert
35
+ expect(screen.queryByTestId("drawer")).toBeNull();
36
+ });
37
+
38
+ it("defers to onOpenChange in controlled mode", async () => {
39
+ // Arrange
40
+ const user = userEvent.setup();
41
+ const onOpenChange = vi.fn();
42
+ render(
43
+ <Drawer.Root open={false} onOpenChange={onOpenChange}>
44
+ <Drawer.Trigger>Open</Drawer.Trigger>
45
+ </Drawer.Root>,
46
+ );
47
+
48
+ // Act
49
+ await user.click(screen.getByRole("button", { name: "Open" }));
50
+
51
+ // Assert
52
+ expect(onOpenChange).toHaveBeenCalledWith(true);
53
+ });
54
+
55
+ it("exposes an imperative open/close handle on Root", () => {
56
+ // Arrange
57
+ const ref = createRef<DrawerImperativeApi>();
58
+ render(
59
+ <Drawer.Root ref={ref}>
60
+ <Drawer.Content data-testid="drawer">body</Drawer.Content>
61
+ </Drawer.Root>,
62
+ );
63
+ expect(screen.getByTestId("drawer")).not.toHaveAttribute("open");
64
+
65
+ // Act — open
66
+ act(() => ref.current?.open());
67
+ // Assert
68
+ expect(screen.getByTestId("drawer")).toHaveAttribute("open");
69
+
70
+ // Act — close
71
+ act(() => ref.current?.close());
72
+ // Assert
73
+ expect(screen.getByTestId("drawer")).not.toHaveAttribute("open");
74
+ });
75
+
76
+ it("forwards a ref to the <dialog> and keeps forceMount mounted while closed", () => {
77
+ // Arrange
78
+ const ref = createRef<HTMLDialogElement>();
79
+
80
+ // Act
81
+ render(
82
+ <Drawer.Root>
83
+ <Drawer.Portal forceMount>
84
+ <Drawer.Overlay forceMount data-testid="overlay" />
85
+ <Drawer.Content ref={ref} data-testid="drawer">
86
+ body
87
+ </Drawer.Content>
88
+ </Drawer.Portal>
89
+ </Drawer.Root>,
90
+ );
91
+
92
+ // Assert — forceMount keeps the subtree in the DOM even though closed
93
+ expect(ref.current).toBe(screen.getByTestId("drawer"));
94
+ expect(ref.current?.tagName).toBe("DIALOG");
95
+ expect(screen.getByTestId("overlay")).toHaveAttribute(
96
+ "data-state",
97
+ "closed",
98
+ );
99
+ });
100
+ });
@@ -0,0 +1,55 @@
1
+ import "./dialog-polyfill";
2
+
3
+ import { render, screen } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+
6
+ import { Drawer } from "../Drawer";
7
+
8
+ // Drawer.Content delegates wholesale to Modal.Content, so it inherits the
9
+ // boundary Tab-wrap focus trap. These two tests are the conformance guard that
10
+ // the inheritance holds — the exhaustive branch coverage lives in
11
+ // Modal.focus-trap.test.tsx. The Trigger sits outside the dialog so a native
12
+ // wrap (without the trap) would land there, not on the asserted element.
13
+ describe("Drawer focus trap — inherited from Modal.Content", () => {
14
+ it("wraps focus from the last element back to the first on Tab", async () => {
15
+ // Arrange
16
+ const user = userEvent.setup();
17
+ render(
18
+ <Drawer.Root defaultOpen>
19
+ <Drawer.Trigger>Open</Drawer.Trigger>
20
+ <Drawer.Content>
21
+ <button type="button">First</button>
22
+ <button type="button">Second</button>
23
+ </Drawer.Content>
24
+ </Drawer.Root>,
25
+ );
26
+ screen.getByRole("button", { name: "Second" }).focus();
27
+
28
+ // Act
29
+ await user.tab();
30
+
31
+ // Assert
32
+ expect(screen.getByRole("button", { name: "First" })).toHaveFocus();
33
+ });
34
+
35
+ it("wraps focus from the first element to the last on Shift+Tab", async () => {
36
+ // Arrange
37
+ const user = userEvent.setup();
38
+ render(
39
+ <Drawer.Root defaultOpen>
40
+ <Drawer.Trigger>Open</Drawer.Trigger>
41
+ <Drawer.Content>
42
+ <button type="button">First</button>
43
+ <button type="button">Second</button>
44
+ </Drawer.Content>
45
+ </Drawer.Root>,
46
+ );
47
+ screen.getByRole("button", { name: "First" }).focus();
48
+
49
+ // Act
50
+ await user.tab({ shift: true });
51
+
52
+ // Assert
53
+ expect(screen.getByRole("button", { name: "Second" })).toHaveFocus();
54
+ });
55
+ });