@mpgd/tutorial 0.1.0 → 0.1.2

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/dist/driver.js CHANGED
@@ -25,20 +25,42 @@ export function createDriverTutorialPresenter(input) {
25
25
  let destroyed = false;
26
26
  let reportedActive = false;
27
27
  let targetSemanticsGuard;
28
+ let targetShadowPointerStyle;
28
29
  let targetRestorePending = false;
30
+ let restartPreferredOnFrame = false;
31
+ let restartingInstance = false;
29
32
  let dismissedPresentationKey = null;
30
33
  let retryableDismissedKey = null;
31
34
  let missingTargetErrorKey = null;
32
35
  let syncModalSemantics;
36
+ let targetAcknowledgeBinding;
37
+ let targetClickState;
38
+ let targetHitTestFrame;
39
+ let targetGeometryTimer;
40
+ let targetOverlayPointerRestore;
33
41
  const pendingSkipOperations = new Set();
42
+ const reportError = (error) => {
43
+ try {
44
+ input.onError?.(error);
45
+ }
46
+ catch {
47
+ // Error reporting must not interrupt presenter lifecycle handling.
48
+ }
49
+ };
34
50
  const reportActive = (active) => {
35
51
  if (reportedActive === active) {
36
52
  return;
37
53
  }
38
54
  reportedActive = active;
39
- input.onActiveChange?.(active);
55
+ try {
56
+ input.onActiveChange?.(active);
57
+ }
58
+ catch (error) {
59
+ reportError(error);
60
+ }
40
61
  };
41
- const scheduleRefresh = () => {
62
+ const scheduleRefresh = (restartPreferred = false) => {
63
+ restartPreferredOnFrame ||= restartPreferred;
42
64
  if (destroyed || frame !== undefined || dismissedPresentationKey !== null) {
43
65
  return;
44
66
  }
@@ -47,14 +69,24 @@ export function createDriverTutorialPresenter(input) {
47
69
  if (destroyed || dismissedPresentationKey !== null) {
48
70
  return;
49
71
  }
72
+ const shouldRestartPreferred = restartPreferredOnFrame;
73
+ restartPreferredOnFrame = false;
50
74
  const currentInstance = instance;
51
75
  if (currentInstance === null || !currentInstance.isActive()) {
52
76
  driveActiveStep();
53
77
  return;
54
78
  }
55
- rebindActiveTarget(currentInstance, activePresentation);
79
+ const presentation = activePresentation;
80
+ if (shouldRestartPreferred
81
+ && presentation !== null
82
+ && shouldPreferActivatableTutorialTarget(presentation)) {
83
+ restartActiveInstance(currentInstance, presentation);
84
+ return;
85
+ }
86
+ rebindActiveTarget(currentInstance, presentation);
56
87
  });
57
88
  };
89
+ const scheduleHostRefresh = () => scheduleRefresh(true);
58
90
  const createInstance = (presentation) => {
59
91
  let created;
60
92
  let modalSemanticsGuard;
@@ -63,9 +95,16 @@ export function createDriverTutorialPresenter(input) {
63
95
  if (popover === null) {
64
96
  return;
65
97
  }
66
- const modalSelector = input.modalSelector ?? '[role="dialog"][aria-modal="true"]';
67
- const modal = resolveUnderlyingTutorialModal(popover, presentation.step.interaction, created.getActiveElement(), modalSelector, modalSemanticsGuard?.modal ?? null);
68
- if (modalSemanticsGuard?.modal === modal && modalSemanticsGuard.popover === popover) {
98
+ const modalSelector = input.modalSelector
99
+ ?? '[role="dialog"][aria-modal="true"], [role="alertdialog"][aria-modal="true"]';
100
+ const retainCurrentModal = modalSemanticsGuard
101
+ ?.isOwnedAttributeCurrent('aria-modal') ?? false;
102
+ const modal = resolveUnderlyingTutorialModal(popover, presentation.step.interaction, created.getActiveElement(), modalSelector, modalSemanticsGuard?.modal ?? null, retainCurrentModal);
103
+ if (modalSemanticsGuard?.modal === modal
104
+ && modalSemanticsGuard.popover === popover
105
+ && (presentation.step.interaction !== 'blocked'
106
+ || modal === null
107
+ || retainCurrentModal)) {
69
108
  return;
70
109
  }
71
110
  modalSemanticsGuard?.restore();
@@ -98,14 +137,21 @@ export function createDriverTutorialPresenter(input) {
98
137
  const dismissedKey = activePresentationKey;
99
138
  dismissedPresentationKey = dismissedKey;
100
139
  retryableDismissedKey = null;
140
+ const skipping = (() => {
141
+ try {
142
+ return Promise.resolve(input.onSkip());
143
+ }
144
+ catch (error) {
145
+ return Promise.reject(error);
146
+ }
147
+ })();
101
148
  destroyInstancePreservingTarget(created);
102
- const skipOperation = Promise.resolve()
103
- .then(() => input.onSkip())
149
+ const skipOperation = skipping
104
150
  .catch((error) => {
105
151
  if (dismissedPresentationKey === dismissedKey) {
106
152
  retryableDismissedKey = dismissedKey;
107
153
  }
108
- input.onError?.(error);
154
+ reportError(error);
109
155
  });
110
156
  pendingSkipOperations.add(skipOperation);
111
157
  void skipOperation
@@ -113,6 +159,8 @@ export function createDriverTutorialPresenter(input) {
113
159
  .catch(() => undefined);
114
160
  },
115
161
  onDestroyed: () => {
162
+ clearTargetAcknowledgeBinding(created);
163
+ clearTargetClickState(created);
116
164
  if (!targetRestorePending) {
117
165
  targetSemanticsGuard?.restore(true);
118
166
  }
@@ -124,11 +172,15 @@ export function createDriverTutorialPresenter(input) {
124
172
  syncModalSemantics = undefined;
125
173
  }
126
174
  if (instance === created) {
175
+ cancelTargetGeometryMonitor();
176
+ clearTargetGeometryObservers();
127
177
  instance = null;
128
- clearTutorialState(stateHost);
129
- reportActive(false);
178
+ if (!restartingInstance) {
179
+ clearTutorialState(stateHost);
180
+ reportActive(false);
181
+ }
130
182
  }
131
- if (!destroyed && activePresentation !== null) {
183
+ if (!destroyed && !restartingInstance && activePresentation !== null) {
132
184
  scheduleRefresh();
133
185
  }
134
186
  },
@@ -142,8 +194,16 @@ export function createDriverTutorialPresenter(input) {
142
194
  popover = wrapper;
143
195
  syncModalSemantics = syncCreatedModalSemantics;
144
196
  syncCreatedModalSemantics();
145
- if (presentation.step.advance.kind === 'acknowledge') {
146
- const initialFocus = created.getActiveStep()?.advanceOnClick === true
197
+ if (presentation.step.advance.kind === 'acknowledge'
198
+ || presentation.step.interaction === 'blocked') {
199
+ const activeTarget = created.getActiveElement();
200
+ const acknowledgeFromTarget = activeTarget instanceof HTMLElement
201
+ && targetClickState?.driver === created
202
+ && targetClickState.target === activeTarget
203
+ && targetClickState.stepId === presentation.step.id
204
+ && targetClickState.acknowledgeOnTargetClick;
205
+ const initialFocus = presentation.step.advance.kind !== 'acknowledge'
206
+ || acknowledgeFromTarget
147
207
  ? closeButton
148
208
  : nextButton;
149
209
  browserWindow.queueMicrotask(() => initialFocus.focus());
@@ -177,20 +237,30 @@ export function createDriverTutorialPresenter(input) {
177
237
  else {
178
238
  missingTargetErrorKey = null;
179
239
  }
240
+ const targetActivatable = target !== null
241
+ && isActivatableTutorialTarget(target, browserWindow);
242
+ const targetClickMode = resolveTutorialTargetClickMode(presentation, target ?? undefined, targetActivatable);
180
243
  const nextInstance = createInstance(presentation);
181
244
  instance = nextInstance;
245
+ setTargetClickState(nextInstance, presentation, target ?? undefined, targetClickMode);
182
246
  targetSemanticsGuard?.restore();
183
247
  targetSemanticsGuard = target === null
184
248
  ? undefined
185
249
  : preserveTutorialTargetSemantics(target, browserWindow);
186
250
  try {
187
- stateHost.setAttribute(tutorialDomAttributes.active, 'true');
188
- stateHost.setAttribute(tutorialDomAttributes.step, presentation.step.id);
189
251
  nextInstance.setSteps([
190
- createDriveStep(input, presentation, target ?? undefined, ownerDocument),
252
+ createDriveStep(input, presentation, target ?? undefined, targetClickMode.acknowledgeOnTargetClick, ownerDocument, reportError),
191
253
  ]);
192
- applyDriverTargetMutation(() => nextInstance.drive());
254
+ applyDriverTargetMutation(() => {
255
+ syncTargetShadowPointerStyle(presentation, target ?? undefined, targetActivatable);
256
+ stateHost.setAttribute(tutorialDomAttributes.active, 'true');
257
+ stateHost.setAttribute(tutorialDomAttributes.step, presentation.step.id);
258
+ nextInstance.drive();
259
+ });
260
+ downgradeInactiveTargetClickMode(nextInstance, presentation, target ?? undefined, targetClickMode);
193
261
  syncModalSemantics?.();
262
+ syncTargetGeometryObservers(presentation);
263
+ syncTargetGeometryMonitor(nextInstance, presentation);
194
264
  reportActive(true);
195
265
  }
196
266
  catch (error) {
@@ -205,20 +275,26 @@ export function createDriverTutorialPresenter(input) {
205
275
  clearTutorialState(stateHost);
206
276
  reportActive(false);
207
277
  }
208
- input.onError?.(error);
278
+ reportError(error);
209
279
  }
210
280
  };
211
281
  const resolvePresentationTarget = (presentation, presentationKey = activePresentationKey) => {
212
282
  if (presentation.step.target === null) {
213
283
  return null;
214
284
  }
285
+ const presentationTarget = presentation.step.target;
215
286
  try {
216
287
  if (input.resolveTarget !== undefined) {
217
- return input.resolveTarget(presentation.step.target, root);
288
+ const target = input.resolveTarget(presentationTarget, root);
289
+ if (target !== null && target.ownerDocument !== ownerDocument) {
290
+ throw new Error('Tutorial target must belong to the presenter document.');
291
+ }
292
+ return target;
218
293
  }
219
294
  return resolveVisibleTutorialTarget({
295
+ preferActivatable: shouldPreferActivatableTutorialTarget(presentation),
220
296
  root,
221
- target: presentation.step.target,
297
+ target: presentationTarget,
222
298
  targetAttribute,
223
299
  view: browserWindow,
224
300
  });
@@ -228,35 +304,409 @@ export function createDriverTutorialPresenter(input) {
228
304
  && presentationKey === activePresentationKey
229
305
  && missingTargetErrorKey !== presentationKey) {
230
306
  missingTargetErrorKey = presentationKey;
231
- input.onError?.(error);
307
+ reportError(error);
232
308
  }
233
309
  return null;
234
310
  }
235
311
  };
236
- const observationRoot = root.nodeType === 9 ? ownerDocument.documentElement : root;
237
- const mutationObserver = new browserWindow.MutationObserver((records) => {
312
+ const resizeObservationRoot = root.nodeType === 9
313
+ ? ownerDocument.documentElement
314
+ : root;
315
+ const mutationOptions = {
316
+ attributes: true,
317
+ characterData: true,
318
+ childList: true,
319
+ subtree: true,
320
+ };
321
+ const mutationObserver = new browserWindow.MutationObserver(processMutationRecords);
322
+ let observedShadowRoots = new Set();
323
+ let observingDocumentMutations = false;
324
+ let resizeObserver = null;
325
+ let intersectionObserver = null;
326
+ const observedGeometryTargets = new Set();
327
+ const targetVisibility = new Map();
328
+ function processMutationRecords(records) {
238
329
  if (activePresentation === null && instance === null) {
330
+ observeDocumentOnlyMutations();
239
331
  return;
240
332
  }
241
- if (records.some((record) => !isDriverOwnedMutation(record))) {
242
- scheduleRefresh();
333
+ reconcileMutationRoots();
334
+ const hostRecords = records.filter((record) => !isDriverOwnedMutation(record));
335
+ if (hostRecords.length > 0) {
336
+ scheduleRefresh(hostRecords.some(isTargetSelectionMutation));
243
337
  }
244
- });
245
- mutationObserver.observe(observationRoot, {
246
- attributes: true,
247
- childList: true,
248
- subtree: true,
249
- });
250
- const resizeObserver = typeof browserWindow.ResizeObserver === 'function'
251
- ? new browserWindow.ResizeObserver(scheduleRefresh)
338
+ }
339
+ function isTargetSelectionMutation(record) {
340
+ const presentation = activePresentation;
341
+ if (presentation === null
342
+ || presentation.step.target === null
343
+ || !shouldPreferActivatableTutorialTarget(presentation)) {
344
+ return false;
345
+ }
346
+ const activeElement = instance?.getActiveElement();
347
+ const currentTarget = activeElement instanceof HTMLElement
348
+ && activeElement.id !== 'driver-dummy-element'
349
+ ? activeElement
350
+ : null;
351
+ if (record.type === 'characterData') {
352
+ return record.target.parentElement instanceof HTMLStyleElement
353
+ && stylesheetMutationAffectsLookupRoot(record.target);
354
+ }
355
+ if (record.type === 'attributes') {
356
+ if (isStylesheetAttributeMutation(record)
357
+ && stylesheetMutationAffectsLookupRoot(record.target)) {
358
+ return true;
359
+ }
360
+ if (!(record.target instanceof Element)) {
361
+ return false;
362
+ }
363
+ const mutationTarget = record.target;
364
+ return collectTargetSelectionElements(presentation, currentTarget)
365
+ .some((target) => attributeMutationMayAffectTarget(target, mutationTarget));
366
+ }
367
+ if (record.type !== 'childList') {
368
+ return false;
369
+ }
370
+ const changedNodes = [...record.addedNodes, ...record.removedNodes];
371
+ const touchesLookupRoot = childListTouchesLookupRoot(record, changedNodes);
372
+ if (isStylesheetDomMutation(record, changedNodes)
373
+ && stylesheetMutationAffectsLookupRoot(record.target)) {
374
+ return true;
375
+ }
376
+ if (!touchesLookupRoot) {
377
+ return false;
378
+ }
379
+ if (currentTarget !== null && !currentTarget.isConnected) {
380
+ return true;
381
+ }
382
+ if (currentTarget !== null && changedNodes.some((node) => node instanceof Element && isComposedAncestorOrSelf(currentTarget, node))) {
383
+ return true;
384
+ }
385
+ return input.resolveTarget === undefined && changedNodes.some((node) => mutationNodeMayContainTarget(node, presentation.step.target ?? ''));
386
+ }
387
+ function childListTouchesLookupRoot(record, changedNodes) {
388
+ if (root.nodeType === 9 || isNodeWithinLookupRoot(record.target)) {
389
+ return true;
390
+ }
391
+ return changedNodes.some((node) => {
392
+ if (node === root) {
393
+ return true;
394
+ }
395
+ return node instanceof Element && isComposedAncestorOrSelf(root, node);
396
+ });
397
+ }
398
+ function isNodeWithinLookupRoot(node) {
399
+ if (root.nodeType === 9) {
400
+ return true;
401
+ }
402
+ const element = node instanceof ShadowRoot
403
+ ? node.host
404
+ : node instanceof Element
405
+ ? node
406
+ : node.parentElement;
407
+ return element !== null && isComposedAncestorOrSelf(element, root);
408
+ }
409
+ function isStylesheetDomMutation(record, changedNodes) {
410
+ return isStylesheetElement(record.target)
411
+ || changedNodes.some(nodeContainsStylesheetElement);
412
+ }
413
+ function isStylesheetAttributeMutation(record) {
414
+ if (record.target instanceof HTMLStyleElement) {
415
+ return true;
416
+ }
417
+ return record.target instanceof HTMLLinkElement
418
+ && ['disabled', 'href', 'media', 'rel'].includes(record.attributeName ?? '');
419
+ }
420
+ function stylesheetMutationAffectsLookupRoot(node) {
421
+ const treeScope = node.getRootNode();
422
+ if (treeScope === ownerDocument) {
423
+ return true;
424
+ }
425
+ const presentation = activePresentation;
426
+ if (presentation === null) {
427
+ return false;
428
+ }
429
+ return collectTargetSelectionElements(presentation)
430
+ .some((target) => composedElementUsesTreeScope(target, treeScope));
431
+ }
432
+ function collectTargetSelectionElements(presentation, currentTarget) {
433
+ const targets = new Set();
434
+ const activeTarget = currentTarget ?? instance?.getActiveElement();
435
+ if (activeTarget instanceof HTMLElement && activeTarget.id !== 'driver-dummy-element') {
436
+ targets.add(activeTarget);
437
+ }
438
+ if (input.resolveTarget === undefined && presentation.step.target !== null) {
439
+ for (const candidate of findComposedTutorialTargets(root, targetAttribute, presentation.step.target)) {
440
+ targets.add(candidate);
441
+ }
442
+ }
443
+ return [...targets];
444
+ }
445
+ function attributeMutationMayAffectTarget(target, mutationTarget) {
446
+ if (isComposedAncestorOrSelf(target, mutationTarget)) {
447
+ return true;
448
+ }
449
+ if (target.getRootNode() !== mutationTarget.getRootNode()) {
450
+ return false;
451
+ }
452
+ if (target.getRootNode() instanceof ShadowRoot) {
453
+ return true;
454
+ }
455
+ if (root instanceof HTMLElement && isNodeWithinLookupRoot(mutationTarget)) {
456
+ return true;
457
+ }
458
+ if (target.contains(mutationTarget)
459
+ || target.parentElement !== null
460
+ && mutationTarget.parentElement === target.parentElement) {
461
+ return true;
462
+ }
463
+ return composedAncestorBranchesAreSiblings(target, mutationTarget);
464
+ }
465
+ function composedAncestorBranchesAreSiblings(target, mutationTarget) {
466
+ const targetAncestors = collectComposedAncestors(target);
467
+ let mutationAncestor = mutationTarget;
468
+ while (mutationAncestor !== null) {
469
+ const mutationParent = getComposedParentElement(mutationAncestor);
470
+ if (mutationParent !== null
471
+ && mutationParent !== ownerDocument.body
472
+ && mutationParent !== ownerDocument.documentElement
473
+ && targetAncestors.some((targetAncestor) => getComposedParentElement(targetAncestor) === mutationParent)) {
474
+ return true;
475
+ }
476
+ mutationAncestor = mutationParent;
477
+ }
478
+ return false;
479
+ }
480
+ function collectComposedAncestors(element) {
481
+ const ancestors = [];
482
+ let current = element;
483
+ while (current !== null) {
484
+ ancestors.push(current);
485
+ current = getComposedParentElement(current);
486
+ }
487
+ return ancestors;
488
+ }
489
+ function nodeContainsStylesheetElement(node) {
490
+ return isStylesheetElement(node)
491
+ || [...node.childNodes].some(nodeContainsStylesheetElement);
492
+ }
493
+ function isStylesheetElement(node) {
494
+ if (node instanceof HTMLStyleElement) {
495
+ return true;
496
+ }
497
+ return node instanceof HTMLLinkElement
498
+ && node.rel.toLowerCase().split(/\s+/u).includes('stylesheet');
499
+ }
500
+ function mutationNodeMayContainTarget(node, target) {
501
+ if (node instanceof HTMLElement
502
+ && (node.getAttribute(targetAttribute) === target || node.shadowRoot !== null)) {
503
+ return true;
504
+ }
505
+ return [...node.childNodes].some((child) => mutationNodeMayContainTarget(child, target));
506
+ }
507
+ function reconcileMutationRoots(force = false) {
508
+ if (destroyed) {
509
+ return;
510
+ }
511
+ if (activePresentation === null && instance === null) {
512
+ observeDocumentOnlyMutations(force);
513
+ return;
514
+ }
515
+ const nextShadowRoots = new Set(findOpenTutorialShadowRoots(ownerDocument));
516
+ const rootsChanged = nextShadowRoots.size !== observedShadowRoots.size
517
+ || [...nextShadowRoots].some((shadowRoot) => !observedShadowRoots.has(shadowRoot));
518
+ if (!force && observingDocumentMutations && !rootsChanged) {
519
+ return;
520
+ }
521
+ mutationObserver.disconnect();
522
+ mutationObserver.observe(ownerDocument.documentElement, mutationOptions);
523
+ observingDocumentMutations = true;
524
+ for (const shadowRoot of nextShadowRoots) {
525
+ mutationObserver.observe(shadowRoot, mutationOptions);
526
+ }
527
+ observedShadowRoots = nextShadowRoots;
528
+ }
529
+ function observeDocumentOnlyMutations(force = false) {
530
+ if (!force && observingDocumentMutations && observedShadowRoots.size === 0) {
531
+ return;
532
+ }
533
+ mutationObserver.disconnect();
534
+ mutationObserver.observe(ownerDocument.documentElement, mutationOptions);
535
+ observingDocumentMutations = true;
536
+ observedShadowRoots.clear();
537
+ }
538
+ function syncTargetGeometryObservers(presentation) {
539
+ const nextTargets = shouldPreferActivatableTutorialTarget(presentation)
540
+ ? new Set(collectTargetSelectionElements(presentation))
541
+ : new Set();
542
+ for (const target of observedGeometryTargets) {
543
+ if (nextTargets.has(target)) {
544
+ continue;
545
+ }
546
+ intersectionObserver?.unobserve(target);
547
+ if (target !== resizeObservationRoot) {
548
+ resizeObserver?.unobserve(target);
549
+ }
550
+ observedGeometryTargets.delete(target);
551
+ targetVisibility.delete(target);
552
+ }
553
+ for (const target of nextTargets) {
554
+ if (observedGeometryTargets.has(target)) {
555
+ continue;
556
+ }
557
+ observedGeometryTargets.add(target);
558
+ targetVisibility.set(target, isVisibleTutorialTarget(target, browserWindow));
559
+ resizeObserver?.observe(target);
560
+ intersectionObserver?.observe(target);
561
+ }
562
+ }
563
+ function clearTargetGeometryObservers() {
564
+ for (const target of observedGeometryTargets) {
565
+ intersectionObserver?.unobserve(target);
566
+ if (target !== resizeObservationRoot) {
567
+ resizeObserver?.unobserve(target);
568
+ }
569
+ }
570
+ intersectionObserver?.takeRecords();
571
+ observedGeometryTargets.clear();
572
+ targetVisibility.clear();
573
+ }
574
+ function syncTargetGeometryMonitor(currentInstance, presentation) {
575
+ cancelTargetGeometryMonitor();
576
+ if (!shouldPreferActivatableTutorialTarget(presentation)) {
577
+ return;
578
+ }
579
+ const target = currentInstance.getActiveElement();
580
+ if (!(target instanceof HTMLElement) || target.id === 'driver-dummy-element') {
581
+ return;
582
+ }
583
+ const presentationKey = activePresentationKey;
584
+ let previousRect = readTutorialTargetRect(target);
585
+ let previousVisible = isVisibleTutorialTargetWithRect(target, browserWindow, previousRect);
586
+ let previousActivatable = isActivatableTutorialTarget(target, browserWindow);
587
+ const monitor = () => {
588
+ targetGeometryTimer = undefined;
589
+ const currentPresentation = activePresentation;
590
+ if (destroyed
591
+ || instance !== currentInstance
592
+ || !currentInstance.isActive()
593
+ || currentPresentation === null
594
+ || activePresentationKey !== presentationKey
595
+ || !shouldPreferActivatableTutorialTarget(currentPresentation)
596
+ || currentInstance.getActiveElement() !== target) {
597
+ return;
598
+ }
599
+ const nextRect = readTutorialTargetRect(target);
600
+ const rectChanged = !tutorialTargetRectsEqual(previousRect, nextRect);
601
+ const selectionInvalid = !target.isConnected
602
+ || !isNodeWithinLookupRoot(target)
603
+ || (input.resolveTarget === undefined
604
+ && target.getAttribute(targetAttribute) !== currentPresentation.step.target);
605
+ const nextVisible = !selectionInvalid
606
+ && isVisibleTutorialTargetWithRect(target, browserWindow, nextRect);
607
+ const visibilityChanged = nextVisible !== previousVisible;
608
+ const nextActivatable = isActivatableTutorialTarget(target, browserWindow);
609
+ const activatableChanged = nextActivatable !== previousActivatable;
610
+ if (rectChanged || selectionInvalid || visibilityChanged || activatableChanged) {
611
+ if (selectionInvalid || visibilityChanged) {
612
+ scheduleHostRefresh();
613
+ }
614
+ else if (activatableChanged) {
615
+ scheduleRefresh();
616
+ }
617
+ else {
618
+ scheduleRefresh();
619
+ }
620
+ previousRect = nextRect;
621
+ previousVisible = nextVisible;
622
+ previousActivatable = nextActivatable;
623
+ }
624
+ targetGeometryTimer = browserWindow.setTimeout(monitor, 250);
625
+ };
626
+ targetGeometryTimer = browserWindow.setTimeout(monitor, 250);
627
+ }
628
+ function cancelTargetGeometryMonitor() {
629
+ if (targetGeometryTimer === undefined) {
630
+ return;
631
+ }
632
+ browserWindow.clearTimeout(targetGeometryTimer);
633
+ targetGeometryTimer = undefined;
634
+ }
635
+ function recordTargetVisibilityChange(target) {
636
+ const previous = targetVisibility.get(target);
637
+ const next = isVisibleTutorialTarget(target, browserWindow);
638
+ targetVisibility.set(target, next);
639
+ return previous !== undefined && previous !== next;
640
+ }
641
+ function processIntersectionRecords(entries) {
642
+ const visibilityChanges = entries.map((entry) => (entry.target instanceof HTMLElement
643
+ && observedGeometryTargets.has(entry.target)
644
+ && recordTargetVisibilityChange(entry.target)));
645
+ if (visibilityChanges.some(Boolean) && preferredTargetIdentityChanged()) {
646
+ scheduleHostRefresh();
647
+ }
648
+ }
649
+ function processResizeRecords(entries) {
650
+ const visibilityChanges = entries.map((entry) => (entry.target instanceof HTMLElement
651
+ && observedGeometryTargets.has(entry.target)
652
+ && recordTargetVisibilityChange(entry.target)));
653
+ if (visibilityChanges.some(Boolean) && preferredTargetIdentityChanged()) {
654
+ scheduleHostRefresh();
655
+ return;
656
+ }
657
+ if (entries.some((entry) => entry.target === resizeObservationRoot)) {
658
+ scheduleViewportRefresh();
659
+ }
660
+ }
661
+ function preferredTargetIdentityChanged() {
662
+ const presentation = activePresentation;
663
+ const currentTarget = instance?.getActiveElement();
664
+ if (presentation === null
665
+ || presentation.step.target === null
666
+ || input.resolveTarget !== undefined
667
+ || !(currentTarget instanceof HTMLElement)) {
668
+ return true;
669
+ }
670
+ const currentVisible = targetVisibility.get(currentTarget)
671
+ ?? isVisibleTutorialTarget(currentTarget, browserWindow);
672
+ if (currentVisible && isStructurallyActivatableTutorialTarget(currentTarget)) {
673
+ return false;
674
+ }
675
+ const candidates = findComposedTutorialTargets(root, targetAttribute, presentation.step.target);
676
+ const nextTarget = candidates.find((candidate) => (isStructurallyActivatableTutorialTarget(candidate)
677
+ && (targetVisibility.get(candidate)
678
+ ?? isVisibleTutorialTarget(candidate, browserWindow))))
679
+ ?? candidates.find((candidate) => (targetVisibility.get(candidate) ?? isVisibleTutorialTarget(candidate, browserWindow)))
680
+ ?? currentTarget;
681
+ return nextTarget !== currentTarget;
682
+ }
683
+ function scheduleViewportRefresh() {
684
+ const presentation = activePresentation;
685
+ const activeElement = instance?.getActiveElement();
686
+ const restartPreferred = presentation !== null
687
+ && shouldPreferActivatableTutorialTarget(presentation)
688
+ && (!(activeElement instanceof HTMLElement)
689
+ || !isVisibleTutorialTarget(activeElement, browserWindow));
690
+ scheduleRefresh(restartPreferred);
691
+ }
692
+ reconcileMutationRoots(true);
693
+ resizeObserver = typeof browserWindow.ResizeObserver === 'function'
694
+ ? new browserWindow.ResizeObserver(processResizeRecords)
695
+ : null;
696
+ intersectionObserver = typeof browserWindow.IntersectionObserver === 'function'
697
+ ? new browserWindow.IntersectionObserver(processIntersectionRecords, { threshold: 0 })
252
698
  : null;
253
- resizeObserver?.observe(observationRoot);
254
- observationRoot.addEventListener('scroll', scheduleRefresh, true);
255
- browserWindow.visualViewport?.addEventListener('resize', scheduleRefresh);
256
- browserWindow.visualViewport?.addEventListener('scroll', scheduleRefresh);
257
- browserWindow.addEventListener('orientationchange', scheduleRefresh);
699
+ resizeObserver?.observe(resizeObservationRoot);
700
+ ownerDocument.addEventListener('scroll', scheduleViewportRefresh, true);
701
+ browserWindow.addEventListener('resize', scheduleViewportRefresh);
702
+ browserWindow.visualViewport?.addEventListener('resize', scheduleViewportRefresh);
703
+ browserWindow.visualViewport?.addEventListener('scroll', scheduleViewportRefresh);
704
+ browserWindow.addEventListener('orientationchange', scheduleViewportRefresh);
258
705
  return {
259
706
  destroy() {
707
+ if (destroyed) {
708
+ return;
709
+ }
260
710
  destroyed = true;
261
711
  activePresentation = null;
262
712
  activePresentationKey = null;
@@ -264,11 +714,17 @@ export function createDriverTutorialPresenter(input) {
264
714
  retryableDismissedKey = null;
265
715
  missingTargetErrorKey = null;
266
716
  mutationObserver.disconnect();
717
+ observingDocumentMutations = false;
718
+ observedShadowRoots.clear();
719
+ cancelTargetGeometryMonitor();
720
+ clearTargetGeometryObservers();
267
721
  resizeObserver?.disconnect();
268
- observationRoot.removeEventListener('scroll', scheduleRefresh, true);
269
- browserWindow.visualViewport?.removeEventListener('resize', scheduleRefresh);
270
- browserWindow.visualViewport?.removeEventListener('scroll', scheduleRefresh);
271
- browserWindow.removeEventListener('orientationchange', scheduleRefresh);
722
+ intersectionObserver?.disconnect();
723
+ ownerDocument.removeEventListener('scroll', scheduleViewportRefresh, true);
724
+ browserWindow.removeEventListener('resize', scheduleViewportRefresh);
725
+ browserWindow.visualViewport?.removeEventListener('resize', scheduleViewportRefresh);
726
+ browserWindow.visualViewport?.removeEventListener('scroll', scheduleViewportRefresh);
727
+ browserWindow.removeEventListener('orientationchange', scheduleViewportRefresh);
272
728
  if (frame !== undefined) {
273
729
  browserWindow.cancelAnimationFrame(frame);
274
730
  }
@@ -277,14 +733,26 @@ export function createDriverTutorialPresenter(input) {
277
733
  }
278
734
  instance = null;
279
735
  targetSemanticsGuard = undefined;
736
+ targetShadowPointerStyle?.remove();
737
+ targetShadowPointerStyle = undefined;
738
+ clearTargetAcknowledgeBinding();
739
+ clearTargetClickState();
280
740
  clearTutorialState(stateHost);
281
741
  reportActive(false);
282
742
  },
283
743
  present(presentation) {
744
+ if (destroyed) {
745
+ return;
746
+ }
284
747
  const nextKey = presentation === null
285
748
  ? null
286
749
  : JSON.stringify([
287
750
  presentation.step.id,
751
+ presentation.step.target,
752
+ presentation.step.interaction,
753
+ presentation.step.advance,
754
+ presentation.step.side ?? null,
755
+ presentation.step.align ?? null,
288
756
  presentation.copy.title,
289
757
  presentation.copy.description,
290
758
  presentation.copy.next,
@@ -305,6 +773,7 @@ export function createDriverTutorialPresenter(input) {
305
773
  }
306
774
  if (activePresentationKey === nextKey) {
307
775
  activePresentation = presentation;
776
+ reconcileMutationRoots(true);
308
777
  scheduleRefresh();
309
778
  return;
310
779
  }
@@ -315,11 +784,16 @@ export function createDriverTutorialPresenter(input) {
315
784
  if (previousInstance !== null) {
316
785
  destroyInstancePreservingTarget(previousInstance);
317
786
  }
787
+ reconcileMutationRoots(true);
318
788
  clearTutorialState(stateHost);
319
789
  reportActive(false);
320
790
  scheduleRefresh();
321
791
  },
322
- refresh: scheduleRefresh,
792
+ refresh() {
793
+ // attachShadow() itself emits no mutation, so explicit refresh also discovers new roots.
794
+ reconcileMutationRoots();
795
+ scheduleHostRefresh();
796
+ },
323
797
  resetForReplay() {
324
798
  dismissedPresentationKey = null;
325
799
  retryableDismissedKey = null;
@@ -332,7 +806,51 @@ export function createDriverTutorialPresenter(input) {
332
806
  }
333
807
  },
334
808
  };
809
+ function restartActiveInstance(currentInstance, presentation) {
810
+ const presentationKey = activePresentationKey;
811
+ restartingInstance = true;
812
+ try {
813
+ applyDriverTargetMutation(() => destroyInstancePreservingTarget(currentInstance));
814
+ }
815
+ finally {
816
+ restartingInstance = false;
817
+ }
818
+ if (!destroyed
819
+ && dismissedPresentationKey === null
820
+ && activePresentation === presentation
821
+ && activePresentationKey === presentationKey) {
822
+ driveActiveStep();
823
+ }
824
+ if (instance === null || !instance.isActive()) {
825
+ clearTutorialState(stateHost);
826
+ reportActive(false);
827
+ }
828
+ }
335
829
  function rebindActiveTarget(currentInstance, presentation) {
830
+ if (presentation !== null && shouldPreferActivatableTutorialTarget(presentation)) {
831
+ if (input.resolveTarget !== undefined && presentation.step.target !== null) {
832
+ const presentationKey = activePresentationKey;
833
+ const target = resolvePresentationTarget(presentation, presentationKey);
834
+ if (destroyed
835
+ || activePresentation !== presentation
836
+ || activePresentationKey !== presentationKey
837
+ || instance !== currentInstance
838
+ || !currentInstance.isActive()) {
839
+ return;
840
+ }
841
+ const activeElement = currentInstance.getActiveElement();
842
+ const targetUnchanged = target === activeElement
843
+ || (target === null && activeElement?.id === 'driver-dummy-element');
844
+ if (!targetUnchanged) {
845
+ restartActiveInstance(currentInstance, presentation);
846
+ return;
847
+ }
848
+ }
849
+ applyDriverTargetMutation(() => currentInstance.refresh());
850
+ syncModalSemantics?.();
851
+ syncPreferredTargetInteraction(currentInstance, presentation);
852
+ return;
853
+ }
336
854
  if (presentation === null || presentation.step.target === null || !currentInstance.isActive()) {
337
855
  applyDriverTargetMutation(() => currentInstance.refresh());
338
856
  syncModalSemantics?.();
@@ -388,18 +906,213 @@ export function createDriverTutorialPresenter(input) {
388
906
  function rebindTarget(currentInstance, presentation, target) {
389
907
  const previousTarget = currentInstance.getActiveElement();
390
908
  targetSemanticsGuard?.restore();
391
- const restorePreviousTarget = !(previousTarget instanceof HTMLElement)
909
+ const restorePreviousTarget = previousTarget === target
910
+ || !(previousTarget instanceof HTMLElement)
392
911
  || previousTarget.id === 'driver-dummy-element'
393
912
  ? undefined
394
913
  : captureTutorialTargetSemantics(previousTarget, target?.parentElement ?? null);
395
914
  targetSemanticsGuard = target === undefined
396
915
  ? undefined
397
916
  : preserveTutorialTargetSemantics(target, browserWindow);
398
- const nextStep = createDriveStep(input, presentation, target, ownerDocument);
399
- applyDriverTargetMutation(() => currentInstance.highlight(nextStep));
917
+ const targetActivatable = target !== undefined
918
+ && isActivatableTutorialTarget(target, browserWindow);
919
+ const targetClickMode = resolveTutorialTargetClickMode(presentation, target, targetActivatable);
920
+ applyTargetClickMode(currentInstance, presentation, target, targetClickMode, targetActivatable);
921
+ downgradeInactiveTargetClickMode(currentInstance, presentation, target, targetClickMode);
400
922
  restorePreviousTarget?.();
401
923
  syncModalSemantics?.();
402
924
  }
925
+ function syncPreferredTargetInteraction(currentInstance, presentation) {
926
+ const target = currentInstance.getActiveElement();
927
+ if (!(target instanceof HTMLElement) || target.id === 'driver-dummy-element') {
928
+ return;
929
+ }
930
+ const targetActivatable = isActivatableTutorialTarget(target, browserWindow);
931
+ const desiredMode = resolveTutorialTargetClickMode(presentation, target, targetActivatable);
932
+ if (targetClickState?.acknowledgeOnTargetClick !== desiredMode.acknowledgeOnTargetClick) {
933
+ applyTargetClickMode(currentInstance, presentation, target, desiredMode, targetActivatable);
934
+ }
935
+ downgradeInactiveTargetClickMode(currentInstance, presentation, target, desiredMode);
936
+ }
937
+ function applyTargetClickMode(currentInstance, presentation, target, mode, targetActivatable) {
938
+ clearTargetOverlayPointerGuard();
939
+ const nextStep = createDriveStep(input, presentation, target, mode.acknowledgeOnTargetClick, ownerDocument, reportError);
940
+ clearTargetAcknowledgeBinding(currentInstance);
941
+ setTargetClickState(currentInstance, presentation, target, mode);
942
+ applyDriverTargetMutation(() => {
943
+ syncTargetShadowPointerStyle(presentation, target, targetActivatable);
944
+ currentInstance.highlight(nextStep);
945
+ });
946
+ }
947
+ function downgradeInactiveTargetClickMode(currentInstance, presentation, target, mode) {
948
+ const shadowTargetHitTestable = target === undefined
949
+ || isShadowTutorialTargetHitTestable(target);
950
+ if (mode.acknowledgeOnTargetClick
951
+ && target !== undefined
952
+ && (!isActivatableTutorialTarget(target, browserWindow)
953
+ || !shadowTargetHitTestable)) {
954
+ applyTargetClickMode(currentInstance, presentation, target, { acknowledgeOnTargetClick: false }, true);
955
+ }
956
+ else if (target !== undefined && shouldAllowTutorialTargetInteraction(presentation)) {
957
+ if (shadowTargetHitTestable) {
958
+ clearTargetOverlayPointerGuard();
959
+ scheduleShadowTargetHitTest(currentInstance, presentation, target);
960
+ }
961
+ else {
962
+ allowShadowTargetThroughOverlay(target);
963
+ }
964
+ }
965
+ syncTargetAcknowledgeBinding(currentInstance, presentation);
966
+ }
967
+ function isShadowTutorialTargetHitTestable(target) {
968
+ if (typeof ownerDocument.elementFromPoint !== 'function') {
969
+ return true;
970
+ }
971
+ const shadowRoots = [];
972
+ let current = target;
973
+ while (current !== null) {
974
+ const treeScope = current.getRootNode();
975
+ if (!(treeScope instanceof ShadowRoot)) {
976
+ break;
977
+ }
978
+ shadowRoots.unshift(treeScope);
979
+ current = treeScope.host;
980
+ }
981
+ const visibleRect = resolveTutorialTargetVisibleRect(target, browserWindow);
982
+ if (visibleRect === null) {
983
+ return false;
984
+ }
985
+ const x = (visibleRect.left + visibleRect.right) / 2;
986
+ const y = (visibleRect.top + visibleRect.bottom) / 2;
987
+ let hit = ownerDocument.elementFromPoint(x, y);
988
+ const outerHost = shadowRoots[0]?.host;
989
+ if (outerHost === undefined) {
990
+ return hit instanceof Element && isComposedAncestorOrSelf(hit, target);
991
+ }
992
+ if (!(hit instanceof Element)
993
+ || !isComposedAncestorOrSelf(hit, outerHost)) {
994
+ return false;
995
+ }
996
+ for (const [index, shadowRoot] of shadowRoots.entries()) {
997
+ if (typeof shadowRoot.elementFromPoint !== 'function') {
998
+ return false;
999
+ }
1000
+ hit = shadowRoot.elementFromPoint(x, y);
1001
+ const expectedTarget = shadowRoots[index + 1]?.host ?? target;
1002
+ if (!(hit instanceof Element)
1003
+ || !isComposedAncestorOrSelf(hit, expectedTarget)) {
1004
+ return false;
1005
+ }
1006
+ }
1007
+ return true;
1008
+ }
1009
+ function scheduleShadowTargetHitTest(currentInstance, presentation, target) {
1010
+ if (typeof ownerDocument.elementFromPoint !== 'function') {
1011
+ return;
1012
+ }
1013
+ cancelShadowTargetHitTest();
1014
+ const state = targetClickState;
1015
+ const presentationKey = activePresentationKey;
1016
+ targetHitTestFrame = browserWindow.requestAnimationFrame(() => {
1017
+ targetHitTestFrame = undefined;
1018
+ if (destroyed
1019
+ || instance !== currentInstance
1020
+ || !currentInstance.isActive()
1021
+ || activePresentation !== presentation
1022
+ || activePresentationKey !== presentationKey
1023
+ || targetClickState?.driver !== currentInstance
1024
+ || targetClickState.target !== target
1025
+ || targetClickState.stepId !== presentation.step.id
1026
+ || isShadowTutorialTargetHitTestable(target)) {
1027
+ return;
1028
+ }
1029
+ if (state?.acknowledgeOnTargetClick === true) {
1030
+ applyTargetClickMode(currentInstance, presentation, target, { acknowledgeOnTargetClick: false }, true);
1031
+ syncTargetAcknowledgeBinding(currentInstance, presentation);
1032
+ syncModalSemantics?.();
1033
+ }
1034
+ else {
1035
+ allowShadowTargetThroughOverlay(target);
1036
+ }
1037
+ });
1038
+ }
1039
+ function allowShadowTargetThroughOverlay(target) {
1040
+ clearTargetOverlayPointerGuard();
1041
+ const overlay = ownerDocument.querySelector('.driver-overlay');
1042
+ const rect = resolveTutorialTargetVisibleRect(target, browserWindow);
1043
+ if (overlay === null || rect === null) {
1044
+ return;
1045
+ }
1046
+ const x = (rect.left + rect.right) / 2;
1047
+ const y = (rect.top + rect.bottom) / 2;
1048
+ const blockingHit = ownerDocument.elementFromPoint(x, y);
1049
+ if (!(blockingHit instanceof Element)
1050
+ || (blockingHit !== overlay && !overlay.contains(blockingHit))) {
1051
+ return;
1052
+ }
1053
+ const previousValue = overlay.style.getPropertyValue('pointer-events');
1054
+ const previousPriority = overlay.style.getPropertyPriority('pointer-events');
1055
+ const overlayZIndex = browserWindow.getComputedStyle(overlay).zIndex;
1056
+ overlay.style.setProperty('pointer-events', 'none', 'important');
1057
+ if (!isShadowTutorialTargetHitTestable(target)) {
1058
+ if (previousValue === '') {
1059
+ overlay.style.removeProperty('pointer-events');
1060
+ }
1061
+ else {
1062
+ overlay.style.setProperty('pointer-events', previousValue, previousPriority);
1063
+ }
1064
+ return;
1065
+ }
1066
+ const viewport = resolveViewportRect(browserWindow);
1067
+ const regions = [
1068
+ { bottom: rect.top, left: viewport.left, right: viewport.right, top: viewport.top },
1069
+ { bottom: viewport.bottom, left: viewport.left, right: viewport.right, top: rect.bottom },
1070
+ { bottom: rect.bottom, left: viewport.left, right: rect.left, top: rect.top },
1071
+ { bottom: rect.bottom, left: rect.right, right: viewport.right, top: rect.top },
1072
+ ];
1073
+ const blockers = regions.filter((region) => (region.right > region.left && region.bottom > region.top)).map((region) => {
1074
+ const blocker = ownerDocument.createElement('div');
1075
+ blocker.dataset.mpgdTutorialOutsideBlocker = 'true';
1076
+ blocker.setAttribute('aria-hidden', 'true');
1077
+ Object.assign(blocker.style, {
1078
+ background: 'transparent',
1079
+ height: `${region.bottom - region.top}px`,
1080
+ left: `${region.left}px`,
1081
+ pointerEvents: 'auto',
1082
+ position: 'fixed',
1083
+ top: `${region.top}px`,
1084
+ width: `${region.right - region.left}px`,
1085
+ zIndex: overlayZIndex,
1086
+ });
1087
+ ownerDocument.body.appendChild(blocker);
1088
+ return blocker;
1089
+ });
1090
+ targetOverlayPointerRestore = () => {
1091
+ for (const blocker of blockers) {
1092
+ blocker.remove();
1093
+ }
1094
+ if (overlay.style.getPropertyValue('pointer-events') === 'none'
1095
+ && overlay.style.getPropertyPriority('pointer-events') === 'important') {
1096
+ if (previousValue === '') {
1097
+ overlay.style.removeProperty('pointer-events');
1098
+ }
1099
+ else {
1100
+ overlay.style.setProperty('pointer-events', previousValue, previousPriority);
1101
+ }
1102
+ }
1103
+ };
1104
+ }
1105
+ function clearTargetOverlayPointerGuard() {
1106
+ targetOverlayPointerRestore?.();
1107
+ targetOverlayPointerRestore = undefined;
1108
+ }
1109
+ function cancelShadowTargetHitTest() {
1110
+ if (targetHitTestFrame === undefined) {
1111
+ return;
1112
+ }
1113
+ browserWindow.cancelAnimationFrame(targetHitTestFrame);
1114
+ targetHitTestFrame = undefined;
1115
+ }
403
1116
  function reportMissingTarget(presentation, presentationKey = activePresentationKey) {
404
1117
  if (presentationKey === null
405
1118
  || presentationKey !== activePresentationKey
@@ -407,11 +1120,121 @@ export function createDriverTutorialPresenter(input) {
407
1120
  return;
408
1121
  }
409
1122
  missingTargetErrorKey = presentationKey;
410
- input.onError?.(new Error(`Tutorial target was not found: ${String(presentation.step.target)}`));
1123
+ const target = String(presentation.step.target);
1124
+ reportError(new Error(`Tutorial target was not found: ${target}`));
1125
+ }
1126
+ function syncTargetAcknowledgeBinding(currentInstance, presentation) {
1127
+ clearTargetAcknowledgeBinding(currentInstance);
1128
+ const state = targetClickState;
1129
+ if (state === undefined
1130
+ || state.driver !== currentInstance
1131
+ || state.stepId !== presentation.step.id
1132
+ || !state.acknowledgeOnTargetClick) {
1133
+ return;
1134
+ }
1135
+ const { target } = state;
1136
+ const presentationKey = activePresentationKey;
1137
+ let acknowledgementQueued = false;
1138
+ const listener = () => {
1139
+ const targetActivatableAtCapture = isActivatableTutorialTarget(target, browserWindow);
1140
+ if (acknowledgementQueued || !targetActivatableAtCapture) {
1141
+ return;
1142
+ }
1143
+ acknowledgementQueued = true;
1144
+ browserWindow.queueMicrotask(() => {
1145
+ acknowledgementQueued = false;
1146
+ if (destroyed
1147
+ || instance !== currentInstance
1148
+ || activePresentationKey !== presentationKey
1149
+ || targetClickState !== state) {
1150
+ return;
1151
+ }
1152
+ try {
1153
+ input.onAcknowledge(presentation.step.id);
1154
+ }
1155
+ catch (error) {
1156
+ reportError(error);
1157
+ }
1158
+ });
1159
+ };
1160
+ target.addEventListener('click', listener, true);
1161
+ targetAcknowledgeBinding = {
1162
+ driver: currentInstance,
1163
+ listener,
1164
+ stepId: presentation.step.id,
1165
+ target,
1166
+ };
1167
+ }
1168
+ function clearTargetAcknowledgeBinding(currentInstance) {
1169
+ const binding = targetAcknowledgeBinding;
1170
+ if (binding === undefined
1171
+ || (currentInstance !== undefined && binding.driver !== currentInstance)) {
1172
+ return;
1173
+ }
1174
+ binding.target.removeEventListener('click', binding.listener, true);
1175
+ targetAcknowledgeBinding = undefined;
1176
+ }
1177
+ function setTargetClickState(currentInstance, presentation, target, mode) {
1178
+ cancelShadowTargetHitTest();
1179
+ targetClickState = target === undefined
1180
+ ? undefined
1181
+ : {
1182
+ ...mode,
1183
+ driver: currentInstance,
1184
+ stepId: presentation.step.id,
1185
+ target,
1186
+ };
1187
+ }
1188
+ function clearTargetClickState(currentInstance) {
1189
+ if (currentInstance !== undefined && targetClickState?.driver !== currentInstance) {
1190
+ return;
1191
+ }
1192
+ cancelShadowTargetHitTest();
1193
+ clearTargetOverlayPointerGuard();
1194
+ targetClickState = undefined;
1195
+ }
1196
+ function syncTargetShadowPointerStyle(presentation, target, targetActivatable) {
1197
+ targetShadowPointerStyle?.remove();
1198
+ targetShadowPointerStyle = undefined;
1199
+ if (target === undefined
1200
+ || !targetActivatable
1201
+ || !shouldAllowTutorialTargetInteraction(presentation)) {
1202
+ return;
1203
+ }
1204
+ const targetRoot = target.getRootNode();
1205
+ if (!(targetRoot instanceof ShadowRoot)) {
1206
+ return;
1207
+ }
1208
+ const targetPosition = browserWindow.getComputedStyle(target).position;
1209
+ const positionRule = targetPosition === '' || targetPosition === 'static'
1210
+ ? 'position: relative !important;'
1211
+ : '';
1212
+ const style = ownerDocument.createElement('style');
1213
+ style.textContent = `
1214
+ .driver-active-element:not(.driver-no-interaction) {
1215
+ ${positionRule}
1216
+ z-index: 1000000001 !important;
1217
+ pointer-events: auto !important;
1218
+ }
1219
+ .driver-active-element:not(.driver-no-interaction) * {
1220
+ pointer-events: auto !important;
1221
+ }
1222
+ .driver-no-interaction,
1223
+ .driver-no-interaction * {
1224
+ pointer-events: none !important;
1225
+ }
1226
+ `;
1227
+ targetRoot.appendChild(style);
1228
+ targetShadowPointerStyle = style;
411
1229
  }
412
1230
  function destroyInstancePreservingTarget(currentInstance) {
1231
+ cancelTargetGeometryMonitor();
1232
+ clearTargetGeometryObservers();
413
1233
  const target = currentInstance.getActiveElement();
414
1234
  targetSemanticsGuard?.restore();
1235
+ targetSemanticsGuard = undefined;
1236
+ targetShadowPointerStyle?.remove();
1237
+ targetShadowPointerStyle = undefined;
415
1238
  const restoreAfterDestroy = !(target instanceof HTMLElement)
416
1239
  ? undefined
417
1240
  : captureTutorialTargetSemantics(target);
@@ -419,12 +1242,30 @@ export function createDriverTutorialPresenter(input) {
419
1242
  currentInstance.destroy();
420
1243
  targetRestorePending = false;
421
1244
  restoreAfterDestroy?.();
422
- targetSemanticsGuard = undefined;
423
1245
  }
424
1246
  function applyDriverTargetMutation(mutation) {
425
- return targetSemanticsGuard === undefined
426
- ? mutation()
427
- : targetSemanticsGuard.applyDriverMutation(mutation);
1247
+ processMutationRecords(mutationObserver.takeRecords());
1248
+ mutationObserver.disconnect();
1249
+ try {
1250
+ return targetSemanticsGuard === undefined
1251
+ ? mutation()
1252
+ : targetSemanticsGuard.applyDriverMutation(mutation);
1253
+ }
1254
+ finally {
1255
+ if (!destroyed) {
1256
+ reconcileMutationRoots(true);
1257
+ }
1258
+ }
1259
+ }
1260
+ function resolveTutorialTargetClickMode(presentation, target, targetActivatable) {
1261
+ return {
1262
+ acknowledgeOnTargetClick: target !== undefined
1263
+ && presentation.acknowledgeOnTargetClick === true
1264
+ && presentation.step.advance.kind === 'acknowledge'
1265
+ && presentation.step.target !== null
1266
+ && target.id !== 'driver-dummy-element'
1267
+ && targetActivatable,
1268
+ };
428
1269
  }
429
1270
  }
430
1271
  export function resolveVisibleTutorialTarget(input) {
@@ -438,39 +1279,103 @@ export function resolveVisibleTutorialTarget(input) {
438
1279
  return null;
439
1280
  }
440
1281
  const attribute = input.targetAttribute ?? tutorialDomAttributes.target;
441
- const selector = `[${attribute}="${escapeAttributeValue(input.target)}"]`;
442
- const candidates = [...root.querySelectorAll(selector)];
1282
+ const candidates = findComposedTutorialTargets(root, attribute, input.target);
1283
+ if (input.preferActivatable === true) {
1284
+ return candidates.find((element) => isActivatableTutorialTarget(element, view)
1285
+ && isVisibleTutorialTarget(element, view))
1286
+ ?? candidates.find((element) => (isActivatableTutorialTarget(element, view) && isEligibleTutorialTarget(element, view)))
1287
+ ?? candidates.find((element) => isVisibleTutorialTarget(element, view))
1288
+ ?? candidates.find((element) => isEligibleTutorialTarget(element, view))
1289
+ ?? null;
1290
+ }
443
1291
  return candidates.find((element) => isVisibleTutorialTarget(element, view))
444
- ?? candidates.find((element) => isRenderableTutorialTarget(element, view))
1292
+ ?? candidates.find((element) => isEligibleTutorialTarget(element, view))
445
1293
  ?? null;
446
1294
  }
1295
+ function findComposedTutorialTargets(root, attribute, target) {
1296
+ return findComposedTutorialCandidates(root, `[${attribute}]`)
1297
+ .filter((candidate) => candidate.getAttribute(attribute) === target);
1298
+ }
447
1299
  export function isVisibleTutorialTarget(element, view = window) {
448
- if (!isRenderableTutorialTarget(element, view)) {
1300
+ return isVisibleTutorialTargetWithRect(element, view, element.getBoundingClientRect());
1301
+ }
1302
+ function isVisibleTutorialTargetWithRect(element, view, rect) {
1303
+ if (!isEligibleTutorialTargetWithRect(element, view, rect)) {
449
1304
  return false;
450
1305
  }
451
- let visible = intersectRect(element.getBoundingClientRect(), {
452
- bottom: view.innerHeight,
453
- left: 0,
454
- right: view.innerWidth,
455
- top: 0,
456
- });
457
- let ancestor = element.parentElement;
1306
+ const visible = resolveTutorialTargetVisibleRect(element, view, rect);
1307
+ return visible !== null;
1308
+ }
1309
+ function resolveTutorialTargetVisibleRect(element, view, rect = element.getBoundingClientRect()) {
1310
+ let visible = intersectRect(rect, resolveViewportRect(view));
1311
+ let ancestor = getComposedParentElement(element);
458
1312
  while (visible !== null && ancestor !== null) {
459
1313
  const style = view.getComputedStyle(ancestor);
460
1314
  if (style.display === 'none'
461
1315
  || style.visibility === 'hidden'
462
1316
  || Number.parseFloat(style.opacity || '1') <= 0) {
463
- return false;
1317
+ return null;
464
1318
  }
465
1319
  if ([style.overflow, style.overflowX, style.overflowY].some((value) => value === 'auto' || value === 'clip' || value === 'hidden' || value === 'scroll')) {
466
1320
  visible = intersectRect(visible, ancestor.getBoundingClientRect());
467
1321
  }
468
- ancestor = ancestor.parentElement;
1322
+ ancestor = getComposedParentElement(ancestor);
469
1323
  }
470
- return visible !== null && visible.right > visible.left && visible.bottom > visible.top;
1324
+ return visible;
1325
+ }
1326
+ function readTutorialTargetRect(element) {
1327
+ const rect = element.getBoundingClientRect();
1328
+ return {
1329
+ bottom: rect.bottom,
1330
+ height: rect.height,
1331
+ left: rect.left,
1332
+ right: rect.right,
1333
+ top: rect.top,
1334
+ width: rect.width,
1335
+ };
1336
+ }
1337
+ function tutorialTargetRectsEqual(left, right) {
1338
+ return left.bottom === right.bottom
1339
+ && left.height === right.height
1340
+ && left.left === right.left
1341
+ && left.right === right.right
1342
+ && left.top === right.top
1343
+ && left.width === right.width;
1344
+ }
1345
+ function resolveViewportRect(view) {
1346
+ const visualViewport = view.visualViewport;
1347
+ if (visualViewport !== undefined
1348
+ && visualViewport !== null
1349
+ && Number.isFinite(visualViewport.offsetLeft)
1350
+ && Number.isFinite(visualViewport.offsetTop)
1351
+ && Number.isFinite(visualViewport.width)
1352
+ && Number.isFinite(visualViewport.height)
1353
+ && visualViewport.width > 0
1354
+ && visualViewport.height > 0) {
1355
+ return {
1356
+ bottom: visualViewport.offsetTop + visualViewport.height,
1357
+ left: visualViewport.offsetLeft,
1358
+ right: visualViewport.offsetLeft + visualViewport.width,
1359
+ top: visualViewport.offsetTop,
1360
+ };
1361
+ }
1362
+ return {
1363
+ bottom: view.innerHeight,
1364
+ left: 0,
1365
+ right: view.innerWidth,
1366
+ top: 0,
1367
+ };
471
1368
  }
472
1369
  export function bindTutorialReplayTrigger(input) {
473
1370
  let pending = false;
1371
+ const reportError = (error) => {
1372
+ try {
1373
+ input.onError?.(error);
1374
+ }
1375
+ catch {
1376
+ // Error reporting must not interrupt replay trigger lifecycle handling.
1377
+ }
1378
+ };
474
1379
  const listener = () => {
475
1380
  if (pending) {
476
1381
  return;
@@ -482,7 +1387,7 @@ export function bindTutorialReplayTrigger(input) {
482
1387
  .then(() => input.presenter.waitForPendingSkip())
483
1388
  .then(() => input.director.replay())
484
1389
  .then(() => input.presenter.resetForReplay())
485
- .catch((error) => input.onError?.(error))
1390
+ .catch(reportError)
486
1391
  .finally(() => {
487
1392
  pending = false;
488
1393
  });
@@ -490,20 +1395,17 @@ export function bindTutorialReplayTrigger(input) {
490
1395
  input.element.addEventListener('click', listener);
491
1396
  return () => input.element.removeEventListener('click', listener);
492
1397
  }
493
- function createDriveStep(input, presentation, target, ownerDocument) {
1398
+ function createDriveStep(input, presentation, target, acknowledgeOnTargetClick, ownerDocument, reportError) {
494
1399
  const { step } = presentation;
495
1400
  const actionGated = step.advance.kind === 'action' || step.advance.kind === 'signal';
496
- const acknowledgeOnTargetClick = presentation.acknowledgeOnTargetClick === true
497
- && step.advance.kind === 'acknowledge'
498
- && step.target !== null
499
- && target !== undefined;
500
1401
  const targetInteraction = presentation.allowTargetInteraction === true
501
1402
  || acknowledgeOnTargetClick
502
1403
  || step.interaction === 'target'
503
1404
  || step.interaction === 'gameplay';
504
1405
  return {
505
1406
  ...(target === undefined ? {} : { element: target }),
506
- advanceOnClick: acknowledgeOnTargetClick,
1407
+ // The presenter owns target clicks so Shadow DOM retargeting cannot hide them from Driver.
1408
+ advanceOnClick: false,
507
1409
  disableActiveInteraction: !targetInteraction,
508
1410
  popover: {
509
1411
  ...(step.side === undefined ? {} : { side: step.side }),
@@ -519,26 +1421,128 @@ function createDriveStep(input, presentation, target, ownerDocument) {
519
1421
  ? ['close']
520
1422
  : ['next', 'close'],
521
1423
  title: escapeTutorialText(ownerDocument, presentation.copy.title),
522
- onNextClick: () => input.onAcknowledge(step.id),
1424
+ onNextClick: () => {
1425
+ try {
1426
+ input.onAcknowledge(step.id);
1427
+ }
1428
+ catch (error) {
1429
+ reportError(error);
1430
+ }
1431
+ },
523
1432
  },
524
1433
  };
525
1434
  }
526
- function resolveUnderlyingTutorialModal(popover, interaction, activeElement, modalSelector, currentModal) {
527
- const activeModal = activeElement?.closest(modalSelector) ?? null;
1435
+ function shouldAllowTutorialTargetInteraction(presentation) {
1436
+ const { step } = presentation;
1437
+ return presentation.allowTargetInteraction === true
1438
+ || (presentation.acknowledgeOnTargetClick === true
1439
+ && step.advance.kind === 'acknowledge')
1440
+ || step.interaction === 'target'
1441
+ || step.interaction === 'gameplay';
1442
+ }
1443
+ function shouldPreferActivatableTutorialTarget(presentation) {
1444
+ const { step } = presentation;
1445
+ const targetInteraction = presentation.allowTargetInteraction === true
1446
+ || step.interaction === 'target'
1447
+ || step.interaction === 'gameplay';
1448
+ const acknowledgeOnTargetClick = presentation.acknowledgeOnTargetClick === true
1449
+ && step.advance.kind === 'acknowledge';
1450
+ const targetGated = targetInteraction
1451
+ && (step.advance.kind === 'action' || step.advance.kind === 'signal');
1452
+ return acknowledgeOnTargetClick || targetGated;
1453
+ }
1454
+ function resolveUnderlyingTutorialModal(popover, interaction, activeElement, modalSelector, currentModal, retainCurrentModal) {
1455
+ const activeModal = findClosestComposedTutorialModal(activeElement, modalSelector);
1456
+ const view = popover.ownerDocument.defaultView;
1457
+ const isRenderableModal = (candidate) => (candidate !== null
1458
+ && candidate !== popover
1459
+ && !popover.contains(candidate)
1460
+ && view !== null
1461
+ && isEligibleUnderlyingTutorialModal(candidate, view));
1462
+ const hostReactivatedCurrentModal = currentModal?.getAttribute('aria-modal')
1463
+ ?.toLowerCase() === 'true';
1464
+ const canSelectCurrentModal = interaction !== 'blocked'
1465
+ || retainCurrentModal
1466
+ || hostReactivatedCurrentModal;
1467
+ const isSelectableModal = (candidate) => (isRenderableModal(candidate)
1468
+ && (candidate !== currentModal || canSelectCurrentModal));
1469
+ const renderableActiveModal = isSelectableModal(activeModal) ? activeModal : null;
1470
+ const renderableCurrentModal = isRenderableModal(currentModal) ? currentModal : null;
528
1471
  if (interaction !== 'blocked') {
529
- return activeModal;
1472
+ return renderableActiveModal;
530
1473
  }
531
- if (activeElement !== undefined
532
- && currentModal?.isConnected === true
533
- && currentModal.contains(activeElement)) {
534
- return currentModal;
1474
+ if (retainCurrentModal
1475
+ && activeElement !== undefined
1476
+ && renderableCurrentModal?.contains(activeElement) === true) {
1477
+ return renderableCurrentModal;
535
1478
  }
536
- return activeModal
537
- ?? [...popover.ownerDocument.querySelectorAll(modalSelector)]
538
- .find((candidate) => candidate !== popover && !popover.contains(candidate))
539
- ?? (currentModal?.isConnected === true ? currentModal : null)
1479
+ return renderableActiveModal
1480
+ ?? (canSelectCurrentModal ? renderableCurrentModal : null)
1481
+ ?? findComposedTutorialCandidates(popover.ownerDocument, modalSelector)
1482
+ .find(isSelectableModal)
540
1483
  ?? null;
541
1484
  }
1485
+ function findClosestComposedTutorialModal(activeElement, selector) {
1486
+ let current = activeElement ?? null;
1487
+ while (current !== null) {
1488
+ if (current instanceof HTMLElement && current.matches(selector)) {
1489
+ return current;
1490
+ }
1491
+ current = getComposedParentElement(current);
1492
+ }
1493
+ return null;
1494
+ }
1495
+ function findComposedTutorialCandidates(root, selector) {
1496
+ const matches = [];
1497
+ visitComposedTutorialElements(root, (element) => {
1498
+ if (element instanceof HTMLElement && element.matches(selector)) {
1499
+ matches.push(element);
1500
+ }
1501
+ });
1502
+ return matches;
1503
+ }
1504
+ function findOpenTutorialShadowRoots(ownerDocument) {
1505
+ const shadowRoots = [];
1506
+ visitComposedTutorialElements(ownerDocument, (element) => {
1507
+ if (element.shadowRoot !== null) {
1508
+ shadowRoots.push(element.shadowRoot);
1509
+ }
1510
+ });
1511
+ return shadowRoots;
1512
+ }
1513
+ function visitComposedTutorialElements(root, visitor) {
1514
+ const visited = new Set();
1515
+ const visit = (element) => {
1516
+ if (visited.has(element)) {
1517
+ return;
1518
+ }
1519
+ visited.add(element);
1520
+ visitor(element);
1521
+ visitComposedChildren(element);
1522
+ };
1523
+ const visitChildren = (parent) => {
1524
+ for (const child of parent.children) {
1525
+ visit(child);
1526
+ }
1527
+ };
1528
+ const visitComposedChildren = (parent) => {
1529
+ if (parent instanceof Element && parent.shadowRoot !== null) {
1530
+ visitChildren(parent.shadowRoot);
1531
+ return;
1532
+ }
1533
+ if (parent instanceof HTMLSlotElement) {
1534
+ const assignedElements = parent.assignedElements({ flatten: true });
1535
+ if (assignedElements.length > 0) {
1536
+ for (const assignedElement of assignedElements) {
1537
+ visit(assignedElement);
1538
+ }
1539
+ return;
1540
+ }
1541
+ }
1542
+ visitChildren(parent);
1543
+ };
1544
+ visitComposedChildren(root);
1545
+ }
542
1546
  function configureTutorialModalSemantics(popover, interaction, underlyingModal) {
543
1547
  const ownedAttributes = new Map();
544
1548
  if (interaction === 'blocked') {
@@ -557,15 +1561,23 @@ function configureTutorialModalSemantics(popover, interaction, underlyingModal)
557
1561
  ownedAttributes.set('aria-owns', [...ownedIds].join(' '));
558
1562
  }
559
1563
  }
560
- const restore = underlyingModal === null
561
- ? () => undefined
1564
+ const attributeGuard = underlyingModal === null
1565
+ ? null
562
1566
  : preserveTutorialModalSemantics(underlyingModal, ownedAttributes, popover.ownerDocument);
563
- return { modal: underlyingModal, popover, restore };
1567
+ return {
1568
+ isOwnedAttributeCurrent: (attribute) => attributeGuard?.isOwned(attribute) ?? false,
1569
+ modal: underlyingModal,
1570
+ popover,
1571
+ restore: () => attributeGuard?.restore(),
1572
+ };
564
1573
  }
565
1574
  function preserveTutorialModalSemantics(element, ownedAttributes, ownerDocument) {
566
1575
  const view = ownerDocument.defaultView;
567
1576
  if (view === null || ownedAttributes.size === 0) {
568
- return () => undefined;
1577
+ return {
1578
+ isOwned: () => false,
1579
+ restore: () => undefined,
1580
+ };
569
1581
  }
570
1582
  const previous = new Map();
571
1583
  const hostValues = new Map();
@@ -586,25 +1598,49 @@ function preserveTutorialModalSemantics(element, ownedAttributes, ownerDocument)
586
1598
  element.setAttribute(attribute, value);
587
1599
  }
588
1600
  if (previous.size === 0) {
589
- return () => undefined;
1601
+ return {
1602
+ isOwned: () => false,
1603
+ restore: () => undefined,
1604
+ };
590
1605
  }
591
1606
  const observer = new view.MutationObserver(captureHostMutations);
592
1607
  observer.observe(element, {
593
1608
  attributeFilter: [...previous.keys()],
594
1609
  attributes: true,
595
1610
  });
596
- return () => {
597
- captureHostMutations(observer.takeRecords());
598
- observer.disconnect();
599
- for (const [attribute, previousValue] of previous) {
600
- if (hostValues.has(attribute)) {
601
- restoreAttribute(element, attribute, hostValues.get(attribute) ?? null);
602
- continue;
1611
+ return {
1612
+ isOwned(attribute) {
1613
+ captureHostMutations(observer.takeRecords());
1614
+ return previous.has(attribute)
1615
+ && !hostValues.has(attribute)
1616
+ && element.getAttribute(attribute) === ownedAttributes.get(attribute);
1617
+ },
1618
+ restore() {
1619
+ captureHostMutations(observer.takeRecords());
1620
+ observer.disconnect();
1621
+ for (const [attribute, previousValue] of previous) {
1622
+ if (hostValues.has(attribute)) {
1623
+ const hostValue = hostValues.get(attribute) ?? null;
1624
+ const restoredHostValue = attribute === 'aria-owns'
1625
+ ? removeTutorialOwnedAriaTokens(hostValue, ownedAttributes.get(attribute) ?? '', previousValue)
1626
+ : hostValue;
1627
+ restoreAttribute(element, attribute, restoredHostValue);
1628
+ continue;
1629
+ }
1630
+ restoreOwnedAttribute(element, attribute, ownedAttributes.get(attribute) ?? null, previousValue);
603
1631
  }
604
- restoreOwnedAttribute(element, attribute, ownedAttributes.get(attribute) ?? null, previousValue);
605
- }
1632
+ },
606
1633
  };
607
1634
  }
1635
+ function removeTutorialOwnedAriaTokens(hostValue, ownedValue, previousValue) {
1636
+ if (hostValue === null) {
1637
+ return null;
1638
+ }
1639
+ const previousTokens = new Set((previousValue ?? '').split(/\s+/u).filter(Boolean));
1640
+ const tutorialTokens = new Set(ownedValue.split(/\s+/u).filter((token) => token !== '' && !previousTokens.has(token)));
1641
+ const remaining = hostValue.split(/\s+/u).filter((token) => token !== '' && !tutorialTokens.has(token));
1642
+ return remaining.length === 0 ? null : remaining.join(' ');
1643
+ }
608
1644
  function isTutorialModalAttribute(attribute) {
609
1645
  return attribute === 'aria-modal' || attribute === 'aria-owns';
610
1646
  }
@@ -691,7 +1727,9 @@ function clearDetachedDriverTargetClasses(element, decoratedParent, preservedPar
691
1727
  }
692
1728
  }
693
1729
  function isRenderableTutorialTarget(element, view) {
694
- const rect = element.getBoundingClientRect();
1730
+ return isRenderableTutorialTargetWithRect(element, view, element.getBoundingClientRect());
1731
+ }
1732
+ function isRenderableTutorialTargetWithRect(element, view, rect) {
695
1733
  if (!element.isConnected || rect.width <= 0 || rect.height <= 0) {
696
1734
  return false;
697
1735
  }
@@ -703,10 +1741,85 @@ function isRenderableTutorialTarget(element, view) {
703
1741
  || Number.parseFloat(style.opacity || '1') <= 0) {
704
1742
  return false;
705
1743
  }
706
- current = current.parentElement;
1744
+ current = getComposedParentElement(current);
1745
+ }
1746
+ return true;
1747
+ }
1748
+ function isEligibleUnderlyingTutorialModal(element, view) {
1749
+ return isEligibleTutorialTarget(element, view);
1750
+ }
1751
+ function isEligibleTutorialTarget(element, view) {
1752
+ return isRenderableTutorialTarget(element, view)
1753
+ && isSemanticallyActiveTutorialElement(element);
1754
+ }
1755
+ function isEligibleTutorialTargetWithRect(element, view, rect) {
1756
+ return isRenderableTutorialTargetWithRect(element, view, rect)
1757
+ && isSemanticallyActiveTutorialElement(element);
1758
+ }
1759
+ function isActivatableTutorialTarget(element, view = element.ownerDocument.defaultView) {
1760
+ if (view === null
1761
+ || view.getComputedStyle(element).pointerEvents === 'none'
1762
+ || !isStructurallyActivatableTutorialTarget(element)
1763
+ || !isSemanticallyActiveTutorialElement(element)) {
1764
+ return false;
1765
+ }
1766
+ return true;
1767
+ }
1768
+ function isStructurallyActivatableTutorialTarget(element) {
1769
+ if (element.matches(':disabled')) {
1770
+ return false;
1771
+ }
1772
+ let current = element;
1773
+ while (current !== null) {
1774
+ if (current.getAttribute('aria-disabled')?.toLowerCase() === 'true') {
1775
+ return false;
1776
+ }
1777
+ current = getComposedParentElement(current);
1778
+ }
1779
+ return true;
1780
+ }
1781
+ function isSemanticallyActiveTutorialElement(element) {
1782
+ let current = element;
1783
+ while (current !== null) {
1784
+ if (current.getAttribute('aria-hidden')?.toLowerCase() === 'true'
1785
+ || current.inert
1786
+ || current.hasAttribute('inert')) {
1787
+ return false;
1788
+ }
1789
+ current = getComposedParentElement(current);
707
1790
  }
708
1791
  return true;
709
1792
  }
1793
+ function getComposedParentElement(element) {
1794
+ if (element.assignedSlot !== null) {
1795
+ return element.assignedSlot;
1796
+ }
1797
+ if (element.parentElement !== null) {
1798
+ return element.parentElement;
1799
+ }
1800
+ const root = element.getRootNode();
1801
+ return root instanceof ShadowRoot && root.host instanceof HTMLElement ? root.host : null;
1802
+ }
1803
+ function isComposedAncestorOrSelf(element, candidateAncestor) {
1804
+ let current = element;
1805
+ while (current !== null) {
1806
+ if (current === candidateAncestor) {
1807
+ return true;
1808
+ }
1809
+ current = getComposedParentElement(current);
1810
+ }
1811
+ return false;
1812
+ }
1813
+ function composedElementUsesTreeScope(element, treeScope) {
1814
+ let current = element;
1815
+ while (current !== null) {
1816
+ if (current.getRootNode() === treeScope) {
1817
+ return true;
1818
+ }
1819
+ current = getComposedParentElement(current);
1820
+ }
1821
+ return false;
1822
+ }
710
1823
  function intersectRect(left, right) {
711
1824
  const intersection = {
712
1825
  bottom: Math.min(left.bottom, right.bottom),
@@ -737,16 +1850,21 @@ function restoreOwnedAttribute(element, name, ownedValue, previousValue) {
737
1850
  restoreAttribute(element, name, previousValue);
738
1851
  }
739
1852
  function isDriverOwnedMutation(record) {
1853
+ if (record.type === 'childList'
1854
+ && [...record.addedNodes, ...record.removedNodes].every(isTutorialOutsideBlocker)) {
1855
+ return true;
1856
+ }
740
1857
  const target = record.target;
741
- if (target.nodeType !== 1) {
1858
+ const element = target instanceof Element ? target : target.parentElement;
1859
+ if (element === null) {
742
1860
  return false;
743
1861
  }
744
- const element = target;
745
- return element.matches('.driver-overlay, .driver-popover, .driver-popover *')
746
- || element.closest('.driver-overlay, .driver-popover') !== null;
1862
+ return element.matches('.driver-overlay, .driver-popover, .driver-popover *, '
1863
+ + '[data-mpgd-tutorial-outside-blocker]')
1864
+ || element.closest('.driver-overlay, .driver-popover, [data-mpgd-tutorial-outside-blocker]') !== null;
747
1865
  }
748
- function escapeAttributeValue(value) {
749
- return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
1866
+ function isTutorialOutsideBlocker(node) {
1867
+ return node instanceof Element && node.matches('[data-mpgd-tutorial-outside-blocker]');
750
1868
  }
751
1869
  function escapeTutorialText(ownerDocument, value) {
752
1870
  const element = ownerDocument.createElement('span');