Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | 802x 802x 802x 802x 802x 52x 52x 802x 802x 802x 433x 31x 433x 19x 10x 10x 19x 19x 802x | import { useEffect, useState } from "react";
import { useLatest } from "@usefy/use-latest";
import type { TourStep } from "../types";
/** Options for {@link useAdvanceGate}. */
export interface UseAdvanceGateOptions {
/** The active step's gate config, or `undefined` (no gate). */
advanceOn: TourStep["advanceOn"];
/** The resolved step target (the default listen target). */
element: Element | null;
/** Master switch — the tour is open. */
enabled: boolean;
/**
* Identity of the step being gated (the step index). A change resets the
* gate, so revisiting a step re-arms it.
*/
stepKey: number;
/** Called once when the gate's event fires — advance the tour. */
onAdvance: () => void;
/**
* Re-resolution tick (the tour's mutation epoch). A change re-runs the
* listener effect, re-querying `advanceOn.selector` — so a gate target that
* mounts after the step settled still arms the gate.
*/
resolveTick?: number;
}
/**
* Implement a step's `advanceOn` interaction gate (SPEC §3.3): the step can
* only be left forward by performing the required user action. The hook
* listens for `advanceOn.event` on the resolved target element (or on
* `advanceOn.selector` resolved against `document` when given); when the
* event fires, the gate is satisfied and `onAdvance` is called once to move
* the tour forward automatically.
*
* Returns `gated` — `true` while the step has an unmet gate, which is what
* hides the Next/Done button and blocks the ArrowRight/Enter hotkeys.
*
* The gate re-arms whenever `stepKey` changes (render-phase reset), so
* navigating back to a gated step gates it again. Listeners are removed on
* step change, close, and unmount. SSR-safe (listening requires a DOM
* element by construction).
*
* @param options - {@link UseAdvanceGateOptions}
* @returns `true` while the current step's gate is unmet.
*
* @internal
*/
export function useAdvanceGate(options: UseAdvanceGateOptions): boolean {
const { advanceOn, element, enabled, stepKey, onAdvance, resolveTick } =
options;
const onAdvanceRef = useLatest(onAdvance);
const [satisfied, setSatisfied] = useState(false);
// Render-phase reset: a step change re-arms the gate in the same commit,
// so a revisited gated step is never momentarily un-gated.
const [prevKey, setPrevKey] = useState(stepKey);
if (!Object.is(prevKey, stepKey)) {
setPrevKey(stepKey);
setSatisfied(false);
}
const event = advanceOn?.event;
const selector = advanceOn?.selector;
useEffect(() => {
if (!enabled || !event || satisfied) return;
const target: Element | null = selector
? document.querySelector(selector)
: element;
if (!target) return;
const handler = () => {
setSatisfied(true);
onAdvanceRef.current();
};
// Capture phase: a consumer handler on a descendant calling
// `stopPropagation()` in the bubble phase must not starve the gate.
target.addEventListener(event, handler, true);
return () => target.removeEventListener(event, handler, true);
// `resolveTick` re-runs the effect so `selector` is re-queried after DOM
// mutations — a late-mounting gate target still arms the gate.
}, [
enabled,
event,
selector,
element,
satisfied,
stepKey,
onAdvanceRef,
resolveTick,
]);
return enabled && event !== undefined && !satisfied;
}
|