@astryxdesign/core 0.4.7 → 0.5.0-canary.54bb922

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 (467) hide show
  1. package/CHANGELOG.md +161 -1
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/AppShell/AppShell.js +3 -2
  4. package/dist/AspectRatio/AspectRatio.d.ts +24 -0
  5. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  6. package/dist/AspectRatio/AspectRatio.js +23 -4
  7. package/dist/Avatar/Avatar.d.ts.map +1 -1
  8. package/dist/Avatar/Avatar.js +15 -10
  9. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  10. package/dist/AvatarGroup/AvatarGroup.js +3 -2
  11. package/dist/Banner/Banner.d.ts +40 -15
  12. package/dist/Banner/Banner.d.ts.map +1 -1
  13. package/dist/Banner/Banner.js +51 -18
  14. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  15. package/dist/BottomSheet/BottomSheet.js +17 -4
  16. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  17. package/dist/BottomSheet/BottomSheetSwitcher.js +3 -2
  18. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  19. package/dist/Breadcrumbs/BreadcrumbItem.js +11 -10
  20. package/dist/Button/Button.d.ts.map +1 -1
  21. package/dist/Button/Button.js +5 -4
  22. package/dist/ButtonGroup/ButtonGroup.d.ts +5 -1
  23. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  24. package/dist/ButtonGroup/ButtonGroup.js +26 -7
  25. package/dist/Calendar/Calendar.d.ts +3 -1
  26. package/dist/Calendar/Calendar.d.ts.map +1 -1
  27. package/dist/Calendar/Calendar.js +13 -14
  28. package/dist/Calendar/getInitialFocusDate.d.ts +46 -0
  29. package/dist/Calendar/getInitialFocusDate.d.ts.map +1 -0
  30. package/dist/Calendar/getInitialFocusDate.js +62 -0
  31. package/dist/Carousel/Carousel.d.ts.map +1 -1
  32. package/dist/Carousel/Carousel.js +3 -3
  33. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  34. package/dist/Chat/ChatComposerInput.js +2 -3
  35. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  36. package/dist/Chat/ChatLayout.js +3 -3
  37. package/dist/Chat/ChatMessageList.d.ts +22 -3
  38. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  39. package/dist/Chat/ChatMessageList.js +6 -3
  40. package/dist/Chat/ChatTokenizedText.d.ts +2 -0
  41. package/dist/Chat/ChatTokenizedText.d.ts.map +1 -1
  42. package/dist/Chat/ChatTokenizedText.js +11 -6
  43. package/dist/Chat/useChatComposerTokens.js +1 -1
  44. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  45. package/dist/CheckboxInput/CheckboxInput.js +3 -2
  46. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  47. package/dist/ClickableCard/ClickableCard.js +3 -3
  48. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  49. package/dist/CommandPalette/CommandPaletteInput.js +3 -2
  50. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  51. package/dist/CommandPalette/CommandPaletteItem.js +3 -2
  52. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  53. package/dist/ContextMenu/ContextMenu.js +4 -3
  54. package/dist/DateInput/DateInput.d.ts.map +1 -1
  55. package/dist/DateInput/DateInput.js +3 -2
  56. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  57. package/dist/DateInput/TouchDateField.js +38 -4
  58. package/dist/DateTimeInput/DateTimeInput.d.ts +21 -1
  59. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  60. package/dist/DateTimeInput/DateTimeInput.js +406 -12
  61. package/dist/DateTimeInput/index.d.ts +1 -1
  62. package/dist/DateTimeInput/index.d.ts.map +1 -1
  63. package/dist/Dialog/Dialog.d.ts +7 -1
  64. package/dist/Dialog/Dialog.d.ts.map +1 -1
  65. package/dist/Dialog/Dialog.js +89 -58
  66. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  67. package/dist/DropdownMenu/DropdownMenuSubMenu.js +6 -5
  68. package/dist/FileInput/FileInput.d.ts.map +1 -1
  69. package/dist/FileInput/FileInput.js +3 -2
  70. package/dist/Heading/Heading.d.ts.map +1 -1
  71. package/dist/Heading/Heading.js +6 -2
  72. package/dist/HoverCard/HoverCard.d.ts +6 -0
  73. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  74. package/dist/HoverCard/useHoverCard.d.ts +5 -0
  75. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  76. package/dist/HoverCard/useHoverCard.js +67 -25
  77. package/dist/Item/Item.d.ts.map +1 -1
  78. package/dist/Item/Item.js +4 -2
  79. package/dist/Kbd/Kbd.d.ts.map +1 -1
  80. package/dist/Kbd/Kbd.js +2 -19
  81. package/dist/Layer/LayerDepthContext.d.ts +52 -0
  82. package/dist/Layer/LayerDepthContext.d.ts.map +1 -0
  83. package/dist/Layer/LayerDepthContext.js +66 -0
  84. package/dist/Layer/index.d.ts +3 -0
  85. package/dist/Layer/index.d.ts.map +1 -1
  86. package/dist/Layer/index.js +4 -0
  87. package/dist/Layer/layerStack.d.ts +98 -0
  88. package/dist/Layer/layerStack.d.ts.map +1 -0
  89. package/dist/Layer/layerStack.js +306 -0
  90. package/dist/Layer/useLayerDismissal.d.ts +93 -0
  91. package/dist/Layer/useLayerDismissal.d.ts.map +1 -0
  92. package/dist/Layer/useLayerDismissal.js +91 -0
  93. package/dist/Lightbox/Lightbox.d.ts +1 -0
  94. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  95. package/dist/Lightbox/Lightbox.js +103 -81
  96. package/dist/Markdown/index.d.ts +1 -1
  97. package/dist/Markdown/index.d.ts.map +1 -1
  98. package/dist/Markdown/parser.d.ts +38 -1
  99. package/dist/Markdown/parser.d.ts.map +1 -1
  100. package/dist/Markdown/parser.js +177 -29
  101. package/dist/MobileNav/MobileNav.d.ts +6 -1
  102. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  103. package/dist/MobileNav/MobileNav.js +61 -36
  104. package/dist/MultiSelector/MultiSelector.d.ts +15 -1
  105. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  106. package/dist/MultiSelector/MultiSelector.js +17 -6
  107. package/dist/MultiSelector/index.d.ts +1 -1
  108. package/dist/MultiSelector/index.d.ts.map +1 -1
  109. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  110. package/dist/NavMenu/NavHeadingMenu.js +3 -2
  111. package/dist/Outline/Outline.d.ts.map +1 -1
  112. package/dist/Outline/Outline.js +3 -2
  113. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  114. package/dist/OverflowList/OverflowList.js +3 -2
  115. package/dist/Overlay/Overlay.d.ts.map +1 -1
  116. package/dist/Overlay/Overlay.js +3 -2
  117. package/dist/Popover/usePopover.d.ts.map +1 -1
  118. package/dist/Popover/usePopover.js +20 -17
  119. package/dist/PowerSearch/PowerSearch.d.ts +8 -2
  120. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  121. package/dist/PowerSearch/PowerSearch.js +19 -6
  122. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +3 -1
  123. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  124. package/dist/PowerSearch/PowerSearchEditPopover.js +28 -6
  125. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +3 -1
  126. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  127. package/dist/PowerSearch/PowerSearchValueEditor.js +19 -9
  128. package/dist/PowerSearch/types.d.ts +2 -0
  129. package/dist/PowerSearch/types.d.ts.map +1 -1
  130. package/dist/PowerSearch/usePowerSearchSource.d.ts +6 -1
  131. package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
  132. package/dist/PowerSearch/usePowerSearchSource.js +14 -5
  133. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  134. package/dist/Resizable/ResizeHandle.js +3 -2
  135. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  136. package/dist/SegmentedControl/SegmentedControl.js +3 -2
  137. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  138. package/dist/SelectableCard/SelectableCard.js +3 -3
  139. package/dist/Selector/Selector.d.ts.map +1 -1
  140. package/dist/Selector/Selector.js +5 -0
  141. package/dist/SideNav/SideNav.d.ts.map +1 -1
  142. package/dist/SideNav/SideNav.js +4 -3
  143. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  144. package/dist/SideNav/SideNavHeading.js +6 -5
  145. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  146. package/dist/SideNav/SideNavItem.js +4 -3
  147. package/dist/Slider/Slider.d.ts.map +1 -1
  148. package/dist/Slider/Slider.js +3 -3
  149. package/dist/Spinner/Spinner.d.ts +3 -3
  150. package/dist/Spinner/Spinner.d.ts.map +1 -1
  151. package/dist/Spinner/Spinner.js +130 -78
  152. package/dist/Stepper/Step.d.ts +140 -0
  153. package/dist/Stepper/Step.d.ts.map +1 -0
  154. package/dist/Stepper/Step.js +1045 -0
  155. package/dist/Stepper/StepStatus.d.ts +24 -0
  156. package/dist/Stepper/StepStatus.d.ts.map +1 -0
  157. package/dist/Stepper/StepStatus.js +1 -0
  158. package/dist/Stepper/Stepper.d.ts +93 -0
  159. package/dist/Stepper/Stepper.d.ts.map +1 -0
  160. package/dist/Stepper/Stepper.js +184 -0
  161. package/dist/Stepper/StepperContext.d.ts +41 -0
  162. package/dist/Stepper/StepperContext.d.ts.map +1 -0
  163. package/dist/Stepper/StepperContext.js +34 -0
  164. package/dist/Stepper/index.d.ts +8 -0
  165. package/dist/Stepper/index.d.ts.map +1 -0
  166. package/dist/Stepper/index.js +7 -0
  167. package/dist/Stepper/stepper.stylex.d.ts +15 -0
  168. package/dist/Stepper/stepper.stylex.d.ts.map +1 -0
  169. package/dist/Stepper/stepper.stylex.js +20 -0
  170. package/dist/Switch/Switch.d.ts.map +1 -1
  171. package/dist/Switch/Switch.js +3 -2
  172. package/dist/TabList/Tab.d.ts +15 -2
  173. package/dist/TabList/Tab.d.ts.map +1 -1
  174. package/dist/TabList/Tab.js +32 -7
  175. package/dist/TabList/TabList.d.ts +46 -7
  176. package/dist/TabList/TabList.d.ts.map +1 -1
  177. package/dist/TabList/TabList.js +353 -23
  178. package/dist/TabList/TabListContext.d.ts +11 -1
  179. package/dist/TabList/TabListContext.d.ts.map +1 -1
  180. package/dist/TabList/TabListContext.js +11 -2
  181. package/dist/TabList/TabMenu.d.ts.map +1 -1
  182. package/dist/TabList/TabMenu.js +3 -2
  183. package/dist/TabList/index.d.ts +2 -2
  184. package/dist/TabList/index.d.ts.map +1 -1
  185. package/dist/Table/BaseTable.d.ts.map +1 -1
  186. package/dist/Table/BaseTable.js +4 -1
  187. package/dist/Table/TableRow.d.ts +16 -5
  188. package/dist/Table/TableRow.d.ts.map +1 -1
  189. package/dist/Table/TableRow.js +16 -5
  190. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  191. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +20 -8
  192. package/dist/Table/plugins/pagination/useTablePagination.d.ts +3 -0
  193. package/dist/Table/plugins/pagination/useTablePagination.d.ts.map +1 -1
  194. package/dist/Table/plugins/pagination/useTablePagination.js +21 -4
  195. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  196. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
  197. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -1
  198. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +10 -3
  199. package/dist/Table/plugins/selection/useTableSelection.d.ts +16 -0
  200. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  201. package/dist/Table/plugins/selection/useTableSelection.js +19 -5
  202. package/dist/Table/types.d.ts +22 -4
  203. package/dist/Table/types.d.ts.map +1 -1
  204. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  205. package/dist/Table/useBaseTablePlugins.js +5 -0
  206. package/dist/Text/Text.d.ts.map +1 -1
  207. package/dist/Text/Text.js +6 -2
  208. package/dist/TextArea/TextArea.d.ts.map +1 -1
  209. package/dist/TextArea/TextArea.js +3 -2
  210. package/dist/TextInput/TextInput.d.ts.map +1 -1
  211. package/dist/TextInput/TextInput.js +3 -2
  212. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  213. package/dist/TimeInput/TimeInput.js +3 -2
  214. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  215. package/dist/Timestamp/Timestamp.js +5 -3
  216. package/dist/Token/TokenLink.d.ts.map +1 -1
  217. package/dist/Token/TokenLink.js +2 -2
  218. package/dist/Tokenizer/Tokenizer.d.ts +3 -1
  219. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  220. package/dist/Tokenizer/Tokenizer.js +2 -0
  221. package/dist/Tooltip/Tooltip.d.ts +5 -0
  222. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  223. package/dist/Tooltip/useTooltip.d.ts +5 -0
  224. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  225. package/dist/Tooltip/useTooltip.js +39 -24
  226. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  227. package/dist/TopNav/TopNavHeading.js +3 -2
  228. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  229. package/dist/TopNav/TopNavMegaMenu.js +3 -3
  230. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  231. package/dist/TopNav/TopNavMenu.js +4 -3
  232. package/dist/Typeahead/BaseTypeahead.d.ts +2 -0
  233. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  234. package/dist/Typeahead/BaseTypeahead.js +68 -29
  235. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  236. package/dist/Typeahead/Typeahead.js +3 -2
  237. package/dist/astryx.css +68 -2
  238. package/dist/hooks/index.d.ts +1 -0
  239. package/dist/hooks/index.d.ts.map +1 -1
  240. package/dist/hooks/index.js +1 -0
  241. package/dist/hooks/useFocusTrap.d.ts +7 -4
  242. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  243. package/dist/hooks/useFocusTrap.js +60 -95
  244. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  245. package/dist/hooks/useHotkeys.js +3 -18
  246. package/dist/hooks/useMergedRefs.d.ts +18 -0
  247. package/dist/hooks/useMergedRefs.d.ts.map +1 -0
  248. package/dist/hooks/useMergedRefs.js +25 -0
  249. package/dist/index.d.ts +1 -0
  250. package/dist/index.d.ts.map +1 -1
  251. package/dist/index.js +1 -0
  252. package/dist/utils/groupItems.d.ts +5 -1
  253. package/dist/utils/groupItems.d.ts.map +1 -1
  254. package/dist/utils/groupItems.js +9 -9
  255. package/dist/utils/index.d.ts +1 -1
  256. package/dist/utils/index.d.ts.map +1 -1
  257. package/dist/utils/isApplePlatform.d.ts +18 -0
  258. package/dist/utils/isApplePlatform.d.ts.map +1 -0
  259. package/dist/utils/isApplePlatform.js +37 -0
  260. package/dist/utils/sharedResizeObserver.d.ts.map +1 -1
  261. package/dist/utils/sharedResizeObserver.js +10 -1
  262. package/dist/utils/timeParser.d.ts +1 -1
  263. package/dist/utils/timeParser.d.ts.map +1 -1
  264. package/dist/utils/timeParser.js +26 -12
  265. package/locales/en.json +31 -7
  266. package/locales/pseudo.json +18 -0
  267. package/package.json +8 -3
  268. package/src/AppShell/AppShell.tsx +3 -2
  269. package/src/AspectRatio/AspectRatio.doc.mjs +6 -3
  270. package/src/AspectRatio/AspectRatio.test.tsx +57 -10
  271. package/src/AspectRatio/AspectRatio.tsx +40 -1
  272. package/src/Avatar/Avatar.doc.mjs +2 -1
  273. package/src/Avatar/Avatar.test.tsx +17 -0
  274. package/src/Avatar/Avatar.tsx +61 -38
  275. package/src/AvatarGroup/AvatarGroup.tsx +3 -2
  276. package/src/Banner/Banner.doc.mjs +13 -11
  277. package/src/Banner/Banner.test.tsx +102 -15
  278. package/src/Banner/Banner.tsx +72 -25
  279. package/src/BottomSheet/BottomSheet.test.tsx +123 -0
  280. package/src/BottomSheet/BottomSheet.tsx +17 -4
  281. package/src/BottomSheet/BottomSheetSwitcher.tsx +3 -2
  282. package/src/Breadcrumbs/BreadcrumbItem.tsx +9 -5
  283. package/src/Button/Button.doc.mjs +22 -0
  284. package/src/Button/Button.test.tsx +13 -0
  285. package/src/Button/Button.tsx +5 -4
  286. package/src/ButtonGroup/ButtonGroup.doc.mjs +10 -1
  287. package/src/ButtonGroup/ButtonGroup.test.tsx +391 -1
  288. package/src/ButtonGroup/ButtonGroup.tsx +23 -5
  289. package/src/Calendar/Calendar.doc.mjs +4 -3
  290. package/src/Calendar/Calendar.test.tsx +52 -0
  291. package/src/Calendar/Calendar.tsx +18 -15
  292. package/src/Calendar/getInitialFocusDate.test.ts +94 -0
  293. package/src/Calendar/getInitialFocusDate.ts +93 -0
  294. package/src/Carousel/Carousel.tsx +3 -2
  295. package/src/Chat/ChatComposerInput.tsx +5 -1
  296. package/src/Chat/ChatLayout.tsx +3 -2
  297. package/src/Chat/ChatMessageList.doc.mjs +9 -1
  298. package/src/Chat/ChatMessageList.test.tsx +46 -0
  299. package/src/Chat/ChatMessageList.tsx +28 -4
  300. package/src/Chat/ChatTokenizedText.tsx +17 -10
  301. package/src/Chat/useChatComposerTokens.ts +1 -1
  302. package/src/CheckboxInput/CheckboxInput.tsx +3 -2
  303. package/src/ClickableCard/ClickableCard.tsx +3 -2
  304. package/src/CodeBlock/CodeBlock.doc.mjs +6 -0
  305. package/src/CommandPalette/CommandPaletteInput.tsx +3 -2
  306. package/src/CommandPalette/CommandPaletteItem.tsx +3 -2
  307. package/src/ContextMenu/ContextMenu.doc.mjs +5 -0
  308. package/src/ContextMenu/ContextMenu.tsx +4 -3
  309. package/src/DateInput/DateInput.tsx +3 -2
  310. package/src/DateInput/DateInputTouch.test.tsx +36 -0
  311. package/src/DateInput/TouchDateField.tsx +38 -4
  312. package/src/DateTimeInput/DateTimeInput.doc.mjs +18 -0
  313. package/src/DateTimeInput/DateTimeInput.test.tsx +1088 -0
  314. package/src/DateTimeInput/DateTimeInput.tsx +556 -24
  315. package/src/DateTimeInput/index.ts +1 -0
  316. package/src/Dialog/Dialog.doc.mjs +8 -3
  317. package/src/Dialog/Dialog.test.tsx +261 -1
  318. package/src/Dialog/Dialog.tsx +109 -58
  319. package/src/DropdownMenu/DropdownMenu.doc.mjs +6 -0
  320. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +8 -5
  321. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +8 -11
  322. package/src/Field/Field.doc.mjs +13 -2
  323. package/src/FileInput/FileInput.tsx +3 -2
  324. package/src/Heading/Heading.test.tsx +23 -1
  325. package/src/Heading/Heading.tsx +6 -2
  326. package/src/HoverCard/HoverCard.doc.mjs +5 -0
  327. package/src/HoverCard/HoverCard.tsx +6 -0
  328. package/src/HoverCard/useHoverCard.tsx +81 -30
  329. package/src/Item/Item.tsx +5 -4
  330. package/src/Kbd/Kbd.tsx +2 -19
  331. package/src/Layer/LayerDepthContext.tsx +62 -0
  332. package/src/Layer/index.ts +9 -0
  333. package/src/Layer/layerDismissalFamilies.test.tsx +359 -0
  334. package/src/Layer/layerDismissalInvariants.test.tsx +417 -0
  335. package/src/Layer/layerStack.ts +362 -0
  336. package/src/Layer/useLayerDismissal.test.tsx +469 -0
  337. package/src/Layer/useLayerDismissal.ts +173 -0
  338. package/src/Layout/Layout.doc.mjs +2 -2
  339. package/src/Lightbox/Lightbox.doc.mjs +12 -0
  340. package/src/Lightbox/Lightbox.tsx +124 -102
  341. package/src/Link/Link.doc.mjs +39 -0
  342. package/src/Markdown/Markdown.doc.mjs +5 -0
  343. package/src/Markdown/incremental.test.ts +16 -2
  344. package/src/Markdown/index.ts +1 -0
  345. package/src/Markdown/parser.perf.test.ts +96 -1
  346. package/src/Markdown/parser.test.ts +145 -2
  347. package/src/Markdown/parser.ts +244 -29
  348. package/src/MobileNav/MobileNav.tsx +62 -36
  349. package/src/MoreMenu/MoreMenu.doc.mjs +7 -1
  350. package/src/MultiSelector/MultiSelector.doc.mjs +27 -0
  351. package/src/MultiSelector/MultiSelector.test.tsx +106 -0
  352. package/src/MultiSelector/MultiSelector.tsx +50 -6
  353. package/src/MultiSelector/index.ts +1 -0
  354. package/src/NavMenu/NavHeadingMenu.tsx +3 -2
  355. package/src/NumberInput/NumberInput.doc.mjs +5 -0
  356. package/src/Outline/Outline.tsx +3 -2
  357. package/src/OverflowList/OverflowList.tsx +3 -2
  358. package/src/Overlay/Overlay.tsx +3 -2
  359. package/src/Popover/usePopover.tsx +31 -28
  360. package/src/PowerSearch/PowerSearch.doc.mjs +160 -28
  361. package/src/PowerSearch/PowerSearch.test.tsx +240 -1
  362. package/src/PowerSearch/PowerSearch.tsx +34 -5
  363. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +201 -0
  364. package/src/PowerSearch/PowerSearchEditPopover.tsx +29 -2
  365. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +47 -1
  366. package/src/PowerSearch/PowerSearchValueEditor.tsx +15 -0
  367. package/src/PowerSearch/types.ts +2 -0
  368. package/src/PowerSearch/usePowerSearchSource.test.ts +82 -2
  369. package/src/PowerSearch/usePowerSearchSource.ts +13 -3
  370. package/src/Resizable/ResizeHandle.tsx +3 -2
  371. package/src/SegmentedControl/SegmentedControl.tsx +3 -2
  372. package/src/SelectableCard/SelectableCard.tsx +3 -2
  373. package/src/Selector/Selector.doc.mjs +12 -1
  374. package/src/Selector/Selector.test.tsx +45 -1
  375. package/src/Selector/Selector.tsx +11 -1
  376. package/src/SideNav/SideNav.doc.mjs +6 -6
  377. package/src/SideNav/SideNav.tsx +4 -2
  378. package/src/SideNav/SideNavHeading.tsx +10 -10
  379. package/src/SideNav/SideNavItem.tsx +4 -2
  380. package/src/Slider/Slider.tsx +3 -2
  381. package/src/Spinner/Spinner.test.tsx +147 -1
  382. package/src/Spinner/Spinner.tsx +120 -101
  383. package/src/Stepper/Step.doc.mjs +98 -0
  384. package/src/Stepper/Step.tsx +1638 -0
  385. package/src/Stepper/StepStatus.ts +26 -0
  386. package/src/Stepper/Stepper.doc.mjs +370 -0
  387. package/src/Stepper/Stepper.test.tsx +1024 -0
  388. package/src/Stepper/Stepper.tsx +258 -0
  389. package/src/Stepper/StepperContext.ts +71 -0
  390. package/src/Stepper/index.ts +18 -0
  391. package/src/Stepper/stepper.stylex.ts +19 -0
  392. package/src/Switch/Switch.tsx +3 -2
  393. package/src/TabList/Tab.doc.mjs +13 -1
  394. package/src/TabList/Tab.tsx +67 -7
  395. package/src/TabList/TabList.doc.mjs +22 -6
  396. package/src/TabList/TabList.test.tsx +559 -0
  397. package/src/TabList/TabList.tsx +490 -22
  398. package/src/TabList/TabListContext.ts +13 -2
  399. package/src/TabList/TabMenu.tsx +3 -2
  400. package/src/TabList/index.ts +6 -2
  401. package/src/Table/BaseTable.tsx +6 -3
  402. package/src/Table/Table.doc.mjs +6 -1
  403. package/src/Table/Table.test.tsx +132 -14
  404. package/src/Table/TableBody.doc.mjs +40 -0
  405. package/src/Table/TableFooter.doc.mjs +40 -0
  406. package/src/Table/TableHeader.doc.mjs +40 -0
  407. package/src/Table/TableRow.tsx +16 -5
  408. package/src/Table/plugins/groupedRows/useTableGroupedRows-perf.test.tsx +112 -0
  409. package/src/Table/plugins/groupedRows/useTableGroupedRows.test.tsx +100 -0
  410. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +17 -8
  411. package/src/Table/plugins/pagination/useTablePagination.test.tsx +34 -4
  412. package/src/Table/plugins/pagination/useTablePagination.tsx +35 -3
  413. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +9 -2
  414. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +13 -7
  415. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +12 -3
  416. package/src/Table/plugins/selection/useTableSelection.test.tsx +76 -0
  417. package/src/Table/plugins/selection/useTableSelection.tsx +40 -7
  418. package/src/Table/types.ts +22 -4
  419. package/src/Table/useBaseTablePlugins.ts +5 -0
  420. package/src/Table/useTableGroupedRows.doc.mjs +5 -4
  421. package/src/Table/useTablePagination.doc.mjs +7 -0
  422. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  423. package/src/Table/useTableSelection.doc.mjs +31 -0
  424. package/src/Text/Text.test.tsx +23 -1
  425. package/src/Text/Text.tsx +6 -2
  426. package/src/TextArea/TextArea.tsx +3 -2
  427. package/src/TextInput/TextInput.doc.mjs +10 -0
  428. package/src/TextInput/TextInput.tsx +3 -2
  429. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  430. package/src/TimeInput/TimeInput.tsx +2 -2
  431. package/src/Timestamp/Timestamp.tsx +4 -2
  432. package/src/Toast/Toast.doc.mjs +6 -0
  433. package/src/Token/TokenLink.tsx +2 -2
  434. package/src/Tokenizer/Tokenizer.doc.mjs +27 -0
  435. package/src/Tokenizer/Tokenizer.tsx +4 -0
  436. package/src/Toolbar/Toolbar.doc.mjs +5 -0
  437. package/src/Tooltip/Tooltip.doc.mjs +5 -0
  438. package/src/Tooltip/Tooltip.test.tsx +32 -0
  439. package/src/Tooltip/Tooltip.tsx +5 -0
  440. package/src/Tooltip/useTooltip.tsx +47 -24
  441. package/src/TopNav/TopNavHeading.tsx +3 -2
  442. package/src/TopNav/TopNavMegaMenu.tsx +3 -2
  443. package/src/TopNav/TopNavMenu.tsx +4 -3
  444. package/src/Typeahead/BaseTypeahead.doc.mjs +11 -0
  445. package/src/Typeahead/BaseTypeahead.tsx +85 -33
  446. package/src/Typeahead/Typeahead.doc.mjs +5 -0
  447. package/src/Typeahead/Typeahead.test.tsx +49 -0
  448. package/src/Typeahead/Typeahead.tsx +3 -2
  449. package/src/hooks/index.ts +2 -0
  450. package/src/hooks/useContainerReveal.doc.mjs +2 -2
  451. package/src/hooks/useFocusTrap.test.tsx +14 -0
  452. package/src/hooks/useFocusTrap.ts +62 -111
  453. package/src/hooks/useFocusTrapEscapeShim.test.tsx +165 -0
  454. package/src/hooks/useHotkeys.ts +3 -18
  455. package/src/hooks/useMergedRefs.doc.mjs +79 -0
  456. package/src/hooks/useMergedRefs.test.tsx +62 -0
  457. package/src/hooks/useMergedRefs.ts +36 -0
  458. package/src/index.ts +1 -0
  459. package/src/theme/MediaTheme.doc.mjs +5 -5
  460. package/src/utils/groupItems.ts +12 -6
  461. package/src/utils/index.ts +1 -1
  462. package/src/utils/isApplePlatform.test.ts +61 -0
  463. package/src/utils/isApplePlatform.ts +37 -0
  464. package/src/utils/sharedResizeObserver.ts +10 -2
  465. package/src/utils/timeParser.test.ts +6 -1
  466. package/src/utils/timeParser.ts +35 -13
  467. package/src/__tests__/inputWidthContract.test.tsx +0 -69
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  export {TabList} from './TabList';
15
- export type {TabListProps} from './TabList';
15
+ export type {TabListProps, TabListOverflow} from './TabList';
16
16
 
17
17
  export {Tab} from './Tab';
18
18
  export type {TabProps} from './Tab';
@@ -21,4 +21,8 @@ export {TabMenu} from './TabMenu';
21
21
  export type {TabMenuProps, TabMenuOption} from './TabMenu';
22
22
 
23
23
  export {useTabListContext} from './TabListContext';
24
- export type {TabListSize, TabListLayout} from './TabListContext';
24
+ export type {
25
+ TabListSize,
26
+ TabListLayout,
27
+ TabListPattern,
28
+ } from './TabListContext';
@@ -183,9 +183,12 @@ function TableRowInner<T extends Record<string, unknown>>({
183
183
  );
184
184
 
185
185
  const isDefaultRenderer = !col.renderCell;
186
- const rawContent = isDefaultRenderer
187
- ? defaultCellRenderer(item, col.key)
188
- : (col.renderCell?.(item) ?? null);
186
+ let rawContent: ReactNode = null;
187
+ if (!cellRenderProps.isContentSuppressed) {
188
+ rawContent = isDefaultRenderer
189
+ ? defaultCellRenderer(item, col.key)
190
+ : (col.renderCell?.(item) ?? null);
191
+ }
189
192
 
190
193
  // In truncate mode, wrap default-rendered string content in
191
194
  // <Text maxLines={1}> for smart tooltips that only appear
@@ -107,7 +107,7 @@ export const docs = {
107
107
  {
108
108
  name: 'children',
109
109
  type: 'ReactNode',
110
- description: 'Children mode: render TableRow/TableCell directly instead of using data-driven rendering.',
110
+ description: 'Children mode: compose the table yourself from TableHeader / TableBody / TableFooter, each holding TableRow and TableCell, instead of using data-driven rendering. The children are passed straight to the <table>, so the section is yours to supply — a TableRow placed directly in Table emits <table><tr>, which is invalid HTML and mismatches on hydration (the parser inserts an implied <tbody> for server-rendered markup; React does not on the client). Data-driven mode renders the sections for you.',
111
111
  },
112
112
  {
113
113
  name: 'xstyle',
@@ -116,6 +116,9 @@ export const docs = {
116
116
  },
117
117
  ],
118
118
  components: [
119
+ {name: 'TableHeader'},
120
+ {name: 'TableBody'},
121
+ {name: 'TableFooter'},
119
122
  {name: 'TableRow'},
120
123
  {name: 'TableCell'},
121
124
  {name: 'TableHeaderCell'},
@@ -135,6 +138,7 @@ export const docs = {
135
138
  bestPractices: [
136
139
  { guidance: true, description: 'Use density and divider variants to match the information density and scanning needs of your data.' },
137
140
  { guidance: true, description: 'Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.' },
141
+ { guidance: true, description: 'In children mode, put every row inside TableHeader, TableBody, or TableFooter. <table> cannot contain a <tr> directly: the HTML parser inserts an implied <tbody> for server-rendered markup and React does not on the client, so unwrapped rows mismatch on hydration.' },
138
142
  { guidance: true, description: 'Set explicit width on every column using proportional() or pixel(). proportional(1) gives equal flex distribution with a 120px minimum that prevents columns from collapsing on narrow viewports. Omitting width skips the minimum.' },
139
143
  { guidance: true, description: 'Use the data-driven API from React Server Components: proportional(), pixel(), and column definitions without function props are server-safe. Columns using renderCell (or any function prop) need the table wrapped in a "use client" component, since functions cannot cross the server-client boundary.' },
140
144
  { guidance: false, description: 'Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.' },
@@ -183,6 +187,7 @@ export const docsDense = {
183
187
  bestPractices: [
184
188
  { guidance: true, description: 'Use density and divider variants to match the information density and scanning needs of your data.' },
185
189
  { guidance: true, description: 'Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.' },
190
+ { guidance: true, description: 'Children mode: wrap rows in TableHeader/TableBody/TableFooter. <table> cannot hold a <tr> directly — parser adds an implied <tbody> for SSR markup, React does not on the client, so unwrapped rows mismatch on hydration.' },
186
191
  { guidance: true, description: 'Set explicit width on every column via proportional() or pixel(). proportional(1) = equal flex w/ 120px min preventing collapse on narrow viewports. Omitting width skips the minimum.' },
187
192
  { guidance: true, description: 'Data-driven API is RSC-safe: proportional(), pixel(), column defs w/o function props work in Server Components. renderCell (any function prop) requires a "use client" wrapper.' },
188
193
  { guidance: false, description: 'Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.' },
@@ -369,9 +369,11 @@ describe('BaseTable', () => {
369
369
  it('renders children mode instead of data', () => {
370
370
  render(
371
371
  <BaseTable>
372
- <tr>
373
- <td>Manual cell</td>
374
- </tr>
372
+ <tbody>
373
+ <tr>
374
+ <td>Manual cell</td>
375
+ </tr>
376
+ </tbody>
375
377
  </BaseTable>,
376
378
  );
377
379
  expect(screen.getByText('Manual cell')).toBeInTheDocument();
@@ -380,9 +382,11 @@ describe('BaseTable', () => {
380
382
  it('does not render thead in children mode without columns', () => {
381
383
  const {container} = render(
382
384
  <BaseTable>
383
- <tr>
384
- <td>Content</td>
385
- </tr>
385
+ <tbody>
386
+ <tr>
387
+ <td>Content</td>
388
+ </tr>
389
+ </tbody>
386
390
  </BaseTable>,
387
391
  );
388
392
  expect(container.querySelector('thead')).toBeNull();
@@ -602,6 +606,41 @@ describe('BaseTable', () => {
602
606
  expect(calls[0]).toEqual({col: 'name', name: 'Alice'});
603
607
  });
604
608
 
609
+ it('isContentSuppressed renders an empty cell and never calls the column renderer', () => {
610
+ const rendered: string[] = [];
611
+ const withRenderer: TableColumn<User>[] = [
612
+ {
613
+ key: 'name',
614
+ header: 'Name',
615
+ renderCell: item => {
616
+ rendered.push(item.name);
617
+ return <b>{item.name}</b>;
618
+ },
619
+ },
620
+ ];
621
+ const plugin: TablePlugin<User> = {
622
+ transformBodyCell: (props, _column, item) =>
623
+ item.name === 'Bob' ? {...props, isContentSuppressed: true} : props,
624
+ };
625
+ render(
626
+ <BaseTable data={users} columns={withRenderer} plugins={[plugin]} />,
627
+ );
628
+ expect(rendered).toEqual(['Alice', 'Charlie']);
629
+ const cells = screen.getAllByRole('cell');
630
+ expect(cells[1]).toBeEmptyDOMElement();
631
+ expect(cells[0]).toHaveTextContent('Alice');
632
+ });
633
+
634
+ it('isContentSuppressed also suppresses the default renderer', () => {
635
+ const plugin: TablePlugin<User> = {
636
+ transformBodyCell: props => ({...props, isContentSuppressed: true}),
637
+ };
638
+ render(<BaseTable data={users} columns={columns} plugins={[plugin]} />);
639
+ for (const cell of screen.getAllByRole('cell')) {
640
+ expect(cell).toBeEmptyDOMElement();
641
+ }
642
+ });
643
+
605
644
  it('composes multiple plugins sequentially', () => {
606
645
  const plugin1: TablePlugin<User> = {
607
646
  transformTable: props => ({
@@ -988,14 +1027,16 @@ describe('Table', () => {
988
1027
  it('renders children mode with TableRow and TableCell', () => {
989
1028
  render(
990
1029
  <Table density="balanced" dividers="rows">
991
- <TableRow>
992
- <TableCell>Streamed A</TableCell>
993
- <TableCell>Streamed B</TableCell>
994
- </TableRow>
995
- <TableRow>
996
- <TableCell>Streamed C</TableCell>
997
- <TableCell>Streamed D</TableCell>
998
- </TableRow>
1030
+ <TableBody>
1031
+ <TableRow>
1032
+ <TableCell>Streamed A</TableCell>
1033
+ <TableCell>Streamed B</TableCell>
1034
+ </TableRow>
1035
+ <TableRow>
1036
+ <TableCell>Streamed C</TableCell>
1037
+ <TableCell>Streamed D</TableCell>
1038
+ </TableRow>
1039
+ </TableBody>
999
1040
  </Table>,
1000
1041
  );
1001
1042
  expect(screen.getByText('Streamed A')).toBeInTheDocument();
@@ -1004,6 +1045,83 @@ describe('Table', () => {
1004
1045
  expect(screen.getAllByRole('cell')).toHaveLength(4);
1005
1046
  });
1006
1047
 
1048
+ // Children mode hands the children straight to <table> — it does not wrap
1049
+ // them the way the data-driven path does. That is deliberate (it is what
1050
+ // makes the section components composable), but it was never written down,
1051
+ // and a caller who skipped the section shipped `<table><tr>` (#5277).
1052
+ it("children mode renders no tbody of its own — the section is the caller's", () => {
1053
+ const {container} = render(
1054
+ <Table density="balanced">
1055
+ {/* The unwrapped shape is the subject of this test, not a mistake. */}
1056
+ {/* eslint-disable-next-line @astryx/require-table-section */}
1057
+ <TableRow>
1058
+ <TableCell>Unwrapped</TableCell>
1059
+ </TableRow>
1060
+ </Table>,
1061
+ );
1062
+ const table = container.querySelector('table');
1063
+ expect(table?.querySelector('tbody')).toBeNull();
1064
+ expect(Array.from(table?.children ?? []).map(el => el.tagName)).toContain(
1065
+ 'TR',
1066
+ );
1067
+ });
1068
+
1069
+ // Columns do not bring a thead with them in children mode — BaseTable's
1070
+ // `children ? children : <>...</>` short-circuits the whole data-driven
1071
+ // branch, header included. The header is TableHeader's job here.
1072
+ it('children mode renders no thead even when columns are supplied', () => {
1073
+ const {container} = render(
1074
+ <Table columns={columns}>
1075
+ <TableBody>
1076
+ <TableRow>
1077
+ <TableCell>Only body</TableCell>
1078
+ </TableRow>
1079
+ </TableBody>
1080
+ </Table>,
1081
+ );
1082
+ expect(container.querySelector('thead')).toBeNull();
1083
+ expect(container.querySelector('tbody > tr')).not.toBeNull();
1084
+ });
1085
+
1086
+ it('children mode renders the thead TableHeader supplies', () => {
1087
+ const {container} = render(
1088
+ <Table columns={columns}>
1089
+ <TableHeader>
1090
+ <TableRow isHeaderRow>
1091
+ <TableCell>H</TableCell>
1092
+ </TableRow>
1093
+ </TableHeader>
1094
+ <TableBody>
1095
+ <TableRow>
1096
+ <TableCell>B</TableCell>
1097
+ </TableRow>
1098
+ </TableBody>
1099
+ </Table>,
1100
+ );
1101
+ const table = container.querySelector('table');
1102
+ expect(Array.from(table?.children ?? []).map(el => el.tagName)).toEqual([
1103
+ 'THEAD',
1104
+ 'TBODY',
1105
+ ]);
1106
+ });
1107
+
1108
+ it('children mode puts the rows in the tbody TableBody renders', () => {
1109
+ const {container} = render(
1110
+ <Table density="balanced">
1111
+ <TableBody>
1112
+ <TableRow>
1113
+ <TableCell>Wrapped</TableCell>
1114
+ </TableRow>
1115
+ </TableBody>
1116
+ </Table>,
1117
+ );
1118
+ const table = container.querySelector('table');
1119
+ expect(
1120
+ Array.from(table?.children ?? []).map(el => el.tagName),
1121
+ ).not.toContain('TR');
1122
+ expect(table?.querySelector('tbody > tr')).not.toBeNull();
1123
+ });
1124
+
1007
1125
  it('passes through idKey string to base table', () => {
1008
1126
  render(<Table data={users} columns={columns} idKey="email" />);
1009
1127
  expect(screen.getAllByRole('row')).toHaveLength(4);
@@ -0,0 +1,40 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
+
5
+ export const docs = {
6
+ name: 'TableBody',
7
+ subComponentOf: 'Table',
8
+ displayName: 'Table Body',
9
+ isHiddenFromOverview: true,
10
+ description:
11
+ '<tbody> wrapper for children mode. Holds the data rows. A row must sit inside a section — <table> cannot contain a <tr> directly, because the HTML parser inserts an implied <tbody> when it parses server-rendered markup and React does not when it renders on the client, so rows written straight into Table mismatch on hydration. The data-driven data={...} mode renders this element itself; in children mode it is yours to supply.',
12
+ props: [
13
+ {
14
+ name: 'children',
15
+ type: 'ReactNode',
16
+ description: 'The <tbody> rows.',
17
+ required: true,
18
+ },
19
+ {
20
+ name: 'xstyle',
21
+ type: 'StyleXStyles',
22
+ description:
23
+ 'StyleX styles for layout customization. Must be a stylex.create() value: not an inline style object like style={{}}.',
24
+ },
25
+ ],
26
+ theming: {
27
+ targets: [{className: 'astryx-table-body'}],
28
+ },
29
+ };
30
+
31
+ export const docsDense = {
32
+ name: 'TableBody',
33
+ isHiddenFromOverview: true,
34
+ displayName: 'Table Body',
35
+ description: '<tbody> wrapper for children mode; holds the data rows — children mode does not add one for you',
36
+ propDescriptions: {
37
+ children: 'The <tbody> rows.',
38
+ xstyle: 'StyleX styles for layout customization',
39
+ },
40
+ };
@@ -0,0 +1,40 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
+
5
+ export const docs = {
6
+ name: 'TableFooter',
7
+ subComponentOf: 'Table',
8
+ displayName: 'Table Footer',
9
+ isHiddenFromOverview: true,
10
+ description:
11
+ '<tfoot> wrapper for children mode. Holds summary or total rows beneath the body. A row must sit inside a section — <table> cannot contain a <tr> directly, because the HTML parser inserts an implied <tbody> when it parses server-rendered markup and React does not when it renders on the client, so the two trees mismatch on hydration. Children mode only: the data-driven data={...} path renders a header and a body, never a footer.',
12
+ props: [
13
+ {
14
+ name: 'children',
15
+ type: 'ReactNode',
16
+ description: 'The <tfoot> rows.',
17
+ required: true,
18
+ },
19
+ {
20
+ name: 'xstyle',
21
+ type: 'StyleXStyles',
22
+ description:
23
+ 'StyleX styles for layout customization. Must be a stylex.create() value: not an inline style object like style={{}}.',
24
+ },
25
+ ],
26
+ theming: {
27
+ targets: [{className: 'astryx-table-footer'}],
28
+ },
29
+ };
30
+
31
+ export const docsDense = {
32
+ name: 'TableFooter',
33
+ isHiddenFromOverview: true,
34
+ displayName: 'Table Footer',
35
+ description: '<tfoot> wrapper for children mode; holds summary rows',
36
+ propDescriptions: {
37
+ children: 'The <tfoot> rows.',
38
+ xstyle: 'StyleX styles for layout customization',
39
+ },
40
+ };
@@ -0,0 +1,40 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
+
5
+ export const docs = {
6
+ name: 'TableHeader',
7
+ subComponentOf: 'Table',
8
+ displayName: 'Table Header',
9
+ isHiddenFromOverview: true,
10
+ description:
11
+ '<thead> wrapper for children mode. Holds the header row, whose cells are TableHeaderCell. A row must sit inside a section — <table> cannot contain a <tr> directly, because the HTML parser inserts an implied <tbody> when it parses server-rendered markup and React does not when it renders on the client, so the two trees mismatch on hydration. The data-driven data={...} mode renders this element itself whenever columns are supplied; in children mode it is yours to supply.',
12
+ props: [
13
+ {
14
+ name: 'children',
15
+ type: 'ReactNode',
16
+ description: 'The <thead> rows.',
17
+ required: true,
18
+ },
19
+ {
20
+ name: 'xstyle',
21
+ type: 'StyleXStyles',
22
+ description:
23
+ 'StyleX styles for layout customization. Must be a stylex.create() value: not an inline style object like style={{}}.',
24
+ },
25
+ ],
26
+ theming: {
27
+ targets: [{className: 'astryx-table-header'}],
28
+ },
29
+ };
30
+
31
+ export const docsDense = {
32
+ name: 'TableHeader',
33
+ isHiddenFromOverview: true,
34
+ displayName: 'Table Header',
35
+ description: '<thead> wrapper for children mode; holds the header row',
36
+ propDescriptions: {
37
+ children: 'The <thead> rows.',
38
+ xstyle: 'StyleX styles for layout customization',
39
+ },
40
+ };
@@ -6,7 +6,8 @@
6
6
  * @file TableRow.tsx
7
7
  * @input React, StyleX, TableContext, theme tokens
8
8
  * @output Exports TableRow component, TableRowProps
9
- * @position Sub-component; used inside Table children mode
9
+ * @position Sub-component; used inside a Table section (TableHeader /
10
+ * TableBody / TableFooter) in children mode
10
11
  *
11
12
  * SYNC: When modified, update:
12
13
  * - /packages/core/src/Table/Table.doc.mjs
@@ -105,13 +106,23 @@ const stripedHoverRowStyles = stylex.create({
105
106
  * When used inside `<Table>`, inherits styling from the table context
106
107
  * (striped, hover, divider overrides). When used standalone, renders a plain `<tr>`.
107
108
  *
109
+ * Rows go inside a section — `<TableBody>`, `<TableHeader>`, or
110
+ * `<TableFooter>`. `<table>` cannot contain a `<tr>` directly: the HTML parser
111
+ * inserts an implied `<tbody>` when it parses server-rendered markup and React
112
+ * does not when it renders on the client, so the two trees mismatch on
113
+ * hydration. Children mode passes children straight through, so the section is
114
+ * the caller's to supply — the data-driven `data={...}` mode renders it for
115
+ * you.
116
+ *
108
117
  * @example
109
118
  * ```
110
119
  * <Table>
111
- * <TableRow>
112
- * <TableCell>Alice</TableCell>
113
- * <TableCell>30</TableCell>
114
- * </TableRow>
120
+ * <TableBody>
121
+ * <TableRow>
122
+ * <TableCell>Alice</TableCell>
123
+ * <TableCell>30</TableCell>
124
+ * </TableRow>
125
+ * </TableBody>
115
126
  * </Table>
116
127
  * ```
117
128
  */
@@ -0,0 +1,112 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useTableGroupedRows-perf.test.tsx
5
+ * @input Table, useTableGroupedRows, React testing utilities
6
+ * @output Performance tests for grouped-rows render behavior
7
+ * @position Test file; validates that grouping costs no extra row renders
8
+ *
9
+ * The plugin keeps group headers out of cell renderers per cell, at render
10
+ * time. Doing it by rewriting the columns instead would hand BaseTable a new
11
+ * column object on every render, and its element-by-element check on the
12
+ * resolved column array is what stops every row re-rendering.
13
+ */
14
+
15
+ import {describe, it, expect} from 'vitest';
16
+ import {render, screen, act} from '@testing-library/react';
17
+ import {useCallback, useMemo, useState} from 'react';
18
+ import userEvent from '@testing-library/user-event';
19
+ import {Table} from '../../Table';
20
+ import type {TableColumn} from '../../types';
21
+ import {useTableGroupedRows} from './useTableGroupedRows';
22
+
23
+ interface Person extends Record<string, unknown> {
24
+ id: string;
25
+ name: string;
26
+ team: string;
27
+ }
28
+
29
+ const people: Person[] = [
30
+ {id: 'a', name: 'Alice', team: 'Core'},
31
+ {id: 'b', name: 'Bob', team: 'Core'},
32
+ {id: 'c', name: 'Carol', team: 'Infra'},
33
+ ];
34
+
35
+ const EMPTY = new Set<string>();
36
+
37
+ function GroupedRenderCountTable({
38
+ renderCounts,
39
+ }: {
40
+ renderCounts: Record<string, number>;
41
+ }) {
42
+ const [tick, setTick] = useState(0);
43
+ const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(EMPTY);
44
+
45
+ const columns = useMemo<TableColumn<Person>[]>(
46
+ () => [
47
+ {
48
+ key: 'name',
49
+ header: 'Name',
50
+ renderCell: (item: Person) => {
51
+ renderCounts[item.id] = (renderCounts[item.id] ?? 0) + 1;
52
+ return item.name;
53
+ },
54
+ },
55
+ ],
56
+ [renderCounts],
57
+ );
58
+
59
+ const grouped = useTableGroupedRows<Person>({
60
+ data: people,
61
+ groupBy: useCallback((p: Person) => p.team, []),
62
+ collapsedGroups,
63
+ onToggleGroup: useCallback(
64
+ (key: string) =>
65
+ setCollapsedGroups(prev => {
66
+ const next = new Set(prev);
67
+ if (!next.delete(key)) {
68
+ next.add(key);
69
+ }
70
+ return next;
71
+ }),
72
+ [],
73
+ ),
74
+ getRowKey: useCallback((p: Person) => p.id, []),
75
+ });
76
+
77
+ return (
78
+ <>
79
+ <button type="button" onClick={() => setTick(t => t + 1)}>
80
+ bump {tick}
81
+ </button>
82
+ <Table
83
+ data={grouped.data}
84
+ columns={columns}
85
+ idKey={grouped.idKey}
86
+ plugins={{grouped: grouped.plugin}}
87
+ />
88
+ </>
89
+ );
90
+ }
91
+
92
+ describe('Grouped rows render performance', () => {
93
+ it('renders each real row once and no group header at all', () => {
94
+ const renderCounts: Record<string, number> = {};
95
+ render(<GroupedRenderCountTable renderCounts={renderCounts} />);
96
+ expect(renderCounts).toEqual({a: 1, b: 1, c: 1});
97
+ });
98
+
99
+ it('a re-render of the surrounding component re-renders no rows', async () => {
100
+ const user = userEvent.setup();
101
+ const renderCounts: Record<string, number> = {};
102
+ render(<GroupedRenderCountTable renderCounts={renderCounts} />);
103
+ const before = {...renderCounts};
104
+
105
+ await act(async () => {
106
+ await user.click(screen.getByRole('button', {name: /bump/}));
107
+ });
108
+
109
+ expect(screen.getByRole('button', {name: 'bump 1'})).toBeInTheDocument();
110
+ expect(renderCounts).toEqual(before);
111
+ });
112
+ });
@@ -5,6 +5,7 @@ import {render, screen, fireEvent, within} from '@testing-library/react';
5
5
  import {useState, useCallback} from 'react';
6
6
  import {Table} from '../../Table';
7
7
  import type {TableColumn} from '../../types';
8
+ import {useTableRowStatus, type TableRowStatus} from '../rowStatus';
8
9
  import {useTableGroupedRows} from './useTableGroupedRows';
9
10
 
10
11
  interface Person extends Record<string, unknown> {
@@ -175,6 +176,105 @@ describe('useTableGroupedRows', () => {
175
176
  ).not.toBeInTheDocument();
176
177
  });
177
178
 
179
+ it('never runs a column renderCell against a group header row', () => {
180
+ // The renderer that breaks: it keys a lookup off a field rather than
181
+ // printing it, so the header Proxy answering `''` is no better than
182
+ // `undefined`.
183
+ const TEAM_META: Record<string, {dot: string}> = {
184
+ Core: {dot: 'green'},
185
+ Infra: {dot: 'blue'},
186
+ };
187
+ const seen: Person[] = [];
188
+ const lookupColumns: TableColumn<Person>[] = [
189
+ {
190
+ key: 'team',
191
+ header: 'Team',
192
+ renderCell: item => {
193
+ seen.push(item);
194
+ return <span>{TEAM_META[item.team].dot}</span>;
195
+ },
196
+ },
197
+ ];
198
+
199
+ function LookupHarness() {
200
+ const [collapsed, setCollapsed] = useState<Set<string>>(EMPTY);
201
+ const grouped = useTableGroupedRows<Person>({
202
+ data: people,
203
+ groupBy: p => p.team,
204
+ collapsedGroups: collapsed,
205
+ onToggleGroup: k => setCollapsed(new Set([k])),
206
+ getRowKey: p => p.id,
207
+ });
208
+ return (
209
+ <Table
210
+ data={grouped.data}
211
+ columns={lookupColumns}
212
+ idKey={grouped.idKey}
213
+ plugins={{grouped: grouped.plugin}}
214
+ />
215
+ );
216
+ }
217
+
218
+ expect(() => render(<LookupHarness />)).not.toThrow();
219
+ expect(seen.map(p => p.name)).toEqual(['Alice', 'Bob', 'Carol']);
220
+ expect(screen.getByText('Core')).toBeInTheDocument();
221
+ expect(screen.getByText('Infra')).toBeInTheDocument();
222
+ });
223
+
224
+ describe.each([
225
+ ['grouped first', true],
226
+ ['rowStatus first', false],
227
+ ])(
228
+ 'with a column contributed by another plugin (%s)',
229
+ (_label, groupedFirst) => {
230
+ // `grouped` and `rowStatus` are both absent from PLUGIN_ORDER, so the
231
+ // record's insertion order is the pipeline order. Whichever way round
232
+ // the consumer types it, the status column's renderCell must not see a
233
+ // header row.
234
+ const STATUS_BY_TEAM: Record<string, TableRowStatus> = {
235
+ Core: {color: 'success', label: 'Healthy'},
236
+ Infra: {color: 'error', label: 'Paging'},
237
+ };
238
+
239
+ function StatusHarness() {
240
+ const [collapsed, setCollapsed] = useState<Set<string>>(EMPTY);
241
+ const grouped = useTableGroupedRows<Person>({
242
+ data: people,
243
+ groupBy: p => p.team,
244
+ collapsedGroups: collapsed,
245
+ onToggleGroup: k => setCollapsed(new Set([k])),
246
+ getRowKey: p => p.id,
247
+ });
248
+ const rowStatus = useTableRowStatus<Person>({
249
+ getStatus: useCallback((item: Person) => {
250
+ const {color, label} = STATUS_BY_TEAM[item.team];
251
+ return {color, label};
252
+ }, []),
253
+ });
254
+ return (
255
+ <Table
256
+ data={grouped.data}
257
+ columns={columns}
258
+ idKey={grouped.idKey}
259
+ plugins={
260
+ groupedFirst
261
+ ? {grouped: grouped.plugin, rowStatus}
262
+ : {rowStatus, grouped: grouped.plugin}
263
+ }
264
+ />
265
+ );
266
+ }
267
+
268
+ it('renders the group headers without throwing', () => {
269
+ expect(() => render(<StatusHarness />)).not.toThrow();
270
+ expect(screen.getByText('Core')).toBeInTheDocument();
271
+ expect(screen.getByText('Infra')).toBeInTheDocument();
272
+ // One indicator per real row, none on the two headers.
273
+ expect(screen.getAllByRole('img')).toHaveLength(3);
274
+ });
275
+ },
276
+ );
277
+
178
278
  it('keeps a group collapsed across a data change (state keyed by group)', () => {
179
279
  function ChangingHarness() {
180
280
  const [collapsed, setCollapsed] = useState<Set<string>>(