@jsenv/navi 0.29.328 → 0.29.330

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.
@@ -23526,10 +23526,16 @@ const createRoutePattern = (
23526
23526
  // search params are updated. When buildMostPreciseUrl performs an ancestor
23527
23527
  // optimisation (e.g. "/map/isochrone/compare" → "/map/isochrone") it is trusted
23528
23528
  // as-is because the built pathname will differ from the route's own base pathname.
23529
- // Weak params are never inherited from their signal, but a url that already
23530
- // carries one keeps it: staying on the same screen while another param
23531
- // changes must not end the visit this param qualifies.
23532
- const carryOverWeakParams = (currentUrl, params) => {
23529
+ // A weak param is never inherited from its signal by a url built for a link
23530
+ // (see readSignalForUrlBuild). A url amended in place is another matter: the
23531
+ // route is matching, so the signal IS this visit's state, kept in step with
23532
+ // the address by every routing pass — and what it holds is carried over, so
23533
+ // that changing another param does not end the visit this one qualifies.
23534
+ // Read from the signal, not from the url being amended: several signals
23535
+ // written in one batch flush one url write at a time, and the routing pass
23536
+ // of the first would take the others for gone — and reset them — if the url
23537
+ // it wrote did not already carry what they hold.
23538
+ const carryOverWeakParams = (params) => {
23533
23539
  let paramsWithWeak = params;
23534
23540
  for (const connection of connections) {
23535
23541
  const { paramName } = connection;
@@ -23537,17 +23543,13 @@ const createRoutePattern = (
23537
23543
  continue;
23538
23544
  }
23539
23545
  const currentValue = connection.signal.peek();
23540
- if (currentValue === undefined) {
23541
- continue;
23542
- }
23543
- const currentParams = applyOn(currentUrl);
23544
- if (!currentParams || currentParams[paramName] === undefined) {
23546
+ if (connection.isDefaultValue(currentValue)) {
23545
23547
  continue;
23546
23548
  }
23547
23549
  if (paramsWithWeak === params) {
23548
23550
  paramsWithWeak = { ...params };
23549
23551
  }
23550
- paramsWithWeak[paramName] = currentParams[paramName];
23552
+ paramsWithWeak[paramName] = currentValue;
23551
23553
  }
23552
23554
  return paramsWithWeak;
23553
23555
  };
@@ -23583,7 +23585,7 @@ const createRoutePattern = (
23583
23585
  };
23584
23586
 
23585
23587
  const buildUrlPreservingPath = (currentUrl, params = {}) => {
23586
- params = carryOverWeakParams(currentUrl, params);
23588
+ params = carryOverWeakParams(params);
23587
23589
  if (currentUrl) {
23588
23590
  params = carryOverPathParams(currentUrl, params);
23589
23591
  }
@@ -31867,9 +31869,10 @@ registerNaviCommand("--navi-void", (source) => {
31867
31869
  });
31868
31870
 
31869
31871
  // "--navi-update:smooth" asks the control to be SEEN moving to the value — a
31870
- // wheel scrolls to it rather than swapping its digits — so a shortcut under
31871
- // two wheels shows which one it changed, and by how much. The value itself is
31872
- // set at once either way; only the drawing is concerned.
31872
+ // wheel scrolls to it rather than swapping its digits, a spin plays the travel
31873
+ // its chevron would play — so a shortcut under a control shows which value it
31874
+ // changed, and by how much. What that movement is, and whether the value
31875
+ // travels with it, belongs to the control: see control_value.md.
31873
31876
  registerNaviCommand("--navi-update", (source, event, { argument }) => {
31874
31877
  const target =
31875
31878
  resolveExplicitTarget(source) || resolveFirstParentControl(source);
@@ -35898,7 +35901,7 @@ const useUIStateController = (
35898
35901
  // reach until that popup opens. Letting it block interaction would make
35899
35902
  // a picker whose content is loading impossible to open at all.
35900
35903
  getInteractionBlockingControls: () => [],
35901
- onUIAction: (e, { skipCommand } = {}) => {
35904
+ onUIAction: (e, { skipCommand, skipBoundSignal } = {}) => {
35902
35905
  if (controlType === "button" && controller.controlHostProps.name) {
35903
35906
  const buttonName = controller.controlHostProps.name;
35904
35907
  const parentController = controller.parentUIStateController;
@@ -35924,7 +35927,9 @@ const useUIStateController = (
35924
35927
  }
35925
35928
  // Trigger uiAction/command side effects without changing UI state.
35926
35929
  const currentUIState = controller.uiState;
35927
- writeBoundSignal(currentUIState);
35930
+ if (!skipBoundSignal) {
35931
+ writeBoundSignal(currentUIState);
35932
+ }
35928
35933
  s.uiActionInternal?.(currentUIState, e);
35929
35934
  if (s.uiAction) {
35930
35935
  s.uiAction(currentUIState, e);
@@ -36146,7 +36151,15 @@ const useUIStateController = (
36146
36151
  // sync, but do NOT fire the command and do NOT notify the parent —
36147
36152
  // both would cause an infinite loop when a parent cascades state
36148
36153
  // down to its children (child command would re-trigger the cascade).
36149
- controller.onUIAction(e, { skipCommand: true });
36154
+ // A control following what it was given (state_prop_change) does
36155
+ // not write its bound signal either: the signal is what it
36156
+ // follows. The one write that would change it is an emptied
36157
+ // signal refilled with the suggestion the control fell back on —
36158
+ // a state the app just took back, put back by the control.
36159
+ controller.onUIAction(e, {
36160
+ skipCommand: true,
36161
+ skipBoundSignal: e.type === "state_prop_change",
36162
+ });
36150
36163
  }
36151
36164
  if (
36152
36165
  e.type === "facade_propagate_up" ||
@@ -62736,11 +62749,7 @@ const PickerNative = props => {
62736
62749
  e.preventDefault();
62737
62750
  },
62738
62751
  allowed: () => {
62739
- try {
62740
- pickerInput.showPicker();
62741
- } catch {
62742
- pickerInput.click();
62743
- }
62752
+ showNativePicker(pickerInput, e);
62744
62753
  }
62745
62754
  });
62746
62755
  },
@@ -62756,11 +62765,7 @@ const PickerNative = props => {
62756
62765
  const pickerInput = getPickerInput(pickerEl);
62757
62766
  if (pickerInput.type === "color") ; else {
62758
62767
  // other picker might not open the picker when clicking the input surface (only the calendar picker for instance would open)
62759
- try {
62760
- pickerInput.showPicker();
62761
- } catch {
62762
- pickerInput.click();
62763
- }
62768
+ showNativePicker(pickerInput, e);
62764
62769
  }
62765
62770
  }
62766
62771
  };
@@ -63645,6 +63650,32 @@ const PickerCalloutPopup = ({
63645
63650
  );
63646
63651
  };
63647
63652
 
63653
+ // Under a finger the input is focused before the browser's picker is asked
63654
+ // for: iOS presents a date/time input's picker as that input's own inputView,
63655
+ // so what the tap focuses is what opens. showPicker() presents nothing there
63656
+ // and throws nothing either, so the catch below cannot be what learns about
63657
+ // it — and a synthetic click() does not focus a date input there.
63658
+ // A departure from the coarse-pointer policy (see focus_transfer.js), argued
63659
+ // here: the press asked for this input's picker, and the types whose focus
63660
+ // would raise the on-screen keyboard instead are held readOnly already (see
63661
+ // NON_MOBILE_KEYBOARD_TYPES in picker.jsx). No ring: nobody is about to type.
63662
+ // Not with a mouse, where showPicker() presents the picker on its own: the
63663
+ // keyboard stays where it was — a spin's middle, the button that sent the
63664
+ // command — so one keeps going with the keys once the calendar closes, rather
63665
+ // than on an input drawn behind (see the headless variant in picker.jsx).
63666
+ const showNativePicker = (pickerInput, event) => {
63667
+ if (findEvent(event, isTouchDrivenEvent)) {
63668
+ moveFocusTo(pickerInput, {
63669
+ focusVisible: false
63670
+ });
63671
+ }
63672
+ try {
63673
+ pickerInput.showPicker();
63674
+ } catch {
63675
+ pickerInput.click();
63676
+ }
63677
+ };
63678
+
63648
63679
  installImportMetaCssBuild(import.meta);
63649
63680
  const css$A = /* css */`.navi_picker_confirm_body {
63650
63681
  min-width: 180px;
@@ -72954,6 +72985,10 @@ const css$s = /* css */`@layer navi {
72954
72985
  }
72955
72986
  `;
72956
72987
 
72988
+ // No travel waiting to be played. One array rather than a new one per render,
72989
+ // so "nothing pending" compares equal to itself.
72990
+ const TRAVELS_NONE = [];
72991
+
72957
72992
  /**
72958
72993
  * @type {import("ignore:preact").FunctionComponent<{
72959
72994
  * value?: any,
@@ -73078,8 +73113,11 @@ const Spin = ({
73078
73113
  // elsewhere on the page — and the control follows it. A control seeded from a
73079
73114
  // signal only writes back into it (see resolveInputProps), which is enough
73080
73115
  // for a field one only ever types into and not for a value that is also moved
73081
- // from outside. Undefined is not a value: the signal has nothing to say, so
73082
- // the control goes back to what it started on.
73116
+ // from outside. A signal holding nothing is left alone: the control goes back
73117
+ // to the suggestion it started on by itself (see resolveValueState), and
73118
+ // asking it to reset would be an act of its own, written into the signal —
73119
+ // the signal could then never hold nothing while a spin is bound to it, and a
73120
+ // day the app took back would come back as today.
73083
73121
  const signalValue = signalProp ? signalProp.value : undefined;
73084
73122
  // The value as of the render this effect belongs to, and not one the closure
73085
73123
  // captured a while ago: a step writes the signal, the signal brings us back
@@ -73093,7 +73131,6 @@ const Spin = ({
73093
73131
  return;
73094
73132
  }
73095
73133
  if (signalValue === undefined) {
73096
- dispatchRequestResetUIState(controlEl);
73097
73134
  return;
73098
73135
  }
73099
73136
  if (signalValue !== valueRef.current) {
@@ -73210,6 +73247,126 @@ const Spin = ({
73210
73247
  const valueAtEnd = startIsNext ? valuePrevious : valueNext;
73211
73248
  const startAllowed = startIsNext ? nextAllowed : previousAllowed;
73212
73249
  const endAllowed = startIsNext ? previousAllowed : nextAllowed;
73250
+
73251
+ // A value asked to be SEEN arriving (`--navi-update:smooth`): the spin plays
73252
+ // the travel a chevron would play and lets the value land with the slides, so
73253
+ // a shortcut under it reads as "go to that day" rather than "be that day".
73254
+ // Whatever the distance it is ONE travel — a spin is not a wheel, and three
73255
+ // weeks are read as "it moved forward" rather than as twenty-one slides — so
73256
+ // the slide arriving is the one that shows the value being travelled to.
73257
+ //
73258
+ // Queued rather than held one at a time: two shortcuts pressed in a row are
73259
+ // two travels, each landing on its own value, and the one being played is
73260
+ // always the head. And started from a layout effect rather than from the
73261
+ // request itself, so the arriving slide is written before the container
73262
+ // places it — started there, the first frame would show the neighbouring
73263
+ // value and swap to the target mid-flight.
73264
+ const [travelsPending, setTravelsPendingState] = useState(TRAVELS_NONE);
73265
+ const travelsPendingRef = useRef(TRAVELS_NONE);
73266
+ const travelPending = travelsPending[0];
73267
+ const setTravelsPending = travels => {
73268
+ travelsPendingRef.current = travels.length ? travels : TRAVELS_NONE;
73269
+ setTravelsPendingState(travelsPendingRef.current);
73270
+ };
73271
+ const onRequestSetUIStateRef = useRef();
73272
+ onRequestSetUIStateRef.current = e => {
73273
+ if (editable) {
73274
+ // Nothing travels around a field: the value is typed into the middle.
73275
+ return;
73276
+ }
73277
+ const {
73278
+ behavior,
73279
+ value: valueRequested
73280
+ } = e.detail;
73281
+ if (behavior !== "smooth") {
73282
+ return;
73283
+ }
73284
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
73285
+ // Whoever asked to see less motion is answered first: the value is swapped
73286
+ // in place, which is what `--navi-update` alone does.
73287
+ return;
73288
+ }
73289
+ if (valueRequested === undefined || valueRequested === null || valueRequested === "") {
73290
+ // Nowhere to travel to — the erase a calendar offers, say.
73291
+ return;
73292
+ }
73293
+ const travelsQueued = travelsPendingRef.current;
73294
+ // Where the spin stands when this travel starts: behind the ones already
73295
+ // queued, rather than where the eye is now.
73296
+ const valueFrom = travelsQueued.length ? travelsQueued[travelsQueued.length - 1].value : valueShown;
73297
+ const direction = compareValues(valueRequested, valueFrom);
73298
+ if (direction === 0) {
73299
+ return;
73300
+ }
73301
+ const goingNext = direction > 0;
73302
+ // Out of reach, in both the ways a value can be: past the end of what one
73303
+ // may reach, or one step past it — the first step of the travel is the very
73304
+ // one the chevron that way is refused.
73305
+ const outOfReach = goingNext ? hasMax && (compareValues(valueRequested, max) > 0 || compareValues(valueAtStep(valueFrom, step), max) > 0) : hasMin && (compareValues(valueRequested, min) < 0 || compareValues(valueAtStep(valueFrom, -step), min) < 0);
73306
+ if (outOfReach) {
73307
+ // The same refusal a chevron gives, in the same words — said at the value
73308
+ // rather than at the chevron that way, because the press was not on a
73309
+ // chevron: what was asked about is the value, and that is where the
73310
+ // answer belongs.
73311
+ e.preventDefault();
73312
+ e.stopPropagation();
73313
+ openCallout(naviI18n(goingNext ? "spin.nothing_after" : "spin.nothing_before"), {
73314
+ anchorElement: middleRef.current,
73315
+ status: "info",
73316
+ openingEvent: e.detail.event ?? e
73317
+ });
73318
+ return;
73319
+ }
73320
+ // Taken here and answered when the slides land (see onLoop): the control
73321
+ // must not be given the value on the way past, or there would be nothing
73322
+ // left to arrive.
73323
+ e.stopPropagation();
73324
+ // A direction on the map, the way a chevron says it (see wayOut): the
73325
+ // slides are laid out from start to end, so which value sits where is what
73326
+ // decides the way, not which value is bigger.
73327
+ const atStart = goingNext ? startIsNext : !startIsNext;
73328
+ const towardsEnd = atStart ? -1 : 1;
73329
+ setTravelsPending([...travelsQueued, {
73330
+ value: valueRequested,
73331
+ area: atStart ? "start" : "end",
73332
+ dx: vertical ? 0 : towardsEnd,
73333
+ dy: vertical ? towardsEnd : 0,
73334
+ event: e.detail.event ?? e
73335
+ }]);
73336
+ };
73337
+ useLayoutEffect(() => {
73338
+ const middleEl = middleRef.current;
73339
+ const onRequestSetUIState = e => {
73340
+ onRequestSetUIStateRef.current(e);
73341
+ };
73342
+ // On the way DOWN, and on the box holding the control rather than on the
73343
+ // control: the request is dispatched AT the control host, where the
73344
+ // controller is already listening, and the spin has to be able to take it
73345
+ // before the value is set.
73346
+ middleEl.addEventListener("navi_set_ui_state", onRequestSetUIState, true);
73347
+ return () => {
73348
+ middleEl.removeEventListener("navi_set_ui_state", onRequestSetUIState, true);
73349
+ };
73350
+ }, []);
73351
+ useLayoutEffect(() => {
73352
+ if (!travelPending) {
73353
+ return;
73354
+ }
73355
+ // What `--navi-left` and friends ask the container for (see
73356
+ // registerSlideCommand): asked directly because the container already
73357
+ // carries a `commandfor` of its own — pressing the middle opens the picker
73358
+ // — so a command triggered from it would be about the picker.
73359
+ dispatchCustomEvent(document.getElementById(containerId), "navi_slide_move", {
73360
+ event: travelPending.event,
73361
+ dx: travelPending.dx,
73362
+ dy: travelPending.dy
73363
+ });
73364
+ }, [travelPending]);
73365
+ // What the slide the travel is heading for shows: the value being travelled
73366
+ // TO, not the neighbour that place holds at rest — one travel covers the
73367
+ // whole distance, so the far value is what arrives.
73368
+ const valueDrawnAtStart = travelPending?.area === "start" ? travelPending.value : valueAtStart;
73369
+ const valueDrawnAtEnd = travelPending?.area === "end" ? travelPending.value : valueAtEnd;
73213
73370
  const wayOut = atStart => {
73214
73371
  const isNext = atStart ? startIsNext : !startIsNext;
73215
73372
  return jsx(WayOut, {
@@ -73323,6 +73480,17 @@ const Spin = ({
73323
73480
  dy,
73324
73481
  event
73325
73482
  }) => {
73483
+ // A travel asked for by a VALUE (`--navi-update:smooth`):
73484
+ // that value is what lands, whatever the distance it covered,
73485
+ // and the press that asked for it is what says so to uiAction.
73486
+ // Recognized by that press: a chevron travel hurrying this one
73487
+ // along lands first and is a step of its own.
73488
+ const travelLanding = travelsPendingRef.current[0];
73489
+ if (travelLanding && event?.detail?.event === travelLanding.event) {
73490
+ setTravelsPending(travelsPendingRef.current.slice(1));
73491
+ setValue(travelLanding.value, travelLanding.event);
73492
+ return;
73493
+ }
73326
73494
  // One step, whichever axis it came from: the map is a line, so
73327
73495
  // only one of the two is ever anything but zero. Towards the
73328
73496
  // end of the line is a step forward, unless the line was laid
@@ -73355,7 +73523,7 @@ const Spin = ({
73355
73523
  area: "start",
73356
73524
  flex: true,
73357
73525
  align: "center",
73358
- children: renderValue(valueAtStart, {
73526
+ children: renderValue(valueDrawnAtStart, {
73359
73527
  maxLines
73360
73528
  })
73361
73529
  }), jsx(Slide, {
@@ -73375,7 +73543,7 @@ const Spin = ({
73375
73543
  area: "end",
73376
73544
  flex: true,
73377
73545
  align: "center",
73378
- children: renderValue(valueAtEnd, {
73546
+ children: renderValue(valueDrawnAtEnd, {
73379
73547
  maxLines
73380
73548
  })
73381
73549
  })]