@marimo-team/islands 0.23.10-dev29 → 0.23.10-dev30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{code-visibility-C_uXSW2l.js → code-visibility-wR7WSQ4c.js} +1227 -986
- package/dist/main.js +1103 -1165
- package/dist/{reveal-component-BdSVsJMP.js → reveal-component-BjnkUAZ9.js} +23 -23
- package/package.json +1 -1
- package/src/components/data-table/TableBottomBar.tsx +1 -15
- package/src/components/data-table/TableTopBar.tsx +8 -13
- package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
- package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
- package/src/components/data-table/data-table.tsx +1 -1
|
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
|
9
9
|
import { lt as kioskModeAtom } from "./html-to-image-UEH5lFDZ.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-BNjes6Yx.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist-Dk6PV_d3.js";
|
|
12
|
-
import {
|
|
12
|
+
import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelResizeHandle, i as DEFAULT_DECK_TRANSITION, in as Expand, ot as Panel, rn as EyeOff, s as SlideSidebar, sn as Code, st as PanelGroup, t as useNotebookCodeAvailable } from "./code-visibility-wR7WSQ4c.js";
|
|
13
13
|
import { J as useDebouncedCallback } from "./input-CMYy4hzj.js";
|
|
14
14
|
import "./toDate-d8RCRrRd.js";
|
|
15
15
|
import "./react-dom-BTJzcVJ9.js";
|
|
@@ -7245,38 +7245,38 @@ function _temp(e5) {
|
|
|
7245
7245
|
};
|
|
7246
7246
|
}
|
|
7247
7247
|
var SubslideView = (e5) => {
|
|
7248
|
-
let t = (0, import_compiler_runtime.c)(24), { subslide: n, resolveShowCode: r, isEditable: i, slideConfigs: a } = e5, o, s, c, l, u,
|
|
7248
|
+
let t = (0, import_compiler_runtime.c)(24), { subslide: n, resolveShowCode: r, isEditable: i, slideConfigs: a } = e5, o, s, c, l, u, ro;
|
|
7249
7249
|
if (t[0] !== i || t[1] !== r || t[2] !== a || t[3] !== n) {
|
|
7250
|
-
let { slideLevel: e6, cumulativeByBlock:
|
|
7250
|
+
let { slideLevel: e6, cumulativeByBlock: io2 } = buildSubslideNotes(n, a);
|
|
7251
7251
|
s = e6;
|
|
7252
7252
|
let p2 = n.blocks.some((e7) => e7.cells.some((e8) => r(e8.id)));
|
|
7253
|
-
o = w,
|
|
7253
|
+
o = w, ro = "h-full w-full overflow-auto flex", c = p2 ? "mo-slide-content flex flex-col gap-3" : "mo-slide-content", t[10] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (l = { margin: "auto 20px" }, t[10] = l) : l = t[10], u = n.blocks.map((e7, t2) => {
|
|
7254
7254
|
let n2 = e7.cells.map((e8) => r(e8.id) ? i ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellReadOnlyView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slide, {
|
|
7255
7255
|
cellId: e8.id,
|
|
7256
7256
|
status: e8.status,
|
|
7257
7257
|
output: e8.output
|
|
7258
7258
|
}, e8.id));
|
|
7259
7259
|
if (e7.isFragment) {
|
|
7260
|
-
let e8 =
|
|
7260
|
+
let e8 = io2.get(t2);
|
|
7261
7261
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Bt, {
|
|
7262
7262
|
as: "div",
|
|
7263
7263
|
children: [n2, e8 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NotesAside, { text: e8 })]
|
|
7264
7264
|
}, t2);
|
|
7265
7265
|
}
|
|
7266
7266
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: n2 }, t2);
|
|
7267
|
-
}), t[0] = i, t[1] = r, t[2] = a, t[3] = n, t[4] = o, t[5] = s, t[6] = c, t[7] = l, t[8] = u, t[9] =
|
|
7268
|
-
} else o = t[4], s = t[5], c = t[6], l = t[7], u = t[8],
|
|
7269
|
-
let
|
|
7270
|
-
t[11] !== c || t[12] !== l || t[13] !== u ? (
|
|
7267
|
+
}), t[0] = i, t[1] = r, t[2] = a, t[3] = n, t[4] = o, t[5] = s, t[6] = c, t[7] = l, t[8] = u, t[9] = ro;
|
|
7268
|
+
} else o = t[4], s = t[5], c = t[6], l = t[7], u = t[8], ro = t[9];
|
|
7269
|
+
let io;
|
|
7270
|
+
t[11] !== c || t[12] !== l || t[13] !== u ? (io = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7271
7271
|
className: c,
|
|
7272
7272
|
style: l,
|
|
7273
7273
|
children: u
|
|
7274
|
-
}), t[11] = c, t[12] = l, t[13] = u, t[14] =
|
|
7274
|
+
}), t[11] = c, t[12] = l, t[13] = u, t[14] = io) : io = t[14];
|
|
7275
7275
|
let p;
|
|
7276
|
-
t[15] !==
|
|
7277
|
-
className:
|
|
7278
|
-
children:
|
|
7279
|
-
}), t[15] =
|
|
7276
|
+
t[15] !== ro || t[16] !== io ? (p = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7277
|
+
className: ro,
|
|
7278
|
+
children: io
|
|
7279
|
+
}), t[15] = ro, t[16] = io, t[17] = p) : p = t[17];
|
|
7280
7280
|
let ao;
|
|
7281
7281
|
t[18] === s ? ao = t[19] : (ao = s && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NotesAside, { text: s }), t[18] = s, t[19] = ao);
|
|
7282
7282
|
let oo;
|
|
@@ -7302,9 +7302,9 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7302
7302
|
status: n.status,
|
|
7303
7303
|
output: n.output
|
|
7304
7304
|
}), t[3] = n.id, t[4] = n.output, t[5] = n.status, t[6] = o) : o = t[6], o;
|
|
7305
|
-
}, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: l, activeIndex:
|
|
7305
|
+
}, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: l, activeIndex: u, onSlideChange: f, mode: io, configWidth: fo, isEditable: po = false }) => {
|
|
7306
7306
|
var _a2, _b2;
|
|
7307
|
-
let yo = (0, import_react.useRef)(null), bo = (0, import_react.useRef)(null), { width: So, height: Co } = useSlideDimensions(yo), wo = useFullScreenElement() != null, To = useAtomValue(kioskModeAtom), Eo = (0, import_react.useMemo)(() => To ? [] : [ce], [To]), [Do, Oo] = (0, import_react.useState)(() => /* @__PURE__ */ new Set()), ko = useNotebookCodeAvailable(e5), Ao = !isIslands() && ko, jo =
|
|
7307
|
+
let yo = (0, import_react.useRef)(null), bo = (0, import_react.useRef)(null), { width: So, height: Co } = useSlideDimensions(yo), wo = useFullScreenElement() != null, To = useAtomValue(kioskModeAtom), Eo = (0, import_react.useMemo)(() => To ? [] : [ce], [To]), [Do, Oo] = (0, import_react.useState)(() => /* @__PURE__ */ new Set()), ko = useNotebookCodeAvailable(e5), Ao = !isIslands() && ko, jo = u == null ? void 0 : e5[u], Mo = jo ?? e5.at(0), { parkedPreviewCell: No, isHeldEdit: Po, isNoOutputPreview: Fo, heldEditCellId: Io, heldShowsCode: Lo, toggleHeldShowsCode: Ro } = useParkedPreview({
|
|
7308
7308
|
activeCell: jo,
|
|
7309
7309
|
slideConfigs: o.cells,
|
|
7310
7310
|
noOutputIds: l
|
|
@@ -7350,7 +7350,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7350
7350
|
qo
|
|
7351
7351
|
]), Yo = useEvent_default((t) => {
|
|
7352
7352
|
let n = resolveDeckNavigationTarget({
|
|
7353
|
-
activeIndex:
|
|
7353
|
+
activeIndex: u,
|
|
7354
7354
|
cells: e5,
|
|
7355
7355
|
cellToTarget: Wo,
|
|
7356
7356
|
getId: (e6) => e6.id
|
|
@@ -7361,7 +7361,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7361
7361
|
let e6 = bo.current;
|
|
7362
7362
|
e6 != null && Yo(e6);
|
|
7363
7363
|
}, [
|
|
7364
|
-
|
|
7364
|
+
u,
|
|
7365
7365
|
Wo,
|
|
7366
7366
|
e5,
|
|
7367
7367
|
Yo
|
|
@@ -7391,14 +7391,14 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7391
7391
|
let e6 = bo.current;
|
|
7392
7392
|
if (!e6) return;
|
|
7393
7393
|
let t = resolveActiveCellIndex(Go, e6.getIndices());
|
|
7394
|
-
t != null && (
|
|
7394
|
+
t != null && (f == null ? void 0 : f(t));
|
|
7395
7395
|
}), $o = useEvent_default((t) => {
|
|
7396
|
-
if (!No ||
|
|
7396
|
+
if (!No || u == null || Events.fromInput(t)) return;
|
|
7397
7397
|
let n = classifyNavKey(t);
|
|
7398
7398
|
if (n === 0) return;
|
|
7399
7399
|
t.preventDefault(), t.stopPropagation();
|
|
7400
|
-
let i =
|
|
7401
|
-
i < 0 || i >= e5.length || (
|
|
7400
|
+
let i = u + n;
|
|
7401
|
+
i < 0 || i >= e5.length || (f == null ? void 0 : f(i));
|
|
7402
7402
|
}), ns = useEvent_default(() => {
|
|
7403
7403
|
Qo(), triggerResize(bo.current);
|
|
7404
7404
|
});
|
|
@@ -7494,7 +7494,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7494
7494
|
]
|
|
7495
7495
|
})
|
|
7496
7496
|
});
|
|
7497
|
-
return
|
|
7497
|
+
return io === "read" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7498
7498
|
className: "flex-1 min-w-0 flex flex-row gap-3",
|
|
7499
7499
|
children: os
|
|
7500
7500
|
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
package/package.json
CHANGED
|
@@ -7,10 +7,6 @@ import type { GetRowIds } from "@/plugins/impl/DataTablePlugin";
|
|
|
7
7
|
import { cn } from "@/utils/cn";
|
|
8
8
|
import { Events } from "@/utils/events";
|
|
9
9
|
import { prettyNumber } from "@/utils/numbers";
|
|
10
|
-
import {
|
|
11
|
-
PANEL_TYPES,
|
|
12
|
-
type PanelType,
|
|
13
|
-
} from "../editor/chrome/panels/context-aware-panel/context-aware-panel";
|
|
14
10
|
import { Button } from "../ui/button";
|
|
15
11
|
import { toast } from "../ui/use-toast";
|
|
16
12
|
import { getColumnCountForDisplay } from "./hooks/use-column-visibility";
|
|
@@ -27,7 +23,6 @@ interface TableBottomBarProps<TData> {
|
|
|
27
23
|
getRowIds?: GetRowIds;
|
|
28
24
|
showPageSizeSelector?: boolean;
|
|
29
25
|
tableLoading?: boolean;
|
|
30
|
-
togglePanel?: (panelType: PanelType) => void;
|
|
31
26
|
part?: string;
|
|
32
27
|
className?: string;
|
|
33
28
|
}
|
|
@@ -41,7 +36,6 @@ export const TableBottomBar = <TData,>({
|
|
|
41
36
|
getRowIds,
|
|
42
37
|
showPageSizeSelector,
|
|
43
38
|
tableLoading,
|
|
44
|
-
togglePanel,
|
|
45
39
|
part,
|
|
46
40
|
className,
|
|
47
41
|
}: TableBottomBarProps<TData>) => {
|
|
@@ -159,15 +153,7 @@ export const TableBottomBar = <TData,>({
|
|
|
159
153
|
return (
|
|
160
154
|
<span className="flex items-center gap-1">
|
|
161
155
|
<span>{rowsAndColumns}</span>
|
|
162
|
-
{hiddenSuffix &&
|
|
163
|
-
<button
|
|
164
|
-
type="button"
|
|
165
|
-
className="text-xs underline-offset-2 hover:underline cursor-pointer"
|
|
166
|
-
onClick={() => togglePanel?.(PANEL_TYPES.COLUMN_EXPLORER)}
|
|
167
|
-
>
|
|
168
|
-
{hiddenSuffix}
|
|
169
|
-
</button>
|
|
170
|
-
)}
|
|
156
|
+
{hiddenSuffix && <span className="text-xs">{hiddenSuffix}</span>}
|
|
171
157
|
</span>
|
|
172
158
|
);
|
|
173
159
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
"use no memo";
|
|
3
3
|
|
|
4
|
+
import type { Table } from "@tanstack/react-table";
|
|
4
5
|
import { useDebounce } from "@uidotdev/usehooks";
|
|
5
6
|
import {
|
|
6
7
|
ChartSplineIcon,
|
|
@@ -17,13 +18,15 @@ import {
|
|
|
17
18
|
} from "../editor/chrome/panels/context-aware-panel/context-aware-panel";
|
|
18
19
|
import { Spinner } from "../icons/spinner";
|
|
19
20
|
import { Button } from "../ui/button";
|
|
21
|
+
import { ColumnVisibilityDropdown } from "./column-visibility-dropdown";
|
|
20
22
|
import { type ExportActionProps, ExportMenu } from "./export-actions";
|
|
21
23
|
|
|
22
24
|
const NOOP_ON_SEARCH = () => {
|
|
23
25
|
/** no-op*/
|
|
24
26
|
};
|
|
25
27
|
|
|
26
|
-
interface TableTopBarProps extends Partial<ExportActionProps> {
|
|
28
|
+
interface TableTopBarProps<TData> extends Partial<ExportActionProps> {
|
|
29
|
+
table: Table<TData>;
|
|
27
30
|
enableSearch: boolean;
|
|
28
31
|
searchQuery?: string;
|
|
29
32
|
onSearchQueryChange?: (query: string) => void;
|
|
@@ -38,7 +41,8 @@ interface TableTopBarProps extends Partial<ExportActionProps> {
|
|
|
38
41
|
sizeBytesIsLoading?: boolean;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
export const TableTopBar
|
|
44
|
+
export const TableTopBar = <TData,>({
|
|
45
|
+
table,
|
|
42
46
|
enableSearch,
|
|
43
47
|
searchQuery,
|
|
44
48
|
onSearchQueryChange,
|
|
@@ -52,7 +56,7 @@ export const TableTopBar: React.FC<TableTopBarProps> = ({
|
|
|
52
56
|
downloadAs,
|
|
53
57
|
sizeBytes,
|
|
54
58
|
sizeBytesIsLoading,
|
|
55
|
-
}) => {
|
|
59
|
+
}: TableTopBarProps<TData>) => {
|
|
56
60
|
const [internalValue, setInternalValue] = useState(searchQuery || "");
|
|
57
61
|
const debouncedSearch = useDebounce(internalValue, 500);
|
|
58
62
|
const onSearch = useEvent(onSearchQueryChange ?? NOOP_ON_SEARCH);
|
|
@@ -62,16 +66,6 @@ export const TableTopBar: React.FC<TableTopBarProps> = ({
|
|
|
62
66
|
onSearch(debouncedSearch);
|
|
63
67
|
}, [debouncedSearch, onSearch]);
|
|
64
68
|
|
|
65
|
-
const hasAnyAction =
|
|
66
|
-
(enableSearch && onSearchQueryChange) ||
|
|
67
|
-
showChartBuilder ||
|
|
68
|
-
showTableExplorer ||
|
|
69
|
-
downloadAs;
|
|
70
|
-
|
|
71
|
-
if (!hasAnyAction) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
69
|
return (
|
|
76
70
|
<div className="flex items-center h-10 px-2 border-b gap-2">
|
|
77
71
|
{onSearchQueryChange && enableSearch && (
|
|
@@ -106,6 +100,7 @@ export const TableTopBar: React.FC<TableTopBarProps> = ({
|
|
|
106
100
|
)}
|
|
107
101
|
|
|
108
102
|
<div className="flex items-center shrink-0">
|
|
103
|
+
<ColumnVisibilityDropdown table={table} />
|
|
109
104
|
{showChartBuilder && (
|
|
110
105
|
<Button
|
|
111
106
|
variant="text"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
|
5
5
|
import { render, screen } from "@testing-library/react";
|
|
6
|
-
import { describe, expect, it
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
7
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
8
|
import { CellSelectionProvider } from "../range-focus/provider";
|
|
9
9
|
import { TableBottomBar } from "../TableBottomBar";
|
|
@@ -11,7 +11,6 @@ import { TableBottomBar } from "../TableBottomBar";
|
|
|
11
11
|
function renderWithTable(opts: {
|
|
12
12
|
totalColumns: number;
|
|
13
13
|
hiddenColumns?: string[];
|
|
14
|
-
togglePanel?: (panelType: string) => void;
|
|
15
14
|
}) {
|
|
16
15
|
const Wrapper = () => {
|
|
17
16
|
const table = useReactTable({
|
|
@@ -34,7 +33,6 @@ function renderWithTable(opts: {
|
|
|
34
33
|
pagination={false}
|
|
35
34
|
totalColumns={opts.totalColumns}
|
|
36
35
|
table={table}
|
|
37
|
-
togglePanel={opts.togglePanel}
|
|
38
36
|
/>
|
|
39
37
|
);
|
|
40
38
|
};
|
|
@@ -60,14 +58,10 @@ describe("TableBottomBar — hidden column count", () => {
|
|
|
60
58
|
expect(screen.getByText(/\(1 hidden\)/)).toBeInTheDocument();
|
|
61
59
|
});
|
|
62
60
|
|
|
63
|
-
it("
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
togglePanel,
|
|
69
|
-
});
|
|
70
|
-
screen.getByText(/\(1 hidden\)/).click();
|
|
71
|
-
expect(togglePanel).toHaveBeenCalledWith("column-explorer");
|
|
61
|
+
it("renders '(n hidden)' as inert text, not a button", () => {
|
|
62
|
+
renderWithTable({ totalColumns: 3, hiddenColumns: ["col1"] });
|
|
63
|
+
const suffix = screen.getByText(/\(1 hidden\)/);
|
|
64
|
+
expect(suffix.tagName).toBe("SPAN");
|
|
65
|
+
expect(suffix.closest("button")).toBeNull();
|
|
72
66
|
});
|
|
73
67
|
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ColumnDef,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
useReactTable,
|
|
7
|
+
} from "@tanstack/react-table";
|
|
8
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
9
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
10
|
+
import { ColumnVisibilityDropdown } from "../column-visibility-dropdown";
|
|
11
|
+
import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "../types";
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
global.HTMLElement.prototype.scrollIntoView = () => {
|
|
15
|
+
// jsdom does not implement scrollIntoView; cmdk calls it on selection.
|
|
16
|
+
};
|
|
17
|
+
if (!global.HTMLElement.prototype.hasPointerCapture) {
|
|
18
|
+
global.HTMLElement.prototype.hasPointerCapture = () => false;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
type Row = Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
const TEST_COLUMNS: ColumnDef<Row>[] = [
|
|
25
|
+
{ id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID },
|
|
26
|
+
{ id: INDEX_COLUMN_NAME, accessorKey: INDEX_COLUMN_NAME },
|
|
27
|
+
{ id: "__m_column__0", accessorKey: "__m_column__0" },
|
|
28
|
+
{
|
|
29
|
+
id: "customer_name",
|
|
30
|
+
accessorKey: "customer_name",
|
|
31
|
+
meta: { dataType: "string" },
|
|
32
|
+
},
|
|
33
|
+
{ id: "cust_age", accessorKey: "cust_age", meta: { dataType: "integer" } },
|
|
34
|
+
{
|
|
35
|
+
id: "order_total",
|
|
36
|
+
accessorKey: "order_total",
|
|
37
|
+
meta: { dataType: "number" },
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
interface HarnessProps {
|
|
42
|
+
initiallyHidden?: string[];
|
|
43
|
+
nonHideable?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function Harness({ initiallyHidden = [], nonHideable = [] }: HarnessProps) {
|
|
47
|
+
const table = useReactTable<Row>({
|
|
48
|
+
data: [],
|
|
49
|
+
columns: TEST_COLUMNS.map((column) =>
|
|
50
|
+
nonHideable.includes(column.id as string)
|
|
51
|
+
? { ...column, enableHiding: false }
|
|
52
|
+
: column,
|
|
53
|
+
),
|
|
54
|
+
getCoreRowModel: getCoreRowModel(),
|
|
55
|
+
locale: "en-US",
|
|
56
|
+
initialState: {
|
|
57
|
+
columnVisibility: Object.fromEntries(
|
|
58
|
+
initiallyHidden.map((id) => [id, false]),
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return <ColumnVisibilityDropdown table={table} />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderAndOpen(props?: HarnessProps) {
|
|
66
|
+
const result = render(<Harness {...(props ?? {})} />);
|
|
67
|
+
fireEvent.click(screen.getByTestId("column-visibility-trigger"));
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getOptionTexts(): string[] {
|
|
72
|
+
return screen.getAllByRole("option").map((el) => el.textContent ?? "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getColumnOption(name: string): HTMLElement {
|
|
76
|
+
const option = screen
|
|
77
|
+
.getAllByRole("option")
|
|
78
|
+
.find((el) => el.textContent === name);
|
|
79
|
+
if (!option) {
|
|
80
|
+
throw new Error(`No option for column ${name}`);
|
|
81
|
+
}
|
|
82
|
+
return option;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getSearchInput() {
|
|
86
|
+
return screen.getByPlaceholderText("Search columns...");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("ColumnVisibilityDropdown", () => {
|
|
90
|
+
it("renders user columns and excludes select/index/nameless columns", () => {
|
|
91
|
+
renderAndOpen();
|
|
92
|
+
expect(screen.getByText("customer_name")).toBeInTheDocument();
|
|
93
|
+
expect(screen.getByText("cust_age")).toBeInTheDocument();
|
|
94
|
+
expect(screen.getByText("order_total")).toBeInTheDocument();
|
|
95
|
+
expect(screen.queryByText(SELECT_COLUMN_ID)).not.toBeInTheDocument();
|
|
96
|
+
expect(screen.queryByText(INDEX_COLUMN_NAME)).not.toBeInTheDocument();
|
|
97
|
+
expect(screen.queryByText("__m_column__0")).not.toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("lists hidden columns before shown columns", () => {
|
|
101
|
+
renderAndOpen({ initiallyHidden: ["cust_age"] });
|
|
102
|
+
expect(getOptionTexts()).toEqual([
|
|
103
|
+
"Show all",
|
|
104
|
+
"cust_age",
|
|
105
|
+
"customer_name",
|
|
106
|
+
"order_total",
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("toggling a hidden column flips the icon without moving the row", () => {
|
|
111
|
+
renderAndOpen({ initiallyHidden: ["cust_age"] });
|
|
112
|
+
expect(
|
|
113
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
114
|
+
).not.toBeNull();
|
|
115
|
+
|
|
116
|
+
fireEvent.click(getColumnOption("cust_age"));
|
|
117
|
+
|
|
118
|
+
expect(getOptionTexts()).toEqual([
|
|
119
|
+
"Show all",
|
|
120
|
+
"cust_age",
|
|
121
|
+
"customer_name",
|
|
122
|
+
"order_total",
|
|
123
|
+
]);
|
|
124
|
+
expect(
|
|
125
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
126
|
+
).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("re-sorts on reopen after a toggle", () => {
|
|
130
|
+
renderAndOpen({ initiallyHidden: ["cust_age"] });
|
|
131
|
+
// Hide customer_name: it stays in the shown section until reopen.
|
|
132
|
+
fireEvent.click(getColumnOption("customer_name"));
|
|
133
|
+
expect(getOptionTexts()).toEqual([
|
|
134
|
+
"Show all",
|
|
135
|
+
"cust_age",
|
|
136
|
+
"customer_name",
|
|
137
|
+
"order_total",
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const trigger = screen.getByTestId("column-visibility-trigger");
|
|
141
|
+
fireEvent.click(trigger);
|
|
142
|
+
fireEvent.click(trigger);
|
|
143
|
+
|
|
144
|
+
// Reopen sorts both hidden columns first, preserving table order.
|
|
145
|
+
expect(getOptionTexts()).toEqual([
|
|
146
|
+
"Show all",
|
|
147
|
+
"customer_name",
|
|
148
|
+
"cust_age",
|
|
149
|
+
"order_total",
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("filters columns with smartMatch", () => {
|
|
154
|
+
renderAndOpen();
|
|
155
|
+
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
156
|
+
expect(screen.getByText("customer_name")).toBeInTheDocument();
|
|
157
|
+
expect(screen.getByText("cust_age")).toBeInTheDocument();
|
|
158
|
+
expect(screen.queryByText("order_total")).not.toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("shows 'No results.' when nothing matches", () => {
|
|
162
|
+
renderAndOpen();
|
|
163
|
+
fireEvent.change(getSearchInput(), { target: { value: "xyz" } });
|
|
164
|
+
expect(screen.getByText("No results.")).toBeInTheDocument();
|
|
165
|
+
expect(screen.queryByText("customer_name")).not.toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("disables 'Show all' when no columns are hidden", () => {
|
|
169
|
+
renderAndOpen();
|
|
170
|
+
expect(getColumnOption("Show all")).toHaveAttribute(
|
|
171
|
+
"aria-disabled",
|
|
172
|
+
"true",
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("'Show all' restores hidden columns", () => {
|
|
177
|
+
renderAndOpen({ initiallyHidden: ["cust_age", "order_total"] });
|
|
178
|
+
fireEvent.click(getColumnOption("Show all"));
|
|
179
|
+
|
|
180
|
+
expect(
|
|
181
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
182
|
+
).toBeNull();
|
|
183
|
+
expect(
|
|
184
|
+
getColumnOption("order_total").querySelector(".lucide-eye-off"),
|
|
185
|
+
).toBeNull();
|
|
186
|
+
expect(getColumnOption("Show all")).toHaveAttribute(
|
|
187
|
+
"aria-disabled",
|
|
188
|
+
"true",
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("hides and shows matching columns via bulk actions while searching", () => {
|
|
193
|
+
renderAndOpen();
|
|
194
|
+
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
195
|
+
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument();
|
|
196
|
+
|
|
197
|
+
fireEvent.click(getColumnOption("Hide 2 matching"));
|
|
198
|
+
expect(
|
|
199
|
+
getColumnOption("customer_name").querySelector(".lucide-eye-off"),
|
|
200
|
+
).not.toBeNull();
|
|
201
|
+
expect(
|
|
202
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
203
|
+
).not.toBeNull();
|
|
204
|
+
expect(screen.queryByText(/Hide \d+ matching/)).not.toBeInTheDocument();
|
|
205
|
+
|
|
206
|
+
fireEvent.click(getColumnOption("Show 2 matching"));
|
|
207
|
+
expect(
|
|
208
|
+
getColumnOption("customer_name").querySelector(".lucide-eye-off"),
|
|
209
|
+
).toBeNull();
|
|
210
|
+
expect(screen.queryByText(/Show \d+ matching/)).not.toBeInTheDocument();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("offers both bulk actions when matches are mixed", () => {
|
|
214
|
+
renderAndOpen({ initiallyHidden: ["cust_age"] });
|
|
215
|
+
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
216
|
+
expect(screen.getByText(/Hide 1 matching/)).toBeInTheDocument();
|
|
217
|
+
expect(screen.getByText(/Show 1 matching/)).toBeInTheDocument();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("renders non-hideable columns disabled and without an eye toggle", () => {
|
|
221
|
+
renderAndOpen({ nonHideable: ["customer_name"] });
|
|
222
|
+
const option = getColumnOption("customer_name");
|
|
223
|
+
expect(option).toHaveAttribute("aria-disabled", "true");
|
|
224
|
+
expect(option.querySelector(".lucide-eye")).toBeNull();
|
|
225
|
+
expect(option.querySelector(".lucide-eye-off")).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
});
|