@glowhop/core-tour 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/config/index.js +3 -1
- package/dom/tour-view-driver.d.ts +90 -2
- package/elements/popover.d.ts +5 -0
- package/index.js +282 -45
- package/package.json +6 -4
- package/runtime/active-step.d.ts +2 -1
- package/runtime/tour-controller.d.ts +18 -0
- package/types/index.d.ts +34 -1
- package/utils/utils.d.ts +16 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @glowhop/core-tour
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 781a1f7: Report the wait when a step's target is still resolving. A target given as an async resolver, or one the `"wait"` strategy is polling for, keeps the tour on the step the user asked to leave: for as long as that wait lasts, the popover on screen carries `data-glow-tour-awaiting-target` and its advance control is disabled, so a tour waiting on a slow target no longer looks idle with a button that does nothing. Both are cleared when the target settles, and `TourState` gains `awaitingTarget` so a UI that renders its own controls - every adapter's trigger components, and any custom one - can show the wait rather than read it from the DOM. Cancel and previous stay available, a target that resolves synchronously never enters this state, and the freeze of a target lost mid-step is unchanged.
|
|
8
|
+
- 91d778a: Add `tour.hidePopover()` and `tour.showPopover()`, also returned by `useGlowTour` and `injectGlowTour`. Hiding the popover keeps the overlay, the indicator and the scroll lock, and the tour keeps running: the page leaves `inert`, focus moves from the popover to the target, and the keyboard shortcuts do nothing until the popover is shown again. It stays hidden across steps, until `showPopover()` or the end of the tour, and a new `start()` shows it again. Showing it replays its entrance, makes the step modal again and moves focus back into it. `TourState` gains `popoverHidden`.
|
|
9
|
+
- d63e152: Step the popover aside while the user scrolls. The popover and the pointer used to chase the target through a fade every few pixels of travel, and settled in the middle of the screen once the target had scrolled out of view. They now fade out at the first scroll and come back once the page has been still for a moment, while the spotlight keeps following the target. When the user left part of the target outside the viewport, the step scrolls it back after the new `behavior.scroll.returnDelay` (500 ms by default, `false` to leave the page where the user put it); scrolling again restarts the wait, and a wheel or touch drag during the scroll back hands the page back. Steps with `allowScroll: false` or `autoScroll: false` do not scroll back.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 2c568ab: Point each package's npm homepage to its page on glowtour.dev instead of the GitHub README, and describe what each package does in its npm description. Package metadata only: no code, API or export changes.
|
|
14
|
+
- f8259bd: Keep the tour aligned on a phone page that is wider than the device and can be zoomed out. The browser then grows the layout viewport past the initial containing block: the overlay stopped short of the bottom of the screen, leaving an undimmed band, and the popover was placed and clamped against the smaller device-width box. The overlay now spans the taller of `100%` and `100lvh`, and placement measures the box `position: fixed` elements actually use.
|
|
15
|
+
|
|
3
16
|
## 1.4.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
package/config/index.js
CHANGED
|
@@ -480,7 +480,7 @@ var BEHAVIOR_KEYS = [
|
|
|
480
480
|
"scroll",
|
|
481
481
|
"overlayClick"
|
|
482
482
|
];
|
|
483
|
-
var SCROLL_KEYS = ["behavior", "block", "inline"];
|
|
483
|
+
var SCROLL_KEYS = ["behavior", "block", "inline", "returnDelay"];
|
|
484
484
|
var CONFIG_VERSION = "1.1";
|
|
485
485
|
var BUILTIN_ACTION_KEYS = {
|
|
486
486
|
wait: ["type", "ms"],
|
|
@@ -847,6 +847,8 @@ function validateScrollShape(path, value, issues) {
|
|
|
847
847
|
validateOptionalEnum(`${path}.behavior`, value.behavior, ["auto", "smooth"], issues);
|
|
848
848
|
validateOptionalEnum(`${path}.block`, value.block, ["start", "center", "end", "nearest"], issues);
|
|
849
849
|
validateOptionalEnum(`${path}.inline`, value.inline, ["start", "center", "end", "nearest"], issues);
|
|
850
|
+
if (value.returnDelay !== false)
|
|
851
|
+
validateOptionalFiniteNonNegative(`${path}.returnDelay`, value.returnDelay, issues);
|
|
850
852
|
}
|
|
851
853
|
function validateBehaviorShape(path, value, issues) {
|
|
852
854
|
if (value === undefined)
|
|
@@ -27,6 +27,17 @@ export interface TourViewDriver<T> {
|
|
|
27
27
|
* than throw.
|
|
28
28
|
*/
|
|
29
29
|
retarget(step: ActiveStep<T>, signal: AbortSignal): Promise<void> | void;
|
|
30
|
+
/**
|
|
31
|
+
* Reports that the navigation under way is waiting for the next step's
|
|
32
|
+
* target to resolve. The presented step has not left yet, so its popover is
|
|
33
|
+
* the one that carries the wait.
|
|
34
|
+
*/
|
|
35
|
+
setTargetPending?(pending: boolean): void;
|
|
36
|
+
/**
|
|
37
|
+
* Hides or shows the popover of the running tour. A step still entering reads it as it is
|
|
38
|
+
* presented. Every `start()` shows it again first.
|
|
39
|
+
*/
|
|
40
|
+
setPopoverHidden?(hidden: boolean): void;
|
|
30
41
|
dispose(): void;
|
|
31
42
|
releaseMount?(): void;
|
|
32
43
|
setCommands?(commands: TourViewCommands): void;
|
|
@@ -66,6 +77,14 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
66
77
|
* instead of freezing the presentation.
|
|
67
78
|
*/
|
|
68
79
|
private awaitingStepUi;
|
|
80
|
+
/**
|
|
81
|
+
* True while the popover and pointer stand aside for a user scroll, until the
|
|
82
|
+
* page has been still for `USER_SCROLL_IDLE_DELAY` and, when the target was
|
|
83
|
+
* left off screen, the step has scrolled it back. The spotlight keeps
|
|
84
|
+
* tracking the target throughout.
|
|
85
|
+
*/
|
|
86
|
+
private userScrolling;
|
|
87
|
+
private scrollTimer?;
|
|
69
88
|
private activeTarget;
|
|
70
89
|
private targetFocusedAtFreeze;
|
|
71
90
|
private lastTargetRect;
|
|
@@ -85,6 +104,10 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
85
104
|
private handledTriggerClick;
|
|
86
105
|
private popover;
|
|
87
106
|
private presentationDirty;
|
|
107
|
+
/** True while the controller waits for the next step's target, see `setTargetPending`. */
|
|
108
|
+
private targetPending;
|
|
109
|
+
/** True while the consumer hides the popover, see `setPopoverHidden`. */
|
|
110
|
+
private popoverHidden;
|
|
88
111
|
/**
|
|
89
112
|
* The focus a clear gives back once the popover has faded out. Kept past a clear that a new show
|
|
90
113
|
* supersedes, so that tour returns focus there instead of to the fading popover.
|
|
@@ -104,6 +127,21 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
104
127
|
registerOverlay(element: SVGSVGElement | null): void;
|
|
105
128
|
registerPopover(element: HTMLElement | null): void;
|
|
106
129
|
registerPointer(element: HTMLElement | null): void;
|
|
130
|
+
/**
|
|
131
|
+
* Flags the presented popover as waiting for the next step's target, and
|
|
132
|
+
* disables its advance control for as long as the wait lasts: the step the
|
|
133
|
+
* user asked to leave stays on screen, and an advance that is already
|
|
134
|
+
* refused by the controller must not keep looking available. Cleared by the
|
|
135
|
+
* controller when the target settles, and by any teardown.
|
|
136
|
+
*/
|
|
137
|
+
setTargetPending(pending: boolean): void;
|
|
138
|
+
/**
|
|
139
|
+
* Hides the popover and hands the page back while it is hidden: nothing is left to trap focus
|
|
140
|
+
* in, so the page leaves `inert`, focus moves from the popover to the target, and the shortcuts
|
|
141
|
+
* stop. The overlay, the pointer and the scroll lock stay. Showing it again replays the entrance
|
|
142
|
+
* and makes the step modal again. A step still entering applies it once presented.
|
|
143
|
+
*/
|
|
144
|
+
setPopoverHidden(hidden: boolean): void;
|
|
107
145
|
show(step: ActiveStep<T>, direction: TourDirection, signal: AbortSignal, onBeforePopoverAppear?: () => void | Promise<void>): Promise<void>;
|
|
108
146
|
clear(signal: AbortSignal): Promise<void>;
|
|
109
147
|
releaseMount(): void;
|
|
@@ -114,6 +152,8 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
114
152
|
private syncModality;
|
|
115
153
|
private claimModal;
|
|
116
154
|
private releaseModality;
|
|
155
|
+
/** Gives the page back without releasing the document's modal claim. */
|
|
156
|
+
private liftModality;
|
|
117
157
|
private restoreInertBranches;
|
|
118
158
|
/**
|
|
119
159
|
* Brings the spotlight onto the target, then hands the step's popover and
|
|
@@ -163,6 +203,34 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
163
203
|
private updatePosition;
|
|
164
204
|
/** Per-frame follow-up for the popover and pointer, once they are on screen. */
|
|
165
205
|
private trackStepUi;
|
|
206
|
+
/**
|
|
207
|
+
* A scroll of the page, or of a scroller around the target, while the step is
|
|
208
|
+
* on screen. The popover and pointer stand aside at the first one, since
|
|
209
|
+
* following a travelling rect would fade them out and in every few pixels,
|
|
210
|
+
* and come back once the page has been still for a moment.
|
|
211
|
+
*/
|
|
212
|
+
private onUserScroll;
|
|
213
|
+
private waitForScrollIdle;
|
|
214
|
+
/**
|
|
215
|
+
* The page has been still for a moment: brings the popover and pointer back,
|
|
216
|
+
* after `scroll.returnDelay` has scrolled the target back into view when the
|
|
217
|
+
* user left part of it outside.
|
|
218
|
+
*/
|
|
219
|
+
private settleScroll;
|
|
220
|
+
/**
|
|
221
|
+
* Scrolls the target back in. As on entrance, `awaitingStepUi` leaves the
|
|
222
|
+
* spotlight to follow the page on its own and the scroll events this raises
|
|
223
|
+
* to be ignored; set together with `userScrolling`, it marks the scroll back,
|
|
224
|
+
* which a wheel or touch drag interrupts.
|
|
225
|
+
*/
|
|
226
|
+
private scrollBack;
|
|
227
|
+
private revealAfterScroll;
|
|
228
|
+
private stopUserScroll;
|
|
229
|
+
/**
|
|
230
|
+
* Moves the popover along with its target and returns its placement. A hidden popover stays
|
|
231
|
+
* where it is: moving it would fade it back in. The pointer still keeps clear of its placement.
|
|
232
|
+
*/
|
|
233
|
+
private placePopover;
|
|
166
234
|
private observeDynamicOperation;
|
|
167
235
|
private handleKeydown;
|
|
168
236
|
/**
|
|
@@ -180,6 +248,23 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
180
248
|
private queueTransitionClick;
|
|
181
249
|
private flushPendingCommand;
|
|
182
250
|
private loopFocus;
|
|
251
|
+
/**
|
|
252
|
+
* Makes a presented step modal and moves focus into its popover, or keeps the popover hidden. A
|
|
253
|
+
* popover shown again after the entrance passed over it enters now.
|
|
254
|
+
*/
|
|
255
|
+
private engagePopover;
|
|
256
|
+
/**
|
|
257
|
+
* Fades the popover out and stops guarding focus. Focus left in the popover goes to the target,
|
|
258
|
+
* or is dropped when the target cannot take it: `inert` would otherwise drop it on the body. The
|
|
259
|
+
* focus the tour gives back when it ends is kept for the step that shows the popover again.
|
|
260
|
+
*/
|
|
261
|
+
private concealPopover;
|
|
262
|
+
/**
|
|
263
|
+
* Replays the popover's entrance on the rect the step was last placed on (a frozen step has no
|
|
264
|
+
* live target to measure), then makes the step modal again.
|
|
265
|
+
*/
|
|
266
|
+
private revealPopover;
|
|
267
|
+
private isPopoverConcealed;
|
|
183
268
|
private activateFocus;
|
|
184
269
|
private syncScrollLock;
|
|
185
270
|
private syncShortcutLabels;
|
|
@@ -231,8 +316,9 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
231
316
|
private applyInteraction;
|
|
232
317
|
/**
|
|
233
318
|
* Applies a `behavior.allowInteraction` changed through the step props while the step is on
|
|
234
|
-
* screen. A step still entering reads the new value when it
|
|
235
|
-
* interaction off until `retarget()` restores it. The indicator
|
|
319
|
+
* screen, or stepped aside for a user scroll. A step still entering reads the new value when it
|
|
320
|
+
* presents, and a frozen one keeps interaction off until `retarget()` restores it. The indicator
|
|
321
|
+
* fades in or out instead of snapping.
|
|
236
322
|
*/
|
|
237
323
|
private syncInteraction;
|
|
238
324
|
private isFocusInsideTarget;
|
|
@@ -270,6 +356,8 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
270
356
|
* lets the backdrop appear while the page is still travelling.
|
|
271
357
|
*/
|
|
272
358
|
private beginTargetScroll;
|
|
359
|
+
/** Scrolls the target in as the step asks, and returns the settle wait as `beginTargetScroll` does. */
|
|
360
|
+
private scrollTargetIntoView;
|
|
273
361
|
/**
|
|
274
362
|
* Resolves once the scroller has held still for a couple of frames.
|
|
275
363
|
*
|
package/elements/popover.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export default class PopoverElement extends GlowTourElement {
|
|
|
14
14
|
private readonly mutationLease;
|
|
15
15
|
protected _getNextStyles(position: DOMRect, step: TourElementStep): Keyframe;
|
|
16
16
|
resolvePosition(targetPosition: DOMRect, step: TourElementStep): PopoverPosition;
|
|
17
|
+
/**
|
|
18
|
+
* Marks the popover as waiting for the next step's target. Written through the mutation lease,
|
|
19
|
+
* like every other attribute the popover owns, so unbinding the element gives it back clean.
|
|
20
|
+
*/
|
|
21
|
+
setAwaitingTarget(awaiting: boolean): void;
|
|
17
22
|
private _centerPosition;
|
|
18
23
|
private _applyPositionState;
|
|
19
24
|
/**
|
package/index.js
CHANGED
|
@@ -417,12 +417,34 @@ function isNode(value, context) {
|
|
|
417
417
|
const Node = ownerWindow(context)?.Node ?? globalThis.Node;
|
|
418
418
|
return typeof Node === "function" && value instanceof Node;
|
|
419
419
|
}
|
|
420
|
+
var fixedBoxes = new WeakMap;
|
|
421
|
+
function fixedContainingBlock(document2, root) {
|
|
422
|
+
const view = document2.defaultView;
|
|
423
|
+
const key = `${root.clientWidth} ${root.clientHeight} ${view?.innerWidth} ${view?.innerHeight}`;
|
|
424
|
+
const cached = fixedBoxes.get(document2);
|
|
425
|
+
if (cached?.key === key)
|
|
426
|
+
return cached;
|
|
427
|
+
const probe = document2.createElement?.("div");
|
|
428
|
+
if (!probe?.style)
|
|
429
|
+
return null;
|
|
430
|
+
probe.style.cssText = "position:fixed;top:0;right:0;bottom:0;left:0;visibility:hidden;pointer-events:none";
|
|
431
|
+
root.appendChild(probe);
|
|
432
|
+
const { height, width } = probe.getBoundingClientRect();
|
|
433
|
+
probe.remove();
|
|
434
|
+
if (!(width > 0 && height > 0))
|
|
435
|
+
return null;
|
|
436
|
+
const box = { height, key, width };
|
|
437
|
+
fixedBoxes.set(document2, box);
|
|
438
|
+
return box;
|
|
439
|
+
}
|
|
420
440
|
function viewportDimensions(context) {
|
|
421
|
-
const
|
|
441
|
+
const document2 = ownerDocument(context);
|
|
442
|
+
const root = document2?.documentElement;
|
|
422
443
|
const width = root?.clientWidth;
|
|
423
444
|
const height = root?.clientHeight;
|
|
424
445
|
if (typeof width === "number" && width > 0 && typeof height === "number" && height > 0) {
|
|
425
|
-
|
|
446
|
+
const fixed = document2 && root && fixedContainingBlock(document2, root);
|
|
447
|
+
return fixed ? { height: fixed.height, width: fixed.width } : { width, height };
|
|
426
448
|
}
|
|
427
449
|
const currentWindow = ownerWindow(context);
|
|
428
450
|
return {
|
|
@@ -467,14 +489,17 @@ function roundedRectPath(rect, viewport, options, context) {
|
|
|
467
489
|
"Z"
|
|
468
490
|
].join(" ");
|
|
469
491
|
}
|
|
470
|
-
|
|
492
|
+
function isPendingTarget(value) {
|
|
493
|
+
return typeof value?.then === "function";
|
|
494
|
+
}
|
|
495
|
+
function resolveTargetElement(target, options, path = "target") {
|
|
471
496
|
const rootDocument = options.document;
|
|
497
|
+
const validate = (element) => rootDocument ? validateTargetElement(element, rootDocument, path) : element;
|
|
472
498
|
if (typeof target === "string") {
|
|
473
|
-
|
|
474
|
-
return rootDocument ? validateTargetElement(element, rootDocument, path) : element;
|
|
499
|
+
return validate(rootDocument ? rootDocument.querySelector(target) : typeof document === "undefined" ? null : document.querySelector(target));
|
|
475
500
|
} else if (typeof target === "function") {
|
|
476
|
-
const element =
|
|
477
|
-
return
|
|
501
|
+
const element = target({ signal: options.signal });
|
|
502
|
+
return isPendingTarget(element) ? element.then(validate) : validate(element);
|
|
478
503
|
}
|
|
479
504
|
if (rootDocument)
|
|
480
505
|
return validateTargetElement(target, rootDocument, path);
|
|
@@ -754,7 +779,7 @@ class OverlayElement extends GlowTourElement {
|
|
|
754
779
|
for (const [name, value] of Object.entries(OVERLAY_IDLE_ATTRIBUTES)) {
|
|
755
780
|
el.setAttribute(name, value);
|
|
756
781
|
}
|
|
757
|
-
el.style.setProperty("height", "100lvh");
|
|
782
|
+
el.style.setProperty("height", "max(100%, 100lvh)");
|
|
758
783
|
this.syncViewBox();
|
|
759
784
|
}
|
|
760
785
|
_getPathElement() {
|
|
@@ -1493,6 +1518,9 @@ class PopoverElement extends GlowTourElement {
|
|
|
1493
1518
|
}
|
|
1494
1519
|
return this._centerPosition(popoverPosition, viewport);
|
|
1495
1520
|
}
|
|
1521
|
+
setAwaitingTarget(awaiting) {
|
|
1522
|
+
this.mutationLease.setAttribute("data-glow-tour-awaiting-target", awaiting ? "" : null);
|
|
1523
|
+
}
|
|
1496
1524
|
_centerPosition(popoverPosition, viewport) {
|
|
1497
1525
|
return {
|
|
1498
1526
|
arrowOffset: null,
|
|
@@ -1999,7 +2027,8 @@ function mergeScrollOptions(defaults, overrides) {
|
|
|
1999
2027
|
return {
|
|
2000
2028
|
behavior: overrides?.behavior ?? defaults?.behavior,
|
|
2001
2029
|
block: overrides?.block ?? defaults?.block,
|
|
2002
|
-
inline: overrides?.inline ?? defaults?.inline
|
|
2030
|
+
inline: overrides?.inline ?? defaults?.inline,
|
|
2031
|
+
returnDelay: overrides?.returnDelay ?? defaults?.returnDelay
|
|
2003
2032
|
};
|
|
2004
2033
|
}
|
|
2005
2034
|
function mergeAnimationOptions(defaults, overrides) {
|
|
@@ -2071,6 +2100,8 @@ var SCROLL_SETTLE_STILL_FRAMES = 2;
|
|
|
2071
2100
|
var SCROLL_SETTLE_GRACE_FRAMES = 3;
|
|
2072
2101
|
var SCROLL_SETTLE_EPSILON = 0.5;
|
|
2073
2102
|
var SCROLL_SETTLE_TIMEOUT = 2000;
|
|
2103
|
+
var USER_SCROLL_IDLE_DELAY = 150;
|
|
2104
|
+
var DEFAULT_SCROLL_RETURN_DELAY = 500;
|
|
2074
2105
|
var ACTIVE_MODAL_BY_DOCUMENT = new WeakMap;
|
|
2075
2106
|
var DEFAULT_SHORTCUTS = {
|
|
2076
2107
|
previous: ["ArrowLeft", "Backspace"],
|
|
@@ -2092,6 +2123,8 @@ class DomTourViewDriver {
|
|
|
2092
2123
|
active = false;
|
|
2093
2124
|
frozen = false;
|
|
2094
2125
|
awaitingStepUi = false;
|
|
2126
|
+
userScrolling = false;
|
|
2127
|
+
scrollTimer;
|
|
2095
2128
|
activeTarget = null;
|
|
2096
2129
|
targetFocusedAtFreeze = false;
|
|
2097
2130
|
lastTargetRect = null;
|
|
@@ -2106,6 +2139,8 @@ class DomTourViewDriver {
|
|
|
2106
2139
|
handledTriggerClick = null;
|
|
2107
2140
|
popover = null;
|
|
2108
2141
|
presentationDirty = false;
|
|
2142
|
+
targetPending = false;
|
|
2143
|
+
popoverHidden = false;
|
|
2109
2144
|
focusToRestore = null;
|
|
2110
2145
|
appliedAllowInteraction = false;
|
|
2111
2146
|
pointerFading = false;
|
|
@@ -2164,11 +2199,40 @@ class DomTourViewDriver {
|
|
|
2164
2199
|
this.pointer?.initializeProps();
|
|
2165
2200
|
this.refreshRegisteredElements();
|
|
2166
2201
|
}
|
|
2202
|
+
setTargetPending(pending) {
|
|
2203
|
+
if (this.disposed || this.targetPending === pending)
|
|
2204
|
+
return;
|
|
2205
|
+
this.targetPending = pending;
|
|
2206
|
+
this.popover?.setAwaitingTarget(pending);
|
|
2207
|
+
const step = this.currentStep;
|
|
2208
|
+
if (!step)
|
|
2209
|
+
return;
|
|
2210
|
+
const focused = this.popover?.getElement()?.ownerDocument.activeElement;
|
|
2211
|
+
const refocus = pending && step.autoFocuses() && this.findTriggers("advance").includes(focused);
|
|
2212
|
+
this.syncControlState(step);
|
|
2213
|
+
if (refocus)
|
|
2214
|
+
this.focusGuard.focus();
|
|
2215
|
+
}
|
|
2216
|
+
setPopoverHidden(hidden) {
|
|
2217
|
+
if (this.disposed || this.popoverHidden === hidden)
|
|
2218
|
+
return;
|
|
2219
|
+
this.popoverHidden = hidden;
|
|
2220
|
+
const step = this.currentStep;
|
|
2221
|
+
const target = this.activeTarget;
|
|
2222
|
+
if (!this.active || !step || !target)
|
|
2223
|
+
return;
|
|
2224
|
+
if (hidden) {
|
|
2225
|
+
this.liftModality();
|
|
2226
|
+
this.concealPopover();
|
|
2227
|
+
} else {
|
|
2228
|
+
this.revealPopover(step, target, this.generation);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2167
2231
|
async show(step, direction, signal, onBeforePopoverAppear) {
|
|
2168
2232
|
this.throwIfAborted(signal);
|
|
2169
2233
|
const generation = this.beginGeneration();
|
|
2170
2234
|
const removeAbort = this.cancelAnimationsOnAbort(signal);
|
|
2171
|
-
const replaceVisiblePopover = this.active && onBeforePopoverAppear !== undefined;
|
|
2235
|
+
const replaceVisiblePopover = this.active && !this.popoverHidden && onBeforePopoverAppear !== undefined;
|
|
2172
2236
|
let removeTransitionListeners = () => {};
|
|
2173
2237
|
try {
|
|
2174
2238
|
this.cleanupStepResources();
|
|
@@ -2216,8 +2280,7 @@ class DomTourViewDriver {
|
|
|
2216
2280
|
this.lastTargetRect = snapshotRect(targetRect);
|
|
2217
2281
|
this.lastViewport = snapshotViewport(target);
|
|
2218
2282
|
this.active = true;
|
|
2219
|
-
this.
|
|
2220
|
-
this.activateFocus(step, target, direction, generation);
|
|
2283
|
+
this.engagePopover(step, target, direction, generation);
|
|
2221
2284
|
this.syncScrollLock(step);
|
|
2222
2285
|
this.throwIfStale(generation, signal);
|
|
2223
2286
|
removeTransitionListeners();
|
|
@@ -2238,6 +2301,7 @@ class DomTourViewDriver {
|
|
|
2238
2301
|
this.throwIfAborted(signal);
|
|
2239
2302
|
const generation = this.beginGeneration();
|
|
2240
2303
|
const removeAbort = this.cancelAnimationsOnAbort(signal);
|
|
2304
|
+
this.setTargetPending(false);
|
|
2241
2305
|
try {
|
|
2242
2306
|
this.cleanupStepResources();
|
|
2243
2307
|
this.releaseModality();
|
|
@@ -2270,6 +2334,7 @@ class DomTourViewDriver {
|
|
|
2270
2334
|
releaseMount() {
|
|
2271
2335
|
if (this.disposed)
|
|
2272
2336
|
return;
|
|
2337
|
+
this.setTargetPending(false);
|
|
2273
2338
|
this.beginGeneration();
|
|
2274
2339
|
this.cleanupStepResources();
|
|
2275
2340
|
this.releaseModality();
|
|
@@ -2320,8 +2385,7 @@ class DomTourViewDriver {
|
|
|
2320
2385
|
this.awaitingStepUi = true;
|
|
2321
2386
|
await this.appear(() => targetRect, step, generation, null, false);
|
|
2322
2387
|
this.throwIfStale(generation);
|
|
2323
|
-
this.
|
|
2324
|
-
this.activateFocus(step, target, this.direction, generation);
|
|
2388
|
+
this.engagePopover(step, target, this.direction, generation);
|
|
2325
2389
|
this.syncScrollLock(step);
|
|
2326
2390
|
this.throwIfStale(generation);
|
|
2327
2391
|
this.attachStepResources(step, target, generation, signal);
|
|
@@ -2343,6 +2407,10 @@ class DomTourViewDriver {
|
|
|
2343
2407
|
return;
|
|
2344
2408
|
}
|
|
2345
2409
|
this.claimModal();
|
|
2410
|
+
if (this.popoverHidden) {
|
|
2411
|
+
this.liftModality();
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2346
2414
|
const popover = this.popover?.getElement();
|
|
2347
2415
|
if (isHTMLElement(popover, this.root ?? popover))
|
|
2348
2416
|
popover.setAttribute("aria-modal", "true");
|
|
@@ -2378,13 +2446,16 @@ class DomTourViewDriver {
|
|
|
2378
2446
|
this.modalDocument = document2;
|
|
2379
2447
|
}
|
|
2380
2448
|
releaseModality() {
|
|
2381
|
-
this.
|
|
2382
|
-
this.restoreInertBranches();
|
|
2449
|
+
this.liftModality();
|
|
2383
2450
|
const document2 = this.modalDocument;
|
|
2384
2451
|
if (document2 && ACTIVE_MODAL_BY_DOCUMENT.get(document2) === this.modalToken) {
|
|
2385
2452
|
ACTIVE_MODAL_BY_DOCUMENT.delete(document2);
|
|
2386
2453
|
}
|
|
2387
2454
|
this.modalDocument = null;
|
|
2455
|
+
}
|
|
2456
|
+
liftModality() {
|
|
2457
|
+
this.popover?.getElement()?.removeAttribute("aria-modal");
|
|
2458
|
+
this.restoreInertBranches();
|
|
2388
2459
|
this.modalRoot = null;
|
|
2389
2460
|
}
|
|
2390
2461
|
restoreInertBranches() {
|
|
@@ -2423,7 +2494,7 @@ class DomTourViewDriver {
|
|
|
2423
2494
|
enterStepUi(targetRect, step) {
|
|
2424
2495
|
const popoverPlacement = this.popover?.resolvePosition(targetRect, this.elementProps(step)).placement;
|
|
2425
2496
|
return Promise.all([
|
|
2426
|
-
this.popover?.present(targetRect, this.elementProps(step))
|
|
2497
|
+
!this.popoverHidden && this.popover?.present(targetRect, this.elementProps(step)) || Promise.resolve(),
|
|
2427
2498
|
this.presentPointer(targetRect, step, popoverPlacement) ?? Promise.resolve()
|
|
2428
2499
|
]);
|
|
2429
2500
|
}
|
|
@@ -2472,6 +2543,20 @@ class DomTourViewDriver {
|
|
|
2472
2543
|
}
|
|
2473
2544
|
});
|
|
2474
2545
|
}
|
|
2546
|
+
const targetDocument = ownerDocument(target);
|
|
2547
|
+
if (typeof targetDocument?.addEventListener === "function") {
|
|
2548
|
+
const passive = { capture: true, passive: true };
|
|
2549
|
+
this.listen(targetDocument, "scroll", (event) => this.onUserScroll(event, step, generation), passive);
|
|
2550
|
+
const interrupt = () => {
|
|
2551
|
+
if (!this.isCurrentGeneration(generation) || !this.userScrolling || !this.awaitingStepUi)
|
|
2552
|
+
return;
|
|
2553
|
+
this.awaitingStepUi = false;
|
|
2554
|
+
this.cancelScroll?.();
|
|
2555
|
+
this.waitForScrollIdle(step, generation);
|
|
2556
|
+
};
|
|
2557
|
+
this.listen(targetDocument, "wheel", interrupt, passive);
|
|
2558
|
+
this.listen(targetDocument, "touchmove", interrupt, passive);
|
|
2559
|
+
}
|
|
2475
2560
|
this.attachButtonHandlers(step);
|
|
2476
2561
|
this.observeControls(step, generation);
|
|
2477
2562
|
this.syncControlState(step);
|
|
@@ -2567,7 +2652,7 @@ class DomTourViewDriver {
|
|
|
2567
2652
|
this.syncShortcutLabels(step);
|
|
2568
2653
|
}
|
|
2569
2654
|
this.overlay?.updatePosition(targetRect, this.elementProps(step), presentationChanged, (transition) => this.observeDynamicOperation(transition, generation));
|
|
2570
|
-
if (!this.awaitingStepUi)
|
|
2655
|
+
if (!this.awaitingStepUi && !this.userScrolling)
|
|
2571
2656
|
this.trackStepUi(targetRect, step, generation, presentationChanged);
|
|
2572
2657
|
this.lastTargetRect = targetSnapshot;
|
|
2573
2658
|
this.lastViewport = viewportSnapshot;
|
|
@@ -2575,7 +2660,7 @@ class DomTourViewDriver {
|
|
|
2575
2660
|
this.presentationDirty = false;
|
|
2576
2661
|
}
|
|
2577
2662
|
trackStepUi(targetRect, step, generation, presentationChanged) {
|
|
2578
|
-
const popoverPlacement = this.
|
|
2663
|
+
const popoverPlacement = this.placePopover(targetRect, step, generation);
|
|
2579
2664
|
if (presentationChanged) {
|
|
2580
2665
|
if (this.pointerFading)
|
|
2581
2666
|
this.pointerFading = false;
|
|
@@ -2592,6 +2677,68 @@ class DomTourViewDriver {
|
|
|
2592
2677
|
this.observeDynamicOperation(this.pointer?.disappear(), generation);
|
|
2593
2678
|
}
|
|
2594
2679
|
}
|
|
2680
|
+
onUserScroll(event, step, generation) {
|
|
2681
|
+
const target = this.activeTarget;
|
|
2682
|
+
if (!target || !this.isCurrentGeneration(generation) || this.frozen || this.awaitingStepUi || step.detached || !step.allowsScroll() || !event.target?.contains?.(target))
|
|
2683
|
+
return;
|
|
2684
|
+
if (!this.userScrolling) {
|
|
2685
|
+
this.userScrolling = true;
|
|
2686
|
+
this.popover?.cancelAnimations();
|
|
2687
|
+
this.pointer?.cancelAnimations();
|
|
2688
|
+
if (!this.popoverHidden)
|
|
2689
|
+
this.observeDynamicOperation(this.popover?.disappear(false), generation);
|
|
2690
|
+
this.observeDynamicOperation(this.pointer?.disappear(), generation);
|
|
2691
|
+
}
|
|
2692
|
+
this.waitForScrollIdle(step, generation);
|
|
2693
|
+
}
|
|
2694
|
+
waitForScrollIdle(step, generation) {
|
|
2695
|
+
clearTimeout(this.scrollTimer);
|
|
2696
|
+
this.scrollTimer = setTimeout(() => this.settleScroll(step, generation), USER_SCROLL_IDLE_DELAY);
|
|
2697
|
+
}
|
|
2698
|
+
settleScroll(step, generation) {
|
|
2699
|
+
const target = this.activeTarget;
|
|
2700
|
+
if (!target)
|
|
2701
|
+
return;
|
|
2702
|
+
const behavior = step.props.get().behavior;
|
|
2703
|
+
const returnDelay = behavior?.scroll?.returnDelay ?? DEFAULT_SCROLL_RETURN_DELAY;
|
|
2704
|
+
if (returnDelay === false || behavior?.autoScroll === false || isInViewport(target.getBoundingClientRect(), target))
|
|
2705
|
+
this.revealAfterScroll(step, target, generation);
|
|
2706
|
+
else
|
|
2707
|
+
this.scrollTimer = setTimeout(() => void this.scrollBack(step, target, generation), returnDelay);
|
|
2708
|
+
}
|
|
2709
|
+
async scrollBack(step, target, generation) {
|
|
2710
|
+
const signal = this.currentSignal;
|
|
2711
|
+
if (!signal)
|
|
2712
|
+
return;
|
|
2713
|
+
const scrollingBack = () => this.userScrolling && this.awaitingStepUi && !signal.aborted && this.isCurrentGeneration(generation);
|
|
2714
|
+
this.awaitingStepUi = true;
|
|
2715
|
+
try {
|
|
2716
|
+
await (this.scrollTargetIntoView(step, target, signal) ?? this.waitForScrollToSettle(target, signal));
|
|
2717
|
+
} catch (error) {
|
|
2718
|
+
if (scrollingBack())
|
|
2719
|
+
this.observeDynamicOperation(Promise.reject(error), generation);
|
|
2720
|
+
}
|
|
2721
|
+
if (scrollingBack())
|
|
2722
|
+
this.revealAfterScroll(step, target, generation);
|
|
2723
|
+
}
|
|
2724
|
+
revealAfterScroll(step, target, generation) {
|
|
2725
|
+
this.stopUserScroll();
|
|
2726
|
+
this.awaitingStepUi = false;
|
|
2727
|
+
this.pointerFading = false;
|
|
2728
|
+
this.popover?.cancelAnimations();
|
|
2729
|
+
this.pointer?.cancelAnimations();
|
|
2730
|
+
this.observeDynamicOperation(this.enterStepUi(presentationRect(step, target), step), generation);
|
|
2731
|
+
}
|
|
2732
|
+
stopUserScroll() {
|
|
2733
|
+
clearTimeout(this.scrollTimer);
|
|
2734
|
+
this.userScrolling = false;
|
|
2735
|
+
}
|
|
2736
|
+
placePopover(targetRect, step, generation) {
|
|
2737
|
+
if (this.popoverHidden) {
|
|
2738
|
+
return this.popover?.resolvePosition(targetRect, this.elementProps(step)).placement;
|
|
2739
|
+
}
|
|
2740
|
+
return this.popover?.updatePosition(targetRect, this.elementProps(step), (reposition) => this.observeDynamicOperation(reposition, generation));
|
|
2741
|
+
}
|
|
2595
2742
|
observeDynamicOperation(operation, generation) {
|
|
2596
2743
|
if (!operation)
|
|
2597
2744
|
return;
|
|
@@ -2606,7 +2753,7 @@ class DomTourViewDriver {
|
|
|
2606
2753
|
}
|
|
2607
2754
|
handleKeydown(event) {
|
|
2608
2755
|
const step = this.currentStep;
|
|
2609
|
-
if (!step || event.defaultPrevented || event.isComposing || event.ctrlKey || event.metaKey || event.altKey)
|
|
2756
|
+
if (!step || this.popoverHidden || event.defaultPrevented || event.isComposing || event.ctrlKey || event.metaKey || event.altKey)
|
|
2610
2757
|
return;
|
|
2611
2758
|
if (event.key === "Tab" && !step.allowsInteraction()) {
|
|
2612
2759
|
this.loopFocus(event);
|
|
@@ -2699,6 +2846,44 @@ class DomTourViewDriver {
|
|
|
2699
2846
|
focusable[0]?.focus();
|
|
2700
2847
|
}
|
|
2701
2848
|
}
|
|
2849
|
+
engagePopover(step, target, direction, generation) {
|
|
2850
|
+
this.syncModality(step.allowsInteraction());
|
|
2851
|
+
if (this.popoverHidden)
|
|
2852
|
+
this.concealPopover();
|
|
2853
|
+
else if (this.isPopoverConcealed())
|
|
2854
|
+
this.revealPopover(step, target, generation);
|
|
2855
|
+
else
|
|
2856
|
+
this.activateFocus(step, target, direction, generation);
|
|
2857
|
+
}
|
|
2858
|
+
concealPopover() {
|
|
2859
|
+
this.focusToRestore = this.focusGuard.release() ?? this.focusToRestore;
|
|
2860
|
+
const popover = this.popover;
|
|
2861
|
+
const element = popover?.getElement();
|
|
2862
|
+
if (!popover || !element)
|
|
2863
|
+
return;
|
|
2864
|
+
const focused = element.ownerDocument.activeElement;
|
|
2865
|
+
if (isHTMLElement(focused, element) && element.contains(focused)) {
|
|
2866
|
+
focused.blur();
|
|
2867
|
+
this.activeTarget?.focus();
|
|
2868
|
+
}
|
|
2869
|
+
if (this.isPopoverConcealed())
|
|
2870
|
+
return;
|
|
2871
|
+
popover.cancelAnimations();
|
|
2872
|
+
this.observeDynamicOperation(popover.disappear(), this.generation);
|
|
2873
|
+
}
|
|
2874
|
+
revealPopover(step, target, generation) {
|
|
2875
|
+
this.focusGuard.captureInitialFocus(target, this.focusToRestore);
|
|
2876
|
+
this.popover?.cancelAnimations();
|
|
2877
|
+
this.observeDynamicOperation(this.popover?.present(this.lastTargetRect, this.elementProps(step)).then(() => {
|
|
2878
|
+
if (!this.isCurrentGeneration(generation) || this.popoverHidden)
|
|
2879
|
+
return;
|
|
2880
|
+
this.syncModality(!this.frozen && step.allowsInteraction());
|
|
2881
|
+
this.activateFocus(step, target, this.direction, generation);
|
|
2882
|
+
}), generation);
|
|
2883
|
+
}
|
|
2884
|
+
isPopoverConcealed() {
|
|
2885
|
+
return this.popover?.getElement()?.getAttribute("aria-hidden") === "true";
|
|
2886
|
+
}
|
|
2702
2887
|
activateFocus(step, target, direction, generation) {
|
|
2703
2888
|
const popover = this.popover?.getElement();
|
|
2704
2889
|
if (!isHTMLElement(popover, this.root ?? popover))
|
|
@@ -2867,6 +3052,7 @@ class DomTourViewDriver {
|
|
|
2867
3052
|
cleanupStepResources() {
|
|
2868
3053
|
this.cancelScroll?.();
|
|
2869
3054
|
this.cancelScroll = null;
|
|
3055
|
+
this.stopUserScroll();
|
|
2870
3056
|
this.presentationDirty = false;
|
|
2871
3057
|
this.pointerFading = false;
|
|
2872
3058
|
if (this.rafId !== null)
|
|
@@ -2896,6 +3082,14 @@ class DomTourViewDriver {
|
|
|
2896
3082
|
this.targetFocusedAtFreeze = this.isFocusInsideTarget(target);
|
|
2897
3083
|
this.cleanupTargetResources();
|
|
2898
3084
|
this.applyInteractionLock(step, true);
|
|
3085
|
+
if (this.userScrolling) {
|
|
3086
|
+
this.stopUserScroll();
|
|
3087
|
+
const targetRect = this.lastTargetRect;
|
|
3088
|
+
this.popover?.cancelAnimations();
|
|
3089
|
+
if (targetRect && !this.popoverHidden) {
|
|
3090
|
+
this.observeDynamicOperation(this.popover?.present(targetRect, this.elementProps(step)), generation);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
2899
3093
|
Promise.resolve(this.commands?.targetDisconnected(target)).catch((error) => {
|
|
2900
3094
|
this.commands?.reportError(error).catch(() => {});
|
|
2901
3095
|
});
|
|
@@ -2909,7 +3103,7 @@ class DomTourViewDriver {
|
|
|
2909
3103
|
this.applyInteractionLock(step, false);
|
|
2910
3104
|
this.appliedAllowInteraction = step.allowsInteraction();
|
|
2911
3105
|
const popover = this.popover?.getElement();
|
|
2912
|
-
if (isHTMLElement(popover, this.root ?? popover)) {
|
|
3106
|
+
if (!this.popoverHidden && isHTMLElement(popover, this.root ?? popover)) {
|
|
2913
3107
|
this.focusGuard.update({
|
|
2914
3108
|
allowedTarget: target,
|
|
2915
3109
|
allowTargetInteraction: step.allowsInteraction(),
|
|
@@ -2921,12 +3115,14 @@ class DomTourViewDriver {
|
|
|
2921
3115
|
}
|
|
2922
3116
|
syncInteraction(step) {
|
|
2923
3117
|
const target = this.activeTarget;
|
|
2924
|
-
if (this.disposed || !this.active || this.frozen || this.awaitingStepUi || this.currentStep !== step || !target)
|
|
3118
|
+
if (this.disposed || !this.active || this.frozen || this.awaitingStepUi && !this.userScrolling || this.currentStep !== step || !target)
|
|
2925
3119
|
return;
|
|
2926
3120
|
const focusWasInTarget = this.isFocusInsideTarget(target);
|
|
2927
3121
|
this.applyInteraction(step, target);
|
|
2928
3122
|
if (focusWasInTarget && !step.allowsInteraction() && step.autoFocuses())
|
|
2929
3123
|
this.focusGuard.focus();
|
|
3124
|
+
if (this.userScrolling)
|
|
3125
|
+
return;
|
|
2930
3126
|
const targetRect = target.getBoundingClientRect();
|
|
2931
3127
|
this.pointer?.cancelAnimations();
|
|
2932
3128
|
this.observeDynamicOperation(this.presentPointer(targetRect, step, this.popover?.resolvePosition(targetRect, this.elementProps(step)).placement), this.generation);
|
|
@@ -2965,7 +3161,7 @@ class DomTourViewDriver {
|
|
|
2965
3161
|
const generation = this.generation;
|
|
2966
3162
|
const targetRect = presentationRect(step, target);
|
|
2967
3163
|
this.observeDynamicOperation(this.overlay?.animateTo(targetRect, this.elementProps(step)), generation);
|
|
2968
|
-
const placement = this.
|
|
3164
|
+
const placement = this.placePopover(targetRect, step, generation);
|
|
2969
3165
|
if (this.isPointerEnabled(step)) {
|
|
2970
3166
|
this.observeDynamicOperation(this.pointer?.moveToTarget(targetRect, step.props.get(), true, placement), generation);
|
|
2971
3167
|
}
|
|
@@ -2998,14 +3194,17 @@ class DomTourViewDriver {
|
|
|
2998
3194
|
return null;
|
|
2999
3195
|
if (isInViewport(target.getBoundingClientRect(), target))
|
|
3000
3196
|
return null;
|
|
3001
|
-
|
|
3002
|
-
if (!currentWindow)
|
|
3197
|
+
if (!this.getWindow(target))
|
|
3003
3198
|
return null;
|
|
3004
|
-
|
|
3199
|
+
return this.scrollTargetIntoView(step, target, signal);
|
|
3200
|
+
}
|
|
3201
|
+
scrollTargetIntoView(step, target, signal) {
|
|
3202
|
+
const scroll = step.props.get().behavior?.scroll;
|
|
3203
|
+
const behavior = prefersReducedMotion(target) ? "instant" : scroll?.behavior ?? "smooth";
|
|
3005
3204
|
target.scrollIntoView({
|
|
3006
3205
|
behavior,
|
|
3007
|
-
block:
|
|
3008
|
-
inline:
|
|
3206
|
+
block: scroll?.block ?? "center",
|
|
3207
|
+
inline: scroll?.inline ?? "nearest"
|
|
3009
3208
|
});
|
|
3010
3209
|
if (behavior === "instant")
|
|
3011
3210
|
return null;
|
|
@@ -3284,8 +3483,8 @@ class ActiveStep {
|
|
|
3284
3483
|
allowsScroll() {
|
|
3285
3484
|
return this.props.get().behavior?.allowScroll !== false;
|
|
3286
3485
|
}
|
|
3287
|
-
|
|
3288
|
-
return
|
|
3486
|
+
resolveTarget(signal) {
|
|
3487
|
+
return resolveTargetElement(this.definition.target, { document: this.rootDocument, signal }, this.path);
|
|
3289
3488
|
}
|
|
3290
3489
|
detach() {
|
|
3291
3490
|
const body = (this.rootDocument ?? globalThis.document)?.body ?? null;
|
|
@@ -3628,7 +3827,9 @@ class TourController {
|
|
|
3628
3827
|
recoveringTarget = null;
|
|
3629
3828
|
operationToken = 0;
|
|
3630
3829
|
publicationRevision = 0;
|
|
3830
|
+
awaitingTargetOperation = null;
|
|
3631
3831
|
operation = null;
|
|
3832
|
+
popoverHidden = false;
|
|
3632
3833
|
disposed = false;
|
|
3633
3834
|
retainedPresentation = null;
|
|
3634
3835
|
stateListeners = new Set;
|
|
@@ -3662,7 +3863,7 @@ class TourController {
|
|
|
3662
3863
|
canPrevious: () => this.canNavigate("previous"),
|
|
3663
3864
|
cancel: (source) => this.cancel(source),
|
|
3664
3865
|
goTo: (id) => this.goTo(id),
|
|
3665
|
-
isAdvanceDisabled: () => !this.isPresentedAdvanceAvailable(),
|
|
3866
|
+
isAdvanceDisabled: () => this.awaitingTargetOperation !== null || !this.isPresentedAdvanceAvailable(),
|
|
3666
3867
|
isCancelDisabled: () => !this.isPresentedCancelAvailable(),
|
|
3667
3868
|
isPreviousDisabled: () => !this.isPresentedPreviousAvailable(),
|
|
3668
3869
|
previous: (source) => this.previous(source),
|
|
@@ -3703,6 +3904,8 @@ class TourController {
|
|
|
3703
3904
|
this.error = null;
|
|
3704
3905
|
this.commandSource = "api";
|
|
3705
3906
|
this.retainedPresentation = retainedPresentation;
|
|
3907
|
+
this.popoverHidden = false;
|
|
3908
|
+
this.driver.setPopoverHidden?.(false);
|
|
3706
3909
|
this.pendingTourStart = undefined;
|
|
3707
3910
|
try {
|
|
3708
3911
|
this.setStatus("starting");
|
|
@@ -3756,6 +3959,17 @@ class TourController {
|
|
|
3756
3959
|
await this.handleFailure(error, operation);
|
|
3757
3960
|
}
|
|
3758
3961
|
}
|
|
3962
|
+
setPopoverHidden(hidden) {
|
|
3963
|
+
this.assertNotDisposed();
|
|
3964
|
+
if (!this.isRunning() || this.popoverHidden === hidden)
|
|
3965
|
+
return;
|
|
3966
|
+
this.popoverHidden = hidden;
|
|
3967
|
+
this.driver.setPopoverHidden?.(hidden);
|
|
3968
|
+
this.publish();
|
|
3969
|
+
}
|
|
3970
|
+
isRunning() {
|
|
3971
|
+
return this.status === "starting" || this.status === "transitioning" || this.status === "active";
|
|
3972
|
+
}
|
|
3759
3973
|
dispose() {
|
|
3760
3974
|
if (this.disposed)
|
|
3761
3975
|
return;
|
|
@@ -3924,23 +4138,38 @@ class TourController {
|
|
|
3924
4138
|
const timeout = missingTarget?.timeout ?? DEFAULT_TARGET_TIMEOUT;
|
|
3925
4139
|
const startedAt = Date.now();
|
|
3926
4140
|
step.detached = false;
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
4141
|
+
try {
|
|
4142
|
+
while (true) {
|
|
4143
|
+
const pending = step.resolveTarget(signal);
|
|
4144
|
+
if (isPendingTarget(pending))
|
|
4145
|
+
this.setAwaitingTarget(operation, true);
|
|
4146
|
+
const target = await pending;
|
|
4147
|
+
this.assertCurrent(operation);
|
|
4148
|
+
if (target)
|
|
4149
|
+
return target;
|
|
4150
|
+
if (strategy === "skip")
|
|
4151
|
+
return null;
|
|
4152
|
+
const body = strategy === "detached" && step.detach();
|
|
4153
|
+
if (body)
|
|
4154
|
+
return body;
|
|
4155
|
+
if (strategy !== "wait" || Date.now() - startedAt >= timeout) {
|
|
4156
|
+
throw this.missingTargetError(step);
|
|
4157
|
+
}
|
|
4158
|
+
this.setAwaitingTarget(operation, true);
|
|
4159
|
+
await abortableDelay(16, signal);
|
|
4160
|
+
this.assertCurrent(operation);
|
|
3939
4161
|
}
|
|
3940
|
-
|
|
3941
|
-
this.
|
|
4162
|
+
} finally {
|
|
4163
|
+
this.setAwaitingTarget(operation, false);
|
|
3942
4164
|
}
|
|
3943
4165
|
}
|
|
4166
|
+
setAwaitingTarget(operation, awaiting) {
|
|
4167
|
+
if (this.awaitingTargetOperation === operation === awaiting)
|
|
4168
|
+
return;
|
|
4169
|
+
this.awaitingTargetOperation = awaiting ? operation : null;
|
|
4170
|
+
this.driver.setTargetPending?.(awaiting);
|
|
4171
|
+
this.publish();
|
|
4172
|
+
}
|
|
3944
4173
|
async pollForTarget(step, operation, budgetMs) {
|
|
3945
4174
|
const startedAt = Date.now();
|
|
3946
4175
|
while (true) {
|
|
@@ -4125,6 +4354,10 @@ class TourController {
|
|
|
4125
4354
|
this.operationToken += 1;
|
|
4126
4355
|
this.operation?.abort();
|
|
4127
4356
|
this.operation = null;
|
|
4357
|
+
if (this.awaitingTargetOperation !== null) {
|
|
4358
|
+
this.awaitingTargetOperation = null;
|
|
4359
|
+
this.driver.setTargetPending?.(false);
|
|
4360
|
+
}
|
|
4128
4361
|
}
|
|
4129
4362
|
signalFor(operation) {
|
|
4130
4363
|
this.assertCurrent(operation);
|
|
@@ -4288,6 +4521,8 @@ class TourController {
|
|
|
4288
4521
|
isFirstStep,
|
|
4289
4522
|
isLastStep,
|
|
4290
4523
|
status: this.status,
|
|
4524
|
+
awaitingTarget: this.awaitingTargetOperation !== null,
|
|
4525
|
+
popoverHidden: this.popoverHidden && this.isRunning(),
|
|
4291
4526
|
error: this.error
|
|
4292
4527
|
});
|
|
4293
4528
|
}
|
|
@@ -4326,7 +4561,9 @@ function createGlowTour(options = {}) {
|
|
|
4326
4561
|
create: (name, options2) => controller.create(name, options2),
|
|
4327
4562
|
dispose: () => controller.dispose(),
|
|
4328
4563
|
goTo: (id) => controller.goTo(id),
|
|
4564
|
+
hidePopover: () => controller.setPopoverHidden(true),
|
|
4329
4565
|
previous: () => controller.previous(),
|
|
4566
|
+
showPopover: () => controller.setPopoverHidden(false),
|
|
4330
4567
|
start: (workflow, runOptions) => controller.start(workflow, runOptions),
|
|
4331
4568
|
state: controller.state
|
|
4332
4569
|
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glowhop/core-tour",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Framework-agnostic tour controller and DOM runtime
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Framework-agnostic product tour engine for GlowTour.js: the tour controller and DOM runtime behind onboarding tours in any framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://glowtour.dev/",
|
|
7
7
|
"bugs": {
|
|
8
8
|
"url": "https://github.com/Glowhop/GlowTour.js/issues"
|
|
9
9
|
},
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"dom",
|
|
21
21
|
"headless",
|
|
22
22
|
"framework-agnostic",
|
|
23
|
-
"ssr"
|
|
23
|
+
"ssr",
|
|
24
|
+
"onboarding-tour",
|
|
25
|
+
"intro-js-alternative"
|
|
24
26
|
],
|
|
25
27
|
"engines": {
|
|
26
28
|
"node": ">=18.19.1"
|
package/runtime/active-step.d.ts
CHANGED
|
@@ -22,7 +22,8 @@ export declare class ActiveStep<T> {
|
|
|
22
22
|
autoFocuses(): boolean;
|
|
23
23
|
/** Reads `behavior.allowScroll` live: `props.update({ behavior })` changes it while the step runs. */
|
|
24
24
|
allowsScroll(): boolean;
|
|
25
|
-
|
|
25
|
+
/** Returns the target, or the promise to wait on when the resolver is async. */
|
|
26
|
+
resolveTarget(signal: AbortSignal): HTMLElement | Promise<HTMLElement | null> | null;
|
|
26
27
|
/** Marks the step detached and returns the body it stands on, or `null` without a document. */
|
|
27
28
|
detach(): HTMLElement;
|
|
28
29
|
snapshot(): Readonly<{
|
|
@@ -37,7 +37,14 @@ export declare class TourController<T> {
|
|
|
37
37
|
private recoveringTarget;
|
|
38
38
|
private operationToken;
|
|
39
39
|
private publicationRevision;
|
|
40
|
+
/**
|
|
41
|
+
* The operation currently parked on a target that has not resolved yet, or `null`. The step being
|
|
42
|
+
* left is still on screen while a navigation waits, so the wait is reported on its presentation.
|
|
43
|
+
*/
|
|
44
|
+
private awaitingTargetOperation;
|
|
40
45
|
private operation;
|
|
46
|
+
/** Set by `hidePopover()`, see `TourState.popoverHidden`. Only a running tour reports it. */
|
|
47
|
+
private popoverHidden;
|
|
41
48
|
private disposed;
|
|
42
49
|
private retainedPresentation;
|
|
43
50
|
private readonly stateListeners;
|
|
@@ -63,6 +70,9 @@ export declare class TourController<T> {
|
|
|
63
70
|
/** The direction of a jump from the current step to the step at `index`. */
|
|
64
71
|
private directionTo;
|
|
65
72
|
cancel(source?: TourEventSource): Promise<void>;
|
|
73
|
+
/** `tour.hidePopover()` and `tour.showPopover()`. */
|
|
74
|
+
setPopoverHidden(hidden: boolean): void;
|
|
75
|
+
private isRunning;
|
|
66
76
|
dispose(): void;
|
|
67
77
|
isDisposed(): boolean;
|
|
68
78
|
/** @internal Called by the private root bridge before it releases DOM resources. */
|
|
@@ -84,6 +94,14 @@ export declare class TourController<T> {
|
|
|
84
94
|
private runStepHook;
|
|
85
95
|
private runActions;
|
|
86
96
|
private resolveTarget;
|
|
97
|
+
/**
|
|
98
|
+
* Reports a navigation waiting on the next step's target. The presentation on screen still
|
|
99
|
+
* belongs to the step being left, so the driver marks that popover and disables its advance
|
|
100
|
+
* control until the target settles. Keyed by operation: a superseded navigation never clears the
|
|
101
|
+
* wait its replacement declared. The freeze of a target lost mid-step is deliberately not a wait
|
|
102
|
+
* here, see `recoverDisconnectedTarget`.
|
|
103
|
+
*/
|
|
104
|
+
private setAwaitingTarget;
|
|
87
105
|
/**
|
|
88
106
|
* Repeatedly re-resolves `step.target` until it succeeds or `budgetMs`
|
|
89
107
|
* elapses, polling every 16ms like `resolveTarget`. Unlike `resolveTarget`
|
package/types/index.d.ts
CHANGED
|
@@ -174,12 +174,16 @@ export interface PopoverOptions extends BaseOptions {
|
|
|
174
174
|
gap?: number;
|
|
175
175
|
}
|
|
176
176
|
/**
|
|
177
|
-
*
|
|
177
|
+
* How a step scrolls its target into view, on entrance and after the user scrolled away.
|
|
178
178
|
*
|
|
179
179
|
* A step scrolls only when part of its target falls outside the viewport, and
|
|
180
180
|
* does not wait for the scroll before presenting: the spotlight appears at once
|
|
181
181
|
* and tracks the target as the page travels, and the popover and pointer enter
|
|
182
182
|
* when the page has come to rest.
|
|
183
|
+
*
|
|
184
|
+
* While the user scrolls, the popover and pointer step aside and the spotlight
|
|
185
|
+
* keeps following the target. Once the page is still they come back, after
|
|
186
|
+
* `returnDelay` scrolls the target back into view if it was left outside it.
|
|
183
187
|
*/
|
|
184
188
|
export interface ScrollOptions {
|
|
185
189
|
/** Scroll animation. Forced to `"instant"` when the user prefers reduced motion. @default "smooth" */
|
|
@@ -188,6 +192,12 @@ export interface ScrollOptions {
|
|
|
188
192
|
block?: "start" | "center" | "end" | "nearest";
|
|
189
193
|
/** Horizontal alignment of the target in the viewport. @default "nearest" */
|
|
190
194
|
inline?: "start" | "center" | "end" | "nearest";
|
|
195
|
+
/**
|
|
196
|
+
* Milliseconds to wait, once the user stops scrolling with part of the target outside the
|
|
197
|
+
* viewport, before scrolling it back into view. `false` leaves the page where the user put it.
|
|
198
|
+
* Ignored when `autoScroll` is `false`. @default 500
|
|
199
|
+
*/
|
|
200
|
+
returnDelay?: number | false;
|
|
191
201
|
}
|
|
192
202
|
/** Animation timing configuration. */
|
|
193
203
|
export interface AnimationOptions {
|
|
@@ -380,6 +390,17 @@ export interface TourState<T> {
|
|
|
380
390
|
readonly isLastStep: boolean;
|
|
381
391
|
/** Current status of the tour. */
|
|
382
392
|
readonly status: TourStatus;
|
|
393
|
+
/**
|
|
394
|
+
* Whether a navigation is waiting for the next step's target to resolve, i.e. an async resolver
|
|
395
|
+
* or the `"wait"` missing-target strategy. The step being left stays on screen meanwhile, and its
|
|
396
|
+
* advance control is refused, so a UI can show the wait instead of looking idle.
|
|
397
|
+
*/
|
|
398
|
+
readonly awaitingTarget: boolean;
|
|
399
|
+
/**
|
|
400
|
+
* Whether `hidePopover()` hid the popover of the running tour. `false` again after
|
|
401
|
+
* `showPopover()`, and whenever a tour starts or ends.
|
|
402
|
+
*/
|
|
403
|
+
readonly popoverHidden: boolean;
|
|
383
404
|
/** Error encountered during the tour, if any. */
|
|
384
405
|
readonly error: Error | null;
|
|
385
406
|
}
|
|
@@ -407,6 +428,18 @@ export interface GlowTour<T> {
|
|
|
407
428
|
goTo(id: string): Promise<void>;
|
|
408
429
|
/** Cancel the current tour. */
|
|
409
430
|
cancel(): Promise<void>;
|
|
431
|
+
/**
|
|
432
|
+
* Show the popover again after `hidePopover()`, and move focus into it when the step auto
|
|
433
|
+
* focuses. Does nothing when no tour is running.
|
|
434
|
+
*/
|
|
435
|
+
showPopover(): void;
|
|
436
|
+
/**
|
|
437
|
+
* Hide the popover of the running tour. The overlay, the indicator and the scroll lock stay; the
|
|
438
|
+
* page is no longer inert, focus leaves the popover for the target, and the keyboard shortcuts
|
|
439
|
+
* do nothing until `showPopover()`. The popover stays hidden across steps, until `showPopover()`
|
|
440
|
+
* or the end of the tour. Does nothing when no tour is running.
|
|
441
|
+
*/
|
|
442
|
+
hidePopover(): void;
|
|
410
443
|
/** Dispose the tour and free resources. */
|
|
411
444
|
dispose(): void;
|
|
412
445
|
/** Observable store of the current tour state. */
|
package/utils/utils.d.ts
CHANGED
|
@@ -10,15 +10,16 @@ export declare function isNode(value: unknown, context?: Node | null): value is
|
|
|
10
10
|
* `getBoundingClientRect()` reports coordinates in.
|
|
11
11
|
*
|
|
12
12
|
* Deliberately not `innerWidth`/`innerHeight`: those measure the *visual*
|
|
13
|
-
* viewport, which on mobile shrinks and grows with the
|
|
14
|
-
*
|
|
15
|
-
* `viewBox` against its own
|
|
16
|
-
* `
|
|
17
|
-
* bands and a cutout that no longer
|
|
13
|
+
* viewport on some engines, which on mobile shrinks and grows with the
|
|
14
|
+
* browser's URL bar and pinch zoom, and on desktop includes the classic
|
|
15
|
+
* scrollbar. Either gap skews the overlay's `viewBox` against its own
|
|
16
|
+
* `100%`-sized box, and the default `preserveAspectRatio` then scales and
|
|
17
|
+
* centres the backdrop — leaving undimmed bands and a cutout that no longer
|
|
18
|
+
* lines up with its target.
|
|
18
19
|
*/
|
|
19
20
|
export declare function viewportDimensions(context?: Node | null): {
|
|
20
|
-
width: number;
|
|
21
21
|
height: number;
|
|
22
|
+
width: number;
|
|
22
23
|
};
|
|
23
24
|
/**
|
|
24
25
|
* The box the overlay `<svg>` is actually painted into, in CSS pixels.
|
|
@@ -36,8 +37,8 @@ export declare function viewportDimensions(context?: Node | null): {
|
|
|
36
37
|
* yet — detached nodes, server-rendered markup, test doubles.
|
|
37
38
|
*/
|
|
38
39
|
export declare function paintedBoxDimensions(element?: Element | null): {
|
|
39
|
-
width: number;
|
|
40
40
|
height: number;
|
|
41
|
+
width: number;
|
|
41
42
|
};
|
|
42
43
|
export declare function isInViewport(rect: {
|
|
43
44
|
left: number;
|
|
@@ -55,7 +56,14 @@ export declare function roundedRectPath(rect: RectGeometry, viewport: {
|
|
|
55
56
|
padding: number;
|
|
56
57
|
radius: number;
|
|
57
58
|
}, context?: Node | null): string;
|
|
59
|
+
/** Whether a target resolution is still pending, i.e. the resolver returned a promise. */
|
|
60
|
+
export declare function isPendingTarget(value: HTMLElement | null | Promise<HTMLElement | null>): value is Promise<HTMLElement | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Resolves a step's target. Deliberately not `async`: a selector or an element target settles
|
|
63
|
+
* synchronously, and only a resolver that returns a promise hands back something to wait on.
|
|
64
|
+
* Callers use {@link isPendingTarget} to tell the two apart, and a tour that has to wait says so.
|
|
65
|
+
*/
|
|
58
66
|
export declare function resolveTargetElement(target: TargetResolver, options: {
|
|
59
67
|
readonly document?: Document;
|
|
60
68
|
readonly signal: AbortSignal;
|
|
61
|
-
}, path?: string): Promise<HTMLElement | null>;
|
|
69
|
+
}, path?: string): HTMLElement | null | Promise<HTMLElement | null>;
|