@jsenv/navi 0.29.142 → 0.29.143

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.
@@ -16349,6 +16349,14 @@ const generateSignalId = () => {
16349
16349
  * value, and it goes back to the default value when the route stops matching. Use it for params like an
16350
16350
  * "edit this one" id, where every other link to the screen must lead to the plain screen.
16351
16351
  * Incompatible with `persists` (throws).
16352
+ * @param {"replace"|"push"} [options.history="replace"] - what writing this state
16353
+ * is worth in the browser's history, when it is bound to a url (a route's
16354
+ * `searchParams`, a path param). Replacement by default: a state that
16355
+ * qualifies the screen one is on — a step in a form, a view mode, a sort — is
16356
+ * not a place, and one entry per write turns a single back-press into as many
16357
+ * as the user moved. `"push"` is for a state whose values ARE places one came
16358
+ * from (the photo one is looking at in a gallery). Whatever is said here, one
16359
+ * write can say otherwise: `signal.set(value, { history })`.
16352
16360
  * @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
16353
16361
  * @returns {import("@preact/signals").Signal} A signal that can be synchronized with a source signal and/or persisted in localStorage. The signal includes a `validity` property for validation state.
16354
16362
  *
@@ -16422,8 +16430,14 @@ const stateSignal = (defaultValue, options = {}) => {
16422
16430
  ignoreArrayOrder,
16423
16431
  autoFix,
16424
16432
  weak = false,
16433
+ history = "replace",
16425
16434
  } = options;
16426
16435
 
16436
+ if (history !== "replace" && history !== "push") {
16437
+ throw new TypeError(
16438
+ `stateSignal "${id}": history must be "replace" or "push", got ${history}.`,
16439
+ );
16440
+ }
16427
16441
  if (weak && persists) {
16428
16442
  throw new TypeError(
16429
16443
  `stateSignal "${id}": weak and persists are contradictory — a weak param qualifies one visit, it cannot be restored from a previous session.`,
@@ -16600,7 +16614,35 @@ const stateSignal = (defaultValue, options = {}) => {
16600
16614
  configurable: true,
16601
16615
  });
16602
16616
 
16617
+ // What the write being made right now asks of the history, over what this
16618
+ // state declares. It lives for the length of one assignment: the effect that
16619
+ // writes the url runs synchronously inside it (see route.js), and reads it
16620
+ // there. A write made inside batch() is therefore out of its reach — say the
16621
+ // history on the state itself for those.
16622
+ let historyForCurrentWrite;
16603
16623
  const facadeSignal = preactSignal;
16624
+ /**
16625
+ * Write the state, saying what this ONE write is worth in the history.
16626
+ *
16627
+ * The state says which of the two it usually is (`history` above); a writer
16628
+ * that knows THIS move is not one of them says so here — a slide reached by
16629
+ * dragging inside a container whose slides are places one came from.
16630
+ *
16631
+ * @param {any} value
16632
+ * @param {{history?: "replace"|"push"}} [options]
16633
+ */
16634
+ facadeSignal.set = (value, { history: historyAsked } = {}) => {
16635
+ if (historyAsked === undefined) {
16636
+ facadeSignal.value = value;
16637
+ return;
16638
+ }
16639
+ historyForCurrentWrite = historyAsked;
16640
+ try {
16641
+ facadeSignal.value = value;
16642
+ } finally {
16643
+ historyForCurrentWrite = undefined;
16644
+ }
16645
+ };
16604
16646
  facadeSignal.validity = validity;
16605
16647
  facadeSignal.validSignal = computed(() => {
16606
16648
  // Reading facadeSignal.value establishes the reactive dependency.
@@ -16745,6 +16787,8 @@ const stateSignal = (defaultValue, options = {}) => {
16745
16787
  localStorageKey,
16746
16788
  debug,
16747
16789
  ...options,
16790
+ history,
16791
+ getHistory: () => historyForCurrentWrite || history,
16748
16792
  };
16749
16793
  globalSignalRegistry.set(signalIdString, {
16750
16794
  signal: facadeSignal,
@@ -18012,9 +18056,20 @@ const POSITION_PROPS = {
18012
18056
  return { transform: `skew(${value})` };
18013
18057
  },
18014
18058
  };
18059
+ // Only the inline axis has something to truncate here, so the clip must not
18060
+ // constrain the block one: `hidden` would make the element a scroll container,
18061
+ // whose flex automatic minimum size is 0, and a single line inside a column
18062
+ // flex container shorter than 1lh then shrinks under its own line box and gets
18063
+ // cut through the glyphs. `clip` keeps the content-based minimum, so the line
18064
+ // overflows visibly instead. The margin leaves room for ink drawn outside the
18065
+ // advance width; it starts from the padding box, having no hidden line to hold
18066
+ // back the way lineClampStyles below does.
18015
18067
  const singleLineEllipsisStyles = () => {
18016
18068
  return {
18017
- overflow: "hidden",
18069
+ overflow: "clip",
18070
+ // The `padding-box` keyword is redundant (it is the default box) but
18071
+ // Chromium drops the declaration when a bare calc() follows the property.
18072
+ overflowClipMargin: "padding-box calc((1lh - 1em) / 2)",
18018
18073
  textOverflow: "ellipsis",
18019
18074
  overflowWrap: "normal",
18020
18075
  };
@@ -24540,7 +24595,15 @@ let isUpdatingRoutesFromUrl = false;
24540
24595
  * `:name=${signal}` binds that param to a signal.
24541
24596
  * @param {object} [options]
24542
24597
  * @param {Object<string, import("@preact/signals").Signal>} [options.searchParams]
24543
- * Search params this route two-way syncs with, by name.
24598
+ * Search params this route two-way syncs with, by name. The signal and the url
24599
+ * are the same state: writing the signal rewrites the address, an address
24600
+ * arriving from outside writes the signal, and the param disappears from the
24601
+ * url while the signal holds its default.
24602
+ * Writing one AMENDS the history entry one is on — a param qualifies the
24603
+ * screen, it is not a place, and one entry per write turns a single
24604
+ * back-press into as many as the user moved. A state whose values ARE places
24605
+ * says so where it is declared (`stateSignal(v, { history: "push" })`), and
24606
+ * one write can still say otherwise (`signal.set(v, { history })`).
24544
24607
  * @param {Object<string, RegExp | Array | ((value: string) => boolean)>} [options.params]
24545
24608
  * Which values a path param accepts, by param name: a regexp tested against the
24546
24609
  * decoded segment, the list of accepted values (compared as strings, so it can
@@ -24711,17 +24774,20 @@ const route = (
24711
24774
  const routeUrl = route.buildUrl(params);
24712
24775
  return integration.navTo(routeUrl, options);
24713
24776
  };
24714
- route.redirectTo = (params, { callReason } = {}) => {
24777
+ route.redirectTo = (params, { callReason, history = "replace" } = {}) => {
24715
24778
  if (!integration) {
24716
24779
  return Promise.resolve();
24717
24780
  }
24718
24781
  const routeUrl = route.buildUrl(params);
24719
24782
  return integration.navTo(routeUrl, {
24720
- replace: true,
24783
+ replace: history !== "push",
24721
24784
  callReason,
24722
24785
  });
24723
24786
  };
24724
- route.replaceParams = (newParams, { callReason, isSignalChange } = {}) => {
24787
+ route.replaceParams = (
24788
+ newParams,
24789
+ { callReason, isSignalChange, history = "replace" } = {},
24790
+ ) => {
24725
24791
  const matching = route.matchingSignal.peek();
24726
24792
  if (!matching) {
24727
24793
  console.warn(
@@ -24774,6 +24840,7 @@ const route = (
24774
24840
  }
24775
24841
  return mostSpecificRoute.redirectTo(newParams, {
24776
24842
  callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
24843
+ history,
24777
24844
  });
24778
24845
  }
24779
24846
 
@@ -24789,7 +24856,7 @@ const route = (
24789
24856
  newParams,
24790
24857
  );
24791
24858
  return integration.navTo(targetUrl, {
24792
- replace: true,
24859
+ replace: history !== "push",
24793
24860
  callReason,
24794
24861
  });
24795
24862
  };
@@ -24860,6 +24927,13 @@ const route = (
24860
24927
  const { connections } = routePattern;
24861
24928
  for (const connection of connections) {
24862
24929
  const { signal: paramSignal, debug, paramName } = connection;
24930
+ // What a write of this state is worth in the history: what the state
24931
+ // declares, and what the write happening right now says instead (see
24932
+ // stateSignal's `history` and its `set`). Read at the moment of the
24933
+ // navigation and never before. A plain signal says nothing and replaces,
24934
+ // which is what a param qualifying a screen is.
24935
+ const historyOfWrite = () =>
24936
+ paramSignal.options?.getHistory?.() || "replace";
24863
24937
  if (debug) {
24864
24938
  console.debug(
24865
24939
  `[route] connecting url param "${paramName}" to signal`,
@@ -24902,6 +24976,7 @@ const route = (
24902
24976
  {
24903
24977
  callReason: `${paramName} signal change on ${route}`,
24904
24978
  isSignalChange: true,
24979
+ history: historyOfWrite(),
24905
24980
  },
24906
24981
  );
24907
24982
  return;
@@ -24919,6 +24994,7 @@ const route = (
24919
24994
  {
24920
24995
  callReason: `${paramName} signal reset to default on ${route}`,
24921
24996
  isSignalChange: true,
24997
+ history: historyOfWrite(),
24922
24998
  },
24923
24999
  );
24924
25000
  return;
@@ -24938,6 +25014,7 @@ const route = (
24938
25014
  {
24939
25015
  callReason: `${paramName} signal change on ${route}`,
24940
25016
  isSignalChange: true,
25017
+ history: historyOfWrite(),
24941
25018
  },
24942
25019
  );
24943
25020
  });
@@ -39015,8 +39092,17 @@ const css$X = /* css */`
39015
39092
  display: block;
39016
39093
  min-width: 0;
39017
39094
  text-overflow: ellipsis;
39018
- overflow: hidden;
39095
+ /* Only the inline axis has something to truncate, so the clip must not
39096
+ constrain the block one: overflow hidden would make the element a
39097
+ scroll container, whose flex automatic minimum size is 0, and inside a
39098
+ column flex container shorter than 1lh the text then shrinks under its
39099
+ own line box and gets cut through the glyphs. The margin leaves room
39100
+ for ink drawn outside the advance width. */
39101
+ overflow: clip;
39019
39102
  overflow-wrap: normal;
39103
+ /* The padding-box keyword is redundant (it is the default box) but
39104
+ Chromium drops the declaration when a bare calc() follows. */
39105
+ overflow-clip-margin: padding-box calc((1lh - 1em) / 2);
39020
39106
  }
39021
39107
 
39022
39108
  &[data-skeleton] {
@@ -51130,6 +51216,10 @@ const causeOfEvent = event => {
51130
51216
  }
51131
51217
  return "command";
51132
51218
  };
51219
+
51220
+ // The causes that are not places one went, whatever the state they are written
51221
+ // into says (see writeAreaAsked).
51222
+ const CAUSES_WRITTEN_BY_REPLACEMENT = ["drag", "refusal", "state"];
51133
51223
  const readArea = slideElement => slideElement.getAttribute("data-slide-area") || slideElement.id || "";
51134
51224
 
51135
51225
  /**
@@ -51166,33 +51256,65 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
51166
51256
  * repeated spans), except that a row needs no trailing hole: what is not
51167
51257
  * written is simply not there.
51168
51258
  * @param {string} [props.current] - area (or id) of the slide being shown; omit
51169
- * to keep it here and drive it by command.
51170
- * @param {import("@preact/signals").Signal<string>} [props.signal] - the same
51171
- * thing said the way every navi control says it: the container shows the area
51172
- * the signal holds, and writes into it the area it travels to. One binding
51173
- * instead of `current` + `onCurrentChange`, and the state stays where the app
51174
- * put it — which is what lets something else read where the slides are (a
51175
- * field carrying the current tab into a form, see
51259
+ * to keep it here and drive it by command. An area named from outside is a
51260
+ * REQUEST rather than a placement — see `signal` for what that means.
51261
+ * @param {import("@preact/signals").Signal<string>} [props.signal] - where the
51262
+ * area lives, said the way every navi control says it: the container shows
51263
+ * the area the signal holds, and writes into it the area it travels to. One
51264
+ * binding instead of `current` + `onCurrentChange`, and the state stays where
51265
+ * the app put it — which is what lets something else read where the slides
51266
+ * are (a field carrying the current tab into a form, see
51176
51267
  * docs/control_object.md#a-settings-sheet) or move them by writing it.
51177
51268
  * Excludes `current`; `onCurrentChange` still fires, for the `cause` and for
51178
51269
  * the right to refuse.
51179
- * @param {string} [props.defaultCurrent] - which slide to open on, when the
51180
- * travel is left to the container. Mount-only, like every other `default*`:
51270
+ * An area written from outside is WALKED to, not jumped to: every slide
51271
+ * between here and there is asked to let go the way a key going that way
51272
+ * would ask it, and the first one that holds (`preventNav`, a `required` step
51273
+ * still unanswered) is where one stops — after which the signal is written
51274
+ * with the area actually shown, so it says where one IS and never where one
51275
+ * asked to be.
51276
+ * PUTTING THE AREA IN THE URL is that binding and nothing more: hand it a
51277
+ * `stateSignal` a route declares as a search param, and `?step=<area>` is
51278
+ * written on every travel, read on a load, a bookmark, a link, a traversal —
51279
+ * through the walk above, so an address cannot open a slide the walk may not
51280
+ * reach.
51281
+ * ```js
51282
+ * const stepSignal = stateSignal("when", { id: "step", weak: true });
51283
+ * route("/alerts/:id/edit", { searchParams: { step: stepSignal } });
51284
+ * ```
51285
+ * ```jsx
51286
+ * <SlideContainer signal={stepSignal}>
51287
+ * ```
51288
+ * Where it opens is the state's own default (`stateSignal`'s first argument —
51289
+ * a signal there, for a start that depends on where one is), and the param
51290
+ * stays out of the address while the area IS that default. `weak` keeps the
51291
+ * step from being inherited by links built to that route. Written by
51292
+ * replacement unless the state says `history: "push"`, and even then a slide
51293
+ * reached by DRAGGING replaces — see docs/navigation.md.
51294
+ * @param {string} [props.defaultCurrent] - which slide to open on, for a
51295
+ * container that owns its position. Mount-only, like every other `default*`:
51181
51296
  * it says where one starts, not where one is — say `current` for that.
51182
51297
  * Without it the first slide is the one shown, the way a stack of pages opens
51183
51298
  * on its first page.
51184
- * @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code"|"url", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
51299
+ * A container bound to a `signal` does not own its position, and where it
51300
+ * opens is that state's own default (`stateSignal`'s first argument) rather
51301
+ * than this: one place says where the area starts, and it is the place a
51302
+ * reset goes back to. This still answers for a signal holding nothing at
51303
+ * all.
51304
+ * @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code"|"state", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
51185
51305
  * - the slide being shown has changed. `cause` says what asked for it, which
51186
- * is what tells a place browsed past from a place aimed at: a caller writing
51187
- * this into the URL pushes a history entry for a tab that was pressed and
51188
- * replaces the current one for a slide that was dragged, so three swipes back
51189
- * and forth do not bury the way out of the page.
51306
+ * is what tells a place browsed past from a place aimed at — a slide dragged
51307
+ * to rather than pressed for. `cause: "state"` is the bound state asking (a
51308
+ * write from the application, a load on a link, a traversal) rather than
51309
+ * anything done inside the box, and its `event` is null.
51190
51310
  * Answer `false` to REFUSE the change and the slide goes back where it came
51191
51311
  * from — a guard that says no, a session that is gone. A promise refuses it
51192
51312
  * late, once whatever it had to ask has answered; the travel plays meanwhile
51193
51313
  * and is undone if the answer is no.
51194
- * `cause: "url"` is the address asking (see `urlParam`) — a load on a link, a
51195
- * traversal — rather than anything done inside the box.
51314
+ * It is not where the area is REMEMBERED: `signal` does that in both
51315
+ * directions and is written by every way of travelling, while a callback
51316
+ * assigning it catches only the ones it was written for — see
51317
+ * docs/state_binding.md. What is left here is the `cause` and the refusal.
51196
51318
  * @param {"now"|"rest"} [props.commit="now"] - when the change is told.
51197
51319
  * "rest" waits for the travel to be over, and lets the container hold the
51198
51320
  * slide it is going to meanwhile: the picture moves with the finger and the
@@ -51210,33 +51332,6 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
51210
51332
  * one that just travelled there. Called once the travel is over, and in the
51211
51333
  * same render as the return to rest — anything later shows the old content
51212
51334
  * for a frame.
51213
- * @param {string|{name: string, history?: "replace"|"push"}} [props.urlParam]
51214
- * - the search param this container owns, and where it stands is written
51215
- * into: `urlParam="step"` puts `?step=<area>` in the address on every travel,
51216
- * and opens on the area it names on a load, a bookmark, a traversal, a link
51217
- * from anywhere else in the application.
51218
- * It is the answer to "the step should be readable and should survive a
51219
- * reload" WITHOUT one route per slide: no route to declare, no route guard to
51220
- * re-express the walk's own rules in, and no page transition — what travels
51221
- * is the box, and the address is a label on where the box stands. Say it on a
51222
- * container whose slides are places one may be sent to; a walk nobody links
51223
- * into keeps its position to itself.
51224
- * Written by REPLACEMENT: four steps that each stacked an entry would turn
51225
- * one back-press into four, and the back arrow of a page means "leave this
51226
- * page", not "one question back". `history: "push"` says the opposite for the
51227
- * containers that mean it — slides that ARE places one came from, a gallery
51228
- * one browses — and even there a slide reached by dragging replaces, so
51229
- * swiping back and forth does not bury the way out.
51230
- * The address is READ through the walk, not jumped to: it comes from outside
51231
- * the box, so every slide between here and there is asked to let go the way a
51232
- * key going that way would ask it, and the first one that holds
51233
- * (`preventNav`, or a `required` step still unanswered) is where one stops —
51234
- * `?step=done` cannot open a confirmation screen for something nobody sent.
51235
- * Whatever comes of it, the address is then rewritten with the area actually
51236
- * shown: it says where one IS, never where one asked to be. A step the app
51237
- * knows is already answered on a reload is told so by its own `required`
51238
- * (`required={!alreadyFilled}`) — the container remembers nothing across a
51239
- * load, and cannot.
51240
51335
  * Each way of travelling can be shut off, or narrowed to one axis: `true`
51241
51336
  * (every axis the map has), `false`, `"x"`, `"y"`, `"xy"`.
51242
51337
  *
@@ -51289,7 +51384,6 @@ const SlideContainer = ({
51289
51384
  commit = "now",
51290
51385
  loop,
51291
51386
  onLoop,
51292
- urlParam,
51293
51387
  travelByKeyboard = true,
51294
51388
  travelByDrag = true,
51295
51389
  travelByScroll = "x",
@@ -51306,7 +51400,11 @@ const SlideContainer = ({
51306
51400
  const containerRef = useRef();
51307
51401
  // The AREA of the slide being shown, not its rank: a rank would be wrong the
51308
51402
  // moment a slide appears before it, and there is nothing to renumber here.
51309
- const [currentAreaState, setCurrentAreaState] = useState(defaultCurrent);
51403
+ // This is where the container's own answer lives, and it is the only one the
51404
+ // picture is drawn from — an area coming from outside is a REQUEST that has
51405
+ // to be walked to first (see the effect reading currentFromCaller). At mount
51406
+ // there is no road to walk yet, so what the caller holds is taken as it is.
51407
+ const [currentAreaState, setCurrentAreaState] = useState(() => (currentSignal ? currentSignal.peek() : currentProp) ?? defaultCurrent);
51310
51408
  // The slide this container is travelling to while its controller has not been
51311
51409
  // told yet (commit="rest"): for the length of that travel the container is
51312
51410
  // ahead of whoever holds `current`, and this is where it keeps its own answer
@@ -51384,51 +51482,42 @@ const SlideContainer = ({
51384
51482
  // nothing measured per frame.
51385
51483
  const followerElementsRef = useRef([]);
51386
51484
  const currentFromCaller = currentSignal ? currentSignal.value : currentProp;
51387
- const current = rollingArea ?? provisionalArea ?? currentFromCaller ?? currentAreaState;
51388
- // The search param this container owns, when it owns one: the name it is
51389
- // written under, and whether going somewhere is somewhere one CAME from.
51390
- const urlParamName = typeof urlParam === "string" ? urlParam : urlParam?.name;
51391
- const urlParamHistory = urlParam?.history || "replace";
51392
- // What the address says right now. Read from the signal rather than from
51393
- // window.location so that a traversal — the browser's own back and forward —
51394
- // is a render like any other: the address moved, and the slides follow it.
51395
- // Read only when there is a param to read, so a container that owns none is
51396
- // not re-rendered by every navigation in the application.
51397
- const areaInUrl = urlParamName ? new URL(documentUrlSignal.value).searchParams.get(urlParamName) : null;
51398
- // The last thing this container knows the address said, whether it wrote it
51399
- // or read it. What makes a two-way binding stop turning: the address is only
51400
- // an INSTRUCTION when it changed by itself (a load, a traversal, a link), and
51485
+ const current = rollingArea ?? provisionalArea ?? currentAreaState;
51486
+ // The last area this container knows the caller holds, whether it wrote it
51487
+ // there or read it from there. What makes a two-way binding stop turning: an
51488
+ // area held outside is an INSTRUCTION only when it changed by itself (a load,
51489
+ // a traversal, a link, something in the application writing the signal), and
51401
51490
  // what this container put there is not news. Without it, a container ahead of
51402
- // its own URL (commit="rest", where the picture arrives before the address is
51403
- // written) would read its own lateness as an order to go back.
51404
- const areaInUrlSeenRef = useRef(undefined);
51405
- // Where the container stands, said in the address. `replace` amends the entry
51406
- // one is on: a walk of four steps must not turn one back-press into four, and
51407
- // the back arrow of a page keeps meaning "leave this page". `push` is for
51408
- // slides that ARE places one came from — and even there a slide reached by
51409
- // DRAGGING replaces, because browsing back and forth with a thumb is not a
51410
- // trail one wants to walk home along.
51411
- const writeUrlParam = (area, cause, {
51412
- replace = false
51491
+ // its own state (commit="rest", where the picture arrives before the change
51492
+ // is told) would read its own lateness as an order to go back.
51493
+ const areaAskedSeenRef = useRef(currentSignal ? currentSignal.peek() : currentProp);
51494
+ // Where the container stands, said where the caller holds it.
51495
+ //
51496
+ // Whether a slide is a place one came from is the STATE's own business (see
51497
+ // stateSignal's `history`), and this container has no opinion on it. What it
51498
+ // does know is which of its own writes are not places at all, and those are
51499
+ // written by REPLACEMENT whatever the state declares:
51500
+ // - a drag, because browsing back and forth with a thumb is not a trail one
51501
+ // wants to walk home along;
51502
+ // - a correction — a refused travel put back, a walk that stopped before
51503
+ // where it was asked to go — because it is the entry that asked for the
51504
+ // wrong thing being amended, not a place one was;
51505
+ // - the state itself asking (`cause: "state"`), because reading a value back
51506
+ // and answering it is not somewhere one went.
51507
+ const writeAreaAsked = (area, {
51508
+ cause
51413
51509
  } = {}) => {
51414
- if (!urlParamName) {
51510
+ areaAskedSeenRef.current = area;
51511
+ if (!currentSignal) {
51415
51512
  return;
51416
51513
  }
51417
- areaInUrlSeenRef.current = area;
51418
- const urlObject = new URL(window.location.href);
51419
- if (urlObject.searchParams.get(urlParamName) === area) {
51514
+ if (CAUSES_WRITTEN_BY_REPLACEMENT.includes(cause) && currentSignal.set) {
51515
+ currentSignal.set(area, {
51516
+ history: "replace"
51517
+ });
51420
51518
  return;
51421
51519
  }
51422
- urlObject.searchParams.set(urlParamName, area);
51423
- navTo(urlObject.href, {
51424
- replace: replace || urlParamHistory !== "push" || cause === "drag" ||
51425
- // The address already says it: reading it back is not a place one went.
51426
- cause === "url",
51427
- // What travels is the box, not the page: the address is a label on where
51428
- // the box stands, and a route transition would move the whole document
51429
- // for a slide that has already travelled by itself.
51430
- routeTransition: "none"
51431
- });
51520
+ currentSignal.value = area;
51432
51521
  };
51433
51522
  const vertical = layout === "column";
51434
51523
  // What the map has, and what each way of asking is allowed to use of it.
@@ -52059,7 +52148,7 @@ const SlideContainer = ({
52059
52148
  }
52060
52149
  const leftArea = readArea(currentElement);
52061
52150
  setCurrentAreaState(area);
52062
- if (!onCurrentChange && !currentSignal && !urlParamName) {
52151
+ if (!onCurrentChange && !currentSignal) {
52063
52152
  return true;
52064
52153
  }
52065
52154
  // What asked for this, read off the interaction rather than carried down
@@ -52090,21 +52179,16 @@ const SlideContainer = ({
52090
52179
  return true;
52091
52180
  };
52092
52181
 
52093
- // The caller learns where the container went: the bound signal is written,
52094
- // then the address, then `onCurrentChange` is called — in that order, so a
52095
- // caller reading either from inside its own handler reads where it now is.
52096
- // The signal before the address because writing the address can render on
52097
- // the spot (a navigation lets go of what Preact had queued, see
52098
- // rendering_hold.js), and that render must already find `current` moved.
52182
+ // The caller learns where the container went: the bound state is written
52183
+ // first, then `onCurrentChange` is called, so a caller reading the state from
52184
+ // inside its own handler reads where it now is. Written here rather than by
52185
+ // the caller so that it is told about the travels that HAPPENED and about no
52186
+ // others: the ones a lock refused never reach this point, and one refused
52187
+ // late is written back below (see goBackToRefusedArea).
52099
52188
  const tellCurrentChange = (area, detail, leftArea) => {
52100
- if (currentSignal) {
52101
- currentSignal.value = area;
52102
- }
52103
- // The address is told here rather than by the caller so that it is told
52104
- // about the travels that HAPPENED and about no others: the ones a lock
52105
- // refused never reach this point, and one refused late is written back
52106
- // below (see goBackToRefusedArea).
52107
- writeUrlParam(area, detail.cause);
52189
+ writeAreaAsked(area, {
52190
+ cause: detail.cause
52191
+ });
52108
52192
  if (!onCurrentChange) {
52109
52193
  return;
52110
52194
  }
@@ -52132,47 +52216,44 @@ const SlideContainer = ({
52132
52216
  const goBackToRefusedArea = leftArea => {
52133
52217
  setProvisionalArea(null);
52134
52218
  setCurrentAreaState(leftArea);
52135
- if (currentSignal) {
52136
- currentSignal.value = leftArea;
52137
- }
52138
- // Written over rather than stacked on, whatever this container does with
52139
- // the history otherwise: the entry the refused travel wrote is the one
52140
- // being corrected, and a refusal is not a place one was.
52141
- writeUrlParam(leftArea, "code", {
52142
- replace: true
52219
+ // Written over rather than stacked on, whatever the state says otherwise:
52220
+ // the entry a refused travel wrote is the one being corrected, and a
52221
+ // refusal is not a place one was.
52222
+ writeAreaAsked(leftArea, {
52223
+ cause: "refusal"
52143
52224
  });
52144
52225
  };
52145
52226
 
52146
- // The address asks for a slide. Read on every render it changes on rather
52147
- // than at mount alone, because the browser's own arrows are exactly that —
52148
- // the address moving by itself — and a link from elsewhere in the application
52149
- // is too. What follows is a travel like any other: the slides move, the
52150
- // caller is told, and the address ends up saying where the box actually
52151
- // stands.
52227
+ // Something outside the box asks for a slide: the application writing the
52228
+ // bound signal, and — when that signal is a route's own (see
52229
+ // docs/navigation.md) — a load on a link, a traversal, an address typed by
52230
+ // hand. All the same event here, and the answer to all of them is the same:
52231
+ // it is a REQUEST, walked to rather than jumped to.
52232
+ //
52233
+ // Walked, because the request comes from outside and the road may be closed:
52234
+ // every slide between here and there is asked to let go the way a key going
52235
+ // that way would ask it, and the first one that holds is where one stops.
52236
+ // `?step=done` cannot open a confirmation screen for something nobody sent.
52237
+ // Whatever comes of it, the state is then written with the area actually
52238
+ // shown: it says where one IS, never where one asked to be.
52152
52239
  useLayoutEffect(() => {
52153
- if (!urlParamName) {
52240
+ if (currentFromCaller === undefined) {
52154
52241
  return;
52155
52242
  }
52156
- if (areaInUrl === areaInUrlSeenRef.current) {
52243
+ if (currentFromCaller === areaAskedSeenRef.current) {
52157
52244
  // Not news: either nothing moved, or this container is reading back what
52158
52245
  // it wrote itself.
52159
52246
  return;
52160
52247
  }
52161
- areaInUrlSeenRef.current = areaInUrl;
52162
- if (areaInUrl === null) {
52163
- // An address that says nothing is not an address saying "the first
52164
- // slide": a container opened without the param opens where it would have
52165
- // opened anyway, and the param appears the first time one travels.
52166
- return;
52167
- }
52248
+ areaAskedSeenRef.current = currentFromCaller;
52168
52249
  // Where the box IS, read off the DOM: `current` is undefined until someone
52169
52250
  // names a slide, and the container standing on its first one is a fact only
52170
52251
  // the map knows (see the layout effect that paints it).
52171
- const areaOnScreen = containerRef.current?.getAttribute("data-slide-current");
52172
- if (!areaOnScreen || areaInUrl === areaOnScreen) {
52252
+ const areaOnScreen = containerRef.current?.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
52253
+ if (!areaOnScreen || currentFromCaller === areaOnScreen) {
52173
52254
  return;
52174
52255
  }
52175
- const reached = reachableTowards(areaOnScreen, areaInUrl);
52256
+ const reached = reachableTowards(areaOnScreen, currentFromCaller);
52176
52257
  if (reached && reached !== areaOnScreen) {
52177
52258
  // Told rather than asked: the walk above has already put every lock on
52178
52259
  // the way the question goToArea would have put the first one, and this
@@ -52181,15 +52262,25 @@ const SlideContainer = ({
52181
52262
  // any travel nobody pressed anything for.
52182
52263
  setCurrentAreaState(reached);
52183
52264
  tellCurrentChange(reached, {
52184
- cause: "url"
52265
+ cause: "state"
52185
52266
  }, areaOnScreen);
52186
52267
  return;
52187
52268
  }
52188
- // The param names nowhere this map knows, or somewhere the walk is not
52189
- // allowed to reach: the address is put back on the slide one is actually
52190
- // looking at, rather than left saying one is somewhere one is not.
52191
- writeUrlParam(areaOnScreen, "url");
52192
- }, [urlParamName, areaInUrl]);
52269
+ // Nowhere this map knows, or somewhere the walk is not allowed to reach:
52270
+ // the state is put back on the slide one is actually looking at, rather
52271
+ // than left saying one is somewhere one is not. A caller holding `current`
52272
+ // themselves is told instead — it is their value to correct.
52273
+ if (currentSignal) {
52274
+ writeAreaAsked(areaOnScreen, {
52275
+ cause: "refusal"
52276
+ });
52277
+ return;
52278
+ }
52279
+ onCurrentChange?.(areaOnScreen, {
52280
+ cause: "state",
52281
+ event: null
52282
+ });
52283
+ }, [currentFromCaller]);
52193
52284
 
52194
52285
  // The press kept during a roll, taken once the window rests and the travel is
52195
52286
  // given back (noTravel off): by direction when there was one, so it is read
@@ -53287,6 +53378,10 @@ const SlideContainer = ({
53287
53378
  * ahead to a screen that has nothing to show yet. Answering a step
53288
53379
  * un-answers the ones after it, since what they were answered about has just
53289
53380
  * changed.
53381
+ * A container remembers nothing across a load, and cannot: a step the app
53382
+ * knows is already answered when the page opens says so itself
53383
+ * (`required={!alreadyFilled}`), which is what lets an address naming a later
53384
+ * step be walked to.
53290
53385
  * @param {boolean} [props.preventNav] - hold the user here, whichever way they
53291
53386
  * try to leave.
53292
53387
  * @param {boolean} [props.preventNavNext] - hold them from going right or down.