@jsenv/navi 0.29.121 → 0.29.122

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.
@@ -39650,6 +39650,17 @@ const LinkCurrentIndicator = () => {
39650
39650
  };
39651
39651
  markAsOutsideTextFlow(LinkCurrentIndicator);
39652
39652
 
39653
+ /**
39654
+ * What the container tells what is inside it: which way it travels so a button
39655
+ * can point the right way without being told twice, what a travel handed to a
39656
+ * slide, and the box itself — which is how a way out written inside reads the
39657
+ * same facts a way out written outside reads by id (see useSlideContainer).
39658
+ *
39659
+ * Its own module, tiny on purpose: the hook that reads a container and the
39660
+ * container that fills this in would otherwise have to import each other.
39661
+ */
39662
+ const SlideContainerContext = createContext(null);
39663
+
39653
39664
  /**
39654
39665
  * What a SlideContainer is doing, read from outside it.
39655
39666
  *
@@ -39689,14 +39700,16 @@ markAsOutsideTextFlow(LinkCurrentIndicator);
39689
39700
  // ([data-slide-ways~="right"]).
39690
39701
  const SLIDE_CURRENT_ATTRIBUTE = "data-slide-current";
39691
39702
  const SLIDE_TOWARD_ATTRIBUTE = "data-slide-travel-toward";
39692
- // Where a travel WOULD go right now: there is a slide that way and the one on
39693
- // screen lets go of it.
39703
+ // Every way out that would DO something right now, in the words the commands
39704
+ // use: a direction ("left", "right", "up", "down") when there is a slide that
39705
+ // way, plus "first" and "last" when one is not already standing at that end of
39706
+ // the walk — and, in all cases, the slide on screen letting go.
39694
39707
  const SLIDE_WAYS_ATTRIBUTE = "data-slide-ways";
39695
- // …and where there is a slide that way but the one on screen holds on to the
39696
- // user (preventNav, or a `required` step still unanswered). Two facts, not one:
39697
- // a way out leading nowhere is not there, while a way out being held is there
39698
- // and says no — which is why it stays visible and explainable rather than
39699
- // hidden.
39708
+ // …and the ways out that are there and refused: the slide on screen holds on to
39709
+ // the user that way (preventNav, or a `required` step still unanswered). Two
39710
+ // facts, not one: a way out leading nowhere is not there at all, while a way
39711
+ // out being held IS there and says no — which is why it stays visible and
39712
+ // explainable rather than hidden. A way out is dead in both cases.
39700
39713
  const SLIDE_HELD_ATTRIBUTE = "data-slide-held";
39701
39714
  // …and the one word said out loud, on the box and on every follower of it, when
39702
39715
  // any of the above has actually changed. It carries the whole state as its
@@ -39704,7 +39717,11 @@ const SLIDE_HELD_ATTRIBUTE = "data-slide-held";
39704
39717
  const SLIDE_STATE_EVENT = "navi_slide_state";
39705
39718
 
39706
39719
  const NO_WAYS = [];
39720
+ // No box to read: not "a box with nothing in it". The difference is the whole
39721
+ // point — see `can` below, which must not answer "there is nowhere to go" to
39722
+ // the question "where can one go" when it has not been told anything yet.
39707
39723
  const NOTHING = {
39724
+ known: false,
39708
39725
  current: undefined,
39709
39726
  toward: undefined,
39710
39727
  areas: NO_WAYS,
@@ -39718,11 +39735,13 @@ const wordsOf = (element, attribute) => {
39718
39735
  };
39719
39736
 
39720
39737
  const readSlideContainerState = (element) => ({
39738
+ known: true,
39721
39739
  current: element.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined,
39722
39740
  toward: element.getAttribute(SLIDE_TOWARD_ATTRIBUTE) ?? undefined,
39723
- // In DOM order, which is the order of the walk for a line and the order the
39724
- // areas were written in for a map the same order everything else reads
39725
- // (see readMap).
39741
+ // In DOM order which is the order of the WALK for a line, and not
39742
+ // necessarily for a map: there the order is the one the areas are written in
39743
+ // (see parseAreas). Ask `can("first")` / `can("last")` about the ends rather
39744
+ // than the two ends of this list.
39726
39745
  areas: Array.from(
39727
39746
  element.querySelectorAll(":scope > [data-slide-track] > [data-slide]"),
39728
39747
  (slideElement) =>
@@ -39733,6 +39752,7 @@ const readSlideContainerState = (element) => ({
39733
39752
  });
39734
39753
 
39735
39754
  const sameSlideContainerState = (a, b) =>
39755
+ a.known === b.known &&
39736
39756
  a.current === b.current &&
39737
39757
  a.toward === b.toward &&
39738
39758
  a.areas.join(" ") === b.areas.join(" ") &&
@@ -39743,33 +39763,50 @@ const sameSlideContainerState = (a, b) =>
39743
39763
  * @param {string|Element|{current: Element}} [target] - the container: its id
39744
39764
  * (the way everything else addresses one), the element, or a ref to it. Not a
39745
39765
  * follower — a follower is painted for CSS to draw with, the box is what holds
39746
- * the walk. Nothing at all is allowed and answers "no container": a component
39747
- * that may or may not be wired to one calls this unconditionally, like every
39748
- * hook.
39766
+ * the walk. Left out, it is the box this is written INSIDE, if any: a way out
39767
+ * reads the same facts on either side of the box, which is the whole point of
39768
+ * there being one answer to "what would this do".
39749
39769
  * @returns {{
39770
+ * known: boolean,
39750
39771
  * current: string|undefined,
39751
39772
  * toward: string|undefined,
39752
39773
  * areas: string[],
39753
- * can: (direction: "left"|"right"|"up"|"down") => boolean,
39754
- * held: (direction: "left"|"right"|"up"|"down") => boolean,
39774
+ * can: (wayOut: "left"|"right"|"up"|"down"|"first"|"last") => boolean,
39775
+ * held: (wayOut: "left"|"right"|"up"|"down"|"first"|"last") => boolean,
39755
39776
  * }} where the box stands. `current` is the slide on screen — the one being
39756
39777
  * travelled TO while a travel plays, because that is what one is looking at;
39757
39778
  * `toward` is the other slide in the frame while the picture is between two,
39758
- * and nothing at rest. `can` is "a travel that way would happen", `held` is
39759
- * "there is a slide that way and this one says no" — a chevron is dead in
39760
- * both cases and only the second is worth explaining.
39779
+ * and nothing at rest. `can` is "asking for it would do something", `held` is
39780
+ * "it is there and this slide says no" — a way out is dead in both cases, and
39781
+ * only the second is worth a word to the reader.
39782
+ * `known` is false until the box has been read: no box was named and none is
39783
+ * above, the id names nothing, or — for one commit — this mounted before the
39784
+ * box had painted. Until then `can` answers YES, because it is the answer that
39785
+ * degrades well: a way out offered for one frame and then taken away is a
39786
+ * button that did nothing once, while the reverse hides every way out of every
39787
+ * box that this cannot see and says nothing about it.
39761
39788
  */
39762
39789
  const useSlideContainer = (target) => {
39763
39790
  const [state, setState] = useState(NOTHING);
39791
+ // The box this is written inside, when nothing names one. Read
39792
+ // unconditionally, like every hook, and used only as a fallback.
39793
+ const containerInContext = useContext(SlideContainerContext);
39794
+ const fallbackRef = containerInContext?.containerRef;
39764
39795
 
39765
39796
  useLayoutEffect(() => {
39797
+ const resolved = target ?? fallbackRef;
39766
39798
  const element =
39767
- typeof target === "string"
39768
- ? document.getElementById(target)
39769
- : target && "current" in target
39770
- ? target.current
39771
- : target;
39799
+ typeof resolved === "string"
39800
+ ? document.getElementById(resolved)
39801
+ : resolved && "current" in resolved
39802
+ ? resolved.current
39803
+ : resolved;
39772
39804
  if (!element) {
39805
+ if (typeof resolved === "string") {
39806
+ console.warn(
39807
+ `useSlideContainer("${resolved}") but no element with that id found`,
39808
+ );
39809
+ }
39773
39810
  setState(NOTHING);
39774
39811
  return undefined;
39775
39812
  }
@@ -39788,14 +39825,15 @@ const useSlideContainer = (target) => {
39788
39825
  return () => {
39789
39826
  element.removeEventListener(SLIDE_STATE_EVENT, read);
39790
39827
  };
39791
- }, [target]);
39828
+ }, [target, fallbackRef]);
39792
39829
 
39793
39830
  return {
39831
+ known: state.known,
39794
39832
  current: state.current,
39795
39833
  toward: state.toward,
39796
39834
  areas: state.areas,
39797
- can: (direction) => state.ways.includes(direction),
39798
- held: (direction) => state.held.includes(direction),
39835
+ can: (wayOut) => !state.known || state.ways.includes(wayOut),
39836
+ held: (wayOut) => state.held.includes(wayOut),
39799
39837
  };
39800
39838
  };
39801
39839
 
@@ -49901,9 +49939,6 @@ const isWayOut = element => Boolean(element && element.closest && element.closes
49901
49939
  // empty map, and a re-render changes nothing for them.
49902
49940
  const EMPTY_VALUE_BY_AREA = {};
49903
49941
 
49904
- // What the container tells what is inside it: which way it travels, so a button
49905
- // can point the right way without being told twice.
49906
- const SlideContainerContext = createContext(null);
49907
49942
  // What a slide tells what is inside IT: whether leaving it is allowed right
49908
49943
  // now, so its own prev/next buttons say so instead of failing when pressed.
49909
49944
  const SlideContext = createContext(null);
@@ -51323,6 +51358,16 @@ const SlideContainer = ({
51323
51358
  const paintWays = currentElement => {
51324
51359
  const ways = [];
51325
51360
  const held = [];
51361
+ // Whether the slide being left says no, this way. The one gate again, read
51362
+ // exactly as goToArea reads it at the moment of the travel — so what is
51363
+ // published here and what would happen cannot disagree.
51364
+ const holdsTowards = forward => currentElement?.hasAttribute(forward ? "data-prevent-nav-next" : "data-prevent-nav-previous");
51365
+ const say = (name, offered, forward) => {
51366
+ if (!offered) {
51367
+ return;
51368
+ }
51369
+ (holdsTowards(forward) ? held : ways).push(name);
51370
+ };
51326
51371
  for (const direction of Object.keys(DIRECTIONS)) {
51327
51372
  const {
51328
51373
  dx,
@@ -51333,13 +51378,18 @@ const SlideContainer = ({
51333
51378
  // would be saying it about every row on the page.
51334
51379
  continue;
51335
51380
  }
51336
- if (!areaTowards(dx, dy)) {
51337
- continue;
51338
- }
51339
- const forward = dx > 0 || dy > 0;
51340
- const isHeld = currentElement?.hasAttribute(forward ? "data-prevent-nav-next" : "data-prevent-nav-previous");
51341
- (isHeld ? held : ways).push(direction);
51381
+ say(direction, Boolean(areaTowards(dx, dy)), dx > 0 || dy > 0);
51342
51382
  }
51383
+ // The two ends are ways out like the others — "all the way that way", said
51384
+ // by the map's own order rather than by a direction (see goToEnd, and
51385
+ // SlideContainer.First / .Last). Offered while one is not already standing
51386
+ // there, and held by the same lock the direction they lie in would be.
51387
+ const {
51388
+ order
51389
+ } = readMap();
51390
+ const currentArea = currentElement ? readArea(currentElement) : undefined;
51391
+ say("first", order.length > 0 && order[0] !== currentArea, false);
51392
+ say("last", order.length > 0 && order[order.length - 1] !== currentArea, true);
51343
51393
  for (const element of travelPainters()) {
51344
51394
  if (ways.length) {
51345
51395
  element.setAttribute(SLIDE_WAYS_ATTRIBUTE, ways.join(" "));
@@ -52082,7 +52132,11 @@ const SlideContainer = ({
52082
52132
  answeredAreas,
52083
52133
  done,
52084
52134
  valueByArea,
52085
- settleFocus
52135
+ settleFocus,
52136
+ // The box itself, for what is written INSIDE it to read the same
52137
+ // facts what is written outside reads by id (useSlideContainer):
52138
+ // a way out is a way out on either side of the box.
52139
+ containerRef
52086
52140
  },
52087
52141
  children: children
52088
52142
  })
@@ -52295,14 +52349,19 @@ const SlideMove = ({
52295
52349
  label
52296
52350
  } = DIRECTIONS[direction];
52297
52351
  const forward = dx > 0 || dy > 0;
52298
- // The same fact, read from wherever this way out is written. Inside a slide it
52299
- // comes down as context the slide holding the user is this button's
52300
- // ancestor. Written AROUND the box (only slides go in it, so a chevron pinned
52301
- // to the edge of a full-screen surface has to be), it is not, so it is read
52302
- // off the box this button already names to ask for the travel: one prop, and
52303
- // the way out behaves the same on either side of the box.
52352
+ // What this way out would DO, read from wherever it is written: off the box it
52353
+ // names when it is drawn around the box (only slides go in it, so a chevron
52354
+ // pinned to the edge of a full-screen surface has to be), off the box above it
52355
+ // otherwise. One answer on either side of the box.
52304
52356
  const slides = useSlideContainer(rest.commandFor);
52305
- const locked = (forward ? locks?.preventNavNext : locks?.preventNavPrevious) || slides.held(direction);
52357
+ // Two reasons to be dead, and they are not the same thing to a reader: this
52358
+ // slide holding on to them (worth explaining — see readOnly on SlideNavButton)
52359
+ // and there being nothing that way at all (the end of the walk, which explains
52360
+ // itself). The lock also comes down as context when this is written inside a
52361
+ // slide, which is the same fact one commit earlier: the box publishes it after
52362
+ // the render, and there is no frame where the way out is live for nothing.
52363
+ const held = (forward ? locks?.preventNavNext : locks?.preventNavPrevious) || slides.held(direction);
52364
+ const locked = held || !slides.can(direction);
52306
52365
  return jsx(SlideNavButton, {
52307
52366
  command: command,
52308
52367
  locked: locked,
@@ -52314,16 +52373,24 @@ const SlideMove = ({
52314
52373
 
52315
52374
  // "All the way that way": the first slide of the walk, or the last one. Not a
52316
52375
  // direction — a map reads its own order — so it is its own component rather
52317
- // than a fifth arrow.
52376
+ // than a fifth arrow. Which is exactly why it cannot work out on its own whether
52377
+ // it would do anything: only the box knows its order, so only the box can say
52378
+ // that one is already standing at that end. It says it in the same breath as the
52379
+ // rest (see paintWays).
52318
52380
  const SlideEnd = ({
52319
52381
  last,
52320
52382
  ...rest
52321
- }) => jsx(SlideNavButton, {
52322
- command: last ? "--navi-last" : "--navi-first",
52323
- ChevronSvg: last ? ChevronLastSvg : ChevronFirstSvg,
52324
- "aria-label": last ? "Last slide" : "First slide",
52325
- ...rest
52326
- });
52383
+ }) => {
52384
+ const slides = useSlideContainer(rest.commandFor);
52385
+ const wayOut = last ? "last" : "first";
52386
+ return jsx(SlideNavButton, {
52387
+ command: last ? "--navi-last" : "--navi-first",
52388
+ locked: slides.held(wayOut) || !slides.can(wayOut),
52389
+ ChevronSvg: last ? ChevronLastSvg : ChevronFirstSvg,
52390
+ "aria-label": last ? "Last slide" : "First slide",
52391
+ ...rest
52392
+ });
52393
+ };
52327
52394
  const SlideFirst = props => jsx(SlideEnd, {
52328
52395
  ...props,
52329
52396
  last: false