@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/director.js +131 -21
- package/dist/driver.d.ts +1 -0
- package/dist/driver.js +1225 -107
- package/dist/platform-storage.js +14 -3
- package/dist/testing.js +9 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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 =
|
|
103
|
-
.then(() => input.onSkip())
|
|
149
|
+
const skipOperation = skipping
|
|
104
150
|
.catch((error) => {
|
|
105
151
|
if (dismissedPresentationKey === dismissedKey) {
|
|
106
152
|
retryableDismissedKey = dismissedKey;
|
|
107
153
|
}
|
|
108
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
307
|
+
reportError(error);
|
|
232
308
|
}
|
|
233
309
|
return null;
|
|
234
310
|
}
|
|
235
311
|
};
|
|
236
|
-
const
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
|
|
333
|
+
reconcileMutationRoots();
|
|
334
|
+
const hostRecords = records.filter((record) => !isDriverOwnedMutation(record));
|
|
335
|
+
if (hostRecords.length > 0) {
|
|
336
|
+
scheduleRefresh(hostRecords.some(isTargetSelectionMutation));
|
|
243
337
|
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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(
|
|
254
|
-
|
|
255
|
-
browserWindow.
|
|
256
|
-
browserWindow.visualViewport?.addEventListener('
|
|
257
|
-
browserWindow.addEventListener('
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
browserWindow.
|
|
271
|
-
browserWindow.removeEventListener('
|
|
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
|
|
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 =
|
|
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
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
442
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
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
|
|
1322
|
+
ancestor = getComposedParentElement(ancestor);
|
|
469
1323
|
}
|
|
470
|
-
return visible
|
|
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(
|
|
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
|
-
|
|
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: () =>
|
|
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
|
|
527
|
-
const
|
|
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
|
|
1472
|
+
return renderableActiveModal;
|
|
530
1473
|
}
|
|
531
|
-
if (
|
|
532
|
-
&&
|
|
533
|
-
&&
|
|
534
|
-
return
|
|
1474
|
+
if (retainCurrentModal
|
|
1475
|
+
&& activeElement !== undefined
|
|
1476
|
+
&& renderableCurrentModal?.contains(activeElement) === true) {
|
|
1477
|
+
return renderableCurrentModal;
|
|
535
1478
|
}
|
|
536
|
-
return
|
|
537
|
-
??
|
|
538
|
-
|
|
539
|
-
|
|
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
|
|
561
|
-
?
|
|
1564
|
+
const attributeGuard = underlyingModal === null
|
|
1565
|
+
? null
|
|
562
1566
|
: preserveTutorialModalSemantics(underlyingModal, ownedAttributes, popover.ownerDocument);
|
|
563
|
-
return {
|
|
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
|
|
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
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1858
|
+
const element = target instanceof Element ? target : target.parentElement;
|
|
1859
|
+
if (element === null) {
|
|
742
1860
|
return false;
|
|
743
1861
|
}
|
|
744
|
-
|
|
745
|
-
|
|
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
|
|
749
|
-
return
|
|
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');
|