@eintrek/erp-shell 0.1.63 → 0.1.65
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/guide/dom.d.ts +81 -1
- package/dist/guide/guide-styles.d.ts +1 -1
- package/dist/guide/guide-ui.d.ts +47 -6
- package/dist/guide/machine.d.ts +15 -3
- package/dist/guide/progress.d.ts +7 -0
- package/dist/guide/types.d.ts +35 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +598 -92
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +597 -91
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1975,7 +1975,7 @@ function platformApiBase(app) {
|
|
|
1975
1975
|
* This package's version, attached to help messages so the back office can
|
|
1976
1976
|
* tell which shell a report came from. Keep in step with package.json.
|
|
1977
1977
|
*/
|
|
1978
|
-
const SHELL_VERSION = "0.1.
|
|
1978
|
+
const SHELL_VERSION = "0.1.65";
|
|
1979
1979
|
|
|
1980
1980
|
const HELP_APP_LABEL = {
|
|
1981
1981
|
platform: "แพลตฟอร์ม",
|
|
@@ -3911,6 +3911,52 @@ function needsBridge(step, env, ctx) {
|
|
|
3911
3911
|
!env.sidebarOpen &&
|
|
3912
3912
|
!env.isVisible(step.target, targetIdOf(step, ctx)));
|
|
3913
3913
|
}
|
|
3914
|
+
/**
|
|
3915
|
+
* The page a step happens on: its own `route`, or else where the steps before
|
|
3916
|
+
* it left the user (an earlier step's route trigger or route). Undefined when
|
|
3917
|
+
* nothing says (a mission's first steps, which work from any page).
|
|
3918
|
+
*/
|
|
3919
|
+
function stepHomeRoute(steps, index) {
|
|
3920
|
+
for (let j = Math.min(index, steps.length - 1); j >= 0; j--) {
|
|
3921
|
+
const s = steps[j];
|
|
3922
|
+
if (j < index) {
|
|
3923
|
+
// What this earlier step moved the user to comes after where it happened.
|
|
3924
|
+
const t = triggersOf(s).find((x) => "route" in x);
|
|
3925
|
+
if (t)
|
|
3926
|
+
return t.route;
|
|
3927
|
+
}
|
|
3928
|
+
if (s.route)
|
|
3929
|
+
return s.route;
|
|
3930
|
+
}
|
|
3931
|
+
return undefined;
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* The page a step is shown on, or the "ทำต่อ" card when the user is elsewhere:
|
|
3935
|
+
* its route; for a checkpoint without one, the page the steps before it lead
|
|
3936
|
+
* to (so going back to it from another page cannot loop).
|
|
3937
|
+
*/
|
|
3938
|
+
function placeRoute(steps, index) {
|
|
3939
|
+
const step = steps[index];
|
|
3940
|
+
if (step.route)
|
|
3941
|
+
return step.route;
|
|
3942
|
+
return step.checkpoint ? stepHomeRoute(steps, index) : undefined;
|
|
3943
|
+
}
|
|
3944
|
+
/** The user is not on the page the step happens on. */
|
|
3945
|
+
function offRoute(steps, index, env) {
|
|
3946
|
+
const home = stepHomeRoute(steps, index);
|
|
3947
|
+
return !!home && !matchRoute(home, env.pathname, env.search, env.code);
|
|
3948
|
+
}
|
|
3949
|
+
/** What puts the page back when a replay ends at this step (see GuideStep.abort). */
|
|
3950
|
+
function abortActionsFor(mission, index) {
|
|
3951
|
+
const step = mission.steps[index];
|
|
3952
|
+
const own = step?.abort ?? mission.abort;
|
|
3953
|
+
if (own)
|
|
3954
|
+
return Array.isArray(own) ? own : [own];
|
|
3955
|
+
// Default: Escape to the dialog the tour opened; nothing on a stable page.
|
|
3956
|
+
if (!step || index === 0 || step.checkpoint)
|
|
3957
|
+
return [];
|
|
3958
|
+
return [{ escape: true }];
|
|
3959
|
+
}
|
|
3914
3960
|
function routeParamsFor(step, env) {
|
|
3915
3961
|
if (!step.route)
|
|
3916
3962
|
return {};
|
|
@@ -4052,7 +4098,7 @@ function announceStep(d, suite, env) {
|
|
|
4052
4098
|
const step = mission.steps[s.index];
|
|
4053
4099
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4054
4100
|
const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
|
|
4055
|
-
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : resolveText(step.why, ctx);
|
|
4101
|
+
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
|
|
4056
4102
|
d.effects.push({
|
|
4057
4103
|
type: "announce",
|
|
4058
4104
|
text: `ขั้นที่ ${s.index + 1} จาก ${mission.steps.length}. ${title}${why ? `. ${why}` : ""}`,
|
|
@@ -4075,7 +4121,9 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
4075
4121
|
const params = routeParamsFor(step, env);
|
|
4076
4122
|
const ctx = guideContext(env, d.state.progress.context, params);
|
|
4077
4123
|
if (step.requires && !safe(() => env.can(step.requires), true)) {
|
|
4078
|
-
|
|
4124
|
+
// A replay writes nothing, not even what went wrong.
|
|
4125
|
+
if (!opts.replay)
|
|
4126
|
+
withProgress(d, { last_error: `forbidden:${step.requires}`.slice(0, 128) });
|
|
4079
4127
|
i++;
|
|
4080
4128
|
continue;
|
|
4081
4129
|
}
|
|
@@ -4101,7 +4149,8 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
4101
4149
|
}
|
|
4102
4150
|
const step = steps[i];
|
|
4103
4151
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4104
|
-
const
|
|
4152
|
+
const where = placeRoute(steps, i);
|
|
4153
|
+
const away = !!where && !matchRoute(where, env.pathname, env.search, env.code);
|
|
4105
4154
|
const bridge = !away && needsBridge(step, env, ctx);
|
|
4106
4155
|
setScreen(d, {
|
|
4107
4156
|
name: "step",
|
|
@@ -4147,6 +4196,35 @@ function markReadyIfVisible(d, suite, env) {
|
|
|
4147
4196
|
d.effects.push({ type: "announce", text: s.error.title });
|
|
4148
4197
|
}
|
|
4149
4198
|
}
|
|
4199
|
+
/** Where to send the user to resume at a step: its resumeUrl, or its page filled in. */
|
|
4200
|
+
function resumeHref(mission, index, d, env) {
|
|
4201
|
+
const step = mission.steps[index];
|
|
4202
|
+
const ctx = guideContext(env, d.state.progress.context);
|
|
4203
|
+
const where = placeRoute(mission.steps, index);
|
|
4204
|
+
return (safe(() => step.resumeUrl?.(ctx), undefined) ??
|
|
4205
|
+
(where ? fillRoute(where, { ...d.state.progress.context, code: env.code }) : null));
|
|
4206
|
+
}
|
|
4207
|
+
/**
|
|
4208
|
+
* The user left the page the step happens on (a link, the browser's back):
|
|
4209
|
+
* back to the last checkpoint. On another page that is the "ทำต่อ" card;
|
|
4210
|
+
* `navigate` (ลองอีกครั้ง) goes there at once.
|
|
4211
|
+
*/
|
|
4212
|
+
function recoverOffRoute(d, suite, env, navigate) {
|
|
4213
|
+
const s = d.state.screen;
|
|
4214
|
+
if (s.name !== "step")
|
|
4215
|
+
return;
|
|
4216
|
+
const mission = findMission(suite, s.mission);
|
|
4217
|
+
if (!mission)
|
|
4218
|
+
return;
|
|
4219
|
+
const cp = checkpointAtOrBefore(mission.steps, s.index);
|
|
4220
|
+
enterStep(d, suite, env, mission, cp, { replay: s.replay });
|
|
4221
|
+
const now = d.state.screen;
|
|
4222
|
+
if (navigate && now.name === "step" && now.phase === "away") {
|
|
4223
|
+
const href = resumeHref(mission, now.index, d, env);
|
|
4224
|
+
if (href)
|
|
4225
|
+
d.effects.push({ type: "navigate", href });
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4150
4228
|
function captureInto(d, step, e, ctx) {
|
|
4151
4229
|
if (!step.capture)
|
|
4152
4230
|
return;
|
|
@@ -4214,6 +4292,7 @@ function startMission(d, suite, env, key, replay) {
|
|
|
4214
4292
|
const entry = d.state.progress.missions[key];
|
|
4215
4293
|
// A mission already done is only ever replayed; its done status stays.
|
|
4216
4294
|
const isReplay = replay || entry?.status === "done";
|
|
4295
|
+
const before = d.state.progress;
|
|
4217
4296
|
d.state = { ...d.state, panelOpen: false, justDone: null };
|
|
4218
4297
|
if (d.state.progress.welcome === "not_seen")
|
|
4219
4298
|
withProgress(d, { welcome: "started" });
|
|
@@ -4231,7 +4310,9 @@ function startMission(d, suite, env, key, replay) {
|
|
|
4231
4310
|
index = checkpointAtOrBefore(mission.steps, at);
|
|
4232
4311
|
}
|
|
4233
4312
|
enterStep(d, suite, env, mission, index, { replay: isReplay });
|
|
4234
|
-
|
|
4313
|
+
// A replay never writes progress; only what starting it changed (the
|
|
4314
|
+
// welcome, first seen through a replay) is saved.
|
|
4315
|
+
if (isReplay && d.state.progress !== before)
|
|
4235
4316
|
save(d);
|
|
4236
4317
|
}
|
|
4237
4318
|
function exitMission(d) {
|
|
@@ -4281,6 +4362,17 @@ function apiFailureFor(step, sig) {
|
|
|
4281
4362
|
(!t.api.method || t.api.method.toUpperCase() === sig.method.toUpperCase()) &&
|
|
4282
4363
|
matchApiPath(t.api.path, sig.path));
|
|
4283
4364
|
}
|
|
4365
|
+
/** A replay ends early: what puts the page back (nothing on another page). */
|
|
4366
|
+
function pushAbort(d, suite, s) {
|
|
4367
|
+
if (s.phase === "away")
|
|
4368
|
+
return;
|
|
4369
|
+
const mission = findMission(suite, s.mission);
|
|
4370
|
+
if (!mission)
|
|
4371
|
+
return;
|
|
4372
|
+
const actions = abortActionsFor(mission, s.index);
|
|
4373
|
+
if (actions.length)
|
|
4374
|
+
d.effects.push({ type: "abort", actions });
|
|
4375
|
+
}
|
|
4284
4376
|
function reduceGuide(state, action, suite, env) {
|
|
4285
4377
|
const d = { state, effects: [] };
|
|
4286
4378
|
const s = state.screen;
|
|
@@ -4361,7 +4453,8 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4361
4453
|
const step = mission.steps[s.index];
|
|
4362
4454
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4363
4455
|
if (s.phase === "away") {
|
|
4364
|
-
|
|
4456
|
+
const where = placeRoute(mission.steps, s.index);
|
|
4457
|
+
if (!where || matchRoute(where, env.pathname, env.search, env.code)) {
|
|
4365
4458
|
// Arrived: run the step again from here (it may already be passed).
|
|
4366
4459
|
enterStep(d, suite, env, mission, s.index, { replay: s.replay, error: s.error });
|
|
4367
4460
|
}
|
|
@@ -4378,6 +4471,11 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4378
4471
|
break;
|
|
4379
4472
|
}
|
|
4380
4473
|
}
|
|
4474
|
+
if (!s.bridge && !stepTargetVisible(step, env, ctx, false) && offRoute(mission.steps, s.index, env)) {
|
|
4475
|
+
// The target is missing because the page is another one.
|
|
4476
|
+
recoverOffRoute(d, suite, env, false);
|
|
4477
|
+
break;
|
|
4478
|
+
}
|
|
4381
4479
|
if (armed !== s.armed)
|
|
4382
4480
|
setScreen(d, { ...s, armed });
|
|
4383
4481
|
const cur = d.state.screen;
|
|
@@ -4432,7 +4530,8 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4432
4530
|
: { title: COPY.saveFailed, detail: sig.message };
|
|
4433
4531
|
setScreen(d, { ...s, error });
|
|
4434
4532
|
d.effects.push({ type: "announce", text: [error.title, error.detail].filter(Boolean).join(". ") });
|
|
4435
|
-
|
|
4533
|
+
if (!s.replay)
|
|
4534
|
+
withProgress(d, { last_error: `api_${sig.status}:${step.key}`.slice(0, 128) });
|
|
4436
4535
|
}
|
|
4437
4536
|
break;
|
|
4438
4537
|
}
|
|
@@ -4477,6 +4576,10 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4477
4576
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4478
4577
|
if (stepTargetVisible(step, env, ctx, s.bridge))
|
|
4479
4578
|
break;
|
|
4579
|
+
if (!s.bridge && offRoute(mission.steps, s.index, env)) {
|
|
4580
|
+
recoverOffRoute(d, suite, env, false);
|
|
4581
|
+
break;
|
|
4582
|
+
}
|
|
4480
4583
|
const cp = checkpointAtOrBefore(mission.steps, s.index);
|
|
4481
4584
|
if (cp < s.index && !s.bridge) {
|
|
4482
4585
|
// A dialog or a menu closed under the step: back to what opens it.
|
|
@@ -4494,9 +4597,16 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4494
4597
|
if (!mission)
|
|
4495
4598
|
break;
|
|
4496
4599
|
const step = mission.steps[s.index];
|
|
4600
|
+
if (!s.bridge && offRoute(mission.steps, s.index, env)) {
|
|
4601
|
+
recoverOffRoute(d, suite, env, false);
|
|
4602
|
+
break;
|
|
4603
|
+
}
|
|
4497
4604
|
const anchor = s.bridge ? BRIDGE_TARGET : (step.target ?? step.key);
|
|
4498
|
-
|
|
4499
|
-
|
|
4605
|
+
// Diagnostic only: kept with the progress, sent with its next save (a
|
|
4606
|
+
// replay keeps nothing). The same error again changes nothing, so it
|
|
4607
|
+
// never sends a PUT on its own.
|
|
4608
|
+
if (!s.replay)
|
|
4609
|
+
withProgress(d, { last_error: `target_missing:${anchor}`.slice(0, 128) });
|
|
4500
4610
|
const misses = s.misses + 1;
|
|
4501
4611
|
const policy = step.onMissing ?? "retry";
|
|
4502
4612
|
const cp = checkpointAtOrBefore(mission.steps, s.index);
|
|
@@ -4523,12 +4633,19 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4523
4633
|
}
|
|
4524
4634
|
break;
|
|
4525
4635
|
}
|
|
4526
|
-
case "retry":
|
|
4527
|
-
if (s.name
|
|
4528
|
-
|
|
4529
|
-
|
|
4636
|
+
case "retry": {
|
|
4637
|
+
if (s.name !== "step" || s.phase !== "missing")
|
|
4638
|
+
break;
|
|
4639
|
+
const mission = findMission(suite, s.mission);
|
|
4640
|
+
if (mission && !s.bridge && offRoute(mission.steps, s.index, env)) {
|
|
4641
|
+
// Polling again cannot help on another page: go back to the step's.
|
|
4642
|
+
recoverOffRoute(d, suite, env, true);
|
|
4643
|
+
break;
|
|
4530
4644
|
}
|
|
4645
|
+
setScreen(d, { ...s, phase: "waiting" });
|
|
4646
|
+
markReadyIfVisible(d, suite, env);
|
|
4531
4647
|
break;
|
|
4648
|
+
}
|
|
4532
4649
|
case "blocked":
|
|
4533
4650
|
if (s.name === "step")
|
|
4534
4651
|
d.state = { ...d.state, nudge: d.state.nudge + 1 };
|
|
@@ -4537,6 +4654,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4537
4654
|
if (s.name !== "step")
|
|
4538
4655
|
break;
|
|
4539
4656
|
if (s.replay) {
|
|
4657
|
+
pushAbort(d, suite, s);
|
|
4540
4658
|
setScreen(d, { name: "idle" });
|
|
4541
4659
|
d.effects.push({ type: "toast", text: COPY.replayEnded });
|
|
4542
4660
|
}
|
|
@@ -4557,8 +4675,9 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4557
4675
|
case "endWithoutSave":
|
|
4558
4676
|
if (s.name !== "step" || !s.replay)
|
|
4559
4677
|
break;
|
|
4678
|
+
pushAbort(d, suite, s);
|
|
4560
4679
|
setScreen(d, { name: "idle" });
|
|
4561
|
-
d.effects.push({ type: "
|
|
4680
|
+
d.effects.push({ type: "toast", text: COPY.replayEnded });
|
|
4562
4681
|
break;
|
|
4563
4682
|
case "goResume": {
|
|
4564
4683
|
if (s.name !== "step" || s.phase !== "away")
|
|
@@ -4566,10 +4685,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4566
4685
|
const mission = findMission(suite, s.mission);
|
|
4567
4686
|
if (!mission)
|
|
4568
4687
|
break;
|
|
4569
|
-
const
|
|
4570
|
-
const ctx = guideContext(env, d.state.progress.context);
|
|
4571
|
-
const href = safe(() => step.resumeUrl?.(ctx), undefined) ??
|
|
4572
|
-
(step.route ? fillRoute(step.route, { ...d.state.progress.context, code: env.code }) : null);
|
|
4688
|
+
const href = resumeHref(mission, s.index, d, env);
|
|
4573
4689
|
if (href)
|
|
4574
4690
|
d.effects.push({ type: "navigate", href });
|
|
4575
4691
|
break;
|
|
@@ -4805,6 +4921,27 @@ function putBody(p, app) {
|
|
|
4805
4921
|
TERMINAL.includes(p.server_status),
|
|
4806
4922
|
};
|
|
4807
4923
|
}
|
|
4924
|
+
function stable(v) {
|
|
4925
|
+
if (Array.isArray(v))
|
|
4926
|
+
return `[${v.map(stable).join(",")}]`;
|
|
4927
|
+
if (isRecord(v)) {
|
|
4928
|
+
return `{${Object.keys(v)
|
|
4929
|
+
.sort()
|
|
4930
|
+
.map(k => `${JSON.stringify(k)}:${stable(v[k])}`)
|
|
4931
|
+
.join(",")}}`;
|
|
4932
|
+
}
|
|
4933
|
+
return JSON.stringify(v) ?? "null";
|
|
4934
|
+
}
|
|
4935
|
+
/**
|
|
4936
|
+
* What a PUT would store, without the rev it is checked against: two equal
|
|
4937
|
+
* keys mean the save would change nothing, and it is not sent (a replay
|
|
4938
|
+
* starting, a step entered again, never bump the row's rev). Key order does
|
|
4939
|
+
* not matter.
|
|
4940
|
+
*/
|
|
4941
|
+
function payloadKey(p, app) {
|
|
4942
|
+
const { rev: _rev, restart: _restart, ...rest } = putBody(p, app);
|
|
4943
|
+
return stable(rest);
|
|
4944
|
+
}
|
|
4808
4945
|
const MISSION_RANK = { todo: 0, skipped: 1, active: 1, done: 3 };
|
|
4809
4946
|
const WELCOME_RANK = { not_seen: 0, skipped: 1, started: 1 };
|
|
4810
4947
|
/**
|
|
@@ -4939,8 +5076,9 @@ function isPresent(el) {
|
|
|
4939
5076
|
if (r.width <= 0 || r.height <= 0)
|
|
4940
5077
|
return false;
|
|
4941
5078
|
// Slid off the side (a closed drawer); off the top or bottom is only
|
|
4942
|
-
// scrolled away, and the tour scrolls it back.
|
|
4943
|
-
|
|
5079
|
+
// scrolled away, and the tour scrolls it back. So is a tab scrolled out of
|
|
5080
|
+
// a row of tabs that scrolls sideways, as long as the row is on screen.
|
|
5081
|
+
if ((r.right <= 0 || r.left >= window.innerWidth) && !inSidewaysScroller(el))
|
|
4944
5082
|
return false;
|
|
4945
5083
|
if (el.getAttribute("data-state") === "closed")
|
|
4946
5084
|
return false;
|
|
@@ -5020,6 +5158,26 @@ function sameRect(a, b) {
|
|
|
5020
5158
|
const POPPER_SELECTOR = "[data-radix-popper-content-wrapper]";
|
|
5021
5159
|
/** Toasts stay usable above the tour. */
|
|
5022
5160
|
const TOASTER_SELECTOR = "[data-sonner-toaster]";
|
|
5161
|
+
/** Open dialogs (Radix dialog, alert dialog, sheet, popover content). */
|
|
5162
|
+
const OPEN_DIALOG_SELECTOR = '[role="dialog"][data-state="open"],[role="alertdialog"][data-state="open"]';
|
|
5163
|
+
/**
|
|
5164
|
+
* Whether an open dialog of the app's covers the target: the target is
|
|
5165
|
+
* neither in it nor around it (a confirm opened by the step's own button).
|
|
5166
|
+
* The ring and the hand would be drawn over the dialog, pointing at a button
|
|
5167
|
+
* behind it.
|
|
5168
|
+
*/
|
|
5169
|
+
function coveredByDialog(el) {
|
|
5170
|
+
if (!hasDocument() || !el)
|
|
5171
|
+
return false;
|
|
5172
|
+
for (const d of Array.from(document.querySelectorAll(OPEN_DIALOG_SELECTOR))) {
|
|
5173
|
+
if (inGuideLayer(d) || !isPresent(d))
|
|
5174
|
+
continue;
|
|
5175
|
+
if (d.contains(el) || el.contains(d))
|
|
5176
|
+
continue;
|
|
5177
|
+
return true;
|
|
5178
|
+
}
|
|
5179
|
+
return false;
|
|
5180
|
+
}
|
|
5023
5181
|
function openPoppers() {
|
|
5024
5182
|
if (!hasDocument())
|
|
5025
5183
|
return [];
|
|
@@ -5037,18 +5195,224 @@ function prefersReducedMotion() {
|
|
|
5037
5195
|
function isPhoneWidth() {
|
|
5038
5196
|
return typeof window !== "undefined" && window.innerWidth < 768;
|
|
5039
5197
|
}
|
|
5040
|
-
|
|
5198
|
+
/* ---------------- what is really on screen ---------------- */
|
|
5199
|
+
const INFINITE = 1e7;
|
|
5200
|
+
/** The overlap of two rectangles, or null when they do not meet. */
|
|
5201
|
+
function intersectRect(a, b) {
|
|
5202
|
+
const x = Math.max(a.x, b.x);
|
|
5203
|
+
const y = Math.max(a.y, b.y);
|
|
5204
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
5205
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
5206
|
+
if (right - x <= 0 || bottom - y <= 0)
|
|
5207
|
+
return null;
|
|
5208
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
5209
|
+
}
|
|
5210
|
+
/** The part of `rect` inside every clip (scroll boxes, the viewport); null when none is. */
|
|
5211
|
+
function visibleRect(rect, clips) {
|
|
5212
|
+
let r = rect;
|
|
5213
|
+
for (const c of clips) {
|
|
5214
|
+
if (!r)
|
|
5215
|
+
return null;
|
|
5216
|
+
r = intersectRect(r, c);
|
|
5217
|
+
}
|
|
5218
|
+
return r;
|
|
5219
|
+
}
|
|
5220
|
+
const clipsX = (v) => v !== "visible";
|
|
5221
|
+
const scrollsX = (v) => v === "auto" || v === "scroll";
|
|
5222
|
+
/**
|
|
5223
|
+
* The ancestors that cut the element off: anything with overflow other than
|
|
5224
|
+
* visible on an axis. The body and the root scroll the page and are covered by
|
|
5225
|
+
* the viewport. Read once per target; their rectangles are read every frame.
|
|
5226
|
+
*/
|
|
5227
|
+
function clipAncestors(el) {
|
|
5228
|
+
const out = [];
|
|
5229
|
+
if (typeof getComputedStyle !== "function")
|
|
5230
|
+
return out;
|
|
5231
|
+
for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
5232
|
+
if (n.hasAttribute(GUIDE_LAYER_ATTRIBUTE))
|
|
5233
|
+
break;
|
|
5234
|
+
const s = getComputedStyle(n);
|
|
5235
|
+
const x = clipsX(s.overflowX);
|
|
5236
|
+
const y = clipsX(s.overflowY);
|
|
5237
|
+
if (x || y)
|
|
5238
|
+
out.push({ el: n, x, y });
|
|
5239
|
+
if (s.position === "fixed")
|
|
5240
|
+
break;
|
|
5241
|
+
}
|
|
5242
|
+
return out;
|
|
5243
|
+
}
|
|
5244
|
+
/** An ancestor's clip as a rectangle: unbounded on an axis it does not clip. */
|
|
5245
|
+
function clipRectOf(c) {
|
|
5246
|
+
const r = c.el.getBoundingClientRect();
|
|
5247
|
+
return {
|
|
5248
|
+
x: c.x ? r.left : -INFINITE,
|
|
5249
|
+
y: c.y ? r.top : -INFINITE,
|
|
5250
|
+
width: c.x ? r.width : INFINITE * 2,
|
|
5251
|
+
height: c.y ? r.height : INFINITE * 2,
|
|
5252
|
+
};
|
|
5253
|
+
}
|
|
5254
|
+
function viewportRect() {
|
|
5255
|
+
const vv = typeof window !== "undefined" ? window.visualViewport : undefined;
|
|
5256
|
+
return {
|
|
5257
|
+
x: 0,
|
|
5258
|
+
y: vv ? vv.offsetTop : 0,
|
|
5259
|
+
width: window.innerWidth,
|
|
5260
|
+
height: vv ? vv.height : window.innerHeight,
|
|
5261
|
+
};
|
|
5262
|
+
}
|
|
5263
|
+
/**
|
|
5264
|
+
* The part of the element a person can see: its box cut by every scroll box
|
|
5265
|
+
* around it and by the viewport. The spotlight, its hole, the ring and the
|
|
5266
|
+
* hand use this, so a tab half out of a sideways-scrolling row of tabs is not
|
|
5267
|
+
* spotlit where it cannot be pressed. Null when none of it is on screen.
|
|
5268
|
+
*/
|
|
5269
|
+
function visibleRectOf(el, ancestors = clipAncestors(el)) {
|
|
5270
|
+
return visibleRect(rectOf(el), [...ancestors.map(clipRectOf), viewportRect()]);
|
|
5271
|
+
}
|
|
5272
|
+
/** Inside a box that scrolls sideways (with somewhere to scroll) that is itself on screen. */
|
|
5273
|
+
function inSidewaysScroller(el) {
|
|
5274
|
+
if (typeof getComputedStyle !== "function")
|
|
5275
|
+
return false;
|
|
5276
|
+
for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
5277
|
+
const s = getComputedStyle(n);
|
|
5278
|
+
if (scrollsX(s.overflowX) && n.scrollWidth > n.clientWidth + 1) {
|
|
5279
|
+
const r = n.getBoundingClientRect();
|
|
5280
|
+
return r.width > 0 && r.right > 0 && r.left < window.innerWidth;
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
return false;
|
|
5284
|
+
}
|
|
5285
|
+
/**
|
|
5286
|
+
* How to bring a target into view, or null when it already is: to the middle
|
|
5287
|
+
* when it is above or below the comfortable band (under the header, at the
|
|
5288
|
+
* bottom edge), the nearest edge when only a side is cut off (a tab out of a
|
|
5289
|
+
* sideways-scrolling row, on a phone).
|
|
5290
|
+
*/
|
|
5291
|
+
function revealPlan(rect, visible, viewportHeight) {
|
|
5292
|
+
const offVertically = rect.y < 70 || rect.y + rect.height > viewportHeight - 20;
|
|
5293
|
+
const cutSideways = !visible || visible.width < rect.width - 1;
|
|
5294
|
+
if (!offVertically && !cutSideways)
|
|
5295
|
+
return null;
|
|
5296
|
+
return { block: offVertically ? "center" : "nearest", inline: "nearest" };
|
|
5297
|
+
}
|
|
5298
|
+
/** Room left beside a target scrolled into a sideways scroll box. */
|
|
5299
|
+
const SIDEWAYS_MARGIN = 12;
|
|
5300
|
+
/**
|
|
5301
|
+
* How far to scroll a sideways scroll box so the target sits inside its
|
|
5302
|
+
* visible part (`box`: the box cut by the viewport) with `margin` to spare on
|
|
5303
|
+
* both sides; 0 when it already does. Clamped to what the box can scroll.
|
|
5304
|
+
* A target wider than the room is aligned to its start.
|
|
5305
|
+
*/
|
|
5306
|
+
function sidewaysDelta(target, box, scrollLeft, maxScrollLeft, margin = SIDEWAYS_MARGIN) {
|
|
5307
|
+
const left = box.x + margin;
|
|
5308
|
+
const right = box.x + box.width - margin;
|
|
5309
|
+
let delta = 0;
|
|
5310
|
+
if (target.x < left || target.width > right - left)
|
|
5311
|
+
delta = target.x - left;
|
|
5312
|
+
else if (target.x + target.width > right)
|
|
5313
|
+
delta = target.x + target.width - right;
|
|
5314
|
+
const next = Math.min(Math.max(scrollLeft + delta, 0), Math.max(maxScrollLeft, 0));
|
|
5315
|
+
return next - scrollLeft;
|
|
5316
|
+
}
|
|
5317
|
+
/** The nearest ancestor that scrolls sideways and has somewhere to scroll. */
|
|
5318
|
+
function sidewaysScroller(el) {
|
|
5319
|
+
if (typeof getComputedStyle !== "function")
|
|
5320
|
+
return null;
|
|
5321
|
+
for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
5322
|
+
if (scrollsX(getComputedStyle(n).overflowX) && n.scrollWidth > n.clientWidth + 1)
|
|
5323
|
+
return n;
|
|
5324
|
+
}
|
|
5325
|
+
return null;
|
|
5326
|
+
}
|
|
5327
|
+
/**
|
|
5328
|
+
* Scrolls the target into view (instant under reduced motion). In a box that
|
|
5329
|
+
* scrolls sideways (a row of tabs on a phone) the box is scrolled first,
|
|
5330
|
+
* leaving SIDEWAYS_MARGIN beside the target so its whole label shows; then
|
|
5331
|
+
* the page, up or down only.
|
|
5332
|
+
*/
|
|
5041
5333
|
function revealTarget(el) {
|
|
5042
|
-
const
|
|
5334
|
+
const behavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
5335
|
+
const scroller = sidewaysScroller(el);
|
|
5336
|
+
if (scroller) {
|
|
5337
|
+
const box = intersectRect(rectOf(scroller), viewportRect()) ?? rectOf(scroller);
|
|
5338
|
+
const dx = sidewaysDelta(rectOf(el), box, scroller.scrollLeft, scroller.scrollWidth - scroller.clientWidth);
|
|
5339
|
+
// Instant: the page's own scroll below must not cancel it.
|
|
5340
|
+
if (Math.abs(dx) >= 1)
|
|
5341
|
+
scroller.scrollLeft += dx;
|
|
5342
|
+
}
|
|
5043
5343
|
const vh = window.visualViewport?.height ?? window.innerHeight;
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5344
|
+
const plan = revealPlan(rectOf(el), visibleRectOf(el), vh);
|
|
5345
|
+
if (!plan)
|
|
5346
|
+
return;
|
|
5347
|
+
try {
|
|
5348
|
+
el.scrollIntoView({ ...plan, behavior });
|
|
5349
|
+
}
|
|
5350
|
+
catch {
|
|
5351
|
+
el.scrollIntoView();
|
|
5352
|
+
}
|
|
5353
|
+
}
|
|
5354
|
+
/* ---------------- the scrim's holes, as one union ---------------- */
|
|
5355
|
+
/**
|
|
5356
|
+
* The scrim's blocking area: the screen (with a 10 px margin) minus the union
|
|
5357
|
+
* of the holes, as rectangles that do not overlap. Cutting each hole out with
|
|
5358
|
+
* an even-odd fill made the place where two holes overlap (a field and the
|
|
5359
|
+
* select list open over it) solid again, and the option there could not be
|
|
5360
|
+
* pressed.
|
|
5361
|
+
*
|
|
5362
|
+
* Every hole edge splits the screen into a grid; a cell is blocked when no
|
|
5363
|
+
* hole covers it. Cells in a row are joined, and identical rows stacked.
|
|
5364
|
+
*/
|
|
5365
|
+
function blockedRects(holes, width, height) {
|
|
5366
|
+
const x0 = -10;
|
|
5367
|
+
const y0 = -10;
|
|
5368
|
+
const x1 = width + 10;
|
|
5369
|
+
const y1 = height + 10;
|
|
5370
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
5371
|
+
const hs = holes
|
|
5372
|
+
.filter(h => h.width > 0 && h.height > 0)
|
|
5373
|
+
.map(h => ({
|
|
5374
|
+
l: clamp(h.x, x0, x1),
|
|
5375
|
+
t: clamp(h.y, y0, y1),
|
|
5376
|
+
r: clamp(h.x + h.width, x0, x1),
|
|
5377
|
+
b: clamp(h.y + h.height, y0, y1),
|
|
5378
|
+
}))
|
|
5379
|
+
.filter(h => h.r > h.l && h.b > h.t);
|
|
5380
|
+
if (!hs.length)
|
|
5381
|
+
return [{ x: x0, y: y0, width: x1 - x0, height: y1 - y0 }];
|
|
5382
|
+
const uniq = (v) => Array.from(new Set(v)).sort((a, b) => a - b);
|
|
5383
|
+
const xs = uniq([x0, x1, ...hs.flatMap(h => [h.l, h.r])]);
|
|
5384
|
+
const ys = uniq([y0, y1, ...hs.flatMap(h => [h.t, h.b])]);
|
|
5385
|
+
const out = [];
|
|
5386
|
+
let prev = null;
|
|
5387
|
+
for (let j = 0; j < ys.length - 1; j++) {
|
|
5388
|
+
const top = ys[j];
|
|
5389
|
+
const bottom = ys[j + 1];
|
|
5390
|
+
const cy = (top + bottom) / 2;
|
|
5391
|
+
const row = [];
|
|
5392
|
+
for (let i = 0; i < xs.length - 1; i++) {
|
|
5393
|
+
const left = xs[i];
|
|
5394
|
+
const right = xs[i + 1];
|
|
5395
|
+
const cx = (left + right) / 2;
|
|
5396
|
+
const covered = hs.some(h => cx > h.l && cx < h.r && cy > h.t && cy < h.b);
|
|
5397
|
+
if (covered)
|
|
5398
|
+
continue;
|
|
5399
|
+
const last = row[row.length - 1];
|
|
5400
|
+
if (last && last.x + last.width === left)
|
|
5401
|
+
last.width = right - last.x;
|
|
5402
|
+
else
|
|
5403
|
+
row.push({ x: left, y: top, width: right - left, height: bottom - top });
|
|
5047
5404
|
}
|
|
5048
|
-
|
|
5049
|
-
|
|
5405
|
+
const key = row.map(r => `${r.x},${r.width}`).join("|");
|
|
5406
|
+
if (prev && prev.key === key) {
|
|
5407
|
+
for (const r of prev.rects)
|
|
5408
|
+
r.height += bottom - top;
|
|
5409
|
+
}
|
|
5410
|
+
else {
|
|
5411
|
+
out.push(...row);
|
|
5412
|
+
prev = { key, rects: row };
|
|
5050
5413
|
}
|
|
5051
5414
|
}
|
|
5415
|
+
return out;
|
|
5052
5416
|
}
|
|
5053
5417
|
|
|
5054
5418
|
/**
|
|
@@ -5070,7 +5434,8 @@ const GUIDE_CSS = `
|
|
|
5070
5434
|
.eg-i{pointer-events:auto}
|
|
5071
5435
|
.eg-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
5072
5436
|
.eg-scrim-svg{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
|
|
5073
|
-
.eg-scrim-svg
|
|
5437
|
+
.eg-scrim-svg .eg-scrim-fill{fill:var(--eg-scrim);pointer-events:none}
|
|
5438
|
+
.eg-scrim-svg .eg-scrim-hit{fill:transparent;pointer-events:all}
|
|
5074
5439
|
.eg-scrim{position:fixed;inset:0;background:var(--eg-scrim);pointer-events:auto}
|
|
5075
5440
|
.eg-scrim.eg-light{background:rgba(12,20,34,.45)}
|
|
5076
5441
|
.eg-scrim.eg-faint{background:rgba(12,20,34,.35)}
|
|
@@ -5179,6 +5544,7 @@ const GUIDE_CSS = `
|
|
|
5179
5544
|
.eg-q-item.eg-done .eg-t b{color:var(--eg-muted)}
|
|
5180
5545
|
.eg-q-item .eg-t small{display:block;font-size:12px;color:var(--eg-muted)}
|
|
5181
5546
|
.eg-q-item .eg-t small.eg-sk{color:#b06a00}
|
|
5547
|
+
.eg-q-item .eg-t small.eg-lock{color:var(--eg-fg);font-weight:500}
|
|
5182
5548
|
.eg-q-item .eg-btn{height:32px;padding:0 12px;font-size:13px}
|
|
5183
5549
|
.eg-q-acts{display:flex;flex-direction:column;align-items:flex-end;gap:2px}
|
|
5184
5550
|
.eg-q-acts .eg-link{height:22px;font-size:12px;padding:0 2px}
|
|
@@ -5243,15 +5609,53 @@ function roundRect(r, radius) {
|
|
|
5243
5609
|
`H${x + rad}A${rad} ${rad} 0 0 1 ${x} ${y + h - rad}` +
|
|
5244
5610
|
`V${y + rad}A${rad} ${rad} 0 0 1 ${x + rad} ${y}Z`);
|
|
5245
5611
|
}
|
|
5612
|
+
const rectPath = (r) => `M${r.x} ${r.y}H${r.x + r.width}V${r.y + r.height}H${r.x}Z`;
|
|
5613
|
+
/**
|
|
5614
|
+
* The dim layer that eats every press outside the holes. The holes are a
|
|
5615
|
+
* union: where two overlap (a field and the select list open over it) the
|
|
5616
|
+
* page stays open.
|
|
5617
|
+
*
|
|
5618
|
+
* - The look: one rectangle masked by the rounded holes (a mask adds holes
|
|
5619
|
+
* up, so an overlap is not filled back in).
|
|
5620
|
+
* - The presses: a transparent path made of the screen minus the holes, as
|
|
5621
|
+
* rectangles that do not overlap (blockedRects). A press inside a hole
|
|
5622
|
+
* reaches the page underneath; any other lands on the path (marked
|
|
5623
|
+
* data-eg-block) and is cancelled.
|
|
5624
|
+
* - `open`: the holes a press may go through, when not all of them (a "view"
|
|
5625
|
+
* step's target is shown but not pressable). Default: every hole.
|
|
5626
|
+
*/
|
|
5627
|
+
function Scrim({ holes, open, width, height }) {
|
|
5628
|
+
const maskId = "eg-scrim-mask-" + React__namespace.useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
5629
|
+
const block = blockedRects(open ?? holes, width, height).map(rectPath).join("");
|
|
5630
|
+
return (jsxRuntime.jsxs("svg", { className: "eg-scrim-svg", width: width, height: height, "aria-hidden": "true", children: [holes.length ? (jsxRuntime.jsx("defs", { children: jsxRuntime.jsxs("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: -10, y: -10, width: width + 20, height: height + 20, children: [jsxRuntime.jsx("rect", { x: -10, y: -10, width: width + 20, height: height + 20, fill: "#fff" }), holes.map((h, i) => (jsxRuntime.jsx("path", { d: roundRect(h, 12), fill: "#000" }, i)))] }) })) : null, jsxRuntime.jsx("rect", { className: "eg-scrim-fill", x: -10, y: -10, width: width + 20, height: height + 20, mask: holes.length ? `url(#${maskId})` : undefined }), jsxRuntime.jsx("path", { className: "eg-scrim-hit", d: block, "data-eg-block": "" })] }));
|
|
5631
|
+
}
|
|
5632
|
+
/** Whether two rectangles overlap. */
|
|
5633
|
+
const meets = (a, b) => !!intersectRect(a, b);
|
|
5634
|
+
/** The smallest rectangle around all of them. */
|
|
5635
|
+
function boundingRect(rs) {
|
|
5636
|
+
const x = Math.min(...rs.map(r => r.x));
|
|
5637
|
+
const y = Math.min(...rs.map(r => r.y));
|
|
5638
|
+
const right = Math.max(...rs.map(r => r.x + r.width));
|
|
5639
|
+
const bottom = Math.max(...rs.map(r => r.y + r.height));
|
|
5640
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
5641
|
+
}
|
|
5642
|
+
/** Where the bubble may go, in order of preference. */
|
|
5643
|
+
const BUBBLE_PLACEMENTS = ["right", "bottom", "top", "left"];
|
|
5246
5644
|
/**
|
|
5247
|
-
* The
|
|
5248
|
-
*
|
|
5249
|
-
*
|
|
5645
|
+
* The first placement (in BUBBLE_PLACEMENTS order) whose bubble is on screen,
|
|
5646
|
+
* off the target (with its open list), and off everything to keep clear;
|
|
5647
|
+
* failing that, the first that is only on screen and off the target; -1 when
|
|
5648
|
+
* none is.
|
|
5250
5649
|
*/
|
|
5251
|
-
function
|
|
5252
|
-
const
|
|
5253
|
-
const
|
|
5254
|
-
|
|
5650
|
+
function pickPlacement(results, bw, bh, anchor, clear, bounds) {
|
|
5651
|
+
const box = (r) => ({ x: r.x, y: r.y, width: bw, height: bh });
|
|
5652
|
+
const fits = (b) => b.x >= bounds.x - 1 && b.y >= bounds.y - 1 && b.x + b.width <= bounds.x + bounds.width + 1 && b.y + b.height <= bounds.y + bounds.height + 1;
|
|
5653
|
+
const ok = results.map(r => {
|
|
5654
|
+
const b = box(r);
|
|
5655
|
+
return fits(b) && !meets(b, anchor);
|
|
5656
|
+
});
|
|
5657
|
+
const best = results.findIndex((r, i) => ok[i] && !clear.some(c => meets(box(r), c)));
|
|
5658
|
+
return best >= 0 ? best : ok.indexOf(true);
|
|
5255
5659
|
}
|
|
5256
5660
|
const BUBBLE_TITLE_ID = "eg-bubble-title";
|
|
5257
5661
|
const BUBBLE_WHY_ID = "eg-bubble-why";
|
|
@@ -5291,12 +5695,21 @@ function StepLayer(p) {
|
|
|
5291
5695
|
const gap = p.showHand ? 34 : 14;
|
|
5292
5696
|
const spotKey = spot ? `${spot.x}|${spot.y}|${spot.width}|${spot.height}` : "";
|
|
5293
5697
|
const contentKey = `${p.bubble.title}|${p.bubble.why}|${p.bubble.error?.title ?? ""}|${p.bubble.showNext}`;
|
|
5698
|
+
// What the bubble sits beside: the target, with an open list over it (the
|
|
5699
|
+
// list is what the person is choosing from now). What it must not cover:
|
|
5700
|
+
// open lists, and the other anchors the step allows (a map step's
|
|
5701
|
+
// coordinate fields).
|
|
5702
|
+
const popoverList = p.popovers ?? [];
|
|
5703
|
+
const anchor = spot ? boundingRect([spot, ...popoverList.filter(o => meets(o, spot))]) : null;
|
|
5704
|
+
const clear = [...popoverList, ...p.holes];
|
|
5705
|
+
const anchorKey = anchor ? `${anchor.x}|${anchor.y}|${anchor.width}|${anchor.height}` : "";
|
|
5706
|
+
const clearKey = clear.map(r => `${r.x},${r.y},${r.width},${r.height}`).join(";");
|
|
5294
5707
|
React.useLayoutEffect(() => {
|
|
5295
5708
|
const el = bubbleRef.current;
|
|
5296
5709
|
if (!el || !showBubble)
|
|
5297
5710
|
return;
|
|
5298
5711
|
const m = 16;
|
|
5299
|
-
if (!spot) {
|
|
5712
|
+
if (!spot || !anchor) {
|
|
5300
5713
|
// Nothing to point at: the middle of the screen (the bottom on a phone).
|
|
5301
5714
|
const bw = el.offsetWidth;
|
|
5302
5715
|
const bh = el.offsetHeight;
|
|
@@ -5307,13 +5720,13 @@ function StepLayer(p) {
|
|
|
5307
5720
|
}
|
|
5308
5721
|
if (phone) {
|
|
5309
5722
|
// Docked to the top or the bottom of the screen, whichever covers less
|
|
5310
|
-
// of the target (target in the lower half: top).
|
|
5723
|
+
// of the target and its open list (target in the lower half: top).
|
|
5311
5724
|
const vh = viewport.height;
|
|
5312
5725
|
const oy = viewport.offsetTop;
|
|
5313
5726
|
const bh = el.offsetHeight;
|
|
5314
5727
|
const topT = oy + m + 4;
|
|
5315
5728
|
const botT = oy + vh - bh - m;
|
|
5316
|
-
const { y, height: h } =
|
|
5729
|
+
const { y, height: h } = anchor;
|
|
5317
5730
|
const overlapTop = Math.max(0, topT + bh + 8 - y);
|
|
5318
5731
|
const overlapBot = Math.max(0, y + h + gap - botT);
|
|
5319
5732
|
const top = y + h / 2 > vh / 2
|
|
@@ -5327,40 +5740,42 @@ function StepLayer(p) {
|
|
|
5327
5740
|
return;
|
|
5328
5741
|
}
|
|
5329
5742
|
let cancelled = false;
|
|
5743
|
+
const a = anchor;
|
|
5330
5744
|
const ref = {
|
|
5331
5745
|
getBoundingClientRect: () => ({
|
|
5332
|
-
x:
|
|
5333
|
-
y:
|
|
5334
|
-
left:
|
|
5335
|
-
top:
|
|
5336
|
-
width:
|
|
5337
|
-
height:
|
|
5338
|
-
right:
|
|
5339
|
-
bottom:
|
|
5746
|
+
x: a.x,
|
|
5747
|
+
y: a.y,
|
|
5748
|
+
left: a.x,
|
|
5749
|
+
top: a.y,
|
|
5750
|
+
width: a.width,
|
|
5751
|
+
height: a.height,
|
|
5752
|
+
right: a.x + a.width,
|
|
5753
|
+
bottom: a.y + a.height,
|
|
5340
5754
|
}),
|
|
5341
5755
|
};
|
|
5342
|
-
dom.computePosition(ref, el, {
|
|
5756
|
+
const at = (placement) => dom.computePosition(ref, el, {
|
|
5343
5757
|
strategy: "fixed",
|
|
5344
|
-
placement
|
|
5758
|
+
placement,
|
|
5345
5759
|
middleware: [
|
|
5346
|
-
dom.offset(
|
|
5347
|
-
dom.flip({ fallbackPlacements: ["bottom", "top", "left"], padding: m }),
|
|
5760
|
+
dom.offset(placement.startsWith("bottom") ? gap : 14),
|
|
5348
5761
|
dom.shift({ padding: m }),
|
|
5349
5762
|
...(arrowRef.current ? [dom.arrow({ element: arrowRef.current, padding: 14 })] : []),
|
|
5350
5763
|
],
|
|
5351
|
-
})
|
|
5352
|
-
|
|
5764
|
+
});
|
|
5765
|
+
Promise.all(BUBBLE_PLACEMENTS.map(at))
|
|
5766
|
+
.then(results => {
|
|
5353
5767
|
if (cancelled)
|
|
5354
5768
|
return;
|
|
5355
5769
|
const bw = el.offsetWidth;
|
|
5356
5770
|
const bh = el.offsetHeight;
|
|
5357
|
-
const
|
|
5358
|
-
const
|
|
5359
|
-
if (
|
|
5771
|
+
const bounds = { x: m, y: viewport.offsetTop + m, width: window.innerWidth - 2 * m, height: viewport.height - 2 * m };
|
|
5772
|
+
const i = pickPlacement(results, bw, bh, a, clear, bounds);
|
|
5773
|
+
if (i < 0) {
|
|
5360
5774
|
// Nowhere beside it: the bottom-right corner.
|
|
5361
5775
|
setPos({ left: window.innerWidth - bw - m, top: viewport.height + viewport.offsetTop - bh - m, side: "dock" });
|
|
5362
5776
|
return;
|
|
5363
5777
|
}
|
|
5778
|
+
const { x, y, placement, middlewareData } = results[i];
|
|
5364
5779
|
const side = placement.split("-")[0];
|
|
5365
5780
|
setPos({ left: x, top: y, side, ax: middlewareData.arrow?.x, ay: middlewareData.arrow?.y });
|
|
5366
5781
|
})
|
|
@@ -5371,11 +5786,15 @@ function StepLayer(p) {
|
|
|
5371
5786
|
return () => {
|
|
5372
5787
|
cancelled = true;
|
|
5373
5788
|
};
|
|
5374
|
-
//
|
|
5789
|
+
// The keys stand for spot, the lists, the holes and the bubble's size.
|
|
5375
5790
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5376
|
-
}, [spotKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
5377
|
-
const
|
|
5378
|
-
const
|
|
5791
|
+
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
5792
|
+
const popovers = p.popovers ?? [];
|
|
5793
|
+
const covered = !!p.covered;
|
|
5794
|
+
const holes = [...(spot && !covered ? [spot] : []), ...popovers, ...p.holes];
|
|
5795
|
+
const openHoles = p.targetPressable === false ? [...popovers, ...p.holes] : holes;
|
|
5796
|
+
const ringShown = !!spot && p.phase === "ready" && !covered && !popovers.some(o => meets(o, spot));
|
|
5797
|
+
const hand = spot && p.showHand && p.phase === "ready" && !covered && !popovers.length ? handPosition(spot, viewport.height) : null;
|
|
5379
5798
|
let arrowStyle;
|
|
5380
5799
|
if (pos && pos.side !== "dock") {
|
|
5381
5800
|
if (pos.side === "right")
|
|
@@ -5387,7 +5806,7 @@ function StepLayer(p) {
|
|
|
5387
5806
|
else
|
|
5388
5807
|
arrowStyle = { bottom: -7, left: pos.ax };
|
|
5389
5808
|
}
|
|
5390
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Scrim, { holes: p.phase === "ready" ? holes : [], width: viewport.width, height: Math.max(viewport.height + viewport.offsetTop, 0) }), spot &&
|
|
5809
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Scrim, { holes: p.phase === "ready" ? holes : [], open: p.phase === "ready" ? openHoles : [], width: viewport.width, height: Math.max(viewport.height + viewport.offsetTop, 0) }), spot && ringShown ? (jsxRuntime.jsx("div", { ref: ringRef, className: "eg-ring" + (p.moving ? " eg-move" : ""), style: { left: spot.x, top: spot.y, width: spot.width, height: spot.height }, "aria-hidden": "true" })) : null, hand ? (jsxRuntime.jsx("div", { className: "eg-hand" + (hand.flip ? " eg-flip" : ""), style: { left: hand.left, top: hand.top }, "aria-hidden": "true", children: jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { d: HAND_PATH }) }) })) : null, showBubble ? (jsxRuntime.jsxs("div", { ref: bubbleRef, className: "eg-bubble" + (p.bubble.error ? " eg-err" : ""), role: "dialog", "aria-modal": "false", "aria-labelledby": BUBBLE_TITLE_ID, "aria-describedby": BUBBLE_WHY_ID, tabIndex: -1, "data-eg-bubble": "", style: {
|
|
5391
5810
|
left: pos?.left ?? 0,
|
|
5392
5811
|
top: pos?.top ?? 0,
|
|
5393
5812
|
width: phone ? viewport.width - 32 : undefined,
|
|
@@ -5398,7 +5817,7 @@ function BubbleBody(p) {
|
|
|
5398
5817
|
const b = p.bubble;
|
|
5399
5818
|
const title = b.error ? b.error.title : b.title;
|
|
5400
5819
|
const why = b.error ? (b.error.detail ?? "") : b.why;
|
|
5401
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "eg-b-top", children: [jsxRuntime.jsxs("span", { className: "eg-mis", children: ["\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08 ", b.missionNo, " \u00B7 ", b.missionTitle, b.replay ? " (ทบทวน)" : ""] }), jsxRuntime.jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", b.index + 1, "/", b.total] })] }), jsxRuntime.jsx("div", { className: "eg-pips", "aria-hidden": "true", children: Array.from({ length: b.total }, (_, i) => (jsxRuntime.jsx("i", { className: i < b.index ? "eg-d" : i === b.index ? "eg-c" : undefined }, i))) }), jsxRuntime.jsx("p", { className: "eg-b-t", id: BUBBLE_TITLE_ID, children: jsxRuntime.jsx(GuideTitle, { text: title }) }), jsxRuntime.jsx("p", { className: "eg-b-w", id: BUBBLE_WHY_ID, hidden: !why, children: why }), jsxRuntime.jsxs("div", { className: "eg-b-f", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: b.replay ? "จบการทบทวน" : "ออกจากไกด์" }), jsxRuntime.jsxs("span", { style: { display: "flex", gap: 6 }, children: [b.showEndWithoutSave ? (jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: p.onEndWithoutSave, children: "\u0E08\u0E1A\u0E42\u0E14\u0E22\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01" })) : null, b.showNext ? (jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onNext, children: "\u0E16\u0E31\u0E14\u0E44\u0E1B" })) : null] })] })] }));
|
|
5820
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "eg-b-top", children: [jsxRuntime.jsxs("span", { className: "eg-mis", children: ["\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08 ", b.missionNo, " \u00B7 ", b.missionTitle, b.replay ? " (ทบทวน)" : ""] }), jsxRuntime.jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", b.index + 1, "/", b.total] })] }), jsxRuntime.jsx("div", { className: "eg-pips", "aria-hidden": "true", children: Array.from({ length: b.total }, (_, i) => (jsxRuntime.jsx("i", { className: i < b.index ? "eg-d" : i === b.index ? "eg-c" : undefined }, i))) }), jsxRuntime.jsx("p", { className: "eg-b-t", id: BUBBLE_TITLE_ID, children: jsxRuntime.jsx(GuideTitle, { text: title }) }), jsxRuntime.jsx("p", { className: "eg-b-w", id: BUBBLE_WHY_ID, hidden: !why, children: jsxRuntime.jsx(GuideTitle, { text: why }) }), jsxRuntime.jsxs("div", { className: "eg-b-f", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: b.replay ? "จบการทบทวน" : "ออกจากไกด์" }), jsxRuntime.jsxs("span", { style: { display: "flex", gap: 6 }, children: [b.showEndWithoutSave ? (jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: p.onEndWithoutSave, children: "\u0E08\u0E1A\u0E42\u0E14\u0E22\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01" })) : null, b.showNext ? (jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onNext, children: "\u0E16\u0E31\u0E14\u0E44\u0E1B" })) : null] })] })] }));
|
|
5402
5821
|
}
|
|
5403
5822
|
/** The fingertip at ≈62% across and down the target; upside down near the bottom. */
|
|
5404
5823
|
function handPosition(spot, viewportHeight) {
|
|
@@ -5511,6 +5930,9 @@ function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
|
|
|
5511
5930
|
meta = jsxRuntime.jsx("span", { children: "\u0E02\u0E49\u0E32\u0E21\u0E44\u0E27\u0E49" });
|
|
5512
5931
|
else if (v === "waiting" && gate)
|
|
5513
5932
|
meta = gate.pending;
|
|
5933
|
+
// Locked: what to do first gets its own line under the title, so the title
|
|
5934
|
+
// keeps the row's width and the button stays short.
|
|
5935
|
+
const lockHint = v === "locked" && item.lockedBy ? `ทำ ${item.lockedBy} ก่อน` : null;
|
|
5514
5936
|
let button;
|
|
5515
5937
|
if (v === "done")
|
|
5516
5938
|
button = jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: () => onStart(item.key, true), children: "\u0E17\u0E33\u0E0B\u0E49\u0E33" });
|
|
@@ -5519,11 +5941,11 @@ function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
|
|
|
5519
5941
|
else if (v === "waiting" && gate)
|
|
5520
5942
|
button = (jsxRuntime.jsx("a", { className: "eg-btn eg-ghost", href: gate.href, style: { textDecoration: "none" }, children: gate.label }));
|
|
5521
5943
|
else if (v === "locked")
|
|
5522
|
-
button = (jsxRuntime.
|
|
5944
|
+
button = (jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", disabled: true, "aria-label": lockHint ? `เริ่ม (${lockHint})` : undefined, children: "\u0E40\u0E23\u0E34\u0E48\u0E21" }));
|
|
5523
5945
|
else
|
|
5524
5946
|
button = (jsxRuntime.jsx("button", { type: "button", className: "eg-btn " + (v === "next" ? "eg-primary" : "eg-ghost"), onClick: () => onStart(item.key, false), children: v === "skipped" || v === "active" ? "ทำต่อ" : "เริ่ม" }));
|
|
5525
5947
|
const dismissable = item.optional && (v === "todo" || v === "next");
|
|
5526
|
-
return (jsxRuntime.jsxs("li", { className: cls, children: [jsxRuntime.jsx("span", { className: "eg-n", children: ticked ? jsxRuntime.jsx(IconCheck, {}) : n }), jsxRuntime.jsxs("div", { className: "eg-t", children: [jsxRuntime.jsx("b", { children: item.title }), jsxRuntime.jsx("small", { className: v === "skipped" ? "eg-sk" : undefined, children: meta })] }), dismissable ? (jsxRuntime.jsxs("span", { className: "eg-q-acts", children: [button, jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-link", onClick: () => onDismiss(item.key), children: "\u0E44\u0E21\u0E48\u0E43\u0E0A\u0E49\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E19\u0E35\u0E49" })] })) : (button)] }));
|
|
5948
|
+
return (jsxRuntime.jsxs("li", { className: cls, children: [jsxRuntime.jsx("span", { className: "eg-n", children: ticked ? jsxRuntime.jsx(IconCheck, {}) : n }), jsxRuntime.jsxs("div", { className: "eg-t", children: [jsxRuntime.jsx("b", { children: item.title }), lockHint ? jsxRuntime.jsx("small", { className: "eg-lock", children: lockHint }) : null, jsxRuntime.jsx("small", { className: v === "skipped" ? "eg-sk" : undefined, children: meta })] }), dismissable ? (jsxRuntime.jsxs("span", { className: "eg-q-acts", children: [button, jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-link", onClick: () => onDismiss(item.key), children: "\u0E44\u0E21\u0E48\u0E43\u0E0A\u0E49\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E19\u0E35\u0E49" })] })) : (button)] }));
|
|
5527
5949
|
}
|
|
5528
5950
|
function GuideToast({ text }) {
|
|
5529
5951
|
return (jsxRuntime.jsx("div", { className: "eg-toast", role: "status", children: text }));
|
|
@@ -5666,6 +6088,45 @@ function usePrimaryTab(name) {
|
|
|
5666
6088
|
return primary;
|
|
5667
6089
|
}
|
|
5668
6090
|
/* ------------------------------------------------------------------ */
|
|
6091
|
+
/* Putting the page back after a replay */
|
|
6092
|
+
/* ------------------------------------------------------------------ */
|
|
6093
|
+
const ABORT_DELAY_MS = 60;
|
|
6094
|
+
const ABORT_GAP_MS = 150;
|
|
6095
|
+
/**
|
|
6096
|
+
* Runs a replay's abort actions in order, once the tour has let go of the
|
|
6097
|
+
* page: press an anchor (passed over when it is not there), emit an app
|
|
6098
|
+
* event, or Escape to the open dialog (Radix listens on the document).
|
|
6099
|
+
*/
|
|
6100
|
+
function runAbort(actions) {
|
|
6101
|
+
actions.forEach((a, i) => {
|
|
6102
|
+
setTimeout(() => {
|
|
6103
|
+
try {
|
|
6104
|
+
if ("click" in a) {
|
|
6105
|
+
findAnchor(a.click)?.click();
|
|
6106
|
+
}
|
|
6107
|
+
else if ("event" in a) {
|
|
6108
|
+
emitGuideEvent(a.event);
|
|
6109
|
+
}
|
|
6110
|
+
else if ("escape" in a) {
|
|
6111
|
+
const t = document.activeElement ?? document.body;
|
|
6112
|
+
t.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", bubbles: true, cancelable: true }));
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
catch {
|
|
6116
|
+
/* never breaks the app */
|
|
6117
|
+
}
|
|
6118
|
+
}, ABORT_DELAY_MS + i * ABORT_GAP_MS);
|
|
6119
|
+
});
|
|
6120
|
+
}
|
|
6121
|
+
/** A step's own target takes presses: not on a "view" step, unless the step allows it. */
|
|
6122
|
+
function targetPressable(step, bridge) {
|
|
6123
|
+
if (!step || bridge)
|
|
6124
|
+
return true;
|
|
6125
|
+
if (stepKind(step) !== "view")
|
|
6126
|
+
return true;
|
|
6127
|
+
return !!step.target && (step.allow ?? []).includes(step.target);
|
|
6128
|
+
}
|
|
6129
|
+
/* ------------------------------------------------------------------ */
|
|
5669
6130
|
/* Host */
|
|
5670
6131
|
/* ------------------------------------------------------------------ */
|
|
5671
6132
|
const TARGET_PAD = 6;
|
|
@@ -5711,6 +6172,9 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5711
6172
|
/* ---------------- machine ---------------- */
|
|
5712
6173
|
const [state, setState] = React.useState(null);
|
|
5713
6174
|
const stateRef = React.useRef(null);
|
|
6175
|
+
// What the server holds (payloadKey), so a save that would change nothing
|
|
6176
|
+
// is not sent and does not bump its rev.
|
|
6177
|
+
const savedKey = React.useRef(null);
|
|
5714
6178
|
React.useEffect(() => {
|
|
5715
6179
|
if (!answer || answer.audience === "none" || answer.newerOnServer) {
|
|
5716
6180
|
stateRef.current = null;
|
|
@@ -5719,6 +6183,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5719
6183
|
}
|
|
5720
6184
|
if (stateRef.current)
|
|
5721
6185
|
return;
|
|
6186
|
+
savedKey.current = answer.progress && answer.progress.rev > 0 ? payloadKey(answer.progress, suite.app) : null;
|
|
5722
6187
|
// Resume from whichever copy is further along: the server's, or this
|
|
5723
6188
|
// tab's session copy from before a refresh (its rev is the server's).
|
|
5724
6189
|
const mirror = readMirror(organizationId, suite);
|
|
@@ -5775,7 +6240,11 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5775
6240
|
const cur = stateRef.current;
|
|
5776
6241
|
if (!cur)
|
|
5777
6242
|
return;
|
|
6243
|
+
if (payloadKey(cur.progress, suite.app) === savedKey.current)
|
|
6244
|
+
continue;
|
|
5778
6245
|
const row = await saveGuides(organizationId, suite, cur.progress);
|
|
6246
|
+
if (row)
|
|
6247
|
+
savedKey.current = payloadKey(row, suite.app);
|
|
5779
6248
|
if (row && stateRef.current) {
|
|
5780
6249
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
5781
6250
|
dispatchRef.current({ type: "serverRow", progress: row });
|
|
@@ -5816,13 +6285,9 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5816
6285
|
else
|
|
5817
6286
|
router.push(e.href);
|
|
5818
6287
|
}
|
|
5819
|
-
else if (e.type === "
|
|
5820
|
-
// After this render takes the guard away,
|
|
5821
|
-
|
|
5822
|
-
setTimeout(() => {
|
|
5823
|
-
const t = document.activeElement ?? document.body;
|
|
5824
|
-
t.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", bubbles: true, cancelable: true }));
|
|
5825
|
-
}, 60);
|
|
6288
|
+
else if (e.type === "abort") {
|
|
6289
|
+
// After this render takes the guard away, put the page back.
|
|
6290
|
+
runAbort(e.actions);
|
|
5826
6291
|
}
|
|
5827
6292
|
}
|
|
5828
6293
|
catch {
|
|
@@ -5952,7 +6417,9 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5952
6417
|
scrolling, dialogs animating in, the drawer sliding, the keyboard. */
|
|
5953
6418
|
const [layout, setLayout] = React.useState({
|
|
5954
6419
|
spot: null,
|
|
6420
|
+
covered: false,
|
|
5955
6421
|
holes: [],
|
|
6422
|
+
popovers: [],
|
|
5956
6423
|
vw: 0,
|
|
5957
6424
|
vh: 0,
|
|
5958
6425
|
oy: 0,
|
|
@@ -5964,9 +6431,22 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5964
6431
|
}
|
|
5965
6432
|
let raf = 0;
|
|
5966
6433
|
let lostSince = null;
|
|
5967
|
-
let last = { spot: null, holes: [], vw: 0, vh: 0, oy: 0 };
|
|
6434
|
+
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0 };
|
|
5968
6435
|
targetElRef.current = target ? findAnchor(target.anchor, target.id) : null;
|
|
5969
6436
|
const allow = stepInfo?.step.allow ?? [];
|
|
6437
|
+
// The spotlight is the part of the target a person can see: cut by the
|
|
6438
|
+
// scroll boxes around it (a row of tabs that scrolls sideways) and the
|
|
6439
|
+
// viewport. Wholly out of view (scrolled away up or down), its own box,
|
|
6440
|
+
// and the tour scrolls it back.
|
|
6441
|
+
let clipFor = null;
|
|
6442
|
+
let clips = [];
|
|
6443
|
+
const spotOf = (el) => {
|
|
6444
|
+
if (el !== clipFor) {
|
|
6445
|
+
clipFor = el;
|
|
6446
|
+
clips = clipAncestors(el);
|
|
6447
|
+
}
|
|
6448
|
+
return padRect(visibleRectOf(el, clips) ?? rectOf(el), TARGET_PAD);
|
|
6449
|
+
};
|
|
5970
6450
|
const tick = () => {
|
|
5971
6451
|
raf = requestAnimationFrame(tick);
|
|
5972
6452
|
const vv = window.visualViewport;
|
|
@@ -5981,14 +6461,14 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5981
6461
|
let spot = null;
|
|
5982
6462
|
if (target && el && isPresent(el)) {
|
|
5983
6463
|
lostSince = null;
|
|
5984
|
-
spot =
|
|
6464
|
+
spot = spotOf(el);
|
|
5985
6465
|
}
|
|
5986
6466
|
else if (target) {
|
|
5987
6467
|
// Maybe re-rendered under another node.
|
|
5988
6468
|
const again = findAnchor(target.anchor, target.id);
|
|
5989
6469
|
if (again) {
|
|
5990
6470
|
targetElRef.current = again;
|
|
5991
|
-
spot =
|
|
6471
|
+
spot = spotOf(again);
|
|
5992
6472
|
lostSince = null;
|
|
5993
6473
|
}
|
|
5994
6474
|
else if (phase === "ready") {
|
|
@@ -6001,21 +6481,25 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6001
6481
|
}
|
|
6002
6482
|
if (phase === "waiting" && spot)
|
|
6003
6483
|
dispatchRef.current({ type: "sync" });
|
|
6004
|
-
const
|
|
6484
|
+
const popovers = [];
|
|
6005
6485
|
for (const p of openPoppers())
|
|
6006
|
-
|
|
6486
|
+
popovers.push(padRect(rectOf(p), 2));
|
|
6487
|
+
const holes = [];
|
|
6007
6488
|
for (const a of allow) {
|
|
6008
6489
|
const ael = findAnchor(a);
|
|
6009
6490
|
if (ael)
|
|
6010
6491
|
holes.push(padRect(rectOf(ael), TARGET_PAD));
|
|
6011
6492
|
}
|
|
6012
|
-
const
|
|
6493
|
+
const covered = !!spot && coveredByDialog(targetElRef.current);
|
|
6494
|
+
const next = { spot, covered, holes, popovers, vw, vh, oy };
|
|
6495
|
+
const sameList = (a, b) => a.length === b.length && a.every((h, i) => sameRect(h, b[i]));
|
|
6013
6496
|
if (!sameRect(next.spot, last.spot) ||
|
|
6497
|
+
next.covered !== last.covered ||
|
|
6014
6498
|
next.vw !== last.vw ||
|
|
6015
6499
|
next.vh !== last.vh ||
|
|
6016
6500
|
next.oy !== last.oy ||
|
|
6017
|
-
next.holes
|
|
6018
|
-
next.
|
|
6501
|
+
!sameList(next.holes, last.holes) ||
|
|
6502
|
+
!sameList(next.popovers, last.popovers)) {
|
|
6019
6503
|
last = next;
|
|
6020
6504
|
setLayout(next);
|
|
6021
6505
|
}
|
|
@@ -6146,7 +6630,10 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6146
6630
|
if (!(node instanceof Node))
|
|
6147
6631
|
return false;
|
|
6148
6632
|
const el = targetElRef.current;
|
|
6149
|
-
|
|
6633
|
+
// A "view" step's target is there to be read: ถัดไป is the way on.
|
|
6634
|
+
const sc = stateRef.current?.screen;
|
|
6635
|
+
const pressable = targetPressable(stepInfoRef.current?.step, sc?.name === "step" && sc.bridge);
|
|
6636
|
+
if (el && pressable && el.contains(node))
|
|
6150
6637
|
return true;
|
|
6151
6638
|
const elNode = node instanceof Element ? node : node.parentElement;
|
|
6152
6639
|
if (!elNode)
|
|
@@ -6245,7 +6732,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6245
6732
|
const el = targetElRef.current;
|
|
6246
6733
|
const step = stepInfoRef.current?.step;
|
|
6247
6734
|
const isInput = !!step && !sc.bridge && stepKind(step) === "input";
|
|
6248
|
-
const inTargetList = el ? focusablesIn(isInput ? (fieldOf(el) ?? el) : el) : [];
|
|
6735
|
+
const inTargetList = el && targetPressable(step, sc.bridge) ? focusablesIn(isInput ? (fieldOf(el) ?? el) : el) : [];
|
|
6249
6736
|
const bubble = layer?.querySelector("[data-eg-bubble]") ?? null;
|
|
6250
6737
|
list = [...inTargetList, ...(bubble ? Array.from(bubble.querySelectorAll(FOCUSABLE)) : [])];
|
|
6251
6738
|
for (const p of openPoppers())
|
|
@@ -6320,19 +6807,38 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6320
6807
|
React.useEffect(() => {
|
|
6321
6808
|
const el = document.createElement("div");
|
|
6322
6809
|
el.setAttribute(GUIDE_LAYER_ATTRIBUTE, "");
|
|
6810
|
+
// A Radix modal (dialog, select, menu) hides everything outside it from
|
|
6811
|
+
// screen readers with aria-hidden's hideOthers. It leaves [aria-live]
|
|
6812
|
+
// elements and their insides alone, but walks into their ancestors and
|
|
6813
|
+
// hides the siblings: with only the status line live, the bubble and its
|
|
6814
|
+
// buttons were hidden. "off" announces nothing; it only makes the whole
|
|
6815
|
+
// layer one of the kept elements.
|
|
6816
|
+
el.setAttribute("aria-live", "off");
|
|
6323
6817
|
document.body.appendChild(el);
|
|
6324
6818
|
layerRef.current = el;
|
|
6325
6819
|
setLayer(el);
|
|
6326
|
-
//
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6820
|
+
// And should anything still hide the layer or a part of it (another
|
|
6821
|
+
// library, a later hideOthers), take it off again. hideOthers marks what
|
|
6822
|
+
// it hides with data-aria-hidden (the layer never sets that, or inert);
|
|
6823
|
+
// aria-hidden goes only where it was just added, so the layer's own
|
|
6824
|
+
// decorative aria-hidden stays.
|
|
6825
|
+
const obs = new MutationObserver(records => {
|
|
6826
|
+
for (const r of records) {
|
|
6827
|
+
const n = r.target;
|
|
6828
|
+
if (!(n instanceof Element))
|
|
6829
|
+
continue;
|
|
6830
|
+
if (r.attributeName === "inert" && n.hasAttribute("inert"))
|
|
6831
|
+
n.removeAttribute("inert");
|
|
6832
|
+
else if (r.attributeName === "data-aria-hidden" && n.hasAttribute("data-aria-hidden"))
|
|
6833
|
+
n.removeAttribute("data-aria-hidden");
|
|
6834
|
+
else if (r.attributeName === "aria-hidden" &&
|
|
6835
|
+
n.getAttribute("aria-hidden") === "true" &&
|
|
6836
|
+
(n === el || r.oldValue === null)) {
|
|
6837
|
+
n.removeAttribute("aria-hidden");
|
|
6838
|
+
}
|
|
6839
|
+
}
|
|
6334
6840
|
});
|
|
6335
|
-
obs.observe(el, { attributes: true, attributeFilter: ["aria-hidden", "data-aria-hidden", "inert"] });
|
|
6841
|
+
obs.observe(el, { attributes: true, attributeOldValue: true, subtree: true, attributeFilter: ["aria-hidden", "data-aria-hidden", "inert"] });
|
|
6336
6842
|
return () => {
|
|
6337
6843
|
obs.disconnect();
|
|
6338
6844
|
el.remove();
|
|
@@ -6399,7 +6905,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6399
6905
|
const { mission, step, missionNo } = stepInfo;
|
|
6400
6906
|
const kind = stepKind(step);
|
|
6401
6907
|
const bridge = screen.bridge;
|
|
6402
|
-
content = (jsxRuntime.jsx(StepLayer, { spot: layout.spot, holes: layout.holes, viewport: { width: layout.vw || window.innerWidth, height: layout.vh || window.innerHeight, offsetTop: layout.oy }, phone: phone, showHand: bridge || (kind !== "input" && kind !== "view"), bubble: {
|
|
6908
|
+
content = (jsxRuntime.jsx(StepLayer, { spot: layout.spot, holes: layout.holes, popovers: layout.popovers, covered: layout.covered, viewport: { width: layout.vw || window.innerWidth, height: layout.vh || window.innerHeight, offsetTop: layout.oy }, phone: phone, showHand: bridge || (kind !== "input" && kind !== "view"), bubble: {
|
|
6403
6909
|
missionNo,
|
|
6404
6910
|
missionTitle: mission.title,
|
|
6405
6911
|
replay: screen.replay,
|
|
@@ -6410,7 +6916,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6410
6916
|
error: screen.error,
|
|
6411
6917
|
showNext: !bridge && (kind === "input" || kind === "view"),
|
|
6412
6918
|
showEndWithoutSave: !bridge && screen.replay && kind === "commit",
|
|
6413
|
-
}, hasTarget: bridge || !!step.target, phase: screen.phase, confirmExit: screen.confirmExit, nudge: state.nudge, moving: moving, onExit: () => dispatch({ type: "exit" }), onNext: () => dispatch({ type: "next" }), onEndWithoutSave: () => dispatch({ type: "endWithoutSave" }), onRetry: () => dispatch({ type: "retry" }), onGoResume: () => dispatch({ type: "goResume" }), onExitConfirm: () => dispatch({ type: "exitConfirm" }), onExitCancel: () => dispatch({ type: "exitCancel" }) }));
|
|
6919
|
+
}, hasTarget: bridge || !!step.target, targetPressable: targetPressable(step, bridge), phase: screen.phase, confirmExit: screen.confirmExit, nudge: state.nudge, moving: moving, onExit: () => dispatch({ type: "exit" }), onNext: () => dispatch({ type: "next" }), onEndWithoutSave: () => dispatch({ type: "endWithoutSave" }), onRetry: () => dispatch({ type: "retry" }), onGoResume: () => dispatch({ type: "goResume" }), onExitConfirm: () => dispatch({ type: "exitConfirm" }), onExitCancel: () => dispatch({ type: "exitCancel" }) }));
|
|
6414
6920
|
}
|
|
6415
6921
|
else if (screen.name === "celebrate" && celebrateShown) {
|
|
6416
6922
|
const mission = findMission(suite, screen.mission);
|