From 51b1074d8c33c72b7fe65e4975ceeff5c0e7102f Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Sat, 28 Feb 2026 13:21:15 -0500 Subject: [PATCH 1/5] 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. --- src/higlass/widget.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/higlass/widget.js b/src/higlass/widget.js index 66e72aa..c4e42ef 100644 --- a/src/higlass/widget.js +++ b/src/higlass/widget.js @@ -1,4 +1,4 @@ -import * as hglib from "https://esm.sh/higlass@1.13?deps=react@17,react-dom@17,pixi.js@6"; +import * as hglib from "https://esm.sh/higlass@2.2.2?deps=react@17,react-dom@17,pixi.js@6"; import { v4 } from "https://esm.sh/@lukeed/uuid@2.0.1"; /** @import { AnyModel } from "@anywidget/types" */ @@ -6,6 +6,9 @@ import { v4 } from "https://esm.sh/@lukeed/uuid@2.0.1"; const NAME = "jupyter"; +let HIGLASS_STYLES = new CSSStyleSheet(); +HIGLASS_STYLES.replaceSync(hglib.CSS); + /** * @param {string} href * @returns {Promise} @@ -275,7 +278,10 @@ function addEventListenersTo(el) { }); // prevent wheel events from scrolling the page while allowing HiGlass zoom - el.addEventListener("wheel", (event) => event.stopPropagation(), { + el.addEventListener("wheel", (event) => { + event.preventDefault(); + event.stopPropagation(); + }, { signal: controller.signal, passive: false, }); @@ -303,7 +309,14 @@ export default { model.get("_viewconf"), ); let options = model.get("_options") ?? {}; - let api = await hglib.viewer(el, viewconf, options); + + let shadow = el.shadowRoot ?? el.attachShadow({ mode: "open" }); + shadow.innerHTML = ""; + shadow.adoptedStyleSheets = [HIGLASS_STYLES]; + let container = document.createElement("div"); + shadow.appendChild(container); + + let api = await hglib.viewer(container, viewconf, options); let unlisten = addEventListenersTo(el); model.on("msg:custom", (msg) => { From 81c07692cf18c87b6b2839b5c593890900839921 Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sat, 28 Feb 2026 19:25:44 -0500 Subject: [PATCH 2/5] Upgrade to higlass v2.2.3 for type export fix --- src/higlass/widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/higlass/widget.js b/src/higlass/widget.js index c4e42ef..7b092c6 100644 --- a/src/higlass/widget.js +++ b/src/higlass/widget.js @@ -1,4 +1,4 @@ -import * as hglib from "https://esm.sh/higlass@2.2.2?deps=react@17,react-dom@17,pixi.js@6"; +import * as hglib from "https://esm.sh/higlass@2.2.3?deps=react@17,react-dom@17,pixi.js@6"; import { v4 } from "https://esm.sh/@lukeed/uuid@2.0.1"; /** @import { AnyModel } from "@anywidget/types" */ From 61726863da1883d1ef8af3e9fa1b4958adc29593 Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sat, 28 Feb 2026 19:41:10 -0500 Subject: [PATCH 3/5] Fix type checking and linting --- src/higlass/widget.js | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/higlass/widget.js b/src/higlass/widget.js index 7b092c6..6d108ec 100644 --- a/src/higlass/widget.js +++ b/src/higlass/widget.js @@ -322,22 +322,32 @@ export default { model.on("msg:custom", (msg) => { msg = JSON.parse(msg); let [fn, ...args] = msg; - api[fn](...args); + /** @type {any} */ (api)[fn](...args); }); if (viewconf.views.length === 1) { - api.on("location", (/** @type {GenomicLocation} */ loc) => { - model.set("location", locationToCoordinates(loc)); - model.save_changes(); - }, viewconf.views[0].uid); + api.on( + "location", + (/** @type {GenomicLocation} */ loc) => { + model.set("location", locationToCoordinates(loc)); + model.save_changes(); + }, + viewconf.views[0].uid, + undefined, + ); } else { viewconf.views.forEach((view, idx) => { - api.on("location", (/** @type{GenomicLocation} */ loc) => { - let location = model.get("location").slice(); - location[idx] = locationToCoordinates(loc); - model.set("location", location); - model.save_changes(); - }, view.uid); + api.on( + "location", + (/** @type{GenomicLocation} */ loc) => { + let location = model.get("location").slice(); + location[idx] = locationToCoordinates(loc); + model.set("location", location); + model.save_changes(); + }, + view.uid, + undefined, + ); }); } From 62208620f914c96ccfd9ede1b00b815f0e0faec0 Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sun, 1 Mar 2026 05:19:14 -0500 Subject: [PATCH 4/5] ci: Upload smoke test failure screenshot as artifact --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cb1734..523612d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,11 @@ jobs: - run: deno install - run: npx playwright install chromium - run: deno task vitest --run + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: src/higlass/__screenshots__/ TypecheckJavaScript: name: JavaScript / Typecheck From beb6576adfa22cbf7a70ffd8a666d4e6484f622d Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sun, 1 Mar 2026 05:47:43 -0500 Subject: [PATCH 5/5] Fix smoke test to check shadow root children --- src/higlass/widget.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/higlass/widget.test.ts b/src/higlass/widget.test.ts index c7934a6..f3239a1 100644 --- a/src/higlass/widget.test.ts +++ b/src/higlass/widget.test.ts @@ -58,8 +58,8 @@ test("render creates a HiGlass viewer with a simple viewconf", async () => { const cleanup = await widget.render({ model, el, experimental }); - // resolved without throwing; container has content - expect(el.children.length).toBeGreaterThan(0); + // resolved without throwing; shadow root has content + expect(el.shadowRoot?.children.length).toBeGreaterThan(0); expect(typeof cleanup).toBe("function"); cleanup?.();