From 51b66dcfe929dd6e6c2bb0b25c8aa01c5cc5fe93 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Thu, 16 Jul 2026 20:42:14 +0100 Subject: [PATCH] Cut the active element out of the hint overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overlay is now the same spotlight the tour draws: an svg dim with a rounded cutout around the open hint's element (the path generator moves out of the tour's context into a pure function both share). The element stays visible and interactive inside the cutout — the svg lets pointer events through and only the dimmed path catches them, so clicking the dim still closes the hint. The other beacons now wait under the overlay instead of floating above it; only the open hint's beacon (via its aria-expanded state) and its popover rise above. The cutout tracks the element on scroll and resize. The arrow's drop-shadow halo is gone: it read as a dirty outline against light pages — tours never needed one because the arrow sits against the dark overlay, and hints now either touch the colored beacon (no overlay) or sit against the dim (overlay), so the plain white triangle works in both. --- apps/docs/src/content/guides/changelog.mdx | 12 ++-- apps/docs/src/content/guides/hints.mdx | 8 ++- .../docs/src/content/guides/styling-hints.mdx | 11 ++-- packages/driver/src/hints.css | 28 ++++++---- packages/driver/src/hints.ts | 55 ++++++++++++++++--- packages/driver/src/overlay.ts | 19 +++++-- packages/driver/tests/hints.test.ts | 44 +++++++++++++-- 7 files changed, 134 insertions(+), 43 deletions(-) diff --git a/apps/docs/src/content/guides/changelog.mdx b/apps/docs/src/content/guides/changelog.mdx index 2dcc7ca..2e7c954 100644 --- a/apps/docs/src/content/guides/changelog.mdx +++ b/apps/docs/src/content/guides/changelog.mdx @@ -16,6 +16,8 @@ ships, these entries move under a versioned heading. stays interactive. New `driver.js/hints` entry, ~5KB gzip. [Guide](/docs/hints). ([#363](https://github.com/nilbuild/driver.js/issues/363)) - `--driver-hint-size` and `--driver-hint-color` CSS variables to style the beacons. +- Optional hint overlay (`overlay`, `overlayColor`, `overlayOpacity`) — dims the + page around the open hint's element, tour-style cutout included. - `sideEffects` field in package.json for better tree-shaking. ### Changed @@ -73,12 +75,10 @@ ships, these entries move under a versioned heading. ### Changed -- Breaking Popover footer buttons are now styled - through the `driver-popover-footer-btn` class instead of the - `.driver-popover-footer button` selector. Buttons you add through `onPopoverRender` - no longer inherit the default styling automatically. Add the - `driver-popover-footer-btn` class to opt in, or leave it off to apply your own - styles. The built-in next/previous buttons carry the class out of the box. +- Breaking Popover footer buttons are now styled through the + `driver-popover-footer-btn` class instead of the `.driver-popover-footer button` selector. Buttons you add through + `onPopoverRender` no longer inherit the default styling automatically. Add the `driver-popover-footer-btn` class to + opt in, or leave it off to apply your own styles. The built-in next/previous buttons carry the class out of the box. ([#555](https://github.com/nilbuild/driver.js/issues/555)) - Custom buttons added through `onPopoverRender` now receive their own click events. Only the built-in next, previous, and close buttons have their default diff --git a/apps/docs/src/content/guides/hints.mdx b/apps/docs/src/content/guides/hints.mdx index 0ec5210..11e8a35 100644 --- a/apps/docs/src/content/guides/hints.mdx +++ b/apps/docs/src/content/guides/hints.mdx @@ -126,9 +126,11 @@ Size and colour come from CSS variables: ## Dimming the page -Pass `overlay: true` to dim the page while a hint is open. The beacons and the -popover stay above the overlay, so other hints remain clickable, and clicking -the dimmed page closes the hint like any outside click: +Pass `overlay: true` to dim the page while a hint is open — the hint's element +is cut out of the dim, spotlighting it exactly like a tour step, and it stays +interactive inside the cutout. Everything else, including the other beacons, +sits under the overlay; clicking the dimmed page closes the hint like any +outside click: ```js const productHints = hints({ diff --git a/apps/docs/src/content/guides/styling-hints.mdx b/apps/docs/src/content/guides/styling-hints.mdx index 0d39767..3c45961 100644 --- a/apps/docs/src/content/guides/styling-hints.mdx +++ b/apps/docs/src/content/guides/styling-hints.mdx @@ -147,8 +147,9 @@ const productHints = hints({ ## Dimming the page With `overlay: true` the page dims while a hint is open — a focus mode for -hints. The beacons stay above the overlay, so you can hop from one hint to the -next; clicking the dimmed page closes the hint. +hints. The hint's element is cut out of the dim and stays interactive, exactly +like a tour step's highlight; the other beacons wait under the overlay. +Clicking the dimmed page closes the hint. { }); describe("overlay", () => { - const overlayEl = () => document.querySelector(".driver-hint-overlay"); + const overlayEl = () => document.querySelector(".driver-hint-overlay"); + const overlayPath = () => overlayEl()?.querySelector("path"); it("shows no overlay by default", () => { const productHints = createHints({ hints: SAMPLE_HINTS }); @@ -320,8 +321,42 @@ describe("overlay", () => { productHints.show(); productHints.open("intro"); - expect(overlayEl()?.style.backgroundColor).toBe("rgb(18, 52, 86)"); - expect(overlayEl()?.style.opacity).toBe("0.4"); + expect(["#123456", "rgb(18, 52, 86)"]).toContain(overlayPath()?.style.fill); + expect(overlayPath()?.style.opacity).toBe("0.4"); + }); + + it("cuts the hint's element out of the dim", () => { + const rect = (over: Partial): DOMRect => + ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0, toJSON() {}, ...over }) as DOMRect; + const el = document.querySelector("#intro")!; + el.getBoundingClientRect = () => rect({ x: 400, y: 300, width: 200, height: 40 }); + + const productHints = createHints({ hints: SAMPLE_HINTS, overlay: true }); + productHints.show(); + productHints.open("intro"); + + // Full-screen subpath plus a cutout subpath. The cutout starts at the + // element's x minus the 10px padding, plus the 5px corner radius: 395. + const d = overlayPath()?.getAttribute("d") ?? ""; + expect(d).toContain(`M${window.innerWidth},0L0,0L0,${window.innerHeight}`); + expect(d).toContain("M395,290"); + }); + + it("tracks the element when the page scrolls", async () => { + const rect = (over: Partial): DOMRect => + ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0, toJSON() {}, ...over }) as DOMRect; + const el = document.querySelector("#intro")!; + el.getBoundingClientRect = () => rect({ x: 400, y: 300, width: 200, height: 40 }); + + const productHints = createHints({ hints: SAMPLE_HINTS, overlay: true }); + productHints.show(); + productHints.open("intro"); + + el.getBoundingClientRect = () => rect({ x: 400, y: 100, width: 200, height: 40 }); + window.dispatchEvent(new Event("scroll")); + await nextFrame(); + + expect(overlayPath()?.getAttribute("d")).toContain("M395,90"); }); it("closes the hint when the dimmed page is clicked", () => { @@ -329,7 +364,8 @@ describe("overlay", () => { productHints.show(); productHints.open("intro"); - overlayEl()!.click(); + // Clicks land on the dimmed path (the svg lets pointer events through). + overlayPath()!.dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(popoverEl()).toBeNull(); expect(overlayEl()).toBeNull();