@elabs-ai/components-data 4.2.0 → 5.1.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 +1 -1
- package/dist/chunk-3OVN7MMO.js +2 -0
- package/dist/chunk-3OVN7MMO.js.map +1 -0
- package/dist/chunk-3UFWYVDS.js +2 -0
- package/dist/chunk-3UFWYVDS.js.map +1 -0
- package/dist/chunk-47ZBQZYS.js +47 -0
- package/dist/chunk-47ZBQZYS.js.map +1 -0
- package/dist/chunk-5K2IFOV2.js +2 -0
- package/dist/chunk-5K2IFOV2.js.map +1 -0
- package/dist/chunk-677ORWI6.js +17 -0
- package/dist/chunk-677ORWI6.js.map +1 -0
- package/dist/chunk-7ZYUY3UL.js +2 -0
- package/dist/chunk-7ZYUY3UL.js.map +1 -0
- package/dist/chunk-BEYHV3ZX.js +37 -0
- package/dist/chunk-BEYHV3ZX.js.map +1 -0
- package/dist/chunk-DBPJ7LVM.js +57 -0
- package/dist/chunk-DBPJ7LVM.js.map +1 -0
- package/dist/chunk-E3V3SYY7.js +87 -0
- package/dist/chunk-E3V3SYY7.js.map +1 -0
- package/dist/chunk-GBO56756.js +81 -0
- package/dist/chunk-GBO56756.js.map +1 -0
- package/dist/chunk-HG2YMSRM.js +95 -0
- package/dist/chunk-HG2YMSRM.js.map +1 -0
- package/dist/chunk-K2GHSRCP.js +1823 -0
- package/dist/chunk-K2GHSRCP.js.map +1 -0
- package/dist/chunk-KCB33PD3.js +43 -0
- package/dist/chunk-KCB33PD3.js.map +1 -0
- package/dist/chunk-MV2YVXEZ.js +67 -0
- package/dist/chunk-MV2YVXEZ.js.map +1 -0
- package/dist/chunk-NKVJUQYW.js +82 -0
- package/dist/chunk-NKVJUQYW.js.map +1 -0
- package/dist/chunk-OASM53LL.js +29 -0
- package/dist/chunk-OASM53LL.js.map +1 -0
- package/dist/chunk-RZ5MYZOA.js +2 -0
- package/dist/chunk-RZ5MYZOA.js.map +1 -0
- package/dist/chunk-SK6347WB.js +100 -0
- package/dist/chunk-SK6347WB.js.map +1 -0
- package/dist/chunk-TBB33PHQ.js +58 -0
- package/dist/chunk-TBB33PHQ.js.map +1 -0
- package/dist/chunk-VCLA45IC.js +150 -0
- package/dist/chunk-VCLA45IC.js.map +1 -0
- package/dist/chunk-XN6O6Z2N.js +58 -0
- package/dist/chunk-XN6O6Z2N.js.map +1 -0
- package/dist/chunk-ZFEW36RG.js +77 -0
- package/dist/chunk-ZFEW36RG.js.map +1 -0
- package/dist/chunk-ZQXJ34VJ.js +34 -0
- package/dist/chunk-ZQXJ34VJ.js.map +1 -0
- package/dist/chunk-ZUCGV2XD.js +42 -0
- package/dist/chunk-ZUCGV2XD.js.map +1 -0
- package/dist/column-picker/column-picker.js +8 -0
- package/dist/column-picker/column-picker.js.map +1 -0
- package/dist/column-picker/index.js +9 -0
- package/dist/column-picker/index.js.map +1 -0
- package/dist/data-table/card-layout.js +11 -0
- package/dist/data-table/card-layout.js.map +1 -0
- package/dist/data-table/cell-scales.js +26 -0
- package/dist/data-table/cell-scales.js.map +1 -0
- package/dist/data-table/cells/bar-cell.js +14 -0
- package/dist/data-table/cells/bar-cell.js.map +1 -0
- package/dist/data-table/cells/category-legend.js +9 -0
- package/dist/data-table/cells/category-legend.js.map +1 -0
- package/dist/data-table/cells/columns-cell.js +13 -0
- package/dist/data-table/cells/columns-cell.js.map +1 -0
- package/dist/data-table/cells/heatmap-cell.js +13 -0
- package/dist/data-table/cells/heatmap-cell.js.map +1 -0
- package/dist/data-table/cells/markdown-cell.js +13 -0
- package/dist/data-table/cells/markdown-cell.js.map +1 -0
- package/dist/data-table/cells/sparkline-cell.js +13 -0
- package/dist/data-table/cells/sparkline-cell.js.map +1 -0
- package/dist/data-table/column-meta.js +12 -0
- package/dist/data-table/column-meta.js.map +1 -0
- package/dist/data-table/data-table.js +23 -0
- package/dist/data-table/data-table.js.map +1 -0
- package/dist/data-table/index.js +23 -0
- package/dist/data-table/index.js.map +1 -0
- package/dist/data-table/ranks-column.js +13 -0
- package/dist/data-table/ranks-column.js.map +1 -0
- package/dist/data-table/sticky-rows.js +10 -0
- package/dist/data-table/sticky-rows.js.map +1 -0
- package/dist/data-table/use-table-breakpoint.js +13 -0
- package/dist/data-table/use-table-breakpoint.js.map +1 -0
- package/dist/facet-filter/facet-filter.js +8 -0
- package/dist/facet-filter/facet-filter.js.map +1 -0
- package/dist/facet-filter/index.js +9 -0
- package/dist/facet-filter/index.js.map +1 -0
- package/dist/filter-bar/filter-bar.js +8 -0
- package/dist/filter-bar/filter-bar.js.map +1 -0
- package/dist/filter-bar/filter-chip.js +9 -0
- package/dist/filter-bar/filter-chip.js.map +1 -0
- package/dist/filter-bar/index.js +13 -0
- package/dist/filter-bar/index.js.map +1 -0
- package/dist/index.d.ts +547 -8
- package/dist/index.js +102 -1512
- package/dist/index.js.map +1 -1
- package/dist/search-input/index.js +9 -0
- package/dist/search-input/index.js.map +1 -0
- package/dist/search-input/search-input.js +9 -0
- package/dist/search-input/search-input.js.map +1 -0
- package/dist/to-csv.js +10 -0
- package/dist/to-csv.js.map +1 -0
- package/package.json +7 -7
- package/src/data-table/card-layout.test.tsx +26 -0
- package/src/data-table/card-layout.tsx +74 -0
- package/src/data-table/cell-scales.test.ts +118 -0
- package/src/data-table/cell-scales.ts +246 -0
- package/src/data-table/cells/bar-cell.test.tsx +158 -0
- package/src/data-table/cells/bar-cell.tsx +135 -0
- package/src/data-table/cells/category-legend.test.tsx +37 -0
- package/src/data-table/cells/category-legend.tsx +58 -0
- package/src/data-table/cells/columns-cell.test.tsx +27 -0
- package/src/data-table/cells/columns-cell.tsx +79 -0
- package/src/data-table/cells/heatmap-cell.test.tsx +31 -0
- package/src/data-table/cells/heatmap-cell.tsx +116 -0
- package/src/data-table/cells/markdown-cell.test.tsx +36 -0
- package/src/data-table/cells/markdown-cell.tsx +91 -0
- package/src/data-table/cells/sparkline-cell.test.tsx +43 -0
- package/src/data-table/cells/sparkline-cell.tsx +113 -0
- package/src/data-table/column-meta.test.ts +42 -0
- package/src/data-table/column-meta.ts +282 -0
- package/src/data-table/data-table.stories.tsx +733 -1
- package/src/data-table/data-table.test.tsx +486 -0
- package/src/data-table/data-table.tsx +965 -165
- package/src/data-table/ranks-column.test.tsx +65 -0
- package/src/data-table/ranks-column.tsx +91 -0
- package/src/data-table/sticky-rows.test.ts +16 -0
- package/src/data-table/sticky-rows.ts +77 -0
- package/src/data-table/use-table-breakpoint.test.ts +14 -0
- package/src/data-table/use-table-breakpoint.ts +67 -0
- package/src/filter-bar/filter-chip.stories.tsx +1 -0
- package/src/index.ts +64 -0
- package/src/templates-data-app.stories.tsx +7 -2
|
@@ -537,6 +537,39 @@ describe("DataTable — virtualized a11y + composability", () => {
|
|
|
537
537
|
expect(indexedRows.length).toBeLessThan(bigData.length);
|
|
538
538
|
});
|
|
539
539
|
|
|
540
|
+
it("places sticky rows in the virtualized aria-rowindex sequence and counts them", () => {
|
|
541
|
+
// Sticky rows mount OUTSIDE the virtual window. Without an index of their
|
|
542
|
+
// own AT hears unplaced extra rows, and aria-rowcount (built from the
|
|
543
|
+
// centre row model) under-reports the table by exactly those rows.
|
|
544
|
+
const { container } = render(
|
|
545
|
+
<DataTable
|
|
546
|
+
columns={columns}
|
|
547
|
+
data={bigData}
|
|
548
|
+
enableRowVirtualization
|
|
549
|
+
stickyRows={(row) => {
|
|
550
|
+
if (row.name === "Row 0") return "top";
|
|
551
|
+
if (row.name === "Row 1") return "bottom";
|
|
552
|
+
return undefined;
|
|
553
|
+
}}
|
|
554
|
+
/>,
|
|
555
|
+
);
|
|
556
|
+
// 98 centre + 1 top + 1 bottom + 1 header row.
|
|
557
|
+
expect(container.querySelector("table")).toHaveAttribute(
|
|
558
|
+
"aria-rowcount",
|
|
559
|
+
String(bigData.length + 1),
|
|
560
|
+
);
|
|
561
|
+
const mounted = [...container.querySelectorAll("tbody tr")].filter(
|
|
562
|
+
(tr) => tr.getAttribute("aria-hidden") !== "true",
|
|
563
|
+
);
|
|
564
|
+
// Every mounted, non-spacer row is placed — no row without an index.
|
|
565
|
+
expect(mounted.every((tr) => tr.hasAttribute("aria-rowindex"))).toBe(true);
|
|
566
|
+
const indexOf = (name: string) =>
|
|
567
|
+
mounted.find((tr) => tr.textContent?.includes(name))?.getAttribute("aria-rowindex");
|
|
568
|
+
// Header holds 1; the top-pinned row takes 2, the bottom-pinned row last.
|
|
569
|
+
expect(indexOf("Row 0")).toBe("2");
|
|
570
|
+
expect(indexOf("Row 1")).toBe(String(bigData.length + 1));
|
|
571
|
+
});
|
|
572
|
+
|
|
540
573
|
it("makes the virtualized scroll region keyboard-focusable with a visible focus ring", () => {
|
|
541
574
|
const { container } = render(
|
|
542
575
|
<DataTable columns={columns} data={bigData} enableRowVirtualization />,
|
|
@@ -2925,3 +2958,456 @@ describe("DataTable — #13 row drag-reorder", () => {
|
|
|
2925
2958
|
expect(describedByText).toMatch(/To pick up|space bar/i);
|
|
2926
2959
|
});
|
|
2927
2960
|
});
|
|
2961
|
+
|
|
2962
|
+
// ─── Presentation layer: visuals, format, showAt, cards, sticky rows, ranks ──
|
|
2963
|
+
|
|
2964
|
+
/** Every element measures `width` wide, so the table breakpoint resolves from it. */
|
|
2965
|
+
function mockTableWidth(width: number) {
|
|
2966
|
+
return vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
|
2967
|
+
() =>
|
|
2968
|
+
({
|
|
2969
|
+
width,
|
|
2970
|
+
height: 40,
|
|
2971
|
+
top: 0,
|
|
2972
|
+
left: 0,
|
|
2973
|
+
right: width,
|
|
2974
|
+
bottom: 40,
|
|
2975
|
+
x: 0,
|
|
2976
|
+
y: 0,
|
|
2977
|
+
toJSON: () => ({}),
|
|
2978
|
+
}) as DOMRect,
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
interface CityRow {
|
|
2983
|
+
city: string;
|
|
2984
|
+
rides: number;
|
|
2985
|
+
change: number;
|
|
2986
|
+
q1: number;
|
|
2987
|
+
q2: number;
|
|
2988
|
+
q3: number;
|
|
2989
|
+
region: string;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
const cities: CityRow[] = [
|
|
2993
|
+
{ city: "Oslo", rides: 20, change: -5, q1: 1, q2: 4, q3: 2, region: "North" },
|
|
2994
|
+
{ city: "Lyon", rides: 40, change: 20, q1: 8, q2: 3, q3: 5, region: "South" },
|
|
2995
|
+
{ city: "Graz", rides: 9, change: 5, q1: 2, q2: 2, q3: 8, region: "South" },
|
|
2996
|
+
{ city: "Average", rides: 23, change: 6.7, q1: 3.7, q2: 3, q3: 5, region: "All" },
|
|
2997
|
+
];
|
|
2998
|
+
|
|
2999
|
+
describe("DataTable — presentation layer", () => {
|
|
3000
|
+
it("no presentation prop: the default DOM is unchanged (table, no rank, no layout attr)", () => {
|
|
3001
|
+
const { container } = render(<DataTable columns={columns} data={data} />);
|
|
3002
|
+
expect(container.querySelector("table")).not.toBeNull();
|
|
3003
|
+
expect(container.querySelector("dl")).toBeNull();
|
|
3004
|
+
expect(container.querySelector("[data-layout]")).toBeNull();
|
|
3005
|
+
expect(container.querySelector('[data-slot="data-table-rank-cell"]')).toBeNull();
|
|
3006
|
+
});
|
|
3007
|
+
|
|
3008
|
+
it('layout="auto": <dl> cards under 450 px, a <table> above', () => {
|
|
3009
|
+
const narrow = mockTableWidth(380);
|
|
3010
|
+
const { container, unmount } = render(
|
|
3011
|
+
<DataTable columns={columns} data={data} layout="auto" aria-label="Rows" />,
|
|
3012
|
+
);
|
|
3013
|
+
expect(container.querySelector('[data-layout="cards"]')).not.toBeNull();
|
|
3014
|
+
expect(container.querySelector("table")).toBeNull();
|
|
3015
|
+
expect(container.querySelectorAll('[data-slot="data-table-card"] dl')).toHaveLength(3);
|
|
3016
|
+
expect(screen.getAllByRole("term").map((t) => t.textContent)).toContain("Name");
|
|
3017
|
+
unmount();
|
|
3018
|
+
narrow.mockRestore();
|
|
3019
|
+
const wide = mockTableWidth(900);
|
|
3020
|
+
const again = render(<DataTable columns={columns} data={data} layout="auto" />);
|
|
3021
|
+
expect(again.container.querySelector('[data-layout="table"]')).not.toBeNull();
|
|
3022
|
+
expect(again.container.querySelector("table")).not.toBeNull();
|
|
3023
|
+
expect(again.container.querySelector("dl")).toBeNull();
|
|
3024
|
+
wide.mockRestore();
|
|
3025
|
+
});
|
|
3026
|
+
|
|
3027
|
+
it("cards keep sorting: the sort bar reorders the cards", () => {
|
|
3028
|
+
const { container } = render(<DataTable columns={columns} data={data} layout="cards" />);
|
|
3029
|
+
const firstName = () =>
|
|
3030
|
+
container.querySelector('[data-slot="data-table-card"] dd')?.textContent ?? "";
|
|
3031
|
+
expect(firstName()).toBe("Alpha");
|
|
3032
|
+
// TanStack sorts a numeric column descending first.
|
|
3033
|
+
fireEvent.click(screen.getByRole("button", { name: "Sort by Value, not sorted" }));
|
|
3034
|
+
expect(firstName()).toBe("Alpha");
|
|
3035
|
+
fireEvent.click(screen.getByRole("button", { name: "Sort by Value, descending" }));
|
|
3036
|
+
expect(firstName()).toBe("Beta");
|
|
3037
|
+
});
|
|
3038
|
+
|
|
3039
|
+
it("showAt { base: true, narrow: false } hides the column under 450 px only", () => {
|
|
3040
|
+
const cols: ColumnDef<Row>[] = [
|
|
3041
|
+
columns[0]!,
|
|
3042
|
+
{ ...columns[1]!, meta: { showAt: { base: true, narrow: false } } },
|
|
3043
|
+
];
|
|
3044
|
+
const narrow = mockTableWidth(380);
|
|
3045
|
+
const { unmount } = render(<DataTable columns={cols} data={data} />);
|
|
3046
|
+
expect(screen.queryByRole("columnheader", { name: /Value/ })).toBeNull();
|
|
3047
|
+
expect(screen.getAllByRole("columnheader")).toHaveLength(1);
|
|
3048
|
+
unmount();
|
|
3049
|
+
narrow.mockRestore();
|
|
3050
|
+
const wide = mockTableWidth(900);
|
|
3051
|
+
render(<DataTable columns={cols} data={data} />);
|
|
3052
|
+
expect(screen.getByRole("columnheader", { name: /Value/ })).toBeInTheDocument();
|
|
3053
|
+
wide.mockRestore();
|
|
3054
|
+
});
|
|
3055
|
+
|
|
3056
|
+
it("warns once, instead of failing silently, when row reorder meets the card layout", () => {
|
|
3057
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3058
|
+
{ accessorKey: "city", header: "City" },
|
|
3059
|
+
{ accessorKey: "rides", header: "Rides" },
|
|
3060
|
+
];
|
|
3061
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
3062
|
+
const onRowReorder = vi.fn();
|
|
3063
|
+
const { container, rerender } = render(
|
|
3064
|
+
<DataTable
|
|
3065
|
+
columns={cols}
|
|
3066
|
+
data={cities}
|
|
3067
|
+
enableRowReorder
|
|
3068
|
+
layout="cards"
|
|
3069
|
+
onRowReorder={onRowReorder}
|
|
3070
|
+
/>,
|
|
3071
|
+
);
|
|
3072
|
+
// No drag affordance is mounted in cards — no grip button, no drop target.
|
|
3073
|
+
expect(screen.queryByRole("button", { name: /reorder|drag|move/i })).not.toBeInTheDocument();
|
|
3074
|
+
expect(container.querySelector("[data-slot='data-table-card-region']")).not.toBeNull();
|
|
3075
|
+
expect(onRowReorder).not.toHaveBeenCalled();
|
|
3076
|
+
const reorderWarnings = () =>
|
|
3077
|
+
warn.mock.calls.filter((c) => String(c[0]).includes("`enableRowReorder` is ignored"));
|
|
3078
|
+
expect(reorderWarnings()).toHaveLength(1);
|
|
3079
|
+
// Once per mount, not once per render.
|
|
3080
|
+
rerender(
|
|
3081
|
+
<DataTable
|
|
3082
|
+
columns={cols}
|
|
3083
|
+
data={cities}
|
|
3084
|
+
enableRowReorder
|
|
3085
|
+
layout="cards"
|
|
3086
|
+
onRowReorder={onRowReorder}
|
|
3087
|
+
/>,
|
|
3088
|
+
);
|
|
3089
|
+
expect(reorderWarnings()).toHaveLength(1);
|
|
3090
|
+
warn.mockRestore();
|
|
3091
|
+
});
|
|
3092
|
+
|
|
3093
|
+
it("stickyRows keeps the average row on every page and after sorting", () => {
|
|
3094
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3095
|
+
{ accessorKey: "city", header: "City" },
|
|
3096
|
+
{ accessorKey: "rides", header: "Rides", enableSorting: true },
|
|
3097
|
+
];
|
|
3098
|
+
const { container } = render(
|
|
3099
|
+
<DataTable
|
|
3100
|
+
columns={cols}
|
|
3101
|
+
data={cities}
|
|
3102
|
+
enablePagination
|
|
3103
|
+
pageSize={2}
|
|
3104
|
+
stickyRows={(row) => (row.city === "Average" ? "bottom" : undefined)}
|
|
3105
|
+
/>,
|
|
3106
|
+
);
|
|
3107
|
+
const names = () =>
|
|
3108
|
+
[...container.querySelectorAll("tbody tr")].map(
|
|
3109
|
+
(tr) => tr.querySelector("td")?.textContent ?? "",
|
|
3110
|
+
);
|
|
3111
|
+
expect(names()).toEqual(["Oslo", "Lyon", "Average"]);
|
|
3112
|
+
fireEvent.click(screen.getByRole("button", { name: /Next/i }));
|
|
3113
|
+
expect(names()).toEqual(["Graz", "Average"]);
|
|
3114
|
+
fireEvent.click(screen.getByRole("button", { name: /Previous/i }));
|
|
3115
|
+
fireEvent.click(screen.getByRole("button", { name: "Sort by Rides, not sorted" }));
|
|
3116
|
+
// Descending (numeric columns sort descending first): Lyon 40, Oslo 20 on
|
|
3117
|
+
// page one — the average (23) is not sorted into them, it stays last.
|
|
3118
|
+
expect(names()).toEqual(["Lyon", "Oslo", "Average"]);
|
|
3119
|
+
fireEvent.click(screen.getByRole("button", { name: /Next/i }));
|
|
3120
|
+
expect(names()).toEqual(["Graz", "Average"]);
|
|
3121
|
+
expect(container.querySelector('tr[data-sticky="bottom"]')).toHaveTextContent("Average");
|
|
3122
|
+
});
|
|
3123
|
+
|
|
3124
|
+
it("showRanks renders 1…n in data order, unaffected by sort (sticky rows unranked)", () => {
|
|
3125
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3126
|
+
{ accessorKey: "city", header: "City" },
|
|
3127
|
+
{ accessorKey: "rides", header: "Rides", enableSorting: true },
|
|
3128
|
+
];
|
|
3129
|
+
const { container } = render(
|
|
3130
|
+
<DataTable
|
|
3131
|
+
columns={cols}
|
|
3132
|
+
data={cities}
|
|
3133
|
+
showRanks
|
|
3134
|
+
stickyRows={(row) => (row.city === "Average" ? "bottom" : undefined)}
|
|
3135
|
+
/>,
|
|
3136
|
+
);
|
|
3137
|
+
const ranked = () =>
|
|
3138
|
+
[...container.querySelectorAll("tbody tr")].map((tr) => [
|
|
3139
|
+
tr.querySelector('[data-slot="data-table-rank-cell"]')?.textContent ?? "",
|
|
3140
|
+
tr.querySelectorAll("td")[1]?.textContent ?? "",
|
|
3141
|
+
]);
|
|
3142
|
+
expect(ranked()).toEqual([
|
|
3143
|
+
["1", "Oslo"],
|
|
3144
|
+
["2", "Lyon"],
|
|
3145
|
+
["3", "Graz"],
|
|
3146
|
+
["", "Average"],
|
|
3147
|
+
]);
|
|
3148
|
+
fireEvent.click(screen.getByRole("button", { name: "Sort by Rides, not sorted" }));
|
|
3149
|
+
expect(ranked()).toEqual([
|
|
3150
|
+
["2", "Lyon"],
|
|
3151
|
+
["1", "Oslo"],
|
|
3152
|
+
["3", "Graz"],
|
|
3153
|
+
["", "Average"],
|
|
3154
|
+
]);
|
|
3155
|
+
expect(screen.getAllByRole("columnheader")[0]).toHaveTextContent("#");
|
|
3156
|
+
});
|
|
3157
|
+
|
|
3158
|
+
/**
|
|
3159
|
+
* b-5 — the same table, read: the ranks are the DATA order, so after the
|
|
3160
|
+
* sort above they read 2, 1, 3 beside a descending column. That is only
|
|
3161
|
+
* honest if the table says which position it is printing, in BOTH channels
|
|
3162
|
+
* — the header's accessible name for AT, and a printed key for everyone
|
|
3163
|
+
* else. A bare "#" said it in neither.
|
|
3164
|
+
*/
|
|
3165
|
+
it("showRanks says which position it prints — named for AT, printed as a key", () => {
|
|
3166
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3167
|
+
{ accessorKey: "city", header: "City" },
|
|
3168
|
+
{ accessorKey: "rides", header: "Rides", enableSorting: true },
|
|
3169
|
+
];
|
|
3170
|
+
const { container } = render(<DataTable columns={cols} data={cities} showRanks />);
|
|
3171
|
+
expect(screen.getAllByRole("columnheader")[0]).toHaveAccessibleName(
|
|
3172
|
+
"Position in the data as supplied",
|
|
3173
|
+
);
|
|
3174
|
+
const key = container.querySelector('[data-slot="data-table-rank-key"]');
|
|
3175
|
+
expect(key).toHaveTextContent(
|
|
3176
|
+
"# is each row’s position in the data as supplied, not its position in this view — sorting never renumbers it.",
|
|
3177
|
+
);
|
|
3178
|
+
});
|
|
3179
|
+
|
|
3180
|
+
it("showRanks: a caller's own rankLabel names the column and the key", () => {
|
|
3181
|
+
const cols: ColumnDef<CityRow>[] = [{ accessorKey: "city", header: "City" }];
|
|
3182
|
+
const { container } = render(
|
|
3183
|
+
<DataTable columns={cols} data={cities} showRanks rankLabel="Rank in the 2024 census" />,
|
|
3184
|
+
);
|
|
3185
|
+
expect(screen.getAllByRole("columnheader")[0]).toHaveAccessibleName("Rank in the 2024 census");
|
|
3186
|
+
expect(container.querySelector('[data-slot="data-table-rank-key"]')).toHaveTextContent(
|
|
3187
|
+
"# — Rank in the 2024 census",
|
|
3188
|
+
);
|
|
3189
|
+
});
|
|
3190
|
+
|
|
3191
|
+
it("no ranks, no key: the rank key never renders for a table without showRanks", () => {
|
|
3192
|
+
const cols: ColumnDef<CityRow>[] = [{ accessorKey: "city", header: "City" }];
|
|
3193
|
+
const { container } = render(<DataTable columns={cols} data={cities} />);
|
|
3194
|
+
expect(container.querySelector('[data-slot="data-table-rank-key"]')).toBeNull();
|
|
3195
|
+
expect(container.querySelector('[data-slot="data-table-legends"]')).toBeNull();
|
|
3196
|
+
});
|
|
3197
|
+
|
|
3198
|
+
it('searchMode="exact" matches whole, case-insensitive values only', () => {
|
|
3199
|
+
const { container, rerender } = render(
|
|
3200
|
+
<DataTable columns={columns} data={data} initialView={{ globalFilter: "et" }} />,
|
|
3201
|
+
);
|
|
3202
|
+
expect(container.querySelectorAll("tbody tr")).toHaveLength(1);
|
|
3203
|
+
rerender(<DataTable columns={columns} data={data} globalFilter="beta" searchMode="exact" />);
|
|
3204
|
+
expect(container.querySelectorAll("tbody tr")).toHaveLength(1);
|
|
3205
|
+
expect(container.querySelector("tbody tr")).toHaveTextContent("Beta");
|
|
3206
|
+
rerender(<DataTable columns={columns} data={data} globalFilter="bet" searchMode="exact" />);
|
|
3207
|
+
expect(screen.queryByText("Beta")).toBeNull();
|
|
3208
|
+
});
|
|
3209
|
+
|
|
3210
|
+
it("mergeEmptyHeaders spans an ungrouped column's header down the header rows", () => {
|
|
3211
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3212
|
+
{ accessorKey: "city", header: "City" },
|
|
3213
|
+
{
|
|
3214
|
+
id: "quarters",
|
|
3215
|
+
header: "Quarters",
|
|
3216
|
+
columns: [
|
|
3217
|
+
{ accessorKey: "q1", header: "Q1" },
|
|
3218
|
+
{ accessorKey: "q2", header: "Q2" },
|
|
3219
|
+
],
|
|
3220
|
+
},
|
|
3221
|
+
];
|
|
3222
|
+
const { container, rerender } = render(<DataTable columns={cols} data={cities} />);
|
|
3223
|
+
expect(container.querySelectorAll("thead tr")[0]?.querySelectorAll("th")).toHaveLength(2);
|
|
3224
|
+
rerender(<DataTable columns={cols} data={cities} mergeEmptyHeaders />);
|
|
3225
|
+
const [top, bottom] = [...container.querySelectorAll("thead tr")];
|
|
3226
|
+
const city = [...(top?.querySelectorAll("th") ?? [])].find((th) => th.textContent === "City");
|
|
3227
|
+
expect(city).toHaveAttribute("rowspan", "2");
|
|
3228
|
+
expect(top?.querySelector("th[colspan='2']")).toHaveTextContent("Quarters");
|
|
3229
|
+
expect([...(bottom?.querySelectorAll("th") ?? [])].map((th) => th.textContent)).toEqual([
|
|
3230
|
+
"Q1",
|
|
3231
|
+
"Q2",
|
|
3232
|
+
]);
|
|
3233
|
+
});
|
|
3234
|
+
|
|
3235
|
+
it("heatmap: ramp-token backgrounds, values kept for AT, numeric sort", () => {
|
|
3236
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3237
|
+
{ accessorKey: "city", header: "City" },
|
|
3238
|
+
{
|
|
3239
|
+
accessorKey: "rides",
|
|
3240
|
+
header: "Rides",
|
|
3241
|
+
enableSorting: true,
|
|
3242
|
+
meta: {
|
|
3243
|
+
numeric: true,
|
|
3244
|
+
visual: { kind: "heatmap", scale: { type: "stepped", steps: 3 }, hideValue: true },
|
|
3245
|
+
},
|
|
3246
|
+
},
|
|
3247
|
+
];
|
|
3248
|
+
const { container } = render(<DataTable columns={cols} data={cities.slice(0, 3)} />);
|
|
3249
|
+
const tds = [...container.querySelectorAll("tbody td:nth-child(2)")] as HTMLElement[];
|
|
3250
|
+
for (const td of tds) {
|
|
3251
|
+
expect(td.style.backgroundColor).toMatch(/^var\(--chart-seq-\d\)$/);
|
|
3252
|
+
expect(td.querySelector('[data-slot="heatmap-cell"]')).toHaveClass("sr-only");
|
|
3253
|
+
}
|
|
3254
|
+
expect(tds.map((td) => td.textContent)).toEqual(["20", "40", "9"]);
|
|
3255
|
+
const order = () =>
|
|
3256
|
+
[...container.querySelectorAll("tbody td:nth-child(2)")].map((td) => td.textContent);
|
|
3257
|
+
// Numeric, not string, order both ways ("9" would sort after "40" as text).
|
|
3258
|
+
fireEvent.click(screen.getByRole("button", { name: "Sort by Rides, not sorted" }));
|
|
3259
|
+
expect(order()).toEqual(["40", "20", "9"]);
|
|
3260
|
+
fireEvent.click(screen.getByRole("button", { name: "Sort by Rides, descending" }));
|
|
3261
|
+
expect(order()).toEqual(["9", "20", "40"]);
|
|
3262
|
+
});
|
|
3263
|
+
|
|
3264
|
+
it('bar: range "column" is proportional to the column max; negatives paint left of zero', () => {
|
|
3265
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3266
|
+
{ accessorKey: "city", header: "City" },
|
|
3267
|
+
{ accessorKey: "rides", header: "Rides", meta: { visual: { kind: "bar", track: true } } },
|
|
3268
|
+
{ accessorKey: "change", header: "Change", meta: { visual: { kind: "bar" } } },
|
|
3269
|
+
];
|
|
3270
|
+
const { container } = render(<DataTable columns={cols} data={cities.slice(0, 3)} />);
|
|
3271
|
+
const bars = (col: number) =>
|
|
3272
|
+
[...container.querySelectorAll(`tbody td:nth-child(${col}) [data-slot="bar-cell-bar"]`)].map(
|
|
3273
|
+
(el) => (el as HTMLElement).style,
|
|
3274
|
+
);
|
|
3275
|
+
expect(bars(2).map((s) => s.width)).toEqual(["50%", "100%", "22.5%"]);
|
|
3276
|
+
expect(container.querySelector('[data-slot="bar-cell-track"]')).toHaveClass("bg-muted");
|
|
3277
|
+
const [oslo] = bars(3);
|
|
3278
|
+
expect(oslo?.backgroundColor).toBe("var(--chart-div-neg-2)");
|
|
3279
|
+
expect(oslo?.insetInlineStart).toBe("0%");
|
|
3280
|
+
// The printed value stays in the cell for AT.
|
|
3281
|
+
expect(container.querySelector("tbody td:nth-child(3)")).toHaveTextContent("-5");
|
|
3282
|
+
});
|
|
3283
|
+
|
|
3284
|
+
it("bar: one value box per column, so bar LENGTHS compare down the column", () => {
|
|
3285
|
+
// Without a column-wide reservation the track is whatever each row's number
|
|
3286
|
+
// leaves over, so "9" would draw a longer bar than its share of the column.
|
|
3287
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3288
|
+
{ accessorKey: "city", header: "City" },
|
|
3289
|
+
{ accessorKey: "rides", header: "Rides", meta: { visual: { kind: "bar", track: true } } },
|
|
3290
|
+
{
|
|
3291
|
+
accessorKey: "change",
|
|
3292
|
+
header: "Change",
|
|
3293
|
+
meta: { format: { sign: "always", suffix: " %" }, visual: { kind: "bar" } },
|
|
3294
|
+
},
|
|
3295
|
+
];
|
|
3296
|
+
const { container } = render(<DataTable columns={cols} data={cities.slice(0, 3)} />);
|
|
3297
|
+
const boxes = (col: number) =>
|
|
3298
|
+
[
|
|
3299
|
+
...container.querySelectorAll(`tbody td:nth-child(${col}) [data-slot="bar-cell-value"]`),
|
|
3300
|
+
].map((el) => (el as HTMLElement).style.width);
|
|
3301
|
+
// "20" / "40" / "9" → the column's longest label is 2 characters.
|
|
3302
|
+
expect(boxes(2)).toEqual(["2ch", "2ch", "2ch"]);
|
|
3303
|
+
// "-5 %" / "+20 %" / "+5 %" → 5, on every row including the short ones.
|
|
3304
|
+
expect(boxes(3)).toEqual(["5ch", "5ch", "5ch"]);
|
|
3305
|
+
});
|
|
3306
|
+
|
|
3307
|
+
it('sparkline + columns with range "column" share one y scale across rows', () => {
|
|
3308
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3309
|
+
{ accessorKey: "city", header: "City" },
|
|
3310
|
+
{
|
|
3311
|
+
id: "trend",
|
|
3312
|
+
header: "Trend",
|
|
3313
|
+
meta: { visual: { kind: "sparkline", keys: ["q1", "q2", "q3"], range: "column" } },
|
|
3314
|
+
},
|
|
3315
|
+
{
|
|
3316
|
+
id: "bars",
|
|
3317
|
+
header: "Quarters",
|
|
3318
|
+
meta: { visual: { kind: "columns", keys: ["q1", "q2", "q3"], range: "column" } },
|
|
3319
|
+
},
|
|
3320
|
+
];
|
|
3321
|
+
const { container } = render(<DataTable columns={cols} data={cities.slice(0, 3)} />);
|
|
3322
|
+
const maxes = [...container.querySelectorAll('[data-slot="sparkline-cell-svg"]')].map((svg) =>
|
|
3323
|
+
svg.getAttribute("data-y-max"),
|
|
3324
|
+
);
|
|
3325
|
+
expect(new Set(maxes)).toEqual(new Set(["8"]));
|
|
3326
|
+
const colMaxes = [...container.querySelectorAll('[data-slot="columns-cell-svg"]')].map((svg) =>
|
|
3327
|
+
svg.getAttribute("data-y-max"),
|
|
3328
|
+
);
|
|
3329
|
+
expect(new Set(colMaxes)).toEqual(new Set(["8"]));
|
|
3330
|
+
// Each cell's numbers are in the accessible tree.
|
|
3331
|
+
expect(container.querySelector("tbody tr td:nth-child(2)")).toHaveTextContent(/1.*4.*2/);
|
|
3332
|
+
});
|
|
3333
|
+
|
|
3334
|
+
/**
|
|
3335
|
+
* b-6 — a bar coloured by `region` in a table that prints no region column
|
|
3336
|
+
* carried that category in hue alone: nothing named the groups, and two of
|
|
3337
|
+
* the three categorical tokens are 5/255 apart in greyscale. The key names
|
|
3338
|
+
* them (WCAG 1.4.1), once per source key however many columns use it.
|
|
3339
|
+
*/
|
|
3340
|
+
it("a colorBy column draws a named category key, once per source key", () => {
|
|
3341
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3342
|
+
{
|
|
3343
|
+
accessorKey: "rides",
|
|
3344
|
+
header: "Rides",
|
|
3345
|
+
meta: { visual: { kind: "bar", colorBy: "region", legend: "Region" } },
|
|
3346
|
+
},
|
|
3347
|
+
{
|
|
3348
|
+
accessorKey: "change",
|
|
3349
|
+
header: "Change",
|
|
3350
|
+
meta: { visual: { kind: "bar", colorBy: "region" } },
|
|
3351
|
+
},
|
|
3352
|
+
];
|
|
3353
|
+
render(<DataTable columns={cols} data={cities} />);
|
|
3354
|
+
const key = screen.getByRole("group", { name: "Region" });
|
|
3355
|
+
expect([...key.querySelectorAll("li")].map((li) => li.textContent)).toEqual([
|
|
3356
|
+
"North",
|
|
3357
|
+
"South",
|
|
3358
|
+
"All",
|
|
3359
|
+
]);
|
|
3360
|
+
expect(screen.getAllByRole("group", { name: "Region" })).toHaveLength(1);
|
|
3361
|
+
});
|
|
3362
|
+
|
|
3363
|
+
it("legend: false opts a colorBy column out of the key", () => {
|
|
3364
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3365
|
+
{
|
|
3366
|
+
accessorKey: "rides",
|
|
3367
|
+
header: "Rides",
|
|
3368
|
+
meta: { visual: { kind: "bar", colorBy: "region", legend: false } },
|
|
3369
|
+
},
|
|
3370
|
+
];
|
|
3371
|
+
const { container } = render(<DataTable columns={cols} data={cities} />);
|
|
3372
|
+
expect(container.querySelector('[data-slot="category-legend"]')).toBeNull();
|
|
3373
|
+
});
|
|
3374
|
+
|
|
3375
|
+
it("meta.format formats the default cell; colorBy washes by category", () => {
|
|
3376
|
+
const cols: ColumnDef<CityRow>[] = [
|
|
3377
|
+
{ accessorKey: "city", header: "City", meta: { colorBy: { key: "region", target: "text" } } },
|
|
3378
|
+
{
|
|
3379
|
+
accessorKey: "change",
|
|
3380
|
+
header: "Change",
|
|
3381
|
+
meta: { format: { sign: "always", suffix: " %" } },
|
|
3382
|
+
},
|
|
3383
|
+
];
|
|
3384
|
+
const { container } = render(<DataTable columns={cols} data={cities.slice(0, 2)} />);
|
|
3385
|
+
expect(container.querySelector("tbody td:nth-child(2)")).toHaveTextContent("-5 %");
|
|
3386
|
+
expect(container.querySelectorAll("tbody td:nth-child(2)")[1]).toHaveTextContent("+20 %");
|
|
3387
|
+
const [oslo, lyon] = [...container.querySelectorAll("tbody td:nth-child(1)")] as HTMLElement[];
|
|
3388
|
+
expect(oslo?.style.color).toMatch(/color-mix/);
|
|
3389
|
+
expect(oslo?.style.color).not.toBe(lyon?.style.color);
|
|
3390
|
+
});
|
|
3391
|
+
|
|
3392
|
+
it("hideHeader keeps the header row for AT but hides it visually", () => {
|
|
3393
|
+
render(<DataTable columns={columns} data={data} hideHeader />);
|
|
3394
|
+
expect(screen.getAllByRole("columnheader")).toHaveLength(2);
|
|
3395
|
+
expect(
|
|
3396
|
+
screen.getByRole("button", { name: "Sort by Name, not sorted" }).closest("span"),
|
|
3397
|
+
).toHaveClass("sr-only");
|
|
3398
|
+
});
|
|
3399
|
+
|
|
3400
|
+
// RM-127 (a-5): WCAG 2.2 target size (2.5.8). The sort button is 16–20 px of
|
|
3401
|
+
// text and used to clear the rule only through the space AROUND it — which
|
|
3402
|
+
// `hideHeader` (`h-0 p-0`, 16 px between neighbours) and the card sort bar
|
|
3403
|
+
// (`gap-y-1`, 4 px) both take away. Measured in Chromium at 380 px: the
|
|
3404
|
+
// 11 card-bar buttons were 52.4 x 16 and axe reported 6 serious
|
|
3405
|
+
// `target-size` nodes; they are 24 px tall now and axe passes on all 11.
|
|
3406
|
+
it("sizes every sort button to the 24 px target floor, hidden header or card bar", () => {
|
|
3407
|
+
const { unmount } = render(<DataTable columns={columns} data={data} />);
|
|
3408
|
+
expect(screen.getByRole("button", { name: "Sort by Name, not sorted" })).toHaveClass("min-h-6");
|
|
3409
|
+
unmount();
|
|
3410
|
+
render(<DataTable columns={columns} data={data} hideHeader />);
|
|
3411
|
+
expect(screen.getByRole("button", { name: "Sort by Name, not sorted" })).toHaveClass("min-h-6");
|
|
3412
|
+
});
|
|
3413
|
+
});
|