@jsenv/navi 0.29.116 → 0.29.117

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.
@@ -29628,7 +29628,11 @@ registerNaviCommand("--navi-void", (source) => {
29628
29628
  };
29629
29629
  });
29630
29630
 
29631
- registerNaviCommand("--navi-update", (source, event) => {
29631
+ // "--navi-update:smooth" asks the control to be SEEN moving to the value — a
29632
+ // wheel scrolls to it rather than swapping its digits — so a shortcut under
29633
+ // two wheels shows which one it changed, and by how much. The value itself is
29634
+ // set at once either way; only the drawing is concerned.
29635
+ registerNaviCommand("--navi-update", (source, event, { argument }) => {
29632
29636
  const target =
29633
29637
  resolveExplicitTarget(source) || resolveFirstParentControl(source);
29634
29638
  if (!target) {
@@ -29645,6 +29649,7 @@ registerNaviCommand("--navi-update", (source, event) => {
29645
29649
  const commandValue = resolveCommandValue(source, event);
29646
29650
  dispatchRequestSetUIState(target, commandValue, {
29647
29651
  event,
29652
+ behavior: argument,
29648
29653
  });
29649
29654
  },
29650
29655
  });
@@ -50058,8 +50063,12 @@ const SlideContainer = ({
50058
50063
  // after it — a step that has been answered is not a step to stay on.
50059
50064
  const done = (area, event) => {
50060
50065
  markAnswered(area);
50061
- if (!moveNext(event)) {
50062
- movePrevious(event);
50066
+ if (!moveNext(event, {
50067
+ released: true
50068
+ })) {
50069
+ movePrevious(event, {
50070
+ released: true
50071
+ });
50063
50072
  }
50064
50073
  };
50065
50074
 
@@ -50377,6 +50386,7 @@ const SlideContainer = ({
50377
50386
  forward,
50378
50387
  event,
50379
50388
  value,
50389
+ released,
50380
50390
  dx = 0,
50381
50391
  dy = 0
50382
50392
  } = {}) => {
@@ -50391,6 +50401,7 @@ const SlideContainer = ({
50391
50401
  forward,
50392
50402
  event,
50393
50403
  value,
50404
+ released,
50394
50405
  dx,
50395
50406
  dy
50396
50407
  });
@@ -50427,7 +50438,11 @@ const SlideContainer = ({
50427
50438
  // event dispatched by hand: a slide that holds on to the user holds them
50428
50439
  // whatever they press. Read off the slide being LEFT, because that is what
50429
50440
  // has a reason to keep them (an answer still missing, a step not taken).
50430
- if (currentElement?.hasAttribute(forward ? "data-prevent-nav-next" : "data-prevent-nav-previous")) {
50441
+ // `released` is the slide letting go itself (--navi-done, see Slide): the
50442
+ // one departure the forward hold does not apply to. Said here rather than
50443
+ // by dropping the attribute, which stays the render's to write — a hold
50444
+ // meant to stay (an explicit preventNavNext) is still there on the way back.
50445
+ if (forward ? !released && currentElement?.hasAttribute("data-prevent-nav-next") : currentElement?.hasAttribute("data-prevent-nav-previous")) {
50431
50446
  return false;
50432
50447
  }
50433
50448
  // The focus moves here, while the event that asked for it is still in hand:
@@ -50576,15 +50591,20 @@ const SlideContainer = ({
50576
50591
  dx,
50577
50592
  dy,
50578
50593
  event,
50579
- value
50594
+ value,
50595
+ released
50580
50596
  } = pendingRollsRef.current.shift();
50581
50597
  if (dx || dy) {
50582
- move(dx, dy, event, value);
50598
+ move(dx, dy, event, {
50599
+ value,
50600
+ released
50601
+ });
50583
50602
  return;
50584
50603
  }
50585
50604
  goToArea(area, {
50586
50605
  event,
50587
- value
50606
+ value,
50607
+ released
50588
50608
  });
50589
50609
  }, [noTravel]);
50590
50610
 
@@ -50703,10 +50723,14 @@ const SlideContainer = ({
50703
50723
  return area;
50704
50724
  }
50705
50725
  };
50706
- const move = (dx, dy, event, value) => goToArea(areaTowards(dx, dy), {
50726
+ const move = (dx, dy, event, {
50727
+ value,
50728
+ released
50729
+ } = {}) => goToArea(areaTowards(dx, dy), {
50707
50730
  forward: dx > 0 || dy > 0,
50708
50731
  event,
50709
50732
  value,
50733
+ released,
50710
50734
  dx,
50711
50735
  dy
50712
50736
  });
@@ -50716,8 +50740,8 @@ const SlideContainer = ({
50716
50740
  // slides are. On a map they mean the same thing, and fall back to the other
50717
50741
  // axis when there is nothing that way — a step onwards, however the screens
50718
50742
  // happen to be arranged.
50719
- const moveNext = event => vertical ? move(0, 1, event) || move(1, 0, event) : move(1, 0, event) || move(0, 1, event);
50720
- const movePrevious = event => vertical ? move(0, -1, event) || move(-1, 0, event) : move(-1, 0, event) || move(0, -1, event);
50743
+ const moveNext = (event, options) => vertical ? move(0, 1, event, options) || move(1, 0, event, options) : move(1, 0, event, options) || move(0, 1, event, options);
50744
+ const movePrevious = (event, options) => vertical ? move(0, -1, event, options) || move(-1, 0, event, options) : move(-1, 0, event, options) || move(0, -1, event, options);
50721
50745
 
50722
50746
  // Where the track is right now, as the gesture left it: the resting place of
50723
50747
  // the slide being dragged, plus what the pointer has pulled since.
@@ -51364,7 +51388,9 @@ const SlideContainer = ({
51364
51388
  // (a --navi-right command carries the click that ran it, that click its
51365
51389
  // own mousedown), and the focus transfer reads the whole chain to know
51366
51390
  // where the interaction started.
51367
- move(dx, dy, e, value);
51391
+ move(dx, dy, e, {
51392
+ value
51393
+ });
51368
51394
  }
51369
51395
  // By name rather than by direction (--navi-go-to-slide): the caller says
51370
51396
  // where, the map says nothing about it.
@@ -51524,11 +51550,6 @@ const Slide = ({
51524
51550
  ,
51525
51551
 
51526
51552
  onnavi_done: e => {
51527
- // Dropped imperatively rather than left to the re-render this
51528
- // schedules: moving on happens in this same handler, and the gate it
51529
- // goes through reads this attribute off the DOM (see goToArea) — a
51530
- // render is a microtask away, the move is not.
51531
- e.currentTarget.removeAttribute("data-prevent-nav-next");
51532
51553
  container?.done(slideArea);
51533
51554
  rest.onnavi_done?.(e);
51534
51555
  },
@@ -57182,7 +57203,12 @@ installImportMetaCssBuild(import.meta);const css$B = /* css */`
57182
57203
  --picker-popup-border-radius,
57183
57204
  var(--picker-border-radius)
57184
57205
  );
57185
- --dialog-border-width: var(--picker-dialog-border-width);
57206
+ /* Explicit fallback, for the same reason as --popover-background-color
57207
+ above: the dialog paints border-width from this var with no fallback
57208
+ of its own, so an unset dialogBorderWidth would leave the var
57209
+ guaranteed-invalid and border-width at its initial "medium" (3px).
57210
+ 0px is the dialog's own default. */
57211
+ --dialog-border-width: var(--picker-dialog-border-width, 0px);
57186
57212
  --dialog-border-color: var(--x-picker-border-color);
57187
57213
  /* The picker's own surface is not this one — see the popover branch,
57188
57214
  including why the fallback is spelled out. */
@@ -70558,7 +70584,7 @@ const WheelGroupContext = createContext(null);
70558
70584
  * @param {boolean} [props.glass] - Frost the neighbouring rows so the center reads as a clear "window" (iOS-picker style). Inherited from a WheelGroup.
70559
70585
  * @param {boolean} [props.frameBorder] - Line the center-window edges with a faint frame (off by default; independent of glass). Tune via --wheel-frame-color.
70560
70586
  * @param {boolean|number} [props.zoom] - Make the centered value stand out by size: the neighbours are drawn smaller, so a value grows as it slides into the window and shrinks as it leaves (the centered one keeps its natural size). `true` uses the default ratio (1.3); a number is the ratio itself (1.8 = the center reads 80% bigger than its neighbours). Inherited from a WheelGroup.
70561
- * @param {number} [props.glideSpeed=0.16] - Speed (px/ms) of the programmatic glide used by arrow keys, taps and the navi_scroll "smooth" behavior. Lower = slower, more visible transitions; ≈0.16 covers one 32px row in 200ms.
70587
+ * @param {number} [props.glideSpeed=0.16] - Speed (px/ms) of the programmatic glide used by arrow keys, taps, a value set with `--navi-update:smooth` and the navi_scroll "smooth" behavior. Lower = slower, more visible transitions; ≈0.16 covers one 32px row in 200ms.
70562
70588
  * @param {string} [props.type] - Informative value kind (e.g. "integer", "day"). Used only for rendering hints, like tabular figures for "integer".
70563
70589
  */
70564
70590
  const Wheel = props => {
@@ -71280,6 +71306,23 @@ function WheelUI(props) {
71280
71306
  // faster, so a second press mid-glide reads as accelerating, not restarting.
71281
71307
  const targetPosRef = useRef(null);
71282
71308
  const glideRef = useRef(null);
71309
+ // Who aimed the running glide. A value set from outside (`--navi-update:smooth`)
71310
+ // is already committed by whoever set it: the wheel only catches up with it,
71311
+ // and its arrival must not be reported as a settle — no navi_wheel_settle, no
71312
+ // action. A user input is the opposite: its arrival IS the settle. Written at
71313
+ // every motion start (settle, glideTo).
71314
+ const glideFromOutsideRef = useRef(false);
71315
+ // How the value that just arrived asked to be shown. A set request can say
71316
+ // `behavior: "smooth"` (what `--navi-update:smooth` puts on it); the wheel
71317
+ // then scrolls to the value instead of swapping the digits. Read off the
71318
+ // navi_ui_state_change the controller dispatches — the request may have
71319
+ // reached this wheel through a group distributing to its children, so it is
71320
+ // looked up along the event chain. Consumed by the sync effect below.
71321
+ const pendingBehaviorRef = useRef(null);
71322
+ const onUIStateChange = e => {
71323
+ const setRequest = findEvent(e, "navi_set_ui_state");
71324
+ pendingBehaviorRef.current = setRequest ? setRequest.detail.behavior : null;
71325
+ };
71283
71326
  // The glide loop is bound once (mount effect) but the speed can change live
71284
71327
  // (e.g. a demo control) — read it through a ref so it uses the latest.
71285
71328
  const glideSpeedRef = useRef(glideSpeed);
@@ -71470,11 +71513,11 @@ function WheelUI(props) {
71470
71513
  posRef.current = index * size;
71471
71514
  renderPos(vp);
71472
71515
  }
71473
- if (!interactive) {
71516
+ centeredIndexRef.current = index;
71517
+ if (!interactive || glideFromOutsideRef.current) {
71474
71518
  return;
71475
71519
  }
71476
71520
  const settleEvent = new CustomEvent("navi_wheel_settle");
71477
- centeredIndexRef.current = index;
71478
71521
  debugScroll(`settle: committed → ${trackedItemsRef.current[index].value}`);
71479
71522
  requestSelectValue(trackedItemsRef.current[index].value, settleEvent);
71480
71523
  // The value is now stable → commit the action. The wheel runs actionEvent
@@ -71557,13 +71600,21 @@ function WheelUI(props) {
71557
71600
  // Spring stiffness (fraction of remaining distance per ~frame). Scales with the
71558
71601
  // glide speed so slower = gentler chase; clamped so it never crawls or snaps.
71559
71602
  const glideSpringFactor = () => clampNumber(glideSpeedRef.current * 1.4, 0.06, 0.45);
71560
- const glideTo = (vp, target) => {
71603
+ const glideTo = (vp, target, {
71604
+ fromOutside = false
71605
+ } = {}) => {
71561
71606
  // A discrete glide overrides any fling momentum.
71562
71607
  if (momentumRef.current !== null) {
71563
71608
  cancelAnimationFrame(momentumRef.current);
71564
71609
  momentumRef.current = null;
71565
71610
  }
71566
71611
  targetPosRef.current = target;
71612
+ // A user input anywhere in the movement makes its arrival a user settle;
71613
+ // an outside value re-aiming a glide the user started never takes that
71614
+ // away from them (their tap still has to be answered with a settle).
71615
+ if (glideRef.current === null || !fromOutside) {
71616
+ glideFromOutsideRef.current = fromOutside;
71617
+ }
71567
71618
  if (glideRef.current === null) {
71568
71619
  debugScroll("glide: start");
71569
71620
  glideRef.current = requestAnimationFrame(() => glideStep(vp, performance.now()));
@@ -71579,6 +71630,7 @@ function WheelUI(props) {
71579
71630
  // overshoots only a handful of rows (a picker isn't a free-scrolling list).
71580
71631
  const settle = (vp, velocity) => {
71581
71632
  cancelAnim();
71633
+ glideFromOutsideRef.current = false;
71582
71634
  debugScroll(`settle: momentum start (v=${velocity}px/ms)`);
71583
71635
  // A drag fling: allow the full swipe velocity (see WHEEL_FLING_MAX_VELOCITY)
71584
71636
  // so a hard swipe carries across the list instead of being clipped to a few
@@ -71643,13 +71695,19 @@ function WheelUI(props) {
71643
71695
  glideTo(vp, target);
71644
71696
  };
71645
71697
 
71646
- // Center value `index` (external value / initial / keyboard / click). Smooth
71647
- // glides to the nearest copy (glideTargetFor) so a wrap goes the short way.
71698
+ // Center value `index` for a value the wheel did not choose itself (first
71699
+ // display, a value set from outside). "smooth" glides to the nearest copy
71700
+ // (glideTargetFor) so a wrap goes the short way, and arrives silently (see
71701
+ // glideFromOutsideRef); "auto" puts the row in place at once, stopping
71702
+ // whatever was moving so no glide lands on top of it a frame later.
71648
71703
  const centerOnIndex = (vp, index, behavior) => {
71649
71704
  const target = glideTargetFor(vp, index);
71650
71705
  if (behavior === "smooth") {
71651
- glideTo(vp, target);
71706
+ glideTo(vp, target, {
71707
+ fromOutside: true
71708
+ });
71652
71709
  } else {
71710
+ cancelAnim();
71653
71711
  setPos(vp, target);
71654
71712
  }
71655
71713
  };
@@ -71661,7 +71719,7 @@ function WheelUI(props) {
71661
71719
  const glideToIndex = (vp, index, event) => {
71662
71720
  centeredIndexRef.current = index;
71663
71721
  requestSelectValue(trackedItemsRef.current[index].value, event);
71664
- centerOnIndex(vp, index, "smooth");
71722
+ glideTo(vp, glideTargetFor(vp, index));
71665
71723
  };
71666
71724
  // Index we are heading to (the target, not the mid-glide visual center).
71667
71725
  const currentTargetIndex = vp => {
@@ -71690,14 +71748,12 @@ function WheelUI(props) {
71690
71748
  // Sync the center with the current value — used on first display and whenever
71691
71749
  // the controlled value changes from outside.
71692
71750
  const syncCenterToSelection = (viewportEl, behavior) => {
71693
- // A glide or momentum in flight (tap, arrow, fling) is already taking the
71694
- // wheel to the right row. This runs on every controlled-value re-render, which
71695
- // lags a frame behind our own centeredIndexRef so on rapid taps its guard
71696
- // below would miss and it would snap instantly mid-glide, leaving the wheel
71697
- // stuck off-center. Let the motion finish: commitSelection settles the value
71698
- // and the next render is a no-op. A genuine external change made during motion
71699
- // is honoured once it settles (centeredIndexRef then differs from selection).
71700
- if (glideRef.current !== null || momentumRef.current !== null) {
71751
+ // A fling in flight reports its own value at every row crossing and commits
71752
+ // on settle: the finger that threw it owns the wheel until then, and a value
71753
+ // arriving meanwhile is re-stated by the next crossing anyway. A glide is
71754
+ // different: its target is re-aimed below (glideTo never restarts the
71755
+ // loop), so a value set from outside mid-glide is where the wheel ends up.
71756
+ if (momentumRef.current !== null) {
71701
71757
  return;
71702
71758
  }
71703
71759
  if (trackedItemsRef.current.length === 0) {
@@ -71739,13 +71795,27 @@ function WheelUI(props) {
71739
71795
  };
71740
71796
  }, []);
71741
71797
 
71742
- // React to controlled value changes coming from outside.
71798
+ // React to controlled value changes coming from outside. The row is put in
71799
+ // place at once, unless the request asked for "smooth": the value is already
71800
+ // set for whoever reads it, and the wheel scrolls to it so the eye can follow
71801
+ // the change (a shortcut button under two wheels: which one moved, and by how
71802
+ // much). Never on the first centering (no target yet → nothing to glide from,
71803
+ // the displayed effect above handles it), nor under reduced motion.
71743
71804
  useLayoutEffect(() => {
71805
+ const requestedBehavior = pendingBehaviorRef.current;
71806
+ pendingBehaviorRef.current = null;
71744
71807
  const viewportEl = getViewport();
71745
71808
  if (!viewportEl || viewportEl.offsetParent === null) {
71746
71809
  return;
71747
71810
  }
71748
- syncCenterToSelection(viewportEl, "auto");
71811
+ let behavior = "auto";
71812
+ if (requestedBehavior === "smooth" && centeredIndexRef.current !== null) {
71813
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
71814
+ if (!prefersReducedMotion) {
71815
+ behavior = "smooth";
71816
+ }
71817
+ }
71818
+ syncCenterToSelection(viewportEl, behavior);
71749
71819
  });
71750
71820
  useWheelInteractions({
71751
71821
  ref,
@@ -71813,6 +71883,7 @@ function WheelUI(props) {
71813
71883
  children: [jsx(Box, {
71814
71884
  as: "input",
71815
71885
  ...controlHostProps,
71886
+ onnavi_ui_state_change: onUIStateChange,
71816
71887
  tabindex: -1,
71817
71888
  "aria-hidden": "true",
71818
71889
  className: "navi_wheel_input"