diff --git a/apps/docs/src/content/guides/async-tour.mdx b/apps/docs/src/content/guides/async-tour.mdx index a1e32c9..3236f06 100644 --- a/apps/docs/src/content/guides/async-tour.mdx +++ b/apps/docs/src/content/guides/async-tour.mdx @@ -89,7 +89,7 @@ driverObj.drive(); ## Waiting for Elements -When you know the next element is rendered on demand, you often don't need the manual `onNextClick` dance at all. Give the step a `waitForElement` timeout and the tour waits for the element to appear, staying on the current step in the meantime. Pair it with `advanceOnClick` when the highlighted element itself is what triggers the rendering, e.g. a button that opens a modal: +When you know the next element is rendered on demand, you often don't need the manual `onNextClick` dance at all. Give the step a `waitForElement` timeout and the tour waits for the element to appear, staying on the current step with its popover hidden in the meantime. Pair it with `advanceOnClick` when the highlighted element itself is what triggers the rendering, e.g. a button that opens a modal: ```js const driverObj = driver({ diff --git a/apps/docs/src/content/guides/changelog.mdx b/apps/docs/src/content/guides/changelog.mdx index c89fd7c..606fc52 100644 --- a/apps/docs/src/content/guides/changelog.mdx +++ b/apps/docs/src/content/guides/changelog.mdx @@ -25,6 +25,9 @@ ships, these entries move under a versioned heading. their button is hidden or disabled, and Escape does nothing when the close button is disabled. ([#589](https://github.com/nilbuild/driver.js/issues/589)) +- The current popover is hidden while `waitForElement` waits for the next + step's element, so a stale step no longer stays on screen. + ([#578](https://github.com/nilbuild/driver.js/issues/578)) - Only one instance is on screen at a time. Starting a tour or highlight closes any other active instance; it gets `onDeselected` and `onDestroyed` but not `onDestroyStarted`. diff --git a/apps/docs/src/content/guides/configuration.mdx b/apps/docs/src/content/guides/configuration.mdx index c85e9d6..ac6e8a8 100644 --- a/apps/docs/src/content/guides/configuration.mdx +++ b/apps/docs/src/content/guides/configuration.mdx @@ -84,8 +84,8 @@ type Config = { // Wait up to this many milliseconds for a step's element to appear before // treating it as missing (centered fallback popover, or a skip when - // skipMissingElement is set). The current step stays highlighted while - // waiting. Useful when the next element is rendered on demand, e.g. inside + // skipMissingElement is set). The current step stays highlighted, with its + // popover hidden, while waiting. Useful when the next element is rendered on demand, e.g. inside // a modal that the previous step's click opens. Can be configured at the // step level. (default: 0, off) waitForElement?: number; diff --git a/apps/docs/src/content/guides/interactive-tour.mdx b/apps/docs/src/content/guides/interactive-tour.mdx index 1f8d11d..6fa507a 100644 --- a/apps/docs/src/content/guides/interactive-tour.mdx +++ b/apps/docs/src/content/guides/interactive-tour.mdx @@ -10,7 +10,7 @@ import { CodeSample } from "../../components/CodeSample.tsx"; Sometimes the best way forward in a tour is the product itself: click the button you are pointing at, not a "Next" in the popover. Two options make this work without any custom hooks: - `advanceOnClick` advances the tour when the highlighted element is clicked, exactly as if the next button was pressed. The element's own click behavior still runs. -- `waitForElement` makes a step wait up to the given number of milliseconds for its element to appear, staying on the current step in the meantime. +- `waitForElement` makes a step wait up to the given number of milliseconds for its element to appear, staying on the current step with its popover hidden in the meantime. Both can be set for the whole tour or per step. The tour below has `advanceOnClick` enabled, so you can click the highlighted lines to walk through it. diff --git a/packages/driver/src/driver.ts b/packages/driver/src/driver.ts index 629cba8..56103aa 100644 --- a/packages/driver/src/driver.ts +++ b/packages/driver/src/driver.ts @@ -1,4 +1,4 @@ -import { AllowedButtons, destroyPopover, Popover } from "./popover"; +import { AllowedButtons, destroyPopover, hidePopover, Popover } from "./popover"; import { destroyOverlay } from "./overlay"; import { destroyEvents, initEvents, requireRefresh } from "./events"; import { Config, createContext, DriverHook } from "./context"; @@ -319,10 +319,11 @@ export function driver(options: Config = {}): Driver { const currentStep = steps[stepIndex]; - // The current step stays highlighted while waiting; a timeout falls - // through to the usual missing-element handling below. + // The current step stays highlighted, with its popover hidden, while + // waiting; a timeout falls through to the usual missing-element handling below. const waitTimeout = currentStep.waitForElement ?? ctx.getConfig("waitForElement") ?? 0; if (!hasWaitedForElement && waitTimeout > 0 && currentStep.element && !resolveElement(currentStep.element)) { + hidePopover(ctx.getState("popover")); waitForStepElement(currentStep, waitTimeout, () => drive(stepIndex, true)); return; } diff --git a/packages/driver/tests/wait-for-element.test.ts b/packages/driver/tests/wait-for-element.test.ts index d4fc4d8..65be2aa 100644 --- a/packages/driver/tests/wait-for-element.test.ts +++ b/packages/driver/tests/wait-for-element.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { createDriver, nextFrame, popoverTitle, useDriverHarness } from "./utils"; +import { createDriver, nextFrame, popoverEl, popoverTitle, useDriverHarness } from "./utils"; useDriverHarness(); @@ -36,6 +36,45 @@ describe("waitForElement", () => { expect(d.getActiveElement()?.id).toBe("late"); }); + it("hides the current popover while waiting", async () => { + const d = createDriver({ + animate: false, + steps: [ + { element: "#intro", popover: { title: "Step 1" } }, + { element: "#late", waitForElement: 500, popover: { title: "Step 2" } }, + ], + }); + d.drive(); + d.moveNext(); + + expect(popoverEl()?.style.display).toBe("none"); + expect(document.getElementById("intro")?.classList.contains("driver-active-element")).toBe(true); + + appendElement("late"); + await tick(); + + expect(popoverEl()?.style.display).toBe("block"); + expect(popoverTitle()).toBe("Step 2"); + }); + + it("shows the fallback popover when the wait times out", async () => { + const d = createDriver({ + animate: false, + steps: [ + { element: "#intro", popover: { title: "Step 1" } }, + { element: "#never", waitForElement: 40, popover: { title: "Step 2" } }, + ], + }); + d.drive(); + d.moveNext(); + expect(popoverEl()?.style.display).toBe("none"); + + await tick(80); + + expect(popoverEl()?.style.display).toBe("block"); + expect(popoverTitle()).toBe("Step 2"); + }); + it("waits for the first step's element on the initial drive", async () => { const d = createDriver({ animate: false,