@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.
- package/dist/cjs/components/ChildTable/index.native.js +1 -1
- package/dist/cjs/components/ChildTable/index.native.js.map +1 -1
- package/dist/cjs/components/ColumnHeaderFilter/index.cjs +1 -0
- package/dist/cjs/components/ColumnHeaderFilter/index.native.js +2 -1
- package/dist/cjs/components/ColumnHeaderFilter/index.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableCardControls.native.js +1 -1
- package/dist/cjs/components/DataTable/DataTableCardControls.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableContext.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableMobileCards.cjs +9 -3
- package/dist/cjs/components/DataTable/DataTableMobileCards.native.js +12 -3
- package/dist/cjs/components/DataTable/DataTableMobileCards.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableRoot.cjs +133 -13
- package/dist/cjs/components/DataTable/DataTableRoot.native.js +166 -20
- package/dist/cjs/components/DataTable/DataTableRoot.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableStateContext.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableVirtualized.cjs +3 -1
- package/dist/cjs/components/DataTable/DataTableVirtualized.native.js +3 -1
- package/dist/cjs/components/DataTable/DataTableVirtualized.native.js.map +1 -1
- package/dist/cjs/components/DataTable/index.native.js +50 -13
- package/dist/cjs/components/DataTable/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableBulkActions/index.cjs +16 -1
- package/dist/cjs/components/DataTableBulkActions/index.native.js +20 -1
- package/dist/cjs/components/DataTableBulkActions/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableCell/CellModalEditor.cjs +4 -8
- package/dist/cjs/components/DataTableCell/CellOptionsEditor.native.js.map +1 -1
- package/dist/cjs/components/DataTableCell/index.cjs +1 -33
- package/dist/cjs/components/DataTableCell/index.native.js +1 -34
- package/dist/cjs/components/DataTableCell/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableColumnPinning/index.cjs +2 -2
- package/dist/cjs/components/DataTableColumnPinning/index.native.js +2 -2
- package/dist/cjs/components/DataTableColumnPinning/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableDateFilter/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableExport/index.cjs +1 -0
- package/dist/cjs/components/DataTableExport/index.native.js +1 -0
- package/dist/cjs/components/DataTableExport/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableFilterButton/index.cjs +1 -1
- package/dist/cjs/components/DataTableFilterButton/index.native.js +5 -2
- package/dist/cjs/components/DataTableFilterButton/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableFilterChips/index.cjs +1 -1
- package/dist/cjs/components/DataTableFilterChips/index.native.js +1 -1
- package/dist/cjs/components/DataTableFilterChips/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableFilters/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableGrouping/index.cjs +17 -1
- package/dist/cjs/components/DataTableGrouping/index.native.js +17 -1
- package/dist/cjs/components/DataTableGrouping/index.native.js.map +1 -1
- package/dist/cjs/components/DataTablePagination/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableSearch/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableToolbar/index.cjs +1 -0
- package/dist/cjs/components/DataTableToolbar/index.native.js +2 -1
- package/dist/cjs/components/DataTableToolbar/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableViewOptions/index.native.js.map +1 -1
- package/dist/cjs/components/FilterManager.cjs +18 -14
- package/dist/cjs/components/FilterManager.native.js +18 -14
- package/dist/cjs/components/FilterManager.native.js.map +1 -1
- package/dist/cjs/components/FilterOperatorValueEditor/index.native.js.map +1 -1
- package/dist/cjs/components/KnobDialog.native.js.map +1 -1
- package/dist/cjs/components/SavedFilterDropdown.native.js.map +1 -1
- package/dist/cjs/components/SimpleTable/index.cjs +1 -1
- package/dist/cjs/components/SimpleTable/index.native.js +1 -1
- package/dist/cjs/components/SimpleTable/index.native.js.map +1 -1
- package/dist/cjs/components/TableInput/index.native.js +4 -1
- package/dist/cjs/components/TableInput/index.native.js.map +1 -1
- package/dist/cjs/components/TableInput/shared.native.js.map +1 -1
- package/dist/cjs/hooks/useDataTable.cjs +17 -1
- package/dist/cjs/hooks/useDataTable.native.js +19 -1
- package/dist/cjs/hooks/useDataTable.native.js.map +1 -1
- package/dist/cjs/hooks/useDataTableKeyboard.native.js.map +1 -1
- package/dist/cjs/hooks/useDragScroll.native.js.map +1 -1
- package/dist/cjs/hooks/useInfiniteRows.native.js.map +1 -1
- package/dist/cjs/hooks/useSavedFilters.native.js.map +1 -1
- package/dist/cjs/hooks/useTableColumnPinning.native.js.map +1 -1
- package/dist/cjs/hooks/useTableFiltering.native.js.map +1 -1
- package/dist/cjs/hooks/useTableGrouping.native.js.map +1 -1
- package/dist/cjs/hooks/useTableKeyboard.native.js.map +1 -1
- package/dist/cjs/hooks/useTablePagination.native.js.map +1 -1
- package/dist/cjs/hooks/useTableSearch.native.js.map +1 -1
- package/dist/cjs/hooks/useTableSelection.native.js.map +1 -1
- package/dist/cjs/hooks/useTableSorting.native.js.map +1 -1
- package/dist/cjs/hooks/useTableUrlSync.native.js.map +1 -1
- package/dist/cjs/hooks/useTableViews.native.js.map +1 -1
- package/dist/cjs/shared/t.native.js.map +1 -1
- package/dist/cjs/shims/primitives.native.js.map +1 -1
- package/dist/cjs/shims/tableCellContext.native.js.map +1 -1
- package/dist/cjs/types.native.js.map +1 -1
- package/dist/cjs/utils/columnAlign.native.js.map +1 -1
- package/dist/cjs/utils/filterFields.native.js.map +1 -1
- package/dist/cjs/utils/filterUtils.native.js.map +1 -1
- package/dist/cjs/utils/paginationDefaults.native.js.map +1 -1
- package/dist/cjs/utils/rowPress.cjs +44 -0
- package/dist/cjs/utils/rowPress.native.js +56 -0
- package/dist/cjs/utils/rowPress.native.js.map +1 -0
- package/dist/cjs/utils/urlStateUtils.native.js.map +1 -1
- package/dist/cjs/utils/viewData.native.js.map +1 -1
- package/dist/cjs/utils/withInterp.native.js.map +1 -1
- package/dist/esm/components/ColumnHeaderFilter/index.mjs +1 -0
- package/dist/esm/components/ColumnHeaderFilter/index.mjs.map +1 -1
- package/dist/esm/components/ColumnHeaderFilter/index.native.js +1 -0
- package/dist/esm/components/ColumnHeaderFilter/index.native.js.map +1 -1
- package/dist/esm/components/DataTable/DataTableMobileCards.mjs +9 -3
- package/dist/esm/components/DataTable/DataTableMobileCards.mjs.map +1 -1
- package/dist/esm/components/DataTable/DataTableMobileCards.native.js +12 -3
- package/dist/esm/components/DataTable/DataTableMobileCards.native.js.map +1 -1
- package/dist/esm/components/DataTable/DataTableRoot.mjs +133 -13
- package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
- package/dist/esm/components/DataTable/DataTableRoot.native.js +159 -13
- package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
- package/dist/esm/components/DataTable/DataTableVirtualized.mjs +3 -1
- package/dist/esm/components/DataTable/DataTableVirtualized.mjs.map +1 -1
- package/dist/esm/components/DataTable/DataTableVirtualized.native.js +3 -1
- package/dist/esm/components/DataTable/DataTableVirtualized.native.js.map +1 -1
- package/dist/esm/components/DataTable/index.native.js +40 -3
- package/dist/esm/components/DataTable/index.native.js.map +1 -1
- package/dist/esm/components/DataTableBulkActions/index.mjs +16 -1
- package/dist/esm/components/DataTableBulkActions/index.mjs.map +1 -1
- package/dist/esm/components/DataTableBulkActions/index.native.js +20 -1
- package/dist/esm/components/DataTableBulkActions/index.native.js.map +1 -1
- package/dist/esm/components/DataTableCell/CellModalEditor.mjs +4 -8
- package/dist/esm/components/DataTableCell/CellModalEditor.mjs.map +1 -1
- package/dist/esm/components/DataTableCell/index.mjs +1 -33
- package/dist/esm/components/DataTableCell/index.mjs.map +1 -1
- package/dist/esm/components/DataTableCell/index.native.js +1 -34
- package/dist/esm/components/DataTableCell/index.native.js.map +1 -1
- package/dist/esm/components/DataTableColumnPinning/index.mjs +2 -2
- package/dist/esm/components/DataTableColumnPinning/index.mjs.map +1 -1
- package/dist/esm/components/DataTableColumnPinning/index.native.js +2 -2
- package/dist/esm/components/DataTableColumnPinning/index.native.js.map +1 -1
- package/dist/esm/components/DataTableExport/index.mjs +1 -0
- package/dist/esm/components/DataTableExport/index.mjs.map +1 -1
- package/dist/esm/components/DataTableExport/index.native.js +1 -0
- package/dist/esm/components/DataTableExport/index.native.js.map +1 -1
- package/dist/esm/components/DataTableFilterButton/index.mjs +1 -1
- package/dist/esm/components/DataTableFilterButton/index.mjs.map +1 -1
- package/dist/esm/components/DataTableFilterButton/index.native.js +4 -1
- package/dist/esm/components/DataTableFilterButton/index.native.js.map +1 -1
- package/dist/esm/components/DataTableFilterChips/index.mjs +1 -1
- package/dist/esm/components/DataTableFilterChips/index.mjs.map +1 -1
- package/dist/esm/components/DataTableFilterChips/index.native.js +1 -1
- package/dist/esm/components/DataTableGrouping/index.mjs +18 -3
- package/dist/esm/components/DataTableGrouping/index.mjs.map +1 -1
- package/dist/esm/components/DataTableGrouping/index.native.js +18 -3
- package/dist/esm/components/DataTableGrouping/index.native.js.map +1 -1
- package/dist/esm/components/DataTableToolbar/index.mjs +1 -0
- package/dist/esm/components/DataTableToolbar/index.mjs.map +1 -1
- package/dist/esm/components/DataTableToolbar/index.native.js +1 -0
- package/dist/esm/components/DataTableToolbar/index.native.js.map +1 -1
- package/dist/esm/components/FilterManager.mjs +18 -14
- package/dist/esm/components/FilterManager.mjs.map +1 -1
- package/dist/esm/components/FilterManager.native.js +18 -14
- package/dist/esm/components/FilterManager.native.js.map +1 -1
- package/dist/esm/components/SimpleTable/index.mjs +1 -1
- package/dist/esm/components/SimpleTable/index.mjs.map +1 -1
- package/dist/esm/components/SimpleTable/index.native.js +1 -1
- package/dist/esm/components/TableInput/index.native.js +3 -0
- package/dist/esm/components/TableInput/index.native.js.map +1 -1
- package/dist/esm/hooks/useDataTable.mjs +17 -1
- package/dist/esm/hooks/useDataTable.mjs.map +1 -1
- package/dist/esm/hooks/useDataTable.native.js +19 -1
- package/dist/esm/hooks/useDataTable.native.js.map +1 -1
- package/dist/esm/hooks/useTableGrouping.mjs.map +1 -1
- package/dist/esm/utils/rowPress.mjs +19 -0
- package/dist/esm/utils/rowPress.mjs.map +1 -0
- package/dist/esm/utils/rowPress.native.js +28 -0
- package/dist/esm/utils/rowPress.native.js.map +1 -0
- package/dist/jsx/components/ColumnHeaderFilter/index.mjs +1 -0
- package/dist/jsx/components/ColumnHeaderFilter/index.mjs.map +1 -1
- package/dist/jsx/components/ColumnHeaderFilter/index.native.js +1 -0
- package/dist/jsx/components/ColumnHeaderFilter/index.native.js.map +1 -1
- package/dist/jsx/components/DataTable/DataTableMobileCards.mjs +9 -3
- package/dist/jsx/components/DataTable/DataTableMobileCards.mjs.map +1 -1
- package/dist/jsx/components/DataTable/DataTableMobileCards.native.js +12 -3
- package/dist/jsx/components/DataTable/DataTableMobileCards.native.js.map +1 -1
- package/dist/jsx/components/DataTable/DataTableRoot.mjs +133 -13
- package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
- package/dist/jsx/components/DataTable/DataTableRoot.native.js +159 -13
- package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
- package/dist/jsx/components/DataTable/DataTableVirtualized.mjs +3 -1
- package/dist/jsx/components/DataTable/DataTableVirtualized.mjs.map +1 -1
- package/dist/jsx/components/DataTable/DataTableVirtualized.native.js +3 -1
- package/dist/jsx/components/DataTable/DataTableVirtualized.native.js.map +1 -1
- package/dist/jsx/components/DataTable/index.native.js +40 -3
- package/dist/jsx/components/DataTable/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableBulkActions/index.mjs +16 -1
- package/dist/jsx/components/DataTableBulkActions/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableBulkActions/index.native.js +20 -1
- package/dist/jsx/components/DataTableBulkActions/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableCell/CellModalEditor.mjs +4 -8
- package/dist/jsx/components/DataTableCell/CellModalEditor.mjs.map +1 -1
- package/dist/jsx/components/DataTableCell/index.mjs +1 -33
- package/dist/jsx/components/DataTableCell/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableCell/index.native.js +1 -34
- package/dist/jsx/components/DataTableCell/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableColumnPinning/index.mjs +2 -2
- package/dist/jsx/components/DataTableColumnPinning/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableColumnPinning/index.native.js +2 -2
- package/dist/jsx/components/DataTableColumnPinning/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableExport/index.mjs +1 -0
- package/dist/jsx/components/DataTableExport/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableExport/index.native.js +1 -0
- package/dist/jsx/components/DataTableExport/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableFilterButton/index.mjs +1 -1
- package/dist/jsx/components/DataTableFilterButton/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableFilterButton/index.native.js +4 -1
- package/dist/jsx/components/DataTableFilterButton/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableFilterChips/index.mjs +1 -1
- package/dist/jsx/components/DataTableFilterChips/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableFilterChips/index.native.js +1 -1
- package/dist/jsx/components/DataTableGrouping/index.mjs +18 -3
- package/dist/jsx/components/DataTableGrouping/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableGrouping/index.native.js +17 -1
- package/dist/jsx/components/DataTableGrouping/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableToolbar/index.mjs +1 -0
- package/dist/jsx/components/DataTableToolbar/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableToolbar/index.native.js +1 -0
- package/dist/jsx/components/DataTableToolbar/index.native.js.map +1 -1
- package/dist/jsx/components/FilterManager.mjs +18 -14
- package/dist/jsx/components/FilterManager.mjs.map +1 -1
- package/dist/jsx/components/FilterManager.native.js +18 -14
- package/dist/jsx/components/FilterManager.native.js.map +1 -1
- package/dist/jsx/components/SimpleTable/index.mjs +1 -1
- package/dist/jsx/components/SimpleTable/index.mjs.map +1 -1
- package/dist/jsx/components/SimpleTable/index.native.js +1 -1
- package/dist/jsx/components/SimpleTable/index.native.js.map +1 -1
- package/dist/jsx/components/TableInput/index.native.js +3 -0
- package/dist/jsx/components/TableInput/index.native.js.map +1 -1
- package/dist/jsx/hooks/useDataTable.mjs +17 -1
- package/dist/jsx/hooks/useDataTable.mjs.map +1 -1
- package/dist/jsx/hooks/useDataTable.native.js +19 -1
- package/dist/jsx/hooks/useDataTable.native.js.map +1 -1
- package/dist/jsx/hooks/useTableGrouping.mjs.map +1 -1
- package/dist/jsx/utils/rowPress.mjs +19 -0
- package/dist/jsx/utils/rowPress.mjs.map +1 -0
- package/dist/jsx/utils/rowPress.native.js +56 -0
- package/dist/jsx/utils/rowPress.native.js.map +1 -0
- package/package.json +12 -12
- package/src/components/ColumnHeaderFilter/index.tsx +1 -0
- package/src/components/DataTable/DataTable.spec.tsx +4 -1
- package/src/components/DataTable/DataTable.stories.tsx +190 -5
- package/src/components/DataTable/DataTableMobileCards.tsx +30 -4
- package/src/components/DataTable/DataTableRoot.tsx +195 -8
- package/src/components/DataTable/DataTableVirtualized.tsx +8 -0
- package/src/components/DataTable/cardLayout.spec.tsx +4 -1
- package/src/components/DataTable/customCellAlign.spec.tsx +124 -0
- package/src/components/DataTable/disabledContract.spec.tsx +4 -3
- package/src/components/DataTable/index.native.tsx +47 -1
- package/src/components/DataTable/rowPress.spec.tsx +229 -0
- package/src/components/DataTable/selectionSemantics.spec.tsx +156 -0
- package/src/components/DataTableBulkActions/index.tsx +16 -2
- package/src/components/DataTableBulkActions/selectionCountI18n.spec.tsx +94 -0
- package/src/components/DataTableCell/CellModalEditor.tsx +3 -1
- package/src/components/DataTableCell/DataTableCell.stories.tsx +1 -5
- package/src/components/DataTableCell/cellDraftPolicy.spec.tsx +10 -2
- package/src/components/DataTableCell/dataTableCell.spec.tsx +10 -2
- package/src/components/DataTableCell/index.tsx +5 -36
- package/src/components/DataTableColumnPinning/DataTableColumnPinning.stories.tsx +49 -17
- package/src/components/DataTableDateFilter/DataTableDateFilter.stories.tsx +4 -3
- package/src/components/DataTableExport/DataTableExport.stories.tsx +3 -2
- package/src/components/DataTableExport/index.tsx +1 -0
- package/src/components/DataTableFilterButton/index.tsx +4 -1
- package/src/components/DataTableGrouping/DataTableGrouping.spec.tsx +59 -2
- package/src/components/DataTableGrouping/DataTableGrouping.stories.tsx +17 -5
- package/src/components/DataTableGrouping/index.tsx +1 -0
- package/src/components/DataTablePagination/DataTablePagination.stories.tsx +34 -20
- package/src/components/DataTableToolbar/index.tsx +1 -0
- package/src/components/DataTableViewOptions/DataTableViewOptions.stories.tsx +2 -1
- package/src/components/FilterManager.tsx +21 -13
- package/src/components/TableInput/index.native.tsx +3 -0
- package/src/components/__scratch__/scratch.spec.tsx +46 -0
- package/src/hooks/useDataTable.ts +27 -1
- package/src/types.ts +25 -0
- package/src/utils/rowPress.ts +58 -0
- package/types/components/ColumnHeaderFilter/index.d.ts.map +1 -1
- package/types/components/DataTable/DataTableMobileCards.d.ts +13 -1
- package/types/components/DataTable/DataTableMobileCards.d.ts.map +1 -1
- package/types/components/DataTable/DataTableRoot.d.ts +1 -1
- package/types/components/DataTable/DataTableRoot.d.ts.map +1 -1
- package/types/components/DataTable/DataTableVirtualized.d.ts +7 -1
- package/types/components/DataTable/DataTableVirtualized.d.ts.map +1 -1
- package/types/components/DataTable/index.native.d.ts +1 -1
- package/types/components/DataTable/index.native.d.ts.map +1 -1
- package/types/components/DataTableBulkActions/index.d.ts.map +1 -1
- package/types/components/DataTableCell/CellModalEditor.d.ts.map +1 -1
- package/types/components/DataTableCell/index.d.ts.map +1 -1
- package/types/components/DataTableColumnPinning/index.d.ts.map +1 -1
- package/types/components/DataTableFilterButton/index.d.ts.map +1 -1
- package/types/components/DataTableGrouping/index.d.ts +8 -0
- package/types/components/DataTableGrouping/index.d.ts.map +1 -1
- package/types/components/DataTableToolbar/index.d.ts.map +1 -1
- package/types/components/FilterManager.d.ts +7 -1
- package/types/components/FilterManager.d.ts.map +1 -1
- package/types/components/TableInput/index.native.d.ts.map +1 -1
- package/types/hooks/useDataTable.d.ts.map +1 -1
- package/types/types.d.ts +23 -0
- package/types/types.d.ts.map +1 -1
- package/types/utils/rowPress.d.ts +20 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
?
|
|
140
|
-
|
|
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
|
|