Skip to content

The lifecycle

A tour is two lifecycles, one inside the other. A story has the simple one: it begins on one call and every way it ends, ends all of it. A step has the busy one: hooks run in a fixed order, and the step then rests on screen until exactly one thing moves it.

idlenothing runsa story runsstory.onEnter(story)the steps, one after anotherstory.onLeave(story, next)leko.start(story)the run endsstory.nextasked only when thelast step advanced
Every way a run ends — the close control, stop(), running out of steps with no next, a target that never came back, a handler that threw — goes through story.onLeave, and all but one arrives back at idle. The exception is a story that ran out of steps and named a next: the story it names goes up inside the same operation, so the tour is never idle between the two.
  • start(story) is the only way in from outside. Called while a tour is running it does nothing and reports tour-running — stop() first, then start(). There is no way to begin part-way through, and no way back: a story that should be revisitable is a shorter story, run again — Several short stories.
  • story.onEnter runs first and story.onLeave runs last, bracketing every step. Setup a whole story stands on belongs in the story’s own hooks; clear-up written on the first step’s onLeave fires the moment the tour reaches step 2 — A story sets up what every step stands on.
  • Every ending is the whole ending. The close control, stop(), running out of steps, a target that never turned up, an onEnter that threw — each tears the run down where it stands, and onLeave runs even for a step whose onEnter threw halfway.
  • next is the only way one story leads to another. It is asked when the last step advances — never on stop(), never on a failure — and the handoff is one operation: onLeave is told where the tour is going, and the tour is never idle in between — next joins one story to the one after it.
  • A page load ends the story where it stands. A story that declares next, whose last step waits for a URL, hands its successor on across the load, and the next document’s pickUp(stories) starts that story from its first step — never part-way — DESIGN.md, A page load ends the story, and hands it on.
the tour arrives — after story.onEnter for the first step,after the step before's onLeave for every otheronEnter(step)builds what the step assumesthe target resolved, the step handed overmissing? given 100ms to turn uponStep(step, story)the report — may come before it is on screenon screenstate is 'running' — every call is acceptedresting here until exactly one thing happensthe next controlonly a step with noawaits has one; validatemay refuse — the cutoutshakes, error joins themessage, the tour stays putleko.reached(step.awaits)the only mover on a stepthat declares awaitsit threwnever came backclose control · stop()every ending —next is undefinedonLeave(step, next)advanced — the next step's onEnterthe run ended — story.onLeave runs, and the tour is idle
A green outline is a moment your calls are acted on; an orange outline is Leko inside your own handler, where every call but stop() is refused. The whole chain from onLeave to the nextonStep runs inside the one call that moved the tour — anasync onEnter runs its first line there and the rest after that call has returned.

The order is fixed, and the report deliberately comes last: a step whose onEnter threw is never reported as reached. The report follows the step being handed to be drawn, so a step that glides into view or waits for its target is reported before it is on screen, and one whose target never turns up is reported and then followed by the ending.

What moves a step on is decided by one field:

  • No awaits: the step has a next control, and pressing it is the only mover. validate, if declared, guards that press — a refusal shakes the cutout (or dims it under reduced motion), puts error under the instruction, and the tour stays put.
  • awaits: 'action-name': the step has no next control, and leko.reached('action-name') is the only mover.
  • awaits: { url: /^\/checkout(?:[/?#]|$)/ }: the step has no next control, and a same-document change to the page’s own URL that now matches the pattern is the only mover. It advances on a change, never on an arrival, so a URL that already matched when the step began waiting does nothing. No reached() call moves it — a link has nowhere to put one — and DESIGN.md argues the whole of it under A URL is a signal the page reports.

Nothing else moves a step: not a DOM event, not a timer, and not a signal reported before the step’s onEnter returned — Signals and awaits.

One property holds the whole diagram together: no hook hands anything back, and Leko waits for nothing. The chain from one step’s onLeave to the next step’s onStep runs inside the single synchronous call that moved the tour — a press, a reached(), a start(), a URL change. Work that must finish before the next step makes sense is not a slow onEnter; it is a step of its own, with no target and an awaits, started by its own onEnter and ended by your code reporting in — A wait is a step that declares awaits, not a promise.