@glowhop/core-tour 1.2.0 → 1.3.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 +33 -0
- package/README.md +1 -1
- package/dom/tour-view-driver.d.ts +67 -2
- package/elements/overlay.d.ts +12 -1
- package/elements/popover.d.ts +9 -1
- package/index.js +138 -68
- package/package.json +11 -2
- package/types/index.d.ts +14 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @glowhop/core-tour
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b38bc4a: Present a step while its scroll is still in flight.
|
|
8
|
+
|
|
9
|
+
Entering a step whose target was off screen used to stall: the tour waited for
|
|
10
|
+
the smooth scroll to finish before initialising anything, so the previous step's
|
|
11
|
+
elements sat frozen for the whole journey and everything then snapped into place
|
|
12
|
+
at once. On Safari before 18.2, where `scrollend` does not exist, that wait was a
|
|
13
|
+
full second on every step.
|
|
14
|
+
|
|
15
|
+
The scroll now runs alongside the presentation. The spotlight appears
|
|
16
|
+
immediately and tracks the target as the page travels; the popover and the
|
|
17
|
+
pointer enter once the page has come to rest, on a rect that will not move
|
|
18
|
+
again. When a step scrolls, the spotlight moves with the page rather than
|
|
19
|
+
morphing from the previous step's cutout; steps that do not scroll keep the
|
|
20
|
+
morph.
|
|
21
|
+
|
|
22
|
+
Scroll completion is detected by watching the scroller hold still rather than by
|
|
23
|
+
listening for `scrollend`, so every engine behaves the same. A hidden document,
|
|
24
|
+
which neither animates a smooth scroll nor runs frames often enough to watch one
|
|
25
|
+
settle, does not wait at all.
|
|
26
|
+
|
|
27
|
+
When a step scrolls is unchanged: only when part of its target falls outside the
|
|
28
|
+
viewport, and never when `disableAutoScroll` is set.
|
|
29
|
+
|
|
30
|
+
Two smaller behaviour changes fall out of this. The pointer now arrives together
|
|
31
|
+
with the popover rather than with the spotlight, since its placement is resolved
|
|
32
|
+
against the popover's. And a target lost while its step is still scrolling no
|
|
33
|
+
longer freezes the presentation, because a freeze in that window could never be
|
|
34
|
+
recovered.
|
|
35
|
+
|
|
3
36
|
## 1.2.0
|
|
4
37
|
|
|
5
38
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ const workflow = tour.create("intro").step({ id: "welcome", target: "#welcome",
|
|
|
22
22
|
| --- | --- | --- |
|
|
23
23
|
| Placement | `popover.placementTryOrder`, `indicator.placementTryOrder` | Try `top`, `bottom`, `left`, `right`; the resolved position may be `center`. |
|
|
24
24
|
| Interaction | `behavior.allowInteraction` | Allows pointer interaction through the overlay. |
|
|
25
|
-
| Scroll | step/start `scroll` | Uses `behavior`, `block`, and `inline` scroll options. |
|
|
25
|
+
| Scroll | step/start `scroll` | Uses `behavior`, `block`, and `inline` scroll options. The step appears without waiting for the scroll. |
|
|
26
26
|
| Callbacks | `onStart`, `onCancel`, `onFinish`; `beforeAdvance`, `beforePrevious`, `beforeCancel` | Start callbacks are workflow options; transition callbacks are step builder methods. |
|
|
27
27
|
| Actions | `.do(fn)`, `.wait(ms)`, `.waitUntil(fn)`, `.waitUntilElement(selector)` | `waitUntil` defaults to a 16 ms interval and 3000 ms timeout. |
|
|
28
28
|
| Target events | `.onTargetEvent("click", fn)` | Handlers receive the event and step context. |
|
|
@@ -58,6 +58,13 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
58
58
|
* (the caller gave up and is tearing the presentation down).
|
|
59
59
|
*/
|
|
60
60
|
private frozen;
|
|
61
|
+
/**
|
|
62
|
+
* True between the spotlight's entrance and the popover's, the window in
|
|
63
|
+
* which a step's scroll is still travelling. The tracking loop drives the
|
|
64
|
+
* spotlight alone while it is set, and a lost target stops the tracking
|
|
65
|
+
* instead of freezing the presentation.
|
|
66
|
+
*/
|
|
67
|
+
private awaitingStepUi;
|
|
61
68
|
private activeTarget;
|
|
62
69
|
private targetFocusedAtFreeze;
|
|
63
70
|
private lastTargetRect;
|
|
@@ -74,7 +81,7 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
74
81
|
private rafId;
|
|
75
82
|
private rafCancel;
|
|
76
83
|
private root;
|
|
77
|
-
private
|
|
84
|
+
private cancelScroll;
|
|
78
85
|
constructor(commands?: TourViewCommands);
|
|
79
86
|
setCommands(commands: TourViewCommands): void;
|
|
80
87
|
registerRoot(element: HTMLElement | null): void;
|
|
@@ -91,7 +98,33 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
91
98
|
private syncModality;
|
|
92
99
|
private releaseModality;
|
|
93
100
|
private restoreInertBranches;
|
|
101
|
+
/**
|
|
102
|
+
* Brings the spotlight onto the target, then hands the step's popover and
|
|
103
|
+
* pointer over, in that order but not in lockstep.
|
|
104
|
+
*
|
|
105
|
+
* The spotlight's entrance never gates the popover's: the two animate side
|
|
106
|
+
* by side, as they always have. What does gate the popover is the step's
|
|
107
|
+
* scroll. It is pinned by a transform it only rewrites on entrance, so a
|
|
108
|
+
* rect that is still travelling would make it jump through a fade every
|
|
109
|
+
* fifty pixels; it waits for the page to stop and enters on a rect that will
|
|
110
|
+
* not move again. Meanwhile the spotlight tracks the target down the page.
|
|
111
|
+
*/
|
|
94
112
|
private appear;
|
|
113
|
+
/**
|
|
114
|
+
* Retires the outgoing popover, commits the incoming step's content in its
|
|
115
|
+
* place, waits out the step's scroll, and brings the popover and pointer in.
|
|
116
|
+
*
|
|
117
|
+
* Deliberately one async frame. With nothing to retire, nothing to commit
|
|
118
|
+
* and nothing to scroll, the entrance animations are created in the tick
|
|
119
|
+
* this was called in — which is what callers that abort mid-flight rely on,
|
|
120
|
+
* since `cancelAnimationsOnAbort` can only cancel animations that exist.
|
|
121
|
+
*
|
|
122
|
+
* The content commit runs even when no popover is mounted: it carries the
|
|
123
|
+
* controller's step-index commit and must not be skipped.
|
|
124
|
+
*/
|
|
125
|
+
private presentStepUi;
|
|
126
|
+
/** The popover and pointer entrance itself, started synchronously. */
|
|
127
|
+
private enterStepUi;
|
|
95
128
|
private attachStepResources;
|
|
96
129
|
/**
|
|
97
130
|
* Binds the step's custom event handlers to its target element. Split out
|
|
@@ -102,7 +135,15 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
102
135
|
private attachTargetResources;
|
|
103
136
|
private listen;
|
|
104
137
|
private schedulePosition;
|
|
138
|
+
/**
|
|
139
|
+
* Frame scheduling for the target's own realm, falling back to the ambient
|
|
140
|
+
* one when that realm exposes no frame callbacks. Shared by the tracking
|
|
141
|
+
* loop and the scroll sentinel so both read the same clock.
|
|
142
|
+
*/
|
|
143
|
+
private frameScheduler;
|
|
105
144
|
private updatePosition;
|
|
145
|
+
/** Per-frame follow-up for the popover and pointer, once they are on screen. */
|
|
146
|
+
private trackStepUi;
|
|
106
147
|
private observeDynamicOperation;
|
|
107
148
|
private handleKeydown;
|
|
108
149
|
private handleOverlayClick;
|
|
@@ -176,7 +217,31 @@ export declare class DomTourViewDriver<T> implements TourViewDriver<T> {
|
|
|
176
217
|
private cancelAnimationsOnAbort;
|
|
177
218
|
private cancelElementAnimations;
|
|
178
219
|
private isCurrentGeneration;
|
|
179
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Starts the step's scroll and returns a promise that settles once the page
|
|
222
|
+
* has stopped moving, or `null` when there is nothing to wait for — the step
|
|
223
|
+
* opts out, the scroll was applied instantly, or no scroller can be measured.
|
|
224
|
+
*
|
|
225
|
+
* `scrollIntoView` is called synchronously so a throwing call still rejects
|
|
226
|
+
* the `show()` that asked for it. Only the wait is deferred, which is what
|
|
227
|
+
* lets the backdrop appear while the page is still travelling.
|
|
228
|
+
*/
|
|
229
|
+
private beginTargetScroll;
|
|
230
|
+
/**
|
|
231
|
+
* Resolves once the scroller has held still for a couple of frames.
|
|
232
|
+
*
|
|
233
|
+
* Deliberately not the `scrollend` event: Safari only fires it from 18.2, so
|
|
234
|
+
* older versions would fall through to the safety timeout on every step, and
|
|
235
|
+
* the presentation would stay pinned to a stale position long after the page
|
|
236
|
+
* actually stopped. Watching the offset costs a frame loop the browser is
|
|
237
|
+
* already running during a smooth scroll, works everywhere, and settles just
|
|
238
|
+
* as quickly when the browser decides there was nothing to scroll at all.
|
|
239
|
+
*
|
|
240
|
+
* Returns `null` when the offset cannot be read or frames cannot be
|
|
241
|
+
* requested — there is then no way to observe the scroll, so callers treat it
|
|
242
|
+
* as already finished rather than blocking on something unobservable.
|
|
243
|
+
*/
|
|
244
|
+
private waitForScrollToSettle;
|
|
180
245
|
private throwIfAborted;
|
|
181
246
|
private throwIfStale;
|
|
182
247
|
private getWindow;
|
package/elements/overlay.d.ts
CHANGED
|
@@ -7,7 +7,18 @@ export default class OverlayElement extends GlowTourElement {
|
|
|
7
7
|
private visualState;
|
|
8
8
|
private cssPathDSupported;
|
|
9
9
|
setInteractionAllowed(allowed: boolean): void;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Brings the cutout onto `nextPosition`, morphing from wherever it was.
|
|
12
|
+
*
|
|
13
|
+
* Pass `tracked` when the caller will drive the geometry itself frame by
|
|
14
|
+
* frame, as it does while a step's scroll is in flight. An animation on the
|
|
15
|
+
* `d` property overrides the inline value for as long as it runs, so those
|
|
16
|
+
* per-frame writes would be invisible and the cutout would land on the rect
|
|
17
|
+
* captured here — the target's position before the page moved — then jump.
|
|
18
|
+
* Tracked, the geometry is committed once and left alone; only opacity is
|
|
19
|
+
* ever animated.
|
|
20
|
+
*/
|
|
21
|
+
moveToTarget(nextPosition: DOMRect, step: TourElementStep, tracked?: boolean): Promise<void>;
|
|
11
22
|
animateTo(position: DOMRect, step: TourElementStep): Promise<void>;
|
|
12
23
|
_getNextStyles(position: DOMRect, step: TourElementStep): Keyframe;
|
|
13
24
|
/** The backdrop with a hole punched around `position`, as a CSS `d` value. */
|
package/elements/popover.d.ts
CHANGED
|
@@ -16,7 +16,15 @@ export default class PopoverElement extends GlowTourElement {
|
|
|
16
16
|
resolvePosition(targetPosition: DOMRect, step: TourElementStep): PopoverPosition;
|
|
17
17
|
private _centerPosition;
|
|
18
18
|
private _applyPositionState;
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Fades the popover in at `nextPosition` and commits that placement.
|
|
21
|
+
*
|
|
22
|
+
* The outgoing half of a step change is {@link disappear}, deliberately kept
|
|
23
|
+
* separate: between the two, the caller swaps the step's content while the
|
|
24
|
+
* popover is off screen, and — when the step scrolls — waits for the scroll
|
|
25
|
+
* to settle so this entrance reads a rect that will not move again.
|
|
26
|
+
*/
|
|
27
|
+
present(nextPosition: DOMRect, step: TourElementStep): Promise<void>;
|
|
20
28
|
initializeProps(): void;
|
|
21
29
|
updatePosition(nextPosition: DOMRect, step: TourElementStep, onReposition?: (reposition: Promise<void>) => void): ResolvedPlacement;
|
|
22
30
|
cancelAnimations(): void;
|
package/index.js
CHANGED
|
@@ -655,7 +655,7 @@ class OverlayElement extends GlowTourElement {
|
|
|
655
655
|
this.element.style.setProperty("pointer-events", allowed ? "none" : "auto");
|
|
656
656
|
this.element.setAttribute("data-glow-tour-allow-interaction", String(allowed));
|
|
657
657
|
}
|
|
658
|
-
async moveToTarget(nextPosition, step) {
|
|
658
|
+
async moveToTarget(nextPosition, step, tracked = false) {
|
|
659
659
|
const nextVisualState = this._getVisualState(step);
|
|
660
660
|
const path = this._getPathElement();
|
|
661
661
|
if (!path) {
|
|
@@ -675,8 +675,13 @@ class OverlayElement extends GlowTourElement {
|
|
|
675
675
|
...this._getAnimationOptions(),
|
|
676
676
|
fill: "none"
|
|
677
677
|
}, path);
|
|
678
|
-
if (!animation2 || await this._waitForAnimation(animation2))
|
|
679
|
-
this.applyStyles(path, keyframe);
|
|
678
|
+
if (!animation2 || await this._waitForAnimation(animation2)) {
|
|
679
|
+
this.applyStyles(path, tracked ? { opacity } : keyframe);
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (tracked) {
|
|
684
|
+
this.applyStyles(path, keyframe);
|
|
680
685
|
return;
|
|
681
686
|
}
|
|
682
687
|
const baseStyle = {
|
|
@@ -1509,12 +1514,7 @@ class PopoverElement extends GlowTourElement {
|
|
|
1509
1514
|
}
|
|
1510
1515
|
}
|
|
1511
1516
|
}
|
|
1512
|
-
async
|
|
1513
|
-
if (!appear) {
|
|
1514
|
-
await this._disappear();
|
|
1515
|
-
}
|
|
1516
|
-
if (onChange)
|
|
1517
|
-
await onChange();
|
|
1517
|
+
async present(nextPosition, step) {
|
|
1518
1518
|
await this._appear(nextPosition, step);
|
|
1519
1519
|
}
|
|
1520
1520
|
initializeProps() {
|
|
@@ -1892,7 +1892,10 @@ class ScrollLock {
|
|
|
1892
1892
|
}
|
|
1893
1893
|
|
|
1894
1894
|
// packages/core/src/dom/tour-view-driver.ts
|
|
1895
|
-
var
|
|
1895
|
+
var SCROLL_SETTLE_STILL_FRAMES = 2;
|
|
1896
|
+
var SCROLL_SETTLE_GRACE_FRAMES = 3;
|
|
1897
|
+
var SCROLL_SETTLE_EPSILON = 0.5;
|
|
1898
|
+
var SCROLL_SETTLE_TIMEOUT = 2000;
|
|
1896
1899
|
var ACTIVE_MODAL_BY_DOCUMENT = new WeakMap;
|
|
1897
1900
|
var DEFAULT_SHORTCUTS = {
|
|
1898
1901
|
previous: ["ArrowLeft", "Backspace"],
|
|
@@ -1913,6 +1916,7 @@ class DomTourViewDriver {
|
|
|
1913
1916
|
generation = 0;
|
|
1914
1917
|
active = false;
|
|
1915
1918
|
frozen = false;
|
|
1919
|
+
awaitingStepUi = false;
|
|
1916
1920
|
activeTarget = null;
|
|
1917
1921
|
targetFocusedAtFreeze = false;
|
|
1918
1922
|
lastTargetRect = null;
|
|
@@ -1929,7 +1933,7 @@ class DomTourViewDriver {
|
|
|
1929
1933
|
rafId = null;
|
|
1930
1934
|
rafCancel = null;
|
|
1931
1935
|
root = null;
|
|
1932
|
-
|
|
1936
|
+
cancelScroll = null;
|
|
1933
1937
|
constructor(commands) {
|
|
1934
1938
|
this.commands = commands ?? null;
|
|
1935
1939
|
}
|
|
@@ -2000,6 +2004,7 @@ class DomTourViewDriver {
|
|
|
2000
2004
|
this.lastTargetRect = null;
|
|
2001
2005
|
this.lastViewport = null;
|
|
2002
2006
|
this.presentationDirty = false;
|
|
2007
|
+
this.awaitingStepUi = true;
|
|
2003
2008
|
if (replaceVisiblePopover) {
|
|
2004
2009
|
const listener = (event) => this.queueTransitionKeydown(event, step, generation);
|
|
2005
2010
|
const currentWindow = this.getWindow();
|
|
@@ -2013,12 +2018,13 @@ class DomTourViewDriver {
|
|
|
2013
2018
|
return;
|
|
2014
2019
|
this.activeTarget = target;
|
|
2015
2020
|
this.syncModality(step.behavior?.allowInteraction === true);
|
|
2016
|
-
|
|
2021
|
+
const scrolling = this.beginTargetScroll(step, target, signal);
|
|
2017
2022
|
this.throwIfStale(generation, signal);
|
|
2018
2023
|
this.initializeElements(step);
|
|
2019
|
-
const
|
|
2020
|
-
await this.appear(
|
|
2024
|
+
const resolveRect = () => target.getBoundingClientRect();
|
|
2025
|
+
await this.appear(resolveRect, step, generation, scrolling, replaceVisiblePopover, onBeforePopoverAppear);
|
|
2021
2026
|
this.throwIfStale(generation, signal);
|
|
2027
|
+
const targetRect = resolveRect();
|
|
2022
2028
|
this.lastTargetRect = snapshotRect(targetRect);
|
|
2023
2029
|
this.lastViewport = snapshotViewport(target);
|
|
2024
2030
|
this.active = true;
|
|
@@ -2051,6 +2057,7 @@ class DomTourViewDriver {
|
|
|
2051
2057
|
this.throwIfStale(generation, signal);
|
|
2052
2058
|
this.active = false;
|
|
2053
2059
|
this.frozen = false;
|
|
2060
|
+
this.awaitingStepUi = false;
|
|
2054
2061
|
this.activeTarget = null;
|
|
2055
2062
|
this.targetFocusedAtFreeze = false;
|
|
2056
2063
|
this.currentStep = null;
|
|
@@ -2078,6 +2085,7 @@ class DomTourViewDriver {
|
|
|
2078
2085
|
this.scrollLock.deactivate();
|
|
2079
2086
|
this.active = false;
|
|
2080
2087
|
this.frozen = false;
|
|
2088
|
+
this.awaitingStepUi = false;
|
|
2081
2089
|
this.activeTarget = null;
|
|
2082
2090
|
this.targetFocusedAtFreeze = false;
|
|
2083
2091
|
this.currentStep = null;
|
|
@@ -2117,7 +2125,8 @@ class DomTourViewDriver {
|
|
|
2117
2125
|
return;
|
|
2118
2126
|
this.activeTarget = target;
|
|
2119
2127
|
this.initializeElements(step);
|
|
2120
|
-
|
|
2128
|
+
this.awaitingStepUi = true;
|
|
2129
|
+
await this.appear(() => targetRect, step, generation, null, false);
|
|
2121
2130
|
this.throwIfStale(generation);
|
|
2122
2131
|
this.activateFocus(step, target, this.direction, generation);
|
|
2123
2132
|
this.syncScrollLock(step);
|
|
@@ -2190,19 +2199,34 @@ class DomTourViewDriver {
|
|
|
2190
2199
|
element.setAttribute("inert", previous);
|
|
2191
2200
|
}
|
|
2192
2201
|
}
|
|
2193
|
-
async appear(
|
|
2194
|
-
const
|
|
2195
|
-
const
|
|
2196
|
-
const
|
|
2202
|
+
async appear(resolveRect, step, generation, scrolling, hadVisiblePopover, onBeforePopoverAppear) {
|
|
2203
|
+
const spotlightRect = resolveRect();
|
|
2204
|
+
const stepUi = this.presentStepUi(resolveRect, step, generation, scrolling, hadVisiblePopover, onBeforePopoverAppear);
|
|
2205
|
+
const spotlight = this.overlay?.moveToTarget(spotlightRect, step, scrolling !== null);
|
|
2206
|
+
if (scrolling)
|
|
2207
|
+
this.schedulePosition(generation);
|
|
2208
|
+
await Promise.all([spotlight ?? Promise.resolve(), stepUi]);
|
|
2209
|
+
}
|
|
2210
|
+
async presentStepUi(resolveRect, step, generation, scrolling, hadVisiblePopover, onBeforePopoverAppear) {
|
|
2211
|
+
if (hadVisiblePopover)
|
|
2212
|
+
await this.popover?.disappear();
|
|
2213
|
+
if (onBeforePopoverAppear) {
|
|
2197
2214
|
await onBeforePopoverAppear();
|
|
2198
2215
|
this.syncControlState(step);
|
|
2199
2216
|
this.syncShortcutLabels(step);
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2217
|
+
}
|
|
2218
|
+
if (scrolling)
|
|
2219
|
+
await scrolling;
|
|
2220
|
+
if (!this.isCurrentGeneration(generation) || this.currentSignal?.aborted)
|
|
2221
|
+
return;
|
|
2222
|
+
this.awaitingStepUi = false;
|
|
2223
|
+
await this.enterStepUi(resolveRect(), step);
|
|
2224
|
+
}
|
|
2225
|
+
enterStepUi(targetRect, step) {
|
|
2226
|
+
const popoverPlacement = this.popover?.resolvePosition(targetRect, step).placement;
|
|
2227
|
+
return Promise.all([
|
|
2228
|
+
this.popover?.present(targetRect, step) ?? Promise.resolve(),
|
|
2229
|
+
this.isPointerEnabled(step) ? this.pointer?.moveToTarget(targetRect, step, true, popoverPlacement) ?? Promise.resolve() : this.pointer?.disappear() ?? Promise.resolve()
|
|
2206
2230
|
]);
|
|
2207
2231
|
}
|
|
2208
2232
|
attachStepResources(step, target, generation, signal) {
|
|
@@ -2275,17 +2299,11 @@ class DomTourViewDriver {
|
|
|
2275
2299
|
schedulePosition(generation = this.generation) {
|
|
2276
2300
|
if (!this.isCurrentGeneration(generation) || !this.currentStep || this.rafId !== null || this.frozen)
|
|
2277
2301
|
return;
|
|
2278
|
-
const
|
|
2279
|
-
|
|
2280
|
-
const ownerCancel = owner?.cancelAnimationFrame;
|
|
2281
|
-
const ownerHasFrameCapability = typeof ownerRequest === "function" || typeof ownerCancel === "function";
|
|
2282
|
-
const request = ownerHasFrameCapability ? ownerRequest : globalThis.requestAnimationFrame;
|
|
2283
|
-
const cancel = ownerHasFrameCapability ? ownerCancel : globalThis.cancelAnimationFrame;
|
|
2284
|
-
if (typeof request !== "function" || typeof cancel !== "function")
|
|
2302
|
+
const frames = this.frameScheduler(this.currentStep.target);
|
|
2303
|
+
if (!frames)
|
|
2285
2304
|
return;
|
|
2286
|
-
|
|
2287
|
-
this.
|
|
2288
|
-
this.rafId = request.call(frameWindow, () => {
|
|
2305
|
+
this.rafCancel = frames.cancel;
|
|
2306
|
+
this.rafId = frames.request(() => {
|
|
2289
2307
|
this.rafId = null;
|
|
2290
2308
|
this.rafCancel = null;
|
|
2291
2309
|
if (!this.isCurrentGeneration(generation))
|
|
@@ -2294,13 +2312,29 @@ class DomTourViewDriver {
|
|
|
2294
2312
|
this.schedulePosition(generation);
|
|
2295
2313
|
});
|
|
2296
2314
|
}
|
|
2315
|
+
frameScheduler(context) {
|
|
2316
|
+
const owner = context?.ownerDocument?.defaultView;
|
|
2317
|
+
const ownerRequest = owner?.requestAnimationFrame;
|
|
2318
|
+
const ownerCancel = owner?.cancelAnimationFrame;
|
|
2319
|
+
const ownerHasFrameCapability = typeof ownerRequest === "function" || typeof ownerCancel === "function";
|
|
2320
|
+
const request = ownerHasFrameCapability ? ownerRequest : globalThis.requestAnimationFrame;
|
|
2321
|
+
const cancel = ownerHasFrameCapability ? ownerCancel : globalThis.cancelAnimationFrame;
|
|
2322
|
+
if (typeof request !== "function" || typeof cancel !== "function")
|
|
2323
|
+
return null;
|
|
2324
|
+
const frameWindow = ownerHasFrameCapability && owner ? owner : globalThis;
|
|
2325
|
+
return {
|
|
2326
|
+
request: (callback) => request.call(frameWindow, callback),
|
|
2327
|
+
cancel: (id) => cancel.call(frameWindow, id)
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2297
2330
|
updatePosition(generation) {
|
|
2298
2331
|
const step = this.currentStep;
|
|
2299
2332
|
const target = step?.target;
|
|
2300
2333
|
if (!this.isCurrentGeneration(generation) || !step || !target)
|
|
2301
2334
|
return;
|
|
2302
2335
|
if (!this.isCurrentTargetAvailable(target)) {
|
|
2303
|
-
this.
|
|
2336
|
+
if (!this.awaitingStepUi)
|
|
2337
|
+
this.freezeForDisconnectedTarget(step, target, generation);
|
|
2304
2338
|
return;
|
|
2305
2339
|
}
|
|
2306
2340
|
const targetRect = target.getBoundingClientRect();
|
|
@@ -2317,6 +2351,14 @@ class DomTourViewDriver {
|
|
|
2317
2351
|
this.syncShortcutLabels(step);
|
|
2318
2352
|
}
|
|
2319
2353
|
this.overlay?.updatePosition(targetRect, step, presentationChanged, (transition) => this.observeDynamicOperation(transition, generation));
|
|
2354
|
+
if (!this.awaitingStepUi)
|
|
2355
|
+
this.trackStepUi(targetRect, step, generation, presentationChanged);
|
|
2356
|
+
this.lastTargetRect = targetSnapshot;
|
|
2357
|
+
this.lastViewport = viewportSnapshot;
|
|
2358
|
+
if (presentationChanged)
|
|
2359
|
+
this.presentationDirty = false;
|
|
2360
|
+
}
|
|
2361
|
+
trackStepUi(targetRect, step, generation, presentationChanged) {
|
|
2320
2362
|
const popoverPlacement = this.popover?.updatePosition(targetRect, step, (reposition) => this.observeDynamicOperation(reposition, generation));
|
|
2321
2363
|
if (presentationChanged) {
|
|
2322
2364
|
this.pointer?.syncVisibility(this.isPointerEnabled(step), targetRect, step, popoverPlacement);
|
|
@@ -2330,10 +2372,6 @@ class DomTourViewDriver {
|
|
|
2330
2372
|
} else if (this.pointer?.getElement()?.getAttribute("aria-hidden") !== "true") {
|
|
2331
2373
|
this.observeDynamicOperation(this.pointer?.disappear(), generation);
|
|
2332
2374
|
}
|
|
2333
|
-
this.lastTargetRect = targetSnapshot;
|
|
2334
|
-
this.lastViewport = viewportSnapshot;
|
|
2335
|
-
if (presentationChanged)
|
|
2336
|
-
this.presentationDirty = false;
|
|
2337
2375
|
}
|
|
2338
2376
|
observeDynamicOperation(operation, generation) {
|
|
2339
2377
|
if (!operation)
|
|
@@ -2600,8 +2638,8 @@ class DomTourViewDriver {
|
|
|
2600
2638
|
return step.behavior?.allowInteraction === true && step.indicator?.disabled !== true;
|
|
2601
2639
|
}
|
|
2602
2640
|
cleanupStepResources() {
|
|
2603
|
-
this.
|
|
2604
|
-
this.
|
|
2641
|
+
this.cancelScroll?.();
|
|
2642
|
+
this.cancelScroll = null;
|
|
2605
2643
|
this.presentationDirty = false;
|
|
2606
2644
|
if (this.rafId !== null)
|
|
2607
2645
|
this.rafCancel?.(this.rafId);
|
|
@@ -2715,45 +2753,77 @@ class DomTourViewDriver {
|
|
|
2715
2753
|
isCurrentGeneration(generation) {
|
|
2716
2754
|
return !this.disposed && generation === this.generation;
|
|
2717
2755
|
}
|
|
2718
|
-
|
|
2756
|
+
beginTargetScroll(step, target, signal) {
|
|
2719
2757
|
this.throwIfAborted(signal);
|
|
2720
|
-
if (step.behavior?.disableAutoScroll
|
|
2721
|
-
return;
|
|
2758
|
+
if (step.behavior?.disableAutoScroll)
|
|
2759
|
+
return null;
|
|
2760
|
+
if (isInViewport(target.getBoundingClientRect(), target))
|
|
2761
|
+
return null;
|
|
2722
2762
|
const currentWindow = this.getWindow(target);
|
|
2723
2763
|
if (!currentWindow)
|
|
2724
|
-
return;
|
|
2725
|
-
const
|
|
2726
|
-
|
|
2727
|
-
|
|
2764
|
+
return null;
|
|
2765
|
+
const behavior = prefersReducedMotion(target) ? "instant" : step.behavior?.scroll?.behavior ?? "smooth";
|
|
2766
|
+
target.scrollIntoView({
|
|
2767
|
+
behavior,
|
|
2768
|
+
block: step.behavior?.scroll?.block ?? "center",
|
|
2769
|
+
inline: step.behavior?.scroll?.inline ?? "nearest"
|
|
2770
|
+
});
|
|
2771
|
+
if (behavior === "instant")
|
|
2772
|
+
return null;
|
|
2773
|
+
return this.waitForScrollToSettle(target, signal);
|
|
2774
|
+
}
|
|
2775
|
+
waitForScrollToSettle(target, signal) {
|
|
2776
|
+
const owner = ownerDocument(target);
|
|
2777
|
+
const scroller = owner?.scrollingElement;
|
|
2778
|
+
if (typeof scroller?.scrollTop !== "number")
|
|
2779
|
+
return null;
|
|
2780
|
+
const frames = this.frameScheduler(target);
|
|
2781
|
+
if (!frames)
|
|
2782
|
+
return null;
|
|
2783
|
+
if (owner?.visibilityState === "hidden")
|
|
2784
|
+
return null;
|
|
2785
|
+
return new Promise((resolve, reject) => {
|
|
2786
|
+
let frame = null;
|
|
2728
2787
|
let timeout = null;
|
|
2729
|
-
|
|
2788
|
+
let left = scroller.scrollLeft;
|
|
2789
|
+
let top = scroller.scrollTop;
|
|
2790
|
+
let stillFrames = -SCROLL_SETTLE_GRACE_FRAMES;
|
|
2730
2791
|
const finish = (error) => {
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
controller.signal.removeEventListener("abort", abort);
|
|
2792
|
+
if (frame !== null)
|
|
2793
|
+
frames.cancel(frame);
|
|
2734
2794
|
if (timeout !== null)
|
|
2735
2795
|
clearTimeout(timeout);
|
|
2736
|
-
|
|
2737
|
-
|
|
2796
|
+
signal.removeEventListener("abort", abort);
|
|
2797
|
+
owner?.removeEventListener("visibilitychange", stopIfHidden);
|
|
2798
|
+
if (this.cancelScroll === abort)
|
|
2799
|
+
this.cancelScroll = null;
|
|
2738
2800
|
if (error)
|
|
2739
2801
|
reject(error);
|
|
2740
2802
|
else
|
|
2741
2803
|
resolve();
|
|
2742
2804
|
};
|
|
2743
|
-
const
|
|
2744
|
-
|
|
2805
|
+
const abort = () => finish(abortError2());
|
|
2806
|
+
const stopIfHidden = () => {
|
|
2807
|
+
if (owner?.visibilityState === "hidden")
|
|
2808
|
+
finish();
|
|
2809
|
+
};
|
|
2810
|
+
const watch = () => {
|
|
2811
|
+
frame = null;
|
|
2812
|
+
const nextLeft = scroller.scrollLeft;
|
|
2813
|
+
const nextTop = scroller.scrollTop;
|
|
2814
|
+
const still = Math.abs(nextLeft - left) <= SCROLL_SETTLE_EPSILON && Math.abs(nextTop - top) <= SCROLL_SETTLE_EPSILON;
|
|
2815
|
+
left = nextLeft;
|
|
2816
|
+
top = nextTop;
|
|
2817
|
+
stillFrames = still ? stillFrames + 1 : 0;
|
|
2818
|
+
if (stillFrames >= SCROLL_SETTLE_STILL_FRAMES)
|
|
2819
|
+
return finish();
|
|
2820
|
+
frame = frames.request(watch);
|
|
2821
|
+
};
|
|
2822
|
+
this.cancelScroll = abort;
|
|
2745
2823
|
signal.addEventListener("abort", abort, { once: true });
|
|
2746
|
-
|
|
2747
|
-
timeout = setTimeout(
|
|
2748
|
-
|
|
2749
|
-
target.scrollIntoView({
|
|
2750
|
-
behavior: prefersReducedMotion(target) ? "instant" : step.behavior?.scroll?.behavior ?? "smooth",
|
|
2751
|
-
block: step.behavior?.scroll?.block ?? "center",
|
|
2752
|
-
inline: step.behavior?.scroll?.inline ?? "nearest"
|
|
2753
|
-
});
|
|
2754
|
-
} catch (error) {
|
|
2755
|
-
finish(error instanceof Error ? error : new Error(String(error)));
|
|
2756
|
-
}
|
|
2824
|
+
owner?.addEventListener("visibilitychange", stopIfHidden);
|
|
2825
|
+
timeout = setTimeout(finish, SCROLL_SETTLE_TIMEOUT);
|
|
2826
|
+
frame = frames.request(watch);
|
|
2757
2827
|
});
|
|
2758
2828
|
}
|
|
2759
2829
|
throwIfAborted(signal) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glowhop/core-tour",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Framework-agnostic tour controller and DOM runtime for GlowTour.js.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/Glowhop/GlowTour.js#readme",
|
|
@@ -10,8 +10,17 @@
|
|
|
10
10
|
"keywords": [
|
|
11
11
|
"tour",
|
|
12
12
|
"guided-tour",
|
|
13
|
+
"product-tour",
|
|
13
14
|
"onboarding",
|
|
14
|
-
"
|
|
15
|
+
"walkthrough",
|
|
16
|
+
"user-onboarding",
|
|
17
|
+
"tooltip",
|
|
18
|
+
"spotlight",
|
|
19
|
+
"typescript",
|
|
20
|
+
"dom",
|
|
21
|
+
"headless",
|
|
22
|
+
"framework-agnostic",
|
|
23
|
+
"ssr"
|
|
15
24
|
],
|
|
16
25
|
"engines": {
|
|
17
26
|
"node": ">=18.19.1"
|
package/types/index.d.ts
CHANGED
|
@@ -55,12 +55,12 @@ export interface IndicatorOptions extends BaseOptions {
|
|
|
55
55
|
disabled?: boolean;
|
|
56
56
|
/** Gap between the target and the indicator in pixels. */
|
|
57
57
|
gap?: number;
|
|
58
|
-
/** Placement preference order when positioning the indicator. @default ["
|
|
58
|
+
/** Placement preference order when positioning the indicator. @default ["left", "right", "top", "bottom"] */
|
|
59
59
|
placementTryOrder?: readonly TryOrderOptions[];
|
|
60
60
|
}
|
|
61
61
|
/** Configures the darkened overlay backdrop that highlights the target. */
|
|
62
62
|
export interface OverlayOptions extends BaseOptions {
|
|
63
|
-
/** Color of the overlay backdrop (CSS color).
|
|
63
|
+
/** Color of the overlay backdrop (CSS color). Falls back to the `--glow-tour-overlay-color` theme variable when unset. */
|
|
64
64
|
color?: string;
|
|
65
65
|
/** Opacity of the overlay (0-1). @default 0.7 */
|
|
66
66
|
opacity?: number;
|
|
@@ -77,11 +77,11 @@ export interface PopoverArrowOptions {
|
|
|
77
77
|
color?: string;
|
|
78
78
|
/** Size of the arrow in pixels. @default 12 */
|
|
79
79
|
size?: number;
|
|
80
|
-
/** Border width of the arrow in pixels.
|
|
80
|
+
/** Border width of the arrow in pixels. Falls back to the `--glow-tour-arrow-border-width` theme variable (`1px`) when unset. */
|
|
81
81
|
borderWidth?: number;
|
|
82
82
|
/** Border radius of the arrow in pixels. @default 0 */
|
|
83
83
|
borderRadius?: number;
|
|
84
|
-
/**
|
|
84
|
+
/** Minimum gap the arrow keeps from the popover edges, in pixels. A placement whose arrow would fall inside this margin is rejected in favour of the next one. @default 16 */
|
|
85
85
|
edgePadding?: number;
|
|
86
86
|
/**
|
|
87
87
|
* CSP nonce applied to the `<style>` element GlowTour.js injects for the
|
|
@@ -98,7 +98,7 @@ export interface PopoverArrowOptions {
|
|
|
98
98
|
}
|
|
99
99
|
/** Configures the popover box that displays content for each step. */
|
|
100
100
|
export interface PopoverOptions extends BaseOptions {
|
|
101
|
-
/** Placement preference order for the popover around the target. @default ["
|
|
101
|
+
/** Placement preference order for the popover around the target. @default ["bottom", "top", "right", "left"] */
|
|
102
102
|
placementTryOrder?: readonly TryOrderOptions[];
|
|
103
103
|
/** Arrow configuration. */
|
|
104
104
|
arrow?: PopoverArrowOptions;
|
|
@@ -136,9 +136,16 @@ export interface PopoverOptions extends BaseOptions {
|
|
|
136
136
|
cancel?: readonly string[];
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Scroll behavior options passed to Element.scrollIntoView().
|
|
141
|
+
*
|
|
142
|
+
* A step scrolls only when part of its target falls outside the viewport, and
|
|
143
|
+
* does not wait for the scroll before presenting: the spotlight appears at once
|
|
144
|
+
* and tracks the target as the page travels, and the popover and pointer enter
|
|
145
|
+
* when the page has come to rest.
|
|
146
|
+
*/
|
|
140
147
|
export interface ScrollOptions {
|
|
141
|
-
/** Scroll animation. @default "
|
|
148
|
+
/** Scroll animation. Forced to `"instant"` when the user prefers reduced motion. @default "smooth" */
|
|
142
149
|
behavior?: "auto" | "smooth";
|
|
143
150
|
/** Vertical alignment of the target in the viewport. @default "center" */
|
|
144
151
|
block?: "start" | "center" | "end" | "nearest";
|