@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.
@@ -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 relative order as orderedKeys.
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
- highlight,
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
- highlight: highlight,
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
- const raf = requestAnimationFrame(() => {
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
- // Derive filtered/hidden/nonMatching from the `match` prop + searchNoMatchMode context.
42797
- // The `match` prop replaces the older `filtered`/`hidden` per-item props.
42798
- if (props.match === false) {
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
- highlight,
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 when highlight prop is set.
42886
- useSearchHighlight(ref, highlight, [children, hidden]);
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
- * highlightarray of [start, end] ranges to highlight via CSS Highlight API
43006
+ * matchInfoparticipation 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 it to <ListItem highlight={matchRanges} /> — ListItem handles the object format.
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, score, ranges }
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
- score: result.matchScore,
44734
- ranges: result.matchRanges,
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, ranges: result.matchRanges });
44797
+ scoreEntries[lo][1].push({ item, matchRanges: result.matchRanges });
44755
44798
  } else {
44756
44799
  scoreEntries.splice(lo, 0, [
44757
44800
  score,
44758
- [{ item, ranges: result.matchRanges }],
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, ranges } of bucket) {
44766
- matchInfoMap.set(item, { match: true, score, ranges });
44808
+ for (const { item, matchRanges } of bucket) {
44809
+ matchInfoMap.set(item, { match: true, matchScore: score, matchRanges });
44767
44810
  }
44768
44811
  }
44769
- for (const { item, score, ranges } of nonMatched) {
44770
- matchInfoMap.set(item, { match: false, score, ranges });
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 };