@multiplatform.one/table 7.8.0 → 7.11.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/README.md +60 -0
- package/dist/cjs/components/DataTable/DataTableCardControls.cjs +90 -54
- package/dist/cjs/components/DataTable/DataTableCardControls.native.js +93 -56
- package/dist/cjs/components/DataTable/DataTableCardControls.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableMobileCards.cjs +100 -33
- package/dist/cjs/components/DataTable/DataTableMobileCards.native.js +139 -38
- package/dist/cjs/components/DataTable/DataTableMobileCards.native.js.map +1 -1
- package/dist/cjs/components/DataTable/DataTableRoot.cjs +51 -8
- package/dist/cjs/components/DataTable/DataTableRoot.native.js +53 -9
- package/dist/cjs/components/DataTable/DataTableRoot.native.js.map +1 -1
- package/dist/cjs/components/DataTable/index.native.js +59 -93
- package/dist/cjs/components/DataTable/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableFilterButton/index.cjs +14 -0
- package/dist/cjs/components/DataTableFilterButton/index.native.js +15 -0
- package/dist/cjs/components/DataTableFilterButton/index.native.js.map +1 -1
- package/dist/cjs/components/DataTableToolbar/index.cjs +178 -101
- package/dist/cjs/components/DataTableToolbar/index.native.js +187 -109
- package/dist/cjs/components/DataTableToolbar/index.native.js.map +1 -1
- package/dist/cjs/hooks/useDataTable.cjs +32 -1
- package/dist/cjs/hooks/useDataTable.native.js +37 -1
- package/dist/cjs/hooks/useDataTable.native.js.map +1 -1
- package/dist/esm/components/DataTable/DataTableCardControls.mjs +89 -54
- package/dist/esm/components/DataTable/DataTableCardControls.mjs.map +1 -1
- package/dist/esm/components/DataTable/DataTableCardControls.native.js +92 -56
- package/dist/esm/components/DataTable/DataTableCardControls.native.js.map +1 -1
- package/dist/esm/components/DataTable/DataTableMobileCards.mjs +99 -34
- package/dist/esm/components/DataTable/DataTableMobileCards.mjs.map +1 -1
- package/dist/esm/components/DataTable/DataTableMobileCards.native.js +138 -39
- package/dist/esm/components/DataTable/DataTableMobileCards.native.js.map +1 -1
- package/dist/esm/components/DataTable/DataTableRoot.mjs +53 -10
- package/dist/esm/components/DataTable/DataTableRoot.mjs.map +1 -1
- package/dist/esm/components/DataTable/DataTableRoot.native.js +55 -11
- package/dist/esm/components/DataTable/DataTableRoot.native.js.map +1 -1
- package/dist/esm/components/DataTable/index.native.js +60 -94
- package/dist/esm/components/DataTable/index.native.js.map +1 -1
- package/dist/esm/components/DataTableFilterButton/index.mjs +14 -1
- package/dist/esm/components/DataTableFilterButton/index.mjs.map +1 -1
- package/dist/esm/components/DataTableFilterButton/index.native.js +15 -1
- package/dist/esm/components/DataTableFilterButton/index.native.js.map +1 -1
- package/dist/esm/components/DataTableToolbar/index.mjs +180 -104
- package/dist/esm/components/DataTableToolbar/index.mjs.map +1 -1
- package/dist/esm/components/DataTableToolbar/index.native.js +189 -112
- package/dist/esm/components/DataTableToolbar/index.native.js.map +1 -1
- package/dist/esm/hooks/useDataTable.mjs +32 -1
- package/dist/esm/hooks/useDataTable.mjs.map +1 -1
- package/dist/esm/hooks/useDataTable.native.js +37 -1
- package/dist/esm/hooks/useDataTable.native.js.map +1 -1
- package/dist/jsx/components/DataTable/DataTableCardControls.mjs +89 -54
- package/dist/jsx/components/DataTable/DataTableCardControls.mjs.map +1 -1
- package/dist/jsx/components/DataTable/DataTableCardControls.native.js +93 -56
- package/dist/jsx/components/DataTable/DataTableCardControls.native.js.map +1 -1
- package/dist/jsx/components/DataTable/DataTableMobileCards.mjs +99 -34
- package/dist/jsx/components/DataTable/DataTableMobileCards.mjs.map +1 -1
- package/dist/jsx/components/DataTable/DataTableMobileCards.native.js +139 -38
- package/dist/jsx/components/DataTable/DataTableMobileCards.native.js.map +1 -1
- package/dist/jsx/components/DataTable/DataTableRoot.mjs +53 -10
- package/dist/jsx/components/DataTable/DataTableRoot.mjs.map +1 -1
- package/dist/jsx/components/DataTable/DataTableRoot.native.js +53 -9
- package/dist/jsx/components/DataTable/DataTableRoot.native.js.map +1 -1
- package/dist/jsx/components/DataTable/index.native.js +59 -93
- package/dist/jsx/components/DataTable/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableFilterButton/index.mjs +14 -1
- package/dist/jsx/components/DataTableFilterButton/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableFilterButton/index.native.js +15 -0
- package/dist/jsx/components/DataTableFilterButton/index.native.js.map +1 -1
- package/dist/jsx/components/DataTableToolbar/index.mjs +180 -104
- package/dist/jsx/components/DataTableToolbar/index.mjs.map +1 -1
- package/dist/jsx/components/DataTableToolbar/index.native.js +187 -109
- package/dist/jsx/components/DataTableToolbar/index.native.js.map +1 -1
- package/dist/jsx/hooks/useDataTable.mjs +32 -1
- package/dist/jsx/hooks/useDataTable.mjs.map +1 -1
- package/dist/jsx/hooks/useDataTable.native.js +37 -1
- package/dist/jsx/hooks/useDataTable.native.js.map +1 -1
- package/package.json +14 -14
- package/src/components/DataTable/DataTable.spec.tsx +5 -3
- package/src/components/DataTable/DataTable.stories.tsx +59 -0
- package/src/components/DataTable/DataTableCardControls.stories.tsx +61 -2
- package/src/components/DataTable/DataTableCardControls.tsx +92 -66
- package/src/components/DataTable/DataTableMobileCards.tsx +162 -39
- package/src/components/DataTable/DataTableRoot.tsx +82 -14
- package/src/components/DataTable/cardLayout.spec.tsx +23 -11
- package/src/components/DataTable/cardToolbar.spec.tsx +238 -0
- package/src/components/DataTable/chromeInterplay.spec.tsx +7 -5
- package/src/components/DataTable/columnWidthLayout.spec.tsx +189 -0
- package/src/components/DataTable/dataTableCardControls.spec.tsx +3 -2
- package/src/components/DataTable/dataTableMobileCards.spec.tsx +50 -2
- package/src/components/DataTable/footerLayout.spec.tsx +250 -0
- package/src/components/DataTable/index.native.tsx +80 -124
- package/src/components/DataTable/nativeCardParity.spec.tsx +95 -0
- package/src/components/DataTable/pinningGeometry.spec.tsx +66 -1
- package/src/components/DataTableExport/DataTableExport.spec.tsx +42 -18
- package/src/components/DataTableFilterButton/index.tsx +9 -0
- package/src/components/DataTableToolbar/index.tsx +184 -105
- package/src/hooks/useDataTable.spec.ts +114 -0
- package/src/hooks/useDataTable.ts +23 -1
- package/src/types.ts +14 -12
- package/types/components/DataTable/DataTableCardControls.d.ts +3 -0
- package/types/components/DataTable/DataTableCardControls.d.ts.map +1 -1
- package/types/components/DataTable/DataTableMobileCards.d.ts +42 -8
- package/types/components/DataTable/DataTableMobileCards.d.ts.map +1 -1
- package/types/components/DataTable/DataTableRoot.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/DataTableFilterButton/index.d.ts +2 -1
- package/types/components/DataTableFilterButton/index.d.ts.map +1 -1
- package/types/components/DataTableToolbar/index.d.ts +1 -0
- package/types/components/DataTableToolbar/index.d.ts.map +1 -1
- package/types/hooks/useDataTable.d.ts.map +1 -1
- package/types/types.d.ts +14 -12
- package/types/types.d.ts.map +1 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { DataTableColumn, FilterableField } from "../../types";
|
|
5
|
+
import { DataTableCardToolbar, DataTableToolbar } from "../DataTableToolbar";
|
|
6
|
+
import { DataTableCardSortControl } from "./DataTableCardControls";
|
|
7
|
+
import { useDataTableState } from "./DataTableStateContext";
|
|
8
|
+
import { DataTable } from "./index";
|
|
9
|
+
|
|
10
|
+
const filings = [
|
|
11
|
+
{ id: "a", form: "Alpha", entity: "personal", year: 2023 },
|
|
12
|
+
{ id: "b", form: "Beta", entity: "business", year: 2024 },
|
|
13
|
+
{ id: "c", form: "Gamma", entity: "personal", year: 2023 },
|
|
14
|
+
];
|
|
15
|
+
const columns: DataTableColumn<(typeof filings)[number]>[] = [
|
|
16
|
+
{ accessorKey: "form", header: "Form" },
|
|
17
|
+
{ accessorKey: "entity", header: "Entity" },
|
|
18
|
+
];
|
|
19
|
+
const filterableFields: FilterableField[] = [{ id: "year", label: "Year", type: "number" }];
|
|
20
|
+
|
|
21
|
+
function Cards(props: Record<string, any>) {
|
|
22
|
+
return (
|
|
23
|
+
<DataTable
|
|
24
|
+
data={filings}
|
|
25
|
+
columns={columns}
|
|
26
|
+
filterableFields={filterableFields}
|
|
27
|
+
layout="cards"
|
|
28
|
+
enableGlobalSearch={false}
|
|
29
|
+
onStateChange={vi.fn()}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cardTitles(container: HTMLElement) {
|
|
36
|
+
return [...container.querySelectorAll('[data-card-slot="title"]')].map((el) => el.textContent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function AllCardActionsToolbar(props: Record<string, any>) {
|
|
40
|
+
const { table } = useDataTableState()!;
|
|
41
|
+
return (
|
|
42
|
+
<DataTableCardToolbar
|
|
43
|
+
{...props}
|
|
44
|
+
enableExport
|
|
45
|
+
enableViews
|
|
46
|
+
isFetching
|
|
47
|
+
customActions={<DataTableCardSortControl table={table} contained />}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("default card toolbar", () => {
|
|
53
|
+
it("owns Filter and Sort in one primary row with no duplicate controls", () => {
|
|
54
|
+
const { container } = renderWithProviders(<Cards />);
|
|
55
|
+
const toolbar = screen.getByRole("toolbar", { name: "Table toolbar" });
|
|
56
|
+
expect(screen.getAllByRole("toolbar")).toHaveLength(1);
|
|
57
|
+
const primary = container.querySelector<HTMLElement>("[data-table-card-primary]")!;
|
|
58
|
+
expect(toolbar.contains(primary)).toBe(true);
|
|
59
|
+
expect(primary.contains(screen.getByRole("button", { name: "Filter" }))).toBe(true);
|
|
60
|
+
expect(primary.contains(screen.getByLabelText("Sort rows"))).toBe(true);
|
|
61
|
+
expect(screen.queryByText("Add Filter")).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps search and secondary view/fetching controls outside the primary pair", () => {
|
|
65
|
+
const { container } = renderWithProviders(<Cards enableGlobalSearch enableViews isFetching />);
|
|
66
|
+
const primary = container.querySelector<HTMLElement>("[data-table-card-primary]")!;
|
|
67
|
+
const secondary = container.querySelector<HTMLElement>("[data-table-card-secondary]")!;
|
|
68
|
+
expect(primary).not.toBeNull();
|
|
69
|
+
expect(secondary).not.toBeNull();
|
|
70
|
+
expect(primary.contains(screen.getByLabelText("Search table data"))).toBe(false);
|
|
71
|
+
expect(secondary.contains(screen.getByLabelText("Save view"))).toBe(true);
|
|
72
|
+
expect(secondary.querySelector("[data-table-fetching]")).not.toBeNull();
|
|
73
|
+
expect(primary.contains(secondary)).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("keeps a sort-only toolbar when search, filtering and views are all off", () => {
|
|
77
|
+
renderWithProviders(<Cards enableFiltering={false} enableViews={false} />);
|
|
78
|
+
const toolbar = screen.getByRole("toolbar", { name: "Table toolbar" });
|
|
79
|
+
expect(within(toolbar).getByLabelText("Sort rows")).toBeTruthy();
|
|
80
|
+
expect(screen.queryByRole("button", { name: "Filter" })).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not create an empty toolbar for unsortable columns", () => {
|
|
84
|
+
renderWithProviders(
|
|
85
|
+
<Cards
|
|
86
|
+
enableFiltering={false}
|
|
87
|
+
columns={columns.map((column) => ({ ...column, enableSorting: false }))}
|
|
88
|
+
/>,
|
|
89
|
+
);
|
|
90
|
+
expect(screen.queryByRole("toolbar")).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("exposes the full active sorting identity without replacing the button label recipe", () => {
|
|
94
|
+
const longHeader = "Form with a deliberately long distinguishing classification suffix";
|
|
95
|
+
renderWithProviders(
|
|
96
|
+
<Cards
|
|
97
|
+
columns={[{ accessorKey: "form", header: longHeader }]}
|
|
98
|
+
initialState={{ sorting: [{ id: "form", desc: true }] }}
|
|
99
|
+
/>,
|
|
100
|
+
);
|
|
101
|
+
const trigger = screen.getByLabelText("Sort rows");
|
|
102
|
+
expect(trigger.getAttribute("aria-description")).toBe(`${longHeader} ↓`);
|
|
103
|
+
expect(trigger.textContent).toContain(`${longHeader} ↓`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("retains hidden Year filtering, chip edit and removal in readOnly cards", async () => {
|
|
107
|
+
const { container } = renderWithProviders(<Cards readOnly />);
|
|
108
|
+
expect(screen.queryByText("Add Filter")).toBeNull();
|
|
109
|
+
fireEvent.click(screen.getByRole("button", { name: "Filter" }));
|
|
110
|
+
fireEvent.click(await screen.findByRole("option", { name: "Year" }));
|
|
111
|
+
fireEvent.change(await screen.findByPlaceholderText("Enter value..."), {
|
|
112
|
+
target: { value: "2023" },
|
|
113
|
+
});
|
|
114
|
+
await waitFor(() => expect(cardTitles(container)).toEqual(["Alpha", "Gamma"]));
|
|
115
|
+
fireEvent.keyDown(document, { key: "Escape" });
|
|
116
|
+
await waitFor(() => expect(screen.queryByPlaceholderText("Enter value...")).toBeNull());
|
|
117
|
+
fireEvent.click(await screen.findByRole("button", { name: /Edit filter.*Year/ }));
|
|
118
|
+
const value = await screen.findByPlaceholderText("Enter value...");
|
|
119
|
+
expect((value as HTMLInputElement).value).toBe("2023");
|
|
120
|
+
fireEvent.change(value, { target: { value: "2024" } });
|
|
121
|
+
await waitFor(() => expect(cardTitles(container)).toEqual(["Beta"]));
|
|
122
|
+
fireEvent.keyDown(document, { key: "Escape" });
|
|
123
|
+
await waitFor(() => expect(screen.queryByPlaceholderText("Enter value...")).toBeNull());
|
|
124
|
+
fireEvent.click(screen.getByRole("button", { name: "Remove" }));
|
|
125
|
+
await waitFor(() => expect(cardTitles(container)).toEqual(["Alpha", "Beta", "Gamma"]));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("keeps the custom-toolbar fallback and clears its active filter", async () => {
|
|
129
|
+
const { container } = renderWithProviders(
|
|
130
|
+
<Cards
|
|
131
|
+
renderToolbar={() => <div>Custom actions</div>}
|
|
132
|
+
initialState={{
|
|
133
|
+
filters: [{ id: "f", columnId: "form", value: { operator: "contains", value: "Alpha" } }],
|
|
134
|
+
}}
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
expect(screen.getByText("Custom actions")).toBeTruthy();
|
|
138
|
+
const fallback = screen.getByRole("toolbar", { name: "Card list controls" });
|
|
139
|
+
expect(within(fallback).getByRole("button", { name: /Add Filter/ })).toBeTruthy();
|
|
140
|
+
expect(cardTitles(container)).toEqual(["Alpha"]);
|
|
141
|
+
fireEvent.click(within(fallback).getByRole("button", { name: "Clear" }));
|
|
142
|
+
await waitFor(() => expect(cardTitles(container)).toEqual(["Alpha", "Beta", "Gamma"]));
|
|
143
|
+
fireEvent.click(within(fallback).getByRole("button", { name: "Add Filter" }));
|
|
144
|
+
expect(await screen.findByText("Select column")).toBeTruthy();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("leaves an explicitly supplied canonical custom toolbar intact", () => {
|
|
148
|
+
renderWithProviders(<Cards renderToolbar={(props: any) => <DataTableToolbar {...props} />} />);
|
|
149
|
+
expect(screen.getByRole("button", { name: "Filter" })).toBeTruthy();
|
|
150
|
+
expect(screen.getByRole("button", { name: "Add Filter" })).toBeTruthy();
|
|
151
|
+
expect(screen.getByRole("toolbar", { name: "Card list controls" })).toBeTruthy();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("partitions actual export, long saved view, save and fetching from Filter and Sort", () => {
|
|
155
|
+
const name = "Saved view with a deliberately long distinguishing classification suffix";
|
|
156
|
+
const { container } = renderWithProviders(
|
|
157
|
+
<Cards
|
|
158
|
+
enableGlobalSearch
|
|
159
|
+
initialState={{
|
|
160
|
+
activeViewId: "long",
|
|
161
|
+
views: [
|
|
162
|
+
{
|
|
163
|
+
id: "long",
|
|
164
|
+
name,
|
|
165
|
+
columns: {},
|
|
166
|
+
filters: [],
|
|
167
|
+
sorting: [],
|
|
168
|
+
pagination: { pageIndex: 0, pageSize: 10 },
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
}}
|
|
172
|
+
renderToolbar={(props: any) => <AllCardActionsToolbar {...props} />}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
const primary = container.querySelector<HTMLElement>("[data-table-card-primary]")!;
|
|
176
|
+
const secondary = container.querySelector<HTMLElement>("[data-table-card-secondary]")!;
|
|
177
|
+
expect(within(primary).getByLabelText("Sort rows")).toBeTruthy();
|
|
178
|
+
expect(within(primary).getByRole("button", { name: "Filter" })).toBeTruthy();
|
|
179
|
+
expect(within(secondary).getByRole("button", { name: /Export as CSV/ })).toBeTruthy();
|
|
180
|
+
expect(within(secondary).getByLabelText("Saved views").textContent).toContain(name);
|
|
181
|
+
expect(within(secondary).getByLabelText("Save view")).toBeTruthy();
|
|
182
|
+
expect(secondary.querySelector("[data-table-fetching]")).not.toBeNull();
|
|
183
|
+
expect(within(primary).queryByLabelText("Saved views")).toBeNull();
|
|
184
|
+
const sort = primary.querySelector("[data-table-card-sort]")!;
|
|
185
|
+
expect(sort.contains(within(primary).getByLabelText("Sort rows"))).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("retains hidden filters and sort order when cards change to grid and back", () => {
|
|
189
|
+
const initialState = {
|
|
190
|
+
filters: [{ id: "year", columnId: "year", value: { operator: "is", value: "2023" } }],
|
|
191
|
+
sorting: [{ id: "form", desc: true }],
|
|
192
|
+
};
|
|
193
|
+
const { container, rerender } = renderWithProviders(<Cards initialState={initialState} />);
|
|
194
|
+
expect(cardTitles(container)).toEqual(["Gamma", "Alpha"]);
|
|
195
|
+
rerender(<Cards initialState={initialState} layout="table" />);
|
|
196
|
+
expect(cardTitles(container)).toEqual([]);
|
|
197
|
+
expect(screen.queryByLabelText("Sort rows")).toBeNull();
|
|
198
|
+
expect(screen.getByText("Gamma")).toBeTruthy();
|
|
199
|
+
expect(screen.queryByText("Beta")).toBeNull();
|
|
200
|
+
rerender(<Cards initialState={initialState} />);
|
|
201
|
+
expect(cardTitles(container)).toEqual(["Gamma", "Alpha"]);
|
|
202
|
+
expect(screen.getByLabelText("Sort rows").getAttribute("aria-description")).toBe("Form ↓");
|
|
203
|
+
expect(screen.getAllByRole("button", { name: "Filter" })).toHaveLength(1);
|
|
204
|
+
expect(screen.queryByPlaceholderText("Enter value...")).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("clears hidden and visible advanced filters together without clearing plain column filters", async () => {
|
|
208
|
+
const { container } = renderWithProviders(
|
|
209
|
+
<Cards
|
|
210
|
+
initialState={{
|
|
211
|
+
filters: [
|
|
212
|
+
{ id: "year", columnId: "year", value: { operator: "is", value: "2023" } },
|
|
213
|
+
{ id: "form", columnId: "form", value: { operator: "contains", value: "Alpha" } },
|
|
214
|
+
],
|
|
215
|
+
columnFilters: [{ id: "entity", value: "personal" }],
|
|
216
|
+
}}
|
|
217
|
+
/>,
|
|
218
|
+
);
|
|
219
|
+
expect(cardTitles(container)).toEqual(["Alpha"]);
|
|
220
|
+
fireEvent.click(screen.getByRole("button", { name: "Clear all" }));
|
|
221
|
+
await waitFor(() => expect(cardTitles(container)).toEqual(["Alpha", "Gamma"]));
|
|
222
|
+
expect(screen.queryByRole("button", { name: /Edit filter/ })).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("leaves the public standalone toolbar in its original layout", () => {
|
|
226
|
+
const { container } = renderWithProviders(
|
|
227
|
+
<DataTableToolbar
|
|
228
|
+
enableGlobalSearch
|
|
229
|
+
enableViews
|
|
230
|
+
customActions={<button type="button">Custom action</button>}
|
|
231
|
+
/>,
|
|
232
|
+
);
|
|
233
|
+
expect(container.querySelector<HTMLElement>("[data-table-card-primary]")).toBeNull();
|
|
234
|
+
expect(container.querySelector<HTMLElement>("[data-table-card-secondary]")).toBeNull();
|
|
235
|
+
expect(screen.getByLabelText("Search table data")).toBeTruthy();
|
|
236
|
+
expect(screen.getByText("Custom action")).toBeTruthy();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -204,8 +204,11 @@ describe("chrome interplay: Search × injected filters × enableFiltering", () =
|
|
|
204
204
|
expect(screen.getByLabelText("Search table data")).toBeInTheDocument();
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
it("enableFiltering=false suppresses the card layout's
|
|
207
|
+
it("enableFiltering=false suppresses the card layout's filter control too", () => {
|
|
208
208
|
// auto layout renders cards below sm (the test env default).
|
|
209
|
+
// MPO-300: the default card path owns the canonical all-field Filter in
|
|
210
|
+
// the toolbar; the lower Card list controls toolbar and its Add Filter
|
|
211
|
+
// builder only mount behind a custom toolbar now.
|
|
209
212
|
const control = renderWithProviders(
|
|
210
213
|
<DataTable
|
|
211
214
|
data={rows}
|
|
@@ -214,10 +217,8 @@ describe("chrome interplay: Search × injected filters × enableFiltering", () =
|
|
|
214
217
|
initialState={{ filters: injectedReadyFilter }}
|
|
215
218
|
/>,
|
|
216
219
|
);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// aria-label is the stable handle, as the sibling arms above use.
|
|
220
|
-
expect(screen.getByLabelText("Add Filter")).toBeInTheDocument();
|
|
220
|
+
expect(screen.getByRole("button", { name: "Filter" })).toBeInTheDocument();
|
|
221
|
+
expect(screen.queryByText("Add Filter")).not.toBeInTheDocument();
|
|
221
222
|
control.unmount();
|
|
222
223
|
|
|
223
224
|
renderWithProviders(
|
|
@@ -228,6 +229,7 @@ describe("chrome interplay: Search × injected filters × enableFiltering", () =
|
|
|
228
229
|
initialState={{ filters: injectedReadyFilter }}
|
|
229
230
|
/>,
|
|
230
231
|
);
|
|
232
|
+
expect(screen.queryByRole("button", { name: "Filter" })).not.toBeInTheDocument();
|
|
231
233
|
expect(screen.queryByText("Add Filter")).not.toBeInTheDocument();
|
|
232
234
|
// Injected filters still scope the card list.
|
|
233
235
|
expect(screen.getByText("Alpha")).toBeInTheDocument();
|
|
@@ -11,12 +11,34 @@
|
|
|
11
11
|
* render. The combination warns instead, naming `columnLayout` as the reason.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { fireEvent, waitFor } from "@testing-library/react";
|
|
14
15
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
15
16
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
17
|
import type { DataTableColumn } from "../../types";
|
|
17
18
|
import { __resetTableDevWarnSeen, warnColumnWidthIgnored } from "../../shared/devWarn";
|
|
18
19
|
import { DataTable } from "./index";
|
|
19
20
|
|
|
21
|
+
vi.mock("@tanstack/react-virtual", async (importOriginal) => {
|
|
22
|
+
const actual = await importOriginal<typeof import("@tanstack/react-virtual")>();
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
useVirtualizer: (options: Parameters<typeof actual.useVirtualizer>[0]) =>
|
|
26
|
+
actual.useVirtualizer({
|
|
27
|
+
...options,
|
|
28
|
+
initialRect: { width: 800, height: 240 },
|
|
29
|
+
observeElementRect: (_instance, callback) => {
|
|
30
|
+
callback({ width: 800, height: 240 });
|
|
31
|
+
return () => {};
|
|
32
|
+
},
|
|
33
|
+
observeElementOffset: (_instance, callback) => {
|
|
34
|
+
callback(0, false);
|
|
35
|
+
return () => {};
|
|
36
|
+
},
|
|
37
|
+
scrollToFn: () => {},
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
20
42
|
type Pokemon = { id: number; name: string; xp: number };
|
|
21
43
|
|
|
22
44
|
const sampleData: Pokemon[] = [
|
|
@@ -193,3 +215,170 @@ describe("DataTable columnLayout=equal with per-column widths", () => {
|
|
|
193
215
|
expect(widthWarnings()).toHaveLength(0);
|
|
194
216
|
});
|
|
195
217
|
});
|
|
218
|
+
|
|
219
|
+
function hasSizingStyle(element: HTMLElement, property: string, value: string, token: string) {
|
|
220
|
+
return (
|
|
221
|
+
element.style.getPropertyValue(property) === value || String(element.className).includes(token)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function expectCellWidth(element: HTMLElement | null, width: number) {
|
|
226
|
+
expect(element).not.toBeNull();
|
|
227
|
+
expect(hasSizingStyle(element!, "width", `${width}px`, `_w-${width}px`)).toBe(true);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
describe("DataTable declared column sizes", () => {
|
|
231
|
+
beforeEach(startCapture);
|
|
232
|
+
afterEach(stopCapture);
|
|
233
|
+
|
|
234
|
+
it.each(["alias", "canonical"])(
|
|
235
|
+
"applies %s widths to scroll headers, body and footer below the old floor",
|
|
236
|
+
(spelling) => {
|
|
237
|
+
const sizingColumns = plainColumns.map((column, index) => ({
|
|
238
|
+
...column,
|
|
239
|
+
[spelling === "alias" ? "width" : "size"]: index === 0 ? 320 : 48,
|
|
240
|
+
footer: index === 0 ? "Totals" : "126",
|
|
241
|
+
}));
|
|
242
|
+
const { container } = renderTable(sizingColumns, "scroll");
|
|
243
|
+
expectCellWidth(container.querySelector("#name-header"), 320);
|
|
244
|
+
expectCellWidth(container.querySelector("#xp-header"), 48);
|
|
245
|
+
const cells = container.querySelectorAll('[aria-label="Table data"] [role="cell"]');
|
|
246
|
+
expect(cells.length).toBeGreaterThanOrEqual(4);
|
|
247
|
+
expectCellWidth(cells[0] as HTMLElement, 320);
|
|
248
|
+
expectCellWidth(cells[1] as HTMLElement, 48);
|
|
249
|
+
expect(hasSizingStyle(cells[1] as HTMLElement, "min-width", "0px", "_miw-0px")).toBe(true);
|
|
250
|
+
const footer = container.querySelectorAll('tfoot [role="cell"]');
|
|
251
|
+
expect(footer).toHaveLength(2);
|
|
252
|
+
expectCellWidth(footer[0] as HTMLElement, 320);
|
|
253
|
+
expectCellWidth(footer[1] as HTMLElement, 48);
|
|
254
|
+
expect(widthWarnings()).toHaveLength(0);
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
it("gives unpinned equal cells the same flex policy for canonical and alias declarations", () => {
|
|
259
|
+
const canonical = plainColumns.map((column, index) => ({
|
|
260
|
+
...column,
|
|
261
|
+
size: index === 0 ? 320 : 80,
|
|
262
|
+
}));
|
|
263
|
+
const withSizes = renderTable(canonical);
|
|
264
|
+
const sizeMarkup = stripGeneratedIds(withSizes.container.innerHTML);
|
|
265
|
+
withSizes.unmount();
|
|
266
|
+
const withoutSizes = renderTable(plainColumns);
|
|
267
|
+
expect(stripGeneratedIds(withoutSizes.container.innerHTML)).toBe(sizeMarkup);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("lets resizing use aliases in equal mode while preserving the fixed flex basis", () => {
|
|
271
|
+
const { container } = renderWithProviders(
|
|
272
|
+
<DataTable
|
|
273
|
+
data={sampleData}
|
|
274
|
+
columns={widthColumns}
|
|
275
|
+
columnLayout="equal"
|
|
276
|
+
layout="table"
|
|
277
|
+
enableColumnResizing
|
|
278
|
+
enablePagination={false}
|
|
279
|
+
readOnly
|
|
280
|
+
/>,
|
|
281
|
+
);
|
|
282
|
+
const header = container.querySelector("#name-header") as HTMLElement;
|
|
283
|
+
expectCellWidth(header, 320);
|
|
284
|
+
expect(hasSizingStyle(header, "flex-basis", "auto", "_fb-auto")).toBe(true);
|
|
285
|
+
expect(hasSizingStyle(header, "flex-grow", "0", "_fg-0")).toBe(true);
|
|
286
|
+
expect(widthWarnings()).toHaveLength(0);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("does not inflate a 48px resizable equal-layout cell with the primitive width floor", () => {
|
|
290
|
+
const { container } = renderWithProviders(
|
|
291
|
+
<DataTable
|
|
292
|
+
data={sampleData}
|
|
293
|
+
columns={[{ accessorKey: "xp", header: "XP", width: 48 }]}
|
|
294
|
+
columnLayout="equal"
|
|
295
|
+
layout="table"
|
|
296
|
+
enableColumnResizing
|
|
297
|
+
enablePagination={false}
|
|
298
|
+
readOnly
|
|
299
|
+
/>,
|
|
300
|
+
);
|
|
301
|
+
const header = container.querySelector("#xp-header") as HTMLElement;
|
|
302
|
+
expectCellWidth(header, 48);
|
|
303
|
+
expect(hasSizingStyle(header, "min-width", "0px", "_miw-0px")).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("passes size inputs to auto layout without changing its table algorithm", () => {
|
|
307
|
+
const { container } = renderTable(widthColumns, "auto");
|
|
308
|
+
expectCellWidth(container.querySelector("#name-header"), 320);
|
|
309
|
+
expect((container.querySelector('[role="table"]') as HTMLElement).style.tableLayout).toBe(
|
|
310
|
+
"fixed",
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("resizes an aliased column through the real header handle within its bounds", () => {
|
|
315
|
+
const columns = widthColumns.map((column) => ({ ...column, minWidth: 50, maxWidth: 350 }));
|
|
316
|
+
const { container, getByRole } = renderWithProviders(
|
|
317
|
+
<DataTable
|
|
318
|
+
data={sampleData}
|
|
319
|
+
columns={columns}
|
|
320
|
+
columnLayout="scroll"
|
|
321
|
+
layout="table"
|
|
322
|
+
enableColumnResizing
|
|
323
|
+
enablePagination={false}
|
|
324
|
+
readOnly
|
|
325
|
+
/>,
|
|
326
|
+
);
|
|
327
|
+
const handle = getByRole("separator", { name: "Resize Name column" });
|
|
328
|
+
expect(handle.getAttribute("aria-valuenow")).toBe("320");
|
|
329
|
+
fireEvent.mouseDown(handle, { clientX: 100 });
|
|
330
|
+
fireEvent.mouseMove(document, { clientX: 120 });
|
|
331
|
+
fireEvent.mouseUp(document, { clientX: 120 });
|
|
332
|
+
expectCellWidth(container.querySelector("#name-header"), 340);
|
|
333
|
+
fireEvent.mouseDown(handle, { clientX: 100 });
|
|
334
|
+
fireEvent.mouseMove(document, { clientX: 300 });
|
|
335
|
+
fireEvent.mouseUp(document, { clientX: 300 });
|
|
336
|
+
expectCellWidth(container.querySelector("#name-header"), 350);
|
|
337
|
+
expect(handle.getAttribute("aria-valuenow")).toBe("350");
|
|
338
|
+
const cell = container.querySelector<HTMLElement>('[aria-label="Table data"] [role="cell"]');
|
|
339
|
+
expectCellWidth(cell, 350);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("exposes the normalized size to the view-options display on mount", async () => {
|
|
343
|
+
const { getByText } = renderWithProviders(
|
|
344
|
+
<DataTable
|
|
345
|
+
data={sampleData}
|
|
346
|
+
columns={widthColumns}
|
|
347
|
+
columnLayout="scroll"
|
|
348
|
+
layout="table"
|
|
349
|
+
enableColumnResizing
|
|
350
|
+
enablePagination={false}
|
|
351
|
+
readOnly
|
|
352
|
+
>
|
|
353
|
+
<DataTable.ViewOptions showColumnResizing showColumnVisibility={false} />
|
|
354
|
+
</DataTable>,
|
|
355
|
+
);
|
|
356
|
+
await waitFor(() => expect(getByText("320px")).toBeDefined());
|
|
357
|
+
expect(getByText("80px")).toBeDefined();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("uses the real virtual row renderer with the same declared widths", async () => {
|
|
361
|
+
const { container } = renderWithProviders(
|
|
362
|
+
<DataTable
|
|
363
|
+
data={sampleData}
|
|
364
|
+
columns={widthColumns}
|
|
365
|
+
columnLayout="scroll"
|
|
366
|
+
layout="table"
|
|
367
|
+
enableVirtualization
|
|
368
|
+
virtualizationHeight={240}
|
|
369
|
+
enablePagination={false}
|
|
370
|
+
readOnly
|
|
371
|
+
/>,
|
|
372
|
+
);
|
|
373
|
+
await waitFor(() => {
|
|
374
|
+
const virtualBody = container.querySelector('tbody[role="presentation"] [role="rowgroup"]');
|
|
375
|
+
expect(virtualBody).not.toBeNull();
|
|
376
|
+
expect(virtualBody!.querySelectorAll('[role="row"]')).toHaveLength(2);
|
|
377
|
+
const cells = virtualBody!.querySelectorAll('[role="cell"]');
|
|
378
|
+
expect(cells.length).toBeGreaterThanOrEqual(4);
|
|
379
|
+
expectCellWidth(cells[0] as HTMLElement, 320);
|
|
380
|
+
expectCellWidth(cells[1] as HTMLElement, 80);
|
|
381
|
+
});
|
|
382
|
+
expectCellWidth(container.querySelector("#name-header"), 320);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -42,7 +42,7 @@ function renderCards(props: Record<string, any> = {}) {
|
|
|
42
42
|
describe("DataTableCardControls", () => {
|
|
43
43
|
it("is a labelled toolbar above the card list", () => {
|
|
44
44
|
renderCards({ enableSorting: true, enableFiltering: true });
|
|
45
|
-
const toolbar = screen.getByRole("toolbar", { name: "
|
|
45
|
+
const toolbar = screen.getByRole("toolbar", { name: "Table toolbar" });
|
|
46
46
|
expect(toolbar).toBeTruthy();
|
|
47
47
|
expect(screen.getByLabelText("Sort rows")).toBeTruthy();
|
|
48
48
|
});
|
|
@@ -61,7 +61,8 @@ describe("DataTableCardControls", () => {
|
|
|
61
61
|
it("filtering only: the filter chrome is there, the sort menu is not", () => {
|
|
62
62
|
renderCards({ enableSorting: false, enableFiltering: true });
|
|
63
63
|
expect(screen.queryByLabelText("Sort rows")).toBeNull();
|
|
64
|
-
expect(screen.
|
|
64
|
+
expect(screen.getByRole("button", { name: "Filter" })).toBeTruthy();
|
|
65
|
+
expect(screen.queryByText("Add Filter")).toBeNull();
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
it("with no sort applied the trigger reads the neutral label", () => {
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
* delete cluster beside the title, the delete floor at `minRows`, and the two
|
|
9
9
|
* distinct empty-state copies (no data vs no results) — plus the rule that an
|
|
10
10
|
* in-flight refetch paints NO empty state over a momentarily blank body.
|
|
11
|
+
*
|
|
12
|
+
* It also pins `classifyCardField`, which decides whether a field prints its
|
|
13
|
+
* column name at all. In the grid a header is written once for a whole
|
|
14
|
+
* column; on a card it would be written once per value.
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
14
|
-
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
18
|
+
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
|
15
19
|
import { describe, expect, it, vi } from "vitest";
|
|
16
20
|
import type { DataTableColumn, TableFilter } from "../../types";
|
|
17
21
|
import { DataTable } from "./index";
|
|
@@ -52,7 +56,7 @@ describe("DataTableMobileCards", () => {
|
|
|
52
56
|
expect(cards(container).length).toBe(people.length);
|
|
53
57
|
});
|
|
54
58
|
|
|
55
|
-
it("the first visible column is the card title, the rest are
|
|
59
|
+
it("the first visible column is the card title, the rest are classified fields", () => {
|
|
56
60
|
const { container } = renderCards();
|
|
57
61
|
const first = cards(container)[0];
|
|
58
62
|
expect(first.querySelector('[data-card-slot="title"]')?.textContent).toBe("John");
|
|
@@ -69,6 +73,50 @@ describe("DataTableMobileCards", () => {
|
|
|
69
73
|
expect(first.textContent).not.toContain("Last Name");
|
|
70
74
|
});
|
|
71
75
|
|
|
76
|
+
it("a self-naming string goes bare, into one wrapped flow with its name hidden", () => {
|
|
77
|
+
const { container } = renderCards();
|
|
78
|
+
const first = cards(container)[0];
|
|
79
|
+
const meta = first.querySelector('[data-card-slot="meta"]');
|
|
80
|
+
expect(meta).not.toBeNull();
|
|
81
|
+
expect(meta!.textContent).toContain("Doe");
|
|
82
|
+
expect(within(first).queryByText("Last Name")).toBeNull();
|
|
83
|
+
expect(first.textContent).toContain("Last Name:");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("a figure keeps its column name, inline beside the value", () => {
|
|
87
|
+
const { container } = renderCards();
|
|
88
|
+
const first = cards(container)[0];
|
|
89
|
+
const figure = first.querySelector('[data-card-slot="figure"]');
|
|
90
|
+
expect(figure).not.toBeNull();
|
|
91
|
+
expect(figure!.textContent).toContain("28");
|
|
92
|
+
expect(within(first).getByText("Age")).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("a field with no value spends no row at all", () => {
|
|
96
|
+
const { container } = renderCards({
|
|
97
|
+
data: [{ id: 1, firstName: "John", lastName: "", age: 28 }],
|
|
98
|
+
});
|
|
99
|
+
const first = cards(container)[0];
|
|
100
|
+
expect(first.querySelector('[data-card-slot="meta"]')).toBeNull();
|
|
101
|
+
expect(first.textContent).not.toContain("Last Name");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("a control keeps a row of its own and prints no label", () => {
|
|
105
|
+
// Explicit id on BOTH columns: DataTable seeds columnOrder from
|
|
106
|
+
// `col.id || ""`, so an id-less accessor column beside an id'd one would
|
|
107
|
+
// put `_actions` first and make the control the card title instead.
|
|
108
|
+
const { container } = renderCards({
|
|
109
|
+
columns: [
|
|
110
|
+
{ id: "firstName", accessorKey: "firstName", header: "First Name" },
|
|
111
|
+
{ id: "_actions", accessorKey: "lastName", header: "Actions", meta: { role: "actions" } },
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
const first = cards(container)[0];
|
|
115
|
+
expect(first.querySelector('[data-card-slot="control"]')).not.toBeNull();
|
|
116
|
+
expect(within(first).queryByText("Actions")).toBeNull();
|
|
117
|
+
expect(first.textContent).toContain("Actions:");
|
|
118
|
+
});
|
|
119
|
+
|
|
72
120
|
it("selection adds a select-all bar above the stack and a checkbox per card", () => {
|
|
73
121
|
const { container } = renderCards({ enableRowSelection: true });
|
|
74
122
|
expect(screen.getByLabelText("Select all rows")).toBeTruthy();
|