@marimo-team/islands 0.23.4-dev18 → 0.23.4-dev21
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-BS9U2zaC.js → ConnectedDataExplorerComponent-CWU3Az6F.js} +1 -1
- package/dist/main.js +5 -5
- package/dist/{react-vega-jy3CfYys.js → react-vega-B-rkEqtS.js} +1 -1
- package/dist/{react-vega-C2Rtgjb4.js → react-vega-k9ODWPlI.js} +1085 -1728
- package/dist/{reveal-component-CHxYqj2C.js → reveal-component-DK-5_Ei4.js} +1 -1
- package/dist/{slide-form--t4NFxYc.js → slide-form-CYU9AOO4.js} +567 -537
- package/dist/{vega-component-CjMUUeEZ.js → vega-component-BnCQmtxw.js} +1 -1
- package/package.json +2 -2
- package/src/components/data-table/__tests__/columns.test.tsx +40 -10
- package/src/components/data-table/column-header.tsx +27 -4
- package/src/components/data-table/columns.tsx +12 -1
- package/src/components/data-table/range-focus/__tests__/use-cell-range-selection.test.ts +135 -54
- package/src/components/data-table/range-focus/use-cell-range-selection.ts +36 -4
|
@@ -15,7 +15,7 @@ import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats
|
|
|
15
15
|
import { n as formats } from "./vega-loader.browser-3_z8GoFC.js";
|
|
16
16
|
import { a as getContainerWidth, n as vegaLoadData, s as tooltipHandler } from "./loader-BvW0-YWZ.js";
|
|
17
17
|
import { t as useAsyncData } from "./useAsyncData-CKYzhCis.js";
|
|
18
|
-
import { t as j } from "./react-vega-
|
|
18
|
+
import { t as j } from "./react-vega-k9ODWPlI.js";
|
|
19
19
|
import "./defaultLocale-BpsHxBd7.js";
|
|
20
20
|
import "./defaultLocale-DoeErsX2.js";
|
|
21
21
|
import { t as useDeepCompareMemoize } from "./useDeepCompareMemoize-je76AJS_.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marimo-team/islands",
|
|
3
|
-
"version": "0.23.4-
|
|
3
|
+
"version": "0.23.4-dev21",
|
|
4
4
|
"main": "dist/main.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"typescript-memoize": "^1.1.1",
|
|
156
156
|
"use-acp": "0.2.6",
|
|
157
157
|
"use-resize-observer": "^9.1.0",
|
|
158
|
-
"vega-lite": "6.
|
|
158
|
+
"vega-lite": "6.4.2",
|
|
159
159
|
"vega-loader": "^5.1.0",
|
|
160
160
|
"vega-parser": "^7.1.0",
|
|
161
161
|
"vega-tooltip": "^1.1.0",
|
|
@@ -312,7 +312,7 @@ describe("generateColumns", () => {
|
|
|
312
312
|
expect(cell?.props.className).toContain("center");
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
-
it("should
|
|
315
|
+
it("should align column headers to match textJustifyColumns", () => {
|
|
316
316
|
const columns = generateColumns({
|
|
317
317
|
rowHeaders: [],
|
|
318
318
|
selection: null,
|
|
@@ -330,7 +330,8 @@ describe("generateColumns", () => {
|
|
|
330
330
|
columnDef: { meta: col.meta },
|
|
331
331
|
});
|
|
332
332
|
|
|
333
|
-
//
|
|
333
|
+
// Right-justified column: outer summary wrapper aligns to end, and the
|
|
334
|
+
// header row uses flex-row-reverse so the title sits at the right edge.
|
|
334
335
|
const { container: rightContainer } = render(
|
|
335
336
|
<TooltipProvider>
|
|
336
337
|
{/* oxlint-disable-next-line typescript/no-explicit-any */}
|
|
@@ -345,12 +346,11 @@ describe("generateColumns", () => {
|
|
|
345
346
|
"[data-testid='data-table-column-menu-button']",
|
|
346
347
|
),
|
|
347
348
|
).toBeTruthy();
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
expect(rightWrapper?.className).not.toContain("items-end");
|
|
351
|
-
expect(rightWrapper?.className).not.toContain("flex-row-reverse");
|
|
349
|
+
expect(rightContainer.firstElementChild?.className).toContain("items-end");
|
|
350
|
+
expect(rightContainer.querySelector(".flex-row-reverse")).toBeTruthy();
|
|
352
351
|
|
|
353
|
-
//
|
|
352
|
+
// Center-justified column: outer summary wrapper centers; header row
|
|
353
|
+
// keeps natural order.
|
|
354
354
|
const { container: centerContainer } = render(
|
|
355
355
|
<TooltipProvider>
|
|
356
356
|
{/* oxlint-disable-next-line typescript/no-explicit-any */}
|
|
@@ -365,9 +365,39 @@ describe("generateColumns", () => {
|
|
|
365
365
|
"[data-testid='data-table-column-menu-button']",
|
|
366
366
|
),
|
|
367
367
|
).toBeTruthy();
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
expect(centerContainer.firstElementChild?.className).toContain(
|
|
369
|
+
"items-center",
|
|
370
|
+
);
|
|
371
|
+
expect(centerContainer.querySelector(".flex-row-reverse")).toBeNull();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("should not auto-align numeric column headers without explicit override", () => {
|
|
375
|
+
const columns = generateColumns({
|
|
376
|
+
rowHeaders: [],
|
|
377
|
+
selection: null,
|
|
378
|
+
fieldTypes,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const mockColumn = (col: (typeof columns)[number]) => ({
|
|
382
|
+
id: col.id,
|
|
383
|
+
getCanSort: () => true,
|
|
384
|
+
getCanFilter: () => false,
|
|
385
|
+
getIsSorted: () => false,
|
|
386
|
+
getSortIndex: () => -1,
|
|
387
|
+
getFilterValue: () => undefined,
|
|
388
|
+
columnDef: { meta: col.meta },
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// "age" is numeric: cells auto right-align, but the header stays
|
|
392
|
+
// left-aligned unless the user explicitly opts in via text_justify_columns.
|
|
393
|
+
const { container } = render(
|
|
394
|
+
<TooltipProvider>
|
|
395
|
+
{/* oxlint-disable-next-line typescript/no-explicit-any */}
|
|
396
|
+
{(columns[1].header as any)({ column: mockColumn(columns[1]) })}
|
|
397
|
+
</TooltipProvider>,
|
|
398
|
+
);
|
|
399
|
+
expect(container.firstElementChild?.className).not.toContain("items-end");
|
|
400
|
+
expect(container.querySelector(".flex-row-reverse")).toBeNull();
|
|
371
401
|
});
|
|
372
402
|
|
|
373
403
|
it("should cycle sort button through asc, desc, and clear on clicks", () => {
|
|
@@ -66,6 +66,7 @@ interface DataTableColumnHeaderProps<
|
|
|
66
66
|
column: Column<TData, TValue>;
|
|
67
67
|
header: React.ReactNode;
|
|
68
68
|
subheader?: React.ReactNode;
|
|
69
|
+
justify?: "left" | "center" | "right";
|
|
69
70
|
calculateTopKRows?: CalculateTopKRows;
|
|
70
71
|
table?: Table<TData>;
|
|
71
72
|
}
|
|
@@ -74,6 +75,7 @@ export const DataTableColumnHeader = <TData, TValue>({
|
|
|
74
75
|
column,
|
|
75
76
|
header,
|
|
76
77
|
subheader,
|
|
78
|
+
justify,
|
|
77
79
|
className,
|
|
78
80
|
calculateTopKRows,
|
|
79
81
|
table,
|
|
@@ -89,7 +91,13 @@ export const DataTableColumnHeader = <TData, TValue>({
|
|
|
89
91
|
// No sorting or filtering
|
|
90
92
|
if (!column.getCanSort() && !column.getCanFilter()) {
|
|
91
93
|
return (
|
|
92
|
-
<div
|
|
94
|
+
<div
|
|
95
|
+
className={cn(
|
|
96
|
+
justify === "center" && "text-center",
|
|
97
|
+
justify === "right" && "text-right",
|
|
98
|
+
className,
|
|
99
|
+
)}
|
|
100
|
+
>
|
|
93
101
|
{header}
|
|
94
102
|
{subheader}
|
|
95
103
|
</div>
|
|
@@ -103,9 +111,24 @@ export const DataTableColumnHeader = <TData, TValue>({
|
|
|
103
111
|
<div
|
|
104
112
|
className={cn("group flex flex-col my-1 w-full select-none", className)}
|
|
105
113
|
>
|
|
106
|
-
<div
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
<div
|
|
115
|
+
className={cn(
|
|
116
|
+
"flex items-center gap-1",
|
|
117
|
+
justify === "right" && "flex-row-reverse",
|
|
118
|
+
justify === "center" && "mx-auto",
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
{justify === "center" ? (
|
|
122
|
+
<>
|
|
123
|
+
{column.getCanSort() && <SortButton column={column} />}
|
|
124
|
+
<span>{header}</span>
|
|
125
|
+
</>
|
|
126
|
+
) : (
|
|
127
|
+
<>
|
|
128
|
+
<span>{header}</span>
|
|
129
|
+
{column.getCanSort() && <SortButton column={column} />}
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
109
132
|
<DropdownMenu modal={false}>
|
|
110
133
|
<DropdownMenuTrigger asChild={true}>
|
|
111
134
|
<button
|
|
@@ -197,9 +197,17 @@ export function generateColumns<T>({
|
|
|
197
197
|
const stats = chartSpecModel?.getColumnStats(key);
|
|
198
198
|
const dtype = column.columnDef.meta?.dtype;
|
|
199
199
|
const headerTitle = headerTooltip?.[key];
|
|
200
|
+
const headerJustify = textJustifyColumns?.[key];
|
|
201
|
+
|
|
200
202
|
const dtypeHeader =
|
|
201
203
|
showDataTypes && dtype ? (
|
|
202
|
-
<div
|
|
204
|
+
<div
|
|
205
|
+
className={cn(
|
|
206
|
+
"flex flex-row gap-1",
|
|
207
|
+
headerJustify === "center" && "justify-center",
|
|
208
|
+
headerJustify === "right" && "justify-end",
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
203
211
|
<span className="text-xs text-muted-foreground">{dtype}</span>
|
|
204
212
|
{stats && typeof stats.nulls === "number" && stats.nulls > 0 && (
|
|
205
213
|
<span className="text-xs text-muted-foreground">
|
|
@@ -233,6 +241,7 @@ export function generateColumns<T>({
|
|
|
233
241
|
header={headerWithTooltip}
|
|
234
242
|
subheader={dtypeHeader}
|
|
235
243
|
column={column}
|
|
244
|
+
justify={headerJustify}
|
|
236
245
|
calculateTopKRows={calculateTopKRows}
|
|
237
246
|
table={table}
|
|
238
247
|
/>
|
|
@@ -247,6 +256,8 @@ export function generateColumns<T>({
|
|
|
247
256
|
<div
|
|
248
257
|
className={cn(
|
|
249
258
|
"flex flex-col h-full pt-0.5 pb-3 justify-between items-start",
|
|
259
|
+
headerJustify === "center" && "items-center",
|
|
260
|
+
headerJustify === "right" && "items-end",
|
|
250
261
|
)}
|
|
251
262
|
>
|
|
252
263
|
{dataTableColumnHeader}
|
|
@@ -1,117 +1,198 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
4
|
import { isInteractiveTarget } from "../use-cell-range-selection";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Dispatch a real `mousedown` from `target` (going through real DOM event
|
|
8
|
+
* dispatch so `composedPath()` traverses any open shadow roots) and call
|
|
9
|
+
* `isInteractiveTarget` from inside the listener, where the event's target
|
|
10
|
+
* and composed path are still live.
|
|
11
|
+
*/
|
|
12
|
+
function isInteractive(target: Element, cell: Element): boolean {
|
|
13
|
+
let result: boolean | undefined;
|
|
14
|
+
const handler = (event: Event) => {
|
|
15
|
+
const path = event.composedPath();
|
|
16
|
+
result = isInteractiveTarget({
|
|
17
|
+
target: event.target,
|
|
18
|
+
currentTarget: cell,
|
|
19
|
+
nativeEvent: {
|
|
20
|
+
composedPath: () => path,
|
|
21
|
+
},
|
|
22
|
+
} as unknown as React.MouseEvent);
|
|
23
|
+
};
|
|
24
|
+
cell.addEventListener("mousedown", handler);
|
|
25
|
+
target.dispatchEvent(
|
|
26
|
+
new MouseEvent("mousedown", { bubbles: true, composed: true }),
|
|
27
|
+
);
|
|
28
|
+
cell.removeEventListener("mousedown", handler);
|
|
29
|
+
if (result === undefined) {
|
|
30
|
+
throw new Error("mousedown did not bubble to the cell");
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let mounted: HTMLElement[] = [];
|
|
36
|
+
|
|
37
|
+
function makeCell(): HTMLTableCellElement {
|
|
38
|
+
const table = document.createElement("table");
|
|
39
|
+
const tbody = document.createElement("tbody");
|
|
40
|
+
const row = document.createElement("tr");
|
|
41
|
+
const cell = document.createElement("td");
|
|
42
|
+
row.append(cell);
|
|
43
|
+
tbody.append(row);
|
|
44
|
+
table.append(tbody);
|
|
45
|
+
document.body.append(table);
|
|
46
|
+
mounted.push(table);
|
|
47
|
+
return cell;
|
|
11
48
|
}
|
|
12
49
|
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
for (const el of mounted) {
|
|
52
|
+
el.remove();
|
|
53
|
+
}
|
|
54
|
+
mounted = [];
|
|
55
|
+
});
|
|
56
|
+
|
|
13
57
|
describe("isInteractiveTarget", () => {
|
|
14
58
|
it("returns false when target is the cell itself", () => {
|
|
15
|
-
const cell =
|
|
16
|
-
expect(
|
|
59
|
+
const cell = makeCell();
|
|
60
|
+
expect(isInteractive(cell, cell)).toBe(false);
|
|
17
61
|
});
|
|
18
62
|
|
|
19
63
|
it("returns false when clicking plain text inside a cell", () => {
|
|
20
|
-
const cell =
|
|
64
|
+
const cell = makeCell();
|
|
21
65
|
const span = document.createElement("span");
|
|
22
66
|
cell.append(span);
|
|
23
|
-
expect(
|
|
67
|
+
expect(isInteractive(span, cell)).toBe(false);
|
|
24
68
|
});
|
|
25
69
|
|
|
26
70
|
it.each(["input", "button", "select", "textarea"])(
|
|
27
71
|
"returns true when clicking a <%s>",
|
|
28
72
|
(tag) => {
|
|
29
|
-
const cell =
|
|
73
|
+
const cell = makeCell();
|
|
30
74
|
const el = document.createElement(tag);
|
|
31
75
|
cell.append(el);
|
|
32
|
-
expect(
|
|
76
|
+
expect(isInteractive(el, cell)).toBe(true);
|
|
33
77
|
},
|
|
34
78
|
);
|
|
35
79
|
|
|
36
80
|
it("returns true when clicking an <a> link", () => {
|
|
37
|
-
const cell =
|
|
81
|
+
const cell = makeCell();
|
|
38
82
|
const a = document.createElement("a");
|
|
39
83
|
a.href = "#";
|
|
40
84
|
cell.append(a);
|
|
41
|
-
expect(
|
|
85
|
+
expect(isInteractive(a, cell)).toBe(true);
|
|
42
86
|
});
|
|
43
87
|
|
|
44
88
|
it("returns true when clicking a <label>", () => {
|
|
45
|
-
const cell =
|
|
89
|
+
const cell = makeCell();
|
|
46
90
|
const label = document.createElement("label");
|
|
47
91
|
cell.append(label);
|
|
48
|
-
expect(
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns true for element with role="checkbox"', () => {
|
|
52
|
-
const cell = document.createElement("td");
|
|
53
|
-
const div = document.createElement("div");
|
|
54
|
-
div.setAttribute("role", "checkbox");
|
|
55
|
-
cell.append(div);
|
|
56
|
-
expect(isInteractiveTarget(createMouseEvent(div, cell))).toBe(true);
|
|
92
|
+
expect(isInteractive(label, cell)).toBe(true);
|
|
57
93
|
});
|
|
58
94
|
|
|
59
|
-
it(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
it.each(["checkbox", "button"])(
|
|
96
|
+
'returns true for element with role="%s"',
|
|
97
|
+
(role) => {
|
|
98
|
+
const cell = makeCell();
|
|
99
|
+
const div = document.createElement("div");
|
|
100
|
+
div.setAttribute("role", role);
|
|
101
|
+
cell.append(div);
|
|
102
|
+
expect(isInteractive(div, cell)).toBe(true);
|
|
103
|
+
},
|
|
104
|
+
);
|
|
66
105
|
|
|
67
106
|
it('returns true for contenteditable="true"', () => {
|
|
68
|
-
const cell =
|
|
107
|
+
const cell = makeCell();
|
|
69
108
|
const div = document.createElement("div");
|
|
70
109
|
div.setAttribute("contenteditable", "true");
|
|
71
110
|
cell.append(div);
|
|
72
|
-
expect(
|
|
111
|
+
expect(isInteractive(div, cell)).toBe(true);
|
|
73
112
|
});
|
|
74
113
|
|
|
75
114
|
it("returns true when clicking a child nested inside an interactive element", () => {
|
|
76
|
-
const cell =
|
|
115
|
+
const cell = makeCell();
|
|
77
116
|
const button = document.createElement("button");
|
|
78
117
|
const icon = document.createElement("span");
|
|
79
118
|
button.append(icon);
|
|
80
119
|
cell.append(button);
|
|
81
|
-
expect(
|
|
120
|
+
expect(isInteractive(icon, cell)).toBe(true);
|
|
82
121
|
});
|
|
83
122
|
|
|
84
|
-
it("returns true when clicking inside a marimo-ui-element", () => {
|
|
85
|
-
const cell =
|
|
123
|
+
it("returns true when clicking inside a marimo-ui-element wrapping a real widget", () => {
|
|
124
|
+
const cell = makeCell();
|
|
86
125
|
const marimoEl = document.createElement("marimo-ui-element");
|
|
87
|
-
const
|
|
88
|
-
marimoEl.append(
|
|
126
|
+
const widget = document.createElement("marimo-slider");
|
|
127
|
+
marimoEl.append(widget);
|
|
89
128
|
cell.append(marimoEl);
|
|
90
|
-
expect(
|
|
129
|
+
expect(isInteractive(widget, cell)).toBe(true);
|
|
130
|
+
expect(isInteractive(marimoEl, cell)).toBe(true);
|
|
91
131
|
});
|
|
92
132
|
|
|
93
|
-
it("
|
|
94
|
-
|
|
133
|
+
it.each(["marimo-lazy", "marimo-routes"])(
|
|
134
|
+
"returns false when clicking inside a passive content-wrapper UIElement (%s)",
|
|
135
|
+
(tag) => {
|
|
136
|
+
const cell = makeCell();
|
|
137
|
+
const marimoEl = document.createElement("marimo-ui-element");
|
|
138
|
+
const wrapper = document.createElement(tag);
|
|
139
|
+
const inner = document.createElement("div");
|
|
140
|
+
wrapper.append(inner);
|
|
141
|
+
marimoEl.append(wrapper);
|
|
142
|
+
cell.append(marimoEl);
|
|
143
|
+
expect(isInteractive(inner, cell)).toBe(false);
|
|
144
|
+
expect(isInteractive(wrapper, cell)).toBe(false);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
it("returns false when clicking plain content rendered through mo.lazy's shadow DOM (#9189)", () => {
|
|
149
|
+
// Reproduces the structure marimo creates for mo.lazy(<plain html>):
|
|
150
|
+
// event.target gets retargeted to <marimo-lazy>, so closest() can't see
|
|
151
|
+
// into the shadow root. composedPath() must be used to confirm there's
|
|
152
|
+
// no genuinely interactive descendant.
|
|
153
|
+
const cell = makeCell();
|
|
95
154
|
const marimoEl = document.createElement("marimo-ui-element");
|
|
155
|
+
const lazy = document.createElement("marimo-lazy");
|
|
156
|
+
marimoEl.append(lazy);
|
|
96
157
|
cell.append(marimoEl);
|
|
97
|
-
|
|
158
|
+
|
|
159
|
+
const shadow = lazy.attachShadow({ mode: "open" });
|
|
160
|
+
const img = document.createElement("img");
|
|
161
|
+
shadow.append(img);
|
|
162
|
+
|
|
163
|
+
expect(isInteractive(img, cell)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns true when clicking an interactive widget rendered inside a content wrapper's shadow DOM", () => {
|
|
167
|
+
// mo.lazy(mo.ui.slider(...)): the slider's <marimo-ui-element> lives
|
|
168
|
+
// inside marimo-lazy's shadow root, so closest() from the retargeted
|
|
169
|
+
// host couldn't see it. composedPath() does.
|
|
170
|
+
const cell = makeCell();
|
|
171
|
+
const outerUi = document.createElement("marimo-ui-element");
|
|
172
|
+
const lazy = document.createElement("marimo-lazy");
|
|
173
|
+
outerUi.append(lazy);
|
|
174
|
+
cell.append(outerUi);
|
|
175
|
+
|
|
176
|
+
const lazyShadow = lazy.attachShadow({ mode: "open" });
|
|
177
|
+
const innerUi = document.createElement("marimo-ui-element");
|
|
178
|
+
const slider = document.createElement("marimo-slider");
|
|
179
|
+
innerUi.append(slider);
|
|
180
|
+
lazyShadow.append(innerUi);
|
|
181
|
+
|
|
182
|
+
const sliderShadow = slider.attachShadow({ mode: "open" });
|
|
183
|
+
const input = document.createElement("input");
|
|
184
|
+
input.type = "range";
|
|
185
|
+
sliderShadow.append(input);
|
|
186
|
+
|
|
187
|
+
expect(isInteractive(input, cell)).toBe(true);
|
|
98
188
|
});
|
|
99
189
|
|
|
100
190
|
it("returns false when clicking a non-interactive div", () => {
|
|
101
|
-
const cell =
|
|
191
|
+
const cell = makeCell();
|
|
102
192
|
const wrapper = document.createElement("div");
|
|
103
193
|
const text = document.createElement("span");
|
|
104
194
|
wrapper.append(text);
|
|
105
195
|
cell.append(wrapper);
|
|
106
|
-
expect(
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("returns false when target is a non-Element (e.g. Text node)", () => {
|
|
110
|
-
const cell = document.createElement("td");
|
|
111
|
-
const textNode = document.createTextNode("hello");
|
|
112
|
-
cell.append(textNode);
|
|
113
|
-
expect(isInteractiveTarget(createMouseEvent(textNode as never, cell))).toBe(
|
|
114
|
-
false,
|
|
115
|
-
);
|
|
196
|
+
expect(isInteractive(text, cell)).toBe(false);
|
|
116
197
|
});
|
|
117
198
|
});
|
|
@@ -130,14 +130,46 @@ export const useCellRangeSelection = <TData>({
|
|
|
130
130
|
const INTERACTIVE_SELECTOR =
|
|
131
131
|
'input, button, select, textarea, a, label, [role="checkbox"], [role="button"], [contenteditable="true"], marimo-ui-element';
|
|
132
132
|
|
|
133
|
+
// `<marimo-ui-element>` wraps every stateful UIElement, but content-wrapper
|
|
134
|
+
// UIElements like `mo.lazy` and `mo.routes` are themselves inert. Clicks on
|
|
135
|
+
// their inner content should still allow cell selection.
|
|
136
|
+
// See https://github.com/marimo-team/marimo/issues/9189.
|
|
137
|
+
const CONTENT_WRAPPER_MARIMO_TAGS: ReadonlySet<string> = new Set([
|
|
138
|
+
"marimo-lazy",
|
|
139
|
+
"marimo-routes",
|
|
140
|
+
]);
|
|
141
|
+
|
|
133
142
|
/**
|
|
134
143
|
* Skip cell selection when the click target is inside an interactive element
|
|
135
144
|
* (e.g. a checkbox or button rendered as rich cell content).
|
|
145
|
+
*
|
|
146
|
+
* Walks `composedPath()` so we can see through Shadow DOM boundaries used by
|
|
147
|
+
* marimo plugins. Without this, `event.target` is retargeted to the outermost
|
|
148
|
+
* shadow host (e.g. `<marimo-lazy>`), hiding any genuinely interactive
|
|
149
|
+
* descendants rendered inside the shadow tree.
|
|
136
150
|
*/
|
|
137
151
|
export function isInteractiveTarget(e: React.MouseEvent): boolean {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
const path: readonly EventTarget[] =
|
|
153
|
+
typeof e.nativeEvent?.composedPath === "function"
|
|
154
|
+
? e.nativeEvent.composedPath()
|
|
155
|
+
: [e.target];
|
|
156
|
+
|
|
157
|
+
for (const node of path) {
|
|
158
|
+
if (node === e.currentTarget) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (!(node instanceof Element) || !node.matches(INTERACTIVE_SELECTOR)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// A `<marimo-ui-element>` directly wrapping a passive content-wrapper is
|
|
165
|
+
// inert; keep walking to find a real interactive ancestor (if any).
|
|
166
|
+
if (node.localName === "marimo-ui-element") {
|
|
167
|
+
const inner = node.firstElementChild;
|
|
168
|
+
if (inner && CONTENT_WRAPPER_MARIMO_TAGS.has(inner.localName)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
141
173
|
}
|
|
142
|
-
return
|
|
174
|
+
return false;
|
|
143
175
|
}
|