@multiplatform.one/table 6.4.2 → 6.6.0

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 (295) hide show
  1. package/dist/cjs/components/ChildTable/index.native.js +1 -1
  2. package/dist/cjs/components/ChildTable/index.native.js.map +1 -1
  3. package/dist/cjs/components/ColumnHeaderFilter/index.cjs +1 -0
  4. package/dist/cjs/components/ColumnHeaderFilter/index.native.js +2 -1
  5. package/dist/cjs/components/ColumnHeaderFilter/index.native.js.map +1 -1
  6. package/dist/cjs/components/DataTable/DataTableCardControls.native.js +1 -1
  7. package/dist/cjs/components/DataTable/DataTableCardControls.native.js.map +1 -1
  8. package/dist/cjs/components/DataTable/DataTableContext.native.js.map +1 -1
  9. package/dist/cjs/components/DataTable/DataTableMobileCards.cjs +9 -3
  10. package/dist/cjs/components/DataTable/DataTableMobileCards.native.js +12 -3
  11. package/dist/cjs/components/DataTable/DataTableMobileCards.native.js.map +1 -1
  12. package/dist/cjs/components/DataTable/DataTableRoot.cjs +133 -13
  13. package/dist/cjs/components/DataTable/DataTableRoot.native.js +166 -20
  14. package/dist/cjs/components/DataTable/DataTableRoot.native.js.map +1 -1
  15. package/dist/cjs/components/DataTable/DataTableStateContext.native.js.map +1 -1
  16. package/dist/cjs/components/DataTable/DataTableVirtualized.cjs +3 -1
  17. package/dist/cjs/components/DataTable/DataTableVirtualized.native.js +3 -1
  18. package/dist/cjs/components/DataTable/DataTableVirtualized.native.js.map +1 -1
  19. package/dist/cjs/components/DataTable/index.native.js +50 -13
  20. package/dist/cjs/components/DataTable/index.native.js.map +1 -1
  21. package/dist/cjs/components/DataTableBulkActions/index.cjs +16 -1
  22. package/dist/cjs/components/DataTableBulkActions/index.native.js +20 -1
  23. package/dist/cjs/components/DataTableBulkActions/index.native.js.map +1 -1
  24. package/dist/cjs/components/DataTableCell/CellModalEditor.cjs +4 -8
  25. package/dist/cjs/components/DataTableCell/CellOptionsEditor.native.js.map +1 -1
  26. package/dist/cjs/components/DataTableCell/index.cjs +1 -33
  27. package/dist/cjs/components/DataTableCell/index.native.js +1 -34
  28. package/dist/cjs/components/DataTableCell/index.native.js.map +1 -1
  29. package/dist/cjs/components/DataTableColumnPinning/index.cjs +2 -2
  30. package/dist/cjs/components/DataTableColumnPinning/index.native.js +2 -2
  31. package/dist/cjs/components/DataTableColumnPinning/index.native.js.map +1 -1
  32. package/dist/cjs/components/DataTableDateFilter/index.native.js.map +1 -1
  33. package/dist/cjs/components/DataTableExport/index.cjs +1 -0
  34. package/dist/cjs/components/DataTableExport/index.native.js +1 -0
  35. package/dist/cjs/components/DataTableExport/index.native.js.map +1 -1
  36. package/dist/cjs/components/DataTableFilterButton/index.cjs +1 -1
  37. package/dist/cjs/components/DataTableFilterButton/index.native.js +5 -2
  38. package/dist/cjs/components/DataTableFilterButton/index.native.js.map +1 -1
  39. package/dist/cjs/components/DataTableFilterChips/index.cjs +1 -1
  40. package/dist/cjs/components/DataTableFilterChips/index.native.js +1 -1
  41. package/dist/cjs/components/DataTableFilterChips/index.native.js.map +1 -1
  42. package/dist/cjs/components/DataTableFilters/index.native.js.map +1 -1
  43. package/dist/cjs/components/DataTableGrouping/index.cjs +17 -1
  44. package/dist/cjs/components/DataTableGrouping/index.native.js +17 -1
  45. package/dist/cjs/components/DataTableGrouping/index.native.js.map +1 -1
  46. package/dist/cjs/components/DataTablePagination/index.native.js.map +1 -1
  47. package/dist/cjs/components/DataTableSearch/index.native.js.map +1 -1
  48. package/dist/cjs/components/DataTableToolbar/index.cjs +1 -0
  49. package/dist/cjs/components/DataTableToolbar/index.native.js +2 -1
  50. package/dist/cjs/components/DataTableToolbar/index.native.js.map +1 -1
  51. package/dist/cjs/components/DataTableViewOptions/index.native.js.map +1 -1
  52. package/dist/cjs/components/FilterManager.cjs +18 -14
  53. package/dist/cjs/components/FilterManager.native.js +18 -14
  54. package/dist/cjs/components/FilterManager.native.js.map +1 -1
  55. package/dist/cjs/components/FilterOperatorValueEditor/index.native.js.map +1 -1
  56. package/dist/cjs/components/KnobDialog.native.js.map +1 -1
  57. package/dist/cjs/components/SavedFilterDropdown.native.js.map +1 -1
  58. package/dist/cjs/components/SimpleTable/index.cjs +1 -1
  59. package/dist/cjs/components/SimpleTable/index.native.js +1 -1
  60. package/dist/cjs/components/SimpleTable/index.native.js.map +1 -1
  61. package/dist/cjs/components/TableInput/index.native.js +4 -1
  62. package/dist/cjs/components/TableInput/index.native.js.map +1 -1
  63. package/dist/cjs/components/TableInput/shared.native.js.map +1 -1
  64. package/dist/cjs/hooks/useDataTable.cjs +17 -1
  65. package/dist/cjs/hooks/useDataTable.native.js +19 -1
  66. package/dist/cjs/hooks/useDataTable.native.js.map +1 -1
  67. package/dist/cjs/hooks/useDataTableKeyboard.native.js.map +1 -1
  68. package/dist/cjs/hooks/useDragScroll.native.js.map +1 -1
  69. package/dist/cjs/hooks/useInfiniteRows.native.js.map +1 -1
  70. package/dist/cjs/hooks/useSavedFilters.native.js.map +1 -1
  71. package/dist/cjs/hooks/useTableColumnPinning.native.js.map +1 -1
  72. package/dist/cjs/hooks/useTableFiltering.native.js.map +1 -1
  73. package/dist/cjs/hooks/useTableGrouping.native.js.map +1 -1
  74. package/dist/cjs/hooks/useTableKeyboard.native.js.map +1 -1
  75. package/dist/cjs/hooks/useTablePagination.native.js.map +1 -1
  76. package/dist/cjs/hooks/useTableSearch.native.js.map +1 -1
  77. package/dist/cjs/hooks/useTableSelection.native.js.map +1 -1
  78. package/dist/cjs/hooks/useTableSorting.native.js.map +1 -1
  79. package/dist/cjs/hooks/useTableUrlSync.native.js.map +1 -1
  80. package/dist/cjs/hooks/useTableViews.native.js.map +1 -1
  81. package/dist/cjs/shared/t.native.js.map +1 -1
  82. package/dist/cjs/shims/primitives.native.js.map +1 -1
  83. package/dist/cjs/shims/tableCellContext.native.js.map +1 -1
  84. package/dist/cjs/types.native.js.map +1 -1
  85. package/dist/cjs/utils/columnAlign.native.js.map +1 -1
  86. package/dist/cjs/utils/filterFields.native.js.map +1 -1
  87. package/dist/cjs/utils/filterUtils.native.js.map +1 -1
  88. package/dist/cjs/utils/paginationDefaults.native.js.map +1 -1
  89. package/dist/cjs/utils/rowPress.cjs +44 -0
  90. package/dist/cjs/utils/rowPress.native.js +56 -0
  91. package/dist/cjs/utils/rowPress.native.js.map +1 -0
  92. package/dist/cjs/utils/urlStateUtils.native.js.map +1 -1
  93. package/dist/cjs/utils/viewData.native.js.map +1 -1
  94. package/dist/cjs/utils/withInterp.native.js.map +1 -1
  95. package/dist/esm/components/ColumnHeaderFilter/index.mjs +1 -0
  96. package/dist/esm/components/ColumnHeaderFilter/index.mjs.map +1 -1
  97. package/dist/esm/components/ColumnHeaderFilter/index.native.js +1 -0
  98. package/dist/esm/components/ColumnHeaderFilter/index.native.js.map +1 -1
  99. package/dist/esm/components/DataTable/DataTableMobileCards.mjs +9 -3
  100. package/dist/esm/components/DataTable/DataTableMobileCards.mjs.map +1 -1
  101. package/dist/esm/components/DataTable/DataTableMobileCards.native.js +12 -3
  102. package/dist/esm/components/DataTable/DataTableMobileCards.native.js.map +1 -1
  103. package/dist/esm/components/DataTable/DataTableRoot.mjs +133 -13
  104. package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
  105. package/dist/esm/components/DataTable/DataTableRoot.native.js +159 -13
  106. package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
  107. package/dist/esm/components/DataTable/DataTableVirtualized.mjs +3 -1
  108. package/dist/esm/components/DataTable/DataTableVirtualized.mjs.map +1 -1
  109. package/dist/esm/components/DataTable/DataTableVirtualized.native.js +3 -1
  110. package/dist/esm/components/DataTable/DataTableVirtualized.native.js.map +1 -1
  111. package/dist/esm/components/DataTable/index.native.js +40 -3
  112. package/dist/esm/components/DataTable/index.native.js.map +1 -1
  113. package/dist/esm/components/DataTableBulkActions/index.mjs +16 -1
  114. package/dist/esm/components/DataTableBulkActions/index.mjs.map +1 -1
  115. package/dist/esm/components/DataTableBulkActions/index.native.js +20 -1
  116. package/dist/esm/components/DataTableBulkActions/index.native.js.map +1 -1
  117. package/dist/esm/components/DataTableCell/CellModalEditor.mjs +4 -8
  118. package/dist/esm/components/DataTableCell/CellModalEditor.mjs.map +1 -1
  119. package/dist/esm/components/DataTableCell/index.mjs +1 -33
  120. package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
  121. package/dist/esm/components/DataTableCell/index.native.js +1 -34
  122. package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
  123. package/dist/esm/components/DataTableColumnPinning/index.mjs +2 -2
  124. package/dist/esm/components/DataTableColumnPinning/index.mjs.map +1 -1
  125. package/dist/esm/components/DataTableColumnPinning/index.native.js +2 -2
  126. package/dist/esm/components/DataTableColumnPinning/index.native.js.map +1 -1
  127. package/dist/esm/components/DataTableExport/index.mjs +1 -0
  128. package/dist/esm/components/DataTableExport/index.mjs.map +1 -1
  129. package/dist/esm/components/DataTableExport/index.native.js +1 -0
  130. package/dist/esm/components/DataTableExport/index.native.js.map +1 -1
  131. package/dist/esm/components/DataTableFilterButton/index.mjs +1 -1
  132. package/dist/esm/components/DataTableFilterButton/index.mjs.map +1 -1
  133. package/dist/esm/components/DataTableFilterButton/index.native.js +4 -1
  134. package/dist/esm/components/DataTableFilterButton/index.native.js.map +1 -1
  135. package/dist/esm/components/DataTableFilterChips/index.mjs +1 -1
  136. package/dist/esm/components/DataTableFilterChips/index.mjs.map +1 -1
  137. package/dist/esm/components/DataTableFilterChips/index.native.js +1 -1
  138. package/dist/esm/components/DataTableGrouping/index.mjs +18 -3
  139. package/dist/esm/components/DataTableGrouping/index.mjs.map +1 -1
  140. package/dist/esm/components/DataTableGrouping/index.native.js +18 -3
  141. package/dist/esm/components/DataTableGrouping/index.native.js.map +1 -1
  142. package/dist/esm/components/DataTableToolbar/index.mjs +1 -0
  143. package/dist/esm/components/DataTableToolbar/index.mjs.map +1 -1
  144. package/dist/esm/components/DataTableToolbar/index.native.js +1 -0
  145. package/dist/esm/components/DataTableToolbar/index.native.js.map +1 -1
  146. package/dist/esm/components/FilterManager.mjs +18 -14
  147. package/dist/esm/components/FilterManager.mjs.map +1 -1
  148. package/dist/esm/components/FilterManager.native.js +18 -14
  149. package/dist/esm/components/FilterManager.native.js.map +1 -1
  150. package/dist/esm/components/SimpleTable/index.mjs +1 -1
  151. package/dist/esm/components/SimpleTable/index.mjs.map +1 -1
  152. package/dist/esm/components/SimpleTable/index.native.js +1 -1
  153. package/dist/esm/components/TableInput/index.native.js +3 -0
  154. package/dist/esm/components/TableInput/index.native.js.map +1 -1
  155. package/dist/esm/hooks/useDataTable.mjs +17 -1
  156. package/dist/esm/hooks/useDataTable.mjs.map +1 -1
  157. package/dist/esm/hooks/useDataTable.native.js +19 -1
  158. package/dist/esm/hooks/useDataTable.native.js.map +1 -1
  159. package/dist/esm/hooks/useTableGrouping.mjs.map +1 -1
  160. package/dist/esm/utils/rowPress.mjs +19 -0
  161. package/dist/esm/utils/rowPress.mjs.map +1 -0
  162. package/dist/esm/utils/rowPress.native.js +28 -0
  163. package/dist/esm/utils/rowPress.native.js.map +1 -0
  164. package/dist/jsx/components/ColumnHeaderFilter/index.mjs +1 -0
  165. package/dist/jsx/components/ColumnHeaderFilter/index.mjs.map +1 -1
  166. package/dist/jsx/components/ColumnHeaderFilter/index.native.js +1 -0
  167. package/dist/jsx/components/ColumnHeaderFilter/index.native.js.map +1 -1
  168. package/dist/jsx/components/DataTable/DataTableMobileCards.mjs +9 -3
  169. package/dist/jsx/components/DataTable/DataTableMobileCards.mjs.map +1 -1
  170. package/dist/jsx/components/DataTable/DataTableMobileCards.native.js +12 -3
  171. package/dist/jsx/components/DataTable/DataTableMobileCards.native.js.map +1 -1
  172. package/dist/jsx/components/DataTable/DataTableRoot.mjs +133 -13
  173. package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
  174. package/dist/jsx/components/DataTable/DataTableRoot.native.js +159 -13
  175. package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
  176. package/dist/jsx/components/DataTable/DataTableVirtualized.mjs +3 -1
  177. package/dist/jsx/components/DataTable/DataTableVirtualized.mjs.map +1 -1
  178. package/dist/jsx/components/DataTable/DataTableVirtualized.native.js +3 -1
  179. package/dist/jsx/components/DataTable/DataTableVirtualized.native.js.map +1 -1
  180. package/dist/jsx/components/DataTable/index.native.js +40 -3
  181. package/dist/jsx/components/DataTable/index.native.js.map +1 -1
  182. package/dist/jsx/components/DataTableBulkActions/index.mjs +16 -1
  183. package/dist/jsx/components/DataTableBulkActions/index.mjs.map +1 -1
  184. package/dist/jsx/components/DataTableBulkActions/index.native.js +20 -1
  185. package/dist/jsx/components/DataTableBulkActions/index.native.js.map +1 -1
  186. package/dist/jsx/components/DataTableCell/CellModalEditor.mjs +4 -8
  187. package/dist/jsx/components/DataTableCell/CellModalEditor.mjs.map +1 -1
  188. package/dist/jsx/components/DataTableCell/index.mjs +1 -33
  189. package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
  190. package/dist/jsx/components/DataTableCell/index.native.js +1 -34
  191. package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
  192. package/dist/jsx/components/DataTableColumnPinning/index.mjs +2 -2
  193. package/dist/jsx/components/DataTableColumnPinning/index.mjs.map +1 -1
  194. package/dist/jsx/components/DataTableColumnPinning/index.native.js +2 -2
  195. package/dist/jsx/components/DataTableColumnPinning/index.native.js.map +1 -1
  196. package/dist/jsx/components/DataTableExport/index.mjs +1 -0
  197. package/dist/jsx/components/DataTableExport/index.mjs.map +1 -1
  198. package/dist/jsx/components/DataTableExport/index.native.js +1 -0
  199. package/dist/jsx/components/DataTableExport/index.native.js.map +1 -1
  200. package/dist/jsx/components/DataTableFilterButton/index.mjs +1 -1
  201. package/dist/jsx/components/DataTableFilterButton/index.mjs.map +1 -1
  202. package/dist/jsx/components/DataTableFilterButton/index.native.js +4 -1
  203. package/dist/jsx/components/DataTableFilterButton/index.native.js.map +1 -1
  204. package/dist/jsx/components/DataTableFilterChips/index.mjs +1 -1
  205. package/dist/jsx/components/DataTableFilterChips/index.mjs.map +1 -1
  206. package/dist/jsx/components/DataTableFilterChips/index.native.js +1 -1
  207. package/dist/jsx/components/DataTableGrouping/index.mjs +18 -3
  208. package/dist/jsx/components/DataTableGrouping/index.mjs.map +1 -1
  209. package/dist/jsx/components/DataTableGrouping/index.native.js +17 -1
  210. package/dist/jsx/components/DataTableGrouping/index.native.js.map +1 -1
  211. package/dist/jsx/components/DataTableToolbar/index.mjs +1 -0
  212. package/dist/jsx/components/DataTableToolbar/index.mjs.map +1 -1
  213. package/dist/jsx/components/DataTableToolbar/index.native.js +1 -0
  214. package/dist/jsx/components/DataTableToolbar/index.native.js.map +1 -1
  215. package/dist/jsx/components/FilterManager.mjs +18 -14
  216. package/dist/jsx/components/FilterManager.mjs.map +1 -1
  217. package/dist/jsx/components/FilterManager.native.js +18 -14
  218. package/dist/jsx/components/FilterManager.native.js.map +1 -1
  219. package/dist/jsx/components/SimpleTable/index.mjs +1 -1
  220. package/dist/jsx/components/SimpleTable/index.mjs.map +1 -1
  221. package/dist/jsx/components/SimpleTable/index.native.js +1 -1
  222. package/dist/jsx/components/SimpleTable/index.native.js.map +1 -1
  223. package/dist/jsx/components/TableInput/index.native.js +3 -0
  224. package/dist/jsx/components/TableInput/index.native.js.map +1 -1
  225. package/dist/jsx/hooks/useDataTable.mjs +17 -1
  226. package/dist/jsx/hooks/useDataTable.mjs.map +1 -1
  227. package/dist/jsx/hooks/useDataTable.native.js +19 -1
  228. package/dist/jsx/hooks/useDataTable.native.js.map +1 -1
  229. package/dist/jsx/hooks/useTableGrouping.mjs.map +1 -1
  230. package/dist/jsx/utils/rowPress.mjs +19 -0
  231. package/dist/jsx/utils/rowPress.mjs.map +1 -0
  232. package/dist/jsx/utils/rowPress.native.js +56 -0
  233. package/dist/jsx/utils/rowPress.native.js.map +1 -0
  234. package/package.json +12 -12
  235. package/src/components/ColumnHeaderFilter/index.tsx +1 -0
  236. package/src/components/DataTable/DataTable.spec.tsx +4 -1
  237. package/src/components/DataTable/DataTable.stories.tsx +190 -5
  238. package/src/components/DataTable/DataTableMobileCards.tsx +30 -4
  239. package/src/components/DataTable/DataTableRoot.tsx +195 -8
  240. package/src/components/DataTable/DataTableVirtualized.tsx +8 -0
  241. package/src/components/DataTable/cardLayout.spec.tsx +4 -1
  242. package/src/components/DataTable/customCellAlign.spec.tsx +124 -0
  243. package/src/components/DataTable/disabledContract.spec.tsx +4 -3
  244. package/src/components/DataTable/index.native.tsx +47 -1
  245. package/src/components/DataTable/rowPress.spec.tsx +229 -0
  246. package/src/components/DataTable/selectionSemantics.spec.tsx +156 -0
  247. package/src/components/DataTableBulkActions/index.tsx +16 -2
  248. package/src/components/DataTableBulkActions/selectionCountI18n.spec.tsx +94 -0
  249. package/src/components/DataTableCell/CellModalEditor.tsx +3 -1
  250. package/src/components/DataTableCell/DataTableCell.stories.tsx +1 -5
  251. package/src/components/DataTableCell/cellDraftPolicy.spec.tsx +10 -2
  252. package/src/components/DataTableCell/dataTableCell.spec.tsx +10 -2
  253. package/src/components/DataTableCell/index.tsx +5 -36
  254. package/src/components/DataTableColumnPinning/DataTableColumnPinning.stories.tsx +49 -17
  255. package/src/components/DataTableDateFilter/DataTableDateFilter.stories.tsx +4 -3
  256. package/src/components/DataTableExport/DataTableExport.stories.tsx +3 -2
  257. package/src/components/DataTableExport/index.tsx +1 -0
  258. package/src/components/DataTableFilterButton/index.tsx +4 -1
  259. package/src/components/DataTableGrouping/DataTableGrouping.spec.tsx +59 -2
  260. package/src/components/DataTableGrouping/DataTableGrouping.stories.tsx +17 -5
  261. package/src/components/DataTableGrouping/index.tsx +1 -0
  262. package/src/components/DataTablePagination/DataTablePagination.stories.tsx +34 -20
  263. package/src/components/DataTableToolbar/index.tsx +1 -0
  264. package/src/components/DataTableViewOptions/DataTableViewOptions.stories.tsx +2 -1
  265. package/src/components/FilterManager.tsx +21 -13
  266. package/src/components/TableInput/index.native.tsx +3 -0
  267. package/src/components/__scratch__/scratch.spec.tsx +46 -0
  268. package/src/hooks/useDataTable.ts +27 -1
  269. package/src/types.ts +25 -0
  270. package/src/utils/rowPress.ts +58 -0
  271. package/types/components/ColumnHeaderFilter/index.d.ts.map +1 -1
  272. package/types/components/DataTable/DataTableMobileCards.d.ts +13 -1
  273. package/types/components/DataTable/DataTableMobileCards.d.ts.map +1 -1
  274. package/types/components/DataTable/DataTableRoot.d.ts +1 -1
  275. package/types/components/DataTable/DataTableRoot.d.ts.map +1 -1
  276. package/types/components/DataTable/DataTableVirtualized.d.ts +7 -1
  277. package/types/components/DataTable/DataTableVirtualized.d.ts.map +1 -1
  278. package/types/components/DataTable/index.native.d.ts +1 -1
  279. package/types/components/DataTable/index.native.d.ts.map +1 -1
  280. package/types/components/DataTableBulkActions/index.d.ts.map +1 -1
  281. package/types/components/DataTableCell/CellModalEditor.d.ts.map +1 -1
  282. package/types/components/DataTableCell/index.d.ts.map +1 -1
  283. package/types/components/DataTableColumnPinning/index.d.ts.map +1 -1
  284. package/types/components/DataTableFilterButton/index.d.ts.map +1 -1
  285. package/types/components/DataTableGrouping/index.d.ts +8 -0
  286. package/types/components/DataTableGrouping/index.d.ts.map +1 -1
  287. package/types/components/DataTableToolbar/index.d.ts.map +1 -1
  288. package/types/components/FilterManager.d.ts +7 -1
  289. package/types/components/FilterManager.d.ts.map +1 -1
  290. package/types/components/TableInput/index.native.d.ts.map +1 -1
  291. package/types/hooks/useDataTable.d.ts.map +1 -1
  292. package/types/types.d.ts +23 -0
  293. package/types/types.d.ts.map +1 -1
  294. package/types/utils/rowPress.d.ts +20 -0
  295. package/types/utils/rowPress.d.ts.map +1 -0
@@ -213,7 +213,10 @@ describe("DataTable card layout", () => {
213
213
  onStateChange={vi.fn()}
214
214
  />,
215
215
  );
216
- expect(getAllByLabelText(/^Select row \d+$/).length).toBe(sampleData.length);
216
+ // Selection toggles are named by ROW IDENTITY (first cell value /
217
+ // rowLabel), not a bare position — "Select row Alice", not "Select
218
+ // row 1" (DG-A11Y-06; selectionSemantics.spec pins the derivation).
219
+ expect(getAllByLabelText(/^Select row .+$/).length).toBe(sampleData.length);
217
220
  expect(container.querySelector('[data-testid="data-table-cards"]')).not.toBeNull();
218
221
  });
219
222
  });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Custom-cell alignment (DG-TBL-01 / LC-44 ROW-LINE).
3
+ *
4
+ * Custom `cell` renderers historically LOST the column's `align` on web:
5
+ * in the semantic-table displayMode (columnLayout="auto") `Table.Cell` is
6
+ * `display: table-cell`, where the cell box's own flex `justifyContent` is
7
+ * a no-op — only the built-in DataTableCell self-aligned, and every custom
8
+ * renderer had to hand-roll its own alignment wrapper (the InsightsRowLink
9
+ * class of bug). The fix: DataTable wraps flexRender output in a
10
+ * full-width flex container driven by the resolved column align
11
+ * (`data-cell-align`), in every displayMode, so custom cells align like
12
+ * built-ins with zero consumer code.
13
+ */
14
+
15
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
16
+ import { screen } from "@testing-library/react";
17
+ import { describe, expect, it } from "vitest";
18
+ import { Text } from "tamagui";
19
+ import type { DataTableColumn } from "../../types";
20
+ import { DataTable } from "./index";
21
+
22
+ type Pokemon = {
23
+ id: number;
24
+ name: string;
25
+ xp: number;
26
+ active: boolean;
27
+ };
28
+
29
+ const sampleData: Pokemon[] = [
30
+ { id: 1, name: "Bulbasaur", xp: 64, active: true },
31
+ { id: 2, name: "Charmander", xp: 62, active: false },
32
+ ];
33
+
34
+ const customColumns: DataTableColumn<Pokemon>[] = [
35
+ {
36
+ id: "name",
37
+ accessorKey: "name",
38
+ header: "Name",
39
+ cell: ({ getValue }) => <Text>{String(getValue())}</Text>,
40
+ },
41
+ {
42
+ id: "xp",
43
+ accessorKey: "xp",
44
+ header: "XP",
45
+ align: "right",
46
+ cell: ({ getValue }) => <Text>{String(getValue())}</Text>,
47
+ },
48
+ {
49
+ id: "active",
50
+ accessorKey: "active",
51
+ header: "Active",
52
+ align: "center",
53
+ cell: ({ getValue }) => <Text>{getValue() ? "yes" : "no"}</Text>,
54
+ },
55
+ // Built-in field path (no custom cell) — must NOT grow a wrapper.
56
+ // (accessor `id`, not `xp`: the custom XP column also paints its value,
57
+ // and duplicated cell text breaks the single-match queries below.)
58
+ { id: "plain_id", accessorKey: "id", header: "Plain ID", field: "stepper" },
59
+ ];
60
+
61
+ function alignContainerOf(el: HTMLElement): HTMLElement | null {
62
+ return el.closest("[data-cell-align]") as HTMLElement | null;
63
+ }
64
+
65
+ function renderTable(columnLayout: "auto" | "equal") {
66
+ return renderWithProviders(
67
+ <DataTable
68
+ data={sampleData}
69
+ columns={customColumns as DataTableColumn<Record<string, any>>[]}
70
+ layout="table"
71
+ columnLayout={columnLayout}
72
+ enablePagination={false}
73
+ readOnly
74
+ />,
75
+ );
76
+ }
77
+
78
+ describe.each(["auto", "equal"] as const)(
79
+ "DataTable custom-cell alignment (columnLayout=%s)",
80
+ (columnLayout) => {
81
+ it("wraps custom cells in an alignment container driven by the column align", () => {
82
+ renderTable(columnLayout);
83
+
84
+ const nameContainer = alignContainerOf(screen.getByText("Bulbasaur"));
85
+ expect(nameContainer).not.toBeNull();
86
+ expect(nameContainer!.getAttribute("data-cell-align")).toBe("left");
87
+
88
+ // The wrapper is a full-width flex row whose justifyContent follows
89
+ // the resolved align (this is what display:table-cell's dead
90
+ // justifyContent could not do). happy-dom does not resolve tamagui's
91
+ // class-based styles, so computed flex geometry (display:flex,
92
+ // justify-content, shared ink edges ±1px) is pinned in the live
93
+ // browser probe `p-results/table-rowpress/probe.mjs`; here the
94
+ // structural contract is the container + its declared alignment.
95
+ const xpContainer = alignContainerOf(screen.getByText("64"));
96
+ expect(xpContainer).not.toBeNull();
97
+ expect(xpContainer!.getAttribute("data-cell-align")).toBe("right");
98
+
99
+ const centerContainer = alignContainerOf(screen.getAllByText("yes")[0]);
100
+ expect(centerContainer).not.toBeNull();
101
+ expect(centerContainer!.getAttribute("data-cell-align")).toBe("center");
102
+ });
103
+
104
+ it("headers carry the same resolved alignment as their cells", () => {
105
+ const { container } = renderTable(columnLayout);
106
+ const xpHeader = container.querySelector("#xp-header");
107
+ expect(xpHeader?.getAttribute("data-align")).toBe("right");
108
+ const nameHeader = container.querySelector("#name-header");
109
+ expect(nameHeader?.getAttribute("data-align")).toBe("left");
110
+ });
111
+
112
+ it("built-in field cells stay unwrapped (they self-align internally)", () => {
113
+ const { container } = renderTable(columnLayout);
114
+ // Body cells for the plain field column contain no alignment wrapper.
115
+ const bodyCells = Array.from(
116
+ container.querySelectorAll('[data-align="right"]'),
117
+ ) as HTMLElement[];
118
+ const plainFieldCells = bodyCells.filter(
119
+ (cell) => cell.id === "" && !cell.querySelector("[data-cell-align]"),
120
+ );
121
+ expect(plainFieldCells.length).toBeGreaterThan(0);
122
+ });
123
+ },
124
+ );
@@ -156,7 +156,8 @@ describe("DataTable disabled contract (grid layout)", () => {
156
156
  onStateChange={vi.fn()}
157
157
  />,
158
158
  );
159
- const rowCheckbox = getAllByLabelText(/^Select row 1$/)[0];
159
+ // Selection toggles are named by row identity (first cell value).
160
+ const rowCheckbox = getAllByLabelText(/^Select row John$/)[0];
160
161
  fireEvent.click(rowCheckbox);
161
162
  await waitFor(() => {
162
163
  expect(container.querySelectorAll('[aria-checked="true"]').length).toBe(0);
@@ -233,8 +234,8 @@ describe("DataTable disabled contract (card layout)", () => {
233
234
  expect(onDataChange).not.toHaveBeenCalled();
234
235
  });
235
236
 
236
- // Selection does not toggle.
237
- fireEvent.click(getAllByLabelText(/^Select row 1$/)[0]);
237
+ // Selection does not toggle. (Toggles are named by row identity.)
238
+ fireEvent.click(getAllByLabelText(/^Select row John$/)[0]);
238
239
  await waitFor(() => {
239
240
  expect(container.querySelectorAll('[aria-checked="true"]').length).toBe(0);
240
241
  });
@@ -134,6 +134,8 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
134
134
  enableColumnPinning = false,
135
135
  maxPinnedColumns = 3,
136
136
  enableKeyboardNavigation: _enableKeyboardNavigation = false,
137
+ onRowPress,
138
+ rowLabel,
137
139
  readOnly = false,
138
140
  onCellChange,
139
141
  name,
@@ -456,6 +458,47 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
456
458
 
457
459
  const tableZebraOn = knobProps.tableZebra === "on";
458
460
 
461
+ // ---- Row press (onRowPress): the ROW is one pressable unit ----
462
+ // Native twin of the web contract: press anywhere on the row activates.
463
+ // Inner controls need no isolation walk here — native nested pressables
464
+ // claim the gesture responder, so a child press never bubbles to the row
465
+ // (see utils/rowPress.ts). Roving tabindex is a web-only concern.
466
+ const rowPressActive = Boolean(onRowPress) && !disabled;
467
+
468
+ const deriveRowLabel = useCallback(
469
+ (row: (typeof tableRows)[0], index: number): string => {
470
+ const fromAccessor = rowLabel?.(row.original as TData);
471
+ if (fromAccessor) return fromAccessor;
472
+ let firstCellValue: unknown;
473
+ try {
474
+ firstCellValue = row.getVisibleCells()[0]?.getValue();
475
+ } catch {
476
+ firstCellValue = undefined;
477
+ }
478
+ if (
479
+ (typeof firstCellValue === "string" && firstCellValue.length > 0) ||
480
+ typeof firstCellValue === "number"
481
+ ) {
482
+ return String(firstCellValue);
483
+ }
484
+ return withInterp(t("Row {{row}}", { row: index + 1 }), { row: index + 1 });
485
+ },
486
+ [rowLabel, t],
487
+ );
488
+
489
+ const getRowPressProps = useCallback(
490
+ (row: (typeof tableRows)[0], index: number) => {
491
+ if (!rowPressActive) return {};
492
+ return {
493
+ onPress: () => onRowPress?.(row.original as TData, index),
494
+ role: "button" as const,
495
+ "aria-label": deriveRowLabel(row, index),
496
+ pressStyle: { backgroundColor: "$color4" },
497
+ };
498
+ },
499
+ [rowPressActive, onRowPress, deriveRowLabel],
500
+ );
501
+
459
502
  const renderRow = useCallback(
460
503
  (row: (typeof tableRows)[0], index: number) => (
461
504
  <Table.Row
@@ -467,11 +510,12 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
467
510
  {...({
468
511
  "data-table-zebra": tableZebraOn ? (index % 2 === 0 ? "even" : "odd") : "off",
469
512
  } as any)}
513
+ {...(getRowPressProps(row, index) as any)}
470
514
  >
471
515
  {renderRowContent(row, index)}
472
516
  </Table.Row>
473
517
  ),
474
- [renderRowContent, tableZebraOn, t],
518
+ [renderRowContent, tableZebraOn, t, getRowPressProps],
475
519
  );
476
520
 
477
521
  const renderMobileCard = useCallback(
@@ -487,6 +531,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
487
531
  borderColor="$borderColor"
488
532
  padding="$3"
489
533
  marginBottom="$2"
534
+ {...(getRowPressProps(row, index) as any)}
490
535
  >
491
536
  {showRowNumbers && (
492
537
  <View
@@ -563,6 +608,7 @@ function DataTableRoot<TData extends Record<string, any> = Record<string, any>>(
563
608
  minRows,
564
609
  renderCellContent,
565
610
  t,
611
+ getRowPressProps,
566
612
  ],
567
613
  );
568
614
 
@@ -0,0 +1,229 @@
1
+ /**
2
+ * onRowPress — first-class row activation (LC-49 ONE-TAB-STOP, LC-50
3
+ * KEY-ACTIVATION, DG-A11Y-06 names).
4
+ *
5
+ * The row is ONE pressable unit: click/tap anywhere activates, Enter AND
6
+ * Space activate from the keyboard, the row set exposes exactly one tab
7
+ * stop (roving tabindex, arrows/Home/End move it), and presses that
8
+ * originate on inner interactive controls (selection checkboxes, buttons)
9
+ * never reach the row handler. Cards (layout="cards") take the same
10
+ * contract with role="button".
11
+ *
12
+ * layout="table" pins the grid below the $sm test-env breakpoint.
13
+ */
14
+
15
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
16
+ import { fireEvent, screen, waitFor } from "@testing-library/react";
17
+ import { describe, expect, it, vi } from "vitest";
18
+ import { Button } from "@multiplatform.one/forms";
19
+ import type { DataTableColumn } from "../../types";
20
+ import { DataTable } from "./index";
21
+
22
+ type Person = {
23
+ id: number;
24
+ firstName: string;
25
+ age: number;
26
+ };
27
+
28
+ const sampleData: Person[] = [
29
+ { id: 1, firstName: "Alice", age: 30 },
30
+ { id: 2, firstName: "Bob", age: 40 },
31
+ { id: 3, firstName: "Carol", age: 20 },
32
+ ];
33
+
34
+ // Explicit ids: DataTable seeds columnOrder from `col.id || ""`, so mixing
35
+ // id-less accessor columns with an id'd display column would reorder the
36
+ // display column first (and identity fallbacks would read it as the "first
37
+ // visible cell").
38
+ const columns: DataTableColumn<Person>[] = [
39
+ { id: "firstName", accessorKey: "firstName", header: "First Name" },
40
+ { id: "age", accessorKey: "age", header: "Age" },
41
+ ];
42
+
43
+ function pressRows(container: HTMLElement): HTMLElement[] {
44
+ return Array.from(container.querySelectorAll('[data-row-press="true"]')) as HTMLElement[];
45
+ }
46
+
47
+ function renderRowPressTable(extraProps: Record<string, unknown> = {}) {
48
+ const onRowPress = vi.fn();
49
+ const result = renderWithProviders(
50
+ <DataTable
51
+ data={sampleData}
52
+ columns={columns as DataTableColumn<Record<string, any>>[]}
53
+ layout="table"
54
+ enablePagination={false}
55
+ onRowPress={onRowPress}
56
+ {...extraProps}
57
+ />,
58
+ );
59
+ return { ...result, onRowPress };
60
+ }
61
+
62
+ describe("DataTable onRowPress (grid)", () => {
63
+ it("activates on a click anywhere in the row with the row data and index", () => {
64
+ const { container, onRowPress } = renderRowPressTable();
65
+ const rows = pressRows(container);
66
+ expect(rows).toHaveLength(3);
67
+
68
+ fireEvent.click(screen.getByText("Bob"));
69
+ expect(onRowPress).toHaveBeenCalledTimes(1);
70
+ expect(onRowPress).toHaveBeenCalledWith(
71
+ expect.objectContaining({ firstName: "Bob", id: 2 }),
72
+ 1,
73
+ );
74
+ });
75
+
76
+ it("exposes exactly one tab stop; arrows and Home/End rove it (LC-49)", async () => {
77
+ const { container } = renderRowPressTable();
78
+ const rows = pressRows(container);
79
+
80
+ const tabStops = () => rows.map((row) => row.getAttribute("tabindex"));
81
+ expect(tabStops()).toEqual(["0", "-1", "-1"]);
82
+
83
+ rows[0].focus();
84
+ expect(document.activeElement).toBe(rows[0]);
85
+
86
+ fireEvent.keyDown(rows[0], { key: "ArrowDown" });
87
+ expect(document.activeElement).toBe(rows[1]);
88
+ // The anchor follows focus: still exactly one tabindex=0.
89
+ await waitFor(() => {
90
+ const stops = pressRows(container).map((row) => row.getAttribute("tabindex"));
91
+ expect(stops).toEqual(["-1", "0", "-1"]);
92
+ });
93
+
94
+ fireEvent.keyDown(rows[1], { key: "End" });
95
+ expect(document.activeElement).toBe(rows[2]);
96
+ fireEvent.keyDown(rows[2], { key: "Home" });
97
+ expect(document.activeElement).toBe(rows[0]);
98
+ fireEvent.keyDown(rows[0], { key: "ArrowUp" });
99
+ // Clamped at the first row — no wrap.
100
+ expect(document.activeElement).toBe(rows[0]);
101
+ });
102
+
103
+ it("activates on Enter AND Space, once per physical key press (LC-50)", () => {
104
+ const { container, onRowPress } = renderRowPressTable();
105
+ const rows = pressRows(container);
106
+ rows[0].focus();
107
+
108
+ fireEvent.keyDown(rows[0], { key: "Enter" });
109
+ expect(onRowPress).toHaveBeenCalledTimes(1);
110
+ expect(onRowPress).toHaveBeenLastCalledWith(expect.objectContaining({ firstName: "Alice" }), 0);
111
+
112
+ fireEvent.keyDown(rows[0], { key: " " });
113
+ expect(onRowPress).toHaveBeenCalledTimes(2);
114
+
115
+ // Key auto-repeat never machine-guns activations.
116
+ fireEvent.keyDown(rows[0], { key: "Enter", repeat: true });
117
+ expect(onRowPress).toHaveBeenCalledTimes(2);
118
+ });
119
+
120
+ it("ignores presses that originate on inner interactive controls", () => {
121
+ const buttonPress = vi.fn();
122
+ const interactiveColumns: DataTableColumn<Person>[] = [
123
+ ...columns,
124
+ {
125
+ id: "row_actions",
126
+ header: "Actions",
127
+ cell: ({ row }) => (
128
+ <Button size="$2" onPress={() => buttonPress(row.original.id)}>
129
+ Edit
130
+ </Button>
131
+ ),
132
+ },
133
+ ];
134
+ const { container, onRowPress } = renderRowPressTable({
135
+ columns: interactiveColumns as DataTableColumn<Record<string, any>>[],
136
+ enableRowSelection: true,
137
+ });
138
+
139
+ // Selection checkbox: toggles selection, never fires the row press.
140
+ const rowCheckbox = screen.getByRole("checkbox", { name: "Select row Alice" });
141
+ fireEvent.click(rowCheckbox);
142
+ expect(rowCheckbox.getAttribute("aria-checked")).toBe("true");
143
+ expect(onRowPress).not.toHaveBeenCalled();
144
+
145
+ // Inner button: its own handler fires, the row press does not.
146
+ fireEvent.click(screen.getAllByText("Edit")[0]);
147
+ expect(buttonPress).toHaveBeenCalledWith(1);
148
+ expect(onRowPress).not.toHaveBeenCalled();
149
+
150
+ // A plain cell in the same row still activates the row.
151
+ fireEvent.click(screen.getByText("Alice"));
152
+ expect(onRowPress).toHaveBeenCalledTimes(1);
153
+ expect(container.querySelectorAll('[data-row-press="true"]').length).toBe(3);
154
+ });
155
+
156
+ it("names rows: rowLabel accessor wins; first cell value is the fallback", () => {
157
+ const { container, unmount } = renderRowPressTable({
158
+ rowLabel: (row: Person) => `Open ${row.firstName}`,
159
+ });
160
+ expect(pressRows(container).map((row) => row.getAttribute("aria-label"))).toEqual([
161
+ "Open Alice",
162
+ "Open Bob",
163
+ "Open Carol",
164
+ ]);
165
+ unmount();
166
+
167
+ const { container: fallbackContainer } = renderRowPressTable();
168
+ expect(pressRows(fallbackContainer)[0].getAttribute("aria-label")).toBe("Alice");
169
+ });
170
+ });
171
+
172
+ describe("DataTable onRowPress (cards layout)", () => {
173
+ it("cards are pressable buttons with the same activation + isolation contract", async () => {
174
+ const onRowPress = vi.fn();
175
+ renderWithProviders(
176
+ <DataTable
177
+ data={sampleData}
178
+ columns={columns as DataTableColumn<Record<string, any>>[]}
179
+ layout="cards"
180
+ enablePagination={false}
181
+ enableRowSelection
182
+ onRowPress={onRowPress}
183
+ rowLabel={(row) => (row as Person).firstName}
184
+ />,
185
+ );
186
+
187
+ const card = screen.getByRole("button", { name: "Bob" });
188
+ expect(card.getAttribute("data-row-press")).toBe("true");
189
+
190
+ // Inner selection checkbox stays isolated from the card press.
191
+ const checkbox = screen.getByRole("checkbox", { name: "Select row Bob" });
192
+ fireEvent.click(checkbox);
193
+ expect(checkbox.getAttribute("aria-checked")).toBe("true");
194
+ expect(onRowPress).not.toHaveBeenCalled();
195
+
196
+ fireEvent.click(card);
197
+ expect(onRowPress).toHaveBeenCalledTimes(1);
198
+ expect(onRowPress).toHaveBeenCalledWith(expect.objectContaining({ firstName: "Bob" }), 1);
199
+
200
+ // Enter activates the focused card (LC-50).
201
+ card.focus();
202
+ fireEvent.keyDown(card, { key: "Enter" });
203
+ await waitFor(() => expect(onRowPress).toHaveBeenCalledTimes(2));
204
+ });
205
+
206
+ it("card list exposes one roving tab stop scoped to the list (LC-49)", () => {
207
+ const { container } = renderWithProviders(
208
+ <DataTable
209
+ data={sampleData}
210
+ columns={columns as DataTableColumn<Record<string, any>>[]}
211
+ layout="cards"
212
+ enablePagination={false}
213
+ onRowPress={vi.fn()}
214
+ />,
215
+ );
216
+ const cards = pressRows(container as HTMLElement);
217
+ expect(cards).toHaveLength(3);
218
+ expect(cards.map((card) => card.getAttribute("tabindex"))).toEqual(["0", "-1", "-1"]);
219
+ expect(
220
+ (container as HTMLElement).querySelector(
221
+ '[data-row-press-scope="true"] [data-row-press="true"]',
222
+ ),
223
+ ).not.toBeNull();
224
+
225
+ cards[0].focus();
226
+ fireEvent.keyDown(cards[0], { key: "ArrowDown" });
227
+ expect(document.activeElement).toBe(cards[1]);
228
+ });
229
+ });
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Row-selection semantics (desk-parity audit fix).
3
+ *
4
+ * The audit finding: list-row selection toggles rendered as anonymous
5
+ * pressables — no role=checkbox, no aria-checked, invisible to AT and
6
+ * automation; the bulk-actions bar was unreachable without a mouse. These
7
+ * specs pin the REAL checkbox contract on the package selection column:
8
+ * every toggle is role="checkbox" with aria-checked (header select-all
9
+ * supports "mixed"), named by row identity ("Select row {name}"), meets
10
+ * the 44px press floor through the control's own box (LC-54), toggles
11
+ * from the keyboard, and keyboard-only selection reaches the bulk bar.
12
+ */
13
+
14
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
15
+ import { fireEvent, screen, waitFor } from "@testing-library/react";
16
+ import userEvent from "@testing-library/user-event";
17
+ import { describe, expect, it, vi } from "vitest";
18
+ import type { BulkAction, DataTableColumn } from "../../types";
19
+ import { DataTable } from "./index";
20
+
21
+ type Person = {
22
+ id: number;
23
+ firstName: string;
24
+ age: number;
25
+ };
26
+
27
+ const sampleData: Person[] = [
28
+ { id: 1, firstName: "Alice", age: 30 },
29
+ { id: 2, firstName: "Bob", age: 40 },
30
+ { id: 3, firstName: "Carol", age: 20 },
31
+ ];
32
+
33
+ const columns: DataTableColumn<Person>[] = [
34
+ { accessorKey: "firstName", header: "First Name" },
35
+ { accessorKey: "age", header: "Age" },
36
+ ];
37
+
38
+ function renderSelectionTable(extraProps: Record<string, unknown> = {}) {
39
+ const bulkExecute = vi.fn();
40
+ const bulkActions: BulkAction<Person>[] = [
41
+ { id: "archive", label: "Archive", onExecute: bulkExecute },
42
+ ];
43
+ const result = renderWithProviders(
44
+ <DataTable
45
+ data={sampleData}
46
+ columns={columns as DataTableColumn<Record<string, any>>[]}
47
+ layout="table"
48
+ enablePagination={false}
49
+ enableRowSelection
50
+ bulkActions={bulkActions as BulkAction<Record<string, any>>[]}
51
+ {...extraProps}
52
+ />,
53
+ );
54
+ return { ...result, bulkExecute };
55
+ }
56
+
57
+ describe("DataTable selection semantics", () => {
58
+ it("row toggles are real checkboxes named by row identity", () => {
59
+ renderSelectionTable();
60
+
61
+ const rowCheckboxes = [
62
+ screen.getByRole("checkbox", { name: "Select row Alice" }),
63
+ screen.getByRole("checkbox", { name: "Select row Bob" }),
64
+ screen.getByRole("checkbox", { name: "Select row Carol" }),
65
+ ];
66
+ for (const checkbox of rowCheckboxes) {
67
+ expect(checkbox.getAttribute("aria-checked")).toBe("false");
68
+ // Real disclosure for automation: state is machine-readable, and the
69
+ // toggle is a focusable control (not an anonymous pressable View).
70
+ expect(checkbox.getAttribute("role")).toBe("checkbox");
71
+ expect((checkbox as HTMLElement).tagName).toBe("BUTTON");
72
+ }
73
+ });
74
+
75
+ it("rowLabel drives the checkbox names when provided", () => {
76
+ renderSelectionTable({
77
+ rowLabel: (row: Person) => `${row.firstName} #${row.id}`,
78
+ });
79
+ expect(screen.getByRole("checkbox", { name: "Select row Alice #1" })).toBeDefined();
80
+ expect(screen.getByRole("checkbox", { name: "Select row Carol #3" })).toBeDefined();
81
+ });
82
+
83
+ it("meets the 44px press floor through the control's own box (LC-54)", () => {
84
+ renderSelectionTable();
85
+ const checkbox = screen.getByRole("checkbox", { name: "Select row Alice" });
86
+ const style = getComputedStyle(checkbox);
87
+ expect(parseFloat(style.minWidth)).toBeGreaterThanOrEqual(44);
88
+ expect(parseFloat(style.minHeight)).toBeGreaterThanOrEqual(44);
89
+ });
90
+
91
+ it("header select-all exposes aria-checked including the mixed state", async () => {
92
+ renderSelectionTable();
93
+ const selectAll = screen.getByRole("checkbox", { name: "Select all rows on page" });
94
+ expect(selectAll.getAttribute("aria-checked")).toBe("false");
95
+
96
+ fireEvent.click(screen.getByRole("checkbox", { name: "Select row Alice" }));
97
+ await waitFor(() => expect(selectAll.getAttribute("aria-checked")).toBe("mixed"));
98
+
99
+ fireEvent.click(selectAll);
100
+ await waitFor(() => expect(selectAll.getAttribute("aria-checked")).toBe("true"));
101
+ for (const name of ["Alice", "Bob", "Carol"]) {
102
+ expect(
103
+ screen.getByRole("checkbox", { name: `Select row ${name}` }).getAttribute("aria-checked"),
104
+ ).toBe("true");
105
+ }
106
+
107
+ fireEvent.click(selectAll);
108
+ await waitFor(() => expect(selectAll.getAttribute("aria-checked")).toBe("false"));
109
+ });
110
+
111
+ it("keyboard-only selection reaches the bulk-actions bar", async () => {
112
+ const user = userEvent.setup();
113
+ const { bulkExecute } = renderSelectionTable();
114
+ expect(screen.queryByText("1 selected")).toBeNull();
115
+
116
+ // Space on the focused checkbox (real keystroke; the role=checkbox
117
+ // frame is a native button, so Space activates it).
118
+ const alice = screen.getByRole("checkbox", { name: "Select row Alice" });
119
+ (alice as HTMLElement).focus();
120
+ expect(document.activeElement).toBe(alice);
121
+ await user.keyboard(" ");
122
+ await waitFor(() => expect(alice.getAttribute("aria-checked")).toBe("true"));
123
+
124
+ // The bulk bar appears with the count and reachable actions.
125
+ expect(await screen.findByText("1 selected")).toBeDefined();
126
+
127
+ const carol = screen.getByRole("checkbox", { name: "Select row Carol" });
128
+ (carol as HTMLElement).focus();
129
+ await user.keyboard(" ");
130
+ expect(await screen.findByText("2 selected")).toBeDefined();
131
+
132
+ // The bar's actions are real buttons — Enter activates without a pointer.
133
+ const archive = screen.getByRole("button", { name: "Archive" });
134
+ (archive as HTMLElement).focus();
135
+ await user.keyboard("{Enter}");
136
+ await waitFor(() => expect(bulkExecute).toHaveBeenCalledTimes(1));
137
+ expect(bulkExecute.mock.calls[0][0]).toHaveLength(2);
138
+ });
139
+
140
+ it("cards layout carries the same checkbox semantics", () => {
141
+ renderWithProviders(
142
+ <DataTable
143
+ data={sampleData}
144
+ columns={columns as DataTableColumn<Record<string, any>>[]}
145
+ layout="cards"
146
+ enablePagination={false}
147
+ enableRowSelection
148
+ />,
149
+ );
150
+ const checkbox = screen.getByRole("checkbox", { name: "Select row Bob" });
151
+ expect(checkbox.getAttribute("aria-checked")).toBe("false");
152
+ fireEvent.click(checkbox);
153
+ expect(checkbox.getAttribute("aria-checked")).toBe("true");
154
+ expect(screen.getByRole("checkbox", { name: "Select all rows" })).toBeDefined();
155
+ });
156
+ });
@@ -1,10 +1,12 @@
1
1
  import { DotsThreeIcon } from "@phosphor-icons/react";
2
2
  import { useAccentOnSurface, useResolvedKnobs } from "@multiplatform.one/theme";
3
3
  import { useState } from "react";
4
+ import { useTranslation } from "react-i18next";
4
5
  import { Button, zIndex } from "@multiplatform.one/forms";
5
6
  import { ConfirmDialog, DropdownMenu } from "@multiplatform.one/components";
6
7
  import { Text, View, XStack } from "tamagui";
7
8
  import type { BulkAction } from "../../types";
9
+ import { withInterp } from "../../utils/withInterp";
8
10
  import { useDataTableState } from "../DataTable/DataTableStateContext";
9
11
 
10
12
  export type { BulkAction };
@@ -43,6 +45,7 @@ export function DataTableBulkActions<T = any>(props: DataTableBulkActionsProps<T
43
45
 
44
46
  const [confirmAction, setConfirmAction] = useState<BulkAction<T> | null>(null);
45
47
  const [isExecuting, setIsExecuting] = useState<string | null>(null);
48
+ const { t } = useTranslation();
46
49
  const { knobProps } = useResolvedKnobs();
47
50
  // UX-P04 / Axiom 12: the selection count is accent-hued text on the bar
48
51
  // surface (page), so it needs the page-readable accent — `$accentColor` (the
@@ -135,9 +138,20 @@ export function DataTableBulkActions<T = any>(props: DataTableBulkActionsProps<T
135
138
  >
136
139
  {showSelectionCount && (
137
140
  <Text fontSize="$3" fontWeight="600" color={countColor}>
141
+ {/* D-15: one whole-sentence key per variant (never concatenated
142
+ fragments — word order is locale-owned); withInterp fills the
143
+ params when no i18n instance is mounted (house table pattern). */}
138
144
  {filteredOutCount > 0
139
- ? `${selectedItems.length} selected (+${filteredOutCount} filtered out)`
140
- : `${selectedItems.length} selected`}
145
+ ? withInterp(
146
+ t("{{n}} selected (+{{hidden}} filtered out)", {
147
+ n: selectedItems.length,
148
+ hidden: filteredOutCount,
149
+ }),
150
+ { n: selectedItems.length, hidden: filteredOutCount },
151
+ )
152
+ : withInterp(t("{{n}} selected", { n: selectedItems.length }), {
153
+ n: selectedItems.length,
154
+ })}
141
155
  </Text>
142
156
  )}
143
157