@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
@@ -0,0 +1,86 @@
1
+ import { useCallback, useMemo } from "react";
2
+
3
+ import { useCollection, useControllableState } from "../../hooks/index.ts";
4
+
5
+ type UseSegmentedControlRootArgs = {
6
+ defaultValue?: string;
7
+ value?: string;
8
+ onValueChange?: (value: string) => void;
9
+ };
10
+
11
+ type ItemMeta = {
12
+ element: HTMLButtonElement;
13
+ disabled: boolean;
14
+ };
15
+
16
+ export function useSegmentedControlRoot({
17
+ defaultValue,
18
+ value: controlledValue,
19
+ onValueChange,
20
+ }: UseSegmentedControlRootArgs) {
21
+ const [value, setValue] = useControllableState<string>(
22
+ controlledValue,
23
+ defaultValue,
24
+ onValueChange,
25
+ );
26
+
27
+ const select = useCallback(
28
+ (next: string) => {
29
+ if (value === next) return;
30
+ setValue(next);
31
+ },
32
+ [value, setValue],
33
+ );
34
+
35
+ // Track registered item metadata in a ref (for focus handling) and their
36
+ // ordered values as state — required for the roving-tabindex home base.
37
+ // Disabled values are derived per render from itemsRef; any change that
38
+ // affects the set (mount, unmount, disabled toggle) re-runs the registrar
39
+ // effect, which updates the keys state and forces the recompute.
40
+ const {
41
+ register: registerItemBase,
42
+ itemsRef,
43
+ keys: itemValues,
44
+ } = useCollection<string, ItemMeta>();
45
+
46
+ const registerItem = useCallback(
47
+ (itemValue: string, element: HTMLButtonElement | null, disabled = false) => {
48
+ registerItemBase(itemValue, element ? { element, disabled } : null);
49
+ },
50
+ [registerItemBase],
51
+ );
52
+
53
+ const disabledValues = useMemo(
54
+ () =>
55
+ new Set(
56
+ Array.from(itemsRef.current.entries())
57
+ .filter(([, meta]) => meta.disabled)
58
+ .map(([v]) => v),
59
+ ),
60
+ // itemValues is a fresh array on every register call (new identity even
61
+ // when the keys are unchanged), so the memo re-runs whenever any item
62
+ // mounts, unmounts, or toggles disabled — exactly the trigger we want.
63
+ [itemValues, itemsRef],
64
+ );
65
+
66
+ const focusItem = useCallback(
67
+ (itemValue: string) => {
68
+ // `itemValue` is always sourced from `navigable` (the enabled subset of
69
+ // `itemValues`), and useCollection's register() writes `itemsRef` and
70
+ // the `itemValues` state together — so a value that appears here is
71
+ // guaranteed to already have a live entry in the map.
72
+ // Stryker disable next-line OptionalChaining: unreachable given that invariant.
73
+ itemsRef.current.get(itemValue)?.element.focus();
74
+ },
75
+ [itemsRef],
76
+ );
77
+
78
+ return {
79
+ value,
80
+ select,
81
+ registerItem,
82
+ itemValues,
83
+ disabledValues,
84
+ focusItem,
85
+ };
86
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./SegmentedControl";
2
+ export * from "./types";
@@ -0,0 +1,138 @@
1
+ import { ComponentProps, ReactNode, Ref } from "react";
2
+
3
+ /**
4
+ * Layout axis of the control. `"horizontal"` (default) binds the
5
+ * Arrow Left/Right pair; `"vertical"` binds Arrow Up/Down. Also
6
+ * reflected as `aria-orientation` and `data-orientation` on the root.
7
+ */
8
+ export type SegmentedControlOrientation = "horizontal" | "vertical";
9
+
10
+ /** Reading direction — swaps the horizontal arrow pair when `"rtl"`. */
11
+ export type SegmentedControlReadingDirection = "ltr" | "rtl";
12
+
13
+ /**
14
+ * Shared base for both {@link SegmentedControlRootProps} variants — the
15
+ * native `<div>` attributes (minus the component-owned `role` and the
16
+ * narrowed `dir`) plus the `asChild` escape hatch, orientation, reading
17
+ * direction, group-level `disabled`, and a typed `ref`.
18
+ */
19
+ export type SegmentedControlRootBaseProps = Omit<
20
+ ComponentProps<"div">,
21
+ "role" | "dir" | "defaultValue"
22
+ > & {
23
+ /** The control's segments — {@link SegmentedControlItemProps | `SegmentedControl.Item`}
24
+ * elements. */
25
+ children?: ReactNode;
26
+ /** Forwarded to the underlying `HTMLDivElement`. */
27
+ ref?: Ref<HTMLDivElement>;
28
+ /**
29
+ * When `true`, Root delegates rendering to a single consumer-supplied
30
+ * element via the {@link Slot} pattern instead of the default `<div>`;
31
+ * `role="radiogroup"`, `aria-orientation`, `data-orientation`,
32
+ * `data-disabled`, `dir`, and remaining props are merged onto it.
33
+ * @default false
34
+ */
35
+ asChild?: boolean;
36
+ /**
37
+ * Which arrow-key pair moves focus and selection through the segments,
38
+ * and the visual axis. `"horizontal"` binds Arrow Left/Right;
39
+ * `"vertical"` binds Arrow Up/Down. Reflected as `aria-orientation` and
40
+ * `data-orientation` on the root.
41
+ * @default "horizontal"
42
+ */
43
+ orientation?: SegmentedControlOrientation;
44
+ /**
45
+ * Reading direction. In `"rtl"` the horizontal arrow pair is swapped so
46
+ * Arrow Left moves forward. When omitted, it is inherited from the
47
+ * nearest {@link DirectionProvider}, falling back to `"ltr"`.
48
+ */
49
+ dir?: SegmentedControlReadingDirection;
50
+ /**
51
+ * Disables the entire control — every segment is non-interactive and
52
+ * excluded from arrow-key navigation, and `data-disabled=""` is set on
53
+ * the root. Individual segments can also be disabled via the Item's own
54
+ * `disabled` prop.
55
+ * @default false
56
+ */
57
+ disabled?: boolean;
58
+ };
59
+
60
+ /**
61
+ * Uncontrolled variant of {@link SegmentedControlRootProps}: the component
62
+ * owns the selected value. Pass `defaultValue` (or omit it); `onValueChange`
63
+ * is optional and `value` is forbidden.
64
+ */
65
+ export type SegmentedControlRootUncontrolledProps =
66
+ SegmentedControlRootBaseProps & {
67
+ /** Value of the segment selected on first render. Omit for nothing
68
+ * selected on mount. */
69
+ defaultValue?: string;
70
+ /** Forbidden in uncontrolled mode — use `defaultValue` instead. */
71
+ value?: never;
72
+ /** Called with the new value whenever the selection changes. Optional
73
+ * in uncontrolled mode. */
74
+ onValueChange?: (value: string) => void;
75
+ };
76
+
77
+ /**
78
+ * Controlled variant of {@link SegmentedControlRootProps}: the parent owns
79
+ * the selected value. Pass `value` and `onValueChange` together;
80
+ * `defaultValue` is forbidden.
81
+ */
82
+ export type SegmentedControlRootControlledProps =
83
+ SegmentedControlRootBaseProps & {
84
+ /** Forbidden in controlled mode — use `value` instead. */
85
+ defaultValue?: never;
86
+ /** The currently selected value. Must be kept in sync by the parent
87
+ * via `onValueChange`. */
88
+ value: string;
89
+ /** Called with the new value whenever the selection changes. Required
90
+ * in controlled mode. */
91
+ onValueChange: (value: string) => void;
92
+ };
93
+
94
+ /**
95
+ * Props for {@link SegmentedControlRoot | `SegmentedControl.Root`}. A
96
+ * discriminated union of {@link SegmentedControlRootUncontrolledProps} and
97
+ * {@link SegmentedControlRootControlledProps}, so TypeScript accepts exactly
98
+ * one state mode.
99
+ */
100
+ export type SegmentedControlRootProps =
101
+ | SegmentedControlRootUncontrolledProps
102
+ | SegmentedControlRootControlledProps;
103
+
104
+ /**
105
+ * Props for {@link SegmentedControlItem | `SegmentedControl.Item`} — the
106
+ * segment button. `value` identifies the option; all native `<button>`
107
+ * attributes (minus the component-owned `type`, `role`, `aria-checked`, and
108
+ * `value`) plus the `asChild` escape hatch and a typed `ref` pass through.
109
+ */
110
+ export type SegmentedControlItemProps = Omit<
111
+ ComponentProps<"button">,
112
+ "type" | "role" | "aria-checked" | "value"
113
+ > & {
114
+ /** Uniquely identifies this segment within the control and is reported to
115
+ * `onValueChange` / matched against Root's `value` when selected. Required. */
116
+ value: string;
117
+ /** The segment's visible content — typically its text label. */
118
+ children?: ReactNode;
119
+ /** Forwarded to the underlying `HTMLButtonElement`. */
120
+ ref?: Ref<HTMLButtonElement>;
121
+ /**
122
+ * Disables just this segment: forwards the native `disabled` attribute,
123
+ * removes it from arrow-key navigation and the roving-tabindex home base,
124
+ * and sets `data-disabled=""`. The whole control can also be disabled via
125
+ * Root's `disabled` prop.
126
+ * @default false
127
+ */
128
+ disabled?: boolean;
129
+ /**
130
+ * When `true`, renders the single child element via the {@link Slot}
131
+ * pattern instead of the default `<button>`; ARIA, `data-state`,
132
+ * `data-disabled`, `tabIndex`, `onClick`, `onKeyDown`, `disabled`, and
133
+ * `ref` are merged onto it. A non-focusable child must be made focusable
134
+ * by the consumer.
135
+ * @default false
136
+ */
137
+ asChild?: boolean;
138
+ };
@@ -1,203 +1,168 @@
1
1
  # Select
2
2
 
3
- Headless **Select** — a compound component wrapping the native
4
- `<select>` / `<option>` / `<optgroup>` elements. Zero styles ship.
3
+ Headless **Select** — a single-select compound with two render paths chosen
4
+ by the `native` prop:
5
+
6
+ - **Rich** (`native={false}`, the default) — a fully-styleable Popover-API
7
+ listbox (`Select.Trigger` / `Select.Value` / `Select.Content` /
8
+ `Select.Item` / `Select.ItemIndicator`). Items carry arbitrary content
9
+ (icons, badges, indicators).
10
+ - **Native** (`native={true}`) — a thin wrapper over a real `<select>` /
11
+ `<option>` / `<optgroup>` for flat, OS-native cases (mobile wheel pickers,
12
+ maximum-compatibility forms).
13
+
14
+ Both modes share the same `value` / `onValueChange` / `disabled` / `name`
15
+ (form) API. Zero styles ship.
5
16
 
6
17
  ```tsx
7
18
  import { Select } from "@primitiv-ui/react";
8
19
 
9
- <Select.Root defaultValue="apple" aria-label="Pick a fruit">
10
- <Select.Option value="apple">Apple</Select.Option>
11
- <Select.Option value="banana">Banana</Select.Option>
12
- <Select.Option value="cherry">Cherry</Select.Option>
20
+ // Rich (default)
21
+ <Select.Root value={framework} onValueChange={setFramework}>
22
+ <Select.Trigger>
23
+ <Select.Value placeholder="Pick a framework…" />
24
+ </Select.Trigger>
25
+ <Select.Content>
26
+ <Select.Item value="react">
27
+ <ReactIcon />
28
+ React
29
+ <Select.ItemIndicator>✓</Select.ItemIndicator>
30
+ </Select.Item>
31
+ <Select.Item value="vue">
32
+ <VueIcon />
33
+ Vue
34
+ <Select.ItemIndicator>✓</Select.ItemIndicator>
35
+ </Select.Item>
36
+ </Select.Content>
13
37
  </Select.Root>;
14
38
  ```
15
39
 
16
- Because the underlying element is the real `<select>`, the browser owns
17
- the popup, the keyboard interaction (arrow keys, Home/End, typeahead),
18
- the mobile UX (iOS/Android wheel pickers), and form submission. No
19
- positioning JS, no Portal, no anchor positioning.
20
-
21
40
  ## Sub-components
22
41
 
23
- | Export | Element | ARIA / data hooks | `asChild` |
24
- | --------------------- | ------------ | -------------------------------------------------- | --------- |
25
- | `Select.Root` | `<select>` | implicit `role="combobox"`, `data-disabled` | yes |
26
- | `Select.Option` | `<option>` | implicit `role="option"` | — |
27
- | `Select.Group` | `<optgroup>` | implicit `role="group"`, `label` as accessible name | — |
28
- | `Select.Placeholder` | `<option>` | always `value=""`, `disabled`, `hidden` | — |
42
+ | Export | Rich element | Native element | Notes |
43
+ | ---------------------- | ------------------------ | -------------- | ---------------------------------------------- |
44
+ | `Select.Root` | context boundary + hidden `<select>` | `<select>` | owns value + open state |
45
+ | `Select.Trigger` | `<button>` | — | `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`; `asChild` |
46
+ | `Select.Value` | `<span>` | — | mirrors the selected item's content |
47
+ | `Select.Content` | `<div role="listbox">` (`popover="auto"`) | — | keyboard nav; `asChild` |
48
+ | `Select.Item` | `<div role="option">` | `<option>` | `value` (required), `disabled` |
49
+ | `Select.ItemIndicator` | `<span>` | — | selected-only; `forceMount`, `asChild` |
50
+ | `Select.Group` | `<div role="group">` | `<optgroup>` | `label` (required string) |
51
+ | `Select.Separator` | `<div role="separator">` | — | rich-only divider; skipped by keyboard nav; `asChild` |
52
+ | `Select.Placeholder` | — | `<option value="" hidden disabled>` | native-only initial hint |
29
53
 
30
- ## State modes
54
+ ## State
31
55
 
32
- ### Uncontrolled
56
+ **Selection** — controlled (`value` + `onValueChange`) or uncontrolled
57
+ (`defaultValue`, or omit for none), discriminated at the type level so only
58
+ one shape is accepted. `onValueChange` receives the new value as a string.
33
59
 
34
- Pass `defaultValue` (or omit it). The browser owns the selection.
35
- `onValueChange` is optional.
60
+ **Open** (rich only) — controlled (`open` + `onOpenChange`) or uncontrolled
61
+ (`defaultOpen`). The trigger toggles it; selection, Escape, and light-dismiss
62
+ close it.
36
63
 
37
- ```tsx
38
- <Select.Root defaultValue="banana" aria-label="Pick a fruit">
39
- <Select.Option value="apple">Apple</Select.Option>
40
- <Select.Option value="banana">Banana</Select.Option>
41
- </Select.Root>
42
- ```
64
+ ## Rich mode
43
65
 
44
- ### Controlled
66
+ ### Value mirroring
45
67
 
46
- Pass `value` and `onValueChange` together. The parent owns the
47
- selection; the component defers every transition back through the
48
- callback.
68
+ `Select.Value` **automatically mirrors the selected `Select.Item`'s
69
+ children** into the trigger — write the icon + label once, on the item, and
70
+ it shows up in the closed trigger. `Select.ItemIndicator` is **excluded** from
71
+ the mirror (the checkmark answers "which row is selected" — redundant on the
72
+ trigger it already represents). `placeholder` shows when nothing is selected.
49
73
 
50
- ```tsx
51
- const [fruit, setFruit] = useState("apple");
74
+ The exclusion works **through your own wrappers**: the mirror doesn't test the
75
+ element's type (a wrapper component would be opaque to that, and a styled layer
76
+ always wraps), it publishes its intent through context and each indicator opts
77
+ itself out — at any nesting depth, `forceMount` included. Every other child
78
+ carries through untouched, so a trailing badge on the selected item shows up in
79
+ the trigger too; style it there if it needs different spacing from the row.
52
80
 
53
- <Select.Root value={fruit} onValueChange={setFruit} aria-label="…">
54
- <Select.Option value="apple">Apple</Select.Option>
55
- <Select.Option value="banana">Banana</Select.Option>
56
- </Select.Root>;
57
- ```
81
+ ### Item indicator
58
82
 
59
- `onValueChange` receives the new selection as a plain string. The
60
- consumer's own `onChange` (the raw `ChangeEvent`) still fires alongside
61
- it if provided.
83
+ `Select.ItemIndicator`, nested in a `Select.Item`, renders **only when that
84
+ item is selected** and exposes `data-state="checked" | "unchecked"`. Pass
85
+ `forceMount` to keep it in the DOM (as `unchecked`) for CSS enter/exit
86
+ animation.
62
87
 
63
- ## Placeholder
88
+ ### Keyboard
64
89
 
65
- `Select.Placeholder` renders a non-selectable hint that holds the
66
- initial selection. The underlying option is rendered with `value=""`,
67
- `disabled`, and `hidden`, so it shows in the closed Select's display
68
- but is unreachable from the dropdown after the user picks something.
90
+ While the listbox is open:
69
91
 
70
- ```tsx
71
- <Select.Root required aria-label="Pick a fruit">
72
- <Select.Placeholder>Choose a fruit…</Select.Placeholder>
73
- <Select.Option value="apple">Apple</Select.Option>
74
- <Select.Option value="banana">Banana</Select.Option>
75
- </Select.Root>
76
- ```
92
+ | Key | Behaviour |
93
+ | ----------------------- | -------------------------------------------------- |
94
+ | `ArrowDown` / `ArrowUp` | Move focus to next / previous option (wraps) |
95
+ | `Home` / `End` | First / last option |
96
+ | `Enter` / `Space` | Select the focused option and close |
97
+ | `Escape` | Close and return focus to the trigger |
98
+ | printable character | Typeahead — focus the next option matching prefix |
77
99
 
78
- When a `Select.Placeholder` is among Root's direct children and neither
79
- `value` nor `defaultValue` is set, Root infers `defaultValue=""` so the
80
- placeholder — not the first selectable option — is the initial
81
- selection. Pair with `required` on Root to make the browser's native
82
- form validation catch an unchosen value at submit time.
100
+ Disabled options are skipped by arrows and typeahead. On open, focus moves to
101
+ the selected option (or the first enabled one).
83
102
 
84
- `asChild` on Root walks direct children only for this detection, so the
85
- `asChild` + Placeholder combination requires the consumer to set
86
- `defaultValue=""` explicitly.
103
+ ### Popup placement
87
104
 
88
- ## Groups
105
+ `Select.Content` uses the native Popover API (`popover="auto"`) for the top
106
+ layer and light-dismiss — no Portal, no positioning JS. Consumers place the
107
+ popup themselves via CSS (the component ships no anchor positioning).
89
108
 
90
- `Select.Group` wraps options in a native `<optgroup>` with a labelled,
91
- non-selectable heading.
109
+ ## Native mode
110
+
111
+ Pass `native` to render a real `<select>`. `Select.Item` becomes an
112
+ `<option>` — keeping **only its string/number children** as the option text
113
+ and **dropping element children** (icons, indicators don't render). An
114
+ icon-only item with no text renders an empty, unlabelled `<option>`.
92
115
 
93
116
  ```tsx
94
- <Select.Root aria-label="Pick a food">
117
+ <Select.Root native defaultValue="apple" aria-label="Pick a fruit">
118
+ <Select.Placeholder>Choose a fruit…</Select.Placeholder>
95
119
  <Select.Group label="Fruits">
96
- <Select.Option value="apple">Apple</Select.Option>
97
- <Select.Option value="banana">Banana</Select.Option>
98
- </Select.Group>
99
- <Select.Group label="Vegetables">
100
- <Select.Option value="carrot">Carrot</Select.Option>
120
+ <Select.Item value="apple">Apple</Select.Item>
121
+ <Select.Item value="banana">Banana</Select.Item>
101
122
  </Select.Group>
102
123
  </Select.Root>
103
124
  ```
104
125
 
105
- The `label` is announced as the group's accessible name by assistive
106
- technology.
107
-
108
- ## Disabled
126
+ `Select.Placeholder` (native-only) renders `<option value="" disabled hidden>`.
127
+ When present with no `value`/`defaultValue`, Root infers `defaultValue=""` so
128
+ the placeholder is the initial selection; pair with `required` for native form
129
+ validation. `Select.Group` renders `<optgroup label>`.
109
130
 
110
- Pass `disabled` on Root to disable the whole control, or on an
111
- individual `Select.Option` to disable that single choice. The native
112
- `disabled` attribute does the work; `data-disabled=""` is mirrored on
113
- the root `<select>` for CSS targeting.
131
+ ## Forms
114
132
 
115
- ```tsx
116
- <Select.Root disabled aria-label="Pick a fruit">
117
- <Select.Option value="apple">Apple</Select.Option>
118
- </Select.Root>
119
-
120
- <Select.Root aria-label="Pick a fruit">
121
- <Select.Option value="apple">Apple</Select.Option>
122
- <Select.Option value="durian" disabled>Durian (sold out)</Select.Option>
123
- </Select.Root>
124
- ```
133
+ Pass `name` and place the Select in a `<form>`. In native mode the `<select>`
134
+ submits directly; in rich mode Root renders a visually-hidden native
135
+ `<select name>` that carries the value, so submission works identically. Pair
136
+ with `required` for browser validation.
125
137
 
126
138
  ## Field integration
127
139
 
128
- When rendered inside a [`<Field.Root>`](../Field/README.md),
129
- `Select.Root` reads `FieldContext` and inherits:
130
-
131
- - `id` (from `field.id`)
132
- - `aria-describedby` (composed: consumer ids first, then the field's
133
- `descriptionId`, then `errorId` when invalid)
134
- - `aria-invalid` (`"true"` when the field is invalid)
135
- - `disabled`
136
- - `required`
137
-
138
- Consumer-supplied props always win — pass an explicit value on the
139
- `Select.Root` to override any field-derived one. Outside a `<Field.Root>`,
140
- behaviour is unchanged.
141
-
142
- ```tsx
143
- <Field.Root invalid={!!errors.fruit}>
144
- <Field.Label>Fruit</Field.Label>
145
- <Select.Root {...register("fruit")}>
146
- <Select.Placeholder>Choose a fruit…</Select.Placeholder>
147
- <Select.Option value="apple">Apple</Select.Option>
148
- <Select.Option value="banana">Banana</Select.Option>
149
- </Select.Root>
150
- <Field.ErrorText>{errors.fruit?.message}</Field.ErrorText>
151
- </Field.Root>
152
- ```
153
-
154
- ## Form integration
140
+ Inside a [`<Field.Root>`](../Field/README.md), `Select.Root` reads
141
+ `FieldContext` and inherits `id`, `aria-describedby`, `aria-invalid`,
142
+ `disabled`, and `required` (consumer props win). In native mode these apply to
143
+ the `<select>`; in rich mode `disabled` / `required` / `name` flow to the
144
+ hidden form `<select>`.
155
145
 
156
- Native `<select>` is a form-associated element. Pass `name` and
157
- `required`, place the Select inside a `<form>`, and submission carries
158
- the selected value with no extra wiring.
146
+ ## `asChild`
159
147
 
160
- ```tsx
161
- <form onSubmit={…}>
162
- <label>
163
- Fruit
164
- <Select.Root name="fruit" required>
165
- <Select.Placeholder>Choose a fruit…</Select.Placeholder>
166
- <Select.Option value="apple">Apple</Select.Option>
167
- </Select.Root>
168
- </label>
169
- <button type="submit">Submit</button>
170
- </form>
171
- ```
172
-
173
- ## `asChild` composition
174
-
175
- Root accepts `asChild`. The consumer supplies a single element that
176
- renders a `<select>` (typically a styled wrapper). Root's `onChange`,
177
- `data-disabled`, `value` / `defaultValue`, and other native attributes
178
- are merged onto it.
179
-
180
- ```tsx
181
- function StyledSelect(props: ComponentProps<"select">) {
182
- return <select {...props} className="ds-select" />;
183
- }
184
-
185
- <Select.Root asChild value={fruit} onValueChange={setFruit}>
186
- <StyledSelect>
187
- <Select.Option value="apple">Apple</Select.Option>
188
- </StyledSelect>
189
- </Select.Root>
190
- ```
148
+ - `Select.Root` (native mode) — delegate to a styled `<select>` wrapper.
149
+ - `Select.Trigger` / `Select.Content` / `Select.ItemIndicator` (rich mode) —
150
+ compose the merged props onto a consumer element.
191
151
 
192
152
  ## Styling hooks
193
153
 
194
- | Attribute | Values | Set on |
195
- | --------------- | ---------------------------- | ------------- |
196
- | `data-disabled` | `""` (present when disabled) | `Select.Root` |
154
+ | Attribute | Values | Set on |
155
+ | --------------- | ------------------------------- | ---------------------- |
156
+ | `aria-expanded` | `"true"` / `"false"` | `Select.Trigger` |
157
+ | `aria-selected` | `"true"` / `"false"` | rich `Select.Item` |
158
+ | `data-state` | `"checked"` / `"unchecked"` | rich `Select.Item`, `Select.ItemIndicator` |
159
+ | `data-disabled` | `""` (present when disabled) | rich `Select.Item`, native `Select.Root` |
160
+ | `data-placeholder` | `""` (present while nothing is selected) | `Select.Value` |
161
+
162
+ `data-placeholder` is what a styled layer keys the muted placeholder colour
163
+ off — it disappears the moment an item's content is mirrored into the trigger.
197
164
 
198
- ## Limitations
165
+ ## Future work
199
166
 
200
- Native `<select>` only renders text inside `<option>`. Rich item content
201
- (icons, descriptions, indicators) is not supported. A richer Select and
202
- a Combobox with filtering are planned — see [Future
203
- work](../../../../docs/select-future-work.md).
167
+ A Combobox with filtering is planned — see
168
+ [Future work](../../../../docs/select-future-work.md).