@jsenv/navi 0.27.82 → 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 +166 -72
- package/dist/jsenv_navi.js.map +14 -15
- 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"),
|
|
@@ -18968,6 +18967,17 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
|
|
|
18968
18967
|
*/
|
|
18969
18968
|
|
|
18970
18969
|
|
|
18970
|
+
// Our own compact/custom duration notation interpolates raw numbers
|
|
18971
|
+
// directly (unlike Intl.DurationFormat, which groups thousands on its own,
|
|
18972
|
+
// e.g. "5 400 secondes") — this keeps that consistent without reimplementing
|
|
18973
|
+
// locale-aware grouping. Falls back to the raw value as-is for a
|
|
18974
|
+
// non-numeric mid-edit value (e.g. "2a"), which Intl.NumberFormat can't
|
|
18975
|
+
// format anyway.
|
|
18976
|
+
const formatCompactNumber = (value, lang) => {
|
|
18977
|
+
const n = Number(value);
|
|
18978
|
+
return Number.isFinite(n) ? new Intl.NumberFormat(lang).format(n) : value;
|
|
18979
|
+
};
|
|
18980
|
+
|
|
18971
18981
|
/**
|
|
18972
18982
|
* Formats a date as a human-readable day string.
|
|
18973
18983
|
*
|
|
@@ -19221,15 +19231,20 @@ const formatTime = (date, lang) => {
|
|
|
19221
19231
|
* "compact" uses our own notation that omits the minute symbol when hours are present.
|
|
19222
19232
|
*
|
|
19223
19233
|
* @param {number} minutes
|
|
19224
|
-
* @param {{ lang?: string, format?: "long"|"short"|"narrow"|"compact",
|
|
19225
|
-
* @param {boolean} [options.
|
|
19226
|
-
*
|
|
19227
|
-
*
|
|
19228
|
-
*
|
|
19229
|
-
*
|
|
19230
|
-
*
|
|
19231
|
-
*
|
|
19232
|
-
*
|
|
19234
|
+
* @param {{ lang?: string, format?: "long"|"short"|"narrow"|"compact", clockStyle?: boolean }} [options]
|
|
19235
|
+
* @param {boolean} [options.clockStyle=false] - Set this when `minutes`
|
|
19236
|
+
* represents a time-of-day rather than a real duration (used by
|
|
19237
|
+
* `<Time type="time">`, see time.jsx's own TimeTime) — affects two
|
|
19238
|
+
* things at once, both consequences of a clock's "0" being a meaningful
|
|
19239
|
+
* hour rather than "no hours":
|
|
19240
|
+
* - a zero-hours component is normally dropped entirely (a real 5-minute
|
|
19241
|
+
* duration should print as "5 minutes", not "0 hours 5 minutes"); this
|
|
19242
|
+
* keeps it instead (e.g. "0 h et 5 min"/"0h 5min"/"00h05") so midnight
|
|
19243
|
+
* doesn't collapse to something indistinguishable from an actual
|
|
19244
|
+
* 5-minute duration.
|
|
19245
|
+
* - `format: "compact"` also zero-pads a single-digit hour to 2 digits
|
|
19246
|
+
* (e.g. "5h30" → "05h30"), so it reads closer to a "05:30" clock.
|
|
19247
|
+
* Must not be set for plain duration formatting.
|
|
19233
19248
|
*
|
|
19234
19249
|
* @example
|
|
19235
19250
|
* formatMinuteDuration(90, { lang: "fr" }) // "1 heure 30 minutes" (long, default)
|
|
@@ -19237,20 +19252,21 @@ const formatTime = (date, lang) => {
|
|
|
19237
19252
|
* formatMinuteDuration(90, { lang: "fr", format: "narrow" }) // "1h 30min" (Intl narrow)
|
|
19238
19253
|
* formatMinuteDuration(90, { lang: "fr", format: "compact" }) // "1h30" (custom, no minute symbol)
|
|
19239
19254
|
* formatMinuteDuration(45, { lang: "en", format: "compact" }) // "45min"
|
|
19240
|
-
* formatMinuteDuration(5, { lang: "fr", format: "narrow",
|
|
19255
|
+
* formatMinuteDuration(5, { lang: "fr", format: "narrow", clockStyle: true }) // "0h 5min"
|
|
19256
|
+
* formatMinuteDuration(330, { lang: "fr", format: "compact", clockStyle: true }) // "05h30"
|
|
19241
19257
|
*/
|
|
19242
19258
|
const formatMinuteDuration = (
|
|
19243
19259
|
minutes,
|
|
19244
|
-
{ lang = languagesSignal.value, format = "long",
|
|
19260
|
+
{ lang = languagesSignal.value, format = "long", clockStyle = false } = {},
|
|
19245
19261
|
) => {
|
|
19246
19262
|
const h = Math.floor(minutes / 60);
|
|
19247
19263
|
const m = minutes % 60;
|
|
19248
19264
|
if (format !== "compact" && typeof Intl.DurationFormat !== "undefined") {
|
|
19249
19265
|
const fmt = new Intl.DurationFormat(lang, {
|
|
19250
19266
|
style: format, // "long", "short", or "narrow"
|
|
19251
|
-
...(
|
|
19267
|
+
...(clockStyle ? { hoursDisplay: "always" } : {}),
|
|
19252
19268
|
});
|
|
19253
|
-
if (h === 0 && !
|
|
19269
|
+
if (h === 0 && !clockStyle) {
|
|
19254
19270
|
return fmt.format({ minutes: m });
|
|
19255
19271
|
}
|
|
19256
19272
|
if (m === 0) {
|
|
@@ -19261,13 +19277,16 @@ const formatMinuteDuration = (
|
|
|
19261
19277
|
// format="compact": "1h30", "45min", "2h" — no minute symbol when hours are present
|
|
19262
19278
|
const hSym = naviI18n("time.duration.hour_symbol", undefined, { lang });
|
|
19263
19279
|
const mSym = naviI18n("time.duration.minute_symbol", undefined, { lang });
|
|
19264
|
-
|
|
19280
|
+
const hStr = clockStyle
|
|
19281
|
+
? String(h).padStart(2, "0")
|
|
19282
|
+
: formatCompactNumber(h, lang);
|
|
19283
|
+
if (h === 0 && !clockStyle) {
|
|
19265
19284
|
return `${m}${mSym}`;
|
|
19266
19285
|
}
|
|
19267
19286
|
if (m === 0) {
|
|
19268
|
-
return `${
|
|
19287
|
+
return `${hStr}${hSym}`;
|
|
19269
19288
|
}
|
|
19270
|
-
return `${
|
|
19289
|
+
return `${hStr}${hSym}${String(m).padStart(2, "0")}`;
|
|
19271
19290
|
};
|
|
19272
19291
|
|
|
19273
19292
|
/**
|
|
@@ -19322,7 +19341,9 @@ const formatSecondDuration = (
|
|
|
19322
19341
|
const mSym = naviI18n("time.duration.minute_symbol", undefined, { lang });
|
|
19323
19342
|
const sSym = naviI18n("time.duration.second_symbol", undefined, { lang });
|
|
19324
19343
|
const parts = [];
|
|
19325
|
-
|
|
19344
|
+
// m/s are always 0-59 by construction (never need grouping); h can be
|
|
19345
|
+
// arbitrarily large for a long duration.
|
|
19346
|
+
if (h > 0) parts.push(`${formatCompactNumber(h, lang)}${hSym}`);
|
|
19326
19347
|
if (m > 0) parts.push(`${m}${mSym}`);
|
|
19327
19348
|
if (s > 0 || parts.length === 0) parts.push(`${s}${sSym}`);
|
|
19328
19349
|
return parts.join("");
|
|
@@ -19423,36 +19444,40 @@ const formatDuration = (
|
|
|
19423
19444
|
const parts = [];
|
|
19424
19445
|
|
|
19425
19446
|
if (hasNonZero("years")) {
|
|
19426
|
-
parts.push(`${duration.years}${sym("year")}`);
|
|
19447
|
+
parts.push(`${formatCompactNumber(duration.years, lang)}${sym("year")}`);
|
|
19427
19448
|
}
|
|
19428
19449
|
if (hasNonZero("months")) {
|
|
19429
|
-
parts.push(`${duration.months}${sym("month")}`);
|
|
19450
|
+
parts.push(`${formatCompactNumber(duration.months, lang)}${sym("month")}`);
|
|
19430
19451
|
}
|
|
19431
19452
|
if (hasNonZero("weeks")) {
|
|
19432
|
-
parts.push(`${duration.weeks}${sym("week")}`);
|
|
19453
|
+
parts.push(`${formatCompactNumber(duration.weeks, lang)}${sym("week")}`);
|
|
19433
19454
|
}
|
|
19434
19455
|
if (hasNonZero("days")) {
|
|
19435
|
-
parts.push(`${duration.days}${sym("day")}`);
|
|
19456
|
+
parts.push(`${formatCompactNumber(duration.days, lang)}${sym("day")}`);
|
|
19436
19457
|
}
|
|
19437
19458
|
|
|
19438
|
-
// Hours + minutes: when both present, pad minutes to 2 digits after the h
|
|
19459
|
+
// Hours + minutes: when both present, pad minutes to 2 digits after the h
|
|
19460
|
+
// symbol — minutes stays a plain 2-digit pad (it's always 0-59 by
|
|
19461
|
+
// convention), only hours goes through grouping.
|
|
19439
19462
|
const hSym = sym("hour");
|
|
19440
19463
|
const mSym = sym("minute");
|
|
19441
19464
|
if (hasNonZero("hours") && hasNonZero("minutes")) {
|
|
19442
19465
|
parts.push(
|
|
19443
|
-
`${duration.hours}${hSym}${String(duration.minutes).padStart(2, "0")}`,
|
|
19466
|
+
`${formatCompactNumber(duration.hours, lang)}${hSym}${String(duration.minutes).padStart(2, "0")}`,
|
|
19444
19467
|
);
|
|
19445
19468
|
} else if (hasNonZero("hours")) {
|
|
19446
|
-
parts.push(`${duration.hours}${hSym}`);
|
|
19469
|
+
parts.push(`${formatCompactNumber(duration.hours, lang)}${hSym}`);
|
|
19447
19470
|
} else if (hasNonZero("minutes")) {
|
|
19448
|
-
parts.push(`${duration.minutes}${mSym}`);
|
|
19471
|
+
parts.push(`${formatCompactNumber(duration.minutes, lang)}${mSym}`);
|
|
19449
19472
|
}
|
|
19450
19473
|
|
|
19451
19474
|
if (hasNonZero("seconds")) {
|
|
19452
|
-
parts.push(`${duration.seconds}${sym("second")}`);
|
|
19475
|
+
parts.push(`${formatCompactNumber(duration.seconds, lang)}${sym("second")}`);
|
|
19453
19476
|
}
|
|
19454
19477
|
if (hasNonZero("milliseconds")) {
|
|
19455
|
-
parts.push(
|
|
19478
|
+
parts.push(
|
|
19479
|
+
`${formatCompactNumber(duration.milliseconds, lang)}${sym("millisecond")}`,
|
|
19480
|
+
);
|
|
19456
19481
|
}
|
|
19457
19482
|
return parts.join("") || "0";
|
|
19458
19483
|
};
|
|
@@ -33397,7 +33422,21 @@ const InputTextualWithSuggestions = props => {
|
|
|
33397
33422
|
// Nice side effect is that input_group.jsx will see all input is selected
|
|
33398
33423
|
// and arrow left/right will always nav between inputs.
|
|
33399
33424
|
// (Otherwise we would prevent left/right + show calllout about readonly)
|
|
33425
|
+
//
|
|
33426
|
+
// Not on touch: .select() there triggers the native mobile text-selection UI
|
|
33427
|
+
// (handles + magnifier), which makes no sense for a picker (opens a
|
|
33428
|
+
// popover/dialog on tap, not meant for text selection) and isn't wanted on a
|
|
33429
|
+
// plain readonly text input either. onPointerDown tracks the pointer type
|
|
33430
|
+
// that initiated the interaction (same convention as button_ui.jsx's own
|
|
33431
|
+
// `e.pointerType !== "touch"` check) so the focus handler — which fires
|
|
33432
|
+
// right after, but as a plain FocusEvent with no pointerType of its own —
|
|
33433
|
+
// can also skip select() for that same interaction.
|
|
33400
33434
|
const useAutoSelectReadOnly = (props) => {
|
|
33435
|
+
const lastPointerTypeRef = useRef(null);
|
|
33436
|
+
const onPointerDown = (e) => {
|
|
33437
|
+
props.onPointerDown?.(e);
|
|
33438
|
+
lastPointerTypeRef.current = e.pointerType;
|
|
33439
|
+
};
|
|
33401
33440
|
const onFocus = (e) => {
|
|
33402
33441
|
props.onFocus(e);
|
|
33403
33442
|
if (e.defaultPrevented) {
|
|
@@ -33406,6 +33445,9 @@ const useAutoSelectReadOnly = (props) => {
|
|
|
33406
33445
|
if (!e.target.readOnly) {
|
|
33407
33446
|
return;
|
|
33408
33447
|
}
|
|
33448
|
+
if (lastPointerTypeRef.current === "touch") {
|
|
33449
|
+
return;
|
|
33450
|
+
}
|
|
33409
33451
|
e.preventDefault();
|
|
33410
33452
|
e.target.select();
|
|
33411
33453
|
};
|
|
@@ -33417,11 +33459,14 @@ const useAutoSelectReadOnly = (props) => {
|
|
|
33417
33459
|
if (!e.target.readOnly) {
|
|
33418
33460
|
return;
|
|
33419
33461
|
}
|
|
33462
|
+
if (lastPointerTypeRef.current === "touch") {
|
|
33463
|
+
return;
|
|
33464
|
+
}
|
|
33420
33465
|
e.preventDefault();
|
|
33421
33466
|
e.target.select();
|
|
33422
33467
|
};
|
|
33423
33468
|
|
|
33424
|
-
return { onFocus, onMouseDown };
|
|
33469
|
+
return { onFocus, onMouseDown, onPointerDown };
|
|
33425
33470
|
};
|
|
33426
33471
|
|
|
33427
33472
|
installImportMetaCssBuild(import.meta);/**
|
|
@@ -39974,22 +40019,27 @@ const TimeTime = ({
|
|
|
39974
40019
|
// other hour keeps at least its own "N hour(s)" wording as a hint that
|
|
39975
40020
|
// this is a time-of-day, not a duration — only hour 0 loses that hint
|
|
39976
40021
|
// entirely.
|
|
40022
|
+
// clockStyle: this is always a time-of-day here, never a duration — keeps
|
|
40023
|
+
// a zero hour instead of dropping it (midnight would otherwise be
|
|
40024
|
+
// indistinguishable from an actual 5-minute duration), and in
|
|
40025
|
+
// format="compact" also zero-pads a single-digit hour so "5h30"/"0h05"
|
|
40026
|
+
// read as "05h30"/"00h05", closer to a "HH:MM" clock.
|
|
39977
40027
|
let text;
|
|
39978
40028
|
if (date.getHours() !== 0) {
|
|
39979
40029
|
text = formatMinuteDuration(totalMinutes, {
|
|
39980
40030
|
lang,
|
|
39981
|
-
format
|
|
40031
|
+
format,
|
|
40032
|
+
clockStyle: true
|
|
39982
40033
|
});
|
|
39983
40034
|
} else if (format !== "long") {
|
|
39984
40035
|
// short/narrow/compact: keep the "0 h"/"0h" hour part instead of
|
|
39985
|
-
// dropping it
|
|
39986
|
-
//
|
|
39987
|
-
//
|
|
39988
|
-
// into these otherwise terse, symbol-based formats.
|
|
40036
|
+
// dropping it — e.g. "0 h et 5 min"/"0h 5min"/"00h05" — rather than
|
|
40037
|
+
// substituting a translated "midnight" word, which would look out of
|
|
40038
|
+
// place squeezed into these otherwise terse, symbol-based formats.
|
|
39989
40039
|
text = formatMinuteDuration(totalMinutes, {
|
|
39990
40040
|
lang,
|
|
39991
40041
|
format,
|
|
39992
|
-
|
|
40042
|
+
clockStyle: true
|
|
39993
40043
|
});
|
|
39994
40044
|
} else {
|
|
39995
40045
|
const midnightWord = naviI18n("time.midnight", undefined, {
|
|
@@ -40004,7 +40054,7 @@ const TimeTime = ({
|
|
|
40004
40054
|
text = formatMinuteDuration(totalMinutes, {
|
|
40005
40055
|
lang,
|
|
40006
40056
|
format,
|
|
40007
|
-
|
|
40057
|
+
clockStyle: true
|
|
40008
40058
|
});
|
|
40009
40059
|
} else {
|
|
40010
40060
|
// Swap just the "0 heure(s)" part of the Intl-generated duration
|
|
@@ -40475,7 +40525,11 @@ const createItemTracker = (onChange) => {
|
|
|
40475
40525
|
}
|
|
40476
40526
|
|
|
40477
40527
|
// Build allItems and visibleItems in a single pass over allOrderedKeys.
|
|
40478
|
-
// 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).
|
|
40479
40533
|
const prevAllItems = itemsSignal.peek();
|
|
40480
40534
|
const prevVisibleItems = visibleItemsSignal.peek();
|
|
40481
40535
|
let allItemsChanged = prevAllItems.length !== allOrderedKeys.length;
|
|
@@ -40494,7 +40548,7 @@ const createItemTracker = (onChange) => {
|
|
|
40494
40548
|
if (item.match === false) {
|
|
40495
40549
|
newNoMatchCount++;
|
|
40496
40550
|
}
|
|
40497
|
-
if (!item.hidden) {
|
|
40551
|
+
if (!item.hidden && !item.filtered) {
|
|
40498
40552
|
const visibleIdx = visibleItems.length;
|
|
40499
40553
|
visibleItems.push(item);
|
|
40500
40554
|
if (!visibleItemsChanged && item !== prevVisibleItems[visibleIdx]) {
|
|
@@ -41218,10 +41272,9 @@ const ListItemSelectable = props => {
|
|
|
41218
41272
|
const {
|
|
41219
41273
|
index,
|
|
41220
41274
|
id = defaultId,
|
|
41221
|
-
|
|
41275
|
+
matchInfo,
|
|
41222
41276
|
hidden,
|
|
41223
41277
|
filtered,
|
|
41224
|
-
matchScore,
|
|
41225
41278
|
defaultSelected,
|
|
41226
41279
|
selected,
|
|
41227
41280
|
pointed,
|
|
@@ -41263,10 +41316,9 @@ const ListItemSelectable = props => {
|
|
|
41263
41316
|
return jsxs(Next, {
|
|
41264
41317
|
id: id,
|
|
41265
41318
|
index: index,
|
|
41266
|
-
|
|
41319
|
+
matchInfo: matchInfo,
|
|
41267
41320
|
filtered: filtered,
|
|
41268
41321
|
hidden: hidden,
|
|
41269
|
-
matchScore: matchScore,
|
|
41270
41322
|
"aria-selected": checked,
|
|
41271
41323
|
selected: checked,
|
|
41272
41324
|
"navi-selectable": "",
|
|
@@ -41580,6 +41632,7 @@ const css$m = /* css */`
|
|
|
41580
41632
|
flex-wrap: inherit;
|
|
41581
41633
|
overflow: auto;
|
|
41582
41634
|
overscroll-behavior: inherit; /* inherit select behavior */
|
|
41635
|
+
scrollbar-width: inherit;
|
|
41583
41636
|
}
|
|
41584
41637
|
|
|
41585
41638
|
&[data-expand-x] {
|
|
@@ -41871,7 +41924,7 @@ const ListUI = props => {
|
|
|
41871
41924
|
import.meta.css = [css$m, "@jsenv/navi/src/control/list/list.jsx"];
|
|
41872
41925
|
const {
|
|
41873
41926
|
ref,
|
|
41874
|
-
renderBudget = RENDER_BUDGET_DEFAULT,
|
|
41927
|
+
renderBudget: renderBudgetProp = RENDER_BUDGET_DEFAULT,
|
|
41875
41928
|
renderBudgetSkipCheck,
|
|
41876
41929
|
role,
|
|
41877
41930
|
fallback,
|
|
@@ -41892,6 +41945,15 @@ const ListUI = props => {
|
|
|
41892
41945
|
spacing,
|
|
41893
41946
|
...rest
|
|
41894
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
|
+
}
|
|
41895
41957
|
if (renderBudget < 30 && !renderBudgetSkipCheck) {
|
|
41896
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}).`);
|
|
41897
41959
|
}
|
|
@@ -42021,7 +42083,7 @@ const ListFirstResolver = props => {
|
|
|
42021
42083
|
* action?: (value: any) => void,
|
|
42022
42084
|
* uiAction?: (value: any) => void,
|
|
42023
42085
|
* popover?: boolean,
|
|
42024
|
-
* renderBudget?: number,
|
|
42086
|
+
* renderBudget?: number | string,
|
|
42025
42087
|
* virtualItemSize?: number,
|
|
42026
42088
|
* fallback?: import("ignore:preact").ComponentChildren,
|
|
42027
42089
|
* searchFallback?: import("ignore:preact").ComponentChildren,
|
|
@@ -42282,6 +42344,7 @@ const useListScrollSync = ({
|
|
|
42282
42344
|
// (pour l'instant pas grave car on travaille pour le mode select qui fermera le dialog au select)
|
|
42283
42345
|
const savedScrollRef = useRef(null);
|
|
42284
42346
|
const topMatchScoresKeyRef = useRef("");
|
|
42347
|
+
const restoreScrollRafRef = useRef(null);
|
|
42285
42348
|
useLayoutEffect(() => {
|
|
42286
42349
|
const listScrollContainerEl = ref.current?.querySelector(`.navi_list_scroll_container`);
|
|
42287
42350
|
if (!listScrollContainerEl) {
|
|
@@ -42298,7 +42361,20 @@ const useListScrollSync = ({
|
|
|
42298
42361
|
savedScrollRef.current = null;
|
|
42299
42362
|
debugScroll("Restoring scroll to", savedScroll);
|
|
42300
42363
|
updateRenderWindow(savedScroll.renderWindow.start, savedScroll.renderWindow.end, "restore scroll window");
|
|
42301
|
-
|
|
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;
|
|
42302
42378
|
const left = savedScroll.left;
|
|
42303
42379
|
const top = savedScroll.top;
|
|
42304
42380
|
// use scrollTo to respect eventual css scroll-behavior: smooth;
|
|
@@ -42321,13 +42397,11 @@ const useListScrollSync = ({
|
|
|
42321
42397
|
event: new CustomEvent("navi_scroll_restore")
|
|
42322
42398
|
});
|
|
42323
42399
|
});
|
|
42324
|
-
return
|
|
42325
|
-
cancelAnimationFrame(raf);
|
|
42326
|
-
};
|
|
42400
|
+
return undefined;
|
|
42327
42401
|
}
|
|
42328
42402
|
const visibleItems = tracker.visibleItemsSignal.peek();
|
|
42329
42403
|
const topItems = visibleItems.slice(0, renderBudget);
|
|
42330
|
-
const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchScore ?? ""}`).join(",");
|
|
42404
|
+
const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchInfo?.matchScore ?? ""}`).join(",");
|
|
42331
42405
|
const currentTopMatchScore = topMatchScoresKeyRef.current;
|
|
42332
42406
|
if (topMatchScoresKey === currentTopMatchScore) {
|
|
42333
42407
|
// no changes in top matches -> no need to scroll
|
|
@@ -42742,9 +42816,13 @@ const ListItemUI = props => {
|
|
|
42742
42816
|
const renderWindow = useContext(RenderWindowContext);
|
|
42743
42817
|
const tracker = useContext(ListItemTrackerContext);
|
|
42744
42818
|
const searchNoMatchMode = useContext(SearchNoMatchModeContext);
|
|
42745
|
-
//
|
|
42746
|
-
//
|
|
42747
|
-
|
|
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) {
|
|
42748
42826
|
if (searchNoMatchMode === "remove") {
|
|
42749
42827
|
props.filtered = true;
|
|
42750
42828
|
} else if (searchNoMatchMode === "invisible_and_inert") {
|
|
@@ -42813,7 +42891,7 @@ const ListItemReal = props => {
|
|
|
42813
42891
|
id,
|
|
42814
42892
|
hidden,
|
|
42815
42893
|
muted,
|
|
42816
|
-
|
|
42894
|
+
matchInfo,
|
|
42817
42895
|
children,
|
|
42818
42896
|
...rest
|
|
42819
42897
|
} = props;
|
|
@@ -42831,8 +42909,9 @@ const ListItemReal = props => {
|
|
|
42831
42909
|
pendingScroll.resolve(itemEl);
|
|
42832
42910
|
}, [needScrollOnMount]);
|
|
42833
42911
|
|
|
42834
|
-
// CSS Highlight API: mark matching text ranges
|
|
42835
|
-
|
|
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]);
|
|
42836
42915
|
const columnsOverrideProps = useListItemColumnsOverrideProps(rest.style);
|
|
42837
42916
|
return jsx(Box, {
|
|
42838
42917
|
as: "li",
|
|
@@ -42845,9 +42924,7 @@ const ListItemReal = props => {
|
|
|
42845
42924
|
...rest,
|
|
42846
42925
|
...columnsOverrideProps,
|
|
42847
42926
|
index: undefined,
|
|
42848
|
-
selected: undefined
|
|
42849
|
-
matchScore: undefined,
|
|
42850
|
-
match: undefined
|
|
42927
|
+
selected: undefined
|
|
42851
42928
|
// We use aria-hidden and not hidden because hidden would be forced to
|
|
42852
42929
|
// display: none while here we want to keep it in the DOM to avoid layout shift
|
|
42853
42930
|
// but visually hidden
|
|
@@ -42926,7 +43003,21 @@ const LIST_ITEM_PSEUDO_ELEMENTS = ["::highlight"];
|
|
|
42926
43003
|
* filtered — when true, item is excluded from visible count and removed from DOM entirely
|
|
42927
43004
|
* hidden — when true, item is excluded from visible count (no virtual scroll height)
|
|
42928
43005
|
* but stays in DOM with the native HTML hidden attribute
|
|
42929
|
-
*
|
|
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.
|
|
42930
43021
|
* ...rest — forwarded to the rendered <li> element
|
|
42931
43022
|
*/
|
|
42932
43023
|
const ListItem = createComponentResolver([ListItemFirstResolver, ListItemSelectableResolver, ListItemHeaderOrFooterResolver, ListItemPresentationResolver, ListItemUI]);
|
|
@@ -44586,7 +44677,8 @@ const mergeRanges = (ranges) => {
|
|
|
44586
44677
|
*
|
|
44587
44678
|
* const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
|
|
44588
44679
|
* // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
|
|
44589
|
-
* // Pass
|
|
44680
|
+
* // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
|
|
44681
|
+
* // — ListItem handles the per-selector object format for matchRanges.
|
|
44590
44682
|
* ```
|
|
44591
44683
|
*
|
|
44592
44684
|
* Each field config:
|
|
@@ -44626,7 +44718,9 @@ const createSearch = (fields) => {
|
|
|
44626
44718
|
* followed by non-matched items in their natural order. No item is hidden.
|
|
44627
44719
|
* Returns [orderedItems, getItemMatchInfo].
|
|
44628
44720
|
* - orderedItems: all items, reordered
|
|
44629
|
-
* - 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.
|
|
44630
44724
|
*
|
|
44631
44725
|
* When searchText is empty, natural order is preserved and all items match with score 0.
|
|
44632
44726
|
*
|
|
@@ -44679,8 +44773,8 @@ const buildMatchInfo = (searchText, items, matchFn) => {
|
|
|
44679
44773
|
if (!result.match) {
|
|
44680
44774
|
nonMatched.push({
|
|
44681
44775
|
item,
|
|
44682
|
-
|
|
44683
|
-
|
|
44776
|
+
matchScore: result.matchScore,
|
|
44777
|
+
matchRanges: result.matchRanges,
|
|
44684
44778
|
});
|
|
44685
44779
|
continue;
|
|
44686
44780
|
}
|
|
@@ -44700,23 +44794,23 @@ const buildMatchInfo = (searchText, items, matchFn) => {
|
|
|
44700
44794
|
}
|
|
44701
44795
|
}
|
|
44702
44796
|
if (lo < scoreEntries.length && scoreEntries[lo][0] === score) {
|
|
44703
|
-
scoreEntries[lo][1].push({ item,
|
|
44797
|
+
scoreEntries[lo][1].push({ item, matchRanges: result.matchRanges });
|
|
44704
44798
|
} else {
|
|
44705
44799
|
scoreEntries.splice(lo, 0, [
|
|
44706
44800
|
score,
|
|
44707
|
-
[{ item,
|
|
44801
|
+
[{ item, matchRanges: result.matchRanges }],
|
|
44708
44802
|
]);
|
|
44709
44803
|
}
|
|
44710
44804
|
}
|
|
44711
44805
|
|
|
44712
44806
|
const matchInfoMap = new Map();
|
|
44713
44807
|
for (const [score, bucket] of scoreEntries) {
|
|
44714
|
-
for (const { item,
|
|
44715
|
-
matchInfoMap.set(item, { match: true, score,
|
|
44808
|
+
for (const { item, matchRanges } of bucket) {
|
|
44809
|
+
matchInfoMap.set(item, { match: true, matchScore: score, matchRanges });
|
|
44716
44810
|
}
|
|
44717
44811
|
}
|
|
44718
|
-
for (const { item,
|
|
44719
|
-
matchInfoMap.set(item, { match: false,
|
|
44812
|
+
for (const { item, matchScore, matchRanges } of nonMatched) {
|
|
44813
|
+
matchInfoMap.set(item, { match: false, matchScore, matchRanges });
|
|
44720
44814
|
}
|
|
44721
44815
|
|
|
44722
44816
|
return { scoreEntries, nonMatched, matchInfoMap };
|