@jsenv/navi 0.29.506 → 0.29.507

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.
@@ -69614,6 +69614,40 @@ const causeOfEvent = event => {
69614
69614
  // into says (see writeAreaAsked).
69615
69615
  const CAUSES_WRITTEN_BY_REPLACEMENT = ["drag", "refusal", "state"];
69616
69616
  const readArea = slideElement => slideElement.getAttribute("data-slide-area") || slideElement.id || "";
69617
+ const slidesOf = containerElement => Array.from(containerElement.querySelectorAll(":scope > [data-slide-track] > [data-slide]"));
69618
+
69619
+ // Who is drawing something about a box from outside it: a tab bar saying where
69620
+ // one is, a row of dots. They name the box they follow by its id, the way
69621
+ // everything else that talks to it across the document does (commandfor).
69622
+ const readFollowerElements = containerElement => {
69623
+ const {
69624
+ id
69625
+ } = containerElement;
69626
+ if (!id) {
69627
+ return [];
69628
+ }
69629
+ return Array.from(document.querySelectorAll(`[data-slide-container-follows="${CSS.escape(id)}"]`));
69630
+ };
69631
+
69632
+ // The containers inside a slide bound to the same state as the slide's own
69633
+ // container (see `signal`) — the ones directly under it, a container sharing
69634
+ // with one of those is that one's business.
69635
+ const sharingContainersIn = slideElement => {
69636
+ const holderElement = slideElement.closest("[data-slide-container]");
69637
+ return Array.from(slideElement.querySelectorAll("[data-slide-shares-state]")).filter(innerElement => innerElement.parentElement.closest("[data-slide-container]") === holderElement);
69638
+ };
69639
+
69640
+ // Whether the state holding `area` is on screen when this slide is: it is the
69641
+ // slide's own area, or a slide of a container sharing the state inside it.
69642
+ const slideShows = (slideElement, area) => readArea(slideElement) === area || sharingContainersIn(slideElement).some(innerElement => slidesOf(innerElement).some(innerSlide => slideShows(innerSlide, area)));
69643
+
69644
+ // What the state holds while this slide is on screen: the area a container
69645
+ // sharing the state inside it stands on, its own area when there is none.
69646
+ const areaHeldOn = slideElement => {
69647
+ const [innerElement] = sharingContainersIn(slideElement);
69648
+ const innerCurrent = innerElement && slidesOf(innerElement).find(innerSlide => innerSlide.hasAttribute("data-current"));
69649
+ return innerCurrent ? areaHeldOn(innerCurrent) : readArea(slideElement);
69650
+ };
69617
69651
 
69618
69652
  /**
69619
69653
  * The slide shown can be driven from outside (a `signal`, or `current` +
@@ -69648,6 +69682,10 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
69648
69682
  * "create"]`. Spelled like grid-template-areas ("." is a hole, a name
69649
69683
  * repeated spans), except that a row needs no trailing hole: what is not
69650
69684
  * written is simply not there.
69685
+ * A map is read as a map: a step landing on a hole is not offered. So rows
69686
+ * of different lengths, each walked on its own and changed as a whole (a
69687
+ * column of levels, a row of titles in each), are not a map but a container
69688
+ * in each slide of a column, all bound to one signal — see `signal`.
69651
69689
  * @param {string} [props.current] - area (or id) of the slide being shown; omit
69652
69690
  * to keep it here and drive it by command. An area named from outside is a
69653
69691
  * REQUEST rather than a placement — see `signal` for what that means.
@@ -69688,6 +69726,47 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
69688
69726
  * by links built to that route. Written by replacement unless the state says
69689
69727
  * `history: "push"`, and even then a slide reached by DRAGGING replaces — see
69690
69728
  * docs/navigation.md.
69729
+ * ONE STATE FOR CONTAINERS INSIDE ONE ANOTHER: a container in a slide,
69730
+ * bound to the SAME signal as the container holding it, shares that state.
69731
+ * For screens where one place is said at two levels — a column of
69732
+ * tournament levels with a row of titles in each, the address saying which
69733
+ * title (`?cup=P100:2`): the level is not a second state, it is read from
69734
+ * the title.
69735
+ * ```jsx
69736
+ * const cupSignal = stateSignal(undefined, { id: "cup" });
69737
+ *
69738
+ * <SlideContainer layout="column" signal={cupSignal}>
69739
+ * {levels.map((level) => (
69740
+ * <Slide area={level.name}>
69741
+ * <LevelHeader level={level} />
69742
+ * <SlideContainer signal={cupSignal} defaultCurrent={level.entryKey}>
69743
+ * {level.titles.map((title) => (
69744
+ * <Slide area={title.key}>…</Slide>
69745
+ * ))}
69746
+ * </SlideContainer>
69747
+ * </Slide>
69748
+ * ))}
69749
+ * </SlideContainer>
69750
+ * ```
69751
+ * - The state holds an area of an INNER container. The outer one shows the
69752
+ * slide holding it, the inner ones that do not have it stay where they
69753
+ * are.
69754
+ * - A travel of the outer one writes the area the inner one stands on in the
69755
+ * slide it arrives at: a row left keeps its place for when one comes back,
69756
+ * and a row not seen yet opens on its `defaultCurrent`.
69757
+ * - A value written from outside (an address, a traversal) is walked to by
69758
+ * both, each on its own axis.
69759
+ * - The arrows of the inner one on screen are heard on the outer box and on
69760
+ * what follows it (`data-slide-container-follows`), so a surface follows
69761
+ * the OUTER one and all four arrows walk it.
69762
+ * - `onCurrentChange` still tells each one its own area: the level to the
69763
+ * outer one, the title to the inner one.
69764
+ * What an outer slide holds is read in the DOM: one whose content is not
69765
+ * built (`mount="near"`) holds nothing the state can name.
69766
+ * Neither `current` + `onCurrentChange` on each, ignoring `cause: "state"`
69767
+ * and remembering by hand where each row was left — that is a second copy of
69768
+ * the state (see docs/state_binding.md) — nor one map with rows of different
69769
+ * lengths (see `layout`).
69691
69770
  * @param {string} [props.defaultCurrent] - which slide to open on, for a
69692
69771
  * container that owns its position. Mount-only, like every other `default*`:
69693
69772
  * it says where one starts, not where one is — say `current` for that.
@@ -69697,7 +69776,8 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
69697
69776
  * opens is that state's own default (`stateSignal`'s first argument) rather
69698
69777
  * than this: one place says where the area starts, and it is the place a
69699
69778
  * reset goes back to. This still answers for a signal holding nothing at
69700
- * all.
69779
+ * all. And for a container sharing the state of the one holding it (see
69780
+ * `signal`), it is where it opens while the state names a place elsewhere.
69701
69781
  * @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code"|"state", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
69702
69782
  * - the slide being shown has changed. `cause` says what asked for it, which
69703
69783
  * is what tells a place browsed past from a place aimed at — a slide dragged
@@ -69817,6 +69897,10 @@ const SlideContainer = ({
69817
69897
  // The box itself: it is what takes the keyboard when what is on screen holds
69818
69898
  // nothing that can (see handOverFocus).
69819
69899
  const containerRef = useRef();
69900
+ // Bound to the state of the container holding this one: its slides are
69901
+ // places inside one of that container's slides (see `signal`).
69902
+ const holder = useContext(SlideContainerContext);
69903
+ const sharesState = Boolean(currentSignal) && holder?.signal === currentSignal;
69820
69904
  // The AREA of the slide being shown, not its rank: a rank would be wrong the
69821
69905
  // moment a slide appears before it, and there is nothing to renumber here.
69822
69906
  // This is where the container's own answer lives, and it is the only one the
@@ -69827,7 +69911,10 @@ const SlideContainer = ({
69827
69911
  // walking past every lock in exactly the case the locks are there for.
69828
69912
  // So the picture starts where the state itself would open (a stateSignal's
69829
69913
  // own default), or on `defaultCurrent`, and the walk goes from there.
69830
- const [currentAreaState, setCurrentAreaState] = useState(() => currentSignal?.options?.getDefaultValue?.() ?? defaultCurrent);
69914
+ // A container sharing its holder's state starts on `defaultCurrent`: the
69915
+ // state's default is a place in ONE of the containers sharing it, and the
69916
+ // one holding it is walked there like any other.
69917
+ const [currentAreaState, setCurrentAreaState] = useState(() => sharesState ? defaultCurrent : currentSignal?.options?.getDefaultValue?.() ?? defaultCurrent);
69831
69918
  // The slide this container is travelling to while its controller has not been
69832
69919
  // told yet (commit="rest"): for the length of that travel the container is
69833
69920
  // ahead of whoever holds `current`, and this is where it keeps its own answer
@@ -69917,6 +70004,21 @@ const SlideContainer = ({
69917
70004
  }
69918
70005
  const currentFromCaller = currentSignal ? currentSignal.value : currentProp;
69919
70006
  const current = rollingArea ?? provisionalArea ?? currentAreaState;
70007
+ // The area of this box a value held outside is shown on: the slide of that
70008
+ // name, or the one holding a container that shares the state and has it.
70009
+ // Read off the last commit, so a render before the first sees it as it is.
70010
+ const ownAreaOf = value => {
70011
+ const track = trackRef.current;
70012
+ if (value === undefined || !track) {
70013
+ return value;
70014
+ }
70015
+ const slideElements = Array.from(track.children);
70016
+ if (slideElements.some(slideElement => readArea(slideElement) === value)) {
70017
+ return value;
70018
+ }
70019
+ const holding = slideElements.find(slideElement => slideShows(slideElement, value));
70020
+ return holding ? readArea(holding) : value;
70021
+ };
69920
70022
  // The last area this container knows the caller holds, whether it wrote it
69921
70023
  // there or read it from there. What makes a two-way binding stop turning: an
69922
70024
  // area held outside is an INSTRUCTION only when it changed by itself (a load,
@@ -69935,7 +70037,7 @@ const SlideContainer = ({
69935
70037
  // it is painted, the first request included — and the container's own answer
69936
70038
  // otherwise. What a slide waiting for a travel reads to have its content in
69937
70039
  // the very render that shows it (see Slide's `mount`).
69938
- const areaShownNext = requestIsNews() ? currentFromCaller : current;
70040
+ const areaShownNext = requestIsNews() ? ownAreaOf(currentFromCaller) : current;
69939
70041
  // Where the container stands, said where the caller holds it.
69940
70042
  //
69941
70043
  // Whether a slide is a place one came from is the STATE's own business (see
@@ -69952,17 +70054,31 @@ const SlideContainer = ({
69952
70054
  const writeAreaAsked = (area, {
69953
70055
  cause
69954
70056
  } = {}) => {
69955
- areaAskedSeenRef.current = area;
69956
70057
  if (!currentSignal) {
70058
+ areaAskedSeenRef.current = area;
69957
70059
  return;
69958
70060
  }
70061
+ const value = valueOfArea(area);
70062
+ areaAskedSeenRef.current = value;
69959
70063
  if (CAUSES_WRITTEN_BY_REPLACEMENT.includes(cause) && currentSignal.set) {
69960
- currentSignal.set(area, {
70064
+ currentSignal.set(value, {
69961
70065
  history: "replace"
69962
70066
  });
69963
70067
  return;
69964
70068
  }
69965
- currentSignal.value = area;
70069
+ currentSignal.value = value;
70070
+ };
70071
+ // What the state holds for one of this box's areas: the place a container
70072
+ // sharing it stands on inside that slide (see areaHeldOn). A state already
70073
+ // naming a place in that slide is kept as it is — a container in there may
70074
+ // have written it in this very commit, before its DOM says so.
70075
+ const valueOfArea = area => {
70076
+ const held = currentSignal.peek();
70077
+ if (held !== area && ownAreaOf(held) === area) {
70078
+ return held;
70079
+ }
70080
+ const slideElement = readMap().slideElements.find(slideElement => readArea(slideElement) === area);
70081
+ return slideElement ? areaHeldOn(slideElement) : area;
69966
70082
  };
69967
70083
  const vertical = layout === "column";
69968
70084
  // What the map has, and what each way of asking is allowed to use of it.
@@ -70014,7 +70130,7 @@ const SlideContainer = ({
70014
70130
  if (provisionalArea === null) {
70015
70131
  return;
70016
70132
  }
70017
- const heldOutside = currentFromCaller ?? currentAreaState;
70133
+ const heldOutside = ownAreaOf(currentFromCaller) ?? currentAreaState;
70018
70134
  if (heldOutside === provisionalArea) {
70019
70135
  setProvisionalArea(null);
70020
70136
  }
@@ -70105,20 +70221,6 @@ const SlideContainer = ({
70105
70221
  };
70106
70222
  };
70107
70223
 
70108
- // Who is drawing something about this box from outside it: a tab bar saying
70109
- // where one is, a row of dots. They name the box they follow by its id, the
70110
- // way everything else that talks to it across the document does (commandfor).
70111
- const readFollowerElements = () => {
70112
- const containerEl = containerRef.current;
70113
- const {
70114
- id
70115
- } = containerEl;
70116
- if (!id) {
70117
- return [];
70118
- }
70119
- return Array.from(document.querySelectorAll(`[data-slide-container-follows="${CSS.escape(id)}"]`));
70120
- };
70121
-
70122
70224
  // Which slide is on screen, said in the DOM: it is what anything outside the
70123
70225
  // box reads to know where one is (a tab bar marking its current tab), and
70124
70226
  // there is nothing else for it to read — a slide the container holds by
@@ -70257,7 +70359,7 @@ const SlideContainer = ({
70257
70359
  // Read on every render rather than subscribed to: a follower says who it
70258
70360
  // follows in the DOM, and the render that mounted one is the render this
70259
70361
  // runs after.
70260
- followerElementsRef.current = readFollowerElements();
70362
+ followerElementsRef.current = readFollowerElements(containerRef.current);
70261
70363
  const {
70262
70364
  slideElements,
70263
70365
  placeOf
@@ -70530,7 +70632,7 @@ const SlideContainer = ({
70530
70632
  // this one, in the same commit, and the slides around a picture about to be
70531
70633
  // walked away from would be built for nothing. The neighbours only at
70532
70634
  // rest, see renderNear.
70533
- const settlingArea = requestIsNews() && reachableTowards(currentArea, currentFromCaller) || currentArea;
70635
+ const settlingArea = requestIsNews() && reachableTowards(currentArea, ownAreaOf(currentFromCaller)) || currentArea;
70534
70636
  renderNear(settlingArea, {
70535
70637
  alongside: !stageRef.current
70536
70638
  });
@@ -70780,8 +70882,11 @@ const SlideContainer = ({
70780
70882
  // content arrives a request later. Not a refusal: nothing is remembered and
70781
70883
  // nothing is written, and the render that brings the slide asks again. A
70782
70884
  // name that will never be a slide is the state's to refuse (`oneOf`), not
70783
- // the container's: from here, "later" and "never" look the same.
70784
- if (!readMap().slideElements.some(slideElement => readArea(slideElement) === currentFromCaller)) {
70885
+ // the container's: from here, "later" and "never" look the same. Nor is a
70886
+ // place shown by another container sharing the state — a row of a column
70887
+ // of rows stays where it was while the state names a place in another one.
70888
+ const areaAsked = ownAreaOf(currentFromCaller);
70889
+ if (!readMap().slideElements.some(slideElement => readArea(slideElement) === areaAsked)) {
70785
70890
  return;
70786
70891
  }
70787
70892
  areaAskedSeenRef.current = currentFromCaller;
@@ -70789,10 +70894,10 @@ const SlideContainer = ({
70789
70894
  // names a slide, and the container standing on its first one is a fact only
70790
70895
  // the map knows (see the layout effect that paints it).
70791
70896
  const areaOnScreen = containerRef.current.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
70792
- if (currentFromCaller === areaOnScreen) {
70897
+ if (areaAsked === areaOnScreen) {
70793
70898
  return;
70794
70899
  }
70795
- const reached = reachableTowards(areaOnScreen, currentFromCaller);
70900
+ const reached = reachableTowards(areaOnScreen, areaAsked);
70796
70901
  if (reached && reached !== areaOnScreen) {
70797
70902
  // Told rather than asked: the walk above has already put every lock on
70798
70903
  // the way the question goToArea would have put the first one, and this
@@ -71221,7 +71326,7 @@ const SlideContainer = ({
71221
71326
  // Read again at the start of every travel, not only at every render: a
71222
71327
  // follower appearing does not make this box render, and the travel it must
71223
71328
  // draw is the one about to start.
71224
- followerElementsRef.current = readFollowerElements();
71329
+ followerElementsRef.current = readFollowerElements(containerRef.current);
71225
71330
  cancelTravelProgressAnimation();
71226
71331
  paintTravelProgress(0);
71227
71332
  const painters = travelPainters();
@@ -71534,7 +71639,7 @@ const SlideContainer = ({
71534
71639
  trackAnimationRef.current?.cancel();
71535
71640
  trackAnimationRef.current = null;
71536
71641
  cancelTravelProgressAnimation();
71537
- followerElementsRef.current = readFollowerElements();
71642
+ followerElementsRef.current = readFollowerElements(containerRef.current);
71538
71643
  drag.axis = axis;
71539
71644
  drag.areaBack = areaBack;
71540
71645
  drag.areaOn = areaOn;
@@ -71922,6 +72027,35 @@ const SlideContainer = ({
71922
72027
  }
71923
72028
  };
71924
72029
  });
72030
+ // A container sharing the state of the one holding it is part of that one's
72031
+ // surface: the arrows it walks are heard on the holder's box and on what
72032
+ // follows it, while its slide is the one on screen — a row of a column of
72033
+ // rows walks sideways with the keyboard on the column, or on a chevron of
72034
+ // the column. A press something already travelled on is left alone (it is
72035
+ // marked, see applyKeyboardShortcuts), and so is one pressed inside this box,
72036
+ // which heard it first.
72037
+ useLayoutEffect(() => {
72038
+ const holderElement = sharesState && holder.containerRef.current;
72039
+ if (!holderElement) {
72040
+ return undefined;
72041
+ }
72042
+ const surfaceElements = [holderElement, ...readFollowerElements(holderElement)];
72043
+ const onSurfaceKeyDown = keyDownEvent => {
72044
+ const containerEl = containerRef.current;
72045
+ if (keyDownEvent.defaultPrevented || !containerEl || containerEl.contains(keyDownEvent.target) || !containerEl.closest("[data-slide]")?.hasAttribute("data-current")) {
72046
+ return;
72047
+ }
72048
+ onKeyDownShortcuts(keyDownEvent);
72049
+ };
72050
+ for (const surfaceElement of surfaceElements) {
72051
+ surfaceElement.addEventListener("keydown", onSurfaceKeyDown);
72052
+ }
72053
+ return () => {
72054
+ for (const surfaceElement of surfaceElements) {
72055
+ surfaceElement.removeEventListener("keydown", onSurfaceKeyDown);
72056
+ }
72057
+ };
72058
+ });
71925
72059
  return (
71926
72060
  // Box rather than a plain div: it is how every navi component takes the
71927
72061
  // onnavi_* handlers below — they are navi's own event names, and Box is
@@ -71931,6 +72065,10 @@ const SlideContainer = ({
71931
72065
  ref: containerRef,
71932
72066
  baseClassName: "navi_slide_container",
71933
72067
  "data-slide-container": ""
72068
+ // What the holder reads to show the slide holding the place the state
72069
+ // names, and to write the place this box stands on (see `signal`).
72070
+ ,
72071
+ "data-slide-shares-state": sharesState ? "" : undefined
71934
72072
  // Which axes a touch may travel on, said in the DOM: what the browser
71935
72073
  // does with a finger is decided by CSS (touch-action) before any of this
71936
72074
  // has seen the gesture — and it is also what a box HOLDING this one reads
@@ -72072,6 +72210,9 @@ const SlideContainer = ({
72072
72210
  areaShownNext,
72073
72211
  nearAreas,
72074
72212
  claimOpening: areaShownNext === undefined ? claimOpening : null,
72213
+ // What a container nested in a slide compares its own with, to
72214
+ // know it shares this state (see `signal`).
72215
+ signal: currentSignal,
72075
72216
  // The box itself, for what is written INSIDE it to read the same
72076
72217
  // facts what is written outside reads by id (useSlideContainer):
72077
72218
  // a way out is a way out on either side of the box.