Skip to content

Commit 815e4aa

Browse files
manztnvictus
andauthored
Render HiGlass viewer inside Shadow DOM (#208)
* Render HiGlass viewer inside Shadow DOM HiGlass v2.2.2 exports its CSS as `hglib.CSS`, which lets us encapsulate viewer styles using the Constructable Stylesheets API and a Shadow DOM. This prevents HiGlass styles from leaking into (or being affected by) the host notebook environment. The wheel event handler now also calls `preventDefault()` in addition to `stopPropagation()`. With the shadow DOM boundary, `stopPropagation()` alone wasn't sufficient to prevent page scrolling in marimo since composed events cross the shadow boundary. * Upgrade to higlass v2.2.3 for type export fix * Fix type checking and linting * ci: Upload smoke test failure screenshot as artifact * Fix smoke test to check shadow root children --------- Co-authored-by: Nezar Abdennur <nabdennur@gmail.com>
1 parent 5d2ea41 commit 815e4aa

File tree

3 files changed

+44
-16
lines changed

3 files changed

+44
-16
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ jobs:
6161
- run: deno install
6262
- run: npx playwright install chromium
6363
- run: deno task vitest --run
64+
- uses: actions/upload-artifact@v4
65+
if: failure()
66+
with:
67+
name: screenshots
68+
path: src/higlass/__screenshots__/
6469

6570
TypecheckJavaScript:
6671
name: JavaScript / Typecheck

src/higlass/widget.js

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import * as hglib from "https://esm.sh/higlass@1.13?deps=react@17,react-dom@17,pixi.js@6";
1+
import * as hglib from "https://esm.sh/higlass@2.2.3?deps=react@17,react-dom@17,pixi.js@6";
22
import { v4 } from "https://esm.sh/@lukeed/uuid@2.0.1";
33

44
/** @import { AnyModel } from "@anywidget/types" */
55
/** @import { PluginDataFetcherConstructor, GenomicLocation, Viewconf, DataFetcher} from "./types.ts" */
66

77
const NAME = "jupyter";
88

9+
let HIGLASS_STYLES = new CSSStyleSheet();
10+
HIGLASS_STYLES.replaceSync(hglib.CSS);
11+
912
/**
1013
* @param {string} href
1114
* @returns {Promise<void>}
@@ -275,7 +278,10 @@ function addEventListenersTo(el) {
275278
});
276279

277280
// prevent wheel events from scrolling the page while allowing HiGlass zoom
278-
el.addEventListener("wheel", (event) => event.stopPropagation(), {
281+
el.addEventListener("wheel", (event) => {
282+
event.preventDefault();
283+
event.stopPropagation();
284+
}, {
279285
signal: controller.signal,
280286
passive: false,
281287
});
@@ -303,28 +309,45 @@ export default {
303309
model.get("_viewconf"),
304310
);
305311
let options = model.get("_options") ?? {};
306-
let api = await hglib.viewer(el, viewconf, options);
312+
313+
let shadow = el.shadowRoot ?? el.attachShadow({ mode: "open" });
314+
shadow.innerHTML = "";
315+
shadow.adoptedStyleSheets = [HIGLASS_STYLES];
316+
let container = document.createElement("div");
317+
shadow.appendChild(container);
318+
319+
let api = await hglib.viewer(container, viewconf, options);
307320
let unlisten = addEventListenersTo(el);
308321

309322
model.on("msg:custom", (msg) => {
310323
msg = JSON.parse(msg);
311324
let [fn, ...args] = msg;
312-
api[fn](...args);
325+
/** @type {any} */ (api)[fn](...args);
313326
});
314327

315328
if (viewconf.views.length === 1) {
316-
api.on("location", (/** @type {GenomicLocation} */ loc) => {
317-
model.set("location", locationToCoordinates(loc));
318-
model.save_changes();
319-
}, viewconf.views[0].uid);
329+
api.on(
330+
"location",
331+
(/** @type {GenomicLocation} */ loc) => {
332+
model.set("location", locationToCoordinates(loc));
333+
model.save_changes();
334+
},
335+
viewconf.views[0].uid,
336+
undefined,
337+
);
320338
} else {
321339
viewconf.views.forEach((view, idx) => {
322-
api.on("location", (/** @type{GenomicLocation} */ loc) => {
323-
let location = model.get("location").slice();
324-
location[idx] = locationToCoordinates(loc);
325-
model.set("location", location);
326-
model.save_changes();
327-
}, view.uid);
340+
api.on(
341+
"location",
342+
(/** @type{GenomicLocation} */ loc) => {
343+
let location = model.get("location").slice();
344+
location[idx] = locationToCoordinates(loc);
345+
model.set("location", location);
346+
model.save_changes();
347+
},
348+
view.uid,
349+
undefined,
350+
);
328351
});
329352
}
330353

src/higlass/widget.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ test("render creates a HiGlass viewer with a simple viewconf", async () => {
5858

5959
const cleanup = await widget.render({ model, el, experimental });
6060

61-
// resolved without throwing; container has content
62-
expect(el.children.length).toBeGreaterThan(0);
61+
// resolved without throwing; shadow root has content
62+
expect(el.shadowRoot?.children.length).toBeGreaterThan(0);
6363

6464
expect(typeof cleanup).toBe("function");
6565
cleanup?.();

0 commit comments

Comments
 (0)