From b5105a9225c5efe4eb12412ba94c4e36fcfcd4c6 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 24 Jun 2026 09:01:00 +0100 Subject: [PATCH] Get rid of :has selector (closes #586) --- src/driver.css | 2 +- src/highlight.ts | 14 +++++++++++ tests/highlight.test.ts | 52 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/highlight.test.ts diff --git a/src/driver.css b/src/driver.css index a2e5e33..6c2939d 100644 --- a/src/driver.css +++ b/src/driver.css @@ -136,7 +136,7 @@ } /* Disable the scrolling of parent element if it has an active element*/ -:not(body):has(> .driver-active-element) { +.driver-active-element-parent { overflow: hidden !important; } diff --git a/src/highlight.ts b/src/highlight.ts index 9ef84c3..e20b9e0 100644 --- a/src/highlight.ts +++ b/src/highlight.ts @@ -156,6 +156,10 @@ function transferHighlight(toElement: Element, toStep: DriveStep) { renderPopover(toElement, toStep); } + document.querySelectorAll(".driver-active-element-parent").forEach(element => { + element.classList.remove("driver-active-element-parent"); + }); + fromElement.classList.remove("driver-active-element", "driver-no-interaction"); fromElement.removeAttribute("aria-haspopup"); fromElement.removeAttribute("aria-expanded"); @@ -166,6 +170,11 @@ function transferHighlight(toElement: Element, toStep: DriveStep) { toElement.classList.add("driver-no-interaction"); } + const toParent = toElement.parentElement; + if (toParent && toParent !== document.body) { + toParent.classList.add("driver-active-element-parent"); + } + toElement.classList.add("driver-active-element"); toElement.setAttribute("aria-haspopup", "dialog"); toElement.setAttribute("aria-expanded", "true"); @@ -175,6 +184,11 @@ function transferHighlight(toElement: Element, toStep: DriveStep) { export function destroyHighlight() { document.getElementById("driver-dummy-element")?.remove(); document.querySelectorAll(".driver-active-element").forEach(element => { + const parent = element.parentElement; + if (parent && parent !== document.body) { + parent.classList.remove("driver-active-element-parent"); + } + element.classList.remove("driver-active-element", "driver-no-interaction"); element.removeAttribute("aria-haspopup"); element.removeAttribute("aria-expanded"); diff --git a/tests/highlight.test.ts b/tests/highlight.test.ts new file mode 100644 index 0000000..9a0c2f4 --- /dev/null +++ b/tests/highlight.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { createDriver, navButton, useDriverHarness } from "./utils"; + +useDriverHarness(); + +const PARENT_CLASS = "driver-active-element-parent"; + +const NESTED_HTML = ` +
+
+ +`; + +describe("active element parent", () => { + it("locks scrolling on the highlighted element's parent", () => { + document.body.innerHTML = NESTED_HTML; + const d = createDriver({ animate: false, steps: [{ element: "#child-a" }] }); + d.drive(); + + expect(document.getElementById("container-a")?.classList.contains(PARENT_CLASS)).toBe(true); + }); + + it("moves the lock to the new parent when highlighting a different branch", () => { + document.body.innerHTML = NESTED_HTML; + const d = createDriver({ + animate: false, + steps: [{ element: "#child-a" }, { element: "#child-b" }], + }); + d.drive(); + navButton("next")?.click(); + + expect(document.getElementById("container-a")?.classList.contains(PARENT_CLASS)).toBe(false); + expect(document.getElementById("container-b")?.classList.contains(PARENT_CLASS)).toBe(true); + }); + + it("never locks the body element", () => { + document.body.innerHTML = NESTED_HTML; + const d = createDriver({ animate: false, steps: [{ element: "#top-level" }] }); + d.drive(); + + expect(document.body.classList.contains(PARENT_CLASS)).toBe(false); + }); + + it("releases the lock when the tour is destroyed", () => { + document.body.innerHTML = NESTED_HTML; + const d = createDriver({ animate: false, steps: [{ element: "#child-a" }] }); + d.drive(); + d.destroy(); + + expect(document.getElementById("container-a")?.classList.contains(PARENT_CLASS)).toBe(false); + }); +});