@primitiv-ui/react 0.1.28 → 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
@@ -14,4 +14,9 @@ describe("Portal basic rendering", () => {
14
14
  // Assert
15
15
  expect(screen.getByText("Portal content")).toBeInTheDocument();
16
16
  });
17
+
18
+ it('sets displayName to "Portal" for React DevTools and stack traces', () => {
19
+ // Assert — an empty displayName would render it as an anonymous component.
20
+ expect(Portal.displayName).toBe("Portal");
21
+ });
17
22
  });
@@ -5,6 +5,11 @@ import { ReactNode } from "react";
5
5
  * `container` to render into (defaults to `document.body`).
6
6
  */
7
7
  export type PortalProps = {
8
+ /** Content to render inside the portal. Appears directly inside `container`
9
+ * with no wrapping host element added by `Portal` itself. */
8
10
  children?: ReactNode;
11
+ /** DOM node to mount the portal children into. When omitted, children are
12
+ * mounted into `document.body`.
13
+ * @default document.body */
9
14
  container?: HTMLElement;
10
15
  };
@@ -33,6 +33,16 @@ function defaultGetValueLabel(value: number, max: number): string {
33
33
  * **Styling hooks.** `data-state="indeterminate" | "loading" | "complete"`,
34
34
  * plus `data-value` (determinate only) and `data-max` on the root.
35
35
  *
36
+ * **Validation.** `max` must be a positive, finite number and `value` (when
37
+ * not `null`) must be finite and within `0..max`; either violation throws a
38
+ * descriptive error during render so bad inputs surface early in development.
39
+ *
40
+ * **`asChild` composition.** Pass `asChild` to render the consumer's own
41
+ * element as the root instead of the native `<div>`, merging the ARIA and
42
+ * `data-*` hooks onto it via the {@link Slot} pattern.
43
+ *
44
+ * @extends HTMLDivElement
45
+ *
36
46
  * @example Determinate
37
47
  * ```tsx
38
48
  * <Progress.Root value={60} aria-label="Upload progress">
@@ -46,6 +56,18 @@ function defaultGetValueLabel(value: number, max: number): string {
46
56
  * <Progress.Indicator />
47
57
  * </Progress.Root>
48
58
  * ```
59
+ *
60
+ * @example Custom value label
61
+ * ```tsx
62
+ * <Progress.Root
63
+ * value={3}
64
+ * max={5}
65
+ * getValueLabel={(v, m) => `Step ${v} of ${m}`}
66
+ * aria-label="Setup progress"
67
+ * >
68
+ * <Progress.Indicator />
69
+ * </Progress.Root>
70
+ * ```
49
71
  */
50
72
  export function ProgressRoot({
51
73
  value,
@@ -93,7 +115,11 @@ export function ProgressRoot({
93
115
  );
94
116
  }
95
117
 
96
- /** @internal */
118
+ // Runtime-dead: the compound alias below (same object via Object.assign)
119
+ // overwrites this to "Progress" at load, so the value is never observable. The
120
+ // assignment stays because it declares `displayName` on `typeof ProgressRoot`,
121
+ // which TProgressCompound extends.
122
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
97
123
  ProgressRoot.displayName = "ProgressRoot";
98
124
 
99
125
  /**
@@ -109,6 +135,8 @@ ProgressRoot.displayName = "ProgressRoot";
109
135
  * **`asChild` prop.** Pass `asChild` to render the consumer's own element as
110
136
  * the indicator, with the `data-*` hooks merged in.
111
137
  *
138
+ * @extends HTMLDivElement
139
+ *
112
140
  * @example
113
141
  * ```tsx
114
142
  * <Progress.Root value={75} aria-label="Upload progress">
@@ -127,6 +155,10 @@ export function ProgressIndicator({
127
155
  const indicatorProps = {
128
156
  ...rest,
129
157
  "data-state": state,
158
+ // Stryker disable next-line ConditionalExpression: `value` is null only
159
+ // while indeterminate, and React renders null and undefined attributes
160
+ // identically (both omitted), so the guarded (`undefined`) and unguarded
161
+ // (`null`) forms produce the same DOM — a provably-equivalent mutant.
130
162
  "data-value": value === null ? undefined : value,
131
163
  "data-max": max,
132
164
  };
@@ -1,6 +1,7 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
 
3
3
  import { Progress } from "../Progress";
4
+ import { ProgressContext } from "../ProgressContext";
4
5
 
5
6
  describe("Progress basic rendering", () => {
6
7
  it('renders a <div role="progressbar">', () => {
@@ -30,6 +31,7 @@ describe("Progress basic rendering", () => {
30
31
  // Assert
31
32
  expect(bar).toHaveAttribute("data-state", "indeterminate");
32
33
  expect(bar).not.toHaveAttribute("aria-valuenow");
34
+ expect(bar).not.toHaveAttribute("aria-valuetext");
33
35
  expect(bar).not.toHaveAttribute("data-value");
34
36
  });
35
37
 
@@ -62,4 +64,12 @@ describe("Progress basic rendering", () => {
62
64
  );
63
65
  expect(screen.getByTestId("indicator")).toHaveAttribute("data-max", "100");
64
66
  });
67
+
68
+ it("sets a displayName on the compound, indicator, and context", () => {
69
+ // Assert — empty displayNames would render each as anonymous in DevTools.
70
+ // Root aliases the compound (Object.assign), so its name is "Progress".
71
+ expect(Progress.displayName).toBe("Progress");
72
+ expect(Progress.Indicator.displayName).toBe("ProgressIndicator");
73
+ expect(ProgressContext.displayName).toBe("ProgressContext");
74
+ });
65
75
  });
@@ -66,6 +66,27 @@ describe("Progress value", () => {
66
66
  expect(screen.getByTestId("indicator")).toHaveAttribute("data-value", "70");
67
67
  });
68
68
 
69
+ it("propagates a changed value to the Indicator through context", () => {
70
+ // Arrange
71
+ const { rerender } = render(
72
+ <Progress.Root value={40} aria-label="Upload">
73
+ <Progress.Indicator data-testid="indicator" />
74
+ </Progress.Root>,
75
+ );
76
+ expect(screen.getByTestId("indicator")).toHaveAttribute("data-value", "40");
77
+
78
+ // Act — a new value must recompute the memoised context value, not a stale
79
+ // one frozen by empty dependencies.
80
+ rerender(
81
+ <Progress.Root value={70} aria-label="Upload">
82
+ <Progress.Indicator data-testid="indicator" />
83
+ </Progress.Root>,
84
+ );
85
+
86
+ // Assert
87
+ expect(screen.getByTestId("indicator")).toHaveAttribute("data-value", "70");
88
+ });
89
+
69
90
  describe.each(determinateStateCases)(
70
91
  "value $value of $max",
71
92
  ({ value, max, expectedState }) => {
@@ -21,23 +21,39 @@ export type ProgressRootProps = Omit<
21
21
  > & {
22
22
  /**
23
23
  * Current progress, between `0` and `max`. Pass `null` (or omit) for an
24
- * indeterminate progress bar whose completion ratio is unknown.
24
+ * indeterminate progress bar whose completion ratio is unknown. Drives the
25
+ * resolved {@link ProgressState} and the `aria-valuenow` / `data-value`
26
+ * hooks. A non-`null` value outside `0..max` throws.
27
+ * @default null
25
28
  */
26
29
  value?: number | null;
27
- /** Upper bound of `value`. Must be a positive finite number. Defaults to `100`. */
30
+ /**
31
+ * Upper bound of `value`. Must be a positive, finite number, or Root throws
32
+ * during render.
33
+ * @default 100
34
+ */
28
35
  max?: number;
29
36
  /**
30
37
  * Produces the human-readable `aria-valuetext`. Receives the resolved
31
38
  * `value` and `max`. Defaults to a rounded percentage (e.g. `"42%"`).
32
39
  * Not called while indeterminate.
40
+ * @default (value, max) => `${Math.round((value / max) * 100)}%`
33
41
  */
34
42
  getValueLabel?: (value: number, max: number) => string;
35
- /** Render the consumer's own element instead of the native `<div>`. */
43
+ /**
44
+ * Render the consumer's own element instead of the native `<div>`, merging
45
+ * the ARIA and `data-*` hooks onto it via {@link Slot}.
46
+ * @default false
47
+ */
36
48
  asChild?: boolean;
37
49
  };
38
50
 
39
51
  /** Props for {@link Progress.Indicator} — all `<div>` attributes plus `asChild`. */
40
52
  export type ProgressIndicatorProps = ComponentProps<"div"> & {
41
- /** Render the consumer's own element instead of the native `<div>`. */
53
+ /**
54
+ * Render the consumer's own element instead of the native `<div>`, merging
55
+ * the `data-*` hooks onto it via {@link Slot}.
56
+ * @default false
57
+ */
42
58
  asChild?: boolean;
43
59
  };
@@ -46,6 +46,8 @@ function dataStateOf(checked: boolean) {
46
46
  * visibility off the input's native `:checked` / `:has()` selectors;
47
47
  * `data-state` is a convenience mirror.
48
48
  *
49
+ * @extends HTMLInputElement
50
+ *
49
51
  * @example Native group (the browser owns selection)
50
52
  * ```tsx
51
53
  * <Radio.Root name="density" value="compact" defaultChecked aria-label="Compact">
@@ -112,7 +114,11 @@ export function RadioRoot(props: RadioRootProps): ReactElement {
112
114
  );
113
115
  }
114
116
 
115
- /** @internal */
117
+ // Runtime-dead: the compound alias below (same object via Object.assign)
118
+ // overwrites this to "Radio" at load, so the value is never observable. The
119
+ // assignment stays because it declares `displayName` on `typeof RadioRoot`,
120
+ // which TRadioCompound extends.
121
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
116
122
  RadioRoot.displayName = "RadioRoot";
117
123
 
118
124
  /**
@@ -129,6 +135,8 @@ RadioRoot.displayName = "RadioRoot";
129
135
  * (typically an `<svg>` dot) as the indicator itself, with `data-state` merged
130
136
  * onto that element rather than a wrapper.
131
137
  *
138
+ * @extends HTMLSpanElement
139
+ *
132
140
  * @example Dot as the indicator via `asChild`
133
141
  * ```tsx
134
142
  * <Radio.Indicator asChild>
@@ -63,4 +63,18 @@ describe("Radio basic rendering", () => {
63
63
  "unchecked",
64
64
  );
65
65
  });
66
+
67
+ it("sets a displayName on the compound and the Indicator", () => {
68
+ // Assert — empty displayNames would render each as anonymous in DevTools.
69
+ // Root aliases the compound (Object.assign), so its name is "Radio".
70
+ expect(Radio.displayName).toBe("Radio");
71
+ expect(Radio.Indicator.displayName).toBe("RadioIndicator");
72
+ });
73
+
74
+ it("throws a helpful error when a sub-component renders outside Radio.Root", () => {
75
+ // Assert — the strict-context guard names the required parent.
76
+ expect(() => render(<Radio.Indicator />)).toThrow(
77
+ "Radio sub-components must be rendered inside a <Radio.Root>.",
78
+ );
79
+ });
66
80
  });
@@ -36,6 +36,9 @@ export function useRadioRoot({
36
36
  onChange?.(event);
37
37
  if (event.defaultPrevented) return;
38
38
  const next = event.target.checked;
39
+ // Stryker disable next-line ConditionalExpression: in controlled mode the
40
+ // input reads the controlled value, never `mirror`, so setting mirror here
41
+ // (the mutant's always-run form) has no observable effect — equivalent.
39
42
  if (!isControlled) setMirror(next);
40
43
  onCheckedChange?.(next);
41
44
  };
@@ -8,7 +8,16 @@ import { ChangeEventHandler, ComponentProps, ReactNode, Ref } from "react";
8
8
  * grouped sibling (which fires no React event).
9
9
  */
10
10
  export type RadioIndicatorProps = ComponentProps<"span"> & {
11
+ /** Custom dot content. Omit to let the shipped CSS draw the dot off the
12
+ * input's native `:checked` state; provide your own (an icon, glyph, or
13
+ * nested element) to override it. */
11
14
  children?: ReactNode;
15
+ /**
16
+ * When `true`, render `children` as the indicator element itself (via the
17
+ * {@link Slot} pattern) instead of wrapping them in a `<span>`. `data-state`
18
+ * and `aria-hidden` are merged onto that element.
19
+ * @default false
20
+ */
12
21
  asChild?: boolean;
13
22
  };
14
23
 
@@ -42,7 +51,11 @@ export type RadioRootBaseProps = Omit<
42
51
  * forbidden.
43
52
  */
44
53
  export type RadioRootUncontrolledProps = RadioRootBaseProps & {
54
+ /** Whether this radio is selected on first render; the **browser** owns it
55
+ * thereafter, so native `name`-grouping (including silent deselection of
56
+ * siblings) works for free. Omit for an initially unselected radio. */
45
57
  defaultChecked?: boolean;
58
+ /** Forbidden in uncontrolled mode — use `defaultChecked` instead. */
46
59
  checked?: never;
47
60
  };
48
61
 
@@ -52,8 +65,15 @@ export type RadioRootUncontrolledProps = RadioRootBaseProps & {
52
65
  * forbidden. The consumer owns grouping in this mode.
53
66
  */
54
67
  export type RadioRootControlledProps = RadioRootBaseProps & {
68
+ /** Forbidden in controlled mode — use `checked` instead. */
55
69
  defaultChecked?: never;
70
+ /** Whether this radio is currently selected, owned by the parent. The
71
+ * consumer owns grouping in this mode (typically deriving each radio's
72
+ * `checked` from a single shared value). Keep it in sync via
73
+ * `onCheckedChange`. */
56
74
  checked: boolean;
75
+ /** Called (always with `true`) whenever this radio becomes selected —
76
+ * required in controlled mode so the parent can update its shared value. */
57
77
  onCheckedChange: (checked: boolean) => void;
58
78
  };
59
79
 
@@ -49,6 +49,8 @@ import {
49
49
  * **`asChild` prop.** Pass `asChild` to render any consumer-supplied
50
50
  * element with the RadioCard's props merged in. The native `<div>` is dropped.
51
51
  *
52
+ * @extends HTMLDivElement
53
+ *
52
54
  * @example Uncontrolled
53
55
  * ```tsx
54
56
  * <RadioCard.Root defaultValue="pro" aria-label="Plan">
@@ -130,6 +132,11 @@ export function RadioCardRoot({
130
132
  }
131
133
 
132
134
  /** @internal */
135
+ // Runtime-dead: the compound alias below (same object via Object.assign)
136
+ // overwrites this to "RadioCard" at load, so the value is never observable.
137
+ // The assignment stays because it declares `displayName` on
138
+ // `typeof RadioCardRoot`, which TRadioCardCompound extends.
139
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
133
140
  RadioCardRoot.displayName = "RadioCardRoot";
134
141
 
135
142
  /**
@@ -157,6 +164,8 @@ RadioCardRoot.displayName = "RadioCardRoot";
157
164
  * the Item's ARIA, data-state, tabIndex, onClick, onKeyDown, disabled, and
158
165
  * ref merged onto it.
159
166
  *
167
+ * @extends HTMLButtonElement
168
+ *
160
169
  * @throws if rendered outside a `RadioCard.Root`.
161
170
  */
162
171
  export function RadioCardItem({
@@ -252,6 +261,8 @@ RadioCardItem.displayName = "RadioCardItem";
252
261
  * while unchecked so a CSS exit animation can play against
253
262
  * `data-state="unchecked"`.
254
263
  *
264
+ * @extends HTMLSpanElement
265
+ *
255
266
  * @example
256
267
  * ```tsx
257
268
  * <RadioCard.Item value="pro">
@@ -84,4 +84,12 @@ describe("RadioCard basic rendering", () => {
84
84
  // Assert
85
85
  expect(screen.getByTestId("icon")).toBeInTheDocument();
86
86
  });
87
+
88
+ it("sets a displayName on the compound, Item, and Indicator", () => {
89
+ // Assert — empty displayNames would render each as anonymous in DevTools.
90
+ // Root aliases the compound (Object.assign), so its name is "RadioCard".
91
+ expect(RadioCard.displayName).toBe("RadioCard");
92
+ expect(RadioCard.Item.displayName).toBe("RadioCardItem");
93
+ expect(RadioCard.Indicator.displayName).toBe("RadioCardIndicator");
94
+ });
87
95
  });
@@ -37,6 +37,33 @@ describe("RadioCard disabled items", () => {
37
37
  expect(onValueChange).not.toHaveBeenCalled();
38
38
  });
39
39
 
40
+ it("re-registers an item's disabled flag when it changes after mount", async () => {
41
+ // Arrange — all enabled at mount; Pro becomes disabled afterwards.
42
+ const user = userEvent.setup();
43
+ function Fixture({ proDisabled }: { proDisabled: boolean }) {
44
+ return (
45
+ <RadioCard.Root aria-label="Plan" defaultValue="starter">
46
+ <RadioCard.Item value="starter">Starter</RadioCard.Item>
47
+ <RadioCard.Item value="pro" disabled={proDisabled}>
48
+ Pro
49
+ </RadioCard.Item>
50
+ <RadioCard.Item value="enterprise">Enterprise</RadioCard.Item>
51
+ </RadioCard.Root>
52
+ );
53
+ }
54
+ const { rerender } = render(<Fixture proDisabled={false} />);
55
+
56
+ // Act — disable Pro after mount, then arrow forwards from Starter.
57
+ rerender(<Fixture proDisabled />);
58
+ screen.getByRole("radio", { name: "Starter" }).focus();
59
+ await user.keyboard("{ArrowDown}");
60
+
61
+ // Assert — the registrar effect re-ran, so Pro is skipped and focus lands
62
+ // on Enterprise. A stale registration would keep Pro navigable and strand
63
+ // focus (a disabled button cannot receive it).
64
+ expect(screen.getByRole("radio", { name: "Enterprise" })).toHaveFocus();
65
+ });
66
+
40
67
  it("excludes disabled items from the roving-tabindex home base", () => {
41
68
  // Arrange & Act — no default value, first NON-disabled item is the tab stop
42
69
  render(
@@ -3,6 +3,59 @@ import userEvent from "@testing-library/user-event";
3
3
 
4
4
  import { RadioCard } from "../RadioCard";
5
5
 
6
+ describe("RadioCard roving tab stop", () => {
7
+ it("makes the selected item the tab stop even when it is not first", () => {
8
+ // Arrange & Act — Pro is selected but not the first item.
9
+ render(
10
+ <RadioCard.Root aria-label="Plan" defaultValue="pro">
11
+ <RadioCard.Item value="starter">Starter</RadioCard.Item>
12
+ <RadioCard.Item value="pro">Pro</RadioCard.Item>
13
+ </RadioCard.Root>,
14
+ );
15
+
16
+ // Assert — selection drives the tab stop; falling back to the first item
17
+ // would strand it on Starter.
18
+ expect(screen.getByRole("radio", { name: "Pro" })).toHaveAttribute(
19
+ "tabindex",
20
+ "0",
21
+ );
22
+ expect(screen.getByRole("radio", { name: "Starter" })).toHaveAttribute(
23
+ "tabindex",
24
+ "-1",
25
+ );
26
+ });
27
+
28
+ it("unregisters an unmounted item so the home-base tab stop moves", () => {
29
+ // Arrange — nothing selected, so the first item is the home base.
30
+ function Fixture({ items }: { items: string[] }) {
31
+ return (
32
+ <RadioCard.Root aria-label="Plan">
33
+ {items.map((v) => (
34
+ <RadioCard.Item key={v} value={v}>
35
+ {v}
36
+ </RadioCard.Item>
37
+ ))}
38
+ </RadioCard.Root>
39
+ );
40
+ }
41
+ const { rerender } = render(<Fixture items={["starter", "pro"]} />);
42
+ expect(screen.getByRole("radio", { name: "starter" })).toHaveAttribute(
43
+ "tabindex",
44
+ "0",
45
+ );
46
+
47
+ // Act — remove "starter"; its effect cleanup must unregister it.
48
+ rerender(<Fixture items={["pro"]} />);
49
+
50
+ // Assert — "pro" becomes the home base. A no-op cleanup would leave
51
+ // "starter" registered ahead of it, leaving "pro" at tabindex -1.
52
+ expect(screen.getByRole("radio", { name: "pro" })).toHaveAttribute(
53
+ "tabindex",
54
+ "0",
55
+ );
56
+ });
57
+ });
58
+
6
59
  describe("RadioCard keyboard interaction", () => {
7
60
  it("selects and focuses the next item on ArrowDown", async () => {
8
61
  // Arrange
@@ -0,0 +1,54 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { RadioCard } from "../RadioCard";
4
+
5
+ describe("RadioCard.Item ref forwarding", () => {
6
+ it("forwards the underlying button element to a consumer function ref", () => {
7
+ // Arrange
8
+ const received: (HTMLButtonElement | null)[] = [];
9
+ const functionRef = (node: HTMLButtonElement | null) => {
10
+ received.push(node);
11
+ };
12
+
13
+ // Act
14
+ render(
15
+ <RadioCard.Root aria-label="Plan">
16
+ <RadioCard.Item value="pro" ref={functionRef}>
17
+ Pro
18
+ </RadioCard.Item>
19
+ </RadioCard.Root>,
20
+ );
21
+
22
+ // Assert
23
+ expect(received).toContain(screen.getByRole("radio", { name: "Pro" }));
24
+ });
25
+
26
+ it("re-attaches to a replacement ref when the ref prop changes", () => {
27
+ // Arrange — the item's ref memo must depend on `ref`, or a changed ref
28
+ // callback is never invoked with the element.
29
+ const secondReceived: (HTMLButtonElement | null)[] = [];
30
+ const firstRef = () => {};
31
+ const secondRef = (node: HTMLButtonElement | null) => {
32
+ secondReceived.push(node);
33
+ };
34
+
35
+ // Act — mount with the first ref, then re-render with a different one.
36
+ const { rerender } = render(
37
+ <RadioCard.Root aria-label="Plan">
38
+ <RadioCard.Item value="pro" ref={firstRef}>
39
+ Pro
40
+ </RadioCard.Item>
41
+ </RadioCard.Root>,
42
+ );
43
+ rerender(
44
+ <RadioCard.Root aria-label="Plan">
45
+ <RadioCard.Item value="pro" ref={secondRef}>
46
+ Pro
47
+ </RadioCard.Item>
48
+ </RadioCard.Root>,
49
+ );
50
+
51
+ // Assert — the replacement ref received the button.
52
+ expect(secondReceived).toContain(screen.getByRole("radio", { name: "Pro" }));
53
+ });
54
+ });
@@ -105,4 +105,25 @@ describe("RadioCard uncontrolled state", () => {
105
105
  // Assert
106
106
  expect(onValueChange).not.toHaveBeenCalled();
107
107
  });
108
+
109
+ it("does not re-fire onValueChange when a freshly selected item is re-clicked", async () => {
110
+ // Arrange — no defaultValue, so the guard reads a value that changes after
111
+ // the first click; a stale select closure would re-fire on the second.
112
+ const user = userEvent.setup();
113
+ const onValueChange = vi.fn();
114
+ render(
115
+ <RadioCard.Root aria-label="Plan" onValueChange={onValueChange}>
116
+ <RadioCard.Item value="pro">Pro</RadioCard.Item>
117
+ <RadioCard.Item value="starter">Starter</RadioCard.Item>
118
+ </RadioCard.Root>,
119
+ );
120
+ const pro = screen.getByRole("radio", { name: "Pro" });
121
+
122
+ // Act — select Pro, then click it again while it is already selected.
123
+ await user.click(pro);
124
+ await user.click(pro);
125
+
126
+ // Assert — exactly one change: the redundant second click is a no-op.
127
+ expect(onValueChange).toHaveBeenCalledOnce();
128
+ });
108
129
  });
@@ -46,6 +46,9 @@ export function useRadioCardRoot({
46
46
  ) => {
47
47
  registerItemBase(itemValue, element ? { element, disabled } : null);
48
48
  },
49
+ // `registerItemBase` is a stable useCollection callback (its own deps never
50
+ // change), so emptying this array yields the identical memoised function.
51
+ // Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
49
52
  [registerItemBase],
50
53
  );
51
54
 
@@ -61,8 +64,16 @@ export function useRadioCardRoot({
61
64
 
62
65
  const focusItem = useCallback(
63
66
  (itemValue: string) => {
67
+ // `itemValue` is always sourced from `navigable` (the enabled subset of
68
+ // `itemValues`), and `useCollection`'s register() writes `itemsRef` and
69
+ // the `itemValues` state together in the same call — so a value that
70
+ // appears here is guaranteed to already have a live entry in the map.
71
+ // Stryker disable next-line OptionalChaining: unreachable given that invariant.
64
72
  itemsRef.current.get(itemValue)?.element.focus();
65
73
  },
74
+ // `itemsRef` is a stable RefObject, so emptying this array yields the
75
+ // identical memoised function.
76
+ // Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
66
77
  [itemsRef],
67
78
  );
68
79