@astryxdesign/core 0.7.2-canary.fe2c655 → 0.7.2

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 (297) hide show
  1. package/CHANGELOG.md +156 -2
  2. package/README.md +4 -4
  3. package/dist/AppShell/AppShell.d.ts.map +1 -1
  4. package/dist/AppShell/AppShell.js +40 -1
  5. package/dist/AspectRatio/AspectRatio.d.ts +6 -8
  6. package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
  7. package/dist/AspectRatio/AspectRatio.js +44 -3
  8. package/dist/Avatar/Avatar.d.ts.map +1 -1
  9. package/dist/Avatar/Avatar.js +8 -1
  10. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  11. package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
  12. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  13. package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
  14. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  15. package/dist/Chat/ChatLayoutScrollButton.js +1 -0
  16. package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
  17. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  18. package/dist/CheckboxInput/CheckboxInput.js +3 -1
  19. package/dist/CheckboxList/CheckboxList.d.ts +1 -1
  20. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  21. package/dist/CheckboxList/CheckboxList.js +6 -2
  22. package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
  23. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  24. package/dist/CheckboxList/CheckboxListItem.js +4 -2
  25. package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
  26. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  27. package/dist/CommandPalette/CommandPaletteInput.js +37 -5
  28. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  29. package/dist/ContextMenu/ContextMenu.js +5 -2
  30. package/dist/Dialog/Dialog.d.ts.map +1 -1
  31. package/dist/Dialog/Dialog.js +20 -8
  32. package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
  33. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  34. package/dist/DropdownMenu/DropdownMenu.js +84 -113
  35. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -1
  36. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
  37. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  38. package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
  39. package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
  40. package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
  41. package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
  42. package/dist/DropdownMenu/DropdownMenuGroup.d.ts +6 -4
  43. package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
  44. package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
  45. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
  46. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  47. package/dist/DropdownMenu/DropdownMenuSubMenu.js +15 -4
  48. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
  49. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
  50. package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
  51. package/dist/DropdownMenu/menuRegionStops.d.ts +38 -0
  52. package/dist/DropdownMenu/menuRegionStops.d.ts.map +1 -0
  53. package/dist/DropdownMenu/menuRegionStops.js +131 -0
  54. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  55. package/dist/EmptyState/EmptyState.js +4 -10
  56. package/dist/Field/Field.d.ts +1 -1
  57. package/dist/Field/Field.d.ts.map +1 -1
  58. package/dist/Field/Field.js +8 -4
  59. package/dist/FormLayout/FormLayout.d.ts +5 -4
  60. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  61. package/dist/FormLayout/FormLayout.js +1 -1
  62. package/dist/FormLayout/FormLayoutContext.d.ts +3 -3
  63. package/dist/FormLayout/FormLayoutContext.js +3 -3
  64. package/dist/Grid/Grid.d.ts +8 -7
  65. package/dist/Grid/Grid.d.ts.map +1 -1
  66. package/dist/Grid/Grid.js +5 -4
  67. package/dist/Grid/GridSpan.d.ts +1 -1
  68. package/dist/Grid/GridSpan.d.ts.map +1 -1
  69. package/dist/Grid/GridSpan.js +1 -0
  70. package/dist/Heading/Heading.d.ts.map +1 -1
  71. package/dist/Heading/Heading.js +4 -2
  72. package/dist/Item/Item.d.ts +6 -2
  73. package/dist/Item/Item.d.ts.map +1 -1
  74. package/dist/Item/Item.js +19 -4
  75. package/dist/List/List.d.ts +46 -19
  76. package/dist/List/List.d.ts.map +1 -1
  77. package/dist/List/List.js +28 -10
  78. package/dist/List/ListContext.d.ts +8 -3
  79. package/dist/List/ListContext.d.ts.map +1 -1
  80. package/dist/List/ListContext.js +7 -2
  81. package/dist/List/ListItem.d.ts +13 -5
  82. package/dist/List/ListItem.d.ts.map +1 -1
  83. package/dist/List/ListItem.js +21 -8
  84. package/dist/List/index.d.ts +1 -1
  85. package/dist/List/index.d.ts.map +1 -1
  86. package/dist/Markdown/Markdown.d.ts.map +1 -1
  87. package/dist/Markdown/Markdown.js +8 -1
  88. package/dist/Markdown/parser.d.ts.map +1 -1
  89. package/dist/Markdown/parser.js +8 -2
  90. package/dist/Markdown/plugins/headingLinks.d.ts.map +1 -1
  91. package/dist/Markdown/plugins/headingLinks.js +9 -1
  92. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  93. package/dist/MetadataList/MetadataList.js +5 -1
  94. package/dist/MetadataList/MetadataListContext.d.ts +1 -1
  95. package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
  96. package/dist/MobileNav/MobileNav.d.ts +9 -0
  97. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  98. package/dist/MobileNav/MobileNav.js +30 -3
  99. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  100. package/dist/PowerSearch/PowerSearch.js +5 -0
  101. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  102. package/dist/PowerSearch/PowerSearchValueEditor.js +45 -11
  103. package/dist/RadioList/RadioList.d.ts +1 -1
  104. package/dist/RadioList/RadioList.d.ts.map +1 -1
  105. package/dist/RadioList/RadioList.js +6 -2
  106. package/dist/RadioList/RadioListItem.d.ts +9 -1
  107. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  108. package/dist/RadioList/RadioListItem.js +2 -0
  109. package/dist/SideNav/SideNavItem.d.ts +8 -1
  110. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  111. package/dist/SideNav/SideNavItem.js +76 -10
  112. package/dist/Text/Text.d.ts.map +1 -1
  113. package/dist/Text/Text.js +4 -2
  114. package/dist/ToggleButton/ToggleButton.d.ts +3 -2
  115. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  116. package/dist/ToggleButton/ToggleButton.js +12 -1
  117. package/dist/Token/Token.js +1 -1
  118. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  119. package/dist/TopNav/TopNavMegaMenu.js +5 -3
  120. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  121. package/dist/TopNav/TopNavMenu.js +5 -3
  122. package/dist/TreeList/TreeList.d.ts.map +1 -1
  123. package/dist/TreeList/TreeList.js +1 -0
  124. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  125. package/dist/TreeList/TreeListBranches.js +2 -2
  126. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  127. package/dist/TreeList/TreeListItem.js +57 -9
  128. package/dist/astryx.css +27 -4
  129. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  130. package/dist/hooks/useMenuHover.js +18 -13
  131. package/dist/reset.a11y.chromium.spec.js +50 -1
  132. package/dist/utils/plainDate.d.ts.map +1 -1
  133. package/dist/utils/plainDate.js +15 -9
  134. package/docs.mjs +4 -4
  135. package/package.json +4 -4
  136. package/scripts/agent-doc-state.mjs +66 -6
  137. package/scripts/postinstall.mjs +44 -3
  138. package/src/AppShell/AppShell.tsx +45 -1
  139. package/src/AspectRatio/AspectRatio.test.tsx +44 -43
  140. package/src/AspectRatio/AspectRatio.tsx +38 -18
  141. package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
  142. package/src/Avatar/Avatar.doc.mjs +5 -3
  143. package/src/Avatar/Avatar.tsx +11 -2
  144. package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
  145. package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +24 -1
  146. package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -0
  147. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  148. package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
  149. package/src/Card/Card.doc.mjs +1 -1
  150. package/src/Chat/ChatLayoutScrollButton.test.tsx +13 -0
  151. package/src/Chat/ChatLayoutScrollButton.tsx +4 -2
  152. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +64 -19
  153. package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
  154. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  155. package/src/CheckboxInput/CheckboxInput.tsx +3 -1
  156. package/src/CheckboxList/CheckboxList.fr10.test.tsx +29 -2
  157. package/src/CheckboxList/CheckboxList.test.tsx +22 -0
  158. package/src/CheckboxList/CheckboxList.tsx +4 -1
  159. package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
  160. package/src/CheckboxList/CheckboxListItem.tsx +10 -0
  161. package/src/Code/Code.doc.mjs +1 -0
  162. package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
  163. package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
  164. package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
  165. package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
  166. package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
  167. package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
  168. package/src/ContextMenu/ContextMenu.test.tsx +12 -2
  169. package/src/ContextMenu/ContextMenu.tsx +4 -1
  170. package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
  171. package/src/Dialog/Dialog.test.tsx +46 -0
  172. package/src/Dialog/Dialog.tsx +20 -7
  173. package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
  174. package/src/Drawer/Drawer.doc.mjs +1 -0
  175. package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
  176. package/src/DropdownMenu/DropdownMenu.test.tsx +79 -6
  177. package/src/DropdownMenu/DropdownMenu.tsx +88 -144
  178. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +1 -1
  179. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -1
  180. package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
  181. package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
  182. package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
  183. package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
  184. package/src/DropdownMenu/DropdownMenuGroup.doc.mjs +169 -10
  185. package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
  186. package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
  187. package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +1 -1
  188. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
  189. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
  190. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +17 -3
  191. package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
  192. package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
  193. package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
  194. package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
  195. package/src/DropdownMenu/menuRegionStops.ts +167 -0
  196. package/src/EmptyState/EmptyState.doc.mjs +3 -2
  197. package/src/EmptyState/EmptyState.spec.md +189 -0
  198. package/src/EmptyState/EmptyState.test.tsx +74 -3
  199. package/src/EmptyState/EmptyState.tsx +4 -8
  200. package/src/Field/Field.doc.mjs +14 -8
  201. package/src/Field/Field.spec.md +217 -0
  202. package/src/Field/Field.test.tsx +60 -2
  203. package/src/Field/Field.tsx +17 -8
  204. package/src/Field/FieldLabel.doc.mjs +133 -26
  205. package/src/Field/FieldLabel.spec.md +172 -0
  206. package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
  207. package/src/FormLayout/FormLayout.doc.mjs +6 -3
  208. package/src/FormLayout/FormLayout.spec.md +192 -0
  209. package/src/FormLayout/FormLayout.test.tsx +15 -0
  210. package/src/FormLayout/FormLayout.tsx +7 -6
  211. package/src/FormLayout/FormLayoutContext.ts +3 -3
  212. package/src/Grid/Grid.doc.mjs +7 -1
  213. package/src/Grid/Grid.spec.md +93 -113
  214. package/src/Grid/Grid.test.tsx +22 -0
  215. package/src/Grid/Grid.tsx +10 -9
  216. package/src/Grid/GridSpan.doc.mjs +3 -3
  217. package/src/Grid/GridSpan.tsx +4 -1
  218. package/src/Heading/Heading.test.tsx +57 -0
  219. package/src/Heading/Heading.tsx +6 -2
  220. package/src/Icon/Icon.doc.mjs +36 -0
  221. package/src/Indicator/Indicator.doc.mjs +3 -3
  222. package/src/Item/Item.doc.mjs +12 -8
  223. package/src/Item/Item.test.tsx +61 -0
  224. package/src/Item/Item.tsx +40 -4
  225. package/src/Layout/LayoutHeader.doc.mjs +5 -0
  226. package/src/List/List.doc.mjs +14 -1
  227. package/src/List/List.spec.md +129 -77
  228. package/src/List/List.test.tsx +163 -94
  229. package/src/List/List.tsx +74 -18
  230. package/src/List/ListContext.tsx +9 -3
  231. package/src/List/ListItem.doc.mjs +16 -0
  232. package/src/List/ListItem.tsx +56 -27
  233. package/src/List/index.ts +1 -1
  234. package/src/Markdown/Markdown.tsx +14 -1
  235. package/src/Markdown/parser.ts +8 -2
  236. package/src/Markdown/plugins/headingLinks.ts +9 -1
  237. package/src/MetadataList/MetadataList.doc.mjs +3 -3
  238. package/src/MetadataList/MetadataList.test.tsx +38 -0
  239. package/src/MetadataList/MetadataList.tsx +8 -1
  240. package/src/MetadataList/MetadataListContext.tsx +1 -1
  241. package/src/MobileNav/MobileNav.tsx +30 -3
  242. package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
  243. package/src/MoreMenu/MoreMenu.test.tsx +5 -1
  244. package/src/PowerSearch/PowerSearch.test.tsx +62 -0
  245. package/src/PowerSearch/PowerSearch.tsx +9 -0
  246. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +71 -0
  247. package/src/PowerSearch/PowerSearchValueEditor.tsx +45 -0
  248. package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
  249. package/src/RadioList/RadioList.fr10.test.tsx +45 -2
  250. package/src/RadioList/RadioList.test.tsx +22 -0
  251. package/src/RadioList/RadioList.tsx +4 -1
  252. package/src/RadioList/RadioListItem.doc.mjs +16 -0
  253. package/src/RadioList/RadioListItem.tsx +10 -1
  254. package/src/Resizable/useResizable.doc.mjs +2 -0
  255. package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
  256. package/src/SideNav/SideNav.spec.md +227 -0
  257. package/src/SideNav/SideNav.test.tsx +220 -0
  258. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  259. package/src/SideNav/SideNavItem.tsx +99 -17
  260. package/src/Slider/Slider.spec.md +39 -18
  261. package/src/Spinner/Spinner.doc.mjs +1 -1
  262. package/src/TabList/TabList.spec.md +57 -44
  263. package/src/Table/Table.spec.md +6 -1
  264. package/src/Table/plugins/activation/useTableActivation.spec.md +254 -0
  265. package/src/Text/Text.test.tsx +57 -0
  266. package/src/Text/Text.tsx +6 -2
  267. package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
  268. package/src/Timestamp/Timestamp.doc.mjs +1 -1
  269. package/src/ToggleButton/ToggleButton.test.tsx +95 -0
  270. package/src/ToggleButton/ToggleButton.tsx +12 -1
  271. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
  272. package/src/Token/Token.tsx +1 -1
  273. package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +86 -0
  274. package/src/Toolbar/Toolbar.test.tsx +0 -8
  275. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
  276. package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
  277. package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
  278. package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
  279. package/src/TopNav/TopNavMegaMenu.test.tsx +47 -0
  280. package/src/TopNav/TopNavMegaMenu.tsx +9 -3
  281. package/src/TopNav/TopNavMenu.test.tsx +46 -1
  282. package/src/TopNav/TopNavMenu.tsx +13 -3
  283. package/src/TreeList/TreeList.doc.mjs +1 -0
  284. package/src/TreeList/TreeList.test.tsx +327 -6
  285. package/src/TreeList/TreeList.tsx +5 -1
  286. package/src/TreeList/TreeListBranches.tsx +6 -0
  287. package/src/TreeList/TreeListItem.tsx +88 -13
  288. package/src/docPropLiterals.test.ts +36 -10
  289. package/src/hooks/useMenuHover.ts +18 -13
  290. package/src/postinstall.test.mjs +79 -0
  291. package/src/reset.a11y.chromium.spec.ts +52 -1
  292. package/src/reset.css +9 -29
  293. package/src/reset.test.ts +11 -0
  294. package/src/theme/derivedVarRegistry.test.ts +7 -0
  295. package/src/unicodePropertyEscapes.source-build.test.mjs +69 -0
  296. package/src/utils/plainDate.test.ts +37 -1
  297. package/src/utils/plainDate.ts +15 -5
@@ -0,0 +1,254 @@
1
+ ---
2
+ schema_version: 3
3
+ template_version: 4
4
+ kind: module
5
+ id: module:Table/useTableActivation
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers:
13
+ [public-api, behavior, layout, theming, accessibility, interaction]
14
+ verified_by:
15
+ [
16
+ packages/core/src/Table/plugins/activation/useTableActivation.test.tsx,
17
+ packages/core/src/Table/__tests__/TableActivation.a11y.chromium.spec.ts,
18
+ packages/core/src/Table/Table.perf.test.tsx,
19
+ scripts/check-knowledge.mjs,
20
+ ]
21
+ parent_component: component:Table
22
+ references:
23
+ [
24
+ architecture:component-theming-surface,
25
+ architecture:interaction-modality,
26
+ architecture:public-component-api,
27
+ spec:AST-002,
28
+ spec:AST-009,
29
+ ]
30
+ ---
31
+
32
+ # useTableActivation module contract
33
+
34
+ ## Contract at a glance
35
+
36
+ | Area | Contract |
37
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
38
+ | Public contract | Candidate `useTableActivation<T>` hook returning one `TablePlugin<T>` from controlled `activeKey`, `onActivate`, and optional row eligibility. V1 has one press interaction model and consumes Table's canonical resolved row key rather than introducing a second extractor. |
39
+ | Behavior | Eligible rows request activation from a primary pointer click or Enter/Space after earlier row handlers run. Cancellation, nested or portalled controls, repeated keys, and competing whole-row owners do not trigger duplicate activation; native row semantics remain intact. |
40
+ | End-user impact | People can keep one current row visually anchored while it drives adjacent detail or preview content, without losing cell controls or other Table behavior. |
41
+ | Builder impact | Builders own the active key and response to activation. The plugin owns row-state reflection and one whole-row activation path; it does not own routing, detail content, or selection. |
42
+ | Compatibility/readiness | Additive, unreleased module. This draft is not implementation authority. Canonical row identity, competing activation-owner enforcement, ineligible active-state treatment, and active-state paint require owner decisions before promotion. |
43
+ | Review checks | Reject selection coupling, a second row-key extractor, duplicate keys activating multiple rows, implicit navigation, button/link roles on `<tr>`, default `aria-current`, hover activation, non-primary or repeated activation, missing Enter/Space metadata, clobbered or uncancellable handlers, activation from nested/portalled controls or an in-row selection gesture, active paint that fails on pinned cells, and plugin identity churn for stable inputs. |
44
+ | Governing rules | `component:Table` FR9-FR12 and ORD1-ORD4 own plugin composition; `architecture:interaction-modality` owns shared input-modality rules; `architecture:public-component-api` and `spec:AST-002` own public caller burden; `spec:AST-009` owns accessibility-evidence escalation. |
45
+
46
+ This table is a review projection; the body below is authoritative.
47
+
48
+ ## Intent
49
+
50
+ `useTableActivation` gives a data-driven Table one controlled current product-row state
51
+ for master-detail, preview, and inspector layouts. It consumes identity through the
52
+ parent dependency in OQ6, requests activation, reflects active state, composes with
53
+ existing row behavior, and keeps state visually coherent through pinned cells.
54
+
55
+ Activation means “this row currently drives adjacent product state.” It is not a
56
+ selected-set operation, a navigation promise, or focus ownership. Those distinctions
57
+ are part of the module contract rather than left to each product implementation.
58
+
59
+ ## Compatibility and migration
60
+
61
+ - Released default preserved: `yes`; Table behavior remains unchanged unless the new plugin is supplied.
62
+ - Compatibility class: additive public hook, types, documentation, and examples.
63
+ - Migration decision: `module:Table/useTableActivation/DEC-1` after the remaining identity, ownership, eligibility, and active-presentation decisions are approved.
64
+
65
+ Consumer migration instructions belong in consumer docs and release notes.
66
+
67
+ ## Ownership boundary
68
+
69
+ **Owns**
70
+
71
+ - The public `useTableActivation` hook, its configuration, and the controlled active-row contract.
72
+ - Consumption of Table's canonical resolved row key, row eligibility, activation request boundaries, and active-state reflection.
73
+ - Composition with existing row handlers and first-party Table plugins, including existing-handler-first cancellation through `preventDefault`.
74
+ - Suppression of accidental activation from nested or portalled interactive content, non-primary clicks, repeated key presses, and active text selection.
75
+ - Primary-pointer and Enter/Space parity while preserving native row semantics, prior focus ownership, and visible focus.
76
+ - One whole-row activation owner; selection, expansion, tree controls, and other non-owning row behavior may compose without duplicate activation.
77
+ - Active-row paint, pinned-cell coherence, accessibility, performance, and evidence.
78
+
79
+ **Does not own / non-goals**
80
+
81
+ - Aggregate `TablePlugin` protocol, transform order, failure isolation, or plugin-array identity — owned by `component:Table`.
82
+ - Selection state, checkbox semantics, or `aria-selected` — owned by `useTableSelection` and its caller.
83
+ - Routing, URLs, link semantics, or `aria-current="page"` — owned by a navigation surface, not activation.
84
+ - Adjacent detail content, preview loading, prefetching, or focus movement — owned by the product or another module.
85
+ - Cell controls. Buttons, links, inputs, editable content, and portalled controls remain independently operable and do not activate their row as a side effect.
86
+ - Hover-driven activation. Hover may remain a visual enhancement, but v1 never changes controlled active state from pointer movement alone.
87
+ - Children-mode rows. The plugin pipeline applies to data-driven rows only.
88
+ - A new public theme target. The active row remains part of the existing `table-row` anatomy unless a separate theming decision proves another owner is necessary.
89
+
90
+ ## Public API and concepts
91
+
92
+ The candidate public shape is:
93
+
94
+ ```ts
95
+ export interface UseTableActivationConfig<T extends Record<string, unknown>> {
96
+ activeKey: string | number | null;
97
+ onActivate: (item: T, key: string | number) => void;
98
+ getIsItemActivatable?: (item: T) => boolean;
99
+ }
100
+
101
+ export function useTableActivation<T extends Record<string, unknown>>(
102
+ config: UseTableActivationConfig<T>,
103
+ ): TablePlugin<T>;
104
+ ```
105
+
106
+ Consumer docs own final signatures, defaults, imports, and examples after promotion.
107
+
108
+ | Concept | Closed values or states | Meaning | Default | Owner | Stability |
109
+ | -------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | --------------------------------- | ------------------------------ |
110
+ | Active key | Stable unique `string`, `number`, or `null` | Matches Table's one validated canonical row key; `null` means no active row. | Controlled `null` | Caller through this module | Candidate |
111
+ | Row eligibility | Product row eligible or inert; synthetic group header inert | Distinguishes product activation from grouping controls before applying caller eligibility. | Every product row eligible; group headers excluded | `module:Table/useTableActivation` | Parent distinction pending OQ7 |
112
+ | Activation request | `(item, key)` callback | Asks the caller to update controlled state; the plugin never mutates `activeKey`. | No implicit state update | Caller through this module | Candidate |
113
+ | Activation inputs | Primary pointer click, Enter, or Space | One press interaction model requests activation; hover and auxiliary clicks never change active state. | All three paths enabled | `module:Table/useTableActivation` | Candidate |
114
+ | Active presentation | Inactive or active | Reflects controlled state on the existing row anatomy and through pinned cells. | Inactive | `module:Table/useTableActivation` | Unsettled |
115
+ | Selection/navigation | Independent | Activation neither changes selection nor claims a route, destination, or navigation role. | Unchanged | Their respective owners | Required |
116
+
117
+ ## Behavioral contract
118
+
119
+ | ID | Candidate invariant | Basis | Draft review state |
120
+ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------ |
121
+ | FR1 | One mounted hook instance returns one stable `TablePlugin`, and each mounted row keeps a stable composed activation handler while controlled keys or current callbacks change. | `component:Table` FR12 and PR1; existing stable-identity plugin evidence | Settled candidate |
122
+ | FR2 | `activeKey` is fully controlled. Activation depends on the unapproved parent contract in OQ6: one explicit stable unique `idKey`, its resolved key and validity available to activation, and duplicate-key rows inert in all builds with one deduplicated development warning. Current Table derives keys but does not validate them or pass them to row transforms. The candidate compares `activeKey` with the resolved key using strict equality and emits that same key: a property-name `idKey` resolves `String(item[idKey])`, so `"42"` matches and `42` does not; a function preserves its returned string or number. Without `idKey`, activation is inert with one deduplicated development warning, not keyed by row index. No second extractor or activation-side coercion is added. | One row identity across parent and module | Parent API decision required |
123
+ | FR3 | An eligible row whose validated canonical key equals `activeKey` exposes a stable `data-active` state and component-owned active presentation; all other rows remove both. No default `aria-current` or `aria-selected` is inferred. | Activation/selection/navigation boundary | Settled candidate |
124
+ | FR4 | An ineligible product row contributes no activation handler, keyboard affordance, active paint, or active-state attribute even if its key equals the controlled key, subject to OQ5. Synthetic `useTableGroupedRows` headers are always ineligible for product activation, including when an eligibility predicate returns true; their existing group toggle and disclosure button remain independent. Safe product/header distinction is an OQ7 parent dependency; grouped composition is not admitted until that boundary is enforceable. | Disabled interaction boundary | Verify whether controlled active state should remain visible |
125
+ | FR5 | A primary pointer click, Enter, or Space calls `onActivate(item, key)` once. The module runs each earlier accumulated handler first, preserves its side effects, and stops before activation when that handler calls `preventDefault`; it never replaces a handler already present when its transform runs. | Parent sequential plugin contract; proven interaction composition | Settled candidate |
126
+ | FR6 | An event originating in a nested button, link, input, select, textarea, editable surface, equivalent interactive role, or portalled control outside the row does not activate the row. Text selection suppresses activation only when the non-collapsed selection associated with that gesture intersects the activated row. | Accidental-activation prevention | Settled candidate |
127
+ | FR7 | Each eligible row is a sequential focus stop by default and activates on Enter or Space while retaining native `<tr>` semantics and visible focus. An earlier plugin's explicit `tabIndex`, including `-1` for roving focus, remains authoritative; a programmatically focused row still activates. The row advertises `Enter Space` through composed `aria-keyshortcuts` metadata. Promotion evidence covers representative large unvirtualized tables because the default sequential path grows with mounted eligible rows. | Proven keyboard model; WCAG 2.2 SC 2.1.1, 2.4.7, and 2.4.11 | Settled candidate |
128
+ | FR8 | Only a primary-button click activates; auxiliary clicks remain available to earlier handlers. Keyboard auto-repeat never activates. Repeated Space remains default-prevented so holding it cannot scroll the page, while repeated Enter is ignored without claiming a browser default. Pointer hover never changes `activeKey` in v1. | Proven press boundary; shared modality rules | Settled candidate |
129
+ | FR9 | Activation is independent from selection, expansion, tree controls, context menus, sorting, and navigation. Non-owning plugins retain their handlers and controlled state. A product row has at most one whole-row activation owner; expansion configured with `hasRowClickExpansion` and future navigation are competing owners. Synthetic `useTableGroupedRows` headers remain grouping-owned and ineligible under FR4. OQ7 requires parent ownership enforcement and product/header distinction; plugin insertion order must not decide whether a group toggle also calls product `onActivate`. | Module boundary, single ownership, and plugin composition | Parent enforcement decision required |
130
+ | FR10 | Active presentation uses a component-owned fallback on existing row anatomy. OQ3 owns the unresolved paint precedence across striping, hover, focus, selection's row wash, `hasRowHighlight`, and active state; no selected-plus-active combination is implementation-ready without that decision. Each admitted combination defines one effective fill/overlay for both normal and pinned cells and a distinguishable active cue independent of colour. `hasRowHighlight=false` continues to disable selection-owned wash, not selection semantics; whether activation replaces the current detail-panel background-reclamation use case remains an OQ3 decision. No theme-authored `active` state is implied. | Current private row overlay and theming-surface boundaries | Human visual decision |
131
+ | FR11 | Children mode, empty state, and rows excluded by upstream transforms remain unchanged. Any module-owned resource usage is bounded by mounted rows, fully released when the owning row or Table detaches, and produces no activation or global side effect after release. | Observable resource limits; implementation freedom | Settled candidate |
132
+ | FR12 | For valid unique canonical keys, changing only `activeKey` does not replace the plugin array, row handlers, columns, or product data and does not React-rerender unrelated rows. At most the old active and new active mounted rows change their reflected state/presentation. | Bounded-update performance requirement and parent row memoisation | Settled candidate |
133
+
134
+ ### Transformation and precedence order
135
+
136
+ - **ORD1 — Eligibility before state.** Distinguish product rows from synthetic grouped headers through the approved OQ7 parent contract, then resolve product eligibility. Group headers remain ineligible regardless of the caller predicate; ineligible rows return prior accumulated row props unchanged.
137
+ - **ORD2 — Controlled state.** The OQ6 dependency supplies one validated canonical key. Compare it with `activeKey` using FR2's strict, type-preserving equality and emit the same key; never derive a second identity or infer active state from focus or a prior event. Missing explicit identity or duplicate-key rows are inert.
138
+ - **ORD3 — Existing handler.** Run the accumulated handler for that event before the module's request. A handler that calls `preventDefault` cancels activation without discarding its own side effects.
139
+ - **ORD4 — Event boundary.** Nested interactive descendants, portalled events outside the row, and a non-collapsed selection intersecting the activated row suppress the module request without suppressing descendant behavior; an unrelated selection elsewhere does not.
140
+ - **ORD5 — Accepted press.** After the prior handler and boundary guards, accept only a primary click or non-repeated Enter/Space. Repeated Space prevents scrolling without activating; hover and auxiliary clicks never activate.
141
+ - **ORD6 — Presentation.** Apply inactive or active state after eligibility while preserving prior classes, styles, refs, attributes, and children.
142
+ - **ORD7 — Parent pipeline.** Relative order with non-owning plugins remains the order supplied by `component:Table`; this module creates no private second pipeline. Competing whole-row owners follow the OQ7 parent enforcement outcome.
143
+
144
+ ### Performance and resources
145
+
146
+ - **PR1 — Constant module overhead.** Canonical-key comparison, state reflection, and handler guards add O(1) module-owned work per relevant row update. The module invokes `getIsItemActivatable` at most once when resolving that row update; caller callback complexity is outside the module's guarantee and is measured separately by call count.
147
+ - **PR2 — Stable public identities.** The returned plugin object, Table's resolved plugin array, and mounted row activation handlers stay referentially stable across `activeKey`, callback, and eligibility-function changes while still observing their current values.
148
+ - **PR3 — Bounded active update.** For valid unique keys, changing only `activeKey` changes reflected state/presentation on at most the old active and new active mounted rows and does not React-rerender unrelated rows.
149
+ - **PR4 — Bounded lifetime.** Module-owned retained resources scale no faster than mounted rows, are released when their owning row/Table detaches, and leave no post-release callback or global side effect. Equivalent implementations that meet those observable bounds remain valid.
150
+
151
+ ## Accessibility contract
152
+
153
+ - **AR1 — Native table semantics.** Body rows retain their native row role; the module does not claim button, link, option, or gridcell semantics it cannot fulfil.
154
+ - **AR2 — Keyboard parity.** Eligible rows are focusable by default, preserve an earlier roving-focus `tabIndex`, activate on Enter/Space, compose `aria-keyshortcuts="Enter Space"`, and keep visible unobscured focus without replacing native row semantics (WCAG 2.2 SC 2.1.1, 2.4.7, and 2.4.11).
155
+ - **AR3 — Nested and portalled controls.** Interactive descendants and portalled controls remain reachable and operable without activating the row (WCAG 2.2 SC 2.1.1).
156
+ - **AR4 — State honesty.** The module does not emit `aria-selected` or default `aria-current`; active presentation alone does not claim selection or navigation semantics.
157
+ - **AR5 — Non-colour state.** If active state communicates current position required to use an adjacent view, its focus/current treatment cannot rely on colour alone (WCAG 2.2 SC 1.4.1). The exact representation is blocked on OQ3.
158
+ - **AR6 — No hover-only activation.** Hover may enhance row paint, but it never changes active state; pointer, touch, and keyboard users share the press-owned activation outcome (WCAG 2.2 SC 2.1.1 and `architecture:interaction-modality` INV4).
159
+ - **AR7 — Proportionate evidence.** Unit, DOM, accessibility-tree, axe, and real-browser keyboard evidence prove static semantics, nested-control isolation, focus, and reflected state. Real AT is added only if an approved outcome depends on announcement, browse/virtual-cursor behavior, or a recorded AT/browser divergence, as required by `spec:AST-009`.
160
+
161
+ ## Design relationships
162
+
163
+ | Anatomy or state | Design requirement | Representation authority | Module contract |
164
+ | -------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------ |
165
+ | Activatable row | Communicates availability without replacing native table structure or cell controls. | This module plus `architecture:interaction-modality` | FR5-FR9, AR1-AR3 |
166
+ | Active row | Remains visually traceable through normal and pinned cells. | Component-owned fallback; public theme-state admission remains OQ3 | FR3, FR10, AR4-AR5 |
167
+ | Focused row | Shows keyboard focus independently from controlled active state. | Shared focus rules and existing row anatomy | FR2, FR7, AR2, AR5 |
168
+ | Nested cell control | Keeps its own interaction, focus, and semantic contract. | The nested component | FR6, FR9, AR3 |
169
+ | Selection/navigation | Never inferred from activation. | Selection or navigation owner | FR2, FR3, FR9, AR4 |
170
+
171
+ The default contract adds no public theme target or theme-authored `active` state. The module owns a component fallback and must propagate its effective overlay to pinned cells. OQ3 may admit an `active` state on the existing `table-row` target only if it defines the fallback, guaranteed paint properties, state precedence, and pinned-overlay projection required by `architecture:component-theming-surface`. A separate direct target still requires another theming decision.
172
+
173
+ ## Parent and system relationships
174
+
175
+ - `component:Table` owns the aggregate `TablePlugin` protocol, canonical ordering, sequential transforms, failure isolation, and stable plugin-array identity. Its current contract does not promise validated canonical identity or key delivery to activation. OQ6 requires a separately approved parent addition for explicit stable unique `idKey` validation, resolved-key equality, and duplicate-key inertness; OQ7 requires a separately approved ownership and product/header distinction contract. This module neither supplies nor approves those missing parent contracts.
176
+ - `architecture:interaction-modality` owns cross-component keyboard/pointer modality rules; this module owns only their row-activation projection.
177
+ - `architecture:public-component-api` and `spec:AST-002` own additive API discipline and caller burden. The module exposes controlled product state rather than routing or data-fetching dependencies.
178
+ - `spec:AST-009` owns accessibility-evidence escalation. This module uses lower-layer semantic and browser evidence unless an approved outcome depends on assistive-technology behavior.
179
+ - `useTableSelection`, tree, expansion, context-menu, sticky-column, and future navigation modules retain their independent state and semantics.
180
+
181
+ ## Verification map
182
+
183
+ | Contract | Verification | Representative states | Mutation or failure expectation |
184
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
185
+ | FR1-FR4, FR11-FR12, PR1-PR4 | Public plugin/handler identity, controlled render/mutation/callback counts, invalid-key fallback, and resource-lifetime tests | Active, inactive, `null`, eligible, ineligible, missing `idKey`, property-key `"42"` versus numeric `42`, function-returned numeric keys, duplicate resolved keys, grouped headers, callback/key changes | Plugin/handlers churn, duplicate-key rows activate, unrelated rows rerender, more than two valid-key rows mutate, callbacks overrun, or resources leak. |
186
+ | FR5-FR9, ORD1-ORD7 | Unit/browser interaction tests with earlier handlers and selection, grouped-row headers, control-only expansion, `hasRowClickExpansion`, tree, sticky, context-menu, navigation-owner, and custom plugins | Primary/auxiliary pointer, Enter/Space, auto-repeat, prior `tabIndex`, grouped headers and product rows in both plugin orders, eligibility override, nested/portalled controls, in-row/unrelated text selection, cancellation | Handlers are replaced, callbacks fire twice, auxiliary/repeated/hover input activates, Space repeat scrolls, earlier focus ownership is lost, unrelated selection blocks activation, nested/portalled controls activate rows, or two whole-row owners fire. |
187
+ | FR10, AR5-AR6 | Real-browser screenshots in light/dark, striped, hover, pinned-column, keyboard-focus, touch emulation, and forced-colors states | OQ3's complete selection/activation/`hasRowHighlight` matrix, selection-first and activation-first updates, focus/hover, light/dark, and normal/pinned cells | Component fallback or approved theme state disappears under pinned cells, relies on colour alone, loses focus indication, or hover changes active state. |
188
+ | AR1-AR7 | Unit/DOM/accessibility-tree tests, axe, and real-browser keyboard automation; real AT only for an adopted AT-dependent outcome | Native traversal in representative small and large unvirtualized tables, default/roving/programmatic focus, Enter/Space metadata and activation, nested link/button/input, portalled control, state update | Row semantics are replaced, Enter/Space is undiscoverable or unavailable, focus order becomes incoherent at representative scale, earlier focus ownership is lost, nested controls activate, or selection/navigation semantics are falsely exposed. |
189
+ | Plugin composition | Focused integration matrix plus existing Table selection/tree/expansion/sticky/context-menu suites | Every supported first-party combination and custom row handler order | Enabling activation breaks another plugin, changes column order, or loses accumulated row props. |
190
+ | Public API and documentation | Public-subpath compile fixture including required explicit `idKey` and parent-supplied validated canonical row key, generated inventory, `.doc.mjs`, Storybook, and one recipe | Generic row type, controlled update, eligibility, duplicate keys, primary click, Enter/Space | A second extractor or mode switch appears, duplicate behavior is undefined, the hook/types are unavailable, or docs imply hover activation. |
191
+ | Performance | Extend `Table.perf.test.tsx` with plugin/handler identity, render and DOM-mutation counts, eligibility-callback counts, stable no-op, and adjacent valid-key changes | Large representative data, same key, adjacent key, callbacks changed | One key change replaces the plugin path, rerenders unrelated rows, mutates more than old/new rows, over-invokes eligibility, or exceeds budgets. |
192
+ | Structural ownership | `pnpm check:knowledge` | Draft module and parent backlink | A missing, orphaned, misnamed, wrong-kind, or duplicate module record passes validation. |
193
+
194
+ ## Decision log
195
+
196
+ ### DEC-1 — Activation is controlled and independent
197
+
198
+ **Reference:** `module:Table/useTableActivation/DEC-1`
199
+ **Decider:** pending owner approval
200
+
201
+ Activation represents one caller-owned current product row. It depends on OQ6's parent validation and key projection, not a validation guarantee in current Table. The candidate requires explicit stable unique `idKey`, compares the resolved key with `activeKey` as specified in FR2, and emits the same key; missing identity and duplicate-key rows remain inert. Grouped headers do not become product rows under an eligibility override. The module never owns product state, selection, routing, focus, detail content, or prefetching.
202
+
203
+ Rejected: a second activation-specific key extractor, deriving activation from selection or focus, storing an internal fallback key, or combining row navigation and activation into one hook.
204
+
205
+ ### DEC-2 — Preserve native row semantics
206
+
207
+ **Reference:** `module:Table/useTableActivation/DEC-2`
208
+ **Decider:** pending owner approval
209
+
210
+ Whole-row convenience keeps native semantic table-row structure. Eligible rows are focusable by default, preserve an earlier roving-focus `tabIndex`, advertise Enter/Space through `aria-keyshortcuts`, and activate from primary click, Enter, or Space without claiming button or link semantics.
211
+
212
+ Rejected: `role="button"` or `role="link"` on `<tr>`, pointer-only activation, and
213
+ implicit `aria-selected` or `aria-current`.
214
+
215
+ ### DEC-3 — Active state uses existing row anatomy
216
+
217
+ **Reference:** `module:Table/useTableActivation/DEC-3`
218
+ **Decider:** pending owner approval
219
+
220
+ The module reflects active state through existing row anatomy, with one effective fill/overlay reaching normal and pinned cells. OQ3 owns the exact representation and precedence, including selection's wash and `hasRowHighlight`; neither selected-plus-active precedence nor a replacement for the detail-panel background-reclamation path is implied. A second target is not justified. Theme-authored `active` state is admitted only if OQ3 defines fallback, properties, precedence, and pinned projection.
221
+
222
+ Rejected: styling only transparent row backgrounds that disappear under pinned cells, implying that the current `table-row` target already accepts an undeclared `active` state, and adding a public `table-row-activation` target without an independent theme-author need.
223
+
224
+ ### DEC-4 — Press owns activation and earlier handlers may cancel
225
+
226
+ **Reference:** `module:Table/useTableActivation/DEC-4`
227
+ **Decider:** pending owner approval
228
+
229
+ The module has one press interaction model. It runs the accumulated handler first, treats `preventDefault` as cancellation, and then accepts only a primary click or non-repeated Enter/Space outside nested or portalled controls and intersecting text selection. Repeated Space prevents page scrolling without activation. A product row has at most one whole-row activation owner; grouped headers remain grouping-owned and ineligible. OQ7 owns the missing parent distinction and enforcement contract.
230
+
231
+ Rejected: hover activation, replacing earlier handlers, activating after cancellation, and activating from auxiliary clicks or key auto-repeat.
232
+
233
+ ## Open questions
234
+
235
+ - **OQ3 — How is required current state communicated?** (`human-design`) Decide the component-owned fallback, focus-versus-active precedence, non-colour cue, and any admitted `active` state on the existing `table-row` target. The decision must specify every state in the matrix below, including whether selected-plus-active uses selection-dominant fill with a distinct active cue or active-dominant fill with a persistent selection cue. `hasRowHighlight=false` removes selection wash while preserving selection semantics; it is not an activation disable flag. Decide whether an active row driving detail content replaces the current background-reclamation use case or needs another explicit caller choice. Each outcome must define the same effective paint on normal and pinned cells, including selection/active changes in either order. No active or selected-plus-active paint is admitted while this question remains open. An opt-in caller-supplied `aria-current` value, if warranted for genuine current-item sets, is a separate decision; default selection/navigation semantics remain forbidden.
236
+
237
+ | Selection | Activation | `hasRowHighlight` | Required outcome before admission |
238
+ | ---------- | ---------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
239
+ | Unselected | Inactive | Either | Existing striping, hover, and focus treatment. |
240
+ | Selected | Inactive | `true` | Existing selection wash and selection semantics on normal and pinned cells. |
241
+ | Selected | Inactive | `false` | No selection-owned wash; selection semantics remain. |
242
+ | Unselected | Active | Either | OQ3 defines active fill, non-colour cue, and focus/hover precedence independently of the selection flag. |
243
+ | Selected | Active | `true` | OQ3 chooses selection-versus-active fill precedence and preserves distinguishable selection, active state, and focus on normal and pinned cells. |
244
+ | Selected | Active | `false` | OQ3 defines active paint without reintroducing selection wash; selection semantics remain. |
245
+
246
+ - **OQ5 — Should an ineligible product row matching `activeKey` remain visibly active?** (`human-api`) Decide whether caller eligibility governs interaction only or both interaction and controlled presentation for real product rows. This decision does not relax FR4's exclusion of synthetic grouped headers.
247
+ - **OQ6 — How does Table expose validated canonical row identity?** (`human-api`) Approve the missing parent-owned validation and key-delivery contract. Current Table resolves an index when `idKey` is omitted, `String(item[idKey])` for a property name, or the function's returned key; it does not validate uniqueness or deliver the key to `transformBodyRow`. The activation candidate requires explicit stable unique identity, FR2's strict resolved-key equality and same-key callback, inert missing/duplicate identity in all builds, and deduplicated development diagnostics. The approved parent record must own these additions and their compatibility; this module adds no activation-specific extractor or coercion.
248
+ - **OQ7 — How does Table enforce whole-row ownership and product eligibility?** (`human-api`) Approve the missing parent contract for distinguishing caller product rows from synthetic `useTableGroupedRows` headers and enforcing FR4's default header ineligibility. Group headers keep their grouping-owned click and disclosure-button paths; `hasRowClickExpansion` and future navigation compete with activation on product rows. Define deterministic rejection or neutralisation, development diagnostics, and production fallback for every admitted combination without making plugin insertion order select an owner. Grouping integration remains unsupported until its distinction and ownership boundary are enforceable; key prefixes or private marker inspection are not caller obligations.
249
+
250
+ ## Content boundary
251
+
252
+ This record does not duplicate consumer signatures/examples, the parent Table plugin
253
+ protocol or ordering, shared input-modality rules, current usage audits, implementation
254
+ steps, or navigation/selection/prefetch contracts. It links to their canonical owners.
@@ -346,3 +346,60 @@ describe('truncated text shows one tooltip, not two', () => {
346
346
  }
347
347
  });
348
348
  });
349
+
350
+ // With hasTruncateTooltip={false} nothing reads the measured overflow, so a
351
+ // long table of truncated cells must not pay a layout read per cell.
352
+ function countScrollWidthReads(run: () => void): number {
353
+ const proto = window.HTMLElement.prototype;
354
+ const original = Object.getOwnPropertyDescriptor(proto, 'scrollWidth');
355
+ let reads = 0;
356
+ Object.defineProperty(proto, 'scrollWidth', {
357
+ configurable: true,
358
+ get() {
359
+ reads += 1;
360
+ return 0;
361
+ },
362
+ });
363
+ try {
364
+ run();
365
+ } finally {
366
+ if (original) {
367
+ Object.defineProperty(proto, 'scrollWidth', original);
368
+ } else {
369
+ delete (proto as {scrollWidth?: number}).scrollWidth;
370
+ }
371
+ }
372
+ return reads;
373
+ }
374
+
375
+ describe('Text maxLines without a truncation tooltip', () => {
376
+ it('takes no truncation measurement when hasTruncateTooltip is false', () => {
377
+ const reads = countScrollWidthReads(() => {
378
+ render(
379
+ <>
380
+ {Array.from({length: 20}, (_, index) => (
381
+ <Text
382
+ type="body"
383
+ maxLines={1}
384
+ hasTruncateTooltip={false}
385
+ key={index}>
386
+ Cell {index} with text far wider than its column
387
+ </Text>
388
+ ))}
389
+ </>,
390
+ );
391
+ });
392
+ expect(reads).toBe(0);
393
+ });
394
+
395
+ it('still measures when the tooltip is enabled', () => {
396
+ const reads = countScrollWidthReads(() => {
397
+ render(
398
+ <Text type="body" maxLines={1}>
399
+ Text far wider than its column
400
+ </Text>,
401
+ );
402
+ });
403
+ expect(reads).toBeGreaterThan(0);
404
+ });
405
+ });
package/src/Text/Text.tsx CHANGED
@@ -260,8 +260,12 @@ export function Text({
260
260
  // Resolve display - force block when maxLines > 0 or hasCapsize
261
261
  const resolvedDisplay = maxLines > 0 || hasCapsize ? 'block' : display;
262
262
 
263
- // Truncation detection
264
- const truncation = useTruncation({maxLines});
263
+ // Truncation detection. Only the tooltip reads the measured overflow, so
264
+ // with the tooltip off the text is clipped by CSS alone and nothing is
265
+ // measured or observed.
266
+ const truncation = useTruncation({
267
+ maxLines: hasTruncateTooltip === false ? 0 : maxLines,
268
+ });
265
269
 
266
270
  // Tooltip for truncated text
267
271
  const tooltipPlacement: LayerPlacement =
@@ -191,6 +191,21 @@ for (const [surface, presentation, selector] of SURFACES) {
191
191
  expect(own?.textContent).toContain('Start');
192
192
  });
193
193
 
194
+ it('puts hidden, inert, and dir on the control', () => {
195
+ render(
196
+ <TimeInput
197
+ label="Start"
198
+ presentation={presentation}
199
+ hidden
200
+ inert
201
+ dir="rtl"
202
+ />,
203
+ );
204
+ expect(control(selector)).toHaveAttribute('hidden');
205
+ expect(control(selector)).toHaveAttribute('inert');
206
+ expect(control(selector)).toHaveAttribute('dir', 'rtl');
207
+ });
208
+
194
209
  it('keeps control* on the control box in a horizontal-labels FormLayout', () => {
195
210
  vi.spyOn(console, 'warn').mockImplementation(() => {});
196
211
  const {container} = render(
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'Timestamp',
7
7
  displayName: 'Timestamp',
8
8
  category: 'Content',
9
- keywords: ['date', 'time', 'datetime', 'relative', 'ago', 'clock', 'format', 'duration'],
9
+ keywords: ['date', 'time', 'datetime', 'relative', 'ago', 'clock', 'format', 'duration', 'relativetime'],
10
10
  playground: {
11
11
  // `value` is required but has no semantic default, so the properties-tab
12
12
  // preview fell back to the literal string "value" — which
@@ -497,6 +497,78 @@ describe('ToggleButton', () => {
497
497
  expect(button).not.toHaveAttribute('aria-busy', 'true');
498
498
  await act(async () => {});
499
499
  });
500
+
501
+ // family:buttons FR5: a caller onClick on a standalone ToggleButton runs
502
+ // before onPressedChange and pressedChangeAction, never replaces the toggle
503
+ // request, and cancels it only by calling preventDefault() itself.
504
+ describe('with a caller onClick', () => {
505
+ it('runs the caller onClick first, then requests the next state and runs the Action', async () => {
506
+ const calls: string[] = [];
507
+ render(
508
+ <ToggleButton
509
+ label="Compact"
510
+ isPressed={false}
511
+ onClick={() => calls.push('onClick')}
512
+ onPressedChange={next => calls.push(`onPressedChange:${next}`)}
513
+ pressedChangeAction={next => {
514
+ calls.push(`pressedChangeAction:${next}`);
515
+ }}
516
+ />,
517
+ );
518
+
519
+ fireEvent.click(screen.getByRole('button', {name: 'Compact'}));
520
+ await act(async () => {});
521
+
522
+ expect(calls).toEqual([
523
+ 'onClick',
524
+ 'onPressedChange:true',
525
+ 'pressedChangeAction:true',
526
+ ]);
527
+ });
528
+
529
+ it('requests no next state when the caller onClick calls preventDefault', async () => {
530
+ const onPressedChange = vi.fn();
531
+ const pressedChangeAction = vi.fn();
532
+ render(
533
+ <ToggleButton
534
+ label="Compact"
535
+ isPressed={false}
536
+ onClick={event => event.preventDefault()}
537
+ onPressedChange={onPressedChange}
538
+ pressedChangeAction={pressedChangeAction}
539
+ />,
540
+ );
541
+
542
+ const button = screen.getByRole('button', {name: 'Compact'});
543
+ fireEvent.click(button);
544
+ await act(async () => {});
545
+
546
+ expect(onPressedChange).not.toHaveBeenCalled();
547
+ expect(pressedChangeAction).not.toHaveBeenCalled();
548
+ expect(button).toHaveAttribute('aria-pressed', 'false');
549
+ });
550
+
551
+ it('still requests the next state when the click was already prevented before the caller onClick ran', () => {
552
+ const onPressedChange = vi.fn();
553
+ render(
554
+ <div onClickCapture={event => event.preventDefault()}>
555
+ <ToggleButton
556
+ label="Compact"
557
+ isPressed={false}
558
+ onClick={() => {}}
559
+ onPressedChange={onPressedChange}
560
+ />
561
+ </div>,
562
+ );
563
+
564
+ fireEvent.click(screen.getByRole('button', {name: 'Compact'}));
565
+
566
+ expect(onPressedChange).toHaveBeenCalledExactlyOnceWith(
567
+ true,
568
+ expect.anything(),
569
+ );
570
+ });
571
+ });
500
572
  });
501
573
 
502
574
  // =============================================================================
@@ -721,6 +793,29 @@ describe.each(['single', 'multiple'] as const)(
721
793
  expect(onPressedChange).not.toHaveBeenCalled();
722
794
  expect(pressedChangeAction).not.toHaveBeenCalled();
723
795
  });
796
+
797
+ // family:buttons FR11: a member's selection request comes from the group,
798
+ // and a caller onClick on the member must not replace it.
799
+ it('still asks the group to toggle a member that receives an onClick', async () => {
800
+ const onChange = vi.fn();
801
+ const onClick = vi.fn();
802
+ const groupProps =
803
+ type === 'single'
804
+ ? {type, value: null, onChange}
805
+ : {type, value: [], onChange};
806
+ render(
807
+ <ToggleButtonGroup {...groupProps} label="View">
808
+ <ToggleButton value="grid" label="Grid" onClick={onClick} />
809
+ </ToggleButtonGroup>,
810
+ );
811
+
812
+ await userEvent.setup().click(screen.getByRole('button', {name: 'Grid'}));
813
+
814
+ expect(onClick).toHaveBeenCalledTimes(1);
815
+ expect(onChange).toHaveBeenCalledExactlyOnceWith(
816
+ type === 'single' ? 'grid' : ['grid'],
817
+ );
818
+ });
724
819
  },
725
820
  );
726
821
 
@@ -9,7 +9,8 @@
9
9
  * @position Thin wrapper over Button; adds controlled toggle pattern
10
10
  *
11
11
  * ToggleButton wraps Button with `isPressed` and adds:
12
- * - `onPressedChange` from internal `onClick`; `pressedChangeAction` via Button
12
+ * - `onPressedChange` from internal `onClick`, after any caller `onClick`;
13
+ * `pressedChangeAction` via Button
13
14
  * - `pressedIcon` for outline-to-filled icon swap
14
15
  * - Font weight shift on press with width reservation to prevent layout shift
15
16
  * - Group integration via ToggleButtonGroupContext
@@ -258,6 +259,7 @@ export function ToggleButton({
258
259
  xstyle,
259
260
  className,
260
261
  style,
262
+ onClick: onClickProp,
261
263
  ...props
262
264
  }: ToggleButtonProps): ReactNode {
263
265
  const group = useToggleButtonGroup();
@@ -287,11 +289,20 @@ export function ToggleButton({
287
289
  if (isDisabled) {
288
290
  return;
289
291
  }
292
+ // A caller onClick (a trigger wrapper often supplies one) runs first and
293
+ // never replaces the toggle's own request (family:buttons FR5, FR11).
294
+ const wasPrevented = event.defaultPrevented;
295
+ onClickProp?.(event);
290
296
  if (group && value != null) {
291
297
  group.toggle(value);
292
298
  event.preventDefault();
293
299
  return;
294
300
  }
301
+ // A standalone toggle is cancelled only by the caller's own
302
+ // preventDefault(); Button then skips pressedChangeAction too.
303
+ if (!wasPrevented && event.defaultPrevented) {
304
+ return;
305
+ }
295
306
 
296
307
  onPressedChangeProp?.(nextPressed, event);
297
308
  };
@@ -6,6 +6,10 @@ export const docs = {
6
6
  name: 'ToggleButtonGroup',
7
7
  subComponentOf: 'ToggleButton',
8
8
  displayName: 'Toggle Button Group',
9
+ keywords: [
10
+ 'togglegroup',
11
+ 'buttontogglegroup',
12
+ ],
9
13
  description: 'Groups toggle buttons for exclusive (single) or multi-select behavior. Uses discriminated union on type for type-safe value/onChange.',
10
14
  props: [
11
15
  {
@@ -190,7 +190,7 @@ const styles = stylex.create({
190
190
  '::after': {
191
191
  content: '""',
192
192
  position: 'absolute',
193
- inset: '-14px',
193
+ inset: '-4px',
194
194
  },
195
195
  },
196
196
  });