@glowhop/core-tour 1.3.1 → 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/index.js CHANGED
@@ -12,17 +12,6 @@ function cloneStepProps(props) {
12
12
  ...props.popover,
13
13
  animation: props.popover.animation && { ...props.popover.animation },
14
14
  arrow: props.popover.arrow && { ...props.popover.arrow },
15
- keyboardShortcuts: props.popover.keyboardShortcuts && {
16
- previous: props.popover.keyboardShortcuts.previous && [
17
- ...props.popover.keyboardShortcuts.previous
18
- ],
19
- advance: props.popover.keyboardShortcuts.advance && [
20
- ...props.popover.keyboardShortcuts.advance
21
- ],
22
- cancel: props.popover.keyboardShortcuts.cancel && [
23
- ...props.popover.keyboardShortcuts.cancel
24
- ]
25
- },
26
15
  placementTryOrder: props.popover.placementTryOrder && [...props.popover.placementTryOrder]
27
16
  },
28
17
  indicator: props.indicator && {
@@ -31,9 +20,29 @@ function cloneStepProps(props) {
31
20
  placementTryOrder: props.indicator.placementTryOrder && [
32
21
  ...props.indicator.placementTryOrder
33
22
  ]
34
- }
23
+ },
24
+ behavior: props.behavior && {
25
+ ...props.behavior,
26
+ missingTarget: props.behavior.missingTarget && { ...props.behavior.missingTarget },
27
+ scroll: props.behavior.scroll && { ...props.behavior.scroll }
28
+ },
29
+ controls: cloneControls(props.controls),
30
+ classNames: cloneClassNames(props.classNames)
35
31
  };
36
32
  }
33
+ function cloneControls(controls) {
34
+ return controls && Object.freeze({
35
+ previous: cloneControl(controls.previous),
36
+ advance: cloneControl(controls.advance),
37
+ cancel: cloneControl(controls.cancel)
38
+ });
39
+ }
40
+ function cloneControl(control) {
41
+ return control && Object.freeze({ state: control.state, keys: control.keys && Object.freeze([...control.keys]) });
42
+ }
43
+ function cloneClassNames(classNames) {
44
+ return classNames && Object.freeze({ ...classNames });
45
+ }
37
46
  function freezeStepProps(props) {
38
47
  const cloned = cloneStepProps(props);
39
48
  if (cloned.data)
@@ -46,14 +55,6 @@ function freezeStepProps(props) {
46
55
  Object.freeze(cloned.popover.animation);
47
56
  if (cloned.popover?.arrow)
48
57
  Object.freeze(cloned.popover.arrow);
49
- if (cloned.popover?.keyboardShortcuts?.previous)
50
- Object.freeze(cloned.popover.keyboardShortcuts.previous);
51
- if (cloned.popover?.keyboardShortcuts?.advance)
52
- Object.freeze(cloned.popover.keyboardShortcuts.advance);
53
- if (cloned.popover?.keyboardShortcuts?.cancel)
54
- Object.freeze(cloned.popover.keyboardShortcuts.cancel);
55
- if (cloned.popover?.keyboardShortcuts)
56
- Object.freeze(cloned.popover.keyboardShortcuts);
57
58
  if (cloned.popover?.placementTryOrder)
58
59
  Object.freeze(cloned.popover.placementTryOrder);
59
60
  if (cloned.popover)
@@ -64,6 +65,12 @@ function freezeStepProps(props) {
64
65
  Object.freeze(cloned.indicator.placementTryOrder);
65
66
  if (cloned.indicator)
66
67
  Object.freeze(cloned.indicator);
68
+ if (cloned.behavior?.missingTarget)
69
+ Object.freeze(cloned.behavior.missingTarget);
70
+ if (cloned.behavior?.scroll)
71
+ Object.freeze(cloned.behavior.scroll);
72
+ if (cloned.behavior)
73
+ Object.freeze(cloned.behavior);
67
74
  return Object.freeze(cloned);
68
75
  }
69
76
  // packages/core/src/definition/workflow-definition.ts
@@ -84,11 +91,6 @@ function freezePopover(options) {
84
91
  ...options,
85
92
  animation: freezeAnimation(options.animation),
86
93
  arrow: options.arrow && freezeRecord({ ...options.arrow }),
87
- keyboardShortcuts: options.keyboardShortcuts && freezeRecord({
88
- previous: options.keyboardShortcuts.previous && freezeRecord([...options.keyboardShortcuts.previous]),
89
- advance: options.keyboardShortcuts.advance && freezeRecord([...options.keyboardShortcuts.advance]),
90
- cancel: options.keyboardShortcuts.cancel && freezeRecord([...options.keyboardShortcuts.cancel])
91
- }),
92
94
  placementTryOrder: options.placementTryOrder && freezeRecord([...options.placementTryOrder])
93
95
  });
94
96
  }
@@ -103,17 +105,11 @@ function freezeStep(draft) {
103
105
  return freezeRecord({
104
106
  id: draft.id,
105
107
  target: draft.target,
106
- resetPropsOnEnter: draft.resetPropsOnEnter,
107
108
  props: freezeStepProps(draft.props),
108
- behavior: draft.behavior && freezeRecord({
109
- ...draft.behavior,
110
- scroll: draft.behavior.scroll && freezeRecord({ ...draft.behavior.scroll })
111
- }),
112
109
  actions: freezeRecord(draft.actions.map((action) => action)),
113
- eventHandlers: freezeRecord(draft.eventHandlers.map((handler) => freezeRecord({ ...handler }))),
114
- advanceAction: draft.advanceAction,
115
- previousAction: draft.previousAction,
116
- cancelAction: draft.cancelAction
110
+ targetEvents: freezeRecord(draft.targetEvents.map((handler) => freezeRecord({ ...handler }))),
111
+ beforeEnter: draft.beforeEnter,
112
+ beforeLeave: draft.beforeLeave
117
113
  });
118
114
  }
119
115
  function freezeOptions(options) {
@@ -122,8 +118,11 @@ function freezeOptions(options) {
122
118
  overlay: freezeOverlay(options.overlay),
123
119
  popover: freezePopover(options.popover),
124
120
  indicator: freezeIndicator(options.indicator),
121
+ classNames: cloneClassNames(options.classNames),
122
+ controls: cloneControls(options.controls),
125
123
  behavior: options.behavior && freezeRecord({
126
124
  ...options.behavior,
125
+ missingTarget: options.behavior.missingTarget && freezeRecord({ ...options.behavior.missingTarget }),
127
126
  scroll: options.behavior.scroll && freezeRecord({ ...options.behavior.scroll })
128
127
  })
129
128
  });
@@ -133,7 +132,7 @@ function cloneWorkflowStepDraft(definition) {
133
132
  ...definition,
134
133
  props: cloneStepProps(definition.props),
135
134
  actions: definition.actions.map((action) => action),
136
- eventHandlers: [...definition.eventHandlers]
135
+ targetEvents: [...definition.targetEvents]
137
136
  };
138
137
  }
139
138
  function assertUniqueStepIds(name, drafts) {
@@ -246,21 +245,21 @@ class WorkflowBuilder {
246
245
  this.currentStep = new WorkflowStepBuilder(this, {
247
246
  id: options.id,
248
247
  target: options.target,
249
- resetPropsOnEnter: options.resetPropsOnEnter,
250
248
  props: {
251
249
  title: options.title,
252
250
  content: options.content,
253
251
  data: cloneStepProps(options).data,
254
252
  overlay: options.overlay,
255
253
  popover: options.popover,
256
- indicator: options.indicator
254
+ indicator: options.indicator,
255
+ behavior: options.behavior,
256
+ controls: options.controls,
257
+ classNames: options.classNames
257
258
  },
258
- behavior: options.behavior,
259
259
  actions: [],
260
- eventHandlers: [],
261
- advanceAction: null,
262
- previousAction: null,
263
- cancelAction: null
260
+ targetEvents: [],
261
+ beforeEnter: null,
262
+ beforeLeave: null
264
263
  });
265
264
  return this.currentStep;
266
265
  }
@@ -364,19 +363,14 @@ class WorkflowStepBuilder {
364
363
  this.draft.actions.push(callback);
365
364
  return this;
366
365
  }
367
- beforeAdvance(callback) {
366
+ beforeEnter(callback) {
368
367
  this.assertActive();
369
- this.draft.advanceAction = callback;
368
+ this.draft.beforeEnter = callback;
370
369
  return this;
371
370
  }
372
- beforePrevious(callback) {
371
+ beforeLeave(callback) {
373
372
  this.assertActive();
374
- this.draft.previousAction = callback;
375
- return this;
376
- }
377
- beforeCancel(callback) {
378
- this.assertActive();
379
- this.draft.cancelAction = callback;
373
+ this.draft.beforeLeave = callback;
380
374
  return this;
381
375
  }
382
376
  onTargetEvent(eventOrEvents, callback) {
@@ -387,7 +381,7 @@ class WorkflowStepBuilder {
387
381
  for (const event of events) {
388
382
  if (event.length === 0)
389
383
  throw new TypeError("event name must not be empty");
390
- this.draft.eventHandlers.push({
384
+ this.draft.targetEvents.push({
391
385
  event,
392
386
  callback
393
387
  });
@@ -423,12 +417,34 @@ function isNode(value, context) {
423
417
  const Node = ownerWindow(context)?.Node ?? globalThis.Node;
424
418
  return typeof Node === "function" && value instanceof Node;
425
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
+ }
426
440
  function viewportDimensions(context) {
427
- const root = ownerDocument(context)?.documentElement;
441
+ const document2 = ownerDocument(context);
442
+ const root = document2?.documentElement;
428
443
  const width = root?.clientWidth;
429
444
  const height = root?.clientHeight;
430
445
  if (typeof width === "number" && width > 0 && typeof height === "number" && height > 0) {
431
- return { width, height };
446
+ const fixed = document2 && root && fixedContainingBlock(document2, root);
447
+ return fixed ? { height: fixed.height, width: fixed.width } : { width, height };
432
448
  }
433
449
  const currentWindow = ownerWindow(context);
434
450
  return {
@@ -473,14 +489,17 @@ function roundedRectPath(rect, viewport, options, context) {
473
489
  "Z"
474
490
  ].join(" ");
475
491
  }
476
- async function resolveTargetElement(target, options, path = "target") {
492
+ function isPendingTarget(value) {
493
+ return typeof value?.then === "function";
494
+ }
495
+ function resolveTargetElement(target, options, path = "target") {
477
496
  const rootDocument = options.document;
497
+ const validate = (element) => rootDocument ? validateTargetElement(element, rootDocument, path) : element;
478
498
  if (typeof target === "string") {
479
- const element = rootDocument ? rootDocument.querySelector(target) : typeof document === "undefined" ? null : document.querySelector(target);
480
- return rootDocument ? validateTargetElement(element, rootDocument, path) : element;
499
+ return validate(rootDocument ? rootDocument.querySelector(target) : typeof document === "undefined" ? null : document.querySelector(target));
481
500
  } else if (typeof target === "function") {
482
- const element = await target({ signal: options.signal });
483
- return rootDocument ? validateTargetElement(element, rootDocument, path) : element;
501
+ const element = target({ signal: options.signal });
502
+ return isPendingTarget(element) ? element.then(validate) : validate(element);
484
503
  }
485
504
  if (rootDocument)
486
505
  return validateTargetElement(target, rootDocument, path);
@@ -504,6 +523,7 @@ class GlowTourElement {
504
523
  element;
505
524
  options;
506
525
  animations = new Set;
526
+ filledAnimations = new Set;
507
527
  cancelledAnimations = new WeakSet;
508
528
  released = false;
509
529
  constructor(element, options) {
@@ -568,13 +588,23 @@ class GlowTourElement {
568
588
  } finally {
569
589
  stopWatchingVisibility();
570
590
  this.animations.delete(animation);
591
+ const fill = animation.effect?.getTiming?.().fill;
592
+ if (!this.released && !this.cancelledAnimations.has(animation) && (fill === "forwards" || fill === "both")) {
593
+ this.filledAnimations.add(animation);
594
+ }
571
595
  }
572
596
  }
597
+ _releaseFilledAnimations() {
598
+ for (const animation of this.filledAnimations) {
599
+ this._cancelAnimation(animation);
600
+ }
601
+ this.filledAnimations.clear();
602
+ }
573
603
  getElement() {
574
604
  return this.released ? null : this.element;
575
605
  }
576
- disappear() {
577
- return this._disappear();
606
+ disappear(hideFromAssistiveTechnology) {
607
+ return this._disappear(hideFromAssistiveTechnology);
578
608
  }
579
609
  release() {
580
610
  if (this.released)
@@ -588,6 +618,7 @@ class GlowTourElement {
588
618
  this._cancelAnimation(animation);
589
619
  }
590
620
  this.animations.clear();
621
+ this._releaseFilledAnimations();
591
622
  }
592
623
  _cancelAnimation(animation) {
593
624
  this.cancelledAnimations.add(animation);
@@ -733,7 +764,7 @@ class OverlayElement extends GlowTourElement {
733
764
  }
734
765
  _cutoutPath(position, step) {
735
766
  return `path("${roundedRectPath(position, paintedBoxDimensions(this.element), {
736
- padding: step.overlay?.padding ?? DEFAULT_OVERLAY_PADDING,
767
+ padding: step.detached ? 0 : step.overlay?.padding ?? DEFAULT_OVERLAY_PADDING,
737
768
  radius: step.overlay?.radius ?? DEFAULT_OVERLAY_RADIUS
738
769
  }, this.element)}")`;
739
770
  }
@@ -748,7 +779,7 @@ class OverlayElement extends GlowTourElement {
748
779
  for (const [name, value] of Object.entries(OVERLAY_IDLE_ATTRIBUTES)) {
749
780
  el.setAttribute(name, value);
750
781
  }
751
- el.style.setProperty("height", "100lvh");
782
+ el.style.setProperty("height", "max(100%, 100lvh)");
752
783
  this.syncViewBox();
753
784
  }
754
785
  _getPathElement() {
@@ -1438,7 +1469,7 @@ class PopoverElement extends GlowTourElement {
1438
1469
  };
1439
1470
  }
1440
1471
  const gap = Math.max(0, step.popover?.gap ?? DEFAULT_POPOVER_GAP);
1441
- const arrowDisabled = step.popover?.arrow?.disabled === true;
1472
+ const arrowDisabled = step.popover?.arrow?.hidden === true;
1442
1473
  const arrowEdgePadding = step.popover?.arrow?.edgePadding ?? DEFAULT_ARROW_EDGE_PADDING;
1443
1474
  const popoverPosition = currentElement.getBoundingClientRect();
1444
1475
  const viewport = viewportDimensions(currentElement);
@@ -1448,7 +1479,7 @@ class PopoverElement extends GlowTourElement {
1448
1479
  const maxY = viewport.height - gap - popoverPosition.height;
1449
1480
  const targetCenterX = targetPosition.left + targetPosition.width / 2;
1450
1481
  const targetCenterY = targetPosition.top + targetPosition.height / 2;
1451
- if (maxX < minX || maxY < minY) {
1482
+ if (step.detached || maxX < minX || maxY < minY) {
1452
1483
  return this._centerPosition(popoverPosition, viewport);
1453
1484
  }
1454
1485
  const centeredX = targetCenterX - popoverPosition.width / 2;
@@ -1487,6 +1518,9 @@ class PopoverElement extends GlowTourElement {
1487
1518
  }
1488
1519
  return this._centerPosition(popoverPosition, viewport);
1489
1520
  }
1521
+ setAwaitingTarget(awaiting) {
1522
+ this.mutationLease.setAttribute("data-glow-tour-awaiting-target", awaiting ? "" : null);
1523
+ }
1490
1524
  _centerPosition(popoverPosition, viewport) {
1491
1525
  return {
1492
1526
  arrowOffset: null,
@@ -1517,7 +1551,7 @@ class PopoverElement extends GlowTourElement {
1517
1551
  async present(nextPosition, step) {
1518
1552
  await this._appear(nextPosition, step);
1519
1553
  }
1520
- initializeProps() {
1554
+ initializeProps(hideFromAssistiveTechnology = true) {
1521
1555
  const el = this.getElement();
1522
1556
  if (!el) {
1523
1557
  return;
@@ -1528,8 +1562,12 @@ class PopoverElement extends GlowTourElement {
1528
1562
  if (!el.hasAttribute("tabindex")) {
1529
1563
  this.mutationLease.setAttribute("tabindex", POPOVER_IDLE_ATTRIBUTES.tabindex);
1530
1564
  }
1531
- this.mutationLease.setAttribute("aria-hidden", POPOVER_IDLE_ATTRIBUTES["aria-hidden"]);
1532
- this.mutationLease.setAttribute("inert", POPOVER_IDLE_ATTRIBUTES.inert);
1565
+ if (hideFromAssistiveTechnology) {
1566
+ this.mutationLease.setAttribute("aria-hidden", POPOVER_IDLE_ATTRIBUTES["aria-hidden"]);
1567
+ this.mutationLease.setAttribute("inert", POPOVER_IDLE_ATTRIBUTES.inert);
1568
+ } else {
1569
+ this.mutationLease.setStyle("pointer-events", "none");
1570
+ }
1533
1571
  }
1534
1572
  updatePosition(nextPosition, step, onReposition) {
1535
1573
  const nextCoordinates = this.resolvePosition(nextPosition, step);
@@ -1578,7 +1616,7 @@ class PopoverElement extends GlowTourElement {
1578
1616
  let pending = this.pendingReposition;
1579
1617
  this.pendingReposition = null;
1580
1618
  this.repositionPhase = "fading-out";
1581
- await this._disappear();
1619
+ await this._disappear(false);
1582
1620
  if (generation !== this.repositionGeneration || !this.getElement())
1583
1621
  return;
1584
1622
  pending = this.pendingReposition ?? pending;
@@ -1615,7 +1653,7 @@ class PopoverElement extends GlowTourElement {
1615
1653
  _applyArrowStyles(step) {
1616
1654
  const arrow = step.popover?.arrow;
1617
1655
  ensurePopoverArrowStyles(this.element, {
1618
- disabled: arrow?.disableAutoStyles,
1656
+ disabled: arrow?.autoStyles === false,
1619
1657
  nonce: arrow?.styleNonce
1620
1658
  });
1621
1659
  this._applyArrowStyle(ARROW_STYLE_PROPERTIES.color, arrow?.color);
@@ -1633,6 +1671,7 @@ class PopoverElement extends GlowTourElement {
1633
1671
  }
1634
1672
  }
1635
1673
  async _appear(position, step) {
1674
+ this._releaseFilledAnimations();
1636
1675
  const defaultStyles = this._getNextStyles(position, step);
1637
1676
  for (const [key, value] of Object.entries(defaultStyles)) {
1638
1677
  if (value != null)
@@ -1647,27 +1686,36 @@ class PopoverElement extends GlowTourElement {
1647
1686
  return;
1648
1687
  this._applyVisibleState();
1649
1688
  }
1650
- async _disappear() {
1689
+ async _disappear(hideFromAssistiveTechnology = true) {
1690
+ this._releaseFilledAnimations();
1651
1691
  const animation = this._startAnimation({
1652
1692
  opacity: 0
1653
1693
  }, this._getAnimationOptions());
1654
1694
  if (animation && !await this._waitForAnimation(animation))
1655
1695
  return;
1656
- this._applyHiddenState();
1696
+ if (hideFromAssistiveTechnology)
1697
+ this._applyHiddenState();
1698
+ else
1699
+ this._applyFadedState();
1657
1700
  }
1658
1701
  _release() {
1659
1702
  this.mutationLease.release();
1660
1703
  }
1661
1704
  _applyVisibleState() {
1662
1705
  this.mutationLease.setStyle("opacity", "1");
1706
+ this.mutationLease.releaseStyle("pointer-events");
1663
1707
  this.mutationLease.setAttribute("aria-hidden", null);
1664
1708
  this.mutationLease.setAttribute("inert", null);
1665
1709
  }
1666
- _applyHiddenState() {
1710
+ _applyFadedState() {
1667
1711
  this.mutationLease.setStyle("opacity", "0");
1712
+ this.mutationLease.setStyle("pointer-events", "none");
1713
+ this.mutationLease.setStyle("transform", null);
1714
+ }
1715
+ _applyHiddenState() {
1716
+ this._applyFadedState();
1668
1717
  this.mutationLease.setAttribute("aria-hidden", "true");
1669
1718
  this.mutationLease.setAttribute("inert", "true");
1670
- this.mutationLease.setStyle("transform", null);
1671
1719
  }
1672
1720
  }
1673
1721
  function toPixels(value) {
@@ -1750,15 +1798,18 @@ class FocusGuard {
1750
1798
  this.deactivate();
1751
1799
  if (!this.active) {
1752
1800
  this.document = document2;
1753
- this.initialFocus = isHTMLElement(this.document.activeElement, scope.popover) ? this.document.activeElement : null;
1801
+ this.captureInitialFocus(scope.popover);
1754
1802
  this.document.addEventListener("focusin", this.handleFocusIn, true);
1755
1803
  this.active = true;
1756
1804
  }
1757
1805
  this.update(scope);
1806
+ if (scope.autoFocus === false)
1807
+ return;
1758
1808
  const currentFocus = this.document?.activeElement;
1759
- if (scope.autoFocus !== false || !isNode(currentFocus, scope.popover) || !this.isAllowed(currentFocus)) {
1760
- this.focusFallback();
1809
+ if (scope.deferFocus && isNode(currentFocus, scope.popover) && this.isAllowed(currentFocus)) {
1810
+ return;
1761
1811
  }
1812
+ this.focusFallback();
1762
1813
  }
1763
1814
  update(scope) {
1764
1815
  this.popover = scope.popover;
@@ -1771,10 +1822,23 @@ class FocusGuard {
1771
1822
  if (this.active)
1772
1823
  this.focusFallback();
1773
1824
  }
1774
- deactivate() {
1775
- if (!this.active) {
1825
+ captureInitialFocus(reference, pending) {
1826
+ if (this.active || this.initialFocus)
1776
1827
  return;
1828
+ const activeElement = pending ?? ownerDocument(reference)?.activeElement;
1829
+ this.initialFocus = isHTMLElement(activeElement, reference) ? activeElement : null;
1830
+ }
1831
+ deactivate() {
1832
+ const focusToRestore = this.release();
1833
+ if (focusToRestore?.isConnected) {
1834
+ focusToRestore.focus();
1777
1835
  }
1836
+ }
1837
+ release() {
1838
+ const focusToRestore = this.initialFocus;
1839
+ this.initialFocus = null;
1840
+ if (!this.active)
1841
+ return null;
1778
1842
  this.document?.removeEventListener("focusin", this.handleFocusIn, true);
1779
1843
  this.document = null;
1780
1844
  this.active = false;
@@ -1782,11 +1846,7 @@ class FocusGuard {
1782
1846
  this.allowedTarget = null;
1783
1847
  this.allowTargetInteraction = false;
1784
1848
  this.restoreFallback();
1785
- const focusToRestore = this.initialFocus;
1786
- this.initialFocus = null;
1787
- if (focusToRestore?.isConnected) {
1788
- focusToRestore.focus();
1789
- }
1849
+ return focusToRestore;
1790
1850
  }
1791
1851
  isAllowed(target) {
1792
1852
  if (target === this.fallback)
@@ -1825,8 +1885,8 @@ class FocusGuard {
1825
1885
  }
1826
1886
  findFocusable(root, direction) {
1827
1887
  const candidates = focusableTourControls(root);
1828
- const selector = direction === "advance" ? "[data-glow-tour-advance-trigger]" : "[data-glow-tour-previous-trigger]";
1829
- return candidates.find((candidate) => candidate.matches(selector)) ?? null;
1888
+ const find = (trigger) => candidates.find((candidate) => candidate.matches(`[data-glow-tour-${trigger}-trigger]`));
1889
+ return direction === "previous" && find("previous") || find("advance") || null;
1830
1890
  }
1831
1891
  }
1832
1892
  function belongsToScope(target, scope) {
@@ -1891,11 +1951,157 @@ class ScrollLock {
1891
1951
  }
1892
1952
  }
1893
1953
 
1954
+ // packages/core/src/utils/options.ts
1955
+ function mergeStepProps(base, patch) {
1956
+ return {
1957
+ ...base,
1958
+ ...patch,
1959
+ data: patch.data ? { ...base.data, ...patch.data } : base.data,
1960
+ overlay: mergeOverlayOptions(base.overlay, patch.overlay),
1961
+ popover: mergePopoverOptions(base.popover, patch.popover),
1962
+ indicator: mergeIndicatorOptions(base.indicator, patch.indicator),
1963
+ behavior: mergeStepBehavior(base.behavior, patch.behavior),
1964
+ controls: mergeControls(base.controls, patch.controls),
1965
+ classNames: mergeClassNames(base.classNames, patch.classNames)
1966
+ };
1967
+ }
1968
+ function mergeClassNames(base, patch) {
1969
+ if (!patch)
1970
+ return base;
1971
+ const merged = { ...base };
1972
+ for (const slot in patch) {
1973
+ const key = slot;
1974
+ merged[key] = patch[key] ?? merged[key];
1975
+ }
1976
+ return merged;
1977
+ }
1978
+ function mergeOverlayOptions(defaults, overrides) {
1979
+ if (!defaults && !overrides) {
1980
+ return;
1981
+ }
1982
+ return {
1983
+ ...mergeBaseOptions(defaults, overrides),
1984
+ color: overrides?.color ?? defaults?.color,
1985
+ opacity: overrides?.opacity ?? defaults?.opacity,
1986
+ padding: overrides?.padding ?? defaults?.padding,
1987
+ radius: overrides?.radius ?? defaults?.radius
1988
+ };
1989
+ }
1990
+ function mergeIndicatorOptions(defaults, overrides) {
1991
+ if (!defaults && !overrides) {
1992
+ return;
1993
+ }
1994
+ return {
1995
+ ...mergeBaseOptions(defaults, overrides),
1996
+ hidden: overrides?.hidden ?? defaults?.hidden,
1997
+ gap: overrides?.gap ?? defaults?.gap,
1998
+ placementTryOrder: cloneArray(overrides?.placementTryOrder ?? defaults?.placementTryOrder)
1999
+ };
2000
+ }
2001
+ function mergePopoverOptions(defaults, overrides) {
2002
+ if (!defaults && !overrides) {
2003
+ return;
2004
+ }
2005
+ const placementTryOrder = overrides?.placementTryOrder ?? defaults?.placementTryOrder;
2006
+ const hasArrow = !!defaults?.arrow || !!overrides?.arrow;
2007
+ return {
2008
+ ...mergeBaseOptions(defaults, overrides),
2009
+ arrow: hasArrow ? {
2010
+ hidden: overrides?.arrow?.hidden ?? defaults?.arrow?.hidden,
2011
+ color: overrides?.arrow?.color ?? defaults?.arrow?.color,
2012
+ size: overrides?.arrow?.size ?? defaults?.arrow?.size,
2013
+ borderWidth: overrides?.arrow?.borderWidth ?? defaults?.arrow?.borderWidth,
2014
+ borderRadius: overrides?.arrow?.borderRadius ?? defaults?.arrow?.borderRadius,
2015
+ edgePadding: overrides?.arrow?.edgePadding ?? defaults?.arrow?.edgePadding,
2016
+ styleNonce: overrides?.arrow?.styleNonce ?? defaults?.arrow?.styleNonce,
2017
+ autoStyles: overrides?.arrow?.autoStyles ?? defaults?.arrow?.autoStyles
2018
+ } : undefined,
2019
+ gap: overrides?.gap ?? defaults?.gap,
2020
+ placementTryOrder: cloneArray(placementTryOrder)
2021
+ };
2022
+ }
2023
+ function mergeScrollOptions(defaults, overrides) {
2024
+ if (!defaults && !overrides) {
2025
+ return;
2026
+ }
2027
+ return {
2028
+ behavior: overrides?.behavior ?? defaults?.behavior,
2029
+ block: overrides?.block ?? defaults?.block,
2030
+ inline: overrides?.inline ?? defaults?.inline,
2031
+ returnDelay: overrides?.returnDelay ?? defaults?.returnDelay
2032
+ };
2033
+ }
2034
+ function mergeAnimationOptions(defaults, overrides) {
2035
+ if (!defaults) {
2036
+ return overrides ? { ...overrides } : undefined;
2037
+ }
2038
+ if (!overrides) {
2039
+ return { ...defaults };
2040
+ }
2041
+ return {
2042
+ duration: overrides.duration ?? defaults.duration,
2043
+ easing: overrides.easing ?? defaults.easing
2044
+ };
2045
+ }
2046
+ function mergeBaseOptions(defaults, overrides) {
2047
+ return {
2048
+ animated: overrides?.animated ?? defaults?.animated,
2049
+ animation: mergeAnimationOptions(defaults?.animation, overrides?.animation)
2050
+ };
2051
+ }
2052
+ function mergeStepBehavior(defaults, overrides) {
2053
+ if (!defaults && !overrides) {
2054
+ return;
2055
+ }
2056
+ return {
2057
+ allowInteraction: overrides?.allowInteraction ?? defaults?.allowInteraction,
2058
+ allowScroll: overrides?.allowScroll ?? defaults?.allowScroll,
2059
+ autoFocus: overrides?.autoFocus ?? defaults?.autoFocus,
2060
+ autoScroll: overrides?.autoScroll ?? defaults?.autoScroll,
2061
+ missingTarget: mergeMissingTarget(defaults?.missingTarget, overrides?.missingTarget),
2062
+ scroll: mergeScrollOptions(defaults?.scroll, overrides?.scroll),
2063
+ overlayClick: overrides?.overlayClick ?? defaults?.overlayClick
2064
+ };
2065
+ }
2066
+ function mergeControls(defaults, overrides) {
2067
+ if (!defaults && !overrides)
2068
+ return;
2069
+ return {
2070
+ previous: mergeControl(defaults?.previous, overrides?.previous),
2071
+ advance: mergeControl(defaults?.advance, overrides?.advance),
2072
+ cancel: mergeControl(defaults?.cancel, overrides?.cancel)
2073
+ };
2074
+ }
2075
+ function mergeControl(defaults, overrides) {
2076
+ if (!defaults && !overrides)
2077
+ return;
2078
+ return {
2079
+ state: overrides?.state ?? defaults?.state,
2080
+ keys: cloneArray(overrides?.keys ?? defaults?.keys)
2081
+ };
2082
+ }
2083
+ function mergeMissingTarget(defaults, overrides) {
2084
+ if (!defaults && !overrides)
2085
+ return;
2086
+ return {
2087
+ strategy: overrides?.strategy ?? defaults?.strategy,
2088
+ timeout: overrides?.timeout ?? defaults?.timeout
2089
+ };
2090
+ }
2091
+ function cloneArray(value) {
2092
+ return value ? [...value] : undefined;
2093
+ }
2094
+ function isControlAvailable(props, command) {
2095
+ return props !== undefined && props.controls?.[command]?.state !== "disabled";
2096
+ }
2097
+
1894
2098
  // packages/core/src/dom/tour-view-driver.ts
1895
2099
  var SCROLL_SETTLE_STILL_FRAMES = 2;
1896
2100
  var SCROLL_SETTLE_GRACE_FRAMES = 3;
1897
2101
  var SCROLL_SETTLE_EPSILON = 0.5;
1898
2102
  var SCROLL_SETTLE_TIMEOUT = 2000;
2103
+ var USER_SCROLL_IDLE_DELAY = 150;
2104
+ var DEFAULT_SCROLL_RETURN_DELAY = 500;
1899
2105
  var ACTIVE_MODAL_BY_DOCUMENT = new WeakMap;
1900
2106
  var DEFAULT_SHORTCUTS = {
1901
2107
  previous: ["ArrowLeft", "Backspace"],
@@ -1917,6 +2123,8 @@ class DomTourViewDriver {
1917
2123
  active = false;
1918
2124
  frozen = false;
1919
2125
  awaitingStepUi = false;
2126
+ userScrolling = false;
2127
+ scrollTimer;
1920
2128
  activeTarget = null;
1921
2129
  targetFocusedAtFreeze = false;
1922
2130
  lastTargetRect = null;
@@ -1925,11 +2133,17 @@ class DomTourViewDriver {
1925
2133
  modalDocument = null;
1926
2134
  modalRoot = null;
1927
2135
  overlay = null;
1928
- pendingKeyboardCommand = null;
2136
+ pendingCommand = null;
1929
2137
  pointer = null;
1930
2138
  pendingFocusGeneration = null;
2139
+ handledTriggerClick = null;
1931
2140
  popover = null;
1932
2141
  presentationDirty = false;
2142
+ targetPending = false;
2143
+ popoverHidden = false;
2144
+ focusToRestore = null;
2145
+ appliedAllowInteraction = false;
2146
+ pointerFading = false;
1933
2147
  rafId = null;
1934
2148
  rafCancel = null;
1935
2149
  root = null;
@@ -1985,12 +2199,41 @@ class DomTourViewDriver {
1985
2199
  this.pointer?.initializeProps();
1986
2200
  this.refreshRegisteredElements();
1987
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
+ }
1988
2231
  async show(step, direction, signal, onBeforePopoverAppear) {
1989
2232
  this.throwIfAborted(signal);
1990
2233
  const generation = this.beginGeneration();
1991
2234
  const removeAbort = this.cancelAnimationsOnAbort(signal);
1992
- const replaceVisiblePopover = this.active && onBeforePopoverAppear !== undefined;
1993
- let removeTransitionKeydown = () => {};
2235
+ const replaceVisiblePopover = this.active && !this.popoverHidden && onBeforePopoverAppear !== undefined;
2236
+ let removeTransitionListeners = () => {};
1994
2237
  try {
1995
2238
  this.cleanupStepResources();
1996
2239
  this.throwIfStale(generation, signal);
@@ -2005,41 +2248,50 @@ class DomTourViewDriver {
2005
2248
  this.lastViewport = null;
2006
2249
  this.presentationDirty = false;
2007
2250
  this.awaitingStepUi = true;
2008
- if (replaceVisiblePopover) {
2009
- const listener = (event) => this.queueTransitionKeydown(event, step, generation);
2010
- const currentWindow = this.getWindow();
2011
- if (typeof currentWindow?.addEventListener === "function") {
2012
- currentWindow.addEventListener("keydown", listener);
2013
- removeTransitionKeydown = () => currentWindow.removeEventListener("keydown", listener);
2014
- }
2251
+ const modal = !step.allowsInteraction();
2252
+ if (modal)
2253
+ this.claimModal();
2254
+ const onKeydown = (event) => this.queueTransitionKeydown(event, step, generation, replaceVisiblePopover);
2255
+ const onClick = (event) => {
2256
+ if (replaceVisiblePopover)
2257
+ this.queueTransitionClick(event, step, generation);
2258
+ };
2259
+ const currentWindow = this.getWindow();
2260
+ if (typeof currentWindow?.addEventListener === "function") {
2261
+ currentWindow.addEventListener("keydown", onKeydown);
2262
+ currentWindow.addEventListener("click", onClick);
2263
+ removeTransitionListeners = () => {
2264
+ currentWindow.removeEventListener("keydown", onKeydown);
2265
+ currentWindow.removeEventListener("click", onClick);
2266
+ };
2015
2267
  }
2016
2268
  const target = step.target;
2017
2269
  if (!target)
2018
2270
  return;
2019
2271
  this.activeTarget = target;
2020
- this.syncModality(step.behavior?.allowInteraction === true);
2272
+ this.focusGuard.captureInitialFocus(target, this.focusToRestore);
2021
2273
  const scrolling = this.beginTargetScroll(step, target, signal);
2022
2274
  this.throwIfStale(generation, signal);
2023
- this.initializeElements(step);
2024
- const resolveRect = () => target.getBoundingClientRect();
2275
+ this.initializeElements(step, replaceVisiblePopover);
2276
+ const resolveRect = () => presentationRect(step, target);
2025
2277
  await this.appear(resolveRect, step, generation, scrolling, replaceVisiblePopover, onBeforePopoverAppear);
2026
2278
  this.throwIfStale(generation, signal);
2027
2279
  const targetRect = resolveRect();
2028
2280
  this.lastTargetRect = snapshotRect(targetRect);
2029
2281
  this.lastViewport = snapshotViewport(target);
2030
2282
  this.active = true;
2031
- this.activateFocus(step, target, direction, generation);
2283
+ this.engagePopover(step, target, direction, generation);
2032
2284
  this.syncScrollLock(step);
2033
2285
  this.throwIfStale(generation, signal);
2034
- removeTransitionKeydown();
2035
- removeTransitionKeydown = () => {};
2286
+ removeTransitionListeners();
2287
+ removeTransitionListeners = () => {};
2036
2288
  this.attachStepResources(step, target, generation, signal);
2037
2289
  } catch (error) {
2038
2290
  if (this.isCurrentGeneration(generation))
2039
2291
  this.releaseModality();
2040
2292
  throw error;
2041
2293
  } finally {
2042
- removeTransitionKeydown();
2294
+ removeTransitionListeners();
2043
2295
  removeAbort();
2044
2296
  }
2045
2297
  }
@@ -2049,10 +2301,11 @@ class DomTourViewDriver {
2049
2301
  this.throwIfAborted(signal);
2050
2302
  const generation = this.beginGeneration();
2051
2303
  const removeAbort = this.cancelAnimationsOnAbort(signal);
2304
+ this.setTargetPending(false);
2052
2305
  try {
2053
2306
  this.cleanupStepResources();
2054
2307
  this.releaseModality();
2055
- this.focusGuard.deactivate();
2308
+ this.focusToRestore = this.focusGuard.release() ?? this.focusToRestore;
2056
2309
  this.scrollLock.deactivate();
2057
2310
  this.throwIfStale(generation, signal);
2058
2311
  this.active = false;
@@ -2064,20 +2317,24 @@ class DomTourViewDriver {
2064
2317
  this.currentSignal = null;
2065
2318
  this.lastTargetRect = null;
2066
2319
  this.lastViewport = null;
2067
- this.popover?.getElement()?.removeAttribute("aria-modal");
2068
2320
  await Promise.allSettled([
2069
2321
  this.overlay?.disappear() ?? Promise.resolve(),
2070
2322
  this.popover?.disappear() ?? Promise.resolve(),
2071
2323
  this.pointer?.disappear() ?? Promise.resolve()
2072
2324
  ]);
2073
- this.throwIfStale(generation, signal);
2074
2325
  } finally {
2326
+ if (this.isCurrentGeneration(generation)) {
2327
+ this.focusToRestore?.focus();
2328
+ this.focusToRestore = null;
2329
+ }
2075
2330
  removeAbort();
2076
2331
  }
2332
+ this.throwIfStale(generation, signal);
2077
2333
  }
2078
2334
  releaseMount() {
2079
2335
  if (this.disposed)
2080
2336
  return;
2337
+ this.setTargetPending(false);
2081
2338
  this.beginGeneration();
2082
2339
  this.cleanupStepResources();
2083
2340
  this.releaseModality();
@@ -2124,45 +2381,43 @@ class DomTourViewDriver {
2124
2381
  if (this.disposed || !step || !target || !targetRect || !signal)
2125
2382
  return;
2126
2383
  this.activeTarget = target;
2127
- this.initializeElements(step);
2384
+ this.initializeElements(step, false);
2128
2385
  this.awaitingStepUi = true;
2129
2386
  await this.appear(() => targetRect, step, generation, null, false);
2130
2387
  this.throwIfStale(generation);
2131
- this.activateFocus(step, target, this.direction, generation);
2388
+ this.engagePopover(step, target, this.direction, generation);
2132
2389
  this.syncScrollLock(step);
2133
2390
  this.throwIfStale(generation);
2134
2391
  this.attachStepResources(step, target, generation, signal);
2135
2392
  }
2136
- initializeElements(step) {
2137
- const interactionAllowed = step.behavior?.allowInteraction === true;
2393
+ initializeElements(step, replaceVisiblePopover) {
2394
+ const interactionAllowed = step.allowsInteraction();
2138
2395
  this.overlay?.initializeProps();
2139
- this.overlay?.setAnimationOptions(animationOptions(step, step.overlay, this.overlay.getElement()));
2396
+ this.overlay?.setAnimationOptions(animationOptions(step, step.props.get().overlay, this.overlay.getElement()));
2140
2397
  this.overlay?.setInteractionAllowed(interactionAllowed);
2141
- this.popover?.initializeProps();
2142
- this.popover?.setAnimationOptions(animationOptions(step, step.popover, this.popover.getElement()));
2143
- const popover = this.popover?.getElement();
2144
- if (isHTMLElement(popover, this.root ?? popover) && !interactionAllowed)
2145
- popover.setAttribute("aria-modal", "true");
2398
+ this.appliedAllowInteraction = interactionAllowed;
2399
+ this.popover?.initializeProps(!replaceVisiblePopover);
2400
+ this.popover?.setAnimationOptions(animationOptions(step, step.props.get().popover, this.popover.getElement()));
2146
2401
  this.pointer?.initializeProps();
2147
- this.pointer?.setAnimationOptions(animationOptions(step, step.indicator, this.pointer.getElement()));
2402
+ this.pointer?.setAnimationOptions(animationOptions(step, step.props.get().indicator, this.pointer.getElement()));
2148
2403
  }
2149
2404
  syncModality(interactionAllowed) {
2150
2405
  if (interactionAllowed) {
2151
2406
  this.releaseModality();
2152
2407
  return;
2153
2408
  }
2154
- const root = this.root;
2155
- if (!root)
2409
+ this.claimModal();
2410
+ if (this.popoverHidden) {
2411
+ this.liftModality();
2156
2412
  return;
2157
- const document2 = root.ownerDocument;
2158
- const owner = ACTIVE_MODAL_BY_DOCUMENT.get(document2);
2159
- if (owner && owner !== this.modalToken) {
2160
- throw new Error("GlowTour.js only supports one active modal tour per document");
2161
2413
  }
2162
- ACTIVE_MODAL_BY_DOCUMENT.set(document2, this.modalToken);
2163
- this.modalDocument = document2;
2164
- if (this.modalRoot === root)
2414
+ const popover = this.popover?.getElement();
2415
+ if (isHTMLElement(popover, this.root ?? popover))
2416
+ popover.setAttribute("aria-modal", "true");
2417
+ const root = this.root;
2418
+ if (!root || this.modalRoot === root)
2165
2419
  return;
2420
+ const document2 = root.ownerDocument;
2166
2421
  this.restoreInertBranches();
2167
2422
  this.modalRoot = root;
2168
2423
  let branch = root;
@@ -2179,14 +2434,28 @@ class DomTourViewDriver {
2179
2434
  branch = parent;
2180
2435
  }
2181
2436
  }
2437
+ claimModal() {
2438
+ const document2 = this.root?.ownerDocument;
2439
+ if (!document2)
2440
+ return;
2441
+ const owner = ACTIVE_MODAL_BY_DOCUMENT.get(document2);
2442
+ if (owner && owner !== this.modalToken) {
2443
+ throw new Error("GlowTour.js only supports one active modal tour per document");
2444
+ }
2445
+ ACTIVE_MODAL_BY_DOCUMENT.set(document2, this.modalToken);
2446
+ this.modalDocument = document2;
2447
+ }
2182
2448
  releaseModality() {
2183
- this.popover?.getElement()?.removeAttribute("aria-modal");
2184
- this.restoreInertBranches();
2449
+ this.liftModality();
2185
2450
  const document2 = this.modalDocument;
2186
2451
  if (document2 && ACTIVE_MODAL_BY_DOCUMENT.get(document2) === this.modalToken) {
2187
2452
  ACTIVE_MODAL_BY_DOCUMENT.delete(document2);
2188
2453
  }
2189
2454
  this.modalDocument = null;
2455
+ }
2456
+ liftModality() {
2457
+ this.popover?.getElement()?.removeAttribute("aria-modal");
2458
+ this.restoreInertBranches();
2190
2459
  this.modalRoot = null;
2191
2460
  }
2192
2461
  restoreInertBranches() {
@@ -2202,14 +2471,14 @@ class DomTourViewDriver {
2202
2471
  async appear(resolveRect, step, generation, scrolling, hadVisiblePopover, onBeforePopoverAppear) {
2203
2472
  const spotlightRect = resolveRect();
2204
2473
  const stepUi = this.presentStepUi(resolveRect, step, generation, scrolling, hadVisiblePopover, onBeforePopoverAppear);
2205
- const spotlight = this.overlay?.moveToTarget(spotlightRect, step, scrolling !== null);
2474
+ const spotlight = this.overlay?.moveToTarget(spotlightRect, this.elementProps(step), scrolling !== null);
2206
2475
  if (scrolling)
2207
2476
  this.schedulePosition(generation);
2208
2477
  await Promise.all([spotlight ?? Promise.resolve(), stepUi]);
2209
2478
  }
2210
2479
  async presentStepUi(resolveRect, step, generation, scrolling, hadVisiblePopover, onBeforePopoverAppear) {
2211
2480
  if (hadVisiblePopover)
2212
- await this.popover?.disappear();
2481
+ await this.popover?.disappear(false);
2213
2482
  if (onBeforePopoverAppear) {
2214
2483
  await onBeforePopoverAppear();
2215
2484
  this.syncControlState(step);
@@ -2223,12 +2492,15 @@ class DomTourViewDriver {
2223
2492
  await this.enterStepUi(resolveRect(), step);
2224
2493
  }
2225
2494
  enterStepUi(targetRect, step) {
2226
- const popoverPlacement = this.popover?.resolvePosition(targetRect, step).placement;
2495
+ const popoverPlacement = this.popover?.resolvePosition(targetRect, this.elementProps(step)).placement;
2227
2496
  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()
2497
+ !this.popoverHidden && this.popover?.present(targetRect, this.elementProps(step)) || Promise.resolve(),
2498
+ this.presentPointer(targetRect, step, popoverPlacement) ?? Promise.resolve()
2230
2499
  ]);
2231
2500
  }
2501
+ presentPointer(targetRect, step, popoverPlacement) {
2502
+ return this.isPointerEnabled(step) ? this.pointer?.moveToTarget(targetRect, step.props.get(), true, popoverPlacement) : this.pointer?.disappear();
2503
+ }
2232
2504
  attachStepResources(step, target, generation, signal) {
2233
2505
  let initialPropsNotification = true;
2234
2506
  this.stepCleanups.push(step.props.subscribe(() => {
@@ -2239,7 +2511,14 @@ class DomTourViewDriver {
2239
2511
  return;
2240
2512
  }
2241
2513
  this.presentationDirty = true;
2514
+ this.syncScrollLock(step);
2515
+ if (step.allowsInteraction() === this.appliedAllowInteraction)
2516
+ return;
2517
+ this.syncInteraction(step);
2518
+ this.pointerFading = step.allowsInteraction() === this.appliedAllowInteraction;
2242
2519
  }));
2520
+ if (step.allowsInteraction() !== this.appliedAllowInteraction)
2521
+ this.syncInteraction(step);
2243
2522
  this.stepCleanups.push(this.commands?.subscribeCapabilities?.((active) => {
2244
2523
  if (!this.isCurrentGeneration(generation))
2245
2524
  return;
@@ -2249,7 +2528,7 @@ class DomTourViewDriver {
2249
2528
  this.focusGuard.focus();
2250
2529
  }
2251
2530
  if (active)
2252
- this.flushPendingKeyboardCommand(step, generation);
2531
+ this.flushPendingCommand(step, generation);
2253
2532
  }) ?? (() => {}));
2254
2533
  this.attachTargetResources(step, target, generation, signal);
2255
2534
  const currentWindow = this.getWindow(target);
@@ -2264,6 +2543,20 @@ class DomTourViewDriver {
2264
2543
  }
2265
2544
  });
2266
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
+ }
2267
2560
  this.attachButtonHandlers(step);
2268
2561
  this.observeControls(step, generation);
2269
2562
  this.syncControlState(step);
@@ -2271,14 +2564,22 @@ class DomTourViewDriver {
2271
2564
  this.schedulePosition(generation);
2272
2565
  }
2273
2566
  attachTargetResources(step, target, generation, signal) {
2274
- for (const handler of step.definition.eventHandlers) {
2567
+ if (step.detached)
2568
+ return;
2569
+ for (const handler of step.definition.targetEvents) {
2275
2570
  const listener = (event) => {
2276
2571
  if (!this.isCurrentGeneration(generation))
2277
2572
  return;
2278
2573
  const context = Object.freeze({
2279
2574
  advance: () => this.commandForStep("advance", step, signal),
2280
2575
  cancel: () => this.commandForStep("cancel", step, signal),
2576
+ goTo: async (id) => {
2577
+ if (!signal.aborted && this.currentStep === step)
2578
+ await this.commands?.goTo(id);
2579
+ },
2281
2580
  previous: () => this.commandForStep("previous", step, signal),
2581
+ direction: step.direction,
2582
+ initialProps: step.initialProps,
2282
2583
  props: step.props,
2283
2584
  signal,
2284
2585
  target
@@ -2337,21 +2638,21 @@ class DomTourViewDriver {
2337
2638
  this.freezeForDisconnectedTarget(step, target, generation);
2338
2639
  return;
2339
2640
  }
2340
- const targetRect = target.getBoundingClientRect();
2641
+ const targetRect = presentationRect(step, target);
2341
2642
  const targetSnapshot = snapshotRect(targetRect);
2342
2643
  const viewportSnapshot = snapshotViewport(target);
2343
2644
  const presentationChanged = this.presentationDirty;
2344
2645
  if (!presentationChanged && sameRect(targetSnapshot, this.lastTargetRect) && sameViewport(viewportSnapshot, this.lastViewport))
2345
2646
  return;
2346
2647
  if (presentationChanged) {
2347
- this.overlay?.setAnimationOptions(animationOptions(step, step.overlay, this.overlay.getElement()));
2348
- this.popover?.setAnimationOptions(animationOptions(step, step.popover, this.popover.getElement()));
2349
- this.pointer?.setAnimationOptions(animationOptions(step, step.indicator, this.pointer.getElement()));
2648
+ this.overlay?.setAnimationOptions(animationOptions(step, step.props.get().overlay, this.overlay.getElement()));
2649
+ this.popover?.setAnimationOptions(animationOptions(step, step.props.get().popover, this.popover.getElement()));
2650
+ this.pointer?.setAnimationOptions(animationOptions(step, step.props.get().indicator, this.pointer.getElement()));
2350
2651
  this.syncControlState(step);
2351
2652
  this.syncShortcutLabels(step);
2352
2653
  }
2353
- this.overlay?.updatePosition(targetRect, step, presentationChanged, (transition) => this.observeDynamicOperation(transition, generation));
2354
- if (!this.awaitingStepUi)
2654
+ this.overlay?.updatePosition(targetRect, this.elementProps(step), presentationChanged, (transition) => this.observeDynamicOperation(transition, generation));
2655
+ if (!this.awaitingStepUi && !this.userScrolling)
2355
2656
  this.trackStepUi(targetRect, step, generation, presentationChanged);
2356
2657
  this.lastTargetRect = targetSnapshot;
2357
2658
  this.lastViewport = viewportSnapshot;
@@ -2359,20 +2660,85 @@ class DomTourViewDriver {
2359
2660
  this.presentationDirty = false;
2360
2661
  }
2361
2662
  trackStepUi(targetRect, step, generation, presentationChanged) {
2362
- const popoverPlacement = this.popover?.updatePosition(targetRect, step, (reposition) => this.observeDynamicOperation(reposition, generation));
2663
+ const popoverPlacement = this.placePopover(targetRect, step, generation);
2363
2664
  if (presentationChanged) {
2364
- this.pointer?.syncVisibility(this.isPointerEnabled(step), targetRect, step, popoverPlacement);
2665
+ if (this.pointerFading)
2666
+ this.pointerFading = false;
2667
+ else
2668
+ this.pointer?.syncVisibility(this.isPointerEnabled(step), targetRect, step.props.get(), popoverPlacement);
2365
2669
  } else if (this.isPointerEnabled(step)) {
2366
2670
  const pointer = this.pointer?.getElement();
2367
2671
  if (pointer?.getAttribute("aria-hidden") === "true") {
2368
- this.observeDynamicOperation(this.pointer?.moveToTarget(targetRect, step, true, popoverPlacement), generation);
2672
+ this.observeDynamicOperation(this.pointer?.moveToTarget(targetRect, step.props.get(), true, popoverPlacement), generation);
2369
2673
  } else {
2370
- this.pointer?.updatePosition(targetRect, step, popoverPlacement);
2674
+ this.pointer?.updatePosition(targetRect, step.props.get(), popoverPlacement);
2371
2675
  }
2372
2676
  } else if (this.pointer?.getElement()?.getAttribute("aria-hidden") !== "true") {
2373
2677
  this.observeDynamicOperation(this.pointer?.disappear(), generation);
2374
2678
  }
2375
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
+ }
2376
2742
  observeDynamicOperation(operation, generation) {
2377
2743
  if (!operation)
2378
2744
  return;
@@ -2387,72 +2753,77 @@ class DomTourViewDriver {
2387
2753
  }
2388
2754
  handleKeydown(event) {
2389
2755
  const step = this.currentStep;
2390
- 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)
2391
2757
  return;
2392
- if (event.key === "Tab" && step.behavior?.allowInteraction !== true) {
2758
+ if (event.key === "Tab" && !step.allowsInteraction()) {
2393
2759
  this.loopFocus(event);
2394
2760
  return;
2395
2761
  }
2396
- const shortcuts = step.popover?.keyboardShortcuts;
2397
- if ((shortcuts?.cancel ?? DEFAULT_SHORTCUTS.cancel).includes(event.key) && this.canCommand("cancel", step)) {
2398
- event.preventDefault();
2399
- this.command("cancel", "keyboard");
2400
- return;
2401
- }
2402
- if (isEditable(event.target, this.root))
2762
+ const command = this.keyboardCommand(event, step, (command2) => this.canCommand(command2, step));
2763
+ if (!command)
2403
2764
  return;
2404
- if ((shortcuts?.advance ?? DEFAULT_SHORTCUTS.advance).includes(event.key) && this.canCommand("advance", step)) {
2405
- event.preventDefault();
2406
- this.command("advance", "keyboard");
2407
- } else if ((shortcuts?.previous ?? DEFAULT_SHORTCUTS.previous).includes(event.key) && this.canCommand("previous", step)) {
2408
- event.preventDefault();
2409
- this.command("previous", "keyboard");
2410
- }
2765
+ event.preventDefault();
2766
+ this.command(command, "keyboard");
2767
+ }
2768
+ keyboardCommand(event, step, available) {
2769
+ if (activatesControl(event, this.root))
2770
+ return null;
2771
+ const controls = step.props.get().controls;
2772
+ const shortcut = (command) => (controls?.[command]?.keys ?? DEFAULT_SHORTCUTS[command]).includes(event.key) && available(command);
2773
+ return shortcut("cancel") ? "cancel" : isEditable(event.target, this.root) ? null : shortcut("advance") ? "advance" : shortcut("previous") ? "previous" : null;
2411
2774
  }
2412
2775
  handleOverlayClick(event, step, target) {
2413
2776
  if (this.currentStep !== step || event.defaultPrevented)
2414
2777
  return;
2415
- if (step.behavior?.allowInteraction === true)
2778
+ if (step.allowsInteraction())
2416
2779
  return;
2417
2780
  const path = event.composedPath();
2418
2781
  const popover = this.popover?.getElement();
2419
2782
  const pointer = this.pointer?.getElement();
2420
- if (path.includes(target) || popover && path.includes(popover) || pointer && path.includes(pointer))
2783
+ if (!step.detached && path.includes(target) || popover && path.includes(popover) || pointer && path.includes(pointer))
2421
2784
  return;
2422
- const overlayClick = step.behavior?.overlayClick ?? "none";
2785
+ const overlayClick = step.props.get().behavior?.overlayClick ?? "none";
2423
2786
  if (overlayClick === "advance" && this.canCommand("advance", step)) {
2424
2787
  this.command("advance", "overlay");
2425
2788
  } else if (overlayClick === "cancel" && this.canCommand("cancel", step)) {
2426
2789
  this.command("cancel", "overlay");
2427
2790
  }
2428
2791
  }
2429
- queueTransitionKeydown(event, step, generation) {
2792
+ queueTransitionKeydown(event, step, generation, queue) {
2793
+ const target = event.target;
2794
+ const root = this.root;
2430
2795
  if (!this.isCurrentGeneration(generation) || event.defaultPrevented || event.isComposing || event.ctrlKey || event.metaKey || event.altKey)
2431
2796
  return;
2432
- const shortcuts = step.popover?.keyboardShortcuts;
2433
- let command = null;
2434
- if ((shortcuts?.cancel ?? DEFAULT_SHORTCUTS.cancel).includes(event.key))
2435
- command = "cancel";
2436
- else if (!isEditable(event.target, this.root)) {
2437
- if ((shortcuts?.advance ?? DEFAULT_SHORTCUTS.advance).includes(event.key) && step.props.get().popover?.disableAdvanceButton !== true)
2438
- command = "advance";
2439
- else if ((shortcuts?.previous ?? DEFAULT_SHORTCUTS.previous).includes(event.key) && step.props.get().popover?.disablePreviousButton !== true)
2440
- command = "previous";
2797
+ if (!step.allowsInteraction() && isHTMLElement(target, root) && target !== target.ownerDocument.body && !root?.contains(target)) {
2798
+ event.preventDefault();
2799
+ return;
2441
2800
  }
2801
+ if (!queue)
2802
+ return;
2803
+ const command = this.keyboardCommand(event, step, (command2) => isControlAvailable(step.props.get(), command2));
2442
2804
  if (!command)
2443
2805
  return;
2444
2806
  event.preventDefault();
2445
- this.pendingKeyboardCommand ??= { command, generation };
2807
+ this.pendingCommand ??= { command, generation, source: "keyboard" };
2808
+ }
2809
+ queueTransitionClick(event, step, generation) {
2810
+ const scope = this.root ?? this.popover?.getElement();
2811
+ if (!this.isCurrentGeneration(generation) || event.defaultPrevented || event === this.handledTriggerClick || !isHTMLElement(scope, scope) || !isElement(event.target, scope))
2812
+ return;
2813
+ const match = this.findClickedTrigger(event.target, scope);
2814
+ if (!match || this.isLiveDisabled(match.trigger) || !isControlAvailable(step.props.get(), match.command))
2815
+ return;
2816
+ this.pendingCommand ??= { command: match.command, generation, source: "trigger" };
2446
2817
  }
2447
- flushPendingKeyboardCommand(step, generation) {
2448
- const pending = this.pendingKeyboardCommand;
2818
+ flushPendingCommand(step, generation) {
2819
+ const pending = this.pendingCommand;
2449
2820
  if (!pending || pending.generation !== generation)
2450
2821
  return;
2451
- this.pendingKeyboardCommand = null;
2822
+ this.pendingCommand = null;
2452
2823
  queueMicrotask(() => {
2453
2824
  if (!this.isCurrentGeneration(generation) || this.currentStep !== step || !this.canCommand(pending.command, step))
2454
2825
  return;
2455
- this.commandForGeneration(pending.command, generation, "keyboard");
2826
+ this.commandForGeneration(pending.command, generation, pending.source);
2456
2827
  });
2457
2828
  }
2458
2829
  loopFocus(event) {
@@ -2475,36 +2846,74 @@ class DomTourViewDriver {
2475
2846
  focusable[0]?.focus();
2476
2847
  }
2477
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
+ }
2478
2887
  activateFocus(step, target, direction, generation) {
2479
2888
  const popover = this.popover?.getElement();
2480
2889
  if (!isHTMLElement(popover, this.root ?? popover))
2481
2890
  return;
2482
- const autoFocus = step.behavior?.disableAutoFocus !== true;
2891
+ const autoFocus = step.autoFocuses();
2483
2892
  const deferFocus = autoFocus && this.commands?.subscribeCapabilities !== undefined;
2484
2893
  if (deferFocus)
2485
2894
  this.pendingFocusGeneration = generation;
2486
2895
  this.focusGuard.activate({
2487
2896
  allowedTarget: target,
2488
- allowTargetInteraction: step.behavior?.allowInteraction === true,
2489
- autoFocus: autoFocus && !deferFocus,
2897
+ allowTargetInteraction: step.allowsInteraction(),
2898
+ autoFocus,
2899
+ deferFocus,
2490
2900
  direction,
2491
2901
  fallback: this.root ?? popover.parentElement,
2492
2902
  popover
2493
2903
  });
2494
2904
  }
2495
2905
  syncScrollLock(step) {
2496
- if (step.allowScroll) {
2906
+ if (step.allowsScroll()) {
2497
2907
  this.scrollLock.deactivate();
2498
2908
  return;
2499
2909
  }
2500
2910
  this.scrollLock.activate(this.root?.ownerDocument ?? this.popover?.getElement()?.ownerDocument);
2501
2911
  }
2502
2912
  syncShortcutLabels(step) {
2503
- for (const advance of this.findTriggers("advance")) {
2504
- syncKeyShortcuts(advance, step.popover?.keyboardShortcuts?.advance ?? DEFAULT_SHORTCUTS.advance);
2505
- }
2506
- for (const previous of this.findTriggers("previous")) {
2507
- syncKeyShortcuts(previous, step.popover?.keyboardShortcuts?.previous ?? DEFAULT_SHORTCUTS.previous);
2913
+ for (const command of ["advance", "previous"]) {
2914
+ const shortcuts = isControlAvailable(step.props.get(), command) ? step.props.get().controls?.[command]?.keys ?? DEFAULT_SHORTCUTS[command] : [];
2915
+ for (const trigger of this.findTriggers(command))
2916
+ syncKeyShortcuts(trigger, shortcuts);
2508
2917
  }
2509
2918
  }
2510
2919
  attachButtonHandlers(step) {
@@ -2518,6 +2927,7 @@ class DomTourViewDriver {
2518
2927
  const match = this.findClickedTrigger(event.target, scope);
2519
2928
  if (!match)
2520
2929
  return;
2930
+ this.handledTriggerClick = event;
2521
2931
  this.deferTriggerCommand(match.command, event, step, generation, match.trigger);
2522
2932
  });
2523
2933
  }
@@ -2568,13 +2978,13 @@ class DomTourViewDriver {
2568
2978
  }
2569
2979
  syncControlState(step) {
2570
2980
  for (const advance of this.findTriggers("advance")) {
2571
- this.syncControl(advance, this.commands?.isAdvanceDisabled() === true || step.props.get().popover?.disableAdvanceButton === true);
2981
+ this.syncControl(advance, this.commands?.isAdvanceDisabled() === true || !isControlAvailable(step.props.get(), "advance"));
2572
2982
  }
2573
2983
  for (const previous of this.findTriggers("previous")) {
2574
- this.syncControl(previous, this.commands?.isPreviousDisabled() === true || step.props.get().popover?.disablePreviousButton === true);
2984
+ this.syncControl(previous, this.commands?.isPreviousDisabled() === true || !isControlAvailable(step.props.get(), "previous"));
2575
2985
  }
2576
2986
  for (const cancel of this.findTriggers("cancel")) {
2577
- this.syncControl(cancel, this.commands?.isCancelDisabled() === true);
2987
+ this.syncControl(cancel, this.commands?.isCancelDisabled() === true || !isControlAvailable(step.props.get(), "cancel"));
2578
2988
  }
2579
2989
  }
2580
2990
  findTriggers(direction) {
@@ -2606,14 +3016,12 @@ class DomTourViewDriver {
2606
3016
  return !signal.aborted && this.currentStep === step ? this.command(command, "api") : Promise.resolve();
2607
3017
  }
2608
3018
  canCommand(command, step, trigger) {
2609
- if (this.isConsumerDisabled(trigger ?? null))
3019
+ if (this.isConsumerDisabled(trigger ?? null) || !isControlAvailable(step.props.get(), command))
2610
3020
  return false;
2611
- if (command === "advance") {
2612
- return (this.commands?.canAdvance?.() ?? true) && step.props.get().popover?.disableAdvanceButton !== true;
2613
- }
2614
- if (command === "previous") {
2615
- return (this.commands?.canPrevious?.() ?? true) && step.props.get().popover?.disablePreviousButton !== true;
2616
- }
3021
+ if (command === "advance")
3022
+ return this.commands?.canAdvance?.() ?? true;
3023
+ if (command === "previous")
3024
+ return this.commands?.canPrevious?.() ?? true;
2617
3025
  return this.commands?.canCancel?.() ?? true;
2618
3026
  }
2619
3027
  syncControl(element, disabled) {
@@ -2634,13 +3042,19 @@ class DomTourViewDriver {
2634
3042
  isLiveDisabled(element) {
2635
3043
  return element.disabled || element.getAttribute("aria-disabled") === "true" || this.isConsumerDisabled(element);
2636
3044
  }
3045
+ elementProps(step) {
3046
+ const props = step.props.get();
3047
+ return step.detached ? { ...props, detached: true } : props;
3048
+ }
2637
3049
  isPointerEnabled(step) {
2638
- return step.behavior?.allowInteraction === true && step.indicator?.disabled !== true;
3050
+ return step.allowsInteraction() && step.props.get().indicator?.hidden !== true;
2639
3051
  }
2640
3052
  cleanupStepResources() {
2641
3053
  this.cancelScroll?.();
2642
3054
  this.cancelScroll = null;
3055
+ this.stopUserScroll();
2643
3056
  this.presentationDirty = false;
3057
+ this.pointerFading = false;
2644
3058
  if (this.rafId !== null)
2645
3059
  this.rafCancel?.(this.rafId);
2646
3060
  this.rafId = null;
@@ -2668,22 +3082,51 @@ class DomTourViewDriver {
2668
3082
  this.targetFocusedAtFreeze = this.isFocusInsideTarget(target);
2669
3083
  this.cleanupTargetResources();
2670
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
+ }
2671
3093
  Promise.resolve(this.commands?.targetDisconnected(target)).catch((error) => {
2672
3094
  this.commands?.reportError(error).catch(() => {});
2673
3095
  });
2674
3096
  }
2675
3097
  applyInteractionLock(step, locked) {
2676
- const allowed = !locked && step.behavior?.allowInteraction === true;
3098
+ const allowed = !locked && step.allowsInteraction();
2677
3099
  this.overlay?.setInteractionAllowed(allowed);
2678
3100
  this.syncModality(allowed);
3101
+ }
3102
+ applyInteraction(step, target) {
3103
+ this.applyInteractionLock(step, false);
3104
+ this.appliedAllowInteraction = step.allowsInteraction();
2679
3105
  const popover = this.popover?.getElement();
2680
- if (isHTMLElement(popover, this.root ?? popover)) {
2681
- if (allowed)
2682
- popover.removeAttribute("aria-modal");
2683
- else
2684
- popover.setAttribute("aria-modal", "true");
3106
+ if (!this.popoverHidden && isHTMLElement(popover, this.root ?? popover)) {
3107
+ this.focusGuard.update({
3108
+ allowedTarget: target,
3109
+ allowTargetInteraction: step.allowsInteraction(),
3110
+ direction: this.direction,
3111
+ fallback: this.root ?? popover.parentElement,
3112
+ popover
3113
+ });
2685
3114
  }
2686
3115
  }
3116
+ syncInteraction(step) {
3117
+ const target = this.activeTarget;
3118
+ if (this.disposed || !this.active || this.frozen || this.awaitingStepUi && !this.userScrolling || this.currentStep !== step || !target)
3119
+ return;
3120
+ const focusWasInTarget = this.isFocusInsideTarget(target);
3121
+ this.applyInteraction(step, target);
3122
+ if (focusWasInTarget && !step.allowsInteraction() && step.autoFocuses())
3123
+ this.focusGuard.focus();
3124
+ if (this.userScrolling)
3125
+ return;
3126
+ const targetRect = target.getBoundingClientRect();
3127
+ this.pointer?.cancelAnimations();
3128
+ this.observeDynamicOperation(this.presentPointer(targetRect, step, this.popover?.resolvePosition(targetRect, this.elementProps(step)).placement), this.generation);
3129
+ }
2687
3130
  isFocusInsideTarget(target) {
2688
3131
  const activeElement = target.ownerDocument?.activeElement;
2689
3132
  if (!activeElement)
@@ -2700,23 +3143,15 @@ class DomTourViewDriver {
2700
3143
  this.frozen = false;
2701
3144
  this.currentSignal = signal;
2702
3145
  this.activeTarget = target;
2703
- this.applyInteractionLock(step, false);
3146
+ this.applyInteraction(step, target);
2704
3147
  this.attachTargetResources(step, target, this.generation, signal);
2705
- const popover = this.popover?.getElement();
2706
- if (isHTMLElement(popover, this.root ?? popover)) {
2707
- this.focusGuard.update({
2708
- allowedTarget: target,
2709
- allowTargetInteraction: step.behavior?.allowInteraction === true,
2710
- direction: this.direction,
2711
- fallback: this.root ?? popover.parentElement,
2712
- popover
2713
- });
2714
- }
2715
- if (this.targetFocusedAtFreeze) {
2716
- this.targetFocusedAtFreeze = false;
2717
- if (step.behavior?.allowInteraction === true)
3148
+ if (this.targetFocusedAtFreeze && step.autoFocuses()) {
3149
+ if (step.allowsInteraction())
2718
3150
  target.focus();
3151
+ else
3152
+ this.focusGuard.focus();
2719
3153
  }
3154
+ this.targetFocusedAtFreeze = false;
2720
3155
  this.syncControlState(step);
2721
3156
  this.syncShortcutLabels(step);
2722
3157
  this.moveToRetargetedRect(step, target);
@@ -2724,18 +3159,18 @@ class DomTourViewDriver {
2724
3159
  }
2725
3160
  moveToRetargetedRect(step, target) {
2726
3161
  const generation = this.generation;
2727
- const targetRect = target.getBoundingClientRect();
2728
- this.observeDynamicOperation(this.overlay?.animateTo(targetRect, step), generation);
2729
- const placement = this.popover?.updatePosition(targetRect, step, (reposition) => this.observeDynamicOperation(reposition, generation));
3162
+ const targetRect = presentationRect(step, target);
3163
+ this.observeDynamicOperation(this.overlay?.animateTo(targetRect, this.elementProps(step)), generation);
3164
+ const placement = this.placePopover(targetRect, step, generation);
2730
3165
  if (this.isPointerEnabled(step)) {
2731
- this.observeDynamicOperation(this.pointer?.moveToTarget(targetRect, step, true, placement), generation);
3166
+ this.observeDynamicOperation(this.pointer?.moveToTarget(targetRect, step.props.get(), true, placement), generation);
2732
3167
  }
2733
3168
  this.lastTargetRect = snapshotRect(targetRect);
2734
3169
  this.lastViewport = snapshotViewport(target);
2735
3170
  }
2736
3171
  beginGeneration() {
2737
3172
  this.generation += 1;
2738
- this.pendingKeyboardCommand = null;
3173
+ this.pendingCommand = null;
2739
3174
  this.pendingFocusGeneration = null;
2740
3175
  this.cancelElementAnimations();
2741
3176
  return this.generation;
@@ -2755,18 +3190,21 @@ class DomTourViewDriver {
2755
3190
  }
2756
3191
  beginTargetScroll(step, target, signal) {
2757
3192
  this.throwIfAborted(signal);
2758
- if (step.behavior?.disableAutoScroll)
3193
+ if (step.detached || step.props.get().behavior?.autoScroll === false)
2759
3194
  return null;
2760
3195
  if (isInViewport(target.getBoundingClientRect(), target))
2761
3196
  return null;
2762
- const currentWindow = this.getWindow(target);
2763
- if (!currentWindow)
3197
+ if (!this.getWindow(target))
2764
3198
  return null;
2765
- const behavior = prefersReducedMotion(target) ? "instant" : step.behavior?.scroll?.behavior ?? "smooth";
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";
2766
3204
  target.scrollIntoView({
2767
3205
  behavior,
2768
- block: step.behavior?.scroll?.block ?? "center",
2769
- inline: step.behavior?.scroll?.inline ?? "nearest"
3206
+ block: scroll?.block ?? "center",
3207
+ inline: scroll?.inline ?? "nearest"
2770
3208
  });
2771
3209
  if (behavior === "instant")
2772
3210
  return null;
@@ -2845,6 +3283,12 @@ function animationOptions(step, options, element) {
2845
3283
  easing: options?.animation?.easing
2846
3284
  };
2847
3285
  }
3286
+ function presentationRect(step, target) {
3287
+ if (!step.detached)
3288
+ return target.getBoundingClientRect();
3289
+ const viewport = viewportDimensions(target);
3290
+ return { height: 0, left: viewport.width / 2, top: viewport.height / 2, width: 0 };
3291
+ }
2848
3292
  function abortError2() {
2849
3293
  return new DOMException("The operation was aborted", "AbortError");
2850
3294
  }
@@ -2880,6 +3324,10 @@ function sameViewport(left, right) {
2880
3324
  function finite(value, fallback = 0) {
2881
3325
  return Number.isFinite(value) ? value : fallback;
2882
3326
  }
3327
+ function activatesControl(event, context) {
3328
+ const target = event.target;
3329
+ return event.key === "Enter" && isHTMLElement(target, context) && target.matches(FOCUSABLE_SELECTOR);
3330
+ }
2883
3331
  function isEditable(target, context) {
2884
3332
  if (!isHTMLElement(target, context))
2885
3333
  return false;
@@ -2950,12 +3398,13 @@ function validateIndicator(path, indicator) {
2950
3398
  validateFiniteNonNegative(`${path}.gap`, indicator.gap);
2951
3399
  }
2952
3400
  function validateBehavior(path, behavior) {
2953
- validateFiniteNonNegative(`${path}.targetTimeout`, behavior?.targetTimeout);
3401
+ validateFiniteNonNegative(`${path}.missingTarget.timeout`, behavior?.missingTarget?.timeout);
2954
3402
  }
2955
3403
  function validateStepProps(path, props) {
2956
3404
  validateOverlay(`${path}.overlay`, props.overlay);
2957
3405
  validatePopover(`${path}.popover`, props.popover);
2958
3406
  validateIndicator(`${path}.indicator`, props.indicator);
3407
+ validateBehavior(`${path}.behavior`, props.behavior);
2959
3408
  }
2960
3409
  function validateWorkflowOptions(workflow) {
2961
3410
  validateOverlay("options.overlay", workflow.options.overlay);
@@ -2963,115 +3412,10 @@ function validateWorkflowOptions(workflow) {
2963
3412
  validateIndicator("options.indicator", workflow.options.indicator);
2964
3413
  validateBehavior("options.behavior", workflow.options.behavior);
2965
3414
  for (const [index, step] of workflow.steps.entries()) {
2966
- const path = `steps[${index}]`;
2967
- validateBehavior(`${path}.behavior`, step.behavior);
2968
- validateStepProps(path, step.props);
3415
+ validateStepProps(`steps[${index}]`, step.props);
2969
3416
  }
2970
3417
  }
2971
3418
 
2972
- // packages/core/src/utils/options.ts
2973
- function mergeOverlayOptions(defaults, overrides) {
2974
- if (!defaults && !overrides) {
2975
- return;
2976
- }
2977
- return {
2978
- ...mergeBaseOptions(defaults, overrides),
2979
- color: overrides?.color ?? defaults?.color,
2980
- opacity: overrides?.opacity ?? defaults?.opacity,
2981
- padding: overrides?.padding ?? defaults?.padding,
2982
- radius: overrides?.radius ?? defaults?.radius
2983
- };
2984
- }
2985
- function mergeIndicatorOptions(defaults, overrides) {
2986
- if (!defaults && !overrides) {
2987
- return;
2988
- }
2989
- return {
2990
- ...mergeBaseOptions(defaults, overrides),
2991
- disabled: overrides?.disabled ?? defaults?.disabled,
2992
- gap: overrides?.gap ?? defaults?.gap,
2993
- placementTryOrder: cloneArray(overrides?.placementTryOrder ?? defaults?.placementTryOrder)
2994
- };
2995
- }
2996
- function mergePopoverOptions(defaults, overrides) {
2997
- if (!defaults && !overrides) {
2998
- return;
2999
- }
3000
- const placementTryOrder = overrides?.placementTryOrder ?? defaults?.placementTryOrder;
3001
- const hasArrow = !!defaults?.arrow || !!overrides?.arrow;
3002
- const hasKeyboardShortcuts = !!defaults?.keyboardShortcuts || !!overrides?.keyboardShortcuts;
3003
- return {
3004
- ...mergeBaseOptions(defaults, overrides),
3005
- arrow: hasArrow ? {
3006
- disabled: overrides?.arrow?.disabled ?? defaults?.arrow?.disabled,
3007
- color: overrides?.arrow?.color ?? defaults?.arrow?.color,
3008
- size: overrides?.arrow?.size ?? defaults?.arrow?.size,
3009
- borderWidth: overrides?.arrow?.borderWidth ?? defaults?.arrow?.borderWidth,
3010
- borderRadius: overrides?.arrow?.borderRadius ?? defaults?.arrow?.borderRadius,
3011
- edgePadding: overrides?.arrow?.edgePadding ?? defaults?.arrow?.edgePadding,
3012
- styleNonce: overrides?.arrow?.styleNonce ?? defaults?.arrow?.styleNonce,
3013
- disableAutoStyles: overrides?.arrow?.disableAutoStyles ?? defaults?.arrow?.disableAutoStyles
3014
- } : undefined,
3015
- disableAdvanceButton: overrides?.disableAdvanceButton ?? defaults?.disableAdvanceButton,
3016
- disablePreviousButton: overrides?.disablePreviousButton ?? defaults?.disablePreviousButton,
3017
- gap: overrides?.gap ?? defaults?.gap,
3018
- hideAdvanceButton: overrides?.hideAdvanceButton ?? defaults?.hideAdvanceButton,
3019
- hideFooter: overrides?.hideFooter ?? defaults?.hideFooter,
3020
- hidePreviousButton: overrides?.hidePreviousButton ?? defaults?.hidePreviousButton,
3021
- placementTryOrder: cloneArray(placementTryOrder),
3022
- keyboardShortcuts: hasKeyboardShortcuts ? {
3023
- previous: cloneArray(overrides?.keyboardShortcuts?.previous ?? defaults?.keyboardShortcuts?.previous),
3024
- advance: cloneArray(overrides?.keyboardShortcuts?.advance ?? defaults?.keyboardShortcuts?.advance),
3025
- cancel: cloneArray(overrides?.keyboardShortcuts?.cancel ?? defaults?.keyboardShortcuts?.cancel)
3026
- } : undefined
3027
- };
3028
- }
3029
- function mergeScrollOptions(defaults, overrides) {
3030
- if (!defaults && !overrides) {
3031
- return;
3032
- }
3033
- return {
3034
- behavior: overrides?.behavior ?? defaults?.behavior,
3035
- block: overrides?.block ?? defaults?.block,
3036
- inline: overrides?.inline ?? defaults?.inline
3037
- };
3038
- }
3039
- function mergeAnimationOptions(defaults, overrides) {
3040
- if (!defaults) {
3041
- return overrides ? { ...overrides } : undefined;
3042
- }
3043
- if (!overrides) {
3044
- return { ...defaults };
3045
- }
3046
- return {
3047
- duration: overrides.duration ?? defaults.duration,
3048
- easing: overrides.easing ?? defaults.easing
3049
- };
3050
- }
3051
- function mergeBaseOptions(defaults, overrides) {
3052
- return {
3053
- animated: overrides?.animated ?? defaults?.animated,
3054
- animation: mergeAnimationOptions(defaults?.animation, overrides?.animation)
3055
- };
3056
- }
3057
- function mergeStepBehavior(defaults, overrides) {
3058
- if (!defaults && !overrides) {
3059
- return;
3060
- }
3061
- return {
3062
- allowInteraction: overrides?.allowInteraction ?? defaults?.allowInteraction,
3063
- disableAutoFocus: overrides?.disableAutoFocus ?? defaults?.disableAutoFocus,
3064
- disableAutoScroll: overrides?.disableAutoScroll ?? defaults?.disableAutoScroll,
3065
- missingTargetStrategy: overrides?.missingTargetStrategy ?? defaults?.missingTargetStrategy,
3066
- scroll: mergeScrollOptions(defaults?.scroll, overrides?.scroll),
3067
- targetTimeout: overrides?.targetTimeout ?? defaults?.targetTimeout,
3068
- overlayClick: overrides?.overlayClick ?? defaults?.overlayClick
3069
- };
3070
- }
3071
- function cloneArray(value) {
3072
- return value ? [...value] : undefined;
3073
- }
3074
-
3075
3419
  // packages/core/src/runtime/step-props-store.ts
3076
3420
  function createStepPropsStore(initialProps, reportListenerError, path = "steps[0]") {
3077
3421
  let current = freezeStepProps(initialProps);
@@ -3085,16 +3429,18 @@ function createStepPropsStore(initialProps, reportListenerError, path = "steps[0
3085
3429
  } catch {}
3086
3430
  }
3087
3431
  };
3432
+ const set = (update) => {
3433
+ const next = typeof update === "function" ? update(current) : update;
3434
+ validateStepProps(path, next);
3435
+ current = freezeStepProps(next);
3436
+ const published = current;
3437
+ for (const listener of Array.from(listeners))
3438
+ notify(listener, published);
3439
+ };
3088
3440
  return Object.freeze({
3089
3441
  get: () => current,
3090
- set: (update) => {
3091
- const next = typeof update === "function" ? update(current) : update;
3092
- validateStepProps(path, next);
3093
- current = freezeStepProps(next);
3094
- const published = current;
3095
- for (const listener of Array.from(listeners))
3096
- notify(listener, published);
3097
- },
3442
+ set,
3443
+ update: (patch) => set((props) => mergeStepProps(props, typeof patch === "function" ? patch(props) : patch)),
3098
3444
  subscribe: (listener) => {
3099
3445
  notify(listener, current);
3100
3446
  listeners.add(listener);
@@ -3116,41 +3462,34 @@ class ActiveStep {
3116
3462
  rootDocument;
3117
3463
  initialProps;
3118
3464
  props;
3119
- behavior;
3120
3465
  animated;
3121
- allowScroll;
3122
3466
  target = null;
3467
+ detached = false;
3468
+ direction = "advance";
3123
3469
  constructor(definition, defaults, reportSubscriberError = () => {}, path = "steps[0]", rootDocument) {
3124
3470
  this.definition = definition;
3125
3471
  this.path = path;
3126
3472
  this.rootDocument = rootDocument;
3127
- this.initialProps = freezeStepProps({
3128
- title: definition.props.title,
3129
- content: definition.props.content,
3130
- data: definition.props.data,
3131
- overlay: mergeOverlayOptions(defaults.overlay, definition.props.overlay),
3132
- popover: mergePopoverOptions(defaults.popover, definition.props.popover),
3133
- indicator: mergeIndicatorOptions(defaults.indicator, definition.props.indicator)
3134
- });
3473
+ this.initialProps = freezeStepProps(mergeStepProps(defaults, definition.props));
3135
3474
  this.props = createStepPropsStore(this.initialProps, reportSubscriberError, path);
3136
- this.behavior = mergeStepBehavior(defaults.behavior, definition.behavior);
3137
3475
  this.animated = defaults.animated;
3138
- this.allowScroll = defaults.allowScroll !== false;
3139
3476
  }
3140
- reset() {
3141
- this.props.set(this.initialProps);
3477
+ allowsInteraction() {
3478
+ return !this.detached && this.props.get().behavior?.allowInteraction === true;
3142
3479
  }
3143
- get overlay() {
3144
- return this.props.get().overlay;
3480
+ autoFocuses() {
3481
+ return this.props.get().behavior?.autoFocus !== false;
3145
3482
  }
3146
- get popover() {
3147
- return this.props.get().popover;
3483
+ allowsScroll() {
3484
+ return this.props.get().behavior?.allowScroll !== false;
3148
3485
  }
3149
- get indicator() {
3150
- return this.props.get().indicator;
3486
+ resolveTarget(signal) {
3487
+ return resolveTargetElement(this.definition.target, { document: this.rootDocument, signal }, this.path);
3151
3488
  }
3152
- async resolveTarget(signal) {
3153
- return await resolveTargetElement(this.definition.target, { document: this.rootDocument, signal }, this.path);
3489
+ detach() {
3490
+ const body = (this.rootDocument ?? globalThis.document)?.body ?? null;
3491
+ this.detached = body !== null;
3492
+ return body;
3154
3493
  }
3155
3494
  snapshot() {
3156
3495
  return Object.freeze({
@@ -3231,7 +3570,7 @@ class TourRootBinding {
3231
3570
  hasPopover() {
3232
3571
  return this.popover !== null;
3233
3572
  }
3234
- get document() {
3573
+ getDocument() {
3235
3574
  return this.root.ownerDocument;
3236
3575
  }
3237
3576
  release() {
@@ -3365,11 +3704,11 @@ function attachRootBridge(tour, driver, isDisposed, onMountReleaseStart, onMount
3365
3704
  return {
3366
3705
  assertCanRun(workflow) {
3367
3706
  if (!binding)
3368
- throw new Error("Glow tour requires a connected root before run()");
3707
+ throw new Error("Glow tour requires a connected root before start()");
3369
3708
  if (workflow.steps.length > 0 && !binding.hasPopover()) {
3370
- throw new Error("Glow tour requires a connected popover before run()");
3709
+ throw new Error("Glow tour requires a connected popover before start()");
3371
3710
  }
3372
- return binding.document;
3711
+ return binding.getDocument();
3373
3712
  },
3374
3713
  release() {
3375
3714
  binding?.release();
@@ -3459,13 +3798,10 @@ function prefixReservations(document2) {
3459
3798
  var DEFAULT_TARGET_TIMEOUT = 3000;
3460
3799
  var TARGET_LOSS_GRACE_MS = 150;
3461
3800
  var DISPOSED_ERROR_MESSAGE = "Tour controller is disposed";
3462
- function resolveStartIndex(workflow, startAt) {
3463
- if (startAt === undefined)
3464
- return 0;
3465
- const index = workflow.steps.findIndex((step) => step.id === startAt);
3466
- if (index === -1) {
3467
- throw new Error(`Workflow "${workflow.name}" has no step with id "${startAt}" to start at.`);
3468
- }
3801
+ function resolveStepIndex(workflow, id) {
3802
+ const index = workflow.steps.findIndex((step) => step.id === id);
3803
+ if (index === -1)
3804
+ throw new Error(`Workflow "${workflow.name}" has no step with id "${id}".`);
3469
3805
  return index;
3470
3806
  }
3471
3807
  function normalizedError(error) {
@@ -3491,13 +3827,16 @@ class TourController {
3491
3827
  recoveringTarget = null;
3492
3828
  operationToken = 0;
3493
3829
  publicationRevision = 0;
3830
+ awaitingTargetOperation = null;
3494
3831
  operation = null;
3832
+ popoverHidden = false;
3495
3833
  disposed = false;
3496
3834
  retainedPresentation = null;
3497
3835
  stateListeners = new Set;
3498
3836
  stepPropsSubscriptions = [];
3499
3837
  tourStartedAt = 0;
3500
3838
  stepEnteredAt = 0;
3839
+ pendingTourStart;
3501
3840
  commandSource = "api";
3502
3841
  state = Object.freeze({
3503
3842
  get: () => this.snapshot,
@@ -3523,7 +3862,8 @@ class TourController {
3523
3862
  canCancel: () => this.status === "active" && this.isCancelAvailable(),
3524
3863
  canPrevious: () => this.canNavigate("previous"),
3525
3864
  cancel: (source) => this.cancel(source),
3526
- isAdvanceDisabled: () => !this.isPresentedAdvanceAvailable(),
3865
+ goTo: (id) => this.goTo(id),
3866
+ isAdvanceDisabled: () => this.awaitingTargetOperation !== null || !this.isPresentedAdvanceAvailable(),
3527
3867
  isCancelDisabled: () => !this.isPresentedCancelAvailable(),
3528
3868
  isPreviousDisabled: () => !this.isPresentedPreviousAvailable(),
3529
3869
  previous: (source) => this.previous(source),
@@ -3544,10 +3884,10 @@ class TourController {
3544
3884
  create(name, options = {}) {
3545
3885
  return new WorkflowBuilder(name, options);
3546
3886
  }
3547
- async run(workflow, runOptions = {}) {
3887
+ async start(workflow, runOptions = {}) {
3548
3888
  this.assertNotDisposed();
3549
3889
  validateWorkflowOptions(workflow);
3550
- const startIndex = resolveStartIndex(workflow, runOptions.startAt);
3890
+ const startIndex = runOptions.startAt === undefined ? 0 : resolveStepIndex(workflow, runOptions.startAt);
3551
3891
  const rootDocument = this.options.assertCanRun?.(workflow) ?? undefined;
3552
3892
  const retainedPresentation = this.capturePresentation();
3553
3893
  const operation = this.beginOperation();
@@ -3564,6 +3904,9 @@ class TourController {
3564
3904
  this.error = null;
3565
3905
  this.commandSource = "api";
3566
3906
  this.retainedPresentation = retainedPresentation;
3907
+ this.popoverHidden = false;
3908
+ this.driver.setPopoverHidden?.(false);
3909
+ this.pendingTourStart = undefined;
3567
3910
  try {
3568
3911
  this.setStatus("starting");
3569
3912
  this.assertCurrent(operation);
@@ -3571,16 +3914,16 @@ class TourController {
3571
3914
  await workflow.options.onStart?.(startContext);
3572
3915
  this.assertCurrent(operation);
3573
3916
  if (isStartAborted()) {
3574
- this.resetToIdle();
3917
+ await this.resetToIdle(operation);
3575
3918
  return;
3576
3919
  }
3577
3920
  this.tourStartedAt = Date.now();
3578
- this.emit("tour:start", this.steps[startIndex] ?? null, 0);
3921
+ this.pendingTourStart = this.steps[startIndex] ?? null;
3579
3922
  if (this.steps.length === 0) {
3580
3923
  await this.finish(operation);
3581
3924
  return;
3582
3925
  }
3583
- await this.enter(startIndex, "advance", operation);
3926
+ await this.navigate(startIndex, "advance", operation);
3584
3927
  } catch (error) {
3585
3928
  await this.handleFailure(error, operation);
3586
3929
  }
@@ -3593,15 +3936,16 @@ class TourController {
3593
3936
  this.assertNotDisposed();
3594
3937
  await this.transitionFromPublic("previous", undefined, source);
3595
3938
  }
3596
- async goToStep(index) {
3939
+ async goTo(id) {
3597
3940
  this.assertNotDisposed();
3598
- if (this.status === "starting" || this.status === "transitioning")
3941
+ const workflow = this.workflow;
3942
+ if (!workflow || this.status === "starting" || this.status === "transitioning")
3599
3943
  return;
3600
- if (index < 0 || index >= this.steps.length) {
3601
- throw new Error(`Step index ${index} is out of bounds`);
3602
- }
3603
- const direction = index > this.index ? "advance" : "previous";
3604
- await this.transitionFromPublic(direction, index, "api");
3944
+ const index = resolveStepIndex(workflow, id);
3945
+ await this.transitionFromPublic(this.directionTo(index), index, "api");
3946
+ }
3947
+ directionTo(index) {
3948
+ return index > this.index ? "advance" : "previous";
3605
3949
  }
3606
3950
  async cancel(source = "api") {
3607
3951
  this.assertNotDisposed();
@@ -3615,6 +3959,17 @@ class TourController {
3615
3959
  await this.handleFailure(error, operation);
3616
3960
  }
3617
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
+ }
3618
3973
  dispose() {
3619
3974
  if (this.disposed)
3620
3975
  return;
@@ -3627,6 +3982,7 @@ class TourController {
3627
3982
  this.direction = "advance";
3628
3983
  this.error = null;
3629
3984
  this.retainedPresentation = null;
3985
+ this.pendingTourStart = undefined;
3630
3986
  this.status = "disposed";
3631
3987
  this.publish(true);
3632
3988
  this.stateListeners.clear();
@@ -3647,29 +4003,57 @@ class TourController {
3647
4003
  this.direction = "advance";
3648
4004
  this.error = null;
3649
4005
  this.retainedPresentation = null;
4006
+ this.pendingTourStart = undefined;
3650
4007
  this.status = "idle";
3651
4008
  }
3652
4009
  completeMountRelease() {
3653
4010
  if (!this.disposed)
3654
4011
  this.publish();
3655
4012
  }
3656
- async enter(index, direction, operation) {
3657
- this.direction = direction;
3658
- this.emitStepLeave(this.currentStep());
3659
- this.setStatus("transitioning");
4013
+ async navigate(index, direction, operation, lostStep) {
4014
+ const from = this.currentStep();
4015
+ if (this.status !== "transitioning")
4016
+ this.setStatus("transitioning");
3660
4017
  this.assertCurrent(operation);
3661
- const step = this.steps[index];
3662
- if (!step)
3663
- throw new Error(`Step index ${index} is out of bounds`);
3664
- if (step.definition.resetPropsOnEnter !== false)
3665
- step.reset();
3666
- const target = await this.resolveTarget(step, operation);
3667
- this.assertCurrent(operation);
3668
- if (!target) {
3669
- await this.advancePastMissingTarget(index, direction, operation);
4018
+ const skipped = [];
4019
+ let step = this.steps[index];
4020
+ let target = null;
4021
+ while (step) {
4022
+ target = await this.resolveTarget(step, operation);
4023
+ this.assertCurrent(operation);
4024
+ if (target)
4025
+ break;
4026
+ skipped.push(step);
4027
+ index += direction === "advance" ? 1 : -1;
4028
+ step = this.steps[index];
4029
+ }
4030
+ if (!step || !target) {
4031
+ if (index >= this.steps.length || lostStep)
4032
+ this.direction = direction;
4033
+ if (index >= this.steps.length)
4034
+ await this.finish(operation, skipped);
4035
+ else if (!lostStep)
4036
+ this.setStatus("active");
4037
+ else
4038
+ await this.cancelCurrent(operation);
3670
4039
  return;
3671
4040
  }
3672
4041
  step.target = target;
4042
+ step.direction = direction;
4043
+ if (await this.runStepHook(step.definition.beforeEnter, step, operation, direction)) {
4044
+ if (lostStep)
4045
+ throw this.missingTargetError(lostStep);
4046
+ if (from)
4047
+ this.setStatus("active");
4048
+ else
4049
+ await this.resetToIdle(operation);
4050
+ return;
4051
+ }
4052
+ this.direction = direction;
4053
+ this.flushTourStart();
4054
+ for (const skippedStep of skipped)
4055
+ this.emit("step:skip", skippedStep, 0);
4056
+ this.emitStepLeave(from);
3673
4057
  let committed = false;
3674
4058
  const commitStep = () => {
3675
4059
  this.assertCurrent(operation);
@@ -3708,29 +4092,24 @@ class TourController {
3708
4092
  return;
3709
4093
  this.setStatus("transitioning");
3710
4094
  this.assertCurrent(operation);
3711
- const hook = direction === "advance" ? step.definition.advanceAction : step.definition.previousAction;
3712
- await hook?.(this.createBeforeActionStepContext(step));
3713
- this.assertCurrent(operation);
3714
- if (destination !== undefined) {
3715
- await this.enter(destination, direction, operation);
3716
- return;
3717
- }
3718
- if (direction === "advance") {
3719
- const nextIndex = this.index + 1;
3720
- if (nextIndex >= this.steps.length)
3721
- await this.finish(operation);
3722
- else
3723
- await this.enter(nextIndex, direction, operation);
3724
- return;
3725
- }
3726
- if (this.index === 0) {
3727
- if (this.canCancel())
3728
- await this.cancelCurrent(operation);
3729
- else
3730
- this.setStatus("active");
4095
+ if (await this.runStepHook(step.definition.beforeLeave, step, operation, direction)) {
4096
+ this.setStatus("active");
3731
4097
  return;
3732
4098
  }
3733
- await this.enter(this.index - 1, direction, operation);
4099
+ await this.navigate(destination ?? this.index + (direction === "advance" ? 1 : -1), direction, operation);
4100
+ }
4101
+ async runStepHook(hook, step, operation, direction) {
4102
+ if (!hook)
4103
+ return false;
4104
+ let aborted = false;
4105
+ await hook(Object.freeze({
4106
+ ...this.createStepHookContext(step, operation, direction),
4107
+ abort: () => {
4108
+ aborted = true;
4109
+ }
4110
+ }));
4111
+ this.assertCurrent(operation);
4112
+ return aborted;
3734
4113
  }
3735
4114
  async runActions(operation) {
3736
4115
  const step = this.currentStep();
@@ -3754,23 +4133,43 @@ class TourController {
3754
4133
  }
3755
4134
  async resolveTarget(step, operation) {
3756
4135
  const signal = this.signalFor(operation);
3757
- const strategy = step.behavior?.missingTargetStrategy ?? "error";
3758
- const timeout = step.behavior?.targetTimeout ?? DEFAULT_TARGET_TIMEOUT;
4136
+ const missingTarget = step.props.get().behavior?.missingTarget;
4137
+ const strategy = missingTarget?.strategy ?? "error";
4138
+ const timeout = missingTarget?.timeout ?? DEFAULT_TARGET_TIMEOUT;
3759
4139
  const startedAt = Date.now();
3760
- while (true) {
3761
- const target = await step.resolveTarget(signal);
3762
- this.assertCurrent(operation);
3763
- if (target)
3764
- return target;
3765
- if (strategy === "skip")
3766
- return null;
3767
- if (strategy !== "wait" || Date.now() - startedAt >= timeout) {
3768
- throw this.missingTargetError(step);
4140
+ step.detached = false;
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);
3769
4161
  }
3770
- await abortableDelay(16, signal);
3771
- this.assertCurrent(operation);
4162
+ } finally {
4163
+ this.setAwaitingTarget(operation, false);
3772
4164
  }
3773
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
+ }
3774
4173
  async pollForTarget(step, operation, budgetMs) {
3775
4174
  const startedAt = Date.now();
3776
4175
  while (true) {
@@ -3796,28 +4195,20 @@ class TourController {
3796
4195
  const operation = this.beginOperation();
3797
4196
  try {
3798
4197
  this.assertCurrent(operation);
3799
- const recoveredDuringGrace = await this.pollForTarget(step, operation, TARGET_LOSS_GRACE_MS);
4198
+ let recovered = await this.pollForTarget(step, operation, TARGET_LOSS_GRACE_MS);
3800
4199
  this.assertCurrent(operation);
3801
- if (recoveredDuringGrace) {
3802
- step.target = recoveredDuringGrace;
3803
- await this.driver.retarget(step, this.signalFor(operation));
4200
+ if (!recovered) {
4201
+ const strategy = step.props.get().behavior?.missingTarget?.strategy ?? "error";
4202
+ if (strategy === "skip") {
4203
+ await this.navigate(index + (direction === "advance" ? 1 : -1), direction, operation, step);
4204
+ return;
4205
+ }
4206
+ recovered = strategy === "detached" ? step.detach() : strategy === "wait" ? await this.pollForTarget(step, operation, (step.props.get().behavior?.missingTarget?.timeout ?? DEFAULT_TARGET_TIMEOUT) - TARGET_LOSS_GRACE_MS) : null;
3804
4207
  this.assertCurrent(operation);
3805
- this.publish();
3806
- return;
3807
- }
3808
- const strategy = step.behavior?.missingTargetStrategy ?? "error";
3809
- if (strategy === "skip") {
3810
- await this.advancePastRecoveryMissingTarget(step, index, direction, operation);
3811
- return;
4208
+ if (!recovered)
4209
+ throw this.missingTargetError(step);
3812
4210
  }
3813
- if (strategy !== "wait")
3814
- throw this.missingTargetError(step);
3815
- const timeout = step.behavior?.targetTimeout ?? DEFAULT_TARGET_TIMEOUT;
3816
- const recoveredAfterWait = await this.pollForTarget(step, operation, Math.max(0, timeout - TARGET_LOSS_GRACE_MS));
3817
- this.assertCurrent(operation);
3818
- if (!recoveredAfterWait)
3819
- throw this.missingTargetError(step);
3820
- step.target = recoveredAfterWait;
4211
+ step.target = recovered;
3821
4212
  await this.driver.retarget(step, this.signalFor(operation));
3822
4213
  this.assertCurrent(operation);
3823
4214
  this.publish();
@@ -3830,34 +4221,10 @@ class TourController {
3830
4221
  this.recoveringTarget = null;
3831
4222
  }
3832
4223
  }
3833
- async advancePastMissingTarget(index, direction, operation) {
3834
- const nextIndex = direction === "advance" ? index + 1 : index - 1;
3835
- if (nextIndex >= this.steps.length)
3836
- await this.finish(operation);
3837
- else if (nextIndex < 0) {
3838
- if (this.canCancel())
3839
- await this.cancelCurrent(operation);
3840
- else
3841
- this.setStatus("active");
3842
- } else
3843
- await this.enter(nextIndex, direction, operation);
3844
- }
3845
- async advancePastRecoveryMissingTarget(step, index, direction, operation) {
3846
- const nextIndex = direction === "advance" ? index + 1 : index - 1;
3847
- if (nextIndex >= this.steps.length)
3848
- await this.finish(operation);
3849
- else if (nextIndex < 0) {
3850
- if (this.canCancel())
3851
- await this.cancelCurrent(operation);
3852
- else
3853
- throw this.missingTargetError(step);
3854
- } else
3855
- await this.enter(nextIndex, direction, operation);
3856
- }
3857
4224
  missingTargetError(step) {
3858
4225
  return new Error(`Missing target at ${step.path}: ${String(step.definition.target)}`);
3859
4226
  }
3860
- async finish(operation) {
4227
+ async finish(operation, skipped = []) {
3861
4228
  this.assertCurrent(operation);
3862
4229
  const step = this.currentStep();
3863
4230
  const { context, isAborted } = this.createLifecycleHookContext(step);
@@ -3867,9 +4234,12 @@ class TourController {
3867
4234
  if (step)
3868
4235
  this.setStatus("active");
3869
4236
  else
3870
- this.resetToIdle();
4237
+ await this.resetToIdle(operation);
3871
4238
  return;
3872
4239
  }
4240
+ this.flushTourStart();
4241
+ for (const skippedStep of skipped)
4242
+ this.emit("step:skip", skippedStep, 0);
3873
4243
  this.emitStepLeave(step);
3874
4244
  await this.driver.clear(this.signalFor(operation));
3875
4245
  this.assertCurrent(operation);
@@ -3880,10 +4250,6 @@ class TourController {
3880
4250
  }
3881
4251
  async cancelCurrent(operation) {
3882
4252
  const step = this.currentStep();
3883
- if (step?.definition.cancelAction) {
3884
- await step.definition.cancelAction(this.createBeforeActionStepContext(step));
3885
- }
3886
- this.assertCurrent(operation);
3887
4253
  const { context, isAborted } = this.createLifecycleHookContext(step);
3888
4254
  await this.workflow?.options.onCancel?.(context);
3889
4255
  this.assertCurrent(operation);
@@ -3891,6 +4257,7 @@ class TourController {
3891
4257
  this.setStatus("active");
3892
4258
  return;
3893
4259
  }
4260
+ this.flushTourStart();
3894
4261
  this.emitStepLeave(step);
3895
4262
  await this.driver.clear(this.signalFor(operation));
3896
4263
  this.assertCurrent(operation);
@@ -3909,12 +4276,17 @@ class TourController {
3909
4276
  });
3910
4277
  return { context, isAborted: () => aborted };
3911
4278
  }
3912
- resetToIdle() {
4279
+ async resetToIdle(operation) {
4280
+ if (this.retainedPresentation) {
4281
+ await this.driver.clear(this.signalFor(operation));
4282
+ this.assertCurrent(operation);
4283
+ }
3913
4284
  this.workflow = null;
3914
4285
  this.releaseStepPropsSubscriptions();
3915
4286
  this.steps = [];
3916
4287
  this.index = -1;
3917
4288
  this.retainedPresentation = null;
4289
+ this.pendingTourStart = undefined;
3918
4290
  this.setStatus("idle");
3919
4291
  }
3920
4292
  async handleFailure(reason, operation) {
@@ -3924,6 +4296,7 @@ class TourController {
3924
4296
  this.error = error;
3925
4297
  this.retainedPresentation = null;
3926
4298
  this.setStatus("error");
4299
+ this.flushTourStart();
3927
4300
  this.emit("tour:error", this.currentStep(), Date.now() - this.tourStartedAt, error);
3928
4301
  if (!this.isCurrent(operation))
3929
4302
  throw error;
@@ -3938,9 +4311,8 @@ class TourController {
3938
4311
  return this.operationToken;
3939
4312
  }
3940
4313
  createStepContext(step, operation, onControl) {
3941
- if (!step.target)
3942
- throw new Error("Cannot create a step context without a target");
3943
4314
  return Object.freeze({
4315
+ ...this.createStepHookContext(step, operation, step.direction),
3944
4316
  advance: async () => {
3945
4317
  onControl?.();
3946
4318
  this.assertCurrent(operation);
@@ -3952,21 +4324,29 @@ class TourController {
3952
4324
  if (this.canCancel())
3953
4325
  await this.cancelCurrent(operation);
3954
4326
  },
4327
+ goTo: async (id) => {
4328
+ onControl?.();
4329
+ this.assertCurrent(operation);
4330
+ if (!this.workflow)
4331
+ return;
4332
+ const index = resolveStepIndex(this.workflow, id);
4333
+ await this.transition(this.directionTo(index), operation, index);
4334
+ },
3955
4335
  previous: async () => {
3956
4336
  onControl?.();
3957
4337
  this.assertCurrent(operation);
3958
4338
  await this.transition("previous", operation);
3959
- },
3960
- props: step.props,
3961
- signal: this.signalFor(operation),
3962
- target: step.target
4339
+ }
3963
4340
  });
3964
4341
  }
3965
- createBeforeActionStepContext(step) {
4342
+ createStepHookContext(step, operation, direction) {
3966
4343
  if (!step.target)
3967
4344
  throw new Error("Cannot create a step context without a target");
3968
4345
  return Object.freeze({
3969
- ...step.props.get(),
4346
+ direction,
4347
+ initialProps: step.initialProps,
4348
+ props: step.props,
4349
+ signal: this.signalFor(operation),
3970
4350
  target: step.target
3971
4351
  });
3972
4352
  }
@@ -3974,6 +4354,10 @@ class TourController {
3974
4354
  this.operationToken += 1;
3975
4355
  this.operation?.abort();
3976
4356
  this.operation = null;
4357
+ if (this.awaitingTargetOperation !== null) {
4358
+ this.awaitingTargetOperation = null;
4359
+ this.driver.setTargetPending?.(false);
4360
+ }
3977
4361
  }
3978
4362
  signalFor(operation) {
3979
4363
  this.assertCurrent(operation);
@@ -4009,12 +4393,10 @@ class TourController {
4009
4393
  return direction === "advance" || this.index > 0;
4010
4394
  }
4011
4395
  isAdvanceButtonAvailable() {
4012
- const props = this.currentStep()?.props.get();
4013
- return props !== undefined && props.popover?.disableAdvanceButton !== true;
4396
+ return isControlAvailable(this.currentStep()?.props.get(), "advance");
4014
4397
  }
4015
4398
  isPreviousButtonAvailable() {
4016
- const props = this.currentStep()?.props.get();
4017
- return props !== undefined && props.popover?.disablePreviousButton !== true && this.index > 0;
4399
+ return isControlAvailable(this.currentStep()?.props.get(), "previous") && this.index > 0;
4018
4400
  }
4019
4401
  isCancelAvailable() {
4020
4402
  return this.currentStep() !== null && this.canCancel();
@@ -4029,7 +4411,7 @@ class TourController {
4029
4411
  return this.currentStep() ? this.isCancelAvailable() : this.retainedPresentation?.canCancel ?? false;
4030
4412
  }
4031
4413
  canCancel() {
4032
- return (this.workflow?.options.cancellable ?? true) && this.status !== "finished" && this.status !== "cancelled" && this.status !== "error";
4414
+ return this.status !== "finished" && this.status !== "cancelled" && this.status !== "error";
4033
4415
  }
4034
4416
  setStatus(status) {
4035
4417
  this.status = status;
@@ -4087,6 +4469,13 @@ class TourController {
4087
4469
  this.reportSubscriberError(error);
4088
4470
  }
4089
4471
  }
4472
+ flushTourStart() {
4473
+ const step = this.pendingTourStart;
4474
+ if (step === undefined)
4475
+ return;
4476
+ this.pendingTourStart = undefined;
4477
+ this.emit("tour:start", step, 0);
4478
+ }
4090
4479
  emitStepLeave(step) {
4091
4480
  if (!step || !this.hasEventListeners())
4092
4481
  return;
@@ -4132,6 +4521,8 @@ class TourController {
4132
4521
  isFirstStep,
4133
4522
  isLastStep,
4134
4523
  status: this.status,
4524
+ awaitingTarget: this.awaitingTargetOperation !== null,
4525
+ popoverHidden: this.popoverHidden && this.isRunning(),
4135
4526
  error: this.error
4136
4527
  });
4137
4528
  }
@@ -4169,9 +4560,11 @@ function createGlowTour(options = {}) {
4169
4560
  cancel: () => controller.cancel(),
4170
4561
  create: (name, options2) => controller.create(name, options2),
4171
4562
  dispose: () => controller.dispose(),
4172
- goToStep: (index) => controller.goToStep(index),
4563
+ goTo: (id) => controller.goTo(id),
4564
+ hidePopover: () => controller.setPopoverHidden(true),
4173
4565
  previous: () => controller.previous(),
4174
- run: (workflow, runOptions) => controller.run(workflow, runOptions),
4566
+ showPopover: () => controller.setPopoverHidden(false),
4567
+ start: (workflow, runOptions) => controller.start(workflow, runOptions),
4175
4568
  state: controller.state
4176
4569
  };
4177
4570
  bridge = attachRootBridge(tour, driver, () => controller.isDisposed(), () => controller.beginMountRelease(), () => controller.completeMountRelease());