@marimo-team/islands 0.23.10-dev3 → 0.23.10-dev31
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/{ConnectedDataExplorerComponent-CyV83R2m.js → ConnectedDataExplorerComponent-DmBropAy.js} +31 -31
- package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
- package/dist/{any-language-editor-DfdpyDv_.js → any-language-editor-DNmoSiWL.js} +20 -20
- package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
- package/dist/assets/worker-CC0Oul9k.js +73 -0
- package/dist/{chat-ui-C1tL1pML.js → chat-ui-D6oraHT2.js} +76 -76
- package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
- package/dist/{code-visibility-DfnO0DcH.js → code-visibility-B9qGgair.js} +2166 -1292
- package/dist/{copy-BuQpJEzp.js → copy-UqRYxiOg.js} +33 -33
- package/dist/dist-7QfXoMdB.js +5 -0
- package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
- package/dist/dist-BEXXyZig.js +5 -0
- package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
- package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
- package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
- package/dist/dist-BYj57OV4.js +5 -0
- package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
- package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
- package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
- package/dist/dist-BlSvQzNr.js +5 -0
- package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
- package/dist/dist-CCBlxAgS.js +8 -0
- package/dist/dist-CIDTVIUf.js +5 -0
- package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
- package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
- package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
- package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
- package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
- package/dist/dist-CqQyhAM8.js +8 -0
- package/dist/dist-CwRu2Xzh.js +5 -0
- package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
- package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
- package/dist/dist-D8CDTVgf.js +6 -0
- package/dist/dist-D8DNB0nO.js +8 -0
- package/dist/dist-DL6N_q-A.js +5 -0
- package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
- package/dist/dist-DOFbNV_b.js +8 -0
- package/dist/dist-DPrYzMY0.js +6 -0
- package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
- package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
- package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
- package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
- package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
- package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
- package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
- package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
- package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
- package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
- package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
- package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
- package/dist/dist-cdmMjgsn.js +5 -0
- package/dist/dist-hT4QzYX-.js +1247 -0
- package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
- package/dist/{error-banner-5bz0L9hS.js → error-banner-Cc0I3C9e.js} +1 -1
- package/dist/esm-BaH2eg5-.js +1171 -0
- package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
- package/dist/{extends-BgdxCfYu.js → extends-D_hDsj6R.js} +4 -4
- package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
- package/dist/{glide-data-editor-BOmK9ETQ.js → glide-data-editor-Qhu8oCX-.js} +12 -12
- package/dist/{html-to-image-CNa5ok96.js → html-to-image-UEH5lFDZ.js} +2318 -2275
- package/dist/{input-_2sjvfne.js → input-CMYy4hzj.js} +187 -185
- package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
- package/dist/main.js +6941 -6913
- package/dist/{mermaid-lXOw5Py9.js → mermaid-zuLgJ8J8.js} +4 -4
- package/dist/{process-output-DKr4f1di.js → process-output-CyMLTogj.js} +3 -3
- package/dist/{reveal-component-UdMnCK5U.js → reveal-component-Co2AuBAx.js} +697 -619
- package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
- package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
- package/dist/style.css +1 -1
- package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
- package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
- package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
- package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
- package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
- package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
- package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
- package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
- package/dist/{vega-component-Dq-SH463.js → vega-component-9h1ACS78.js} +8 -8
- package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
- package/package.json +3 -3
- 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/__tests__/data-table.test.tsx +154 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
- package/src/components/data-table/data-table.tsx +1 -1
- package/src/components/data-table/filter-by-values-picker.tsx +39 -17
- package/src/components/data-table/filter-pills.tsx +1 -1
- package/src/components/data-table/hover-tooltip/__tests__/content.test.ts +60 -0
- package/src/components/data-table/hover-tooltip/content.ts +44 -0
- package/src/components/data-table/hover-tooltip/hover-tooltip.tsx +55 -0
- package/src/components/data-table/hover-tooltip/use-table-hover-tooltip.ts +159 -0
- package/src/components/data-table/renderers.tsx +27 -43
- package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
- package/src/components/datasources/datasources.tsx +92 -3
- package/src/components/editor/cell/cell-context-menu.tsx +15 -2
- package/src/components/editor/cell/code/language-toggle.tsx +7 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
- package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
- package/src/components/editor/documentation.css +35 -0
- package/src/components/editor/file-tree/file-explorer.tsx +8 -18
- package/src/components/editor/file-tree/tree-actions.tsx +46 -1
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
- package/src/components/editor/renderers/slides-layout/types.ts +1 -0
- package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
- package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
- package/src/components/slides/minimap.tsx +127 -10
- package/src/components/slides/reveal-component.tsx +287 -61
- package/src/components/slides/slide-cell-view.tsx +26 -2
- package/src/components/slides/slide-form.tsx +26 -4
- package/src/components/storage/__tests__/storage-inspector.test.ts +53 -0
- package/src/components/storage/storage-inspector.tsx +68 -48
- package/src/components/ui/__tests__/use-toast.test.ts +75 -0
- package/src/components/ui/combobox.tsx +51 -32
- package/src/components/ui/reorderable-list.tsx +13 -0
- package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
- package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
- package/src/components/ui/select-core/index.ts +16 -0
- package/src/components/ui/select-core/option-row.tsx +33 -0
- package/src/components/ui/select-core/render-slot.ts +20 -0
- package/src/components/ui/select-core/select-list.tsx +248 -0
- package/src/components/ui/select-core/types.ts +44 -0
- package/src/components/ui/select-core/use-select-list.ts +347 -0
- package/src/components/ui/select-core/utils.ts +121 -0
- package/src/components/ui/use-toast.ts +33 -13
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -28
- package/src/core/cells/__tests__/cell.test.ts +29 -2
- package/src/core/cells/cell.ts +5 -1
- package/src/core/codemirror/go-to-definition/commands.ts +4 -3
- package/src/core/codemirror/language/languages/python.ts +2 -0
- package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
- package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
- package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
- package/src/core/datasets/data-source-connections.ts +2 -0
- package/src/core/network/__tests__/requests-static.test.ts +30 -0
- package/src/core/network/requests-static.ts +14 -10
- package/src/core/wasm/worker/bootstrap.ts +12 -4
- package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
- package/src/plugins/impl/SearchableSelect.tsx +16 -97
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
- package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
- package/src/plugins/layout/DownloadPlugin.tsx +1 -1
- package/src/utils/lazy.ts +6 -1
- package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
- package/dist/assets/worker-ip3AI_sN.js +0 -73
- package/dist/dist-0Fif7jnk.js +0 -5
- package/dist/dist-B5h_9sHB.js +0 -6
- package/dist/dist-B9M6R5ye.js +0 -5
- package/dist/dist-BCt3tnck.js +0 -8
- package/dist/dist-BUIJwMwn.js +0 -8
- package/dist/dist-BpquMd3k.js +0 -5
- package/dist/dist-BzJsqYfz.js +0 -5
- package/dist/dist-CA5ELXAf.js +0 -6
- package/dist/dist-CLBRs6Uv.js +0 -5
- package/dist/dist-CStVCMbq.js +0 -5
- package/dist/dist-CZRIEY3Y.js +0 -8
- package/dist/dist-CuUHbFD0.js +0 -5
- package/dist/dist-DV7Iabxb.js +0 -8
- package/dist/dist-DhHh0jLg.js +0 -1247
- package/dist/dist-DuEeHMvL.js +0 -5
- package/dist/esm-BfhQmZjp.js +0 -1171
- package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
- /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
|
@@ -5,8 +5,8 @@ import type {
|
|
|
5
5
|
RowSelectionState,
|
|
6
6
|
SortingState,
|
|
7
7
|
} from "@tanstack/react-table";
|
|
8
|
-
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
9
|
-
import { describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
10
10
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
11
11
|
import { DataTable } from "../data-table";
|
|
12
12
|
|
|
@@ -16,6 +16,12 @@ interface TestData {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
describe("DataTable", () => {
|
|
19
|
+
// Restore real timers unconditionally so a failed assertion in a
|
|
20
|
+
// fake-timer test can't leak fake timers into later tests.
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
});
|
|
24
|
+
|
|
19
25
|
it("should maintain selection state when remounted", () => {
|
|
20
26
|
const mockOnRowSelectionChange = vi.fn();
|
|
21
27
|
const testData: TestData[] = [
|
|
@@ -63,17 +69,15 @@ describe("DataTable", () => {
|
|
|
63
69
|
expect(commonProps.rowSelection).toEqual(initialRowSelection);
|
|
64
70
|
});
|
|
65
71
|
|
|
66
|
-
it("
|
|
72
|
+
it("shows the hoverTemplate text as a styled tooltip on hover", async () => {
|
|
73
|
+
vi.useFakeTimers();
|
|
67
74
|
interface RowData {
|
|
68
75
|
id: number;
|
|
69
76
|
first: string;
|
|
70
77
|
last: string;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
const testData: RowData[] = [
|
|
74
|
-
{ id: 1, first: "Michael", last: "Scott" },
|
|
75
|
-
{ id: 2, first: "Jim", last: "Halpert" },
|
|
76
|
-
];
|
|
80
|
+
const testData: RowData[] = [{ id: 1, first: "Michael", last: "Scott" }];
|
|
77
81
|
|
|
78
82
|
const columns: ColumnDef<RowData>[] = [
|
|
79
83
|
{ accessorKey: "first", header: "First" },
|
|
@@ -86,7 +90,7 @@ describe("DataTable", () => {
|
|
|
86
90
|
data={testData}
|
|
87
91
|
columns={columns}
|
|
88
92
|
selection={null}
|
|
89
|
-
totalRows={
|
|
93
|
+
totalRows={1}
|
|
90
94
|
totalColumns={2}
|
|
91
95
|
pagination={false}
|
|
92
96
|
hoverTemplate={"{{first}} {{last}}"}
|
|
@@ -94,11 +98,149 @@ describe("DataTable", () => {
|
|
|
94
98
|
</TooltipProvider>,
|
|
95
99
|
);
|
|
96
100
|
|
|
97
|
-
// Grab all rows and assert title attribute computed from template
|
|
98
101
|
const rows = screen.getAllByRole("row");
|
|
99
|
-
//
|
|
100
|
-
expect(rows[1]).toHaveAttribute("title"
|
|
101
|
-
|
|
102
|
+
// Native title is gone; hover text now comes from the styled tooltip.
|
|
103
|
+
expect(rows[1]).not.toHaveAttribute("title");
|
|
104
|
+
|
|
105
|
+
const cell = within(rows[1]).getAllByRole("cell")[0];
|
|
106
|
+
fireEvent.mouseOver(cell, { buttons: 0 });
|
|
107
|
+
act(() => {
|
|
108
|
+
vi.advanceTimersByTime(400);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Radix renders the content twice (visible + a11y-hidden), so match all.
|
|
112
|
+
expect(screen.getAllByText("Michael Scott").length).toBeGreaterThan(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("shows per-cell hover text from cellHoverTexts on hover", () => {
|
|
116
|
+
vi.useFakeTimers();
|
|
117
|
+
const testData: TestData[] = [{ id: 1, name: "Test 1" }];
|
|
118
|
+
const columns: ColumnDef<TestData>[] = [
|
|
119
|
+
{ id: "name", accessorKey: "name", header: "Name" },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
render(
|
|
123
|
+
<TooltipProvider>
|
|
124
|
+
<DataTable
|
|
125
|
+
data={testData}
|
|
126
|
+
columns={columns}
|
|
127
|
+
selection={null}
|
|
128
|
+
totalRows={1}
|
|
129
|
+
totalColumns={1}
|
|
130
|
+
pagination={false}
|
|
131
|
+
cellHoverTexts={{ "0": { name: "per-cell tip" } }}
|
|
132
|
+
/>
|
|
133
|
+
</TooltipProvider>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const cell = within(screen.getAllByRole("row")[1]).getByRole("cell");
|
|
137
|
+
fireEvent.mouseOver(cell, { buttons: 0 });
|
|
138
|
+
act(() => {
|
|
139
|
+
vi.advanceTimersByTime(400);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(screen.getAllByText("per-cell tip").length).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("links the focused cell to the tooltip content for assistive tech", () => {
|
|
146
|
+
const testData: TestData[] = [{ id: 1, name: "Test 1" }];
|
|
147
|
+
const columns: ColumnDef<TestData>[] = [
|
|
148
|
+
{ id: "name", accessorKey: "name", header: "Name" },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
render(
|
|
152
|
+
<TooltipProvider>
|
|
153
|
+
<DataTable
|
|
154
|
+
data={testData}
|
|
155
|
+
columns={columns}
|
|
156
|
+
selection={null}
|
|
157
|
+
totalRows={1}
|
|
158
|
+
totalColumns={1}
|
|
159
|
+
pagination={false}
|
|
160
|
+
cellHoverTexts={{ "0": { name: "focus tip" } }}
|
|
161
|
+
/>
|
|
162
|
+
</TooltipProvider>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const cell = within(screen.getAllByRole("row")[1]).getByRole("cell");
|
|
166
|
+
fireEvent.focus(cell);
|
|
167
|
+
|
|
168
|
+
const describedBy = cell.getAttribute("aria-describedby");
|
|
169
|
+
expect(describedBy).toBeTruthy();
|
|
170
|
+
expect(document.getElementById(describedBy as string)).toHaveTextContent(
|
|
171
|
+
"focus tip",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
fireEvent.blur(cell);
|
|
175
|
+
expect(cell).not.toHaveAttribute("aria-describedby");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not show a tooltip on pointer-induced focus", () => {
|
|
179
|
+
const testData: TestData[] = [{ id: 1, name: "Test 1" }];
|
|
180
|
+
const columns: ColumnDef<TestData>[] = [
|
|
181
|
+
{ id: "name", accessorKey: "name", header: "Name" },
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
render(
|
|
185
|
+
<TooltipProvider>
|
|
186
|
+
<DataTable
|
|
187
|
+
data={testData}
|
|
188
|
+
columns={columns}
|
|
189
|
+
selection={null}
|
|
190
|
+
totalRows={1}
|
|
191
|
+
totalColumns={1}
|
|
192
|
+
pagination={false}
|
|
193
|
+
cellHoverTexts={{ "0": { name: "click tip" } }}
|
|
194
|
+
/>
|
|
195
|
+
</TooltipProvider>,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const cell = within(screen.getAllByRole("row")[1]).getByRole("cell");
|
|
199
|
+
// A click focuses the cell; the resulting focus must not show a tooltip.
|
|
200
|
+
fireEvent.mouseDown(cell);
|
|
201
|
+
fireEvent.focus(cell);
|
|
202
|
+
|
|
203
|
+
expect(cell).not.toHaveAttribute("aria-describedby");
|
|
204
|
+
expect(screen.queryByText("click tip")).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("does not let a pending hover timer overwrite a focus tooltip", () => {
|
|
208
|
+
vi.useFakeTimers();
|
|
209
|
+
interface RowData {
|
|
210
|
+
id: number;
|
|
211
|
+
a: string;
|
|
212
|
+
b: string;
|
|
213
|
+
}
|
|
214
|
+
const testData: RowData[] = [{ id: 1, a: "a", b: "b" }];
|
|
215
|
+
const columns: ColumnDef<RowData>[] = [
|
|
216
|
+
{ id: "a", accessorKey: "a", header: "A" },
|
|
217
|
+
{ id: "b", accessorKey: "b", header: "B" },
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
render(
|
|
221
|
+
<TooltipProvider>
|
|
222
|
+
<DataTable
|
|
223
|
+
data={testData}
|
|
224
|
+
columns={columns}
|
|
225
|
+
selection={null}
|
|
226
|
+
totalRows={1}
|
|
227
|
+
totalColumns={2}
|
|
228
|
+
pagination={false}
|
|
229
|
+
cellHoverTexts={{ "0": { a: "hover A", b: "focus B" } }}
|
|
230
|
+
/>
|
|
231
|
+
</TooltipProvider>,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const cells = within(screen.getAllByRole("row")[1]).getAllByRole("cell");
|
|
235
|
+
// Start a pending hover-show on cell A, then focus cell B before it fires.
|
|
236
|
+
fireEvent.mouseOver(cells[0], { buttons: 0 });
|
|
237
|
+
fireEvent.focus(cells[1]);
|
|
238
|
+
act(() => {
|
|
239
|
+
vi.advanceTimersByTime(400);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(screen.getAllByText("focus B").length).toBeGreaterThan(0);
|
|
243
|
+
expect(screen.queryByText("hover A")).toBeNull();
|
|
102
244
|
});
|
|
103
245
|
|
|
104
246
|
it("does not virtualize small datasets without pagination", () => {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
"use no memo";
|
|
3
|
+
|
|
4
|
+
// tanstack/table is not compatible with React compiler
|
|
5
|
+
// https://github.com/TanStack/table/issues/5567
|
|
6
|
+
|
|
7
|
+
import type { Table } from "@tanstack/react-table";
|
|
8
|
+
import { Columns3Icon, EyeIcon, EyeOffIcon } from "lucide-react";
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { ColumnName } from "@/components/datasources/components";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import {
|
|
13
|
+
Command,
|
|
14
|
+
CommandEmpty,
|
|
15
|
+
CommandInput,
|
|
16
|
+
CommandItem,
|
|
17
|
+
CommandList,
|
|
18
|
+
CommandSeparator,
|
|
19
|
+
} from "@/components/ui/command";
|
|
20
|
+
import {
|
|
21
|
+
Popover,
|
|
22
|
+
PopoverContent,
|
|
23
|
+
PopoverTrigger,
|
|
24
|
+
} from "@/components/ui/popover";
|
|
25
|
+
import { type BulkAction, useSelectList } from "@/components/ui/select-core";
|
|
26
|
+
import type { DataType } from "@/core/kernel/messages";
|
|
27
|
+
import { cn } from "@/utils/cn";
|
|
28
|
+
import { Events } from "@/utils/events";
|
|
29
|
+
import { smartMatchFilter } from "@/utils/smartMatch";
|
|
30
|
+
import { NAMELESS_COLUMN_PREFIX } from "./columns";
|
|
31
|
+
import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "./types";
|
|
32
|
+
|
|
33
|
+
function getUserColumns<TData>(table: Table<TData>) {
|
|
34
|
+
return table
|
|
35
|
+
.getAllLeafColumns()
|
|
36
|
+
.filter(
|
|
37
|
+
(column) =>
|
|
38
|
+
column.id !== SELECT_COLUMN_ID &&
|
|
39
|
+
column.id !== INDEX_COLUMN_NAME &&
|
|
40
|
+
!column.id.startsWith(NAMELESS_COLUMN_PREFIX),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const ColumnVisibilityDropdown = <TData,>({
|
|
45
|
+
table,
|
|
46
|
+
}: {
|
|
47
|
+
table: Table<TData>;
|
|
48
|
+
}) => {
|
|
49
|
+
const userColumns = getUserColumns(table);
|
|
50
|
+
const options = userColumns.map((column) => ({
|
|
51
|
+
value: column.id,
|
|
52
|
+
label: column.id,
|
|
53
|
+
disabled: !column.getCanHide(),
|
|
54
|
+
data: { dataType: column.columnDef.meta?.dataType },
|
|
55
|
+
}));
|
|
56
|
+
// Modeled as a select list over hidden columns: "selected" means hidden, so
|
|
57
|
+
// the hook's pinning floats hidden columns to the top and freezes that order
|
|
58
|
+
// while the menu is open.
|
|
59
|
+
const hiddenIds = userColumns
|
|
60
|
+
.filter((column) => !column.getIsVisible())
|
|
61
|
+
.map((column) => column.id);
|
|
62
|
+
|
|
63
|
+
const applyHidden = (next: string[] | string | null) => {
|
|
64
|
+
const hidden = new Set(Array.isArray(next) ? next : []);
|
|
65
|
+
table.setColumnVisibility((previous) => ({
|
|
66
|
+
...previous,
|
|
67
|
+
...Object.fromEntries(
|
|
68
|
+
userColumns.map((column) => [column.id, !hidden.has(column.id)]),
|
|
69
|
+
),
|
|
70
|
+
}));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const list = useSelectList<string>({
|
|
74
|
+
options,
|
|
75
|
+
value: hiddenIds,
|
|
76
|
+
onChange: applyHidden,
|
|
77
|
+
multiple: true,
|
|
78
|
+
filterFn: smartMatchFilter,
|
|
79
|
+
pinSelected: true,
|
|
80
|
+
});
|
|
81
|
+
// With selection modeling hidden columns, select-matching hides the visible
|
|
82
|
+
// matches and deselect-matching shows the hidden ones.
|
|
83
|
+
const matchingActions = list.bulkActions.filter(
|
|
84
|
+
(
|
|
85
|
+
action,
|
|
86
|
+
): action is Extract<
|
|
87
|
+
BulkAction<string>,
|
|
88
|
+
{ kind: "select-matching" | "deselect-matching" }
|
|
89
|
+
> =>
|
|
90
|
+
action.kind === "select-matching" || action.kind === "deselect-matching",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Popover open={list.open} onOpenChange={list.setOpen}>
|
|
95
|
+
<PopoverTrigger asChild={true}>
|
|
96
|
+
<Button
|
|
97
|
+
variant="text"
|
|
98
|
+
size="xs"
|
|
99
|
+
data-testid="column-visibility-trigger"
|
|
100
|
+
onMouseDown={Events.preventFocus}
|
|
101
|
+
className={cn(
|
|
102
|
+
"print:hidden text-xs gap-1",
|
|
103
|
+
list.open ? "text-primary" : "text-muted-foreground",
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
<Columns3Icon className="w-3.5 h-3.5" />
|
|
107
|
+
Columns
|
|
108
|
+
</Button>
|
|
109
|
+
</PopoverTrigger>
|
|
110
|
+
<PopoverContent className="w-64 p-0" align="end">
|
|
111
|
+
<Command shouldFilter={false}>
|
|
112
|
+
<CommandInput
|
|
113
|
+
placeholder="Search columns..."
|
|
114
|
+
value={list.searchQuery}
|
|
115
|
+
onValueChange={list.setSearchQuery}
|
|
116
|
+
/>
|
|
117
|
+
<CommandList>
|
|
118
|
+
<CommandEmpty>No results.</CommandEmpty>
|
|
119
|
+
{list.searchQuery === "" ? (
|
|
120
|
+
<>
|
|
121
|
+
<CommandItem
|
|
122
|
+
value="__show_all__"
|
|
123
|
+
disabled={hiddenIds.length === 0}
|
|
124
|
+
onSelect={() => applyHidden([])}
|
|
125
|
+
className="cursor-pointer"
|
|
126
|
+
>
|
|
127
|
+
<EyeIcon className="w-3 h-3 mr-1.5" />
|
|
128
|
+
Show all
|
|
129
|
+
</CommandItem>
|
|
130
|
+
<CommandSeparator />
|
|
131
|
+
</>
|
|
132
|
+
) : (
|
|
133
|
+
matchingActions.length > 0 && (
|
|
134
|
+
<>
|
|
135
|
+
{matchingActions.map((action) => (
|
|
136
|
+
<CommandItem
|
|
137
|
+
key={action.kind}
|
|
138
|
+
value={`__bulk_${action.kind}`}
|
|
139
|
+
onSelect={action.run}
|
|
140
|
+
className="cursor-pointer"
|
|
141
|
+
>
|
|
142
|
+
{action.kind === "select-matching" ? (
|
|
143
|
+
<EyeOffIcon className="w-3 h-3 mr-1.5" />
|
|
144
|
+
) : (
|
|
145
|
+
<EyeIcon className="w-3 h-3 mr-1.5" />
|
|
146
|
+
)}
|
|
147
|
+
{action.kind === "select-matching" ? "Hide" : "Show"}{" "}
|
|
148
|
+
{action.items.length} matching
|
|
149
|
+
</CommandItem>
|
|
150
|
+
))}
|
|
151
|
+
<CommandSeparator />
|
|
152
|
+
</>
|
|
153
|
+
)
|
|
154
|
+
)}
|
|
155
|
+
{list.visibleOptions.map((option, index) => {
|
|
156
|
+
const hidden = list.isChecked(option.value);
|
|
157
|
+
const { dataType } = option.data as {
|
|
158
|
+
dataType: DataType | undefined;
|
|
159
|
+
};
|
|
160
|
+
const isSectionBoundary =
|
|
161
|
+
index === list.pinnedCount &&
|
|
162
|
+
list.pinnedCount > 0 &&
|
|
163
|
+
list.pinnedCount < list.visibleOptions.length;
|
|
164
|
+
return (
|
|
165
|
+
<React.Fragment key={option.value}>
|
|
166
|
+
{isSectionBoundary && <CommandSeparator />}
|
|
167
|
+
<CommandItem
|
|
168
|
+
value={option.value}
|
|
169
|
+
disabled={option.disabled}
|
|
170
|
+
onSelect={() => list.toggle(option.value)}
|
|
171
|
+
className="flex items-center gap-1.5 cursor-pointer"
|
|
172
|
+
>
|
|
173
|
+
{dataType === undefined ? (
|
|
174
|
+
<span>{option.label}</span>
|
|
175
|
+
) : (
|
|
176
|
+
<ColumnName
|
|
177
|
+
columnName={option.label}
|
|
178
|
+
dataType={dataType}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
{!option.disabled && (
|
|
182
|
+
<span
|
|
183
|
+
className={cn(
|
|
184
|
+
"ml-auto",
|
|
185
|
+
hidden ? "text-primary" : "text-muted-foreground",
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
{hidden ? (
|
|
189
|
+
<EyeOffIcon className="w-3 h-3" />
|
|
190
|
+
) : (
|
|
191
|
+
<EyeIcon className="w-3 h-3" />
|
|
192
|
+
)}
|
|
193
|
+
</span>
|
|
194
|
+
)}
|
|
195
|
+
</CommandItem>
|
|
196
|
+
</React.Fragment>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
</CommandList>
|
|
200
|
+
</Command>
|
|
201
|
+
</PopoverContent>
|
|
202
|
+
</Popover>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
@@ -356,6 +356,7 @@ const DataTableInternal = <TData,>({
|
|
|
356
356
|
className={cn(className || "rounded-md border overflow-hidden")}
|
|
357
357
|
>
|
|
358
358
|
<TableTopBar
|
|
359
|
+
table={table}
|
|
359
360
|
enableSearch={enableSearch}
|
|
360
361
|
searchQuery={searchQuery}
|
|
361
362
|
onSearchQueryChange={onSearchQueryChange}
|
|
@@ -413,7 +414,6 @@ const DataTableInternal = <TData,>({
|
|
|
413
414
|
getRowIds={getRowIds}
|
|
414
415
|
showPageSizeSelector={showPageSizeSelector}
|
|
415
416
|
tableLoading={reloading}
|
|
416
|
-
togglePanel={togglePanel}
|
|
417
417
|
/>
|
|
418
418
|
</div>
|
|
419
419
|
</CellSelectionProvider>
|
|
@@ -7,6 +7,8 @@ import { useMemo, useState } from "react";
|
|
|
7
7
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
8
8
|
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
9
9
|
import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
|
|
10
|
+
import type { Option } from "@/components/ui/select-core";
|
|
11
|
+
import { useSelectList } from "@/components/ui/select-core";
|
|
10
12
|
import { Logger } from "@/utils/Logger";
|
|
11
13
|
import { Sets } from "@/utils/sets";
|
|
12
14
|
import { smartMatch } from "@/utils/smartMatch";
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
23
25
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
24
26
|
import { SentinelCell } from "./sentinel-cell";
|
|
25
27
|
import { detectSentinel, stringifyUnknownValue } from "./utils";
|
|
26
|
-
import { CompactChipRow } from "
|
|
28
|
+
import { CompactChipRow } from "@/components/ui/value-chips";
|
|
27
29
|
|
|
28
30
|
const TOP_K_ROWS = 30;
|
|
29
31
|
|
|
@@ -101,8 +103,6 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
101
103
|
onChange,
|
|
102
104
|
creatable = false,
|
|
103
105
|
}: FilterByValuesListProps<TData, TValue>) => {
|
|
104
|
-
const [query, setQuery] = useState<string>("");
|
|
105
|
-
|
|
106
106
|
const { data, isPending, error } = useAsyncData(async () => {
|
|
107
107
|
if (!calculateTopKRows) {
|
|
108
108
|
return null;
|
|
@@ -111,27 +111,49 @@ export const FilterByValuesList = <TData, TValue>({
|
|
|
111
111
|
return res.data;
|
|
112
112
|
}, [calculateTopKRows, column.id]);
|
|
113
113
|
|
|
114
|
-
const
|
|
114
|
+
const options = useMemo<Array<Option<unknown>>>(() => {
|
|
115
115
|
if (!data) {
|
|
116
116
|
return [];
|
|
117
117
|
}
|
|
118
118
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
smartMatch(query, str) ||
|
|
127
|
-
str.toLowerCase().includes(query.toLowerCase())
|
|
128
|
-
);
|
|
129
|
-
});
|
|
119
|
+
return data
|
|
120
|
+
.filter(([value]) => value !== undefined)
|
|
121
|
+
.map(([value, count]) => ({
|
|
122
|
+
value,
|
|
123
|
+
label: String(value),
|
|
124
|
+
data: { count },
|
|
125
|
+
}));
|
|
130
126
|
} catch (error_) {
|
|
131
|
-
Logger.error("Error
|
|
127
|
+
Logger.error("Error building filter options", error_);
|
|
132
128
|
return [];
|
|
133
129
|
}
|
|
134
|
-
}, [data
|
|
130
|
+
}, [data]);
|
|
131
|
+
|
|
132
|
+
const list = useSelectList<unknown>({
|
|
133
|
+
options,
|
|
134
|
+
value: [...chosenValues],
|
|
135
|
+
onChange: (next) => onChange(next as unknown[]),
|
|
136
|
+
multiple: true,
|
|
137
|
+
filterFn: (label, q) =>
|
|
138
|
+
smartMatch(q, label) || label.toLowerCase().includes(q.toLowerCase())
|
|
139
|
+
? 1
|
|
140
|
+
: 0,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const query = list.searchQuery;
|
|
144
|
+
const setQuery = list.setSearchQuery;
|
|
145
|
+
|
|
146
|
+
const filteredData = useMemo<Array<[unknown, number | undefined]>>(
|
|
147
|
+
() =>
|
|
148
|
+
list.visibleOptions.map(
|
|
149
|
+
(o) =>
|
|
150
|
+
[o.value, (o.data as { count: number }).count] as [
|
|
151
|
+
unknown,
|
|
152
|
+
number | undefined,
|
|
153
|
+
],
|
|
154
|
+
),
|
|
155
|
+
[list.visibleOptions],
|
|
156
|
+
);
|
|
135
157
|
|
|
136
158
|
// Surface chosen values that aren't in the top-K so they stay visible/uncheckable.
|
|
137
159
|
// Count is undefined for these rows; the cell renders an em-dash.
|
|
@@ -21,7 +21,7 @@ import { AddFilterButton } from "./add-filter-button";
|
|
|
21
21
|
import { FilterPillEditor } from "./filter-pill-editor";
|
|
22
22
|
import { type ColumnFilterValue, formatValue, type Snapshot } from "./filters";
|
|
23
23
|
import { extractTimezone } from "./types";
|
|
24
|
-
import { ChipWithComma, CompactChipRow } from "
|
|
24
|
+
import { ChipWithComma, CompactChipRow } from "@/components/ui/value-chips";
|
|
25
25
|
|
|
26
26
|
interface Props<TData> {
|
|
27
27
|
filters: ColumnFiltersState | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import type { Cell } from "@tanstack/react-table";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { applyHoverTemplate, computeCellTooltipContent } from "../content";
|
|
5
|
+
|
|
6
|
+
function fakeCell(columnId: string, value: unknown, hoverTitle?: string) {
|
|
7
|
+
return {
|
|
8
|
+
column: { id: columnId },
|
|
9
|
+
getValue: () => value,
|
|
10
|
+
getHoverTitle: () => hoverTitle,
|
|
11
|
+
} as unknown as Cell<unknown, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("applyHoverTemplate", () => {
|
|
15
|
+
it("substitutes column placeholders", () => {
|
|
16
|
+
const cells = [fakeCell("first", "Michael"), fakeCell("last", "Scott")];
|
|
17
|
+
expect(applyHoverTemplate("{{first}} {{last}}", cells)).toBe(
|
|
18
|
+
"Michael Scott",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders nulls as empty strings", () => {
|
|
23
|
+
const cells = [fakeCell("a", null)];
|
|
24
|
+
expect(applyHoverTemplate("[{{a}}]", cells)).toBe("[]");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("leaves unknown placeholders intact", () => {
|
|
28
|
+
expect(applyHoverTemplate("{{missing}}", [fakeCell("a", 1)])).toBe(
|
|
29
|
+
"{{missing}}",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("computeCellTooltipContent", () => {
|
|
35
|
+
it("prefers cell-level hover title", () => {
|
|
36
|
+
const cell = fakeCell("a", 1, "cell text");
|
|
37
|
+
expect(computeCellTooltipContent(cell, "{{a}}")).toBe("cell text");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("falls back to row template when no cell title", () => {
|
|
41
|
+
const cell = {
|
|
42
|
+
column: { id: "first" },
|
|
43
|
+
getValue: () => "X",
|
|
44
|
+
getHoverTitle: () => undefined,
|
|
45
|
+
row: {
|
|
46
|
+
getVisibleCells: () => [
|
|
47
|
+
fakeCell("first", "Jim"),
|
|
48
|
+
fakeCell("last", "Halpert"),
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
} as unknown as Cell<unknown, unknown>;
|
|
52
|
+
expect(computeCellTooltipContent(cell, "{{first}} {{last}}")).toBe(
|
|
53
|
+
"Jim Halpert",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns undefined with no title and no template", () => {
|
|
58
|
+
expect(computeCellTooltipContent(fakeCell("a", 1), null)).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import type { Cell, RowData } from "@tanstack/react-table";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { stringifyUnknownValue } from "../utils";
|
|
5
|
+
|
|
6
|
+
export function applyHoverTemplate<TData extends RowData>(
|
|
7
|
+
template: string,
|
|
8
|
+
cells: Cell<TData, unknown>[],
|
|
9
|
+
): string {
|
|
10
|
+
const variableRegex = /{{(\w+)}}/g;
|
|
11
|
+
const idToValue = new Map<string, string>();
|
|
12
|
+
for (const c of cells) {
|
|
13
|
+
const s = stringifyUnknownValue({
|
|
14
|
+
value: c.getValue(),
|
|
15
|
+
nullAsEmptyString: true,
|
|
16
|
+
});
|
|
17
|
+
idToValue.set(c.column.id, s);
|
|
18
|
+
}
|
|
19
|
+
return template.replaceAll(variableRegex, (_substr, varName: string) => {
|
|
20
|
+
const val = idToValue.get(varName);
|
|
21
|
+
return val === undefined ? `{{${varName}}}` : val;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the tooltip content for a hovered cell.
|
|
27
|
+
*
|
|
28
|
+
* Cell-level (callable `hover_template`) takes precedence; otherwise the
|
|
29
|
+
* row-level string template is rendered against the row's visible cells.
|
|
30
|
+
* Returns `undefined` when there is nothing to show.
|
|
31
|
+
*/
|
|
32
|
+
export function computeCellTooltipContent<TData extends RowData>(
|
|
33
|
+
cell: Cell<TData, unknown>,
|
|
34
|
+
hoverTemplate: string | null,
|
|
35
|
+
): ReactNode {
|
|
36
|
+
const cellTitle = cell.getHoverTitle?.();
|
|
37
|
+
if (cellTitle != null && cellTitle !== "") {
|
|
38
|
+
return cellTitle;
|
|
39
|
+
}
|
|
40
|
+
if (hoverTemplate) {
|
|
41
|
+
return applyHoverTemplate(hoverTemplate, cell.row.getVisibleCells());
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import {
|
|
3
|
+
TooltipContent,
|
|
4
|
+
TooltipPortal,
|
|
5
|
+
TooltipRoot,
|
|
6
|
+
TooltipTrigger,
|
|
7
|
+
} from "@/components/ui/tooltip";
|
|
8
|
+
import type { HoverTooltipState } from "./use-table-hover-tooltip";
|
|
9
|
+
|
|
10
|
+
interface HoverTooltipProps {
|
|
11
|
+
state: HoverTooltipState | null;
|
|
12
|
+
contentId: string;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A single radix tooltip whose anchor is repositioned to the hovered cell.
|
|
18
|
+
* Rendering one instance per table (instead of one per cell) keeps the cost
|
|
19
|
+
* constant regardless of how many cells are on screen.
|
|
20
|
+
*/
|
|
21
|
+
export const HoverTooltip = ({
|
|
22
|
+
state,
|
|
23
|
+
contentId,
|
|
24
|
+
onClose,
|
|
25
|
+
}: HoverTooltipProps) => {
|
|
26
|
+
return (
|
|
27
|
+
<TooltipRoot
|
|
28
|
+
open={state != null}
|
|
29
|
+
onOpenChange={(open) => {
|
|
30
|
+
if (!open) {
|
|
31
|
+
onClose();
|
|
32
|
+
}
|
|
33
|
+
}}
|
|
34
|
+
delayDuration={0}
|
|
35
|
+
disableHoverableContent={true}
|
|
36
|
+
>
|
|
37
|
+
<TooltipTrigger asChild={true}>
|
|
38
|
+
<div
|
|
39
|
+
aria-hidden={true}
|
|
40
|
+
style={{
|
|
41
|
+
position: "fixed",
|
|
42
|
+
top: state?.rect.top ?? 0,
|
|
43
|
+
left: state?.rect.left ?? 0,
|
|
44
|
+
width: state?.rect.width ?? 0,
|
|
45
|
+
height: state?.rect.height ?? 0,
|
|
46
|
+
pointerEvents: "none",
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
</TooltipTrigger>
|
|
50
|
+
<TooltipPortal>
|
|
51
|
+
<TooltipContent id={contentId}>{state?.content}</TooltipContent>
|
|
52
|
+
</TooltipPortal>
|
|
53
|
+
</TooltipRoot>
|
|
54
|
+
);
|
|
55
|
+
};
|