@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.
- package/dist/jsenv_navi.js +113 -9
- package/dist/jsenv_navi.js.map +22 -5
- package/docs/drag_to_travel.md +17 -0
- package/docs/scroll.md +5 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -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
|
|
55544
|
-
* (see the range's own `around`), then put back by MEASURING it,
|
|
55545
|
-
* where it was even if rows were inserted before it, and whatever
|
|
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
|
|
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
|