@marimo-team/islands 0.23.14-dev23 → 0.23.14-dev25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.14-dev23",
3
+ "version": "0.23.14-dev25",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -45,7 +45,7 @@
45
45
  "@dnd-kit/utilities": "^3.2.2",
46
46
  "@emotion/cache": "^11.14.0",
47
47
  "@emotion/react": "^11.14.0",
48
- "@glideapps/glide-data-grid": "6.0.4-alpha9",
48
+ "@glideapps/glide-data-grid": "6.0.4-alpha24",
49
49
  "@hookform/resolvers": "^5.2.2",
50
50
  "@img-comparison-slider/react": "^8.0.2",
51
51
  "@internationalized/date": "^3.10.1",
@@ -138,3 +138,17 @@
138
138
  display: inline-block;
139
139
  padding: 2px;
140
140
  }
141
+
142
+ /* Rename symbol popup (F2). The upstream popup hardcodes white inline styles;
143
+ override with theme tokens. */
144
+ .cm-rename-popup {
145
+ background: var(--popover) !important;
146
+ border-color: var(--border) !important;
147
+ color: var(--popover-foreground) !important;
148
+ }
149
+
150
+ .cm-rename-popup input {
151
+ background: var(--background) !important;
152
+ border-color: var(--border) !important;
153
+ color: var(--foreground) !important;
154
+ }
@@ -0,0 +1,187 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import React from "react";
4
+ import { act, render, screen, waitFor } from "@testing-library/react";
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ import { GlideDataEditor } from "../glide-data-editor";
8
+
9
+ const capturedPortalRef = vi.hoisted(() => ({
10
+ ref: undefined as React.RefObject<HTMLElement> | undefined,
11
+ }));
12
+
13
+ vi.mock("@glideapps/glide-data-grid", async () => {
14
+ const React = await import("react");
15
+ return {
16
+ default: React.forwardRef(function MockDataEditor(
17
+ props: { portalElementRef?: React.RefObject<HTMLElement> },
18
+ _ref: React.Ref<HTMLDivElement>,
19
+ ) {
20
+ capturedPortalRef.ref = props.portalElementRef;
21
+ return <div data-testid="mock-data-editor" />;
22
+ }),
23
+ CompactSelection: {
24
+ empty: () => ({}),
25
+ },
26
+ GridCellKind: {
27
+ Text: "text",
28
+ Number: "number",
29
+ Boolean: "boolean",
30
+ },
31
+ GridColumnIcon: {
32
+ ProtectedColumnOverlay: "protected",
33
+ },
34
+ };
35
+ });
36
+
37
+ vi.mock("@/theme/useTheme", () => ({
38
+ useTheme: () => ({ theme: "light" }),
39
+ }));
40
+
41
+ const editorProps = {
42
+ data: [{ name: "alice" }],
43
+ setData: vi.fn(),
44
+ columnFields: new Map([["name", "string"]]) as Map<string, "string">,
45
+ setColumnFields: vi.fn(),
46
+ editableColumns: "all" as const,
47
+ edits: [],
48
+ onAddEdits: vi.fn(),
49
+ onAddRows: vi.fn(),
50
+ onDeleteRows: vi.fn(),
51
+ onRenameColumn: vi.fn(),
52
+ onDeleteColumn: vi.fn(),
53
+ onAddColumn: vi.fn(),
54
+ };
55
+
56
+ describe("GlideDataEditor portal", () => {
57
+ let fullscreenElement: Element | null;
58
+
59
+ beforeEach(() => {
60
+ fullscreenElement = null;
61
+ Object.defineProperty(document, "fullscreenElement", {
62
+ get: () => fullscreenElement,
63
+ configurable: true,
64
+ });
65
+ document.body
66
+ .querySelectorAll("[data-testid='glide-data-editor-portal']")
67
+ .forEach((node) => {
68
+ node.remove();
69
+ });
70
+ });
71
+
72
+ it("renders a body-level portal and passes it to DataEditor", async () => {
73
+ const { container } = render(
74
+ <TooltipProvider>
75
+ <GlideDataEditor {...editorProps} />
76
+ </TooltipProvider>,
77
+ );
78
+
79
+ const portal = screen.getByTestId("glide-data-editor-portal");
80
+ expect(portal.parentElement).toBe(document.body);
81
+ expect(container.contains(portal)).toBe(false);
82
+ expect(capturedPortalRef.ref).toBeDefined();
83
+ await waitFor(() => {
84
+ expect(capturedPortalRef.ref?.current).toBe(portal);
85
+ });
86
+ });
87
+
88
+ it("mounts into the fullscreen element when fullscreen is already active", () => {
89
+ const fullscreenContainer = document.createElement("div");
90
+ document.body.appendChild(fullscreenContainer);
91
+ fullscreenElement = fullscreenContainer;
92
+
93
+ render(
94
+ <TooltipProvider>
95
+ <GlideDataEditor {...editorProps} />
96
+ </TooltipProvider>,
97
+ );
98
+
99
+ const portal = fullscreenContainer.querySelector(
100
+ "[data-testid='glide-data-editor-portal']",
101
+ );
102
+ expect(portal).not.toBeNull();
103
+ expect(portal?.parentElement).toBe(fullscreenContainer);
104
+
105
+ fullscreenContainer.remove();
106
+ });
107
+
108
+ it("moves the portal into the fullscreen element while fullscreen is active", async () => {
109
+ render(
110
+ <TooltipProvider>
111
+ <GlideDataEditor {...editorProps} />
112
+ </TooltipProvider>,
113
+ );
114
+
115
+ expect(
116
+ document.body.querySelector("[data-testid='glide-data-editor-portal']"),
117
+ ).not.toBeNull();
118
+
119
+ const fullscreenContainer = document.createElement("div");
120
+ document.body.appendChild(fullscreenContainer);
121
+
122
+ act(() => {
123
+ fullscreenElement = fullscreenContainer;
124
+ document.dispatchEvent(new Event("fullscreenchange"));
125
+ });
126
+
127
+ await waitFor(() => {
128
+ const portal = fullscreenContainer.querySelector(
129
+ "[data-testid='glide-data-editor-portal']",
130
+ );
131
+ expect(portal?.parentElement).toBe(fullscreenContainer);
132
+ });
133
+
134
+ act(() => {
135
+ fullscreenElement = null;
136
+ document.dispatchEvent(new Event("fullscreenchange"));
137
+ });
138
+
139
+ await waitFor(() => {
140
+ const portal = document.body.querySelector(
141
+ "[data-testid='glide-data-editor-portal']",
142
+ );
143
+ expect(portal?.parentElement).toBe(document.body);
144
+ });
145
+
146
+ fullscreenContainer.remove();
147
+ });
148
+
149
+ it("unmounts cleanly while fullscreen is active", async () => {
150
+ const fullscreenContainer = document.createElement("div");
151
+ document.body.appendChild(fullscreenContainer);
152
+
153
+ const { unmount } = render(
154
+ <TooltipProvider>
155
+ <GlideDataEditor {...editorProps} />
156
+ </TooltipProvider>,
157
+ );
158
+
159
+ act(() => {
160
+ fullscreenElement = fullscreenContainer;
161
+ document.dispatchEvent(new Event("fullscreenchange"));
162
+ });
163
+
164
+ await waitFor(() => {
165
+ expect(
166
+ fullscreenContainer.querySelector(
167
+ "[data-testid='glide-data-editor-portal']",
168
+ ),
169
+ ).not.toBeNull();
170
+ });
171
+
172
+ expect(() => {
173
+ act(() => unmount());
174
+ }).not.toThrow();
175
+
176
+ expect(
177
+ fullscreenContainer.querySelector(
178
+ "[data-testid='glide-data-editor-portal']",
179
+ ),
180
+ ).toBeNull();
181
+ expect(
182
+ document.body.querySelector("[data-testid='glide-data-editor-portal']"),
183
+ ).toBeNull();
184
+
185
+ fullscreenContainer.remove();
186
+ });
187
+ });
@@ -56,6 +56,7 @@ import {
56
56
  removeColumn,
57
57
  renameColumn,
58
58
  } from "./data-utils";
59
+ import { GlideDataEditorPortal } from "./glide-portal";
59
60
 
60
61
  interface GlideDataEditorProps<T> {
61
62
  data: T[];
@@ -88,6 +89,7 @@ export const GlideDataEditor = <T,>({
88
89
  }: GlideDataEditorProps<T>) => {
89
90
  const { theme } = useTheme();
90
91
  const dataEditorRef = useRef<DataEditorRef>(null);
92
+ const portalElementRef = useRef<HTMLDivElement>(null);
91
93
 
92
94
  const [menu, setMenu] = useState<{ col: number; bounds: Rectangle }>();
93
95
  const [showSearch, setShowSearch] = useState<boolean>(false);
@@ -613,9 +615,13 @@ export const GlideDataEditor = <T,>({
613
615
 
614
616
  return (
615
617
  <div className="relative w-full min-w-0">
618
+ <GlideDataEditorPortal portalRef={portalElementRef} />
616
619
  <ErrorBoundary>
617
620
  <DataEditor
618
621
  ref={dataEditorRef}
622
+ // Glide types portalElementRef as RefObject<HTMLElement> (non-null); React 19 refs include null.
623
+ // @ts-expect-error glide-data-grid stale RefObject typing
624
+ portalElementRef={portalElementRef}
619
625
  getCellContent={getCellContent}
620
626
  columns={columns}
621
627
  gridSelection={selection}
@@ -0,0 +1,73 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+
6
+ /**
7
+ * Dismiss any open Glide Data Grid overlay by firing Escape on it.
8
+ */
9
+ export function dismissGlideOverlay() {
10
+ const overlay = document.querySelector(
11
+ "[data-testid='glide-data-editor-portal'] .gdg-clip-region",
12
+ );
13
+ if (overlay) {
14
+ overlay.dispatchEvent(
15
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
16
+ );
17
+ }
18
+ }
19
+
20
+ function getGlidePortalContainer(): Element {
21
+ return document.fullscreenElement ?? document.body;
22
+ }
23
+
24
+ function useGlidePortalContainer(): Element {
25
+ const [container, setContainer] = useState<Element>(getGlidePortalContainer);
26
+
27
+ useEffect(() => {
28
+ const handleFullscreenChange = () => {
29
+ if (document.fullscreenElement) {
30
+ setContainer(document.fullscreenElement);
31
+ } else {
32
+ dismissGlideOverlay();
33
+ setContainer(document.body);
34
+ }
35
+ };
36
+
37
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
38
+ // Sync on mount in case fullscreen became active before this editor mounted.
39
+ handleFullscreenChange();
40
+
41
+ return () => {
42
+ document.removeEventListener("fullscreenchange", handleFullscreenChange);
43
+ };
44
+ }, []);
45
+
46
+ return container;
47
+ }
48
+
49
+ /**
50
+ * Per-instance Glide Data Grid overlay portal on document.body.
51
+ * Moves into the active fullscreen element via createPortal so edit overlays stay visible
52
+ */
53
+ export function GlideDataEditorPortal({
54
+ portalRef,
55
+ }: {
56
+ portalRef: React.RefObject<HTMLDivElement | null>;
57
+ }) {
58
+ const container = useGlidePortalContainer();
59
+
60
+ return createPortal(
61
+ <div
62
+ ref={portalRef}
63
+ data-testid="glide-data-editor-portal"
64
+ style={{
65
+ position: "fixed",
66
+ left: 0,
67
+ top: 0,
68
+ zIndex: 9999,
69
+ }}
70
+ />,
71
+ container,
72
+ );
73
+ }
@@ -1,128 +0,0 @@
1
- import { s as __toESM } from "./chunk-BNovOVIE.js";
2
- import { t as require_react } from "./react-DA-nE2FX.js";
3
- import { t as require_react_dom } from "./react-dom-BTJzcVJ9.js";
4
- import { _ as isEditableGridCell, i as makeCSSStyle, n as ThemeContext, t as ClickOutsideContainer, v as isInnerOnlyCell, y as isObjectEditorCallbackResult } from "./click-outside-container-BDd67_1U.js";
5
- import { t as styled_default } from "./dist-D_bzzWBm.js";
6
- var import_react_dom = require_react_dom(), import_react = /* @__PURE__ */ __toESM(require_react(), 1), _exp2 = () => (n) => n.targetX, _exp3 = () => (n) => n.targetY, _exp4 = () => (n) => n.targetWidth, _exp5 = () => (n) => n.targetHeight, _exp6 = () => (n) => n.targetY + 10, _exp7 = () => (n) => Math.max(0, (n.targetHeight - 28) / 2);
7
- const DataGridOverlayEditorStyle = /* @__PURE__ */ styled_default("div")({
8
- name: "DataGridOverlayEditorStyle",
9
- class: "gdg-d19meir1",
10
- propsAsIs: false,
11
- vars: {
12
- "d19meir1-0": [_exp3(), "px"],
13
- "d19meir1-1": [_exp2(), "px"],
14
- "d19meir1-2": [_exp4(), "px"],
15
- "d19meir1-3": [_exp5(), "px"],
16
- "d19meir1-4": [_exp6(), "px"],
17
- "d19meir1-5": [_exp7(), "px"]
18
- }
19
- });
20
- function useRefState() {
21
- let [n, c] = import_react.useState();
22
- return [n ?? void 0, c];
23
- }
24
- function useStayOnScreen() {
25
- let [n, c] = useRefState(), [l, u] = import_react.useState(0), [d, f] = import_react.useState(true);
26
- return import_react.useLayoutEffect(() => {
27
- if (n === void 0 || !("IntersectionObserver" in window)) return;
28
- let c2 = new IntersectionObserver((n2) => {
29
- n2.length !== 0 && f(n2[0].isIntersecting);
30
- }, { threshold: 1 });
31
- return c2.observe(n), () => c2.disconnect();
32
- }, [n]), import_react.useEffect(() => {
33
- if (d || n === void 0) return;
34
- let c2, l2 = () => {
35
- let { right: d2 } = n.getBoundingClientRect();
36
- u((n2) => Math.min(n2 + window.innerWidth - d2 - 10, 0)), c2 = requestAnimationFrame(l2);
37
- };
38
- return c2 = requestAnimationFrame(l2), () => {
39
- c2 !== void 0 && cancelAnimationFrame(c2);
40
- };
41
- }, [n, d]), {
42
- ref: c,
43
- style: import_react.useMemo(() => ({ transform: `translateX(${l}px)` }), [l])
44
- };
45
- }
46
- var data_grid_overlay_editor_default = (n) => {
47
- let { target: c, content: l, onFinishEditing: p, forceEditMode: m, initialValue: h, imageEditorOverride: g, markdownDivCreateNode: _, highlight: v, className: y, theme: b, id: x, cell: S, bloom: C, validateCell: w, getCellRenderer: T, provideEditor: E, isOutsideClick: D, customEventTarget: O } = n, [k, A] = import_react.useState(m ? l : void 0), j = import_react.useRef(k ?? l);
48
- j.current = k ?? l;
49
- let [M, N] = import_react.useState(() => w === void 0 ? true : !(isEditableGridCell(l) && (w == null ? void 0 : w(S, l, j.current)) === false)), P = import_react.useCallback((n2, c2) => {
50
- p(M ? n2 : void 0, c2);
51
- }, [M, p]), F = import_react.useCallback((n2) => {
52
- if (w !== void 0 && n2 !== void 0 && isEditableGridCell(n2)) {
53
- let c2 = w(S, n2, j.current);
54
- c2 === false ? N(false) : (typeof c2 == "object" && (n2 = c2), N(true));
55
- }
56
- A(n2);
57
- }, [S, w]), I = import_react.useRef(false), L = import_react.useRef(void 0), R = import_react.useCallback(() => {
58
- P(k, [0, 0]), I.current = true;
59
- }, [k, P]), z = import_react.useCallback((n2, c2) => {
60
- P(n2, c2 ?? L.current ?? [0, 0]), I.current = true;
61
- }, [P]), B = import_react.useCallback(async (n2) => {
62
- let c2 = false;
63
- n2.key === "Escape" ? (n2.stopPropagation(), n2.preventDefault(), L.current = [0, 0]) : n2.key === "Enter" && !n2.shiftKey ? (n2.stopPropagation(), n2.preventDefault(), L.current = [0, 1], c2 = true) : n2.key === "Tab" && (n2.stopPropagation(), n2.preventDefault(), L.current = [n2.shiftKey ? -1 : 1, 0], c2 = true), window.setTimeout(() => {
64
- !I.current && L.current !== void 0 && (P(c2 ? k : void 0, L.current), I.current = true);
65
- }, 0);
66
- }, [P, k]), V = k ?? l, [H, U] = import_react.useMemo(() => {
67
- var _a, _b;
68
- if (isInnerOnlyCell(l)) return [];
69
- let n2 = E == null ? void 0 : E(l);
70
- return n2 === void 0 ? [(_b = (_a = T(l)) == null ? void 0 : _a.provideEditor) == null ? void 0 : _b.call(_a, l), false] : [n2, false];
71
- }, [
72
- l,
73
- T,
74
- E
75
- ]), { ref: W, style: G } = useStayOnScreen(), K = true, q, J = true, Y;
76
- if (H !== void 0) {
77
- K = H.disablePadding !== true, J = H.disableStyling !== true;
78
- let n2 = isObjectEditorCallbackResult(H);
79
- n2 && (Y = H.styleOverride);
80
- let l2 = n2 ? H.editor : H;
81
- q = import_react.createElement(l2, {
82
- isHighlighted: v,
83
- onChange: F,
84
- value: V,
85
- initialValue: h,
86
- onFinishedEditing: z,
87
- validatedSelection: isEditableGridCell(V) ? V.selectionRange : void 0,
88
- forceEditMode: m,
89
- target: c,
90
- imageEditorOverride: g,
91
- markdownDivCreateNode: _,
92
- isValid: M,
93
- theme: b
94
- });
95
- }
96
- Y = {
97
- ...Y,
98
- ...G
99
- };
100
- let X = document.getElementById("portal");
101
- if (X === null) return console.error('Cannot open Data Grid overlay editor, because portal not found. Please add `<div id="portal" />` as the last child of your `<body>`.'), null;
102
- let Z = J ? "gdg-style" : "gdg-unstyle";
103
- M || (Z += " gdg-invalid"), K && (Z += " gdg-pad");
104
- let Q = (C == null ? void 0 : C[0]) ?? 1, $ = (C == null ? void 0 : C[1]) ?? 1;
105
- return (0, import_react_dom.createPortal)(import_react.createElement(ThemeContext.Provider, { value: b }, import_react.createElement(ClickOutsideContainer, {
106
- style: makeCSSStyle(b),
107
- className: y,
108
- onClickOutside: R,
109
- isOutsideClick: D,
110
- customEventTarget: O
111
- }, import_react.createElement(DataGridOverlayEditorStyle, {
112
- ref: W,
113
- id: x,
114
- className: Z,
115
- style: Y,
116
- as: U === true ? "label" : void 0,
117
- targetX: c.x - Q,
118
- targetY: c.y - $,
119
- targetWidth: c.width + Q * 2,
120
- targetHeight: c.height + $ * 2
121
- }, import_react.createElement("div", {
122
- className: "gdg-clip-region",
123
- onKeyDown: B
124
- }, q)))), X);
125
- };
126
- export {
127
- data_grid_overlay_editor_default as default
128
- };