@eintrek/erp-shell 0.1.64 → 0.1.66
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 +15 -1
- package/dist/guide/guide-ui.d.ts +9 -1
- package/dist/guide/machine.d.ts +15 -3
- package/dist/guide/types.d.ts +35 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +253 -40
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +253 -40
- 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.66";
|
|
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 {};
|
|
@@ -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;
|
|
@@ -4284,6 +4362,17 @@ function apiFailureFor(step, sig) {
|
|
|
4284
4362
|
(!t.api.method || t.api.method.toUpperCase() === sig.method.toUpperCase()) &&
|
|
4285
4363
|
matchApiPath(t.api.path, sig.path));
|
|
4286
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
|
+
}
|
|
4287
4376
|
function reduceGuide(state, action, suite, env) {
|
|
4288
4377
|
const d = { state, effects: [] };
|
|
4289
4378
|
const s = state.screen;
|
|
@@ -4364,7 +4453,8 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4364
4453
|
const step = mission.steps[s.index];
|
|
4365
4454
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4366
4455
|
if (s.phase === "away") {
|
|
4367
|
-
|
|
4456
|
+
const where = placeRoute(mission.steps, s.index);
|
|
4457
|
+
if (!where || matchRoute(where, env.pathname, env.search, env.code)) {
|
|
4368
4458
|
// Arrived: run the step again from here (it may already be passed).
|
|
4369
4459
|
enterStep(d, suite, env, mission, s.index, { replay: s.replay, error: s.error });
|
|
4370
4460
|
}
|
|
@@ -4381,6 +4471,11 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4381
4471
|
break;
|
|
4382
4472
|
}
|
|
4383
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
|
+
}
|
|
4384
4479
|
if (armed !== s.armed)
|
|
4385
4480
|
setScreen(d, { ...s, armed });
|
|
4386
4481
|
const cur = d.state.screen;
|
|
@@ -4435,7 +4530,8 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4435
4530
|
: { title: COPY.saveFailed, detail: sig.message };
|
|
4436
4531
|
setScreen(d, { ...s, error });
|
|
4437
4532
|
d.effects.push({ type: "announce", text: [error.title, error.detail].filter(Boolean).join(". ") });
|
|
4438
|
-
|
|
4533
|
+
if (!s.replay)
|
|
4534
|
+
withProgress(d, { last_error: `api_${sig.status}:${step.key}`.slice(0, 128) });
|
|
4439
4535
|
}
|
|
4440
4536
|
break;
|
|
4441
4537
|
}
|
|
@@ -4480,6 +4576,10 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4480
4576
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4481
4577
|
if (stepTargetVisible(step, env, ctx, s.bridge))
|
|
4482
4578
|
break;
|
|
4579
|
+
if (!s.bridge && offRoute(mission.steps, s.index, env)) {
|
|
4580
|
+
recoverOffRoute(d, suite, env, false);
|
|
4581
|
+
break;
|
|
4582
|
+
}
|
|
4483
4583
|
const cp = checkpointAtOrBefore(mission.steps, s.index);
|
|
4484
4584
|
if (cp < s.index && !s.bridge) {
|
|
4485
4585
|
// A dialog or a menu closed under the step: back to what opens it.
|
|
@@ -4497,9 +4597,16 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4497
4597
|
if (!mission)
|
|
4498
4598
|
break;
|
|
4499
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
|
+
}
|
|
4500
4604
|
const anchor = s.bridge ? BRIDGE_TARGET : (step.target ?? step.key);
|
|
4501
|
-
|
|
4502
|
-
|
|
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) });
|
|
4503
4610
|
const misses = s.misses + 1;
|
|
4504
4611
|
const policy = step.onMissing ?? "retry";
|
|
4505
4612
|
const cp = checkpointAtOrBefore(mission.steps, s.index);
|
|
@@ -4526,12 +4633,19 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4526
4633
|
}
|
|
4527
4634
|
break;
|
|
4528
4635
|
}
|
|
4529
|
-
case "retry":
|
|
4530
|
-
if (s.name
|
|
4531
|
-
|
|
4532
|
-
|
|
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;
|
|
4533
4644
|
}
|
|
4645
|
+
setScreen(d, { ...s, phase: "waiting" });
|
|
4646
|
+
markReadyIfVisible(d, suite, env);
|
|
4534
4647
|
break;
|
|
4648
|
+
}
|
|
4535
4649
|
case "blocked":
|
|
4536
4650
|
if (s.name === "step")
|
|
4537
4651
|
d.state = { ...d.state, nudge: d.state.nudge + 1 };
|
|
@@ -4540,6 +4654,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4540
4654
|
if (s.name !== "step")
|
|
4541
4655
|
break;
|
|
4542
4656
|
if (s.replay) {
|
|
4657
|
+
pushAbort(d, suite, s);
|
|
4543
4658
|
setScreen(d, { name: "idle" });
|
|
4544
4659
|
d.effects.push({ type: "toast", text: COPY.replayEnded });
|
|
4545
4660
|
}
|
|
@@ -4560,8 +4675,9 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4560
4675
|
case "endWithoutSave":
|
|
4561
4676
|
if (s.name !== "step" || !s.replay)
|
|
4562
4677
|
break;
|
|
4678
|
+
pushAbort(d, suite, s);
|
|
4563
4679
|
setScreen(d, { name: "idle" });
|
|
4564
|
-
d.effects.push({ type: "
|
|
4680
|
+
d.effects.push({ type: "toast", text: COPY.replayEnded });
|
|
4565
4681
|
break;
|
|
4566
4682
|
case "goResume": {
|
|
4567
4683
|
if (s.name !== "step" || s.phase !== "away")
|
|
@@ -4569,10 +4685,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4569
4685
|
const mission = findMission(suite, s.mission);
|
|
4570
4686
|
if (!mission)
|
|
4571
4687
|
break;
|
|
4572
|
-
const
|
|
4573
|
-
const ctx = guideContext(env, d.state.progress.context);
|
|
4574
|
-
const href = safe(() => step.resumeUrl?.(ctx), undefined) ??
|
|
4575
|
-
(step.route ? fillRoute(step.route, { ...d.state.progress.context, code: env.code }) : null);
|
|
4688
|
+
const href = resumeHref(mission, s.index, d, env);
|
|
4576
4689
|
if (href)
|
|
4577
4690
|
d.effects.push({ type: "navigate", href });
|
|
4578
4691
|
break;
|
|
@@ -5182,14 +5295,57 @@ function revealPlan(rect, visible, viewportHeight) {
|
|
|
5182
5295
|
return null;
|
|
5183
5296
|
return { block: offVertically ? "center" : "nearest", inline: "nearest" };
|
|
5184
5297
|
}
|
|
5185
|
-
/**
|
|
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
|
+
*/
|
|
5186
5333
|
function revealTarget(el) {
|
|
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
|
+
}
|
|
5187
5343
|
const vh = window.visualViewport?.height ?? window.innerHeight;
|
|
5188
5344
|
const plan = revealPlan(rectOf(el), visibleRectOf(el), vh);
|
|
5189
5345
|
if (!plan)
|
|
5190
5346
|
return;
|
|
5191
5347
|
try {
|
|
5192
|
-
el.scrollIntoView({ ...plan, behavior
|
|
5348
|
+
el.scrollIntoView({ ...plan, behavior });
|
|
5193
5349
|
}
|
|
5194
5350
|
catch {
|
|
5195
5351
|
el.scrollIntoView();
|
|
@@ -5465,10 +5621,12 @@ const rectPath = (r) => `M${r.x} ${r.y}H${r.x + r.width}V${r.y + r.height}H${r.x
|
|
|
5465
5621
|
* rectangles that do not overlap (blockedRects). A press inside a hole
|
|
5466
5622
|
* reaches the page underneath; any other lands on the path (marked
|
|
5467
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.
|
|
5468
5626
|
*/
|
|
5469
|
-
function Scrim({ holes, width, height }) {
|
|
5627
|
+
function Scrim({ holes, open, width, height }) {
|
|
5470
5628
|
const maskId = "eg-scrim-mask-" + React__namespace.useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
5471
|
-
const block = blockedRects(holes, width, height).map(rectPath).join("");
|
|
5629
|
+
const block = blockedRects(open ?? holes, width, height).map(rectPath).join("");
|
|
5472
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": "" })] }));
|
|
5473
5631
|
}
|
|
5474
5632
|
/** Whether two rectangles overlap. */
|
|
@@ -5634,6 +5792,7 @@ function StepLayer(p) {
|
|
|
5634
5792
|
const popovers = p.popovers ?? [];
|
|
5635
5793
|
const covered = !!p.covered;
|
|
5636
5794
|
const holes = [...(spot && !covered ? [spot] : []), ...popovers, ...p.holes];
|
|
5795
|
+
const openHoles = p.targetPressable === false ? [...popovers, ...p.holes] : holes;
|
|
5637
5796
|
const ringShown = !!spot && p.phase === "ready" && !covered && !popovers.some(o => meets(o, spot));
|
|
5638
5797
|
const hand = spot && p.showHand && p.phase === "ready" && !covered && !popovers.length ? handPosition(spot, viewport.height) : null;
|
|
5639
5798
|
let arrowStyle;
|
|
@@ -5647,7 +5806,7 @@ function StepLayer(p) {
|
|
|
5647
5806
|
else
|
|
5648
5807
|
arrowStyle = { bottom: -7, left: pos.ax };
|
|
5649
5808
|
}
|
|
5650
|
-
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 && 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: {
|
|
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: {
|
|
5651
5810
|
left: pos?.left ?? 0,
|
|
5652
5811
|
top: pos?.top ?? 0,
|
|
5653
5812
|
width: phone ? viewport.width - 32 : undefined,
|
|
@@ -5929,6 +6088,45 @@ function usePrimaryTab(name) {
|
|
|
5929
6088
|
return primary;
|
|
5930
6089
|
}
|
|
5931
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
|
+
/* ------------------------------------------------------------------ */
|
|
5932
6130
|
/* Host */
|
|
5933
6131
|
/* ------------------------------------------------------------------ */
|
|
5934
6132
|
const TARGET_PAD = 6;
|
|
@@ -6087,13 +6285,9 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6087
6285
|
else
|
|
6088
6286
|
router.push(e.href);
|
|
6089
6287
|
}
|
|
6090
|
-
else if (e.type === "
|
|
6091
|
-
// After this render takes the guard away,
|
|
6092
|
-
|
|
6093
|
-
setTimeout(() => {
|
|
6094
|
-
const t = document.activeElement ?? document.body;
|
|
6095
|
-
t.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", bubbles: true, cancelable: true }));
|
|
6096
|
-
}, 60);
|
|
6288
|
+
else if (e.type === "abort") {
|
|
6289
|
+
// After this render takes the guard away, put the page back.
|
|
6290
|
+
runAbort(e.actions);
|
|
6097
6291
|
}
|
|
6098
6292
|
}
|
|
6099
6293
|
catch {
|
|
@@ -6436,7 +6630,10 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6436
6630
|
if (!(node instanceof Node))
|
|
6437
6631
|
return false;
|
|
6438
6632
|
const el = targetElRef.current;
|
|
6439
|
-
|
|
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))
|
|
6440
6637
|
return true;
|
|
6441
6638
|
const elNode = node instanceof Element ? node : node.parentElement;
|
|
6442
6639
|
if (!elNode)
|
|
@@ -6535,7 +6732,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6535
6732
|
const el = targetElRef.current;
|
|
6536
6733
|
const step = stepInfoRef.current?.step;
|
|
6537
6734
|
const isInput = !!step && !sc.bridge && stepKind(step) === "input";
|
|
6538
|
-
const inTargetList = el ? focusablesIn(isInput ? (fieldOf(el) ?? el) : el) : [];
|
|
6735
|
+
const inTargetList = el && targetPressable(step, sc.bridge) ? focusablesIn(isInput ? (fieldOf(el) ?? el) : el) : [];
|
|
6539
6736
|
const bubble = layer?.querySelector("[data-eg-bubble]") ?? null;
|
|
6540
6737
|
list = [...inTargetList, ...(bubble ? Array.from(bubble.querySelectorAll(FOCUSABLE)) : [])];
|
|
6541
6738
|
for (const p of openPoppers())
|
|
@@ -6613,16 +6810,32 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6613
6810
|
document.body.appendChild(el);
|
|
6614
6811
|
layerRef.current = el;
|
|
6615
6812
|
setLayer(el);
|
|
6616
|
-
// A Radix modal
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6813
|
+
// A Radix modal (dialog, select, menu) hides everything outside it from
|
|
6814
|
+
// screen readers with aria-hidden's hideOthers. It keeps [aria-live]
|
|
6815
|
+
// elements but walks into their ancestors and hides the other children:
|
|
6816
|
+
// with the status line live, the bubble and its buttons were hidden.
|
|
6817
|
+
// Take off what it adds, at once. hideOthers marks what it hides with
|
|
6818
|
+
// data-aria-hidden (the layer never sets that, or inert); aria-hidden goes
|
|
6819
|
+
// only where it was just added, so the layer's own decorative aria-hidden
|
|
6820
|
+
// stays. (aria-live="off" on the layer would make hideOthers skip it, but
|
|
6821
|
+
// it changed what test tools match by text.)
|
|
6822
|
+
const obs = new MutationObserver(records => {
|
|
6823
|
+
for (const r of records) {
|
|
6824
|
+
const n = r.target;
|
|
6825
|
+
if (!(n instanceof Element))
|
|
6826
|
+
continue;
|
|
6827
|
+
if (r.attributeName === "inert" && n.hasAttribute("inert"))
|
|
6828
|
+
n.removeAttribute("inert");
|
|
6829
|
+
else if (r.attributeName === "data-aria-hidden" && n.hasAttribute("data-aria-hidden"))
|
|
6830
|
+
n.removeAttribute("data-aria-hidden");
|
|
6831
|
+
else if (r.attributeName === "aria-hidden" &&
|
|
6832
|
+
n.getAttribute("aria-hidden") === "true" &&
|
|
6833
|
+
(n === el || r.oldValue === null)) {
|
|
6834
|
+
n.removeAttribute("aria-hidden");
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6624
6837
|
});
|
|
6625
|
-
obs.observe(el, { attributes: true, attributeFilter: ["aria-hidden", "data-aria-hidden", "inert"] });
|
|
6838
|
+
obs.observe(el, { attributes: true, attributeOldValue: true, subtree: true, attributeFilter: ["aria-hidden", "data-aria-hidden", "inert"] });
|
|
6626
6839
|
return () => {
|
|
6627
6840
|
obs.disconnect();
|
|
6628
6841
|
el.remove();
|
|
@@ -6700,7 +6913,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6700
6913
|
error: screen.error,
|
|
6701
6914
|
showNext: !bridge && (kind === "input" || kind === "view"),
|
|
6702
6915
|
showEndWithoutSave: !bridge && screen.replay && kind === "commit",
|
|
6703
|
-
}, 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" }) }));
|
|
6916
|
+
}, 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" }) }));
|
|
6704
6917
|
}
|
|
6705
6918
|
else if (screen.name === "celebrate" && celebrateShown) {
|
|
6706
6919
|
const mission = findMission(suite, screen.mission);
|