@jsenv/navi 0.29.50 → 0.29.52

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.
@@ -49186,6 +49186,21 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
49186
49186
  * a component of your own wrapping one. Nothing here assumes the children ARE
49187
49187
  * the slides, so nothing breaks when they are not.
49188
49188
  *
49189
+ * Only slides go in it, so everything drawn AROUND the travel is written around
49190
+ * the box and reaches it by id: a chevron pinned to the edge of a full-screen
49191
+ * viewer, a "3 / 8" counter under it, a tab bar. Two things are said to such an
49192
+ * element, and both are needed for it to feel part of the same thing:
49193
+ * `commandFor={id}` on a button, which is how it asks for a travel
49194
+ * (--navi-left/--navi-right/--navi-first/…), and
49195
+ * `data-slide-container-follows={id}` on whatever holds them, which makes it a
49196
+ * follower: the travel's progress is painted onto it to draw with, and the
49197
+ * arrows walk the slides wherever the focus is inside it — otherwise they stop
49198
+ * working the moment one Tabs onto the chevron that walks them. Say it on the
49199
+ * outermost element of the surface — the <Dialog> itself rather than a box
49200
+ * inside it — since that is what holds the keyboard when nothing in it does.
49201
+ * <Nav slideContainer={id}> is a tab bar built out of exactly that. See the
49202
+ * full-screen section of the demo.
49203
+ *
49189
49204
  * @param {object} props
49190
49205
  * @param {"row"|"column"|string[]} [props.layout="row"] - where the slides are.
49191
49206
  * A word for a line — "row" to the right, "column" downwards, both in DOM
@@ -50699,6 +50714,38 @@ const SlideContainer = ({
50699
50714
  handler: e => travelled(goToEnd(true, e))
50700
50715
  }
50701
50716
  });
50717
+ // …and they are the same shortcuts wherever they are pressed in what follows
50718
+ // this box: a shortcut only ever reaches what has the focus, and a way out
50719
+ // drawn BESIDE the container — a chevron in the column next to it, a counter
50720
+ // one can Tab to — is not inside it. So the arrows that walk the slides stop
50721
+ // walking the moment one Tabs onto the chevron that walks them. A follower
50722
+ // already says which box it is about (data-slide-container-follows), which is
50723
+ // the same thing said for the same reason.
50724
+ // Read on every render: a follower appearing is not a render of this box, and
50725
+ // the list is refreshed by the layout effect above, which runs first.
50726
+ useLayoutEffect(() => {
50727
+ const followerElements = followerElementsRef.current;
50728
+ if (followerElements.length === 0) {
50729
+ return undefined;
50730
+ }
50731
+ const onFollowerKeyDown = keyDownEvent => {
50732
+ // Already answered: a follower AROUND this box — the frame of a
50733
+ // full-screen surface, holding the slides and the ways out — hears the
50734
+ // same press bubbling out of it.
50735
+ if (containerRef.current?.contains(keyDownEvent.target)) {
50736
+ return;
50737
+ }
50738
+ onKeyDownShortcuts(keyDownEvent);
50739
+ };
50740
+ for (const followerElement of followerElements) {
50741
+ followerElement.addEventListener("keydown", onFollowerKeyDown);
50742
+ }
50743
+ return () => {
50744
+ for (const followerElement of followerElements) {
50745
+ followerElement.removeEventListener("keydown", onFollowerKeyDown);
50746
+ }
50747
+ };
50748
+ });
50702
50749
  return (
50703
50750
  // Box rather than a plain div: it is how every navi component takes the
50704
50751
  // onnavi_* handlers below — they are navi's own event names, and Box is
@@ -55540,10 +55587,13 @@ const ListFirstResolver = props => {
55540
55587
  * thread read backwards — the last rows are the ones to show, and the ones
55541
55588
  * asked for first. A number opens on that row of the collection. `{id,
55542
55589
  * offset}` — what `onScrolledChange` hands out — opens on a NAMED row,
55543
- * `offset` pixels below the top of the view: the row is asked for by name
55544
- * (see the range's own `around`), then put back by MEASURING it, so it lands
55545
- * where it was even if rows were inserted before it, and whatever the screen
55546
- * it was saved on.
55590
+ * `offset` pixels below where the row would land on its own: the row is asked
55591
+ * for by name (see the range's own `around`), then put back by MEASURING it,
55592
+ * so it lands where it was even if rows were inserted before it, and whatever
55593
+ * the screen it was saved on. `offset: 0` is where a `scrollIntoView()` puts
55594
+ * it — in front of the fixed bar the scroller gives room for, below the
55595
+ * sticky header and the group label the row lives under — so nothing of that
55596
+ * room has to be restated as a number by whoever asks.
55547
55597
  * @param {"start"|"end"|number|{id: string, offset?: number}} [props.scrolled]
55548
55598
  * The same, but held: the list goes back there every time this changes, even
55549
55599
  * after the user has scrolled — the caller owns where the list is (see
@@ -55557,7 +55607,8 @@ const ListFirstResolver = props => {
55557
55607
  * user reaches for the list.
55558
55608
  * @param {(scrolled: {id: string, index: number, offset: number}) => void} [props.onScrolledChange]
55559
55609
  * Where the list is, as the user scrolls: the row at the top of the view and
55560
- * how far below the top of the view it starts. Keep it to come back to it
55610
+ * how far below the place a row lands on its own (see `defaultScrolled`) it
55611
+ * starts. Keep it to come back to it
55561
55612
  * later through `scrolled`/`defaultScrolled` — an index would not do, since
55562
55613
  * rows get inserted while a list is being read.
55563
55614
  * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
@@ -56168,7 +56219,7 @@ const useListScrollSync = ({
56168
56219
  }
56169
56220
  const viewportRect = getScrollerViewportRect(scrollerEl);
56170
56221
  const rowRect = rowEl.getBoundingClientRect();
56171
- const offsetWanted = resolveOpenOffset(openAt.offset || 0, horizontal ? viewportRect.width : viewportRect.height, horizontal ? rowRect.width : rowRect.height);
56222
+ const offsetWanted = resolveOpenOffset(getRowScrollInset(scrollerEl, rowEl, horizontal) + (openAt.offset || 0), horizontal ? viewportRect.width : viewportRect.height, horizontal ? rowRect.width : rowRect.height);
56172
56223
  const offsetNow = horizontal ? rowRect.left - viewportRect.left : rowRect.top - viewportRect.top;
56173
56224
  const delta = offsetNow - offsetWanted;
56174
56225
  if (delta > -0.5 && delta < 0.5) {
@@ -56245,10 +56296,14 @@ const useListScrollSync = ({
56245
56296
  return;
56246
56297
  }
56247
56298
  positionRef.current = position;
56248
- onScrolledChangeRef.current?.({
56299
+ if (!onScrolledChangeRef.current) {
56300
+ return;
56301
+ }
56302
+ const rowEl = findRowElement(getListEl(), position.id);
56303
+ onScrolledChangeRef.current({
56249
56304
  id: position.id,
56250
56305
  index: position.index,
56251
- offset: position.offset
56306
+ offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
56252
56307
  });
56253
56308
  };
56254
56309
 
@@ -56713,6 +56768,38 @@ const resolveOpenOffset = (offset, viewportSize, rowSize) => {
56713
56768
  return offset;
56714
56769
  };
56715
56770
 
56771
+ // The room a row must be given at the top (or left) of the view: the
56772
+ // scroller's own scroll-padding — where a fixed bar publishes the space it
56773
+ // takes — plus the row's scroll-margin, where the list puts its sticky header
56774
+ // and the height of the group label it lives under. `scrollIntoView()` on a row
56775
+ // lands past both; a position given as `{id, offset}` means the same place, so
56776
+ // `offset` is the caller's own few pixels and not a number restating what the
56777
+ // CSS already measures.
56778
+ const getRowScrollInset = (scrollerEl, rowEl, horizontal) => {
56779
+ if (!rowEl) {
56780
+ return 0;
56781
+ }
56782
+ const viewportRect = getScrollerViewportRect(scrollerEl);
56783
+ const viewportSize = horizontal ? viewportRect.width : viewportRect.height;
56784
+ const scrollerStyle = window.getComputedStyle(scrollerEl);
56785
+ const rowStyle = window.getComputedStyle(rowEl);
56786
+ const scrollPadding = resolveScrollInset(horizontal ? scrollerStyle.scrollPaddingLeft : scrollerStyle.scrollPaddingTop, viewportSize);
56787
+ const scrollMargin = resolveScrollInset(horizontal ? rowStyle.scrollMarginLeft : rowStyle.scrollMarginTop, viewportSize);
56788
+ return scrollPadding + scrollMargin;
56789
+ };
56790
+ // scroll-padding is a length, a percentage of the scrollport, or "auto" (the
56791
+ // browser decides, which for placing a row means nothing).
56792
+ const resolveScrollInset = (value, viewportSize) => {
56793
+ const number = parseFloat(value);
56794
+ if (!number) {
56795
+ return 0;
56796
+ }
56797
+ if (value.endsWith("%")) {
56798
+ return number * viewportSize / 100;
56799
+ }
56800
+ return number;
56801
+ };
56802
+
56716
56803
  // The row with that id, IN THIS LIST. Not document.getElementById: an id is
56717
56804
  // only ever unique within a list — two lists on the same page can be showing
56718
56805
  // the same collection — and a list acting on a row that belongs to another one
@@ -57846,6 +57933,7 @@ const VISIBILITY_HIDDEN_STYLE = {
57846
57933
  * count?: number,
57847
57934
  * groupBy?: (item: any, index: number) => any,
57848
57935
  * renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
57936
+ * groupLabelProps?: (item: any, index: number) => object,
57849
57937
  * pageSize?: number,
57850
57938
  * memoryBudget?: number,
57851
57939
  * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
@@ -57863,6 +57951,12 @@ const VISIBILITY_HIDDEN_STYLE = {
57863
57951
  * the only way a list that discovers its rows page by page can have any.
57864
57952
  * @param {(item: any, index: number) => any} [props.renderGroupLabel]
57865
57953
  * The label of the group a row opens, given that row.
57954
+ * @param {(item: any, index: number) => object} [props.groupLabelProps]
57955
+ * The props the label of the group a row opens carries — `class`,
57956
+ * `data-*`, anything a `<span>` takes. For a label that says something about
57957
+ * its group (a day behind us, today, one ahead) rather than just naming it:
57958
+ * the state then sits on the element the CSS styles, instead of being read
57959
+ * back from a child.
57866
57960
  * @param {number} [props.pageSize]
57867
57961
  * How many rows to ask for at a time. A turn of the wheel opens a hole three
57868
57962
  * rows wide; asking for exactly that would ask again at the next turn.
@@ -57890,6 +57984,7 @@ const ListItems = ({
57890
57984
  memoryBudget,
57891
57985
  groupBy,
57892
57986
  renderGroupLabel,
57987
+ groupLabelProps,
57893
57988
  renderSkeleton,
57894
57989
  renderError
57895
57990
  }) => {
@@ -58029,6 +58124,7 @@ const ListItems = ({
58029
58124
  }
58030
58125
  rows.push(jsx(ListItemGroup, {
58031
58126
  label: group.label,
58127
+ labelProps: group.labelProps,
58032
58128
  children: group.children
58033
58129
  }, `${ownerId}_group_${group.key}`));
58034
58130
  group = null;
@@ -58045,6 +58141,7 @@ const ListItems = ({
58045
58141
  group = {
58046
58142
  key: groupKey,
58047
58143
  label: renderGroupLabel ? renderGroupLabel(item, rowIndex) : groupKey,
58144
+ labelProps: groupLabelProps ? groupLabelProps(item, rowIndex) : undefined,
58048
58145
  children: []
58049
58146
  };
58050
58147
  }
@@ -58485,6 +58582,7 @@ const useItemStore = ({
58485
58582
  */
58486
58583
  const ListItemGroup = ({
58487
58584
  label,
58585
+ labelProps,
58488
58586
  hiddenWhileEmpty,
58489
58587
  children,
58490
58588
  ...rest
@@ -58510,6 +58608,11 @@ const ListItemGroup = ({
58510
58608
  groupEl.style.setProperty("--list-group-label-height", `${rect.height}px`);
58511
58609
  groupEl.style.setProperty("--list-group-label-width", `${rect.width}px`);
58512
58610
  }, []);
58611
+ const {
58612
+ className: labelClassName,
58613
+ class: labelClass,
58614
+ ...labelRest
58615
+ } = labelProps || {};
58513
58616
  return jsxs(ListItem, {
58514
58617
  ...rest,
58515
58618
  ref: groupRef,
@@ -58517,9 +58620,10 @@ const ListItemGroup = ({
58517
58620
  role: "presentation",
58518
58621
  "data-hidden-while-empty": hiddenWhileEmpty ? "" : undefined,
58519
58622
  children: [jsx("span", {
58623
+ ...labelRest,
58520
58624
  ref: labelRef,
58521
58625
  id: groupId,
58522
- className: "navi_list_item_group_label",
58626
+ className: withPropsClassName("navi_list_item_group_label", labelClassName || labelClass),
58523
58627
  role: "presentation",
58524
58628
  "aria-hidden": labelHidden ? "true" : undefined,
58525
58629
  inert: labelHidden ? true : undefined