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();