@multiplatform.one/table 6.4.3 → 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 (263) 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 +49 -12
  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.native.js.map +1 -1
  30. package/dist/cjs/components/DataTableDateFilter/index.native.js.map +1 -1
  31. package/dist/cjs/components/DataTableExport/index.cjs +1 -0
  32. package/dist/cjs/components/DataTableExport/index.native.js +1 -0
  33. package/dist/cjs/components/DataTableExport/index.native.js.map +1 -1
  34. package/dist/cjs/components/DataTableFilterButton/index.cjs +1 -1
  35. package/dist/cjs/components/DataTableFilterButton/index.native.js +5 -2
  36. package/dist/cjs/components/DataTableFilterButton/index.native.js.map +1 -1
  37. package/dist/cjs/components/DataTableFilterChips/index.native.js.map +1 -1
  38. package/dist/cjs/components/DataTableFilters/index.native.js.map +1 -1
  39. package/dist/cjs/components/DataTableGrouping/index.cjs +1 -0
  40. package/dist/cjs/components/DataTableGrouping/index.native.js +1 -0
  41. package/dist/cjs/components/DataTableGrouping/index.native.js.map +1 -1
  42. package/dist/cjs/components/DataTablePagination/index.native.js.map +1 -1
  43. package/dist/cjs/components/DataTableSearch/index.native.js.map +1 -1
  44. package/dist/cjs/components/DataTableToolbar/index.cjs +1 -0
  45. package/dist/cjs/components/DataTableToolbar/index.native.js +2 -1
  46. package/dist/cjs/components/DataTableToolbar/index.native.js.map +1 -1
  47. package/dist/cjs/components/DataTableViewOptions/index.native.js.map +1 -1
  48. package/dist/cjs/components/FilterManager.cjs +18 -14
  49. package/dist/cjs/components/FilterManager.native.js +18 -14
  50. package/dist/cjs/components/FilterManager.native.js.map +1 -1
  51. package/dist/cjs/components/FilterOperatorValueEditor/index.native.js.map +1 -1
  52. package/dist/cjs/components/KnobDialog.native.js.map +1 -1
  53. package/dist/cjs/components/SavedFilterDropdown.native.js.map +1 -1
  54. package/dist/cjs/components/SimpleTable/index.native.js.map +1 -1
  55. package/dist/cjs/components/TableInput/index.native.js +4 -1
  56. package/dist/cjs/components/TableInput/index.native.js.map +1 -1
  57. package/dist/cjs/components/TableInput/shared.native.js.map +1 -1
  58. package/dist/cjs/hooks/useDataTable.cjs +17 -1
  59. package/dist/cjs/hooks/useDataTable.native.js +19 -1
  60. package/dist/cjs/hooks/useDataTable.native.js.map +1 -1
  61. package/dist/cjs/hooks/useDataTableKeyboard.native.js.map +1 -1
  62. package/dist/cjs/hooks/useDragScroll.native.js.map +1 -1
  63. package/dist/cjs/hooks/useInfiniteRows.native.js.map +1 -1
  64. package/dist/cjs/hooks/useSavedFilters.native.js.map +1 -1
  65. package/dist/cjs/hooks/useTableColumnPinning.native.js.map +1 -1
  66. package/dist/cjs/hooks/useTableFiltering.native.js.map +1 -1
  67. package/dist/cjs/hooks/useTableGrouping.native.js.map +1 -1
  68. package/dist/cjs/hooks/useTableKeyboard.native.js.map +1 -1
  69. package/dist/cjs/hooks/useTablePagination.native.js.map +1 -1
  70. package/dist/cjs/hooks/useTableSearch.native.js.map +1 -1
  71. package/dist/cjs/hooks/useTableSelection.native.js.map +1 -1
  72. package/dist/cjs/hooks/useTableSorting.native.js.map +1 -1
  73. package/dist/cjs/hooks/useTableUrlSync.native.js.map +1 -1
  74. package/dist/cjs/hooks/useTableViews.native.js.map +1 -1
  75. package/dist/cjs/shared/t.native.js.map +1 -1
  76. package/dist/cjs/shims/primitives.native.js.map +1 -1
  77. package/dist/cjs/shims/tableCellContext.native.js.map +1 -1
  78. package/dist/cjs/types.native.js.map +1 -1
  79. package/dist/cjs/utils/columnAlign.native.js.map +1 -1
  80. package/dist/cjs/utils/filterFields.native.js.map +1 -1
  81. package/dist/cjs/utils/filterUtils.native.js.map +1 -1
  82. package/dist/cjs/utils/paginationDefaults.native.js.map +1 -1
  83. package/dist/cjs/utils/rowPress.cjs +44 -0
  84. package/dist/cjs/utils/rowPress.native.js +56 -0
  85. package/dist/cjs/utils/rowPress.native.js.map +1 -0
  86. package/dist/cjs/utils/urlStateUtils.native.js.map +1 -1
  87. package/dist/cjs/utils/viewData.native.js.map +1 -1
  88. package/dist/cjs/utils/withInterp.native.js.map +1 -1
  89. package/dist/esm/components/ColumnHeaderFilter/index.mjs +1 -0
  90. package/dist/esm/components/ColumnHeaderFilter/index.mjs.map +1 -1
  91. package/dist/esm/components/ColumnHeaderFilter/index.native.js +1 -0
  92. package/dist/esm/components/ColumnHeaderFilter/index.native.js.map +1 -1
  93. package/dist/esm/components/DataTable/DataTableMobileCards.mjs +9 -3
  94. package/dist/esm/components/DataTable/DataTableMobileCards.mjs.map +1 -1
  95. package/dist/esm/components/DataTable/DataTableMobileCards.native.js +12 -3
  96. package/dist/esm/components/DataTable/DataTableMobileCards.native.js.map +1 -1
  97. package/dist/esm/components/DataTable/DataTableRoot.mjs +133 -13
  98. package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
  99. package/dist/esm/components/DataTable/DataTableRoot.native.js +159 -13
  100. package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
  101. package/dist/esm/components/DataTable/DataTableVirtualized.mjs +3 -1
  102. package/dist/esm/components/DataTable/DataTableVirtualized.mjs.map +1 -1
  103. package/dist/esm/components/DataTable/DataTableVirtualized.native.js +3 -1
  104. package/dist/esm/components/DataTable/DataTableVirtualized.native.js.map +1 -1
  105. package/dist/esm/components/DataTable/index.native.js +39 -2
  106. package/dist/esm/components/DataTable/index.native.js.map +1 -1
  107. package/dist/esm/components/DataTableBulkActions/index.mjs +16 -1
  108. package/dist/esm/components/DataTableBulkActions/index.mjs.map +1 -1
  109. package/dist/esm/components/DataTableBulkActions/index.native.js +20 -1
  110. package/dist/esm/components/DataTableBulkActions/index.native.js.map +1 -1
  111. package/dist/esm/components/DataTableCell/CellModalEditor.mjs +4 -8
  112. package/dist/esm/components/DataTableCell/CellModalEditor.mjs.map +1 -1
  113. package/dist/esm/components/DataTableCell/index.mjs +1 -33
  114. package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
  115. package/dist/esm/components/DataTableCell/index.native.js +1 -34
  116. package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
  117. package/dist/esm/components/DataTableExport/index.mjs +1 -0
  118. package/dist/esm/components/DataTableExport/index.mjs.map +1 -1
  119. package/dist/esm/components/DataTableExport/index.native.js +1 -0
  120. package/dist/esm/components/DataTableExport/index.native.js.map +1 -1
  121. package/dist/esm/components/DataTableFilterButton/index.mjs +1 -1
  122. package/dist/esm/components/DataTableFilterButton/index.mjs.map +1 -1
  123. package/dist/esm/components/DataTableFilterButton/index.native.js +4 -1
  124. package/dist/esm/components/DataTableFilterButton/index.native.js.map +1 -1
  125. package/dist/esm/components/DataTableGrouping/index.mjs +1 -0
  126. package/dist/esm/components/DataTableGrouping/index.mjs.map +1 -1
  127. package/dist/esm/components/DataTableGrouping/index.native.js +1 -0
  128. package/dist/esm/components/DataTableGrouping/index.native.js.map +1 -1
  129. package/dist/esm/components/DataTableToolbar/index.mjs +1 -0
  130. package/dist/esm/components/DataTableToolbar/index.mjs.map +1 -1
  131. package/dist/esm/components/DataTableToolbar/index.native.js +1 -0
  132. package/dist/esm/components/DataTableToolbar/index.native.js.map +1 -1
  133. package/dist/esm/components/FilterManager.mjs +18 -14
  134. package/dist/esm/components/FilterManager.mjs.map +1 -1
  135. package/dist/esm/components/FilterManager.native.js +18 -14
  136. package/dist/esm/components/FilterManager.native.js.map +1 -1
  137. package/dist/esm/components/TableInput/index.native.js +3 -0
  138. package/dist/esm/components/TableInput/index.native.js.map +1 -1
  139. package/dist/esm/hooks/useDataTable.mjs +17 -1
  140. package/dist/esm/hooks/useDataTable.mjs.map +1 -1
  141. package/dist/esm/hooks/useDataTable.native.js +19 -1
  142. package/dist/esm/hooks/useDataTable.native.js.map +1 -1
  143. package/dist/esm/utils/rowPress.mjs +19 -0
  144. package/dist/esm/utils/rowPress.mjs.map +1 -0
  145. package/dist/esm/utils/rowPress.native.js +28 -0
  146. package/dist/esm/utils/rowPress.native.js.map +1 -0
  147. package/dist/jsx/components/ColumnHeaderFilter/index.mjs +1 -0
  148. package/dist/jsx/components/ColumnHeaderFilter/index.mjs.map +1 -1
  149. package/dist/jsx/components/ColumnHeaderFilter/index.native.js +1 -0
  150. package/dist/jsx/components/ColumnHeaderFilter/index.native.js.map +1 -1
  151. package/dist/jsx/components/DataTable/DataTableMobileCards.mjs +9 -3
  152. package/dist/jsx/components/DataTable/DataTableMobileCards.mjs.map +1 -1
  153. package/dist/jsx/components/DataTable/DataTableMobileCards.native.js +12 -3
  154. package/dist/jsx/components/DataTable/DataTableMobileCards.native.js.map +1 -1
  155. package/dist/jsx/components/DataTable/DataTableRoot.mjs +133 -13
  156. package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
  157. package/dist/jsx/components/DataTable/DataTableRoot.native.js +159 -13
  158. package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
  159. package/dist/jsx/components/DataTable/DataTableVirtualized.mjs +3 -1
  160. package/dist/jsx/components/DataTable/DataTableVirtualized.mjs.map +1 -1
  161. package/dist/jsx/components/DataTable/DataTableVirtualized.native.js +3 -1
  162. package/dist/jsx/components/DataTable/DataTableVirtualized.native.js.map +1 -1
  163. package/dist/jsx/components/DataTable/index.native.js +39 -2
  164. package/dist/jsx/components/DataTable/index.native.js.map +1 -1
  165. package/dist/jsx/components/DataTableBulkActions/index.mjs +16 -1
  166. package/dist/jsx/components/DataTableBulkActions/index.mjs.map +1 -1
  167. package/dist/jsx/components/DataTableBulkActions/index.native.js +20 -1
  168. package/dist/jsx/components/DataTableBulkActions/index.native.js.map +1 -1
  169. package/dist/jsx/components/DataTableCell/CellModalEditor.mjs +4 -8
  170. package/dist/jsx/components/DataTableCell/CellModalEditor.mjs.map +1 -1
  171. package/dist/jsx/components/DataTableCell/index.mjs +1 -33
  172. package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
  173. package/dist/jsx/components/DataTableCell/index.native.js +1 -34
  174. package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
  175. package/dist/jsx/components/DataTableExport/index.mjs +1 -0
  176. package/dist/jsx/components/DataTableExport/index.mjs.map +1 -1
  177. package/dist/jsx/components/DataTableExport/index.native.js +1 -0
  178. package/dist/jsx/components/DataTableExport/index.native.js.map +1 -1
  179. package/dist/jsx/components/DataTableFilterButton/index.mjs +1 -1
  180. package/dist/jsx/components/DataTableFilterButton/index.mjs.map +1 -1
  181. package/dist/jsx/components/DataTableFilterButton/index.native.js +4 -1
  182. package/dist/jsx/components/DataTableFilterButton/index.native.js.map +1 -1
  183. package/dist/jsx/components/DataTableGrouping/index.mjs +1 -0
  184. package/dist/jsx/components/DataTableGrouping/index.mjs.map +1 -1
  185. package/dist/jsx/components/DataTableGrouping/index.native.js +1 -0
  186. package/dist/jsx/components/DataTableGrouping/index.native.js.map +1 -1
  187. package/dist/jsx/components/DataTableToolbar/index.mjs +1 -0
  188. package/dist/jsx/components/DataTableToolbar/index.mjs.map +1 -1
  189. package/dist/jsx/components/DataTableToolbar/index.native.js +1 -0
  190. package/dist/jsx/components/DataTableToolbar/index.native.js.map +1 -1
  191. package/dist/jsx/components/FilterManager.mjs +18 -14
  192. package/dist/jsx/components/FilterManager.mjs.map +1 -1
  193. package/dist/jsx/components/FilterManager.native.js +18 -14
  194. package/dist/jsx/components/FilterManager.native.js.map +1 -1
  195. package/dist/jsx/components/TableInput/index.native.js +3 -0
  196. package/dist/jsx/components/TableInput/index.native.js.map +1 -1
  197. package/dist/jsx/hooks/useDataTable.mjs +17 -1
  198. package/dist/jsx/hooks/useDataTable.mjs.map +1 -1
  199. package/dist/jsx/hooks/useDataTable.native.js +19 -1
  200. package/dist/jsx/hooks/useDataTable.native.js.map +1 -1
  201. package/dist/jsx/utils/rowPress.mjs +19 -0
  202. package/dist/jsx/utils/rowPress.mjs.map +1 -0
  203. package/dist/jsx/utils/rowPress.native.js +56 -0
  204. package/dist/jsx/utils/rowPress.native.js.map +1 -0
  205. package/package.json +12 -12
  206. package/src/components/ColumnHeaderFilter/index.tsx +1 -0
  207. package/src/components/DataTable/DataTable.spec.tsx +4 -1
  208. package/src/components/DataTable/DataTable.stories.tsx +190 -5
  209. package/src/components/DataTable/DataTableMobileCards.tsx +30 -4
  210. package/src/components/DataTable/DataTableRoot.tsx +195 -8
  211. package/src/components/DataTable/DataTableVirtualized.tsx +8 -0
  212. package/src/components/DataTable/cardLayout.spec.tsx +4 -1
  213. package/src/components/DataTable/customCellAlign.spec.tsx +124 -0
  214. package/src/components/DataTable/disabledContract.spec.tsx +4 -3
  215. package/src/components/DataTable/index.native.tsx +47 -1
  216. package/src/components/DataTable/rowPress.spec.tsx +229 -0
  217. package/src/components/DataTable/selectionSemantics.spec.tsx +156 -0
  218. package/src/components/DataTableBulkActions/index.tsx +16 -2
  219. package/src/components/DataTableBulkActions/selectionCountI18n.spec.tsx +94 -0
  220. package/src/components/DataTableCell/CellModalEditor.tsx +3 -1
  221. package/src/components/DataTableCell/cellDraftPolicy.spec.tsx +10 -2
  222. package/src/components/DataTableCell/dataTableCell.spec.tsx +10 -2
  223. package/src/components/DataTableCell/index.tsx +5 -36
  224. package/src/components/DataTableColumnPinning/DataTableColumnPinning.stories.tsx +49 -17
  225. package/src/components/DataTableDateFilter/DataTableDateFilter.stories.tsx +4 -3
  226. package/src/components/DataTableExport/DataTableExport.stories.tsx +3 -2
  227. package/src/components/DataTableExport/index.tsx +1 -0
  228. package/src/components/DataTableFilterButton/index.tsx +4 -1
  229. package/src/components/DataTableGrouping/DataTableGrouping.spec.tsx +59 -2
  230. package/src/components/DataTableGrouping/DataTableGrouping.stories.tsx +17 -5
  231. package/src/components/DataTableGrouping/index.tsx +1 -0
  232. package/src/components/DataTablePagination/DataTablePagination.stories.tsx +34 -20
  233. package/src/components/DataTableToolbar/index.tsx +1 -0
  234. package/src/components/DataTableViewOptions/DataTableViewOptions.stories.tsx +2 -1
  235. package/src/components/FilterManager.tsx +21 -13
  236. package/src/components/TableInput/index.native.tsx +3 -0
  237. package/src/components/__scratch__/scratch.spec.tsx +46 -0
  238. package/src/hooks/useDataTable.ts +27 -1
  239. package/src/types.ts +25 -0
  240. package/src/utils/rowPress.ts +58 -0
  241. package/types/components/ColumnHeaderFilter/index.d.ts.map +1 -1
  242. package/types/components/DataTable/DataTableMobileCards.d.ts +13 -1
  243. package/types/components/DataTable/DataTableMobileCards.d.ts.map +1 -1
  244. package/types/components/DataTable/DataTableRoot.d.ts +1 -1
  245. package/types/components/DataTable/DataTableRoot.d.ts.map +1 -1
  246. package/types/components/DataTable/DataTableVirtualized.d.ts +7 -1
  247. package/types/components/DataTable/DataTableVirtualized.d.ts.map +1 -1
  248. package/types/components/DataTable/index.native.d.ts +1 -1
  249. package/types/components/DataTable/index.native.d.ts.map +1 -1
  250. package/types/components/DataTableBulkActions/index.d.ts.map +1 -1
  251. package/types/components/DataTableCell/CellModalEditor.d.ts.map +1 -1
  252. package/types/components/DataTableCell/index.d.ts.map +1 -1
  253. package/types/components/DataTableFilterButton/index.d.ts.map +1 -1
  254. package/types/components/DataTableGrouping/index.d.ts.map +1 -1
  255. package/types/components/DataTableToolbar/index.d.ts.map +1 -1
  256. package/types/components/FilterManager.d.ts +7 -1
  257. package/types/components/FilterManager.d.ts.map +1 -1
  258. package/types/components/TableInput/index.native.d.ts.map +1 -1
  259. package/types/hooks/useDataTable.d.ts.map +1 -1
  260. package/types/types.d.ts +23 -0
  261. package/types/types.d.ts.map +1 -1
  262. package/types/utils/rowPress.d.ts +20 -0
  263. package/types/utils/rowPress.d.ts.map +1 -0
@@ -62,6 +62,7 @@ import { DataTableVirtualized } from "./DataTableVirtualized";
62
62
  import { alignToFlex, getDefaultColumnAlign } from "../../utils/columnAlign";
63
63
  import { mapFieldToFilterType } from "../../utils/filterFields";
64
64
  import { INFINITE_TRIGGER_ROWS } from "../../utils/paginationDefaults";
65
+ import { isInteractiveInnerTarget } from "../../utils/rowPress";
65
66
  import { bidiIsolate, withInterp } from "../../utils/withInterp";
66
67
 
67
68
  const SELECTION_COL_WIDTH = 40;
@@ -134,6 +135,8 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
134
135
  maxPinnedColumns = 3,
135
136
  enableGrouping = false,
136
137
  enableKeyboardNavigation = false,
138
+ onRowPress,
139
+ rowLabel,
137
140
  enableRowSelection = false,
138
141
  selectAllScope: selectAllScopeProp,
139
142
  bulkActions,
@@ -473,6 +476,159 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
473
476
  : undefined,
474
477
  });
475
478
 
479
+ const isSSR = typeof window === "undefined";
480
+
481
+ // ---- Row press (onRowPress): the ROW is one pressable unit ----
482
+
483
+ const rowPressActive = Boolean(onRowPress) && !disabled;
484
+ // LC-49 roving anchor: the row set exposes exactly one tab stop — the
485
+ // focused row while focus is inside the set, else the first row.
486
+ const [rowPressAnchor, setRowPressAnchor] = useState(0);
487
+ const effectiveRowPressAnchor = Math.min(rowPressAnchor, Math.max(tableRows.length - 1, 0));
488
+
489
+ // Row identity for accessible names (DG-A11Y-06): consumer accessor →
490
+ // first visible column's cell value → undefined (caller picks its own
491
+ // positional fallback). Shared by the row-press label and the selection
492
+ // checkbox name so "which row is this control for" reads the same
493
+ // everywhere.
494
+ const deriveRowIdentity = useCallback(
495
+ (row: any): string | undefined => {
496
+ const fromAccessor = rowLabel?.(row.original as TData);
497
+ if (fromAccessor) return fromAccessor;
498
+ let firstCellValue: unknown;
499
+ try {
500
+ firstCellValue = row.getVisibleCells()[0]?.getValue();
501
+ } catch {
502
+ firstCellValue = undefined;
503
+ }
504
+ if (
505
+ (typeof firstCellValue === "string" && firstCellValue.length > 0) ||
506
+ typeof firstCellValue === "number"
507
+ ) {
508
+ return String(firstCellValue);
509
+ }
510
+ return undefined;
511
+ },
512
+ [rowLabel],
513
+ );
514
+
515
+ // Accessible name per pressable row: identity, else positional "Row {{row}}".
516
+ const deriveRowLabel = useCallback(
517
+ (row: any, rowIndex: number): string =>
518
+ deriveRowIdentity(row) ??
519
+ withInterp(t("Row {{row}}", { row: rowIndex + 1 }), { row: rowIndex + 1 }),
520
+ [deriveRowIdentity, t],
521
+ );
522
+
523
+ // Selection-toggle name: row identity beats a bare index ("Select row
524
+ // Bulbasaur", falling back to "Select row 3") so AT and automation can
525
+ // target a specific row's checkbox by name.
526
+ const deriveSelectRowLabel = useCallback(
527
+ (row: any, rowIndex: number): string => {
528
+ const identity = deriveRowIdentity(row) ?? String(rowIndex + 1);
529
+ return withInterp(t("Select row {{row}}", { row: identity }), { row: identity });
530
+ },
531
+ [deriveRowIdentity, t],
532
+ );
533
+
534
+ const activateRowPress = useCallback(
535
+ (row: any, rowIndex: number) => {
536
+ onRowPress?.(row.original as TData, rowIndex);
537
+ },
538
+ [onRowPress],
539
+ );
540
+
541
+ /**
542
+ * Press/keyboard/AT props for one pressable row (grid `<tr>`, virtualized
543
+ * row host, or mobile card). Spread LAST so the roving tabIndex overrides
544
+ * the static -1 the grid row carries otherwise.
545
+ *
546
+ * - Pointer: activation skips presses originating on inner interactive
547
+ * elements (isInteractiveInnerTarget — the stopPropagation contract's
548
+ * defensive arm).
549
+ * - Keyboard (LC-50): Enter AND Space each synthesize exactly one
550
+ * activation (repeat + defaultPrevented guarded); arrows/Home/End rove
551
+ * within the row set; Shift+Tab exits (no key trap).
552
+ */
553
+ const getRowPressProps = useCallback(
554
+ (row: any, rowIndex: number, variant: "row" | "card" = "row") => {
555
+ if (!rowPressActive) return {};
556
+ const handlePress = (event: any) => {
557
+ if (event?.defaultPrevented) return;
558
+ const targetEl = event?.target ?? event?.nativeEvent?.target;
559
+ if (isWeb && typeof Element !== "undefined" && targetEl instanceof Element) {
560
+ const boundary = targetEl.closest('[data-row-press="true"]');
561
+ if (isInteractiveInnerTarget(targetEl, boundary)) return;
562
+ }
563
+ activateRowPress(row, rowIndex);
564
+ };
565
+ return {
566
+ "aria-label": deriveRowLabel(row, rowIndex),
567
+ onPress: handlePress,
568
+ cursor: "pointer" as const,
569
+ pressStyle: { backgroundColor: "$color4" },
570
+ ...(variant === "card" ? { role: "button" as const } : {}),
571
+ ...(isWeb && !isSSR
572
+ ? {
573
+ tabIndex: rowIndex === effectiveRowPressAnchor ? 0 : -1,
574
+ focusVisibleStyle: ensureFocusVisibleRing({ outlineOffset: -2 }),
575
+ "data-row-press": "true",
576
+ onFocus: (e: React.FocusEvent) => {
577
+ // Only a focused ROW moves the anchor; focus on an inner
578
+ // control (own tab stop) leaves the roving state alone.
579
+ if (e.target === e.currentTarget) setRowPressAnchor(rowIndex);
580
+ },
581
+ onBlur: (e: React.FocusEvent) => {
582
+ const next = e.relatedTarget;
583
+ if (!(next instanceof Element) || !next.closest('[data-row-press="true"]')) {
584
+ setRowPressAnchor(0);
585
+ }
586
+ },
587
+ onKeyDown: (e: React.KeyboardEvent) => {
588
+ if (e.defaultPrevented) return;
589
+ // Keys on focused inner controls belong to those controls.
590
+ if (e.target !== e.currentTarget) return;
591
+ const key = e.key;
592
+ if (key === "Enter" || key === " " || key === "Spacebar") {
593
+ // preventDefault stops Space page-scroll; stopPropagation
594
+ // keeps the container-level keyboard nav (Excel mode) from
595
+ // double-activating the same physical key.
596
+ e.preventDefault();
597
+ e.stopPropagation();
598
+ if (e.repeat) return;
599
+ activateRowPress(row, rowIndex);
600
+ return;
601
+ }
602
+ if (key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End") {
603
+ e.preventDefault();
604
+ e.stopPropagation();
605
+ const rowEl = e.currentTarget as HTMLElement;
606
+ const scope =
607
+ rowEl.closest('[data-row-press-scope="true"]') ?? rowEl.ownerDocument;
608
+ const pressRows = Array.from(
609
+ scope.querySelectorAll('[data-row-press="true"]'),
610
+ ) as HTMLElement[];
611
+ const currentIdx = pressRows.indexOf(rowEl);
612
+ if (currentIdx < 0) return;
613
+ const nextIdx =
614
+ key === "Home"
615
+ ? 0
616
+ : key === "End"
617
+ ? pressRows.length - 1
618
+ : Math.min(
619
+ Math.max(currentIdx + (key === "ArrowDown" ? 1 : -1), 0),
620
+ pressRows.length - 1,
621
+ );
622
+ pressRows[nextIdx]?.focus();
623
+ }
624
+ },
625
+ }
626
+ : {}),
627
+ };
628
+ },
629
+ [rowPressActive, deriveRowLabel, activateRowPress, effectiveRowPressAnchor, isSSR],
630
+ );
631
+
476
632
  const handleCellChange = useCallback(
477
633
  async (rowIndex: number, columnId: string, newValue: any) => {
478
634
  if (readOnly || disabled) return;
@@ -519,7 +675,28 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
519
675
  const value = cell.getValue();
520
676
 
521
677
  if (columnDef.cell && !columnDef.field) {
522
- return flexRender(columnDef.cell, cell.getContext());
678
+ // DG-TBL-01 for CUSTOM cell renderers: the cell box's own
679
+ // justifyContent no-ops in the semantic-table displayMode
680
+ // (display: table-cell is not a flex container), so the column's
681
+ // resolved alignment rides this full-width flex container instead —
682
+ // consumers never self-align. The built-in DataTableCell branch
683
+ // below aligns internally (CellWrapper) and stays unwrapped.
684
+ const customAlign = resolveColumnAlign({
685
+ ...(columnDef as DataTableColumn),
686
+ id: cell.column.id,
687
+ });
688
+ return (
689
+ <View
690
+ width="100%"
691
+ minWidth={0}
692
+ flexDirection="row"
693
+ alignItems="center"
694
+ justifyContent={alignToFlex(customAlign)}
695
+ {...({ "data-cell-align": customAlign } as any)}
696
+ >
697
+ {flexRender(columnDef.cell, cell.getContext())}
698
+ </View>
699
+ );
523
700
  }
524
701
 
525
702
  const columnField = getColumnField(columnDef);
@@ -556,8 +733,6 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
556
733
  [handleCellChange, readOnly, disabled, knobProps.sizeToken, isCompact],
557
734
  );
558
735
 
559
- const isSSR = typeof window === "undefined";
560
-
561
736
  const extraColumnsBefore =
562
737
  (enableRowSelection ? 1 : 0) + (showRowNumbers ? 1 : 0) + (enableRowExpansion ? 1 : 0);
563
738
  const extraColumnsAfter = showDeleteRow && !readOnly ? 1 : 0;
@@ -651,10 +826,9 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
651
826
  disabled={disabled}
652
827
  checkboxProps={{
653
828
  // Row identity in the name so repeated per-row actions are
654
- // differentiated for screen readers (DG-A11Y-06).
655
- "aria-label": withInterp(t("Select row {{row}}", { row: rowIndex + 1 }), {
656
- row: rowIndex + 1,
657
- }),
829
+ // differentiated for screen readers (DG-A11Y-06). The
830
+ // aria-label rides the role="checkbox" frame itself.
831
+ "aria-label": deriveSelectRowLabel(row, rowIndex),
658
832
  }}
659
833
  compact={isCompact}
660
834
  />
@@ -840,6 +1014,7 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
840
1014
  minRows,
841
1015
  isCompact,
842
1016
  t,
1017
+ deriveSelectRowLabel,
843
1018
  ExpandCaretIcon,
844
1019
  expandedCaretRotation,
845
1020
  clipBodyCells,
@@ -1084,6 +1259,8 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
1084
1259
  selectAllIndeterminate={selectAllIndeterminate}
1085
1260
  onToggleSelectAll={toggleSelectAll}
1086
1261
  compact={isCompact}
1262
+ selectRowLabel={deriveSelectRowLabel}
1263
+ {...(rowPressActive ? { getRowPressProps } : {})}
1087
1264
  />
1088
1265
  </>
1089
1266
  ) : (
@@ -1099,7 +1276,13 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
1099
1276
  }}
1100
1277
  flexDirection="column"
1101
1278
  width="100%"
1102
- flexGrow={1}
1279
+ // Grow 0 / shrink 1 / basis auto: the grid frame hugs its
1280
+ // rows and only compresses (into internal scroll) when the
1281
+ // parent is shorter. flexGrow 1 stretched the frame to fill
1282
+ // tall parents, painting a dead zone between the last row
1283
+ // and the frame's bottom border on short pages (the frame
1284
+ // claimed data area that held no data — Axiom 6/LC-37).
1285
+ flexGrow={0}
1103
1286
  flexShrink={1}
1104
1287
  flexBasis="auto"
1105
1288
  minHeight={0}
@@ -1144,6 +1327,8 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
1144
1327
  // to it instead of the scrolling content View. Content
1145
1328
  // fits the max-content box, so nothing needs the clip.
1146
1329
  {...(hScroll && enableColumnPinning ? { overflow: "visible" as any } : {})}
1330
+ // Roving-focus DOM scope for onRowPress arrow keys (LC-49).
1331
+ {...(rowPressActive ? ({ "data-row-press-scope": "true" } as any) : {})}
1147
1332
  >
1148
1333
  {/* Table Header */}
1149
1334
  <Table.Head zIndex="$1" aria-label={t("Table headers")}>
@@ -1553,6 +1738,7 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
1553
1738
  extraColumnsAfter={extraColumnsAfter}
1554
1739
  table={table}
1555
1740
  hasInfiniteTail={hasInfiniteTail}
1741
+ {...(rowPressActive ? { getRowPressProps } : {})}
1556
1742
  />
1557
1743
  </Table.Cell>
1558
1744
  </Table.Row>
@@ -1646,6 +1832,7 @@ export function DataTableRoot<TData extends Record<string, any> = Record<string,
1646
1832
  : "odd"
1647
1833
  : "off",
1648
1834
  } as any)}
1835
+ {...(getRowPressProps(row, index) as any)}
1649
1836
  >
1650
1837
  {renderRowContent(row, index, isLastRow)}
1651
1838
  </Table.Row>
@@ -42,6 +42,12 @@ export interface DataTableVirtualizedProps {
42
42
  * scroll range so appended pages grow geometry seamlessly.
43
43
  */
44
44
  hasInfiniteTail?: boolean;
45
+ /**
46
+ * onRowPress support: press/keyboard/AT props for one pressable row
47
+ * (built by DataTableRoot; spread on the row host so virtual rows keep
48
+ * the same activation + roving-focus contract as grid rows).
49
+ */
50
+ getRowPressProps?: (row: any, rowIndex: number, variant?: "row" | "card") => Record<string, any>;
45
51
  }
46
52
 
47
53
  export function DataTableVirtualized({
@@ -60,6 +66,7 @@ export function DataTableVirtualized({
60
66
  getColSizeProps,
61
67
  renderRowContent,
62
68
  hasInfiniteTail = false,
69
+ getRowPressProps,
63
70
  }: DataTableVirtualizedProps) {
64
71
  return (
65
72
  <div
@@ -188,6 +195,7 @@ export function DataTableVirtualized({
188
195
  }
189
196
  role="row"
190
197
  aria-rowindex={virtualRow.index + 1}
198
+ {...(getRowPressProps ? (getRowPressProps(row, virtualRow.index) as any) : {})}
191
199
  >
192
200
  {/* div cells (role="cell" via the primitive): a real <td> is
193
201
  not a valid child of the absolutely-positioned div rows */}
@@ -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