@multiplatform.one/table 7.10.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/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/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/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/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/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/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.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/DataTableRoot.tsx +82 -14
- package/src/components/DataTable/cardLayout.spec.tsx +4 -3
- 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/footerLayout.spec.tsx +250 -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/DataTableRoot.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
|
@@ -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", () => {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { Preset } from "@multiplatform.one/theme";
|
|
3
|
+
import { fireEvent, waitFor } from "@testing-library/react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { DataTableColumn } from "../../types";
|
|
6
|
+
import { DataTable } from "./index";
|
|
7
|
+
|
|
8
|
+
// Happy DOM has no viewport measurements. Keep the actual virtualizer and row renderer.
|
|
9
|
+
vi.mock("@tanstack/react-virtual", async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal<typeof import("@tanstack/react-virtual")>();
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
useVirtualizer: (options: Parameters<typeof actual.useVirtualizer>[0]) =>
|
|
14
|
+
actual.useVirtualizer({
|
|
15
|
+
...options,
|
|
16
|
+
initialRect: { width: 800, height: 240 },
|
|
17
|
+
observeElementRect: (_instance, callback) => {
|
|
18
|
+
callback({ width: 800, height: 240 });
|
|
19
|
+
return () => {};
|
|
20
|
+
},
|
|
21
|
+
observeElementOffset: (_instance, callback) => {
|
|
22
|
+
callback(0, false);
|
|
23
|
+
return () => {};
|
|
24
|
+
},
|
|
25
|
+
scrollToFn: () => {},
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const data = [{ id: "1", account: "Account 1", balance: "10.00" }];
|
|
31
|
+
const columns: DataTableColumn<(typeof data)[number]>[] = [
|
|
32
|
+
{ accessorKey: "account", header: "Account", size: 320, footer: "Totals" },
|
|
33
|
+
{ accessorKey: "balance", header: "Balance", size: 80, footer: "10.00" },
|
|
34
|
+
];
|
|
35
|
+
const allAuxiliaries = {
|
|
36
|
+
enableRowSelection: true,
|
|
37
|
+
showRowNumbers: true,
|
|
38
|
+
enableRowExpansion: true,
|
|
39
|
+
showDeleteRow: true,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function renderTable(props = {}, size: "small" | "medium" | "large" = "medium") {
|
|
43
|
+
return renderWithProviders(
|
|
44
|
+
<Preset overrides={{ size }}>
|
|
45
|
+
<DataTable
|
|
46
|
+
data={data}
|
|
47
|
+
columns={columns}
|
|
48
|
+
layout="table"
|
|
49
|
+
columnLayout="scroll"
|
|
50
|
+
enablePagination={false}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
</Preset>,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cells(row: Element): HTMLElement[] {
|
|
58
|
+
return [...row.children].filter((element) =>
|
|
59
|
+
["cell", "columnheader"].includes(element.getAttribute("role") ?? ""),
|
|
60
|
+
) as HTMLElement[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function width(cell: HTMLElement): number {
|
|
64
|
+
return Number.parseFloat(
|
|
65
|
+
cell.style.width || /(?:^| )_w-([\d.]+)px(?: |$)/.exec(cell.className)?.[1] || "NaN",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function rows(container: HTMLElement) {
|
|
70
|
+
const header = container.querySelector('thead [role="row"]')!;
|
|
71
|
+
const body = container.querySelector('[aria-label="Row 1"]')!;
|
|
72
|
+
const footer = container.querySelector('tfoot [role="row"]')!;
|
|
73
|
+
expect(header).not.toBeNull();
|
|
74
|
+
expect(body).not.toBeNull();
|
|
75
|
+
expect(footer).not.toBeNull();
|
|
76
|
+
return { header: cells(header), body: cells(body), footer: cells(footer) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function expectPinned(cell: HTMLElement, side: "start" | "end", offset: number) {
|
|
80
|
+
expect(cell.style.position).toBe("sticky");
|
|
81
|
+
expect(cell.style[side === "start" ? "insetInlineStart" : "insetInlineEnd"]).toBe(`${offset}px`);
|
|
82
|
+
expect(cell.style.zIndex).toBe("2");
|
|
83
|
+
expect(cell.style.backgroundColor).toBe("var(--color2)");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let previousDirection: string | null;
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
previousDirection = document.documentElement.getAttribute("dir");
|
|
89
|
+
});
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
if (previousDirection === null) document.documentElement.removeAttribute("dir");
|
|
92
|
+
else document.documentElement.setAttribute("dir", previousDirection);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe.each(["ltr", "rtl"])("footer layout in %s", (direction) => {
|
|
96
|
+
beforeEach(() => document.documentElement.setAttribute("dir", direction));
|
|
97
|
+
|
|
98
|
+
it.each([
|
|
99
|
+
{ name: "no auxiliaries", props: {}, leading: 0, trailing: 0 },
|
|
100
|
+
{ name: "selection", props: { enableRowSelection: true }, leading: 1, trailing: 0 },
|
|
101
|
+
{ name: "row numbers", props: { showRowNumbers: true }, leading: 1, trailing: 0 },
|
|
102
|
+
{ name: "expansion", props: { enableRowExpansion: true }, leading: 1, trailing: 0 },
|
|
103
|
+
{ name: "delete", props: { showDeleteRow: true }, leading: 0, trailing: 1 },
|
|
104
|
+
{ name: "all auxiliaries", props: allAuxiliaries, leading: 3, trailing: 1 },
|
|
105
|
+
{
|
|
106
|
+
name: "read-only auxiliaries",
|
|
107
|
+
props: { ...allAuxiliaries, readOnly: true },
|
|
108
|
+
leading: 3,
|
|
109
|
+
trailing: 0,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "read-only delete suppressed",
|
|
113
|
+
props: { showDeleteRow: true, readOnly: true },
|
|
114
|
+
leading: 0,
|
|
115
|
+
trailing: 0,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "compact auxiliaries",
|
|
119
|
+
props: { ...allAuxiliaries, compact: true },
|
|
120
|
+
leading: 3,
|
|
121
|
+
trailing: 1,
|
|
122
|
+
},
|
|
123
|
+
])("matches resolved header/body slots with $name", ({ props, leading, trailing }) => {
|
|
124
|
+
const { container } = renderTable(props);
|
|
125
|
+
const row = rows(container);
|
|
126
|
+
const headerWidths = row.header.map(width);
|
|
127
|
+
expect(row.header).toHaveLength(leading + 2 + trailing);
|
|
128
|
+
expect(headerWidths.every((value) => Number.isFinite(value) && value > 0)).toBe(true);
|
|
129
|
+
expect(headerWidths.slice(leading, leading + 2)).toEqual([320, 80]);
|
|
130
|
+
expect(row.body.map(width)).toEqual(headerWidths);
|
|
131
|
+
expect(row.footer.map(width)).toEqual(headerWidths);
|
|
132
|
+
expect(row.footer[leading]).toHaveTextContent("Totals");
|
|
133
|
+
expect(row.footer[leading + 1]).toHaveTextContent("10.00");
|
|
134
|
+
const auxiliaryCells = [...row.footer.slice(0, leading), ...row.footer.slice(leading + 2)];
|
|
135
|
+
for (const cell of auxiliaryCells) {
|
|
136
|
+
expect(cell.textContent).toBe("");
|
|
137
|
+
expect(cell.tabIndex).toBe(-1);
|
|
138
|
+
expect(
|
|
139
|
+
cell.querySelector('button, input, [role="button"], [role="checkbox"], [tabindex]'),
|
|
140
|
+
).toBeNull();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it.each(["left", "right"] as const)(
|
|
145
|
+
"resolves pinned %s footer paint like header/body",
|
|
146
|
+
async (side) => {
|
|
147
|
+
const utils = renderTable({ enableColumnPinning: true });
|
|
148
|
+
fireEvent.click(utils.getByLabelText("Pin Account column left"));
|
|
149
|
+
if (side === "right") fireEvent.click(utils.getByLabelText("Pin Account column right"));
|
|
150
|
+
await waitFor(() =>
|
|
151
|
+
expect(
|
|
152
|
+
(utils.container.querySelector("#account-header") as HTMLElement).style.position,
|
|
153
|
+
).toBe("sticky"),
|
|
154
|
+
);
|
|
155
|
+
const row = rows(utils.container);
|
|
156
|
+
const index = row.header.findIndex((cell) => cell.id === "account-header");
|
|
157
|
+
const logicalSide = side === "left" ? "start" : "end";
|
|
158
|
+
expectPinned(row.header[index], logicalSide, 0);
|
|
159
|
+
expectPinned(row.body[index], logicalSide, 0);
|
|
160
|
+
expectPinned(row.footer[index], logicalSide, 0);
|
|
161
|
+
expect(row.footer[1 - index].style.position).not.toBe("sticky");
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
it("follows resolved auxiliary widths at small and large preset sizes", () => {
|
|
166
|
+
const widthsBySize = ["small", "large"].map((size) => {
|
|
167
|
+
const utils = renderTable(allAuxiliaries, size as "small" | "large");
|
|
168
|
+
const row = rows(utils.container);
|
|
169
|
+
const headerWidths = row.header.map(width);
|
|
170
|
+
expect(headerWidths).toHaveLength(6);
|
|
171
|
+
expect(headerWidths.slice(3, 5)).toEqual([320, 80]);
|
|
172
|
+
expect(row.body.map(width)).toEqual(headerWidths);
|
|
173
|
+
expect(row.footer.map(width)).toEqual(headerWidths);
|
|
174
|
+
utils.unmount();
|
|
175
|
+
return headerWidths;
|
|
176
|
+
});
|
|
177
|
+
for (const index of [0, 1, 2, 5]) {
|
|
178
|
+
expect(widthsBySize[0][index]).toBeLessThan(widthsBySize[1][index]);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it.each([false, true])(
|
|
183
|
+
"reserves auxiliary flow around both pin sides, readOnly=%s",
|
|
184
|
+
async (readOnly) => {
|
|
185
|
+
const utils = renderTable({ ...allAuxiliaries, readOnly, enableColumnPinning: true });
|
|
186
|
+
fireEvent.click(utils.getByLabelText("Pin Account column left"));
|
|
187
|
+
fireEvent.click(utils.getByLabelText("Pin Balance column left"));
|
|
188
|
+
fireEvent.click(utils.getByLabelText("Pin Balance column right"));
|
|
189
|
+
await waitFor(() => expect(rows(utils.container).header[3].style.position).toBe("sticky"));
|
|
190
|
+
const row = rows(utils.container);
|
|
191
|
+
const headerWidths = row.header.map(width);
|
|
192
|
+
const leadingWidth = headerWidths.slice(0, 3).reduce((sum, value) => sum + value, 0);
|
|
193
|
+
const trailingWidth = readOnly ? 0 : headerWidths[5];
|
|
194
|
+
expect(leadingWidth).toBeGreaterThan(0);
|
|
195
|
+
expect(row.body.map(width)).toEqual(headerWidths);
|
|
196
|
+
expect(row.footer.map(width)).toEqual(headerWidths);
|
|
197
|
+
for (const section of [row.header, row.body, row.footer]) {
|
|
198
|
+
expectPinned(section[3], "start", leadingWidth);
|
|
199
|
+
expectPinned(section[4], "end", trailingWidth);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
it("keeps actual virtual rows and auxiliary footer slots aligned after pinned resize", async () => {
|
|
205
|
+
const utils = renderWithProviders(
|
|
206
|
+
<DataTable
|
|
207
|
+
data={[
|
|
208
|
+
{ id: "1", rank: 1, label: "Record 1" },
|
|
209
|
+
{ id: "2", rank: 2, label: "Record 2" },
|
|
210
|
+
]}
|
|
211
|
+
columns={[
|
|
212
|
+
{ accessorKey: "rank", header: "Rank", size: 48, minSize: 32, maxSize: 64, footer: "2" },
|
|
213
|
+
{ accessorKey: "label", header: "Label", size: 320, footer: "Total" },
|
|
214
|
+
]}
|
|
215
|
+
layout="table"
|
|
216
|
+
columnLayout="scroll"
|
|
217
|
+
enableColumnResizing
|
|
218
|
+
enableColumnPinning
|
|
219
|
+
enableVirtualization
|
|
220
|
+
virtualizationHeight={240}
|
|
221
|
+
enablePagination={false}
|
|
222
|
+
{...allAuxiliaries}
|
|
223
|
+
/>,
|
|
224
|
+
);
|
|
225
|
+
const virtualRows = () =>
|
|
226
|
+
utils.container.querySelectorAll('tbody[role="presentation"] [role="rowgroup"] [role="row"]');
|
|
227
|
+
await waitFor(() => expect(virtualRows()).toHaveLength(2));
|
|
228
|
+
fireEvent.click(utils.getByLabelText("Pin Rank column left"));
|
|
229
|
+
fireEvent.click(utils.getByLabelText("Pin Label column left"));
|
|
230
|
+
const header = () => cells(utils.container.querySelector('thead [role="row"]')!);
|
|
231
|
+
const leadingWidth = header()
|
|
232
|
+
.slice(0, 3)
|
|
233
|
+
.reduce((sum, cell) => sum + width(cell), 0);
|
|
234
|
+
expect(width(header()[3])).toBe(48);
|
|
235
|
+
expectPinned(header()[4], "start", leadingWidth + 48);
|
|
236
|
+
const handle = utils.getByRole("separator", { name: "Resize Rank column" });
|
|
237
|
+
fireEvent.mouseDown(handle, { clientX: 100 });
|
|
238
|
+
fireEvent.mouseMove(document, { clientX: 130 });
|
|
239
|
+
fireEvent.mouseUp(document, { clientX: 130 });
|
|
240
|
+
expect(width(header()[3])).toBe(64);
|
|
241
|
+
expectPinned(header()[4], "start", leadingWidth + 64);
|
|
242
|
+
const footer = cells(utils.container.querySelector('tfoot [role="row"]')!);
|
|
243
|
+
expect(virtualRows()).toHaveLength(2);
|
|
244
|
+
for (const section of [...virtualRows()].map(cells).concat([footer])) {
|
|
245
|
+
expect(section.map(width)).toEqual(header().map(width));
|
|
246
|
+
expectPinned(section[3], "start", leadingWidth);
|
|
247
|
+
expectPinned(section[4], "start", leadingWidth + 64);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
18
18
|
import { fireEvent, waitFor } from "@testing-library/react";
|
|
19
|
-
import { describe, expect, it, vi } from "vitest";
|
|
19
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
20
20
|
import type { DataTableColumn } from "../../types";
|
|
21
21
|
import { DataTable } from "./index";
|
|
22
22
|
|
|
@@ -142,3 +142,68 @@ describe("column pinning geometry", () => {
|
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
144
|
});
|
|
145
|
+
|
|
146
|
+
afterEach(() => document.documentElement.removeAttribute("dir"));
|
|
147
|
+
|
|
148
|
+
it.each(["ltr", "rtl"])(
|
|
149
|
+
"keeps declared small pinned boxes and logical offsets aligned in %s",
|
|
150
|
+
async (direction) => {
|
|
151
|
+
document.documentElement.setAttribute("dir", direction);
|
|
152
|
+
const utils = renderPinned({
|
|
153
|
+
columns: columns.map((column, index) => ({
|
|
154
|
+
...column,
|
|
155
|
+
footer: String(column.header),
|
|
156
|
+
width: index === 0 ? 48 : 320,
|
|
157
|
+
})),
|
|
158
|
+
});
|
|
159
|
+
await pinIdColumn(utils);
|
|
160
|
+
fireEvent.click(utils.getByLabelText("Pin First Name column left"));
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
const first = utils.container.querySelector("#id-header") as HTMLElement;
|
|
163
|
+
const second = utils.container.querySelector("#firstName-header") as HTMLElement;
|
|
164
|
+
expect(hasStyle(first, "width", "48px", "_w-48px")).toBe(true);
|
|
165
|
+
expect(hasStyle(first, "minWidth", "48px", "_miw-48px")).toBe(true);
|
|
166
|
+
expect(hasStyle(first, "maxWidth", "48px", "_maw-48px")).toBe(true);
|
|
167
|
+
expect(second.style.insetInlineStart).toBe("48px");
|
|
168
|
+
const body = utils.container.querySelector('[role="row"][aria-label="Row 1"]')!;
|
|
169
|
+
const cells = body.querySelectorAll('[role="cell"]');
|
|
170
|
+
expect(hasStyle(cells[0] as HTMLElement, "width", "48px", "_w-48px")).toBe(true);
|
|
171
|
+
expect((cells[1] as HTMLElement).style.insetInlineStart).toBe("48px");
|
|
172
|
+
const footer = utils.container.querySelectorAll('tfoot [role="cell"]');
|
|
173
|
+
expect(hasStyle(footer[0] as HTMLElement, "width", "48px", "_w-48px")).toBe(true);
|
|
174
|
+
expect((footer[1] as HTMLElement).style.insetInlineStart).toBe("48px");
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
it.each(["ltr", "rtl"])(
|
|
180
|
+
"aligns right-pinned headers, rows and footers in %s",
|
|
181
|
+
async (direction) => {
|
|
182
|
+
document.documentElement.setAttribute("dir", direction);
|
|
183
|
+
const utils = renderPinned({
|
|
184
|
+
columns: columns.map((column, index) => ({
|
|
185
|
+
...column,
|
|
186
|
+
footer: String(column.header),
|
|
187
|
+
width: index === 3 ? 76 : 320,
|
|
188
|
+
})),
|
|
189
|
+
});
|
|
190
|
+
for (const label of ["Last Name", "Age"]) {
|
|
191
|
+
fireEvent.click(utils.getByLabelText(`Pin ${label} column left`));
|
|
192
|
+
fireEvent.click(utils.getByLabelText(`Pin ${label} column right`));
|
|
193
|
+
}
|
|
194
|
+
await waitFor(() => {
|
|
195
|
+
const last = utils.container.querySelector("#age-header") as HTMLElement;
|
|
196
|
+
const previous = utils.container.querySelector("#lastName-header") as HTMLElement;
|
|
197
|
+
expect(hasStyle(last, "width", "76px", "_w-76px")).toBe(true);
|
|
198
|
+
expect(last.style.insetInlineEnd).toBe("0px");
|
|
199
|
+
expect(previous.style.insetInlineEnd).toBe("76px");
|
|
200
|
+
const body = utils.container.querySelector('[role="row"][aria-label="Row 1"]')!;
|
|
201
|
+
const cells = body.querySelectorAll('[role="cell"]');
|
|
202
|
+
expect(hasStyle(cells[3] as HTMLElement, "width", "76px", "_w-76px")).toBe(true);
|
|
203
|
+
expect((cells[2] as HTMLElement).style.insetInlineEnd).toBe("76px");
|
|
204
|
+
const footer = utils.container.querySelectorAll('tfoot [role="cell"]');
|
|
205
|
+
expect(hasStyle(footer[3] as HTMLElement, "width", "76px", "_w-76px")).toBe(true);
|
|
206
|
+
expect((footer[2] as HTMLElement).style.insetInlineEnd).toBe("76px");
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
1
|
+
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
|
2
2
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { DataTable } from "../DataTable";
|
|
@@ -203,16 +203,30 @@ describe("DataTableExport", () => {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
it("shows loading state when export is in progress", async () => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
let finishExport!: () => void;
|
|
207
|
+
const pending = new Promise<void>((resolve) => {
|
|
208
|
+
finishExport = resolve;
|
|
209
|
+
});
|
|
210
|
+
const mockOnExport = vi.fn(() => pending) as unknown as ReturnType<typeof vi.fn> &
|
|
211
|
+
((options: ExportOptions) => Promise<void>);
|
|
209
212
|
|
|
210
213
|
renderWithProviders(
|
|
211
214
|
<DataTableExport data={mockData} columns={mockColumns} onExport={mockOnExport} />,
|
|
212
215
|
);
|
|
213
216
|
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
const exportButton = screen.getByRole("button", { name: /Export as CSV/i });
|
|
218
|
+
try {
|
|
219
|
+
fireEvent.click(exportButton);
|
|
220
|
+
expect(screen.getByText("...")).toBeInTheDocument();
|
|
221
|
+
expect(exportButton).toHaveAttribute("aria-disabled", "true");
|
|
222
|
+
} finally {
|
|
223
|
+
await act(async () => {
|
|
224
|
+
finishExport();
|
|
225
|
+
await pending;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
expect(exportButton).toHaveTextContent("CSV");
|
|
229
|
+
expect(exportButton).not.toHaveAttribute("aria-disabled", "true");
|
|
216
230
|
});
|
|
217
231
|
|
|
218
232
|
it("disables buttons when disabled prop is true", () => {
|
|
@@ -599,9 +613,12 @@ describe("DataTableExport", () => {
|
|
|
599
613
|
});
|
|
600
614
|
|
|
601
615
|
it("prevents multiple simultaneous exports", async () => {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
616
|
+
let finishExport!: () => void;
|
|
617
|
+
const pending = new Promise<void>((resolve) => {
|
|
618
|
+
finishExport = resolve;
|
|
619
|
+
});
|
|
620
|
+
const mockOnExport = vi.fn(() => pending) as unknown as ReturnType<typeof vi.fn> &
|
|
621
|
+
((options: ExportOptions) => Promise<void>);
|
|
605
622
|
|
|
606
623
|
renderWithProviders(
|
|
607
624
|
<DataTableExport data={mockData} columns={mockColumns} onExport={mockOnExport} />,
|
|
@@ -609,13 +626,22 @@ describe("DataTableExport", () => {
|
|
|
609
626
|
|
|
610
627
|
// Click export twice quickly - button shows "..." when loading
|
|
611
628
|
const exportButton = screen.getByRole("button", { name: /Export as CSV/i });
|
|
612
|
-
|
|
613
|
-
|
|
629
|
+
try {
|
|
630
|
+
fireEvent.click(exportButton);
|
|
631
|
+
fireEvent.click(exportButton);
|
|
614
632
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
633
|
+
// Should only call once
|
|
634
|
+
await waitFor(() => {
|
|
635
|
+
expect(mockOnExport).toHaveBeenCalledTimes(1);
|
|
636
|
+
});
|
|
637
|
+
} finally {
|
|
638
|
+
await act(async () => {
|
|
639
|
+
finishExport();
|
|
640
|
+
await pending;
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
expect(exportButton).toHaveTextContent("CSV");
|
|
644
|
+
expect(exportButton).not.toHaveAttribute("aria-disabled", "true");
|
|
619
645
|
});
|
|
620
646
|
|
|
621
647
|
it("surfaces an inline error when Blob URLs are unavailable", async () => {
|
|
@@ -629,9 +655,7 @@ describe("DataTableExport", () => {
|
|
|
629
655
|
renderWithProviders(<DataTableExport data={mockData} columns={mockColumns} />);
|
|
630
656
|
fireEvent.click(screen.getByRole("button", { name: /Export as CSV/i }));
|
|
631
657
|
await waitFor(() => {
|
|
632
|
-
expect(screen.getByRole("alert").textContent).toMatch(
|
|
633
|
-
/not available on this platform/i,
|
|
634
|
-
);
|
|
658
|
+
expect(screen.getByRole("alert").textContent).toMatch(/not available on this platform/i);
|
|
635
659
|
});
|
|
636
660
|
expect(mockCreateObjectURL).not.toHaveBeenCalled();
|
|
637
661
|
} finally {
|
|
@@ -91,6 +91,14 @@ function GroupHeader({ children }: { children: string }) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export function DataTableFilterButton() {
|
|
94
|
+
return <FilterButton />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function DataTableCardFilterButton() {
|
|
98
|
+
return <FilterButton contentTrigger />;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function FilterButton({ contentTrigger = false }: { contentTrigger?: boolean }) {
|
|
94
102
|
const ctx = useDataTableState();
|
|
95
103
|
const { knobProps } = useResolvedKnobs();
|
|
96
104
|
// "Back to field list" points toward the panel's start edge — directional,
|
|
@@ -315,6 +323,7 @@ export function DataTableFilterButton() {
|
|
|
315
323
|
open={open}
|
|
316
324
|
onOpenChange={handleOpenChange}
|
|
317
325
|
trigger={trigger}
|
|
326
|
+
{...(contentTrigger ? { triggerSizing: "content" as const } : {})}
|
|
318
327
|
fitContent
|
|
319
328
|
scrollable={false}
|
|
320
329
|
>
|