@jsenv/navi 0.30.4 → 0.30.5

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.
@@ -36505,9 +36505,23 @@ const collectRoutePages = children => {
36505
36505
  * Whether the next container down is a row a hand scrolls: every page has a
36506
36506
  * place on it from the start, and the browser moves between them (see
36507
36507
  * RouteTravel's `nativeScroll`). Told to one container, not to the pages it holds.
36508
+ * False, or `{ renderNeighbour }`.
36508
36509
  */
36509
36510
  const RouteRowContext = createContext(false);
36510
36511
 
36512
+ /**
36513
+ * What stands in the pane of a neighbour until its page does. A component of its
36514
+ * own: what the application calls in there (a hook, a signal) answers to it, however
36515
+ * many neighbours the row has at that moment.
36516
+ */
36517
+ const RouteRowNeighbour = ({
36518
+ renderNeighbour,
36519
+ page
36520
+ }) => jsx("div", {
36521
+ "data-route-row-neighbour": "",
36522
+ children: renderNeighbour(page)
36523
+ });
36524
+
36511
36525
  // A container as a row: one pane per leaf, in the order they are written. A page
36512
36526
  // still builds when its route first matches, since a route's actions only ask
36513
36527
  // while it does: before that its pane is empty. Once built it stays, with what
@@ -36520,6 +36534,9 @@ const RouteRowContext = createContext(false);
36520
36534
  const RouteRowContainer = ({
36521
36535
  children
36522
36536
  }) => {
36537
+ const {
36538
+ renderNeighbour
36539
+ } = useContext(RouteRowContext);
36523
36540
  const {
36524
36541
  activeBranch,
36525
36542
  leaves
@@ -36568,16 +36585,19 @@ const RouteRowContainer = ({
36568
36585
  shown: true
36569
36586
  }, "fallback");
36570
36587
  }
36571
- return leaves.map(leaf => jsx(RouteRowPane, {
36588
+ const shownIndex = activeBranch ? leaves.findIndex(leaf => leaf.id === activeBranch.id) : -1;
36589
+ return leaves.map((leaf, index) => jsx(RouteRowPane, {
36572
36590
  branch: leaf,
36573
36591
  page: pageOf(leaf),
36574
- shown: activeBranch !== null && leaf.id === activeBranch.id
36592
+ shown: index === shownIndex,
36593
+ renderNeighbour: shownIndex !== -1 && (index === shownIndex - 1 || index === shownIndex + 1) ? renderNeighbour : undefined
36575
36594
  }, leaf.id));
36576
36595
  };
36577
36596
  const RouteRowPane = ({
36578
36597
  branch,
36579
36598
  page,
36580
- shown
36599
+ shown,
36600
+ renderNeighbour
36581
36601
  }) => {
36582
36602
  // The very vnode rendered last, while not on screen: Preact does not look into
36583
36603
  // it, and a page nobody can see costs nothing when the address moves.
@@ -36596,7 +36616,10 @@ const RouteRowPane = ({
36596
36616
  "data-route-row-pane": "",
36597
36617
  "data-built": contentRef.current ? "" : undefined,
36598
36618
  inert: !shown,
36599
- children: contentRef.current
36619
+ children: contentRef.current || renderNeighbour && jsx(RouteRowNeighbour, {
36620
+ renderNeighbour: renderNeighbour,
36621
+ page: branchRoutePage(branch)
36622
+ })
36600
36623
  });
36601
36624
  };
36602
36625
 
@@ -40412,7 +40435,9 @@ const readRowStretch = (scroller, pages, page) => {
40412
40435
  /**
40413
40436
  * @param {Element} scroller
40414
40437
  * @param {object} options
40415
- * @param {() => void} options.onScroll - the row moved.
40438
+ * @param {(move: {finger: boolean}) => void} options.onScroll - the row moved.
40439
+ * `finger`: a touch has come since the row last stopped, so a lift will say where
40440
+ * it goes (`onLeftToSnap`). Without one (a trackpad, a wheel) nothing will.
40416
40441
  * @param {(event: Event|null) => void} options.onLand - the row may have stopped.
40417
40442
  * Read `pageAtRest` to know whether it stopped on a page. Heard more than once
40418
40443
  * for one landing, so what it does must find nothing to do the second time.
@@ -40428,11 +40453,13 @@ const watchRowScroll = (
40428
40453
  ) => {
40429
40454
  let stoppedTimeout = null;
40430
40455
  let touching = false;
40456
+ let touchedSinceStop = false;
40431
40457
  let liftedAt = null;
40432
40458
  const landOnSilence = () => {
40433
40459
  liftedAt = null;
40434
40460
  // A finger holding the row still: nothing has stopped.
40435
40461
  if (!touching) {
40462
+ touchedSinceStop = false;
40436
40463
  onLand(null);
40437
40464
  }
40438
40465
  };
@@ -40441,7 +40468,7 @@ const watchRowScroll = (
40441
40468
  stoppedTimeout = setTimeout(landOnSilence, STOPPED_SILENCE);
40442
40469
  };
40443
40470
  const onScrollEvent = () => {
40444
- onScroll();
40471
+ onScroll({ finger: touchedSinceStop });
40445
40472
  armSilence();
40446
40473
  if (!liftedAt) {
40447
40474
  return;
@@ -40457,6 +40484,7 @@ const watchRowScroll = (
40457
40484
  };
40458
40485
  const onTouchStart = () => {
40459
40486
  touching = true;
40487
+ touchedSinceStop = true;
40460
40488
  liftedAt = null;
40461
40489
  clearTimeout(stoppedTimeout);
40462
40490
  };
@@ -40484,6 +40512,8 @@ const watchRowScroll = (
40484
40512
 
40485
40513
  installImportMetaCssBuild(import.meta);
40486
40514
  const css$16 = /* css */`.navi_route_travel {
40515
+ flex-direction: column;
40516
+ display: flex;
40487
40517
  position: relative;
40488
40518
  }
40489
40519
 
@@ -40535,6 +40565,15 @@ const css$16 = /* css */`.navi_route_travel {
40535
40565
  overflow: clip;
40536
40566
  }
40537
40567
 
40568
+ & > [data-route-row-pane] > [data-route-row-neighbour] {
40569
+ contain: layout paint;
40570
+ visibility: visible;
40571
+ flex-direction: column;
40572
+ display: flex;
40573
+ position: absolute;
40574
+ inset: 0;
40575
+ }
40576
+
40538
40577
  & > [data-route-row-live] {
40539
40578
  flex-direction: column;
40540
40579
  grid-row: 1;
@@ -40567,6 +40606,7 @@ const SET_OFF_PULL = 16;
40567
40606
  * routes?: Array<object|{route: object, params?: object}>,
40568
40607
  * travelByDrag?: boolean,
40569
40608
  * onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
40609
+ * renderNeighbour?: (page: {route: object, params: object|undefined}) => import("preact").ComponentChildren,
40570
40610
  * }>}
40571
40611
  * @param {Array<object|{route: object, params?: object}>} [props.routes] - a window
40572
40612
  * over something longer, in order. Read from the <Route> children by default,
@@ -40583,6 +40623,13 @@ const SET_OFF_PULL = 16;
40583
40623
  * of the page under six entries. A tab pressed goes the way its <Link> says —
40584
40624
  * `<Link replace>` for a row of tabs, so a press and a swipe towards the same
40585
40625
  * neighbour write the same history.
40626
+ * @param {(page: {route: object, params: object|undefined}) => import("preact").ComponentChildren} [props.renderNeighbour]
40627
+ * - what stands in the pane of a page next to the one on screen, until the hand
40628
+ * has pulled far enough to bring that page: its background, its skeleton, what the
40629
+ * list already knows. A `<Route>` page already built shows itself. Receives
40630
+ * `{ route, params }`, or the entry of `routes` as it was given, and is a column
40631
+ * the height of the row, as the page is. Its route does not match while it stands
40632
+ * there: what it reads is not asked for.
40586
40633
  *
40587
40634
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`, …).
40588
40635
  * The box has to outlive every page of its row: around those <Route>s, or around
@@ -40598,6 +40645,7 @@ const RouteTravel = ({
40598
40645
  replace: true,
40599
40646
  routeTransition: "none"
40600
40647
  }),
40648
+ renderNeighbour,
40601
40649
  className,
40602
40650
  children,
40603
40651
  ...rest
@@ -40658,9 +40706,7 @@ const RouteTravel = ({
40658
40706
  travelling: false,
40659
40707
  pictures: new Map(),
40660
40708
  resumeScrollRecording: null,
40661
- scrollsDocument: getScrollContainer(row.parentElement, {
40662
- includeHidden: true
40663
- }) === document.documentElement,
40709
+ scrollsDocument: rowFollowsDocument(row),
40664
40710
  ...fields
40665
40711
  };
40666
40712
  movementRef.current = movement;
@@ -40996,6 +41042,30 @@ const RouteTravel = ({
40996
41042
  Promise.resolve().then(endMovement);
40997
41043
  }
40998
41044
  }, [currentIndex]);
41045
+
41046
+ // A neighbour shows from the first frame the compositor moves the row, before
41047
+ // any scroll event: a finger landing is the last word before that.
41048
+ const paintsNeighbours = Boolean(renderNeighbour);
41049
+ useLayoutEffect(() => {
41050
+ if (!paintsNeighbours) {
41051
+ return undefined;
41052
+ }
41053
+ const row = rowRef.current;
41054
+ const onTouchStart = () => {
41055
+ if (!movementRef.current && rowFollowsDocument(row)) {
41056
+ row.style.setProperty("--navi-route-row-pin", `${pinOf(row)}px`);
41057
+ }
41058
+ };
41059
+ row.addEventListener("touchstart", onTouchStart, {
41060
+ passive: true
41061
+ });
41062
+ return () => {
41063
+ row.removeEventListener("touchstart", onTouchStart);
41064
+ };
41065
+ }, [paintsNeighbours]);
41066
+ const rowContext = useMemo(() => ({
41067
+ renderNeighbour
41068
+ }), [renderNeighbour]);
40999
41069
  const rowKind = pages.length < 2 && windowed ? "still" : travelByDrag ? "" : "code";
41000
41070
  return jsx("div", {
41001
41071
  ...rest,
@@ -41008,12 +41078,20 @@ const RouteTravel = ({
41008
41078
  ,
41009
41079
  "data-travel-by-drag": travelByDrag && rowKind !== "still" ? "x" : undefined,
41010
41080
  children: windowed ? jsxs(Fragment, {
41011
- children: [pages.map((page, index) => jsx("div", {
41012
- "data-route-row-pane": "",
41013
- style: {
41014
- gridColumn: index + 1
41015
- }
41016
- }, index)), jsx("div", {
41081
+ children: [pages.map((page, index) => {
41082
+ const holdsNeighbour = paintsNeighbours && isNeighbourPane(index, liveIndex, movementRef.current);
41083
+ return jsx("div", {
41084
+ "data-route-row-pane": "",
41085
+ inert: holdsNeighbour,
41086
+ style: {
41087
+ gridColumn: index + 1
41088
+ },
41089
+ children: holdsNeighbour && jsx(RouteRowNeighbour, {
41090
+ renderNeighbour: renderNeighbour,
41091
+ page: page
41092
+ })
41093
+ }, index);
41094
+ }), jsx("div", {
41017
41095
  ref: liveRef,
41018
41096
  "data-route-row-live": "",
41019
41097
  style: {
@@ -41025,13 +41103,25 @@ const RouteTravel = ({
41025
41103
  "data-route-row-pictures": ""
41026
41104
  })]
41027
41105
  }) : jsx(RouteRowContext.Provider, {
41028
- value: true,
41106
+ value: rowContext,
41029
41107
  children: children
41030
41108
  })
41031
41109
  })
41032
41110
  });
41033
41111
  };
41034
41112
 
41113
+ // The panes on each side of the live page. Not one holding a picture of the page
41114
+ // left there: a page with no background of its own would show it through.
41115
+ const isNeighbourPane = (index, liveIndex, movement) => {
41116
+ if (index !== liveIndex - 1 && index !== liveIndex + 1) {
41117
+ return false;
41118
+ }
41119
+ return !movement || !movement.pictures.has(index);
41120
+ };
41121
+ const rowFollowsDocument = row => getScrollContainer(row.parentElement, {
41122
+ includeHidden: true
41123
+ }) === document.documentElement;
41124
+
41035
41125
  // How far down the row the reader is looking, for a page brought in: a page on
41036
41126
  // screen scrolls with the document, and one arriving has to start from there.
41037
41127
  const pinOf = row => {
@@ -56513,6 +56603,10 @@ const SLIDE_TOWARD_ATTRIBUTE = "data-slide-travel-toward";
56513
56603
  // one: a hand on a row the browser scrolls can carry it past several slides, and
56514
56604
  // what follows it goes on between the two it is standing between.
56515
56605
  const SLIDE_BASE_ATTRIBUTE = "data-slide-travel-base";
56606
+ // The slide the row is on its way to while the current one has not changed yet,
56607
+ // because that only happens at rest: nothing is rendered and no address is written
56608
+ // under a snap, and what says where one is should not wait for it.
56609
+ const SLIDE_HEADING_ATTRIBUTE = "data-slide-heading";
56516
56610
  // Every way out that would DO something right now, in the words the commands
56517
56611
  // use: a direction ("left", "right", "up", "down") when there is a slide that
56518
56612
  // way, plus "first" and "last" when one is not already standing at that end of
@@ -56547,9 +56641,16 @@ const wordsOf = (element, attribute) => {
56547
56641
  return value ? value.split(" ") : NO_WAYS;
56548
56642
  };
56549
56643
 
56644
+ // The slide to say one is on: for whatever tells the reader where they are, as
56645
+ // opposed to data-slide-current, which is what a travel departs from.
56646
+ const readSlideOnScreen = (element) =>
56647
+ element.getAttribute(SLIDE_HEADING_ATTRIBUTE) ??
56648
+ element.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ??
56649
+ undefined;
56650
+
56550
56651
  const readSlideContainerState = (element) => ({
56551
56652
  known: true,
56552
- current: element.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined,
56653
+ current: readSlideOnScreen(element),
56553
56654
  toward: element.getAttribute(SLIDE_TOWARD_ATTRIBUTE) ?? undefined,
56554
56655
  // In DOM order — which is the order of the WALK for a line, and not
56555
56656
  // necessarily for a map: there the order is the one the areas are written in
@@ -56587,11 +56688,14 @@ const sameSlideContainerState = (a, b) =>
56587
56688
  * can: (wayOut: "left"|"right"|"up"|"down"|"first"|"last") => boolean,
56588
56689
  * held: (wayOut: "left"|"right"|"up"|"down"|"first"|"last") => boolean,
56589
56690
  * }} where the box stands. `current` is the slide on screen — the one being
56590
- * travelled TO while a travel plays, because that is what one is looking at;
56691
+ * travelled TO while a travel plays, and the one the row is going to once a hand
56692
+ * has let go, because that is what one is looking at. It can come before the
56693
+ * slide's own signal and `onCurrentChange`, which wait for the row to rest.
56591
56694
  * `toward` is the other slide in the frame while the picture is between two,
56592
- * and nothing at rest. `can` is "asking for it would do something", `held` is
56593
- * "it is there and this slide says no" — a way out is dead in both cases, and
56594
- * only the second is worth a word to the reader.
56695
+ * and nothing at rest. `can` is "asking for it would do something", from the
56696
+ * slide a travel would depart from, `held` is "it is there and this slide says
56697
+ * no" — a way out is dead in both cases, and only the second is worth a word to
56698
+ * the reader.
56595
56699
  * `known` is false until the box has been read: no box was named and none is
56596
56700
  * above, the id names nothing, or — for one commit — this mounted before the
56597
56701
  * box had painted. Until then `can` answers YES, because it is the answer that
@@ -57445,7 +57549,7 @@ const Nav = ({
57445
57549
  }
57446
57550
  slideContainerElementRef.current = containerElement;
57447
57551
  const readContainer = () => {
57448
- setCurrentSlideArea(containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined);
57552
+ setCurrentSlideArea(readSlideOnScreen(containerElement));
57449
57553
  paintIndicatorGeometryRef.current();
57450
57554
  scheduleTabScrollSyncRef.current();
57451
57555
  };
@@ -68444,6 +68548,20 @@ const SlideContainer = ({
68444
68548
  const paintCurrentArea = area => {
68445
68549
  containerRef.current?.setAttribute(SLIDE_CURRENT_ATTRIBUTE, area);
68446
68550
  };
68551
+
68552
+ // Where the row is going, for as long as the current slide has not changed:
68553
+ // anything saying where one is reads this first (see readSlideOnScreen). Left to
68554
+ // the caller to announce, a landing having more to say in the same breath.
68555
+ const writeHeading = headingElement => {
68556
+ const containerEl = containerRef.current;
68557
+ const area = headingElement ? readArea(headingElement) : null;
68558
+ // A window does not change slide, it rolls: it has nowhere to be heading.
68559
+ if (loop || area === null || area === containerEl.getAttribute(SLIDE_CURRENT_ATTRIBUTE)) {
68560
+ containerEl.removeAttribute(SLIDE_HEADING_ATTRIBUTE);
68561
+ return;
68562
+ }
68563
+ containerEl.setAttribute(SLIDE_HEADING_ATTRIBUTE, area);
68564
+ };
68447
68565
  const markAnswered = area => {
68448
68566
  const order = readMap().slideElements.map(readArea);
68449
68567
  const rank = order.indexOf(area);
@@ -68649,6 +68767,10 @@ const SlideContainer = ({
68649
68767
  slideElements[0];
68650
68768
  const currentArea = readArea(currentElement);
68651
68769
  paintCurrentArea(currentArea);
68770
+ // In the commit the slide changes in, so nothing reads the slide left in between.
68771
+ if (!handMovingRef.current) {
68772
+ writeHeading(null);
68773
+ }
68652
68774
  const realPlaceOf = area => placeOf.get(area) || {
68653
68775
  x: 0,
68654
68776
  y: 0
@@ -68681,6 +68803,8 @@ const SlideContainer = ({
68681
68803
  const drawnArea = stage ? stage.area : drawnAreaRef.current;
68682
68804
  const travelStarts = !still && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
68683
68805
  if (travelStarts) {
68806
+ // Asked for over a row still snapping: the travel is what one will see.
68807
+ writeHeading(null);
68684
68808
  // Where the slide being left IS — the place the stage gave it if a travel
68685
68809
  // was already playing (a press landing mid-travel departs from where the
68686
68810
  // eye is, not from the map), its own place otherwise.
@@ -69447,7 +69571,9 @@ const SlideContainer = ({
69447
69571
  // Where a hand has the row, in the terms of --slide-travel-progress: from the
69448
69572
  // current slide while it stands next to it, and past that between the two it
69449
69573
  // stands between, as far as the hand carries it.
69450
- const paintNativeRow = () => {
69574
+ const paintNativeRow = ({
69575
+ finger
69576
+ }) => {
69451
69577
  if (trackAnimationRef.current?.playState === "running") {
69452
69578
  return;
69453
69579
  }
@@ -69459,6 +69585,9 @@ const SlideContainer = ({
69459
69585
  }
69460
69586
  if (restsOn(containerEl, currentElement)) {
69461
69587
  if (handMovingRef.current) {
69588
+ if (!finger) {
69589
+ writeHeading(null);
69590
+ }
69462
69591
  paintTravelProgress();
69463
69592
  }
69464
69593
  return;
@@ -69479,6 +69608,12 @@ const SlideContainer = ({
69479
69608
  }
69480
69609
  element.style.setProperty("--slide-travel-progress", forward ? -fraction : fraction);
69481
69610
  }
69611
+ // Nothing will say when a trackpad's fingers leave, and a long stroke would
69612
+ // keep the first tab lit for the whole trip: the nearest slide answers. A finger
69613
+ // has a lift to wait for (headNativeRow), and until then the slide held stands.
69614
+ if (!finger) {
69615
+ writeHeading(fraction >= 0.5 && toward ? toward : base);
69616
+ }
69482
69617
  paintTravelToward(toward ? readArea(toward) : null);
69483
69618
  // What the hand reaches ahead of the neighbours built at rest is asked for on
69484
69619
  // the way: an empty slide goes by otherwise.
@@ -69486,6 +69621,28 @@ const SlideContainer = ({
69486
69621
  alongside: true
69487
69622
  });
69488
69623
  };
69624
+
69625
+ // A finger has lifted and the row moves: on past the slide it stands after, or
69626
+ // back to it. Already standing on one, it has arrived, whichever way it came.
69627
+ const headNativeRow = movedForward => {
69628
+ if (!handMovingRef.current || trackAnimationRef.current?.playState === "running") {
69629
+ return;
69630
+ }
69631
+ const slideElements = Array.from(trackRef.current.children);
69632
+ const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
69633
+ if (!currentElement) {
69634
+ return;
69635
+ }
69636
+ const {
69637
+ base,
69638
+ toward,
69639
+ fraction,
69640
+ forward
69641
+ } = readRowStretch(containerRef.current, slideElements, currentElement);
69642
+ const goesOn = fraction > 0 && movedForward === forward;
69643
+ writeHeading(goesOn && toward ? toward : base);
69644
+ announceState();
69645
+ };
69489
69646
  const landNativeRow = scrollEndEvent => {
69490
69647
  if (!handMovingRef.current) {
69491
69648
  return;
@@ -69500,13 +69657,15 @@ const SlideContainer = ({
69500
69657
  return;
69501
69658
  }
69502
69659
  handMovingRef.current = false;
69503
- paintTravelProgress();
69504
69660
  const area = readArea(landedElement);
69505
69661
  const currentArea = containerEl.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
69506
69662
  if (area === currentArea) {
69663
+ writeHeading(null);
69664
+ paintTravelProgress();
69507
69665
  settleTravel();
69508
69666
  return;
69509
69667
  }
69668
+ paintTravelProgress();
69510
69669
  const event = scrollEndEvent ?? new Event("scrollend");
69511
69670
  if (loop) {
69512
69671
  // A window does not change slide, it rolls: back at rest in the render the
@@ -69537,6 +69696,8 @@ const SlideContainer = ({
69537
69696
  }
69538
69697
  // Refused: the row is put back in view of the slide that stays.
69539
69698
  handLandedRef.current = null;
69699
+ writeHeading(null);
69700
+ announceState();
69540
69701
  const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
69541
69702
  containerEl.scrollTo({
69542
69703
  left: currentElement.offsetLeft,
@@ -69550,11 +69711,13 @@ const SlideContainer = ({
69550
69711
  const nativeRowHandlersRef = useRef(null);
69551
69712
  nativeRowHandlersRef.current = {
69552
69713
  paintNativeRow,
69714
+ headNativeRow,
69553
69715
  landNativeRow
69554
69716
  };
69555
69717
  useLayoutEffect(() => {
69556
69718
  return watchRowScroll(containerRef.current, {
69557
- onScroll: () => nativeRowHandlersRef.current.paintNativeRow(),
69719
+ onScroll: move => nativeRowHandlersRef.current.paintNativeRow(move),
69720
+ onLeftToSnap: forward => nativeRowHandlersRef.current.headNativeRow(forward),
69558
69721
  onLand: event => nativeRowHandlersRef.current.landNativeRow(event)
69559
69722
  });
69560
69723
  }, []);