Hide the popover while waiting for an element

This commit is contained in:
Kamran Ahmed
2026-10-03 10:25:13 +01:00
parent 758a563744
commit bda4190244
6 changed files with 51 additions and 8 deletions
+1 -1
View File
@@ -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({
@@ -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`.
@@ -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;
@@ -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.
+4 -3
View File
@@ -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;
}
+40 -1
View File
@@ -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,