@jsenv/navi 0.27.83 → 0.27.85
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 +100 -40
- package/dist/jsenv_navi.js.map +12 -11
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -6788,10 +6788,7 @@ const TYPO_PROPS = {
|
|
|
6788
6788
|
// combo from MDN's own text-box example, most useful for compact things
|
|
6789
6789
|
// like buttons/badges/labels. Use textBox/textBoxTrim/textBoxEdge
|
|
6790
6790
|
// directly for any other combination.
|
|
6791
|
-
textBoxCrop: applyToCssPropWhenTruthy(
|
|
6792
|
-
"textBox",
|
|
6793
|
-
"trim-both cap alphabetic",
|
|
6794
|
-
),
|
|
6791
|
+
textBoxCrop: applyToCssPropWhenTruthy("textBox", "trim-both cap alphabetic"),
|
|
6795
6792
|
};
|
|
6796
6793
|
const VISUAL_PROPS = {
|
|
6797
6794
|
outline: PASS_THROUGH,
|
|
@@ -6827,6 +6824,8 @@ const VISUAL_PROPS = {
|
|
|
6827
6824
|
overflowY: PASS_THROUGH,
|
|
6828
6825
|
objectFit: PASS_THROUGH,
|
|
6829
6826
|
accentColor: PASS_THROUGH,
|
|
6827
|
+
scrollbarWidth: PASS_THROUGH,
|
|
6828
|
+
scrollbarGutter: PASS_THROUGH,
|
|
6830
6829
|
};
|
|
6831
6830
|
const CONTENT_PROPS = {
|
|
6832
6831
|
align: applyOnTwoProps("alignX", "alignY"),
|
|
@@ -40526,7 +40525,11 @@ const createItemTracker = (onChange) => {
|
|
|
40526
40525
|
}
|
|
40527
40526
|
|
|
40528
40527
|
// Build allItems and visibleItems in a single pass over allOrderedKeys.
|
|
40529
|
-
// Visible items are those without data.hidden — same
|
|
40528
|
+
// Visible items are those without data.hidden or data.filtered — same
|
|
40529
|
+
// relative order as orderedKeys (syncItem already excludes both from
|
|
40530
|
+
// orderedKeys; this must match or consumers relying on visibleCountSignal
|
|
40531
|
+
// for virtual-scroll accounting, e.g. list.jsx's filler sizing, would
|
|
40532
|
+
// count filtered-out items as if they still took up space).
|
|
40530
40533
|
const prevAllItems = itemsSignal.peek();
|
|
40531
40534
|
const prevVisibleItems = visibleItemsSignal.peek();
|
|
40532
40535
|
let allItemsChanged = prevAllItems.length !== allOrderedKeys.length;
|
|
@@ -40545,7 +40548,7 @@ const createItemTracker = (onChange) => {
|
|
|
40545
40548
|
if (item.match === false) {
|
|
40546
40549
|
newNoMatchCount++;
|
|
40547
40550
|
}
|
|
40548
|
-
if (!item.hidden) {
|
|
40551
|
+
if (!item.hidden && !item.filtered) {
|
|
40549
40552
|
const visibleIdx = visibleItems.length;
|
|
40550
40553
|
visibleItems.push(item);
|
|
40551
40554
|
if (!visibleItemsChanged && item !== prevVisibleItems[visibleIdx]) {
|
|
@@ -41269,10 +41272,9 @@ const ListItemSelectable = props => {
|
|
|
41269
41272
|
const {
|
|
41270
41273
|
index,
|
|
41271
41274
|
id = defaultId,
|
|
41272
|
-
|
|
41275
|
+
matchInfo,
|
|
41273
41276
|
hidden,
|
|
41274
41277
|
filtered,
|
|
41275
|
-
matchScore,
|
|
41276
41278
|
defaultSelected,
|
|
41277
41279
|
selected,
|
|
41278
41280
|
pointed,
|
|
@@ -41314,10 +41316,9 @@ const ListItemSelectable = props => {
|
|
|
41314
41316
|
return jsxs(Next, {
|
|
41315
41317
|
id: id,
|
|
41316
41318
|
index: index,
|
|
41317
|
-
|
|
41319
|
+
matchInfo: matchInfo,
|
|
41318
41320
|
filtered: filtered,
|
|
41319
41321
|
hidden: hidden,
|
|
41320
|
-
matchScore: matchScore,
|
|
41321
41322
|
"aria-selected": checked,
|
|
41322
41323
|
selected: checked,
|
|
41323
41324
|
"navi-selectable": "",
|
|
@@ -41631,6 +41632,7 @@ const css$m = /* css */`
|
|
|
41631
41632
|
flex-wrap: inherit;
|
|
41632
41633
|
overflow: auto;
|
|
41633
41634
|
overscroll-behavior: inherit; /* inherit select behavior */
|
|
41635
|
+
scrollbar-width: inherit;
|
|
41634
41636
|
}
|
|
41635
41637
|
|
|
41636
41638
|
&[data-expand-x] {
|
|
@@ -41922,7 +41924,7 @@ const ListUI = props => {
|
|
|
41922
41924
|
import.meta.css = [css$m, "@jsenv/navi/src/control/list/list.jsx"];
|
|
41923
41925
|
const {
|
|
41924
41926
|
ref,
|
|
41925
|
-
renderBudget = RENDER_BUDGET_DEFAULT,
|
|
41927
|
+
renderBudget: renderBudgetProp = RENDER_BUDGET_DEFAULT,
|
|
41926
41928
|
renderBudgetSkipCheck,
|
|
41927
41929
|
role,
|
|
41928
41930
|
fallback,
|
|
@@ -41943,6 +41945,15 @@ const ListUI = props => {
|
|
|
41943
41945
|
spacing,
|
|
41944
41946
|
...rest
|
|
41945
41947
|
} = props;
|
|
41948
|
+
// Accept a string (e.g. from an HTML attribute: renderBudget="50") the
|
|
41949
|
+
// same way a bare number would work — arithmetic below (renderBudget / 2,
|
|
41950
|
+
// start + renderBudget, etc.) would silently misbehave on a raw string
|
|
41951
|
+
// ("+" concatenates instead of adding).
|
|
41952
|
+
let renderBudget = renderBudgetProp;
|
|
41953
|
+
if (typeof renderBudget === "string") {
|
|
41954
|
+
const parsed = Number(renderBudget);
|
|
41955
|
+
renderBudget = Number.isFinite(parsed) ? parsed : RENDER_BUDGET_DEFAULT;
|
|
41956
|
+
}
|
|
41946
41957
|
if (renderBudget < 30 && !renderBudgetSkipCheck) {
|
|
41947
41958
|
console.warn(`List: renderBudget=${renderBudget} is too low. A renderBudget below 30 is not supported: on large screens or when the list grows, items outside the window would appear as blank space instead of rendered content. Use a value of at least 30, or omit the prop to use the default (${RENDER_BUDGET_DEFAULT}).`);
|
|
41948
41959
|
}
|
|
@@ -42072,7 +42083,7 @@ const ListFirstResolver = props => {
|
|
|
42072
42083
|
* action?: (value: any) => void,
|
|
42073
42084
|
* uiAction?: (value: any) => void,
|
|
42074
42085
|
* popover?: boolean,
|
|
42075
|
-
* renderBudget?: number,
|
|
42086
|
+
* renderBudget?: number | string,
|
|
42076
42087
|
* virtualItemSize?: number,
|
|
42077
42088
|
* fallback?: import("ignore:preact").ComponentChildren,
|
|
42078
42089
|
* searchFallback?: import("ignore:preact").ComponentChildren,
|
|
@@ -42333,6 +42344,7 @@ const useListScrollSync = ({
|
|
|
42333
42344
|
// (pour l'instant pas grave car on travaille pour le mode select qui fermera le dialog au select)
|
|
42334
42345
|
const savedScrollRef = useRef(null);
|
|
42335
42346
|
const topMatchScoresKeyRef = useRef("");
|
|
42347
|
+
const restoreScrollRafRef = useRef(null);
|
|
42336
42348
|
useLayoutEffect(() => {
|
|
42337
42349
|
const listScrollContainerEl = ref.current?.querySelector(`.navi_list_scroll_container`);
|
|
42338
42350
|
if (!listScrollContainerEl) {
|
|
@@ -42349,7 +42361,20 @@ const useListScrollSync = ({
|
|
|
42349
42361
|
savedScrollRef.current = null;
|
|
42350
42362
|
debugScroll("Restoring scroll to", savedScroll);
|
|
42351
42363
|
updateRenderWindow(savedScroll.renderWindow.start, savedScroll.renderWindow.end, "restore scroll window");
|
|
42352
|
-
|
|
42364
|
+
// Tracked in a ref rather than cancelled via this effect's own cleanup:
|
|
42365
|
+
// updateRenderWindow above triggers a re-render, which re-runs this
|
|
42366
|
+
// effect (it has no dependency array — it needs to reactively poll
|
|
42367
|
+
// tracker state on every render) *before* the RAF below fires. That
|
|
42368
|
+
// second invocation sees savedScrollRef.current already nulled and
|
|
42369
|
+
// bails out early — if the RAF were tied to this invocation's cleanup,
|
|
42370
|
+
// it would get cancelled right there with nothing to replace it,
|
|
42371
|
+
// silently dropping the scroll restore (renderWindow ends up correct,
|
|
42372
|
+
// but scrollTop stays wherever it was, showing blank filler space).
|
|
42373
|
+
if (restoreScrollRafRef.current) {
|
|
42374
|
+
cancelAnimationFrame(restoreScrollRafRef.current);
|
|
42375
|
+
}
|
|
42376
|
+
restoreScrollRafRef.current = requestAnimationFrame(() => {
|
|
42377
|
+
restoreScrollRafRef.current = null;
|
|
42353
42378
|
const left = savedScroll.left;
|
|
42354
42379
|
const top = savedScroll.top;
|
|
42355
42380
|
// use scrollTo to respect eventual css scroll-behavior: smooth;
|
|
@@ -42372,13 +42397,11 @@ const useListScrollSync = ({
|
|
|
42372
42397
|
event: new CustomEvent("navi_scroll_restore")
|
|
42373
42398
|
});
|
|
42374
42399
|
});
|
|
42375
|
-
return
|
|
42376
|
-
cancelAnimationFrame(raf);
|
|
42377
|
-
};
|
|
42400
|
+
return undefined;
|
|
42378
42401
|
}
|
|
42379
42402
|
const visibleItems = tracker.visibleItemsSignal.peek();
|
|
42380
42403
|
const topItems = visibleItems.slice(0, renderBudget);
|
|
42381
|
-
const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchScore ?? ""}`).join(",");
|
|
42404
|
+
const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchInfo?.matchScore ?? ""}`).join(",");
|
|
42382
42405
|
const currentTopMatchScore = topMatchScoresKeyRef.current;
|
|
42383
42406
|
if (topMatchScoresKey === currentTopMatchScore) {
|
|
42384
42407
|
// no changes in top matches -> no need to scroll
|
|
@@ -42487,7 +42510,10 @@ const getScrollInfo = (scrollValues, listScrollContainerEl, tracker, virtualItem
|
|
|
42487
42510
|
break;
|
|
42488
42511
|
}
|
|
42489
42512
|
}
|
|
42490
|
-
|
|
42513
|
+
// Shared by the "hit a filler" and "hit nothing at all" cases below: both
|
|
42514
|
+
// mean we don't know the real on-screen index, only the scroll position,
|
|
42515
|
+
// so estimate from it rather than assume nothing changed.
|
|
42516
|
+
const estimateFromScrollPos = reasonPrefix => {
|
|
42491
42517
|
const virtualItemSize = virtualItemSizeSignal.peek();
|
|
42492
42518
|
if (virtualItemSize === 0) {
|
|
42493
42519
|
return null;
|
|
@@ -42497,8 +42523,11 @@ const getScrollInfo = (scrollValues, listScrollContainerEl, tracker, virtualItem
|
|
|
42497
42523
|
return {
|
|
42498
42524
|
item: items[index],
|
|
42499
42525
|
index,
|
|
42500
|
-
reason:
|
|
42526
|
+
reason: `${reasonPrefix}, estimated at ${index} (${items[index]?.value})`
|
|
42501
42527
|
};
|
|
42528
|
+
};
|
|
42529
|
+
if (hitFiller) {
|
|
42530
|
+
return estimateFromScrollPos("hit filler");
|
|
42502
42531
|
}
|
|
42503
42532
|
if (hitEl) {
|
|
42504
42533
|
const hitId = hitEl.id;
|
|
@@ -42512,11 +42541,22 @@ const getScrollInfo = (scrollValues, listScrollContainerEl, tracker, virtualItem
|
|
|
42512
42541
|
reason: `hit item at ${index} (${items[index].value})`
|
|
42513
42542
|
};
|
|
42514
42543
|
}
|
|
42544
|
+
// Neither a real item nor a filler was hit within listEl — e.g. part of
|
|
42545
|
+
// the scan range fell outside the page's actually reachable viewport
|
|
42546
|
+
// (docked devtools shrinks it, for one). Keeping the stale renderWindow
|
|
42547
|
+
// here (as this used to do) means the DOM never gets asked to catch up
|
|
42548
|
+
// with a scrollTop that may have jumped far away — the user ends up
|
|
42549
|
+
// staring at filler space. Same estimate as the hitFiller case is a safe
|
|
42550
|
+
// fallback: it only needs the scroll position, not a successful hit-test.
|
|
42551
|
+
const estimated = estimateFromScrollPos("no hit");
|
|
42552
|
+
if (estimated) {
|
|
42553
|
+
return estimated;
|
|
42554
|
+
}
|
|
42515
42555
|
const fallbackIndex = renderWindowRef.current.start;
|
|
42516
42556
|
return {
|
|
42517
42557
|
item: items[fallbackIndex],
|
|
42518
42558
|
index: fallbackIndex,
|
|
42519
|
-
reason: "no hit"
|
|
42559
|
+
reason: "no hit, no virtualItemSize yet"
|
|
42520
42560
|
};
|
|
42521
42561
|
};
|
|
42522
42562
|
const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal) => {
|
|
@@ -42793,9 +42833,13 @@ const ListItemUI = props => {
|
|
|
42793
42833
|
const renderWindow = useContext(RenderWindowContext);
|
|
42794
42834
|
const tracker = useContext(ListItemTrackerContext);
|
|
42795
42835
|
const searchNoMatchMode = useContext(SearchNoMatchModeContext);
|
|
42796
|
-
//
|
|
42797
|
-
//
|
|
42798
|
-
|
|
42836
|
+
// There is no standalone match/matchScore/highlight prop — participation
|
|
42837
|
+
// in a matching system (search, filter…) only goes through `matchInfo`
|
|
42838
|
+
// (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
|
|
42839
|
+
// matchRanges }), so there is exactly one way to wire it up.
|
|
42840
|
+
const matchInfo = props.matchInfo;
|
|
42841
|
+
// Derive filtered/hidden/muted from matchInfo.match + searchNoMatchMode context.
|
|
42842
|
+
if (matchInfo?.match === false) {
|
|
42799
42843
|
if (searchNoMatchMode === "remove") {
|
|
42800
42844
|
props.filtered = true;
|
|
42801
42845
|
} else if (searchNoMatchMode === "invisible_and_inert") {
|
|
@@ -42864,7 +42908,7 @@ const ListItemReal = props => {
|
|
|
42864
42908
|
id,
|
|
42865
42909
|
hidden,
|
|
42866
42910
|
muted,
|
|
42867
|
-
|
|
42911
|
+
matchInfo,
|
|
42868
42912
|
children,
|
|
42869
42913
|
...rest
|
|
42870
42914
|
} = props;
|
|
@@ -42882,8 +42926,9 @@ const ListItemReal = props => {
|
|
|
42882
42926
|
pendingScroll.resolve(itemEl);
|
|
42883
42927
|
}, [needScrollOnMount]);
|
|
42884
42928
|
|
|
42885
|
-
// CSS Highlight API: mark matching text ranges
|
|
42886
|
-
|
|
42929
|
+
// CSS Highlight API: mark matching text ranges from matchInfo.matchRanges,
|
|
42930
|
+
// if any (there is no standalone highlight prop — see ListItem's own doc).
|
|
42931
|
+
useSearchHighlight(ref, matchInfo?.matchRanges, [children, hidden]);
|
|
42887
42932
|
const columnsOverrideProps = useListItemColumnsOverrideProps(rest.style);
|
|
42888
42933
|
return jsx(Box, {
|
|
42889
42934
|
as: "li",
|
|
@@ -42896,9 +42941,7 @@ const ListItemReal = props => {
|
|
|
42896
42941
|
...rest,
|
|
42897
42942
|
...columnsOverrideProps,
|
|
42898
42943
|
index: undefined,
|
|
42899
|
-
selected: undefined
|
|
42900
|
-
matchScore: undefined,
|
|
42901
|
-
match: undefined
|
|
42944
|
+
selected: undefined
|
|
42902
42945
|
// We use aria-hidden and not hidden because hidden would be forced to
|
|
42903
42946
|
// display: none while here we want to keep it in the DOM to avoid layout shift
|
|
42904
42947
|
// but visually hidden
|
|
@@ -42977,7 +43020,21 @@ const LIST_ITEM_PSEUDO_ELEMENTS = ["::highlight"];
|
|
|
42977
43020
|
* filtered — when true, item is excluded from visible count and removed from DOM entirely
|
|
42978
43021
|
* hidden — when true, item is excluded from visible count (no virtual scroll height)
|
|
42979
43022
|
* but stays in DOM with the native HTML hidden attribute
|
|
42980
|
-
*
|
|
43023
|
+
* matchInfo — participation in a matching system (search, filter…): the
|
|
43024
|
+
* object useSearchText's getItemMatchInfo(item) returns
|
|
43025
|
+
* (or any object shaped the same way):
|
|
43026
|
+
* <ListItem matchInfo={getItemMatchInfo(item)} />
|
|
43027
|
+
* There is no standalone match/matchScore/highlight prop —
|
|
43028
|
+
* matchInfo is the only way to wire this up:
|
|
43029
|
+
* match — false is interpreted per the List's own
|
|
43030
|
+
* searchNoMatchMode ("remove" -> filtered,
|
|
43031
|
+
* "invisible_and_inert" -> hidden,
|
|
43032
|
+
* "muted" -> muted).
|
|
43033
|
+
* matchScore — this item's search relevance score (higher =
|
|
43034
|
+
* more relevant). Only read for search-driven
|
|
43035
|
+
* scroll-to-top-match behavior.
|
|
43036
|
+
* matchRanges — array of [start, end] ranges to highlight via
|
|
43037
|
+
* CSS Highlight API.
|
|
42981
43038
|
* ...rest — forwarded to the rendered <li> element
|
|
42982
43039
|
*/
|
|
42983
43040
|
const ListItem = createComponentResolver([ListItemFirstResolver, ListItemSelectableResolver, ListItemHeaderOrFooterResolver, ListItemPresentationResolver, ListItemUI]);
|
|
@@ -44637,7 +44694,8 @@ const mergeRanges = (ranges) => {
|
|
|
44637
44694
|
*
|
|
44638
44695
|
* const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
|
|
44639
44696
|
* // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
|
|
44640
|
-
* // Pass
|
|
44697
|
+
* // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
|
|
44698
|
+
* // — ListItem handles the per-selector object format for matchRanges.
|
|
44641
44699
|
* ```
|
|
44642
44700
|
*
|
|
44643
44701
|
* Each field config:
|
|
@@ -44677,7 +44735,9 @@ const createSearch = (fields) => {
|
|
|
44677
44735
|
* followed by non-matched items in their natural order. No item is hidden.
|
|
44678
44736
|
* Returns [orderedItems, getItemMatchInfo].
|
|
44679
44737
|
* - orderedItems: all items, reordered
|
|
44680
|
-
* - getItemMatchInfo(item): { match,
|
|
44738
|
+
* - getItemMatchInfo(item): { match, matchScore, matchRanges } — pass the
|
|
44739
|
+
* whole thing straight to <ListItem matchInfo={getItemMatchInfo(item)} />,
|
|
44740
|
+
* there is no need to destructure the three fields by hand.
|
|
44681
44741
|
*
|
|
44682
44742
|
* When searchText is empty, natural order is preserved and all items match with score 0.
|
|
44683
44743
|
*
|
|
@@ -44730,8 +44790,8 @@ const buildMatchInfo = (searchText, items, matchFn) => {
|
|
|
44730
44790
|
if (!result.match) {
|
|
44731
44791
|
nonMatched.push({
|
|
44732
44792
|
item,
|
|
44733
|
-
|
|
44734
|
-
|
|
44793
|
+
matchScore: result.matchScore,
|
|
44794
|
+
matchRanges: result.matchRanges,
|
|
44735
44795
|
});
|
|
44736
44796
|
continue;
|
|
44737
44797
|
}
|
|
@@ -44751,23 +44811,23 @@ const buildMatchInfo = (searchText, items, matchFn) => {
|
|
|
44751
44811
|
}
|
|
44752
44812
|
}
|
|
44753
44813
|
if (lo < scoreEntries.length && scoreEntries[lo][0] === score) {
|
|
44754
|
-
scoreEntries[lo][1].push({ item,
|
|
44814
|
+
scoreEntries[lo][1].push({ item, matchRanges: result.matchRanges });
|
|
44755
44815
|
} else {
|
|
44756
44816
|
scoreEntries.splice(lo, 0, [
|
|
44757
44817
|
score,
|
|
44758
|
-
[{ item,
|
|
44818
|
+
[{ item, matchRanges: result.matchRanges }],
|
|
44759
44819
|
]);
|
|
44760
44820
|
}
|
|
44761
44821
|
}
|
|
44762
44822
|
|
|
44763
44823
|
const matchInfoMap = new Map();
|
|
44764
44824
|
for (const [score, bucket] of scoreEntries) {
|
|
44765
|
-
for (const { item,
|
|
44766
|
-
matchInfoMap.set(item, { match: true, score,
|
|
44825
|
+
for (const { item, matchRanges } of bucket) {
|
|
44826
|
+
matchInfoMap.set(item, { match: true, matchScore: score, matchRanges });
|
|
44767
44827
|
}
|
|
44768
44828
|
}
|
|
44769
|
-
for (const { item,
|
|
44770
|
-
matchInfoMap.set(item, { match: false,
|
|
44829
|
+
for (const { item, matchScore, matchRanges } of nonMatched) {
|
|
44830
|
+
matchInfoMap.set(item, { match: false, matchScore, matchRanges });
|
|
44771
44831
|
}
|
|
44772
44832
|
|
|
44773
44833
|
return { scoreEntries, nonMatched, matchInfoMap };
|