@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/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.64";
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
- withProgress(d, { last_error: `forbidden:${step.requires}`.slice(0, 128) });
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 away = !!step.route && !matchRoute(step.route, env.pathname, env.search, env.code);
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
- if (!step.route || matchRoute(step.route, env.pathname, env.search, env.code)) {
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
- withProgress(d, { last_error: `api_${sig.status}:${step.key}`.slice(0, 128) });
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
- withProgress(d, { last_error: `target_missing:${anchor}`.slice(0, 128) });
4502
- save(d);
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 === "step" && s.phase === "missing") {
4531
- setScreen(d, { ...s, phase: "waiting" });
4532
- markReadyIfVisible(d, suite, env);
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: "closeAppDialog" }, { type: "toast", text: COPY.replayEnded });
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 step = mission.steps[s.index];
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
- /** Scrolls the target into view (instant under reduced motion). */
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: prefersReducedMotion() ? "auto" : "smooth" });
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 === "closeAppDialog") {
6091
- // After this render takes the guard away, Escape to the app's
6092
- // dialog (Radix listens on the document).
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
- if (el && el.contains(node))
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 hides everything else from screen readers; not us.
6617
- const obs = new MutationObserver(() => {
6618
- if (el.getAttribute("aria-hidden") === "true")
6619
- el.removeAttribute("aria-hidden");
6620
- if (el.hasAttribute("data-aria-hidden"))
6621
- el.removeAttribute("data-aria-hidden");
6622
- if (el.hasAttribute("inert"))
6623
- el.removeAttribute("inert");
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);