From 431c0367b762d944960bf34a69be33bbc3e9cd99 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 24 Jun 2026 12:30:08 +0100 Subject: [PATCH] Add allowScroll config to lock body scroll during tour --- docs/src/content/guides/configuration.mdx | 3 ++ playground/src/examples/index.ts | 2 ++ playground/src/examples/scroll.ts | 36 +++++++++++++++++++++ src/config.ts | 2 ++ src/driver.css | 4 +++ src/driver.ts | 5 ++- tests/scroll.test.ts | 38 +++++++++++++++++++++++ 7 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 playground/src/examples/scroll.ts create mode 100644 tests/scroll.test.ts diff --git a/docs/src/content/guides/configuration.mdx b/docs/src/content/guides/configuration.mdx index ef0795c..ed56808 100644 --- a/docs/src/content/guides/configuration.mdx +++ b/docs/src/content/guides/configuration.mdx @@ -36,6 +36,9 @@ type Config = { smoothScroll?: boolean; // Whether to allow closing the popover by clicking on the backdrop. (default: true) allowClose?: boolean; + // Whether to allow scrolling the page while the driver is active. + // Set to false to lock body scrolling for the duration of the tour. (default: true) + allowScroll?: boolean; // Opacity of the backdrop. (default: 0.5) overlayOpacity?: number; // What to do when the overlay backdrop is clicked. diff --git a/playground/src/examples/index.ts b/playground/src/examples/index.ts index 34e6857..21e83f4 100644 --- a/playground/src/examples/index.ts +++ b/playground/src/examples/index.ts @@ -5,6 +5,7 @@ import { arrowGroup } from "./arrow"; import { tourGroup } from "./tour"; import { apiGroup } from "./api"; import { durationGroup } from "./duration"; +import { scrollGroup } from "./scroll"; export const exampleGroups: ExampleGroup[] = [ highlightGroup, @@ -12,6 +13,7 @@ export const exampleGroups: ExampleGroup[] = [ arrowGroup, tourGroup, durationGroup, + scrollGroup, apiGroup, ]; diff --git a/playground/src/examples/scroll.ts b/playground/src/examples/scroll.ts new file mode 100644 index 0000000..f3d2e0f --- /dev/null +++ b/playground/src/examples/scroll.ts @@ -0,0 +1,36 @@ +import { driver } from "driver.js"; +import type { ExampleGroup } from "./types"; +import { basicTourSteps } from "./shared"; + +// Both examples run the same tour; only `allowScroll` differs so the page's +// scrollability during the tour can be compared directly. +function tourWithScroll(allowScroll: boolean) { + driver({ + allowScroll, + showProgress: true, + showButtons: ["next", "previous", "close"], + steps: basicTourSteps, + }).drive(); +} + +export const scrollGroup: ExampleGroup = { + title: "Body Scrolling (test)", + examples: [ + { + id: "scroll-allowed", + title: "Scroll Allowed (default)", + description: "LOOK: while the tour is active you can still scroll the page with the wheel or trackpad.", + run() { + tourWithScroll(true); + }, + }, + { + id: "scroll-locked", + title: "Scroll Locked", + description: "LOOK: with allowScroll:false the body is frozen — wheel and trackpad scrolling do nothing.", + run() { + tourWithScroll(false); + }, + }, + ], +}; diff --git a/src/config.ts b/src/config.ts index 3e382c2..7081e46 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,6 +17,7 @@ export type Config = { overlayOpacity?: number; smoothScroll?: boolean; allowClose?: boolean; + allowScroll?: boolean; overlayClickBehavior?: "close" | "nextStep" | DriverHook; stagePadding?: number; stageRadius?: number; @@ -63,6 +64,7 @@ export function configure(config: Config = {}) { animate: true, duration: 400, allowClose: true, + allowScroll: true, overlayClickBehavior: "close", overlayOpacity: 0.7, smoothScroll: false, diff --git a/src/driver.css b/src/driver.css index f66dc2d..69fc383 100644 --- a/src/driver.css +++ b/src/driver.css @@ -2,6 +2,10 @@ pointer-events: none; } +.driver-active.driver-no-scroll { + overflow: hidden; +} + .driver-active * { pointer-events: none; } diff --git a/src/driver.ts b/src/driver.ts index f385ba6..6941dc4 100644 --- a/src/driver.ts +++ b/src/driver.ts @@ -222,6 +222,9 @@ export function driver(options: Config = {}): Driver { setState("isInitialized", true); document.body.classList.add("driver-active", getConfig("animate") ? "driver-fade" : "driver-simple"); + if (!getConfig("allowScroll")) { + document.body.classList.add("driver-no-scroll"); + } document.body.style.setProperty("--driver-animation-duration", `${getConfig("duration") || 400}ms`); initEvents(); @@ -332,7 +335,7 @@ export function driver(options: Config = {}): Driver { const onDeselected = activeStep?.onDeselected || getConfig("onDeselected"); const onDestroyed = getConfig("onDestroyed"); - document.body.classList.remove("driver-active", "driver-fade", "driver-simple"); + document.body.classList.remove("driver-active", "driver-fade", "driver-simple", "driver-no-scroll"); document.body.style.removeProperty("--driver-animation-duration"); destroyEvents(); diff --git a/tests/scroll.test.ts b/tests/scroll.test.ts new file mode 100644 index 0000000..2212177 --- /dev/null +++ b/tests/scroll.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { createDriver, SAMPLE_STEPS, useDriverHarness } from "./utils"; + +useDriverHarness(); + +const hasNoScroll = () => document.body.classList.contains("driver-no-scroll"); + +describe("allowScroll", () => { + it("allows body scrolling by default", () => { + const d = createDriver({ animate: false }); + d.highlight({ element: "#intro", popover: { title: "Intro" } }); + + expect(d.getConfig().allowScroll).toBe(true); + expect(hasNoScroll()).toBe(false); + }); + + it("locks body scrolling when allowScroll is false", () => { + const d = createDriver({ animate: false, allowScroll: false }); + d.highlight({ element: "#intro", popover: { title: "Intro" } }); + + expect(hasNoScroll()).toBe(true); + }); + + it("locks body scrolling for a tour when allowScroll is false", () => { + const d = createDriver({ animate: false, allowScroll: false, steps: SAMPLE_STEPS }); + d.drive(); + + expect(hasNoScroll()).toBe(true); + }); + + it("releases the scroll lock on destroy", () => { + const d = createDriver({ animate: false, allowScroll: false }); + d.highlight({ element: "#intro", popover: { title: "Intro" } }); + d.destroy(); + + expect(hasNoScroll()).toBe(false); + }); +});