@jsenv/navi 0.27.83 → 0.27.84
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 +80 -37
- package/dist/jsenv_navi.js.map +10 -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
|
|
@@ -42793,9 +42816,13 @@ const ListItemUI = props => {
|
|
|
42793
42816
|
const renderWindow = useContext(RenderWindowContext);
|
|
42794
42817
|
const tracker = useContext(ListItemTrackerContext);
|
|
42795
42818
|
const searchNoMatchMode = useContext(SearchNoMatchModeContext);
|
|
42796
|
-
//
|
|
42797
|
-
//
|
|
42798
|
-
|
|
42819
|
+
// There is no standalone match/matchScore/highlight prop — participation
|
|
42820
|
+
// in a matching system (search, filter…) only goes through `matchInfo`
|
|
42821
|
+
// (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
|
|
42822
|
+
// matchRanges }), so there is exactly one way to wire it up.
|
|
42823
|
+
const matchInfo = props.matchInfo;
|
|
42824
|
+
// Derive filtered/hidden/muted from matchInfo.match + searchNoMatchMode context.
|
|
42825
|
+
if (matchInfo?.match === false) {
|
|
42799
42826
|
if (searchNoMatchMode === "remove") {
|
|
42800
42827
|
props.filtered = true;
|
|
42801
42828
|
} else if (searchNoMatchMode === "invisible_and_inert") {
|
|
@@ -42864,7 +42891,7 @@ const ListItemReal = props => {
|
|
|
42864
42891
|
id,
|
|
42865
42892
|
hidden,
|
|
42866
42893
|
muted,
|
|
42867
|
-
|
|
42894
|
+
matchInfo,
|
|
42868
42895
|
children,
|
|
42869
42896
|
...rest
|
|
42870
42897
|
} = props;
|
|
@@ -42882,8 +42909,9 @@ const ListItemReal = props => {
|
|
|
42882
42909
|
pendingScroll.resolve(itemEl);
|
|
42883
42910
|
}, [needScrollOnMount]);
|
|
42884
42911
|
|
|
42885
|
-
// CSS Highlight API: mark matching text ranges
|
|
42886
|
-
|
|
42912
|
+
// CSS Highlight API: mark matching text ranges from matchInfo.matchRanges,
|
|
42913
|
+
// if any (there is no standalone highlight prop — see ListItem's own doc).
|
|
42914
|
+
useSearchHighlight(ref, matchInfo?.matchRanges, [children, hidden]);
|
|
42887
42915
|
const columnsOverrideProps = useListItemColumnsOverrideProps(rest.style);
|
|
42888
42916
|
return jsx(Box, {
|
|
42889
42917
|
as: "li",
|
|
@@ -42896,9 +42924,7 @@ const ListItemReal = props => {
|
|
|
42896
42924
|
...rest,
|
|
42897
42925
|
...columnsOverrideProps,
|
|
42898
42926
|
index: undefined,
|
|
42899
|
-
selected: undefined
|
|
42900
|
-
matchScore: undefined,
|
|
42901
|
-
match: undefined
|
|
42927
|
+
selected: undefined
|
|
42902
42928
|
// We use aria-hidden and not hidden because hidden would be forced to
|
|
42903
42929
|
// display: none while here we want to keep it in the DOM to avoid layout shift
|
|
42904
42930
|
// but visually hidden
|
|
@@ -42977,7 +43003,21 @@ const LIST_ITEM_PSEUDO_ELEMENTS = ["::highlight"];
|
|
|
42977
43003
|
* filtered — when true, item is excluded from visible count and removed from DOM entirely
|
|
42978
43004
|
* hidden — when true, item is excluded from visible count (no virtual scroll height)
|
|
42979
43005
|
* but stays in DOM with the native HTML hidden attribute
|
|
42980
|
-
*
|
|
43006
|
+
* matchInfo — participation in a matching system (search, filter…): the
|
|
43007
|
+
* object useSearchText's getItemMatchInfo(item) returns
|
|
43008
|
+
* (or any object shaped the same way):
|
|
43009
|
+
* <ListItem matchInfo={getItemMatchInfo(item)} />
|
|
43010
|
+
* There is no standalone match/matchScore/highlight prop —
|
|
43011
|
+
* matchInfo is the only way to wire this up:
|
|
43012
|
+
* match — false is interpreted per the List's own
|
|
43013
|
+
* searchNoMatchMode ("remove" -> filtered,
|
|
43014
|
+
* "invisible_and_inert" -> hidden,
|
|
43015
|
+
* "muted" -> muted).
|
|
43016
|
+
* matchScore — this item's search relevance score (higher =
|
|
43017
|
+
* more relevant). Only read for search-driven
|
|
43018
|
+
* scroll-to-top-match behavior.
|
|
43019
|
+
* matchRanges — array of [start, end] ranges to highlight via
|
|
43020
|
+
* CSS Highlight API.
|
|
42981
43021
|
* ...rest — forwarded to the rendered <li> element
|
|
42982
43022
|
*/
|
|
42983
43023
|
const ListItem = createComponentResolver([ListItemFirstResolver, ListItemSelectableResolver, ListItemHeaderOrFooterResolver, ListItemPresentationResolver, ListItemUI]);
|
|
@@ -44637,7 +44677,8 @@ const mergeRanges = (ranges) => {
|
|
|
44637
44677
|
*
|
|
44638
44678
|
* const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
|
|
44639
44679
|
* // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
|
|
44640
|
-
* // Pass
|
|
44680
|
+
* // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
|
|
44681
|
+
* // — ListItem handles the per-selector object format for matchRanges.
|
|
44641
44682
|
* ```
|
|
44642
44683
|
*
|
|
44643
44684
|
* Each field config:
|
|
@@ -44677,7 +44718,9 @@ const createSearch = (fields) => {
|
|
|
44677
44718
|
* followed by non-matched items in their natural order. No item is hidden.
|
|
44678
44719
|
* Returns [orderedItems, getItemMatchInfo].
|
|
44679
44720
|
* - orderedItems: all items, reordered
|
|
44680
|
-
* - getItemMatchInfo(item): { match,
|
|
44721
|
+
* - getItemMatchInfo(item): { match, matchScore, matchRanges } — pass the
|
|
44722
|
+
* whole thing straight to <ListItem matchInfo={getItemMatchInfo(item)} />,
|
|
44723
|
+
* there is no need to destructure the three fields by hand.
|
|
44681
44724
|
*
|
|
44682
44725
|
* When searchText is empty, natural order is preserved and all items match with score 0.
|
|
44683
44726
|
*
|
|
@@ -44730,8 +44773,8 @@ const buildMatchInfo = (searchText, items, matchFn) => {
|
|
|
44730
44773
|
if (!result.match) {
|
|
44731
44774
|
nonMatched.push({
|
|
44732
44775
|
item,
|
|
44733
|
-
|
|
44734
|
-
|
|
44776
|
+
matchScore: result.matchScore,
|
|
44777
|
+
matchRanges: result.matchRanges,
|
|
44735
44778
|
});
|
|
44736
44779
|
continue;
|
|
44737
44780
|
}
|
|
@@ -44751,23 +44794,23 @@ const buildMatchInfo = (searchText, items, matchFn) => {
|
|
|
44751
44794
|
}
|
|
44752
44795
|
}
|
|
44753
44796
|
if (lo < scoreEntries.length && scoreEntries[lo][0] === score) {
|
|
44754
|
-
scoreEntries[lo][1].push({ item,
|
|
44797
|
+
scoreEntries[lo][1].push({ item, matchRanges: result.matchRanges });
|
|
44755
44798
|
} else {
|
|
44756
44799
|
scoreEntries.splice(lo, 0, [
|
|
44757
44800
|
score,
|
|
44758
|
-
[{ item,
|
|
44801
|
+
[{ item, matchRanges: result.matchRanges }],
|
|
44759
44802
|
]);
|
|
44760
44803
|
}
|
|
44761
44804
|
}
|
|
44762
44805
|
|
|
44763
44806
|
const matchInfoMap = new Map();
|
|
44764
44807
|
for (const [score, bucket] of scoreEntries) {
|
|
44765
|
-
for (const { item,
|
|
44766
|
-
matchInfoMap.set(item, { match: true, score,
|
|
44808
|
+
for (const { item, matchRanges } of bucket) {
|
|
44809
|
+
matchInfoMap.set(item, { match: true, matchScore: score, matchRanges });
|
|
44767
44810
|
}
|
|
44768
44811
|
}
|
|
44769
|
-
for (const { item,
|
|
44770
|
-
matchInfoMap.set(item, { match: false,
|
|
44812
|
+
for (const { item, matchScore, matchRanges } of nonMatched) {
|
|
44813
|
+
matchInfoMap.set(item, { match: false, matchScore, matchRanges });
|
|
44771
44814
|
}
|
|
44772
44815
|
|
|
44773
44816
|
return { scoreEntries, nonMatched, matchInfoMap };
|