@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.
- 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 +49 -12
- 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.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.native.js.map +1 -1
- package/dist/cjs/components/DataTableFilters/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableGrouping/index.cjs +1 -0
- package/dist/cjs/components/DataTableGrouping/index.native.js +1 -0
- 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.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 +39 -2
- 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/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/DataTableGrouping/index.mjs +1 -0
- package/dist/esm/components/DataTableGrouping/index.mjs.map +1 -1
- package/dist/esm/components/DataTableGrouping/index.native.js +1 -0
- 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/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/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 +39 -2
- 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/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/DataTableGrouping/index.mjs +1 -0
- package/dist/jsx/components/DataTableGrouping/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableGrouping/index.native.js +1 -0
- 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/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/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/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/DataTableFilterButton/index.d.ts.map +1 -1
- 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
|
@@ -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
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// D-15: the selection-count label is i18n-safe — each variant is ONE
|
|
2
|
+
// whole-sentence t() key ("{{n}} selected", "{{n}} selected (+{{hidden}}
|
|
3
|
+
// filtered out)"), never concatenated English fragments, so locales own the
|
|
4
|
+
// word order. This spec runs with an initialized i18next instance whose
|
|
5
|
+
// translations deliberately differ from the keys (reordered words), proving
|
|
6
|
+
// the label routes through t() — the sibling DataTableBulkActions.spec.tsx
|
|
7
|
+
// covers the untranslated fallback (withInterp fills the raw key).
|
|
8
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
9
|
+
import { cleanup, screen } from "@testing-library/react";
|
|
10
|
+
import i18next from "i18next";
|
|
11
|
+
import { initReactI18next } from "react-i18next";
|
|
12
|
+
import React from "react";
|
|
13
|
+
import {
|
|
14
|
+
getCoreRowModel,
|
|
15
|
+
getFilteredRowModel,
|
|
16
|
+
getPaginationRowModel,
|
|
17
|
+
useReactTable,
|
|
18
|
+
} from "@tanstack/react-table";
|
|
19
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
20
|
+
import { DataTableStateContext } from "../DataTable/DataTableStateContext";
|
|
21
|
+
import { type BulkAction, DataTableBulkActions } from "./index";
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
// Same natural-language keys registered in packages/i18n/en/common.json,
|
|
25
|
+
// mapped to a pseudo-locale that reorders the sentence.
|
|
26
|
+
await i18next.use(initReactI18next).init({
|
|
27
|
+
lng: "en",
|
|
28
|
+
interpolation: { escapeValue: false },
|
|
29
|
+
resources: {
|
|
30
|
+
en: {
|
|
31
|
+
translation: {
|
|
32
|
+
"{{n}} selected": "selected: {{n}}",
|
|
33
|
+
"{{n}} selected (+{{hidden}} filtered out)":
|
|
34
|
+
"selected: {{n}} (hidden by filter: {{hidden}})",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(cleanup);
|
|
42
|
+
|
|
43
|
+
type Row = { id: string; name: string; status: string };
|
|
44
|
+
|
|
45
|
+
const rows: Row[] = [
|
|
46
|
+
{ id: "a", name: "Alpha", status: "ready" },
|
|
47
|
+
{ id: "b", name: "Bravo", status: "blocked" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const actions: BulkAction<Row>[] = [{ id: "x", label: "X", onExecute: vi.fn() }];
|
|
51
|
+
|
|
52
|
+
function Harness({ columnFilters = [] }: { columnFilters?: Array<{ id: string; value: any }> }) {
|
|
53
|
+
const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({
|
|
54
|
+
a: true,
|
|
55
|
+
b: true,
|
|
56
|
+
});
|
|
57
|
+
const table = useReactTable({
|
|
58
|
+
data: rows,
|
|
59
|
+
columns: [
|
|
60
|
+
{ id: "name", accessorKey: "name", header: "Name" },
|
|
61
|
+
{ id: "status", accessorKey: "status", header: "Status" },
|
|
62
|
+
] as any,
|
|
63
|
+
state: { rowSelection, columnFilters, pagination: { pageIndex: 0, pageSize: 10 } },
|
|
64
|
+
onRowSelectionChange: setRowSelection,
|
|
65
|
+
getCoreRowModel: getCoreRowModel(),
|
|
66
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
67
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
68
|
+
getRowId: (row) => row.id,
|
|
69
|
+
enableRowSelection: true,
|
|
70
|
+
});
|
|
71
|
+
const ctxValue = React.useMemo(
|
|
72
|
+
() => ({ table, state: {} as any, actions: {} as any, computed: {} as any }) as any,
|
|
73
|
+
[table, rowSelection, columnFilters],
|
|
74
|
+
);
|
|
75
|
+
return (
|
|
76
|
+
<DataTableStateContext.Provider value={ctxValue}>
|
|
77
|
+
<DataTableBulkActions actions={actions} />
|
|
78
|
+
</DataTableStateContext.Provider>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("selection count label i18n (D-15)", () => {
|
|
83
|
+
it("base count renders through the {{n}} selected key", () => {
|
|
84
|
+
renderWithProviders(<Harness />);
|
|
85
|
+
expect(screen.getByText("selected: 2")).toBeInTheDocument();
|
|
86
|
+
expect(screen.queryByText(/\d selected/)).not.toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("filtered-out annotation renders through its own whole-sentence key", () => {
|
|
90
|
+
renderWithProviders(<Harness columnFilters={[{ id: "status", value: "ready" }]} />);
|
|
91
|
+
expect(screen.getByText("selected: 2 (hidden by filter: 1)")).toBeInTheDocument();
|
|
92
|
+
expect(screen.queryByText(/filtered out/)).not.toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* footer stays within the two-action dialog budget (LC-31).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { DialogOverlay } from "@multiplatform.one/components";
|
|
15
16
|
import { FieldDisplay, type FieldDisplayType } from "@multiplatform.one/forms";
|
|
16
17
|
import { TableCellContext } from "@multiplatform.one/table-primitives";
|
|
17
18
|
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
@@ -118,11 +119,12 @@ export function CellModalEditor({
|
|
|
118
119
|
</View>
|
|
119
120
|
<Dialog open={open} onOpenChange={handleOpenChange} modal>
|
|
120
121
|
<Dialog.Portal>
|
|
121
|
-
<
|
|
122
|
+
<DialogOverlay opacity={0.5} />
|
|
122
123
|
<Dialog.Content
|
|
123
124
|
borderWidth={1}
|
|
124
125
|
borderColor="$borderColor"
|
|
125
126
|
elevation={knobProps.elevation}
|
|
127
|
+
transition={knobProps.transition}
|
|
126
128
|
enterStyle={{ y: -10, opacity: 0, scale: 0.96 }}
|
|
127
129
|
exitStyle={{ y: 10, opacity: 0, scale: 0.96 }}
|
|
128
130
|
maxWidth={720}
|
|
@@ -188,7 +188,11 @@ function MockMultiDatePicker({ value, onChange }: any) {
|
|
|
188
188
|
);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
// Partial mock: real module first (module-scope exports like `zIndex` keep
|
|
192
|
+
// resolving for transitive imports), field components faked on top — the
|
|
193
|
+
// spec asserts draft/commit policy per field, not the fields themselves.
|
|
194
|
+
vi.mock("@multiplatform.one/forms", async (importOriginal) => ({
|
|
195
|
+
...(await importOriginal<typeof import("@multiplatform.one/forms")>()),
|
|
192
196
|
Checkbox: createMock("Checkbox"),
|
|
193
197
|
Switch: createMock("Switch"),
|
|
194
198
|
Select: MockSelect,
|
|
@@ -231,7 +235,11 @@ vi.mock("@multiplatform.one/rich-text", () => ({
|
|
|
231
235
|
RichTextEditor: createMock("RichTextEditor"),
|
|
232
236
|
}));
|
|
233
237
|
|
|
234
|
-
|
|
238
|
+
// Partial mock (importOriginal): only the knobs hook is stubbed — real
|
|
239
|
+
// module-scope exports (MIN_PRESS_TARGET, press-target helpers, formatters)
|
|
240
|
+
// keep resolving for every component this spec transitively imports.
|
|
241
|
+
vi.mock("@multiplatform.one/theme", async (importOriginal) => ({
|
|
242
|
+
...(await importOriginal<typeof import("@multiplatform.one/theme")>()),
|
|
235
243
|
useResolvedKnobs: () => ({
|
|
236
244
|
knobProps: {
|
|
237
245
|
sizeToken: "$3",
|
|
@@ -30,7 +30,11 @@ function createMock(name: string) {
|
|
|
30
30
|
return Comp;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// Partial mock: real module first (module-scope exports like `zIndex` keep
|
|
34
|
+
// resolving for transitive imports), field components faked on top — the
|
|
35
|
+
// spec asserts ROUTING to fields, not the fields themselves.
|
|
36
|
+
vi.mock("@multiplatform.one/forms", async (importOriginal) => ({
|
|
37
|
+
...(await importOriginal<typeof import("@multiplatform.one/forms")>()),
|
|
34
38
|
Checkbox: createMock("Checkbox"),
|
|
35
39
|
Switch: createMock("Switch"),
|
|
36
40
|
Select: createMock("Select"),
|
|
@@ -94,7 +98,11 @@ vi.mock("@multiplatform.one/rich-text", () => ({
|
|
|
94
98
|
RichTextEditor: createMock("RichTextEditor"),
|
|
95
99
|
}));
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
// Partial mock (importOriginal): only the knobs hook is stubbed — real
|
|
102
|
+
// module-scope exports (MIN_PRESS_TARGET, press-target helpers, formatters)
|
|
103
|
+
// keep resolving for every component this spec transitively imports.
|
|
104
|
+
vi.mock("@multiplatform.one/theme", async (importOriginal) => ({
|
|
105
|
+
...(await importOriginal<typeof import("@multiplatform.one/theme")>()),
|
|
98
106
|
useResolvedKnobs: () => ({
|
|
99
107
|
knobProps: {
|
|
100
108
|
sizeToken: "$3",
|
|
@@ -175,19 +175,16 @@ export function DataTableCell({
|
|
|
175
175
|
// large tables performant and gives heavy fields a purpose-built compact
|
|
176
176
|
// representation. Edit-mode (below) still mounts the full field component.
|
|
177
177
|
if (readOnly) {
|
|
178
|
-
//
|
|
179
|
-
// ISO-
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
fieldProps.currency != null
|
|
183
|
-
? { ...fieldProps, currency: normalizeCurrencyCode(fieldProps.currency) }
|
|
184
|
-
: fieldProps;
|
|
178
|
+
// Currency symbols ride through untouched: the money register in
|
|
179
|
+
// `@multiplatform.one/theme` owns symbol→ISO-code resolution (LC-12 one
|
|
180
|
+
// channel per data class) and prefixes a symbol it has no code for, so a
|
|
181
|
+
// cell never has to sanitize a value on the way to the formatter.
|
|
185
182
|
return (
|
|
186
183
|
<CellWrapper justify={cellJustify}>
|
|
187
184
|
<FieldDisplay
|
|
188
185
|
field={field}
|
|
189
186
|
value={value}
|
|
190
|
-
fieldProps={
|
|
187
|
+
fieldProps={fieldProps}
|
|
191
188
|
linkedValue={_linkedValue}
|
|
192
189
|
/>
|
|
193
190
|
</CellWrapper>
|
|
@@ -1182,34 +1179,6 @@ function DraftFieldEditor({
|
|
|
1182
1179
|
);
|
|
1183
1180
|
}
|
|
1184
1181
|
|
|
1185
|
-
const CURRENCY_SYMBOL_TO_CODE: Record<string, string> = {
|
|
1186
|
-
$: "USD",
|
|
1187
|
-
"€": "EUR",
|
|
1188
|
-
"£": "GBP",
|
|
1189
|
-
"¥": "JPY",
|
|
1190
|
-
"₹": "INR",
|
|
1191
|
-
"₩": "KRW",
|
|
1192
|
-
"₽": "RUB",
|
|
1193
|
-
R$: "BRL",
|
|
1194
|
-
};
|
|
1195
|
-
|
|
1196
|
-
const currencyCodeCache = new Map<string, string | undefined>();
|
|
1197
|
-
|
|
1198
|
-
function normalizeCurrencyCode(currency: unknown): string | undefined {
|
|
1199
|
-
if (typeof currency !== "string" || !currency) return undefined;
|
|
1200
|
-
if (currencyCodeCache.has(currency)) return currencyCodeCache.get(currency);
|
|
1201
|
-
const mapped = CURRENCY_SYMBOL_TO_CODE[currency] ?? currency;
|
|
1202
|
-
let resolved: string | undefined;
|
|
1203
|
-
try {
|
|
1204
|
-
new Intl.NumberFormat(undefined, { style: "currency", currency: mapped });
|
|
1205
|
-
resolved = mapped;
|
|
1206
|
-
} catch {
|
|
1207
|
-
resolved = undefined;
|
|
1208
|
-
}
|
|
1209
|
-
currencyCodeCache.set(currency, resolved);
|
|
1210
|
-
return resolved;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
1182
|
function CellWrapper({
|
|
1214
1183
|
children,
|
|
1215
1184
|
justify = "flex-start",
|