@jsenv/navi 0.27.15 → 0.27.17
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 +2301 -2140
- package/dist/jsenv_navi.js.map +147 -117
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -5729,6 +5729,9 @@ const NO_LOCAL_STORAGE = [() => undefined, () => {}, () => {}];
|
|
|
5729
5729
|
const localStorageTypeMap = {
|
|
5730
5730
|
float: "number",
|
|
5731
5731
|
integer: "number",
|
|
5732
|
+
minute: "number",
|
|
5733
|
+
hour: "number",
|
|
5734
|
+
second: "number",
|
|
5732
5735
|
ratio: "number",
|
|
5733
5736
|
longitude: "number",
|
|
5734
5737
|
latitude: "number",
|
|
@@ -12885,11 +12888,27 @@ const definePseudoClass = (pseudoClass, definition) => {
|
|
|
12885
12888
|
definePseudoClass(":hover", {
|
|
12886
12889
|
attribute: "data-hover",
|
|
12887
12890
|
setup: (el, callback) => {
|
|
12888
|
-
|
|
12891
|
+
const recheckProxy = (e) => {
|
|
12892
|
+
const proxy = findControlProxy(el);
|
|
12893
|
+
if (proxy) {
|
|
12894
|
+
requestPseudoStateCheck(proxy, { event: e });
|
|
12895
|
+
}
|
|
12896
|
+
};
|
|
12897
|
+
const recheckProxyTarget = (e) => {
|
|
12898
|
+
const proxyTarget = findControlProxyTarget(el);
|
|
12899
|
+
if (proxyTarget) {
|
|
12900
|
+
requestPseudoStateCheck(proxyTarget, { event: e });
|
|
12901
|
+
}
|
|
12902
|
+
};
|
|
12903
|
+
let onmouseenter = (e) => {
|
|
12889
12904
|
callback();
|
|
12905
|
+
recheckProxy(e);
|
|
12906
|
+
recheckProxyTarget(e);
|
|
12890
12907
|
};
|
|
12891
|
-
let onmouseleave = () => {
|
|
12908
|
+
let onmouseleave = (e) => {
|
|
12892
12909
|
callback();
|
|
12910
|
+
recheckProxy(e);
|
|
12911
|
+
recheckProxyTarget(e);
|
|
12893
12912
|
};
|
|
12894
12913
|
|
|
12895
12914
|
if (el.tagName === "LABEL") {
|
|
@@ -12912,13 +12931,15 @@ definePseudoClass(":hover", {
|
|
|
12912
12931
|
}
|
|
12913
12932
|
requestPseudoStateCheck(input, { event: e });
|
|
12914
12933
|
};
|
|
12934
|
+
const _onmouseenter = onmouseenter;
|
|
12915
12935
|
onmouseenter = (e) => {
|
|
12916
|
-
callback();
|
|
12917
12936
|
recheckInput(e);
|
|
12937
|
+
_onmouseenter(e);
|
|
12918
12938
|
};
|
|
12939
|
+
const _onmouseleave = onmouseleave;
|
|
12919
12940
|
onmouseleave = (e) => {
|
|
12920
|
-
callback();
|
|
12921
12941
|
recheckInput(e);
|
|
12942
|
+
_onmouseleave(e);
|
|
12922
12943
|
};
|
|
12923
12944
|
}
|
|
12924
12945
|
|
|
@@ -12929,7 +12950,16 @@ definePseudoClass(":hover", {
|
|
|
12929
12950
|
el.removeEventListener("mouseleave", onmouseleave);
|
|
12930
12951
|
};
|
|
12931
12952
|
},
|
|
12932
|
-
test: (el) =>
|
|
12953
|
+
test: (el) => {
|
|
12954
|
+
if (el.matches(":hover")) {
|
|
12955
|
+
return true;
|
|
12956
|
+
}
|
|
12957
|
+
const proxy = findControlProxy(el);
|
|
12958
|
+
if (proxy && proxy.matches(":hover")) {
|
|
12959
|
+
return true;
|
|
12960
|
+
}
|
|
12961
|
+
return false;
|
|
12962
|
+
},
|
|
12933
12963
|
});
|
|
12934
12964
|
definePseudoClass(":disabled", {
|
|
12935
12965
|
attribute: "data-disabled",
|
|
@@ -17384,6 +17414,7 @@ const css$O = /* css */`
|
|
|
17384
17414
|
opacity: 0;
|
|
17385
17415
|
/* will be positioned with transform: translate */
|
|
17386
17416
|
transition: opacity 0.2s ease-in-out;
|
|
17417
|
+
pointer-events: auto; /* Must be interactive to be closabled (overrid list item pointer-events none for instance) */
|
|
17387
17418
|
overflow: visible;
|
|
17388
17419
|
|
|
17389
17420
|
&[data-status="success"] {
|
|
@@ -21851,7 +21882,7 @@ const useCustomValidationRef = (
|
|
|
21851
21882
|
}
|
|
21852
21883
|
const element = elementRef.current;
|
|
21853
21884
|
if (!element) {
|
|
21854
|
-
if (!elementRef.
|
|
21885
|
+
if (!elementRef.nullCanHappen) {
|
|
21855
21886
|
console.warn(
|
|
21856
21887
|
`useCustomValidationRef: elementRef.current is null, make sure to pass a ref to an element
|
|
21857
21888
|
${callSiteRef.current}`,
|
|
@@ -24394,10 +24425,13 @@ const useNextResolver = () => useContext(NextResolverContext);
|
|
|
24394
24425
|
* To terminate the chain early (e.g. render a specialized component), render
|
|
24395
24426
|
* directly without calling Next.
|
|
24396
24427
|
*
|
|
24428
|
+
* The last entry in the array is the final/target component — it receives null
|
|
24429
|
+
* from useNextResolver() indicating it is terminal.
|
|
24430
|
+
*
|
|
24397
24431
|
* Usage:
|
|
24398
|
-
* const renderButton = createComponentResolver([ResolverA, ResolverB]);
|
|
24432
|
+
* const renderButton = createComponentResolver([ResolverA, ResolverB, ButtonTarget]);
|
|
24399
24433
|
* // Then inside a component render:
|
|
24400
|
-
* renderButton(
|
|
24434
|
+
* renderButton(props)
|
|
24401
24435
|
*
|
|
24402
24436
|
* NextResolverContext exposes a stable Next component so resolvers can continue
|
|
24403
24437
|
* the chain via useNextResolver().
|
|
@@ -24406,22 +24440,21 @@ const useNextResolver = () => useContext(NextResolverContext);
|
|
|
24406
24440
|
*/
|
|
24407
24441
|
const createComponentResolver = resolvers => {
|
|
24408
24442
|
const ResolverIndexContext = createContext(0);
|
|
24409
|
-
const TargetComponentContext = createContext(null);
|
|
24410
24443
|
const ChainRunner = props => {
|
|
24411
24444
|
const index = useContext(ResolverIndexContext);
|
|
24412
|
-
const TargetComponent = useContext(TargetComponentContext);
|
|
24413
24445
|
if (index >= resolvers.length) {
|
|
24414
|
-
return
|
|
24415
|
-
value: null,
|
|
24416
|
-
children: jsx(TargetComponent, {
|
|
24417
|
-
...props
|
|
24418
|
-
})
|
|
24419
|
-
});
|
|
24446
|
+
return null;
|
|
24420
24447
|
}
|
|
24421
24448
|
const Resolver = resolvers[index];
|
|
24449
|
+
const isLast = index === resolvers.length - 1;
|
|
24422
24450
|
return jsx(ResolverIndexContext.Provider, {
|
|
24423
24451
|
value: index + 1,
|
|
24424
|
-
children: jsx(
|
|
24452
|
+
children: isLast ? jsx(NextResolverContext.Provider, {
|
|
24453
|
+
value: null,
|
|
24454
|
+
children: jsx(Resolver, {
|
|
24455
|
+
...props
|
|
24456
|
+
})
|
|
24457
|
+
}) : jsx(Resolver, {
|
|
24425
24458
|
...props
|
|
24426
24459
|
})
|
|
24427
24460
|
});
|
|
@@ -24434,11 +24467,11 @@ const createComponentResolver = resolvers => {
|
|
|
24434
24467
|
const NextComponent = props => jsx(ChainRunner, {
|
|
24435
24468
|
...props
|
|
24436
24469
|
});
|
|
24437
|
-
const renderComponent =
|
|
24470
|
+
const renderComponent = props => {
|
|
24438
24471
|
return jsx(NextResolverContext.Provider, {
|
|
24439
24472
|
value: NextComponent,
|
|
24440
|
-
children: jsx(
|
|
24441
|
-
value:
|
|
24473
|
+
children: jsx(ResolverIndexContext.Provider, {
|
|
24474
|
+
value: 0,
|
|
24442
24475
|
children: jsx(ChainRunner, {
|
|
24443
24476
|
...props
|
|
24444
24477
|
})
|
|
@@ -24448,6 +24481,61 @@ const createComponentResolver = resolvers => {
|
|
|
24448
24481
|
return renderComponent;
|
|
24449
24482
|
};
|
|
24450
24483
|
|
|
24484
|
+
const ButtonInsideFormResolver = props => {
|
|
24485
|
+
const Next = useNextResolver();
|
|
24486
|
+
const formContext = useContext(FormContext);
|
|
24487
|
+
if (formContext) {
|
|
24488
|
+
return jsx(ButtonInsideForm, {
|
|
24489
|
+
...props
|
|
24490
|
+
});
|
|
24491
|
+
}
|
|
24492
|
+
return jsx(Next, {
|
|
24493
|
+
...props
|
|
24494
|
+
});
|
|
24495
|
+
};
|
|
24496
|
+
const ButtonInsideForm = props => {
|
|
24497
|
+
const Next = useNextResolver();
|
|
24498
|
+
return jsx(Next
|
|
24499
|
+
// The default action for a button inside a form is to request form action
|
|
24500
|
+
, {
|
|
24501
|
+
action: "send",
|
|
24502
|
+
...props
|
|
24503
|
+
});
|
|
24504
|
+
};
|
|
24505
|
+
|
|
24506
|
+
const ButtonRouteResolver = props => {
|
|
24507
|
+
const Next = useNextResolver();
|
|
24508
|
+
if (props.route) {
|
|
24509
|
+
return jsx(ButtonWithRoute, {
|
|
24510
|
+
...props
|
|
24511
|
+
});
|
|
24512
|
+
}
|
|
24513
|
+
return jsx(Next, {
|
|
24514
|
+
...props
|
|
24515
|
+
});
|
|
24516
|
+
};
|
|
24517
|
+
const ButtonWithRoute = props => {
|
|
24518
|
+
const Next = useNextResolver();
|
|
24519
|
+
const {
|
|
24520
|
+
route,
|
|
24521
|
+
routeParams,
|
|
24522
|
+
children,
|
|
24523
|
+
...rest
|
|
24524
|
+
} = props;
|
|
24525
|
+
const url = route.buildUrl(routeParams);
|
|
24526
|
+
const {
|
|
24527
|
+
matching
|
|
24528
|
+
} = useRouteStatus(route);
|
|
24529
|
+
const paramsAreMatching = route.matchesParams(routeParams);
|
|
24530
|
+
const linkMatching = matching && paramsAreMatching;
|
|
24531
|
+
return jsx(Next, {
|
|
24532
|
+
href: url,
|
|
24533
|
+
"data-href-current": linkMatching ? "" : undefined,
|
|
24534
|
+
...rest,
|
|
24535
|
+
children: children || route.buildRelativeUrl(routeParams)
|
|
24536
|
+
});
|
|
24537
|
+
};
|
|
24538
|
+
|
|
24451
24539
|
const LIGHT_ACCENT_ATTRIBUTE = "data-accent-light";
|
|
24452
24540
|
const VERY_LIGHT_ACCENT_ATTRIBUTE = "data-accent-very-light";
|
|
24453
24541
|
const DARK_CONTRAST_ATTRIBUTE = "data-accent-needs-dark-fg";
|
|
@@ -26520,38 +26608,8 @@ installImportMetaCssBuild(import.meta);const css$I = /* css */`
|
|
|
26520
26608
|
}
|
|
26521
26609
|
}
|
|
26522
26610
|
`;
|
|
26523
|
-
const Button = props => {
|
|
26524
|
-
const defaultRef = useRef(null);
|
|
26525
|
-
props.ref = props.ref || defaultRef;
|
|
26526
|
-
const button = renderButton(ButtonUI, props);
|
|
26527
|
-
return button;
|
|
26528
|
-
};
|
|
26529
|
-
const ButtonRouteResolver = props => {
|
|
26530
|
-
const Next = useNextResolver();
|
|
26531
|
-
if (props.route) {
|
|
26532
|
-
return jsx(ButtonWithRoute, {
|
|
26533
|
-
...props
|
|
26534
|
-
});
|
|
26535
|
-
}
|
|
26536
|
-
return jsx(Next, {
|
|
26537
|
-
...props
|
|
26538
|
-
});
|
|
26539
|
-
};
|
|
26540
|
-
const ButtonInsideFormResolver = props => {
|
|
26541
|
-
const Next = useNextResolver();
|
|
26542
|
-
const formContext = useContext(FormContext);
|
|
26543
|
-
if (formContext) {
|
|
26544
|
-
return jsx(ButtonInsideForm, {
|
|
26545
|
-
...props
|
|
26546
|
-
});
|
|
26547
|
-
}
|
|
26548
|
-
return jsx(Next, {
|
|
26549
|
-
...props
|
|
26550
|
-
});
|
|
26551
|
-
};
|
|
26552
|
-
const renderButton = createComponentResolver([ButtonRouteResolver, ButtonInsideFormResolver]);
|
|
26553
26611
|
const ButtonUI = props => {
|
|
26554
|
-
import.meta.css = [css$I, "@jsenv/navi/src/control/
|
|
26612
|
+
import.meta.css = [css$I, "@jsenv/navi/src/control/input/button_ui.jsx"];
|
|
26555
26613
|
const {
|
|
26556
26614
|
ref,
|
|
26557
26615
|
// href/link
|
|
@@ -26701,36 +26759,16 @@ const ButtonShadow = () => {
|
|
|
26701
26759
|
});
|
|
26702
26760
|
};
|
|
26703
26761
|
markAsOutsideTextFlow(ButtonShadow);
|
|
26704
|
-
|
|
26705
|
-
|
|
26706
|
-
return jsx(Next
|
|
26707
|
-
// The default action for a button inside a form is to request form action
|
|
26708
|
-
, {
|
|
26709
|
-
action: "send",
|
|
26710
|
-
...props
|
|
26711
|
-
});
|
|
26712
|
-
};
|
|
26713
|
-
const ButtonWithRoute = props => {
|
|
26762
|
+
|
|
26763
|
+
const ButtonFirstResolver = props => {
|
|
26714
26764
|
const Next = useNextResolver();
|
|
26715
|
-
const
|
|
26716
|
-
|
|
26717
|
-
routeParams,
|
|
26718
|
-
children,
|
|
26719
|
-
...rest
|
|
26720
|
-
} = props;
|
|
26721
|
-
const url = route.buildUrl(routeParams);
|
|
26722
|
-
const {
|
|
26723
|
-
matching
|
|
26724
|
-
} = useRouteStatus(route);
|
|
26725
|
-
const paramsAreMatching = route.matchesParams(routeParams);
|
|
26726
|
-
const linkMatching = matching && paramsAreMatching;
|
|
26765
|
+
const defaultRef = useRef(null);
|
|
26766
|
+
props.ref = props.ref || defaultRef;
|
|
26727
26767
|
return jsx(Next, {
|
|
26728
|
-
|
|
26729
|
-
"data-href-current": linkMatching ? "" : undefined,
|
|
26730
|
-
...rest,
|
|
26731
|
-
children: children || route.buildRelativeUrl(routeParams)
|
|
26768
|
+
...props
|
|
26732
26769
|
});
|
|
26733
26770
|
};
|
|
26771
|
+
const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonInsideFormResolver, ButtonUI]);
|
|
26734
26772
|
|
|
26735
26773
|
const CloseSvg = () => {
|
|
26736
26774
|
return jsx("svg", {
|
|
@@ -29841,16 +29879,6 @@ const RangeStyleCSSVars = {
|
|
|
29841
29879
|
const RangePseudoClasses = [":hover", ":active", ":-navi-pressed", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
29842
29880
|
const RangePseudoElements = ["::-navi-loader"];
|
|
29843
29881
|
|
|
29844
|
-
const ChevronDownSvg = () => {
|
|
29845
|
-
return jsx("svg", {
|
|
29846
|
-
viewBox: "0 0 16 16",
|
|
29847
|
-
fill: "currentColor",
|
|
29848
|
-
children: jsx("path", {
|
|
29849
|
-
d: "M4.427 7.427l3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427z"
|
|
29850
|
-
})
|
|
29851
|
-
});
|
|
29852
|
-
};
|
|
29853
|
-
|
|
29854
29882
|
const SearchSvg = () => jsx("svg", {
|
|
29855
29883
|
viewBox: "0 0 24 24",
|
|
29856
29884
|
xmlns: "http://www.w3.org/2000/svg",
|
|
@@ -29860,6 +29888,8 @@ const SearchSvg = () => jsx("svg", {
|
|
|
29860
29888
|
})
|
|
29861
29889
|
});
|
|
29862
29890
|
|
|
29891
|
+
const InputTextualContext = createContext(null);
|
|
29892
|
+
|
|
29863
29893
|
installImportMetaCssBuild(import.meta);const css$x = /* css */`
|
|
29864
29894
|
@layer navi {
|
|
29865
29895
|
[data-navi-field] {
|
|
@@ -30084,6 +30114,91 @@ const Label = props => {
|
|
|
30084
30114
|
};
|
|
30085
30115
|
const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
30086
30116
|
|
|
30117
|
+
const InputSlot = ({
|
|
30118
|
+
side,
|
|
30119
|
+
onClick,
|
|
30120
|
+
hideWhileEmpty,
|
|
30121
|
+
...props
|
|
30122
|
+
}) => {
|
|
30123
|
+
const ctx = useContext(InputTextualContext);
|
|
30124
|
+
const {
|
|
30125
|
+
id,
|
|
30126
|
+
readOnly,
|
|
30127
|
+
disabled
|
|
30128
|
+
} = ctx;
|
|
30129
|
+
return jsx(Label, {
|
|
30130
|
+
htmlFor: id,
|
|
30131
|
+
className: "navi_input_slot",
|
|
30132
|
+
disabled: disabled,
|
|
30133
|
+
readOnly: readOnly,
|
|
30134
|
+
"data-readonly": readOnly,
|
|
30135
|
+
"data-disabled": disabled,
|
|
30136
|
+
"data-left": side === "left" ? "" : undefined,
|
|
30137
|
+
"data-right": side === "right" ? "" : undefined,
|
|
30138
|
+
"data-hide-while-empty": hideWhileEmpty ? "" : undefined,
|
|
30139
|
+
inline: true,
|
|
30140
|
+
flex: true,
|
|
30141
|
+
align: "center",
|
|
30142
|
+
onMouseDown: e => {
|
|
30143
|
+
// Only prevent focus from leaving when the input already has focus.
|
|
30144
|
+
// If the input is not focused, let the mousedown proceed normally so
|
|
30145
|
+
// the slot element (e.g. a clear button) can receive focus itself.
|
|
30146
|
+
const inputEl = document.getElementById(id);
|
|
30147
|
+
if (inputEl && inputEl === document.activeElement) {
|
|
30148
|
+
e.preventDefault();
|
|
30149
|
+
}
|
|
30150
|
+
},
|
|
30151
|
+
onClick: e => {
|
|
30152
|
+
onClick?.(e);
|
|
30153
|
+
const input = document.getElementById(id);
|
|
30154
|
+
const allowed = dispatchRequestInteraction(input, e);
|
|
30155
|
+
if (!allowed) {
|
|
30156
|
+
e.preventDefault();
|
|
30157
|
+
}
|
|
30158
|
+
},
|
|
30159
|
+
...props
|
|
30160
|
+
});
|
|
30161
|
+
};
|
|
30162
|
+
const InputLeftSlot = props => {
|
|
30163
|
+
return jsx(InputSlot, {
|
|
30164
|
+
...props,
|
|
30165
|
+
side: "left"
|
|
30166
|
+
});
|
|
30167
|
+
};
|
|
30168
|
+
const InputRightSlot = props => {
|
|
30169
|
+
return jsx(InputSlot, {
|
|
30170
|
+
...props,
|
|
30171
|
+
side: "right"
|
|
30172
|
+
});
|
|
30173
|
+
};
|
|
30174
|
+
|
|
30175
|
+
const InputWithListResolver = props => {
|
|
30176
|
+
const Next = useNextResolver();
|
|
30177
|
+
if (props["navi-list"]) {
|
|
30178
|
+
return jsx(InputWithList, {
|
|
30179
|
+
...props
|
|
30180
|
+
});
|
|
30181
|
+
}
|
|
30182
|
+
return jsx(Next, {
|
|
30183
|
+
...props
|
|
30184
|
+
});
|
|
30185
|
+
};
|
|
30186
|
+
|
|
30187
|
+
/**
|
|
30188
|
+
* InputWithList — connects an input to a SelectableList via its id.
|
|
30189
|
+
*
|
|
30190
|
+
* Usage: <Input navi-list="my-list-id" /> next to <SelectableList id="my-list-id" />
|
|
30191
|
+
*
|
|
30192
|
+
* Behavior:
|
|
30193
|
+
* - ArrowDown / ArrowUp move the list's "current item" without moving focus
|
|
30194
|
+
* - Home / End jump to first/last navigable item
|
|
30195
|
+
* - Enter triggers selection of the current item (same as clicking it)
|
|
30196
|
+
* - aria-controls dynamically points at the current item's real input so the
|
|
30197
|
+
* pseudo-styles focus-inheritance kicks in (list item shows :focus /
|
|
30198
|
+
* :focus-visible while the input is focused).
|
|
30199
|
+
* - aria-activedescendant points at the current item's <li> id (standard
|
|
30200
|
+
* combobox/listbox ARIA pattern).
|
|
30201
|
+
*/
|
|
30087
30202
|
const InputWithList = props => {
|
|
30088
30203
|
const Next = useNextResolver();
|
|
30089
30204
|
const {
|
|
@@ -30156,6 +30271,218 @@ const InputWithList = props => {
|
|
|
30156
30271
|
});
|
|
30157
30272
|
};
|
|
30158
30273
|
|
|
30274
|
+
const ChevronDownSvg = () => {
|
|
30275
|
+
return jsx("svg", {
|
|
30276
|
+
viewBox: "0 0 16 16",
|
|
30277
|
+
fill: "currentColor",
|
|
30278
|
+
children: jsx("path", {
|
|
30279
|
+
d: "M4.427 7.427l3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427z"
|
|
30280
|
+
})
|
|
30281
|
+
});
|
|
30282
|
+
};
|
|
30283
|
+
|
|
30284
|
+
const InputWithSuggestionsResolver = props => {
|
|
30285
|
+
const Next = useNextResolver();
|
|
30286
|
+
if (props["navi-suggestions"]) {
|
|
30287
|
+
return jsx(InputTextualWithSuggestions, {
|
|
30288
|
+
...props
|
|
30289
|
+
});
|
|
30290
|
+
}
|
|
30291
|
+
return jsx(Next, {
|
|
30292
|
+
...props
|
|
30293
|
+
});
|
|
30294
|
+
};
|
|
30295
|
+
const InputTextualWithSuggestions = props => {
|
|
30296
|
+
const Next = useNextResolver();
|
|
30297
|
+
const {
|
|
30298
|
+
ref,
|
|
30299
|
+
suggestions,
|
|
30300
|
+
onInput,
|
|
30301
|
+
onFocus,
|
|
30302
|
+
onBlur,
|
|
30303
|
+
onKeyDown,
|
|
30304
|
+
children,
|
|
30305
|
+
...rest
|
|
30306
|
+
} = props;
|
|
30307
|
+
const [expanded, setExpanded] = useState(false);
|
|
30308
|
+
const expandedRef = useRef(expanded);
|
|
30309
|
+
expandedRef.current = expanded;
|
|
30310
|
+
const expand = () => {
|
|
30311
|
+
expandedRef.current = true;
|
|
30312
|
+
setExpanded(true);
|
|
30313
|
+
};
|
|
30314
|
+
const collapse = () => {
|
|
30315
|
+
expandedRef.current = false;
|
|
30316
|
+
setExpanded(false);
|
|
30317
|
+
};
|
|
30318
|
+
const getListEl = () => {
|
|
30319
|
+
return document.getElementById(suggestions);
|
|
30320
|
+
};
|
|
30321
|
+
const showSuggestions = e => {
|
|
30322
|
+
if (expandedRef.current) {
|
|
30323
|
+
return;
|
|
30324
|
+
}
|
|
30325
|
+
const listEl = getListEl();
|
|
30326
|
+
if (listEl) {
|
|
30327
|
+
dispatchCustomEvent(listEl, "navi_request_open", {
|
|
30328
|
+
event: e,
|
|
30329
|
+
anchor: ref.current
|
|
30330
|
+
});
|
|
30331
|
+
expand();
|
|
30332
|
+
}
|
|
30333
|
+
};
|
|
30334
|
+
const hideSuggestions = e => {
|
|
30335
|
+
if (!expandedRef.current) {
|
|
30336
|
+
return;
|
|
30337
|
+
}
|
|
30338
|
+
const listEl = getListEl();
|
|
30339
|
+
if (listEl) {
|
|
30340
|
+
dispatchCustomEvent(listEl, "navi_request_close", {
|
|
30341
|
+
event: e
|
|
30342
|
+
});
|
|
30343
|
+
collapse();
|
|
30344
|
+
}
|
|
30345
|
+
};
|
|
30346
|
+
useEffect(() => {
|
|
30347
|
+
const inputEl = ref.current;
|
|
30348
|
+
const listEl = getListEl();
|
|
30349
|
+
if (!listEl) {
|
|
30350
|
+
return undefined;
|
|
30351
|
+
}
|
|
30352
|
+
const onSelect = e => {
|
|
30353
|
+
const {
|
|
30354
|
+
item
|
|
30355
|
+
} = e.detail;
|
|
30356
|
+
const {
|
|
30357
|
+
value
|
|
30358
|
+
} = item;
|
|
30359
|
+
inputEl.value = value;
|
|
30360
|
+
inputEl.dispatchEvent(new Event("input", {
|
|
30361
|
+
bubbles: true
|
|
30362
|
+
}));
|
|
30363
|
+
hideSuggestions(e);
|
|
30364
|
+
};
|
|
30365
|
+
listEl.addEventListener("navi_list_select", onSelect);
|
|
30366
|
+
return () => {
|
|
30367
|
+
listEl.removeEventListener("navi_list_select", onSelect);
|
|
30368
|
+
};
|
|
30369
|
+
}, [suggestions]);
|
|
30370
|
+
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
30371
|
+
arrowdown: e => {
|
|
30372
|
+
showSuggestions(e);
|
|
30373
|
+
},
|
|
30374
|
+
arrowup: e => {
|
|
30375
|
+
showSuggestions(e);
|
|
30376
|
+
},
|
|
30377
|
+
escape: e => {
|
|
30378
|
+
if (!expandedRef.current) {
|
|
30379
|
+
return false;
|
|
30380
|
+
}
|
|
30381
|
+
hideSuggestions(e);
|
|
30382
|
+
return true;
|
|
30383
|
+
},
|
|
30384
|
+
home: () => {},
|
|
30385
|
+
end: () => {},
|
|
30386
|
+
enter: () => {}
|
|
30387
|
+
});
|
|
30388
|
+
return jsx(Next, {
|
|
30389
|
+
role: "combobox",
|
|
30390
|
+
"aria-haspopup": "listbox",
|
|
30391
|
+
"aria-expanded": expanded,
|
|
30392
|
+
"aria-autocomplete": "list",
|
|
30393
|
+
autoComplete: "off",
|
|
30394
|
+
basePseudoState: {
|
|
30395
|
+
":-navi-expanded": expanded
|
|
30396
|
+
},
|
|
30397
|
+
onnavi_callout_open: e => {
|
|
30398
|
+
hideSuggestions(e);
|
|
30399
|
+
},
|
|
30400
|
+
...rest,
|
|
30401
|
+
ref: ref,
|
|
30402
|
+
onFocus: e => {
|
|
30403
|
+
onFocus?.(e);
|
|
30404
|
+
showSuggestions(e);
|
|
30405
|
+
},
|
|
30406
|
+
onBlur: e => {
|
|
30407
|
+
onBlur?.(e);
|
|
30408
|
+
hideSuggestions(e);
|
|
30409
|
+
},
|
|
30410
|
+
onInput: e => {
|
|
30411
|
+
onInput?.(e);
|
|
30412
|
+
showSuggestions(e);
|
|
30413
|
+
},
|
|
30414
|
+
onKeyDown: e => {
|
|
30415
|
+
onKeyDown?.(e);
|
|
30416
|
+
onKeyDownShortcuts(e);
|
|
30417
|
+
}
|
|
30418
|
+
// arrowdown: (e) => {
|
|
30419
|
+
// const listEl = getListEl();
|
|
30420
|
+
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30421
|
+
// return requestListNavFromCurrent(listEl, {
|
|
30422
|
+
// event: e,
|
|
30423
|
+
// goal: "down",
|
|
30424
|
+
// });
|
|
30425
|
+
// },
|
|
30426
|
+
// arrowup: (e) => {
|
|
30427
|
+
// const listEl = getListEl();
|
|
30428
|
+
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30429
|
+
// return requestListNavFromCurrent(listEl, {
|
|
30430
|
+
// event: e,
|
|
30431
|
+
// goal: "up",
|
|
30432
|
+
// });
|
|
30433
|
+
// },
|
|
30434
|
+
// home: (e) => {
|
|
30435
|
+
// const listEl = getListEl();
|
|
30436
|
+
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30437
|
+
// return requestListNavFromCurrent(listEl, {
|
|
30438
|
+
// event: e,
|
|
30439
|
+
// goal: "first",
|
|
30440
|
+
// });
|
|
30441
|
+
// },
|
|
30442
|
+
// end: (e) => {
|
|
30443
|
+
// const listEl = getListEl();
|
|
30444
|
+
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30445
|
+
// return requestListNavFromCurrent(listEl, {
|
|
30446
|
+
// event: e,
|
|
30447
|
+
// goal: "last",
|
|
30448
|
+
// });
|
|
30449
|
+
// },
|
|
30450
|
+
// enter: (e) => {
|
|
30451
|
+
// const listEl = getListEl();
|
|
30452
|
+
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30453
|
+
// return requestListSelectCurrent(listEl, { event: e });
|
|
30454
|
+
// },
|
|
30455
|
+
// escape: (e) => {
|
|
30456
|
+
// // prevent escape from reaching eventual <select> ancestor
|
|
30457
|
+
// // when the escape is meant to clear the search input (otherwise it would close the select too)
|
|
30458
|
+
// if (e.currentTarget.type === "search" && e.currentTarget.value !== "") {
|
|
30459
|
+
// e.stopPropagation();
|
|
30460
|
+
// return true;
|
|
30461
|
+
// }
|
|
30462
|
+
// const listEl = getListEl();
|
|
30463
|
+
// // here we allow propagation of escape up to the <select> to allow closing if within a select
|
|
30464
|
+
// // it also means list might catch escape and reset again but it's ok to reset twice here as it won't cause side effects
|
|
30465
|
+
// // (if we need the same pattern for other events where it could be problematic we would have to mark
|
|
30466
|
+
// // event as handled somehow to prevent list containing input to react to it)
|
|
30467
|
+
// return requestListInteractionStateReset(listEl, { event: e });
|
|
30468
|
+
// },
|
|
30469
|
+
,
|
|
30470
|
+
children: children || jsx(InputRightSlot, {
|
|
30471
|
+
onClick: e => {
|
|
30472
|
+
if (expanded) {
|
|
30473
|
+
hideSuggestions(e);
|
|
30474
|
+
} else {
|
|
30475
|
+
showSuggestions(e);
|
|
30476
|
+
}
|
|
30477
|
+
},
|
|
30478
|
+
children: jsx(Icon, {
|
|
30479
|
+
color: "rgba(28, 43, 52, 0.5)",
|
|
30480
|
+
children: jsx(ChevronDownSvg, {})
|
|
30481
|
+
})
|
|
30482
|
+
})
|
|
30483
|
+
});
|
|
30484
|
+
};
|
|
30485
|
+
|
|
30159
30486
|
installImportMetaCssBuild(import.meta);/**
|
|
30160
30487
|
* Input component for all textual input types.
|
|
30161
30488
|
*
|
|
@@ -30414,28 +30741,6 @@ const css$w = /* css */`
|
|
|
30414
30741
|
-webkit-text-fill-color: var(--x-color) !important;
|
|
30415
30742
|
}
|
|
30416
30743
|
`;
|
|
30417
|
-
const InputTextual = props => {
|
|
30418
|
-
const defaultRef = useRef(null);
|
|
30419
|
-
props.ref = props.ref || defaultRef;
|
|
30420
|
-
const input = renderInput(InputTextualControlInterface, props);
|
|
30421
|
-
return input;
|
|
30422
|
-
};
|
|
30423
|
-
const InputTextualWithListResolver = props => {
|
|
30424
|
-
const Next = useNextResolver();
|
|
30425
|
-
if (props["navi-list"]) {
|
|
30426
|
-
return jsx(InputWithList, {
|
|
30427
|
-
...props
|
|
30428
|
-
});
|
|
30429
|
-
}
|
|
30430
|
-
if (props.suggestions) {
|
|
30431
|
-
return jsx(InputTextualWithSuggestions, {
|
|
30432
|
-
...props
|
|
30433
|
-
});
|
|
30434
|
-
}
|
|
30435
|
-
return jsx(Next, {
|
|
30436
|
-
...props
|
|
30437
|
-
});
|
|
30438
|
-
};
|
|
30439
30744
|
const InputTypeResolver = props => {
|
|
30440
30745
|
const Next = useNextResolver();
|
|
30441
30746
|
if (props.type === "search") {
|
|
@@ -30483,7 +30788,6 @@ const InputHeadlessResolver = props => {
|
|
|
30483
30788
|
...props
|
|
30484
30789
|
});
|
|
30485
30790
|
};
|
|
30486
|
-
const renderInput = createComponentResolver([InputTextualWithListResolver, InputTypeResolver, InputHeadlessResolver]);
|
|
30487
30791
|
const InputTextualHeadless = props => {
|
|
30488
30792
|
const [inputProps, remainingProps] = useInputTextualProps(props);
|
|
30489
30793
|
return jsx(BoxForwardedPropsContext.Provider, {
|
|
@@ -30508,8 +30812,7 @@ const useInputTextualProps = props => {
|
|
|
30508
30812
|
});
|
|
30509
30813
|
return [controlProps, remainingProps, ControlChildrenWrapper];
|
|
30510
30814
|
};
|
|
30511
|
-
const
|
|
30512
|
-
const InputTextualControlInterface = props => {
|
|
30815
|
+
const InputTextualUI = props => {
|
|
30513
30816
|
import.meta.css = [css$w, "@jsenv/navi/src/control/input/input_textual.jsx"];
|
|
30514
30817
|
const {
|
|
30515
30818
|
ui,
|
|
@@ -30527,7 +30830,7 @@ const InputTextualControlInterface = props => {
|
|
|
30527
30830
|
const readOnly = basePseudoState[":read-only"];
|
|
30528
30831
|
const loading = basePseudoState[":-navi-loading"];
|
|
30529
30832
|
const childrenWithContext = jsx(ControlChildrenWrapper, {
|
|
30530
|
-
children: jsx(
|
|
30833
|
+
children: jsx(InputTextualContext.Provider, {
|
|
30531
30834
|
value: {
|
|
30532
30835
|
id,
|
|
30533
30836
|
readOnly,
|
|
@@ -30562,6 +30865,15 @@ const InputTextualControlInterface = props => {
|
|
|
30562
30865
|
}), childrenWithContext]
|
|
30563
30866
|
});
|
|
30564
30867
|
};
|
|
30868
|
+
const InputTextualFirstResolver = props => {
|
|
30869
|
+
const Next = useNextResolver();
|
|
30870
|
+
const defaultRef = useRef(null);
|
|
30871
|
+
props.ref = props.ref || defaultRef;
|
|
30872
|
+
return jsx(Next, {
|
|
30873
|
+
...props
|
|
30874
|
+
});
|
|
30875
|
+
};
|
|
30876
|
+
const InputTextual = createComponentResolver([InputTextualFirstResolver, InputWithListResolver, InputWithSuggestionsResolver, InputTypeResolver, InputHeadlessResolver, InputTextualUI]);
|
|
30565
30877
|
const RealInput = props => {
|
|
30566
30878
|
const inputProps = useContext(BoxForwardedPropsContext);
|
|
30567
30879
|
return jsx(Box, {
|
|
@@ -30613,63 +30925,6 @@ const InputStyleCSSVars = {
|
|
|
30613
30925
|
};
|
|
30614
30926
|
const InputPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-has-value", ":-navi-expanded"];
|
|
30615
30927
|
const InputPseudoElements = ["::-navi-loader"];
|
|
30616
|
-
const InputSlot = ({
|
|
30617
|
-
side,
|
|
30618
|
-
onClick,
|
|
30619
|
-
hideWhileEmpty,
|
|
30620
|
-
...props
|
|
30621
|
-
}) => {
|
|
30622
|
-
const ctx = useContext(InputNativeContext);
|
|
30623
|
-
const {
|
|
30624
|
-
id,
|
|
30625
|
-
readOnly,
|
|
30626
|
-
disabled
|
|
30627
|
-
} = ctx;
|
|
30628
|
-
return jsx(Label, {
|
|
30629
|
-
htmlFor: id,
|
|
30630
|
-
className: "navi_input_slot",
|
|
30631
|
-
disabled: disabled,
|
|
30632
|
-
readOnly: readOnly,
|
|
30633
|
-
"data-readonly": readOnly,
|
|
30634
|
-
"data-disabled": disabled,
|
|
30635
|
-
"data-left": side === "left" ? "" : undefined,
|
|
30636
|
-
"data-right": side === "right" ? "" : undefined,
|
|
30637
|
-
"data-hide-while-empty": hideWhileEmpty ? "" : undefined,
|
|
30638
|
-
inline: true,
|
|
30639
|
-
flex: true,
|
|
30640
|
-
align: "center",
|
|
30641
|
-
onMouseDown: e => {
|
|
30642
|
-
// Only prevent focus from leaving when the input already has focus.
|
|
30643
|
-
// If the input is not focused, let the mousedown proceed normally so
|
|
30644
|
-
// the slot element (e.g. a clear button) can receive focus itself.
|
|
30645
|
-
const inputEl = document.getElementById(id);
|
|
30646
|
-
if (inputEl && inputEl === document.activeElement) {
|
|
30647
|
-
e.preventDefault();
|
|
30648
|
-
}
|
|
30649
|
-
},
|
|
30650
|
-
onClick: e => {
|
|
30651
|
-
onClick?.(e);
|
|
30652
|
-
const input = document.getElementById(id);
|
|
30653
|
-
const allowed = dispatchRequestInteraction(input, e);
|
|
30654
|
-
if (!allowed) {
|
|
30655
|
-
e.preventDefault();
|
|
30656
|
-
}
|
|
30657
|
-
},
|
|
30658
|
-
...props
|
|
30659
|
-
});
|
|
30660
|
-
};
|
|
30661
|
-
const InputLeftSlot = props => {
|
|
30662
|
-
return jsx(InputSlot, {
|
|
30663
|
-
...props,
|
|
30664
|
-
side: "left"
|
|
30665
|
-
});
|
|
30666
|
-
};
|
|
30667
|
-
const InputRightSlot = props => {
|
|
30668
|
-
return jsx(InputSlot, {
|
|
30669
|
-
...props,
|
|
30670
|
-
side: "right"
|
|
30671
|
-
});
|
|
30672
|
-
};
|
|
30673
30928
|
const InputSearch = props => {
|
|
30674
30929
|
const Next = useNextResolver();
|
|
30675
30930
|
return jsx(Next, {
|
|
@@ -30682,7 +30937,7 @@ const InputSearch = props => {
|
|
|
30682
30937
|
const InputSearchUI = ({
|
|
30683
30938
|
icon
|
|
30684
30939
|
}) => {
|
|
30685
|
-
const ctx = useContext(
|
|
30940
|
+
const ctx = useContext(InputTextualContext);
|
|
30686
30941
|
const {
|
|
30687
30942
|
id
|
|
30688
30943
|
} = ctx;
|
|
@@ -30771,197 +31026,6 @@ const InputDatetimeLocal = props => {
|
|
|
30771
31026
|
...props
|
|
30772
31027
|
});
|
|
30773
31028
|
};
|
|
30774
|
-
const InputTextualWithSuggestions = props => {
|
|
30775
|
-
const Next = useNextResolver();
|
|
30776
|
-
const {
|
|
30777
|
-
ref,
|
|
30778
|
-
suggestions,
|
|
30779
|
-
onInput,
|
|
30780
|
-
onFocus,
|
|
30781
|
-
onBlur,
|
|
30782
|
-
onKeyDown,
|
|
30783
|
-
children,
|
|
30784
|
-
...rest
|
|
30785
|
-
} = props;
|
|
30786
|
-
const [expanded, setExpanded] = useState(false);
|
|
30787
|
-
const expandedRef = useRef(expanded);
|
|
30788
|
-
expandedRef.current = expanded;
|
|
30789
|
-
const expand = () => {
|
|
30790
|
-
expandedRef.current = true;
|
|
30791
|
-
setExpanded(true);
|
|
30792
|
-
};
|
|
30793
|
-
const collapse = () => {
|
|
30794
|
-
expandedRef.current = false;
|
|
30795
|
-
setExpanded(false);
|
|
30796
|
-
};
|
|
30797
|
-
const getListEl = () => {
|
|
30798
|
-
return document.getElementById(suggestions);
|
|
30799
|
-
};
|
|
30800
|
-
const showSuggestions = e => {
|
|
30801
|
-
if (expandedRef.current) {
|
|
30802
|
-
return;
|
|
30803
|
-
}
|
|
30804
|
-
const listEl = getListEl();
|
|
30805
|
-
if (listEl) {
|
|
30806
|
-
dispatchCustomEvent(listEl, "navi_request_open", {
|
|
30807
|
-
event: e,
|
|
30808
|
-
anchor: ref.current
|
|
30809
|
-
});
|
|
30810
|
-
expand();
|
|
30811
|
-
}
|
|
30812
|
-
};
|
|
30813
|
-
const hideSuggestions = e => {
|
|
30814
|
-
if (!expandedRef.current) {
|
|
30815
|
-
return;
|
|
30816
|
-
}
|
|
30817
|
-
const listEl = getListEl();
|
|
30818
|
-
if (listEl) {
|
|
30819
|
-
dispatchCustomEvent(listEl, "navi_request_close", {
|
|
30820
|
-
event: e
|
|
30821
|
-
});
|
|
30822
|
-
collapse();
|
|
30823
|
-
}
|
|
30824
|
-
};
|
|
30825
|
-
useEffect(() => {
|
|
30826
|
-
const inputEl = ref.current;
|
|
30827
|
-
const listEl = getListEl();
|
|
30828
|
-
if (!listEl) {
|
|
30829
|
-
return undefined;
|
|
30830
|
-
}
|
|
30831
|
-
const onSelect = e => {
|
|
30832
|
-
const {
|
|
30833
|
-
item
|
|
30834
|
-
} = e.detail;
|
|
30835
|
-
const {
|
|
30836
|
-
value
|
|
30837
|
-
} = item;
|
|
30838
|
-
inputEl.value = value;
|
|
30839
|
-
inputEl.dispatchEvent(new Event("input", {
|
|
30840
|
-
bubbles: true
|
|
30841
|
-
}));
|
|
30842
|
-
hideSuggestions(e);
|
|
30843
|
-
};
|
|
30844
|
-
listEl.addEventListener("navi_list_select", onSelect);
|
|
30845
|
-
return () => {
|
|
30846
|
-
listEl.removeEventListener("navi_list_select", onSelect);
|
|
30847
|
-
};
|
|
30848
|
-
}, [suggestions]);
|
|
30849
|
-
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
30850
|
-
arrowdown: e => {
|
|
30851
|
-
showSuggestions(e);
|
|
30852
|
-
},
|
|
30853
|
-
arrowup: e => {
|
|
30854
|
-
showSuggestions(e);
|
|
30855
|
-
},
|
|
30856
|
-
escape: e => {
|
|
30857
|
-
if (!expandedRef.current) {
|
|
30858
|
-
return false;
|
|
30859
|
-
}
|
|
30860
|
-
hideSuggestions(e);
|
|
30861
|
-
return true;
|
|
30862
|
-
},
|
|
30863
|
-
home: () => {},
|
|
30864
|
-
end: () => {},
|
|
30865
|
-
enter: () => {}
|
|
30866
|
-
});
|
|
30867
|
-
return jsx(Next, {
|
|
30868
|
-
role: "combobox",
|
|
30869
|
-
"aria-haspopup": "listbox",
|
|
30870
|
-
"aria-expanded": expanded,
|
|
30871
|
-
"aria-autocomplete": "list",
|
|
30872
|
-
autoComplete: "off",
|
|
30873
|
-
basePseudoState: {
|
|
30874
|
-
":-navi-expanded": expanded
|
|
30875
|
-
},
|
|
30876
|
-
onnavi_callout_open: e => {
|
|
30877
|
-
hideSuggestions(e);
|
|
30878
|
-
},
|
|
30879
|
-
...rest,
|
|
30880
|
-
ref: ref,
|
|
30881
|
-
onFocus: e => {
|
|
30882
|
-
onFocus?.(e);
|
|
30883
|
-
showSuggestions(e);
|
|
30884
|
-
},
|
|
30885
|
-
onBlur: e => {
|
|
30886
|
-
onBlur?.(e);
|
|
30887
|
-
hideSuggestions(e);
|
|
30888
|
-
},
|
|
30889
|
-
onInput: e => {
|
|
30890
|
-
onInput?.(e);
|
|
30891
|
-
showSuggestions(e);
|
|
30892
|
-
},
|
|
30893
|
-
onKeyDown: e => {
|
|
30894
|
-
onKeyDown?.(e);
|
|
30895
|
-
onKeyDownShortcuts(e);
|
|
30896
|
-
}
|
|
30897
|
-
// arrowdown: (e) => {
|
|
30898
|
-
// const listEl = getListEl();
|
|
30899
|
-
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30900
|
-
// return requestListNavFromCurrent(listEl, {
|
|
30901
|
-
// event: e,
|
|
30902
|
-
// goal: "down",
|
|
30903
|
-
// });
|
|
30904
|
-
// },
|
|
30905
|
-
// arrowup: (e) => {
|
|
30906
|
-
// const listEl = getListEl();
|
|
30907
|
-
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30908
|
-
// return requestListNavFromCurrent(listEl, {
|
|
30909
|
-
// event: e,
|
|
30910
|
-
// goal: "up",
|
|
30911
|
-
// });
|
|
30912
|
-
// },
|
|
30913
|
-
// home: (e) => {
|
|
30914
|
-
// const listEl = getListEl();
|
|
30915
|
-
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30916
|
-
// return requestListNavFromCurrent(listEl, {
|
|
30917
|
-
// event: e,
|
|
30918
|
-
// goal: "first",
|
|
30919
|
-
// });
|
|
30920
|
-
// },
|
|
30921
|
-
// end: (e) => {
|
|
30922
|
-
// const listEl = getListEl();
|
|
30923
|
-
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30924
|
-
// return requestListNavFromCurrent(listEl, {
|
|
30925
|
-
// event: e,
|
|
30926
|
-
// goal: "last",
|
|
30927
|
-
// });
|
|
30928
|
-
// },
|
|
30929
|
-
// enter: (e) => {
|
|
30930
|
-
// const listEl = getListEl();
|
|
30931
|
-
// e.stopPropagation(); // when within a list, prevent list from handling it twice
|
|
30932
|
-
// return requestListSelectCurrent(listEl, { event: e });
|
|
30933
|
-
// },
|
|
30934
|
-
// escape: (e) => {
|
|
30935
|
-
// // prevent escape from reaching eventual <select> ancestor
|
|
30936
|
-
// // when the escape is meant to clear the search input (otherwise it would close the select too)
|
|
30937
|
-
// if (e.currentTarget.type === "search" && e.currentTarget.value !== "") {
|
|
30938
|
-
// e.stopPropagation();
|
|
30939
|
-
// return true;
|
|
30940
|
-
// }
|
|
30941
|
-
// const listEl = getListEl();
|
|
30942
|
-
// // here we allow propagation of escape up to the <select> to allow closing if within a select
|
|
30943
|
-
// // it also means list might catch escape and reset again but it's ok to reset twice here as it won't cause side effects
|
|
30944
|
-
// // (if we need the same pattern for other events where it could be problematic we would have to mark
|
|
30945
|
-
// // event as handled somehow to prevent list containing input to react to it)
|
|
30946
|
-
// return requestListInteractionStateReset(listEl, { event: e });
|
|
30947
|
-
// },
|
|
30948
|
-
,
|
|
30949
|
-
|
|
30950
|
-
children: children || jsx(InputRightSlot, {
|
|
30951
|
-
onClick: e => {
|
|
30952
|
-
if (expanded) {
|
|
30953
|
-
hideSuggestions(e);
|
|
30954
|
-
} else {
|
|
30955
|
-
showSuggestions(e);
|
|
30956
|
-
}
|
|
30957
|
-
},
|
|
30958
|
-
children: jsx(Icon, {
|
|
30959
|
-
color: "rgba(28, 43, 52, 0.5)",
|
|
30960
|
-
children: jsx(ChevronDownSvg, {})
|
|
30961
|
-
})
|
|
30962
|
-
})
|
|
30963
|
-
});
|
|
30964
|
-
};
|
|
30965
31029
|
|
|
30966
31030
|
const Input = props => {
|
|
30967
31031
|
const {
|
|
@@ -30986,6 +31050,10 @@ const Input = props => {
|
|
|
30986
31050
|
...props
|
|
30987
31051
|
});
|
|
30988
31052
|
};
|
|
31053
|
+
Input.UI = {
|
|
31054
|
+
LeftSlot: InputLeftSlot,
|
|
31055
|
+
RightSlot: InputRightSlot
|
|
31056
|
+
};
|
|
30989
31057
|
|
|
30990
31058
|
installImportMetaCssBuild(import.meta);/**
|
|
30991
31059
|
* - We must keep the edited element in the DOM so that
|
|
@@ -33739,1943 +33807,2032 @@ const createItemTracker = (onChange) => {
|
|
|
33739
33807
|
};
|
|
33740
33808
|
};
|
|
33741
33809
|
|
|
33742
|
-
|
|
33743
|
-
|
|
33744
|
-
|
|
33745
|
-
|
|
33746
|
-
|
|
33747
|
-
|
|
33748
|
-
* The highlight is not specific to ListItem — any element that wants to mark
|
|
33749
|
-
* search match ranges (e.g. Suggestion, custom search results) can use these.
|
|
33750
|
-
*
|
|
33751
|
-
* CSS to paint the highlight:
|
|
33752
|
-
* ::highlight(navi-search-match) {
|
|
33753
|
-
* color: var(--search-match-color);
|
|
33754
|
-
* background-color: var(--search-match-background-color);
|
|
33755
|
-
* }
|
|
33756
|
-
*
|
|
33757
|
-
* The `highlight` prop can be:
|
|
33758
|
-
* - an array of [start, end] pairs — applied to all text nodes under the root element
|
|
33759
|
-
* - an object { [domSelector]: [[start, end], …] } — applied to each sub-element
|
|
33760
|
-
* matched by the selector (the format produced by createSearch)
|
|
33761
|
-
*/
|
|
33762
|
-
|
|
33763
|
-
// Module-level shared Highlight instance — created lazily once.
|
|
33764
|
-
let naviSearchHighlight = null;
|
|
33765
|
-
const getNaviSearchHighlight = () => {
|
|
33766
|
-
if (!CSS.highlights) {
|
|
33767
|
-
return null;
|
|
33810
|
+
const ListItemHeaderOrFooterResolver = props => {
|
|
33811
|
+
const Next = useNextResolver();
|
|
33812
|
+
if (props.header) {
|
|
33813
|
+
return jsx(ListItemHeader, {
|
|
33814
|
+
...props
|
|
33815
|
+
});
|
|
33768
33816
|
}
|
|
33769
|
-
if (
|
|
33770
|
-
|
|
33771
|
-
|
|
33817
|
+
if (props.footer) {
|
|
33818
|
+
return jsx(ListItemFooter, {
|
|
33819
|
+
...props
|
|
33820
|
+
});
|
|
33772
33821
|
}
|
|
33773
|
-
return
|
|
33822
|
+
return jsx(Next, {
|
|
33823
|
+
...props
|
|
33824
|
+
});
|
|
33774
33825
|
};
|
|
33775
|
-
|
|
33776
|
-
|
|
33777
|
-
|
|
33778
|
-
|
|
33779
|
-
|
|
33780
|
-
|
|
33781
|
-
|
|
33782
|
-
|
|
33783
|
-
|
|
33784
|
-
|
|
33785
|
-
|
|
33786
|
-
|
|
33787
|
-
|
|
33788
|
-
|
|
33789
|
-
|
|
33790
|
-
|
|
33791
|
-
}, [highlight, ...deps]);
|
|
33826
|
+
const ListItemHeader = props => {
|
|
33827
|
+
const Next = useNextResolver();
|
|
33828
|
+
const {
|
|
33829
|
+
ref
|
|
33830
|
+
} = props;
|
|
33831
|
+
useDisplayedLayoutEffect(ref, headerEl => {
|
|
33832
|
+
const listContainerEl = headerEl.closest(".navi_list_container");
|
|
33833
|
+
const rect = headerEl.getBoundingClientRect();
|
|
33834
|
+
listContainerEl.style.setProperty("--list-header-height", `${rect.height}px`);
|
|
33835
|
+
listContainerEl.style.setProperty("--list-header-width", `${rect.width}px`);
|
|
33836
|
+
}, []);
|
|
33837
|
+
return jsx(Next, {
|
|
33838
|
+
...props,
|
|
33839
|
+
role: "presentation",
|
|
33840
|
+
baseClassName: "navi_list_item_header"
|
|
33841
|
+
});
|
|
33792
33842
|
};
|
|
33793
|
-
|
|
33794
|
-
const
|
|
33795
|
-
const
|
|
33796
|
-
|
|
33797
|
-
|
|
33798
|
-
|
|
33799
|
-
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
33803
|
-
|
|
33804
|
-
|
|
33805
|
-
|
|
33806
|
-
:
|
|
33807
|
-
|
|
33808
|
-
|
|
33809
|
-
}));
|
|
33810
|
-
|
|
33811
|
-
if (entries.length === 0) {
|
|
33812
|
-
return undefined;
|
|
33813
|
-
}
|
|
33814
|
-
|
|
33815
|
-
const ownRanges = [];
|
|
33816
|
-
for (const { root, ranges } of entries) {
|
|
33817
|
-
// Collect text nodes under root with cumulative character offsets so that
|
|
33818
|
-
// [start, end] ranges (positions in the field string) map directly to
|
|
33819
|
-
// the correct DOM text node positions without re-searching.
|
|
33820
|
-
const textNodes = [];
|
|
33821
|
-
let totalLength = 0;
|
|
33822
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
33823
|
-
acceptNode: (node) => {
|
|
33824
|
-
// Skip text nodes inside aria-hidden elements (icons, decorative content, etc.)
|
|
33825
|
-
let parent = node.parentElement;
|
|
33826
|
-
while (parent && parent !== root) {
|
|
33827
|
-
if (parent.getAttribute("aria-hidden") === "true") {
|
|
33828
|
-
return NodeFilter.FILTER_REJECT;
|
|
33829
|
-
}
|
|
33830
|
-
parent = parent.parentElement;
|
|
33831
|
-
}
|
|
33832
|
-
return NodeFilter.FILTER_ACCEPT;
|
|
33833
|
-
},
|
|
33834
|
-
});
|
|
33835
|
-
let node;
|
|
33836
|
-
while ((node = walker.nextNode())) {
|
|
33837
|
-
textNodes.push({ node, offset: totalLength });
|
|
33838
|
-
totalLength += node.textContent.length;
|
|
33839
|
-
}
|
|
33840
|
-
for (const [start, end] of ranges) {
|
|
33841
|
-
for (const { node: textNode, offset: nodeOffset } of textNodes) {
|
|
33842
|
-
const nodeEnd = nodeOffset + textNode.textContent.length;
|
|
33843
|
-
if (nodeEnd <= start || nodeOffset >= end) {
|
|
33844
|
-
continue;
|
|
33845
|
-
}
|
|
33846
|
-
const rangeStart = start - nodeOffset;
|
|
33847
|
-
const rangeEnd = end - nodeOffset;
|
|
33848
|
-
const range = new Range();
|
|
33849
|
-
range.setStart(textNode, rangeStart < 0 ? 0 : rangeStart);
|
|
33850
|
-
range.setEnd(
|
|
33851
|
-
textNode,
|
|
33852
|
-
rangeEnd > textNode.textContent.length
|
|
33853
|
-
? textNode.textContent.length
|
|
33854
|
-
: rangeEnd,
|
|
33855
|
-
);
|
|
33856
|
-
hl.add(range);
|
|
33857
|
-
ownRanges.push(range);
|
|
33858
|
-
}
|
|
33859
|
-
}
|
|
33860
|
-
}
|
|
33861
|
-
return () => {
|
|
33862
|
-
for (const range of ownRanges) {
|
|
33863
|
-
hl.delete(range);
|
|
33864
|
-
}
|
|
33865
|
-
};
|
|
33843
|
+
const ListItemFooter = props => {
|
|
33844
|
+
const Next = useNextResolver();
|
|
33845
|
+
const {
|
|
33846
|
+
ref
|
|
33847
|
+
} = props;
|
|
33848
|
+
useDisplayedLayoutEffect(ref, footerEl => {
|
|
33849
|
+
const listContainerEl = footerEl.closest(".navi_list_container");
|
|
33850
|
+
const rect = footerEl.getBoundingClientRect();
|
|
33851
|
+
listContainerEl.style.setProperty("--list-footer-height", `${rect.height}px`);
|
|
33852
|
+
listContainerEl.style.setProperty("--list-footer-width", `${rect.width}px`);
|
|
33853
|
+
}, []);
|
|
33854
|
+
return jsx(Next, {
|
|
33855
|
+
...props,
|
|
33856
|
+
role: "presentation",
|
|
33857
|
+
baseClassName: "navi_list_item_footer"
|
|
33858
|
+
});
|
|
33866
33859
|
};
|
|
33867
33860
|
|
|
33868
|
-
installImportMetaCssBuild(import.meta);const
|
|
33869
|
-
const GroupItemTrackerContext = createContext(null);
|
|
33870
|
-
const PendingScrollRefContext = createContext(null);
|
|
33871
|
-
|
|
33872
|
-
// When total rendered items exceeds renderBudget, a render window [start, end)
|
|
33873
|
-
// is activated to cap the number of DOM nodes. Items outside the window return
|
|
33874
|
-
// null. The window slides as the user scrolls, using actual DOM positions
|
|
33875
|
-
// (getBoundingClientRect) to find the first visible item — no height estimation.
|
|
33876
|
-
const RENDER_BUDGET_DEFAULT = 100;
|
|
33877
|
-
|
|
33878
|
-
// Attribute used on <li> elements rendered by ListItemReal so the scroll listener
|
|
33879
|
-
// and filler-height calculation can find real items without matching presentation ones.
|
|
33880
|
-
const REAL_LIST_ITEM_SELECTOR = `[navi-list-item-real]`;
|
|
33881
|
-
|
|
33882
|
-
// Carries the render window {start, end} (or null = render all) from
|
|
33883
|
-
// List down to each ListItem.
|
|
33884
|
-
const RenderWindowContext = createContext(null);
|
|
33885
|
-
// Carries the separator element/function down to each ListItem so separators
|
|
33886
|
-
// are only rendered between items that actually mount (post-filter, post-window).
|
|
33887
|
-
const SeparatorContext = createContext(null);
|
|
33888
|
-
const css$m = /* css */`
|
|
33861
|
+
installImportMetaCssBuild(import.meta);const css$m = /* css */`
|
|
33889
33862
|
@layer navi {
|
|
33890
33863
|
.navi_list_container {
|
|
33891
|
-
--list-outline-
|
|
33892
|
-
--list-
|
|
33893
|
-
--list-
|
|
33894
|
-
--list-
|
|
33895
|
-
|
|
33896
|
-
--list-
|
|
33864
|
+
--list-outline-color: var(--navi-focus-outline-color);
|
|
33865
|
+
--list-item-outline-color: var(--navi-focus-outline-color);
|
|
33866
|
+
--list-item-outline-width: 2px;
|
|
33867
|
+
--list-item-outline-offset: calc(-1 * var(--list-item-outline-width));
|
|
33868
|
+
/* Hover (mouse) */
|
|
33869
|
+
--list-item-color-hover: var(--list-item-color);
|
|
33870
|
+
--list-item-background-color-hover: light-dark(#f5f5f5, #2a2a2a);
|
|
33871
|
+
/* Pointed by mouse — subtle, just a shade above background */
|
|
33872
|
+
--list-item-color-mouse-pointed: var(--list-item-color);
|
|
33873
|
+
--list-item-background-color-mouse-pointed: light-dark(#ebebeb, #303030);
|
|
33874
|
+
/* Pointed by keyboard — subtle light blue highlight */
|
|
33875
|
+
--list-item-color-keyboard-pointed: var(--list-item-color);
|
|
33876
|
+
--list-item-background-color-keyboard-pointed: light-dark(
|
|
33877
|
+
#c2dcff,
|
|
33878
|
+
#1c3a6e
|
|
33879
|
+
);
|
|
33880
|
+
/* Pointed by proxy */
|
|
33881
|
+
--list-item-color-pointed: var(--list-item-color);
|
|
33882
|
+
--list-item-background-color-pointed: light-dark(#dbeafe, #1c3a6e);
|
|
33883
|
+
/* Selected — vivid blue accent */
|
|
33884
|
+
--list-item-color-selected: white;
|
|
33885
|
+
--list-item-background-color-selected: rgb(3, 30, 60);
|
|
33886
|
+
--list-item-border-color-selected: var(
|
|
33887
|
+
--list-item-background-color-selected
|
|
33888
|
+
);
|
|
33889
|
+
/* Disabled */
|
|
33890
|
+
--list-item-color-disabled: light-dark(#aaa, #555);
|
|
33891
|
+
--list-item-background-color-disabled: var(--list-item-background-color);
|
|
33897
33892
|
}
|
|
33898
|
-
|
|
33899
|
-
--list-item-padding-x: 0px;
|
|
33900
|
-
--list-item-padding-y: 0px;
|
|
33901
|
-
--list-item-padding: var(--list-item-padding-y) var(--list-item-padding-x);
|
|
33902
|
-
--list-item-color: inherit;
|
|
33903
|
-
--list-item-font-weight: inherit;
|
|
33904
|
-
--list-item-background-color: transparent;
|
|
33905
|
-
|
|
33906
|
-
/* Highlight (CSS Highlight API match) */
|
|
33907
|
-
--list-item-color-highlight: inherit;
|
|
33908
|
-
--list-item-background-color-highlight: #ffe066;
|
|
33893
|
+
}
|
|
33909
33894
|
|
|
33910
|
-
|
|
33911
|
-
|
|
33912
|
-
|
|
33913
|
-
.navi_list_item_group_label {
|
|
33914
|
-
--list-group-label-background-color: var(--list-background-color);
|
|
33915
|
-
}
|
|
33916
|
-
.navi_list_item_header {
|
|
33917
|
-
background: var(--list-background-color);
|
|
33918
|
-
}
|
|
33919
|
-
.navi_list_item_footer {
|
|
33920
|
-
background: var(--list-background-color);
|
|
33921
|
-
}
|
|
33895
|
+
fieldset.navi_list_container {
|
|
33896
|
+
margin: 0; /* Reset margin that might come from fieldset */
|
|
33897
|
+
padding: 0; /* Reset padding that might come from fieldset */
|
|
33922
33898
|
}
|
|
33923
33899
|
|
|
33924
33900
|
.navi_list_container {
|
|
33925
|
-
--x-list-
|
|
33926
|
-
|
|
33927
|
-
--x-list-border-color: var(--list-border-color);
|
|
33928
|
-
--x-list-background-color: var(--list-background-color);
|
|
33929
|
-
/* When typing inside an input browser tries to keep caret visible */
|
|
33930
|
-
/* For input within a sticky element inside a scrollable container */
|
|
33931
|
-
/* Browser will try to scroll that input into view */
|
|
33932
|
-
/* When that scrollable container has a scroll padding it causes scroll on each keystroke */
|
|
33933
|
-
/* Even putting a scroll margin on the input won't fix */
|
|
33934
|
-
/* The only solution is to use scroll-margins on each item that can scroll */
|
|
33935
|
-
/* This is why these props are named list-scroll-spacing-top and applied via scroll-margin on items */
|
|
33936
|
-
--x-list-scroll-spacing-top: calc(
|
|
33937
|
-
var(--list-header-height, 0px) + var(--list-scroll-padding-top, 0px)
|
|
33938
|
-
);
|
|
33939
|
-
--x-list-scroll-spacing-bottom: calc(
|
|
33940
|
-
var(--list-footer-height, 0px) + var(--list-scroll-padding-bottom, 0px)
|
|
33901
|
+
--x-list-outline-width: calc(
|
|
33902
|
+
var(--list-outline-width) + var(--list-border-width)
|
|
33941
33903
|
);
|
|
33904
|
+
--x-list-outline-offset: calc(-1 * var(--list-border-width));
|
|
33942
33905
|
|
|
33943
|
-
|
|
33944
|
-
|
|
33945
|
-
|
|
33946
|
-
max-width: 100%;
|
|
33947
|
-
flex-direction: column;
|
|
33948
|
-
background-color: var(--x-list-background-color);
|
|
33949
|
-
border: var(--x-list-border-width) solid var(--x-list-border-color);
|
|
33950
|
-
border-radius: var(--x-list-border-radius);
|
|
33951
|
-
|
|
33952
|
-
transition: opacity 0.2s ease;
|
|
33953
|
-
/* overflow:hidden is required on the container (not the inner scroll element)
|
|
33954
|
-
so that border-radius clips the content correctly. Without it, items near
|
|
33955
|
-
the corners would visually overflow the rounded corners during scroll. */
|
|
33956
|
-
overflow: hidden;
|
|
33957
|
-
|
|
33958
|
-
.navi_list_scroll_container {
|
|
33959
|
-
width: inherit;
|
|
33960
|
-
min-width: inherit;
|
|
33961
|
-
max-width: inherit;
|
|
33962
|
-
max-height: var(--list-max-height);
|
|
33963
|
-
overflow: auto;
|
|
33964
|
-
overscroll-behavior: inherit; /* inherit select behavior */
|
|
33965
|
-
}
|
|
33906
|
+
outline-width: var(--x-list-outline-width);
|
|
33907
|
+
outline-color: var(--list-outline-color);
|
|
33908
|
+
outline-offset: var(--x-list-outline-offset);
|
|
33966
33909
|
|
|
33967
|
-
&[data-
|
|
33968
|
-
|
|
33910
|
+
&[data-focus] {
|
|
33911
|
+
/* outline: var(--list-outline-width) solid var(--navi-focus-outline-color);
|
|
33912
|
+
outline-offset: calc(-1 * var(--list-outline-width)); */
|
|
33969
33913
|
}
|
|
33970
|
-
&[
|
|
33971
|
-
|
|
33972
|
-
inset: unset;
|
|
33973
|
-
display: none;
|
|
33974
|
-
min-width: var(--list-anchor-width, 0px);
|
|
33975
|
-
max-width: 95vw;
|
|
33976
|
-
margin: 0;
|
|
33977
|
-
padding: 0;
|
|
33978
|
-
|
|
33979
|
-
&:popover-open {
|
|
33980
|
-
display: flex;
|
|
33981
|
-
}
|
|
33982
|
-
.navi_list {
|
|
33983
|
-
width: 100%;
|
|
33984
|
-
}
|
|
33985
|
-
|
|
33986
|
-
&[data-anchor-hidden] {
|
|
33987
|
-
opacity: 0;
|
|
33988
|
-
pointer-events: none;
|
|
33989
|
-
}
|
|
33914
|
+
&[data-focus-visible] {
|
|
33915
|
+
outline-style: solid;
|
|
33990
33916
|
}
|
|
33991
|
-
|
|
33992
|
-
|
|
33993
|
-
.navi_list {
|
|
33994
|
-
display: flex;
|
|
33995
|
-
box-sizing: border-box;
|
|
33996
|
-
margin: 0;
|
|
33997
|
-
padding: 0;
|
|
33998
|
-
flex-direction: column;
|
|
33999
|
-
list-style: none;
|
|
34000
|
-
outline: none; /* Focus is displayed on the container */
|
|
34001
|
-
|
|
34002
|
-
/* Would create scrollbars, for now just hide the loader here */
|
|
34003
|
-
.navi_input {
|
|
34004
|
-
.navi_loading_rectangle_wrapper {
|
|
34005
|
-
display: none;
|
|
34006
|
-
}
|
|
33917
|
+
&[data-callout] {
|
|
33918
|
+
--x-list-border-color: var(--callout-color);
|
|
34007
33919
|
}
|
|
34008
33920
|
}
|
|
34009
33921
|
|
|
34010
33922
|
.navi_list_item {
|
|
34011
|
-
--x-list-item-
|
|
34012
|
-
--x-list-item-background-color: var(--list-item-background-color);
|
|
34013
|
-
--x-list-item-font-weight: var(--list-item-font-weight);
|
|
34014
|
-
|
|
34015
|
-
box-sizing: border-box;
|
|
34016
|
-
min-width: 0;
|
|
34017
|
-
max-width: 100%;
|
|
34018
|
-
padding: var(--list-item-padding);
|
|
34019
|
-
color: var(--x-list-item-color);
|
|
34020
|
-
font-weight: var(--x-list-item-font-weight);
|
|
34021
|
-
background-color: var(--x-list-item-background-color);
|
|
34022
|
-
/*
|
|
34023
|
-
CSS impossible d'obtenir un layout qui ferait en gros:
|
|
34024
|
-
width = max(min(max-content, 100%), unbreakable-content)
|
|
34025
|
-
Donc 3 options:
|
|
34026
|
-
- Laisser le contenu overflow
|
|
34027
|
-
- moche, background ne suit pas
|
|
34028
|
-
-> NOPE
|
|
34029
|
-
- Force overflow hidden + ellipsis
|
|
34030
|
-
- casse la lisibilité des mots insécables
|
|
34031
|
-
- possible d'optin en utilisant overflowEllipsis sur le ListItem
|
|
34032
|
-
-> Bien mais pas par défaut
|
|
34033
|
-
- Forcer le retour a la ligne des mot inécables
|
|
34034
|
-
- Aucun des inconvénient ci dessus
|
|
34035
|
-
-> Comportement par défaut
|
|
34036
|
-
*/
|
|
34037
|
-
overflow-wrap: anywhere;
|
|
34038
|
-
/* When list has sticky header/footer, put a scroll padding */
|
|
34039
|
-
scroll-margin-top: var(--x-list-scroll-spacing-top);
|
|
34040
|
-
scroll-margin-bottom: var(--x-list-scroll-spacing-bottom);
|
|
34041
|
-
}
|
|
33923
|
+
--x-list-item-cursor: default;
|
|
34042
33924
|
|
|
34043
|
-
|
|
34044
|
-
|
|
34045
|
-
|
|
34046
|
-
|
|
34047
|
-
|
|
34048
|
-
height: 0px;
|
|
34049
|
-
list-style: none;
|
|
34050
|
-
/* background: pink; */
|
|
34051
|
-
}
|
|
33925
|
+
position: relative;
|
|
33926
|
+
outline-width: var(--list-item-outline-width);
|
|
33927
|
+
outline-color: var(--list-item-outline-color);
|
|
33928
|
+
outline-offset: var(--list-item-outline-offset);
|
|
33929
|
+
cursor: var(--x-list-item-cursor);
|
|
34052
33930
|
|
|
34053
|
-
|
|
34054
|
-
order: 1 pushes fallbacks after all regular items in flex column layout.
|
|
34055
|
-
The list children are open-ended (headers, presentation items, real items),
|
|
34056
|
-
so we cannot control where the consumer places the fallback nodes in the DOM.
|
|
34057
|
-
Using order ensures fallbacks always appear after items regardless of DOM order.
|
|
34058
|
-
matchFallback intentionally shares the same order as fallback so it appears
|
|
34059
|
-
at the same visual position — after an input if present but before any items
|
|
34060
|
-
still displayed (non-matching items remain in DOM with hidden prop):
|
|
34061
|
-
1. Input (sticky header, order: -2)
|
|
34062
|
-
2. matchFallback (order: -1)
|
|
34063
|
-
3. hidden items (regular order, after DOM flow)
|
|
34064
|
-
4. HOT FIX OF THE DEAD for bottom filler + preact issue: order: 1
|
|
34065
|
-
5. sticky footer (order: 2)
|
|
34066
|
-
*/
|
|
34067
|
-
/* order: 0 keeps the header pinned before fallbacks (order: 1) in flex order,
|
|
34068
|
-
ensuring the header (e.g. a search input) always appears above them. */
|
|
34069
|
-
.navi_list_item_header {
|
|
34070
|
-
position: sticky;
|
|
34071
|
-
top: 0;
|
|
34072
|
-
z-index: 1;
|
|
34073
|
-
order: -2;
|
|
34074
|
-
}
|
|
34075
|
-
.navi_list_fallback,
|
|
34076
|
-
.navi_list_no_match_fallback {
|
|
34077
|
-
order: -1;
|
|
34078
|
-
color: light-dark(#888, #aaa);
|
|
34079
|
-
&[navi-default] {
|
|
34080
|
-
display: inline;
|
|
34081
|
-
padding: var(--list-item-padding);
|
|
34082
|
-
text-align: center;
|
|
33931
|
+
&[navi-selectable] {
|
|
34083
33932
|
user-select: none;
|
|
34084
33933
|
}
|
|
34085
|
-
|
|
34086
|
-
|
|
34087
|
-
|
|
34088
|
-
I've noticed that removing the ItemIndexToScrollOnMountRefContext.Provider
|
|
34089
|
-
does fix this issue (I suppose it's because it cause on less render of the list which is the problematic one)
|
|
34090
|
-
this order ENSURE that even when preact hallucinates we are still correctly putting the bottom filler
|
|
34091
|
-
after the list items */
|
|
34092
|
-
order: 1;
|
|
34093
|
-
}
|
|
34094
|
-
/* order: 2 pins the footer after fallbacks (order: 1) and all items. */
|
|
34095
|
-
.navi_list_item_footer {
|
|
34096
|
-
position: sticky;
|
|
34097
|
-
bottom: 0;
|
|
34098
|
-
z-index: 1;
|
|
34099
|
-
order: 2;
|
|
34100
|
-
}
|
|
34101
|
-
|
|
34102
|
-
::highlight(navi-search-match) {
|
|
34103
|
-
color: var(--list-item-color-highlight);
|
|
34104
|
-
background-color: var(--list-item-background-color-highlight);
|
|
34105
|
-
}
|
|
33934
|
+
&[navi-selectable-area-all] {
|
|
33935
|
+
--x-list-item-cursor: pointer;
|
|
33936
|
+
pointer-events: none;
|
|
34106
33937
|
|
|
34107
|
-
|
|
34108
|
-
|
|
34109
|
-
|
|
33938
|
+
[navi-selectable-real-input] {
|
|
33939
|
+
z-index: 0;
|
|
33940
|
+
outline: none;
|
|
33941
|
+
opacity: 0;
|
|
33942
|
+
clip-path: none;
|
|
33943
|
+
cursor: var(--x-list-item-cursor);
|
|
33944
|
+
pointer-events: auto;
|
|
33945
|
+
}
|
|
33946
|
+
}
|
|
34110
33947
|
|
|
34111
|
-
|
|
34112
|
-
|
|
34113
|
-
top: 0;
|
|
34114
|
-
z-index: 1;
|
|
34115
|
-
display: block;
|
|
34116
|
-
background-color: var(--list-group-label-background-color);
|
|
33948
|
+
&[data-interactive] {
|
|
33949
|
+
cursor: pointer;
|
|
34117
33950
|
user-select: none;
|
|
33951
|
+
}
|
|
33952
|
+
&[data-hover] {
|
|
33953
|
+
--x-list-item-color: var(--list-item-color-mouse-pointed);
|
|
33954
|
+
--x-list-item-background-color: var(
|
|
33955
|
+
--list-item-background-color-mouse-pointed
|
|
33956
|
+
);
|
|
33957
|
+
}
|
|
33958
|
+
&[data-pointed] {
|
|
33959
|
+
--x-list-item-color: var(--list-item-color-pointed);
|
|
33960
|
+
--x-list-item-background-color: var(--list-item-background-color-pointed);
|
|
33961
|
+
}
|
|
33962
|
+
/* No input proxy: focused,selected */
|
|
33963
|
+
&:not(:has(input[navi-control-proxy-for])) {
|
|
33964
|
+
&:has([data-focus-visible]) {
|
|
33965
|
+
--x-list-item-color: var(--list-item-color-keyboard-pointed);
|
|
33966
|
+
--x-list-item-background-color: var(
|
|
33967
|
+
--list-item-background-color-keyboard-pointed
|
|
33968
|
+
);
|
|
33969
|
+
outline-style: solid;
|
|
34118
33970
|
|
|
34119
|
-
|
|
34120
|
-
|
|
34121
|
-
|
|
34122
|
-
|
|
34123
|
-
|
|
34124
|
-
|
|
34125
|
-
|
|
33971
|
+
/* Selected must win over keyboard-pointed */
|
|
33972
|
+
&[data-selected] {
|
|
33973
|
+
--x-list-item-background-color: var(
|
|
33974
|
+
--list-item-background-color-selected,
|
|
33975
|
+
var(--list-item-background-color-keyboard-pointed)
|
|
33976
|
+
);
|
|
33977
|
+
}
|
|
34126
33978
|
}
|
|
34127
|
-
}
|
|
34128
|
-
.navi_list_item_group_list {
|
|
34129
|
-
display: flex;
|
|
34130
|
-
width: max-content;
|
|
34131
|
-
min-width: 100%;
|
|
34132
|
-
margin: 0;
|
|
34133
|
-
padding: 0;
|
|
34134
|
-
flex-direction: column;
|
|
34135
|
-
list-style: none;
|
|
34136
33979
|
|
|
34137
|
-
|
|
34138
|
-
|
|
34139
|
-
|
|
34140
|
-
|
|
34141
|
-
var(--x-list-scroll-spacing-top) + var(--list-group-label-height, 0px)
|
|
33980
|
+
&[data-selected] {
|
|
33981
|
+
--x-list-item-border-color: var(--list-item-border-color-selected);
|
|
33982
|
+
--x-list-item-background-color: var(
|
|
33983
|
+
--list-item-background-color-selected
|
|
34142
33984
|
);
|
|
33985
|
+
--x-list-item-color: var(--list-item-color-selected);
|
|
33986
|
+
|
|
33987
|
+
&[data-hover] {
|
|
33988
|
+
--x-list-item-background-color: var(
|
|
33989
|
+
--list-item-background-color-selected,
|
|
33990
|
+
var(--list-item-background-color-mouse-pointed)
|
|
33991
|
+
) !important;
|
|
33992
|
+
}
|
|
34143
33993
|
}
|
|
34144
33994
|
}
|
|
34145
33995
|
|
|
34146
|
-
&[data-
|
|
34147
|
-
|
|
33996
|
+
&[data-disabled] {
|
|
33997
|
+
--x-list-item-color: var(--list-item-color-disabled);
|
|
33998
|
+
--x-list-item-background-color: var(
|
|
33999
|
+
--list-item-background-color-disabled
|
|
34000
|
+
);
|
|
34001
|
+
--x-list-item-cursor: default;
|
|
34002
|
+
pointer-events: none;
|
|
34003
|
+
}
|
|
34004
|
+
&[data-readonly] {
|
|
34005
|
+
--x-list-item-color: var(--list-item-color-disabled);
|
|
34006
|
+
--x-list-item-cursor: default;
|
|
34148
34007
|
}
|
|
34149
34008
|
}
|
|
34150
34009
|
`;
|
|
34151
|
-
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
|
|
34156
|
-
|
|
34157
|
-
|
|
34158
|
-
|
|
34159
|
-
|
|
34160
|
-
|
|
34161
|
-
|
|
34162
|
-
|
|
34163
|
-
* uiAction — called with the selected value on confirm. When provided
|
|
34164
|
-
* the list becomes interactive: tracks hover and keyboard-
|
|
34165
|
-
* pointed state, handles navi_list_nav / navi_list_clear /
|
|
34166
|
-
* navi_list_confirm custom events via ListInteractionContext.
|
|
34167
|
-
* popover — when true, renders as a managed popover positioned near
|
|
34168
|
-
* an anchor element via navi_list_open / navi_list_close events.
|
|
34169
|
-
* renderBudget — max items in DOM at once (default 100, virtual scroll when exceeded)
|
|
34170
|
-
* virtualItemHeight — fixed px height per item when all items have the same height.
|
|
34171
|
-
* Enables precise virtual-scroll filler sizing without a DOM
|
|
34172
|
-
* measurement pass. Required when renderBudget is active and
|
|
34173
|
-
* item height is known up-front.
|
|
34174
|
-
* fallback — content shown when no items exist at all
|
|
34175
|
-
* matchFallback — content shown when items exist but all are hidden (e.g. no search match)
|
|
34176
|
-
* separator — element or function(index, { previousItem, currentItem }) inserted between visible items
|
|
34177
|
-
* lockSize — when true, captures the container's dimensions on first render
|
|
34178
|
-
* (always in unfiltered state). Those values become min-width/
|
|
34179
|
-
* min-height so filtering cannot collapse the layout.
|
|
34180
|
-
* ...rest — forwarded to the outer scroll container <Box>
|
|
34181
|
-
*/
|
|
34182
|
-
const List = props => {
|
|
34183
|
-
const refDefault = useRef(null);
|
|
34184
|
-
props.ref = props.ref || refDefault;
|
|
34185
|
-
const idDefault = useId();
|
|
34186
|
-
props.id = props.id || idDefault;
|
|
34187
|
-
const listVnode = jsx(ListUI, {
|
|
34010
|
+
const SelectableListMultipleContext = createContext(false);
|
|
34011
|
+
// Interactive variant: manages hover/keyboard/selection state and handles the
|
|
34012
|
+
// navi event protocol. When an action is provided it binds the action to ui state
|
|
34013
|
+
// and fires it on select. When only uiAction is provided it calls it directly.
|
|
34014
|
+
const ListSelectableResolver = props => {
|
|
34015
|
+
const Next = useNextResolver();
|
|
34016
|
+
if (props.selectable) {
|
|
34017
|
+
return jsx(ListSelectable, {
|
|
34018
|
+
...props
|
|
34019
|
+
});
|
|
34020
|
+
}
|
|
34021
|
+
return jsx(Next, {
|
|
34188
34022
|
...props
|
|
34189
34023
|
});
|
|
34190
|
-
return listVnode;
|
|
34191
34024
|
};
|
|
34192
|
-
const
|
|
34193
|
-
|
|
34025
|
+
const ListSelectable = props => {
|
|
34026
|
+
const Next = useNextResolver();
|
|
34027
|
+
import.meta.css = [css$m, "@jsenv/navi/src/control/list/list_selectable.jsx"];
|
|
34028
|
+
// we allow ourselves to auto-generate a name
|
|
34029
|
+
const defaultName = useId();
|
|
34030
|
+
props.name = props.name || `listbox_${defaultName}`;
|
|
34194
34031
|
const {
|
|
34195
34032
|
ref,
|
|
34196
|
-
|
|
34197
|
-
|
|
34198
|
-
|
|
34199
|
-
|
|
34200
|
-
noMatchFallback,
|
|
34201
|
-
separator,
|
|
34202
|
-
children,
|
|
34203
|
-
popover,
|
|
34204
|
-
expandX,
|
|
34205
|
-
expand,
|
|
34206
|
-
maxHeight,
|
|
34207
|
-
onListVisibleItemsChange,
|
|
34208
|
-
virtualItemHeight,
|
|
34209
|
-
lockSize,
|
|
34210
|
-
searchText,
|
|
34211
|
-
...rest
|
|
34033
|
+
multiple,
|
|
34034
|
+
selectedIndicator = "backgroundColor",
|
|
34035
|
+
focusGroupDirection,
|
|
34036
|
+
focusGroupWrap
|
|
34212
34037
|
} = props;
|
|
34213
|
-
|
|
34214
|
-
|
|
34215
|
-
|
|
34216
|
-
|
|
34217
|
-
|
|
34218
|
-
|
|
34219
|
-
|
|
34220
|
-
|
|
34221
|
-
|
|
34222
|
-
|
|
34223
|
-
|
|
34224
|
-
|
|
34225
|
-
|
|
34226
|
-
|
|
34227
|
-
// Use borderBoxSize (outer width) not contentRect (which excludes the
|
|
34228
|
-
// scrollbar width). If we used contentRect, min-width would be set to
|
|
34229
|
-
// outerWidth − scrollbarWidth, and the container would shrink by exactly
|
|
34230
|
-
// the scrollbar width when the scrollbar disappears.
|
|
34231
|
-
const borderBoxEntry = entry.borderBoxSize ? entry.borderBoxSize[0] : null;
|
|
34232
|
-
const width = borderBoxEntry ? borderBoxEntry.inlineSize : entry.contentRect.width;
|
|
34233
|
-
const height = borderBoxEntry ? borderBoxEntry.blockSize : entry.contentRect.height;
|
|
34234
|
-
if (width === 0 && height === 0) {
|
|
34235
|
-
return;
|
|
34038
|
+
const [listControlProps, remainingProps, childrenWrapperProps, uiGroupStateController] = useControlgroupProps(props, {
|
|
34039
|
+
stateType: multiple ? "array" : "",
|
|
34040
|
+
controlType: multiple ? "checkbox_group" : "radio_group",
|
|
34041
|
+
childControlFilter: multiple ? childUIStateController => {
|
|
34042
|
+
return childUIStateController.controlType === "input" && childUIStateController.props.type === "checkbox";
|
|
34043
|
+
} : childUIStateController => {
|
|
34044
|
+
return childUIStateController.controlType === "input" && childUIStateController.props.type === "radio";
|
|
34045
|
+
},
|
|
34046
|
+
aggregateChildStates: multiple ? childUIStateControllers => {
|
|
34047
|
+
const values = [];
|
|
34048
|
+
for (const childUIStateController of childUIStateControllers) {
|
|
34049
|
+
if (childUIStateController.uiState) {
|
|
34050
|
+
values.push(childUIStateController.uiState);
|
|
34051
|
+
}
|
|
34236
34052
|
}
|
|
34237
|
-
|
|
34238
|
-
|
|
34239
|
-
|
|
34240
|
-
|
|
34241
|
-
|
|
34242
|
-
|
|
34243
|
-
|
|
34244
|
-
|
|
34245
|
-
};
|
|
34246
|
-
}, [lockSize]);
|
|
34247
|
-
const tracker = useItemTracker({
|
|
34248
|
-
onChange: () => {
|
|
34249
|
-
onListVisibleItemsChange?.(tracker.visibleItemsSignal.peek());
|
|
34250
|
-
}
|
|
34251
|
-
});
|
|
34252
|
-
const {
|
|
34253
|
-
virtualItemHeightSignal,
|
|
34254
|
-
renderWindow,
|
|
34255
|
-
scrollToItem,
|
|
34256
|
-
pendingScrollRef
|
|
34257
|
-
} = useListScrollSync({
|
|
34258
|
-
ref,
|
|
34259
|
-
tracker,
|
|
34260
|
-
renderBudget,
|
|
34261
|
-
virtualItemHeight,
|
|
34262
|
-
searchText
|
|
34263
|
-
});
|
|
34264
|
-
const getItemById = itemId => {
|
|
34265
|
-
return tracker.itemsSignal.peek().find(item => item.id === itemId);
|
|
34266
|
-
};
|
|
34267
|
-
return jsx(Box, {
|
|
34268
|
-
...rest,
|
|
34269
|
-
ref: ref,
|
|
34270
|
-
baseClassName: "navi_list_container",
|
|
34271
|
-
popover: popover,
|
|
34272
|
-
"data-expand-x": expandX || expand ? "" : undefined,
|
|
34273
|
-
expandX: expandX,
|
|
34274
|
-
expand: expand,
|
|
34275
|
-
maxHeight: maxHeight,
|
|
34276
|
-
styleCSSVars: LIST_STYLE_CSS_VARS,
|
|
34277
|
-
pseudoClasses: LIST_PSEUDO_CLASSES,
|
|
34278
|
-
hasChildUsingForwardedProps: true,
|
|
34279
|
-
onnavi_request_scroll: e => {
|
|
34280
|
-
if (!Object.hasOwn(e.detail, "id")) {
|
|
34281
|
-
console.warn(`navi_request_scroll event is missing the "id" property in its detail.`, e);
|
|
34282
|
-
return;
|
|
34053
|
+
return values.length === 0 ? undefined : values;
|
|
34054
|
+
} : childUIStateControllers => {
|
|
34055
|
+
let activeValue;
|
|
34056
|
+
for (const childUIStateController of childUIStateControllers) {
|
|
34057
|
+
if (childUIStateController.uiState) {
|
|
34058
|
+
activeValue = childUIStateController.uiState;
|
|
34059
|
+
break;
|
|
34060
|
+
}
|
|
34283
34061
|
}
|
|
34284
|
-
|
|
34285
|
-
|
|
34286
|
-
} = e.detail;
|
|
34287
|
-
const item = getItemById(id);
|
|
34288
|
-
scrollToItem(item, {
|
|
34289
|
-
event: e,
|
|
34290
|
-
reason: "navi_request_scroll"
|
|
34291
|
-
});
|
|
34292
|
-
},
|
|
34293
|
-
children: jsx(ListContent, {
|
|
34294
|
-
role: role,
|
|
34295
|
-
fallback: fallback,
|
|
34296
|
-
noMatchFallback: noMatchFallback,
|
|
34297
|
-
searchText: searchText,
|
|
34298
|
-
separator: separator,
|
|
34299
|
-
expandX: expandX,
|
|
34300
|
-
expand: expand,
|
|
34301
|
-
tracker: tracker,
|
|
34302
|
-
renderWindow: renderWindow,
|
|
34303
|
-
virtualItemHeightSignal: virtualItemHeightSignal,
|
|
34304
|
-
pendingScrollRef: pendingScrollRef,
|
|
34305
|
-
children: children
|
|
34306
|
-
})
|
|
34307
|
-
});
|
|
34308
|
-
};
|
|
34309
|
-
const ListContent = ({
|
|
34310
|
-
role,
|
|
34311
|
-
fallback,
|
|
34312
|
-
noMatchFallback,
|
|
34313
|
-
searchText,
|
|
34314
|
-
separator,
|
|
34315
|
-
expandX,
|
|
34316
|
-
expand,
|
|
34317
|
-
tracker,
|
|
34318
|
-
renderWindow,
|
|
34319
|
-
virtualItemHeightSignal,
|
|
34320
|
-
pendingScrollRef,
|
|
34321
|
-
children
|
|
34322
|
-
}) => {
|
|
34323
|
-
const listProps = useContext(BoxForwardedPropsContext);
|
|
34324
|
-
return jsx("div", {
|
|
34325
|
-
className: "navi_list_scroll_container",
|
|
34326
|
-
children: jsx(UnorderedList, {
|
|
34327
|
-
role: role,
|
|
34328
|
-
fallback: fallback,
|
|
34329
|
-
noMatchFallback: noMatchFallback,
|
|
34330
|
-
searchText: searchText,
|
|
34331
|
-
separator: separator === true ? jsx(Separator, {
|
|
34332
|
-
margin: "0"
|
|
34333
|
-
}) : separator,
|
|
34334
|
-
expandX: expandX || expand,
|
|
34335
|
-
...listProps,
|
|
34336
|
-
tracker: tracker,
|
|
34337
|
-
renderWindow: renderWindow,
|
|
34338
|
-
virtualItemHeightSignal: virtualItemHeightSignal,
|
|
34339
|
-
children: jsx(PendingScrollRefContext.Provider, {
|
|
34340
|
-
value: pendingScrollRef,
|
|
34341
|
-
children: children
|
|
34342
|
-
})
|
|
34343
|
-
})
|
|
34062
|
+
return activeValue;
|
|
34063
|
+
}
|
|
34344
34064
|
});
|
|
34345
|
-
|
|
34346
|
-
|
|
34347
|
-
|
|
34348
|
-
|
|
34349
|
-
|
|
34350
|
-
borderWidth: "--list-border-width"
|
|
34351
|
-
};
|
|
34352
|
-
const LIST_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled", ":-navi-void", ":-navi-expanded"];
|
|
34353
|
-
const useListScrollSync = ({
|
|
34354
|
-
ref,
|
|
34355
|
-
tracker,
|
|
34356
|
-
renderBudget,
|
|
34357
|
-
virtualItemHeight,
|
|
34358
|
-
searchText
|
|
34359
|
-
}) => {
|
|
34360
|
-
const debugScroll = useDebugScroll();
|
|
34361
|
-
const virtualItemHeightSignal = useVirtualItemHeightSignal(ref, virtualItemHeight);
|
|
34362
|
-
const [renderWindow, setRenderWindow] = useState({
|
|
34363
|
-
start: 0,
|
|
34364
|
-
end: renderBudget
|
|
34065
|
+
useFocusGroup(ref, {
|
|
34066
|
+
direction: focusGroupDirection,
|
|
34067
|
+
wrap: focusGroupWrap,
|
|
34068
|
+
// Up/Down navigate between list items only (the visually-hidden real inputs).
|
|
34069
|
+
ySelector: "[navi-selectable-real-input]"
|
|
34365
34070
|
});
|
|
34366
|
-
|
|
34367
|
-
|
|
34368
|
-
|
|
34369
|
-
|
|
34370
|
-
|
|
34371
|
-
|
|
34372
|
-
|
|
34373
|
-
|
|
34374
|
-
|
|
34375
|
-
|
|
34376
|
-
|
|
34377
|
-
|
|
34378
|
-
start: newStart,
|
|
34379
|
-
end: newEnd
|
|
34380
|
-
};
|
|
34381
|
-
renderWindowRef.current = renderWindow;
|
|
34382
|
-
setRenderWindow(renderWindow);
|
|
34383
|
-
};
|
|
34384
|
-
const pendingScrollRef = useRef();
|
|
34385
|
-
const scrollToItem = (item, {
|
|
34386
|
-
event,
|
|
34387
|
-
reason
|
|
34388
|
-
}) => {
|
|
34389
|
-
if (!item) {
|
|
34390
|
-
return;
|
|
34391
|
-
}
|
|
34392
|
-
const items = tracker.itemsSignal.peek();
|
|
34393
|
-
const itemCount = items.length;
|
|
34394
|
-
if (itemCount === 0) {
|
|
34071
|
+
|
|
34072
|
+
// "Current item" tracking — the item that an external controller (e.g. an
|
|
34073
|
+
// <input navi-list>) navigates from. Defaults to the first selected item,
|
|
34074
|
+
// else the first navigable item. Updated when:
|
|
34075
|
+
// - an item's real input gains focus (via Tab, click, etc.)
|
|
34076
|
+
// - the controller dispatches navi_request_list_nav
|
|
34077
|
+
// The current id is announced via navi_list_current_change (bubbling) so a
|
|
34078
|
+
// connected input can update its aria-controls / aria-activedescendant.
|
|
34079
|
+
const currentIdRef = useRef(null);
|
|
34080
|
+
const setCurrentId = (id, event) => {
|
|
34081
|
+
const previousId = currentIdRef.current;
|
|
34082
|
+
if (previousId === id) {
|
|
34395
34083
|
return;
|
|
34396
34084
|
}
|
|
34397
|
-
|
|
34398
|
-
|
|
34085
|
+
currentIdRef.current = id;
|
|
34086
|
+
const listEl = ref.current;
|
|
34087
|
+
if (!listEl) {
|
|
34399
34088
|
return;
|
|
34400
34089
|
}
|
|
34401
|
-
if (
|
|
34402
|
-
|
|
34090
|
+
if (id) {
|
|
34091
|
+
listEl.setAttribute("navi-current-id", id);
|
|
34092
|
+
} else {
|
|
34093
|
+
listEl.removeAttribute("navi-current-id");
|
|
34403
34094
|
}
|
|
34404
|
-
|
|
34405
|
-
|
|
34406
|
-
|
|
34407
|
-
|
|
34408
|
-
|
|
34409
|
-
|
|
34410
|
-
|
|
34411
|
-
|
|
34412
|
-
|
|
34413
|
-
|
|
34414
|
-
block
|
|
34415
|
-
});
|
|
34416
|
-
const listEl = ref.current.querySelector(".navi_list");
|
|
34417
|
-
dispatchPublicCustomEvent(listEl, "navi_scroll", {
|
|
34418
|
-
event,
|
|
34419
|
-
item
|
|
34420
|
-
});
|
|
34421
|
-
};
|
|
34422
|
-
const {
|
|
34423
|
-
start,
|
|
34424
|
-
end
|
|
34425
|
-
} = renderWindowRef.current;
|
|
34426
|
-
const isInWindow = index >= start && index < end;
|
|
34427
|
-
if (isInWindow) {
|
|
34428
|
-
const itemEl = document.getElementById(item.id);
|
|
34429
|
-
if (itemEl) {
|
|
34430
|
-
scrollItemIntoView(itemEl);
|
|
34431
|
-
return;
|
|
34432
|
-
}
|
|
34095
|
+
dispatchPublicCustomEvent(listEl, "navi_current_change", {
|
|
34096
|
+
event,
|
|
34097
|
+
id,
|
|
34098
|
+
realInputId: id ? `${id}_input` : null
|
|
34099
|
+
});
|
|
34100
|
+
};
|
|
34101
|
+
const getNavigableElements = () => {
|
|
34102
|
+
const listEl = ref.current;
|
|
34103
|
+
if (!listEl) {
|
|
34104
|
+
return [];
|
|
34433
34105
|
}
|
|
34434
|
-
|
|
34435
|
-
|
|
34436
|
-
|
|
34437
|
-
|
|
34438
|
-
|
|
34439
|
-
pendingScrollRef.current = null;
|
|
34440
|
-
scrollItemIntoView(itemEl);
|
|
34106
|
+
const itemEls = Array.from(listEl.querySelectorAll("[navi-list-item-real]"));
|
|
34107
|
+
const navigableEls = [];
|
|
34108
|
+
for (const itemEl of itemEls) {
|
|
34109
|
+
if (itemEl.hidden) {
|
|
34110
|
+
continue;
|
|
34441
34111
|
}
|
|
34442
|
-
|
|
34443
|
-
|
|
34444
|
-
|
|
34445
|
-
const newEnd = newStart + renderBudget;
|
|
34446
|
-
updateRenderWindow(newStart, newEnd, `item to scroll (at ${index}) is out of render window`);
|
|
34447
|
-
};
|
|
34448
|
-
const currentScrollRef = useRef(null);
|
|
34449
|
-
const updateCurrentScroll = () => {
|
|
34450
|
-
const listScrollContainerEl = ref.current.querySelector(`.navi_list_scroll_container`);
|
|
34451
|
-
const currentScrollLeft = listScrollContainerEl.scrollLeft;
|
|
34452
|
-
const currentScrollTop = listScrollContainerEl.scrollTop;
|
|
34453
|
-
const renderWindow = renderWindowRef.current;
|
|
34454
|
-
currentScrollRef.current = {
|
|
34455
|
-
left: currentScrollLeft,
|
|
34456
|
-
top: currentScrollTop,
|
|
34457
|
-
renderWindow: {
|
|
34458
|
-
...renderWindow
|
|
34112
|
+
const realInput = itemEl.querySelector("[navi-selectable-real-input]");
|
|
34113
|
+
if (!realInput || realInput.disabled) {
|
|
34114
|
+
continue;
|
|
34459
34115
|
}
|
|
34460
|
-
|
|
34461
|
-
debugScroll(`store currentScroll: scrollTop=${currentScrollTop}, renderWindow=[${renderWindow.start}, ${renderWindow.end})`);
|
|
34462
|
-
};
|
|
34463
|
-
const searchTextRef = useRef();
|
|
34464
|
-
let searchTextBecomesActive = false;
|
|
34465
|
-
if (searchTextRef.current === undefined) {
|
|
34466
|
-
searchTextRef.current = searchText;
|
|
34467
|
-
} else {
|
|
34468
|
-
const searchTextPrevious = searchTextRef.current;
|
|
34469
|
-
searchTextRef.current = searchText;
|
|
34470
|
-
if (!searchTextPrevious && searchText) {
|
|
34471
|
-
searchTextBecomesActive = true;
|
|
34472
|
-
}
|
|
34473
|
-
}
|
|
34474
|
-
// Scroll to the selected item only the FIRST time the list is presented on screen,
|
|
34475
|
-
// so the user can see what's selected on initial open. On subsequent re-displays
|
|
34476
|
-
// (e.g. reopening a popover containing the list), we intentionally keep the previous
|
|
34477
|
-
// scroll position — it's less disruptive UX to land where the user last was, even
|
|
34478
|
-
// if that means the selected item isn't currently visible.
|
|
34479
|
-
// Skipped when inside a closed <dialog>/<details> (scrollIntoView is a no-op
|
|
34480
|
-
// on hidden elements); re-runs automatically every time the ancestor opens.
|
|
34481
|
-
const hasBeenDisplayedRef = useRef(false);
|
|
34482
|
-
useDisplayedLayoutEffect(ref, (el, openEvent) => {
|
|
34483
|
-
updateCurrentScroll();
|
|
34484
|
-
if (hasBeenDisplayedRef.current) {
|
|
34485
|
-
return;
|
|
34486
|
-
}
|
|
34487
|
-
hasBeenDisplayedRef.current = true;
|
|
34488
|
-
const items = tracker.itemsSignal.peek();
|
|
34489
|
-
const firstSelected = items.find(i => i.selected);
|
|
34490
|
-
if (firstSelected) {
|
|
34491
|
-
scrollToItem(firstSelected, {
|
|
34492
|
-
event: new CustomEvent("navi_displayed", {
|
|
34493
|
-
detail: {
|
|
34494
|
-
originalEvent: openEvent
|
|
34495
|
-
}
|
|
34496
|
-
}),
|
|
34497
|
-
reason: "scroll to selected"
|
|
34498
|
-
});
|
|
34499
|
-
} else {
|
|
34500
|
-
scrollToItem(items[0], {
|
|
34501
|
-
event: new CustomEvent("navi_displayed", {
|
|
34502
|
-
detail: {
|
|
34503
|
-
originalEvent: openEvent
|
|
34504
|
-
}
|
|
34505
|
-
}),
|
|
34506
|
-
reason: "scroll to top (no selected item)"
|
|
34507
|
-
});
|
|
34116
|
+
navigableEls.push(itemEl);
|
|
34508
34117
|
}
|
|
34509
|
-
|
|
34510
|
-
|
|
34511
|
-
//
|
|
34512
|
-
//
|
|
34513
|
-
// We save the first-visible item ID so restoration is item-precise
|
|
34514
|
-
// and survives render-window shifts or item reordering.
|
|
34515
|
-
|
|
34516
|
-
// NOTE POUR LE JOUR OU ON A LE MULTISELECT:
|
|
34517
|
-
// Lorsqu'on selectionne quelque chose pendant une recherche, alors ensuite meme si on clear
|
|
34518
|
-
// on veut pas revenir a la position scroll précédente car on veut garde l'item qu'on a selectionné visible
|
|
34519
|
-
// (pour l'instant pas grave car on travaille pour le mode select qui fermera le dialog au select)
|
|
34520
|
-
const savedScrollRef = useRef(null);
|
|
34521
|
-
const topMatchScoresKeyRef = useRef("");
|
|
34118
|
+
return navigableEls;
|
|
34119
|
+
};
|
|
34120
|
+
// On mount: set the initial current item to the first selected, else the first navigable.
|
|
34121
|
+
// After that, focusin events on the list keep currentIdRef up to date.
|
|
34522
34122
|
useLayoutEffect(() => {
|
|
34523
|
-
const
|
|
34524
|
-
if (
|
|
34525
|
-
return
|
|
34123
|
+
const navigableEls = getNavigableElements();
|
|
34124
|
+
if (navigableEls.length === 0) {
|
|
34125
|
+
return;
|
|
34526
34126
|
}
|
|
34527
|
-
|
|
34528
|
-
|
|
34529
|
-
|
|
34530
|
-
|
|
34531
|
-
|
|
34532
|
-
|
|
34533
|
-
return undefined;
|
|
34127
|
+
let initialEl;
|
|
34128
|
+
for (const el of navigableEls) {
|
|
34129
|
+
const realInput = el.querySelector("[navi-selectable-real-input]");
|
|
34130
|
+
if (realInput && realInput.checked) {
|
|
34131
|
+
initialEl = el;
|
|
34132
|
+
break;
|
|
34534
34133
|
}
|
|
34535
|
-
savedScrollRef.current = null;
|
|
34536
|
-
debugScroll("Restoring scroll to", savedScroll);
|
|
34537
|
-
updateRenderWindow(savedScroll.renderWindow.start, savedScroll.renderWindow.end, "restore scroll window");
|
|
34538
|
-
const raf = requestAnimationFrame(() => {
|
|
34539
|
-
const left = savedScroll.left;
|
|
34540
|
-
const top = savedScroll.top;
|
|
34541
|
-
// use scrollTo to respect eventual css scroll-behavior: smooth;
|
|
34542
|
-
debugScroll(`restore scroll: ${getElementSignature(listScrollContainerEl)}.scrollTo({ left: ${left}, top: ${top} })`);
|
|
34543
|
-
// The reliable way to restore scroll is to use scrollTop because otherwise we will estimate the item to scroll
|
|
34544
|
-
// based on virtual item height which can wrongly restore the scroll.
|
|
34545
|
-
// However we have a contract with outside to inside which item is scrolled
|
|
34546
|
-
// (used by keyboard nav to enable anchoring the item for list item nav with arrow keys)
|
|
34547
|
-
// so we do our best to give that item back
|
|
34548
|
-
const {
|
|
34549
|
-
item
|
|
34550
|
-
} = getScrollInfo({
|
|
34551
|
-
scrollTop: savedScroll.top
|
|
34552
|
-
}, listScrollContainerEl, tracker, virtualItemHeightSignal, renderWindowRef);
|
|
34553
|
-
listScrollContainerEl.scrollTo({
|
|
34554
|
-
left: savedScroll.left,
|
|
34555
|
-
top: savedScroll.top
|
|
34556
|
-
});
|
|
34557
|
-
const listEl = ref.current.querySelector(".navi_list");
|
|
34558
|
-
dispatchPublicCustomEvent(listEl, "navi_scroll", {
|
|
34559
|
-
item,
|
|
34560
|
-
event: new CustomEvent("navi_scroll_restore")
|
|
34561
|
-
});
|
|
34562
|
-
});
|
|
34563
|
-
return () => {
|
|
34564
|
-
cancelAnimationFrame(raf);
|
|
34565
|
-
};
|
|
34566
|
-
}
|
|
34567
|
-
const visibleItems = tracker.visibleItemsSignal.peek();
|
|
34568
|
-
const topItems = visibleItems.slice(0, renderBudget);
|
|
34569
|
-
const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchScore ?? ""}`).join(",");
|
|
34570
|
-
const currentTopMatchScore = topMatchScoresKeyRef.current;
|
|
34571
|
-
if (topMatchScoresKey === currentTopMatchScore) {
|
|
34572
|
-
// no changes in top matches -> no need to scroll
|
|
34573
|
-
return undefined;
|
|
34574
34134
|
}
|
|
34575
|
-
|
|
34576
|
-
|
|
34577
|
-
if (searchTextBecomesActive) {
|
|
34578
|
-
// search just started -> save the currently scrolled item id to restore later
|
|
34579
|
-
const currentScroll = currentScrollRef.current;
|
|
34580
|
-
savedScrollRef.current = currentScroll;
|
|
34581
|
-
debugScroll(`Saving scroll: { top: ${currentScroll.top}, renderWindowStart: ${currentScroll.renderWindow.start}, renderWindowEnd: ${currentScroll.renderWindow.end} }`);
|
|
34135
|
+
if (!initialEl) {
|
|
34136
|
+
initialEl = navigableEls[0];
|
|
34582
34137
|
}
|
|
34583
|
-
|
|
34584
|
-
|
|
34585
|
-
|
|
34586
|
-
|
|
34587
|
-
|
|
34588
|
-
|
|
34138
|
+
setCurrentId(initialEl.id);
|
|
34139
|
+
}, []);
|
|
34140
|
+
const listVnode = jsx(Next, {
|
|
34141
|
+
as: "fieldset",
|
|
34142
|
+
"navi-has-selected-background": selectedIndicator === "backgroundColor" ? "" : undefined,
|
|
34143
|
+
...listControlProps,
|
|
34144
|
+
...remainingProps,
|
|
34145
|
+
name: undefined,
|
|
34146
|
+
selectedIndicator: undefined,
|
|
34147
|
+
multiple: undefined
|
|
34148
|
+
// Track focus inside the list: whichever item gets focus becomes current.
|
|
34149
|
+
,
|
|
34589
34150
|
|
|
34590
|
-
|
|
34591
|
-
|
|
34592
|
-
|
|
34593
|
-
if (!listContainerEl) {
|
|
34594
|
-
return undefined;
|
|
34595
|
-
}
|
|
34596
|
-
const listScrollContainerEl = listContainerEl.querySelector(`.navi_list_scroll_container`);
|
|
34597
|
-
const listEl = listContainerEl.querySelector(".navi_list");
|
|
34598
|
-
const onScroll = () => {
|
|
34599
|
-
updateCurrentScroll();
|
|
34600
|
-
const visibleItemCount = tracker.visibleCountSignal.peek();
|
|
34601
|
-
if (visibleItemCount <= renderBudget) {
|
|
34151
|
+
onFocusIn: e => {
|
|
34152
|
+
const realInput = e.target.closest("[navi-selectable-real-input]");
|
|
34153
|
+
if (!realInput) {
|
|
34602
34154
|
return;
|
|
34603
34155
|
}
|
|
34604
|
-
const
|
|
34605
|
-
if (
|
|
34606
|
-
|
|
34156
|
+
const itemEl = realInput.closest("[navi-list-item-real]");
|
|
34157
|
+
if (itemEl && itemEl.id) {
|
|
34158
|
+
setCurrentId(itemEl.id, e);
|
|
34607
34159
|
}
|
|
34608
|
-
|
|
34609
|
-
|
|
34610
|
-
|
|
34611
|
-
|
|
34612
|
-
|
|
34160
|
+
},
|
|
34161
|
+
onnavi_request_select: e => {
|
|
34162
|
+
const {
|
|
34163
|
+
id
|
|
34164
|
+
} = e.detail;
|
|
34165
|
+
if (id === undefined) {
|
|
34166
|
+
return;
|
|
34167
|
+
}
|
|
34168
|
+
const inputId = `${id}_input`;
|
|
34169
|
+
const childController = uiGroupStateController.findChildById(inputId);
|
|
34170
|
+
if (!childController) {
|
|
34171
|
+
return;
|
|
34172
|
+
}
|
|
34173
|
+
const list = ref.current;
|
|
34174
|
+
const allowed = dispatchRequestInteraction(list, e, "select");
|
|
34175
|
+
if (!allowed) {
|
|
34176
|
+
e.preventDefault();
|
|
34613
34177
|
return;
|
|
34614
34178
|
}
|
|
34179
|
+
if (childController.setUIState(true, e)) {
|
|
34180
|
+
dispatchRequestAction(list, {
|
|
34181
|
+
event: e
|
|
34182
|
+
});
|
|
34183
|
+
}
|
|
34184
|
+
},
|
|
34185
|
+
onnavi_request_unselect: e => {
|
|
34615
34186
|
const {
|
|
34616
|
-
|
|
34617
|
-
|
|
34618
|
-
|
|
34619
|
-
|
|
34620
|
-
const half = Math.floor(renderBudget / 2);
|
|
34621
|
-
let newStart = Math.max(0, index - half);
|
|
34622
|
-
let newEnd = Math.min(visibleItemCount, newStart + renderBudget);
|
|
34623
|
-
if (newEnd === visibleItemCount) {
|
|
34624
|
-
newStart = Math.max(0, visibleItemCount - renderBudget);
|
|
34187
|
+
id
|
|
34188
|
+
} = e.detail;
|
|
34189
|
+
if (id === undefined) {
|
|
34190
|
+
return;
|
|
34625
34191
|
}
|
|
34626
|
-
|
|
34627
|
-
|
|
34628
|
-
|
|
34629
|
-
|
|
34630
|
-
|
|
34631
|
-
|
|
34632
|
-
|
|
34633
|
-
|
|
34634
|
-
|
|
34635
|
-
|
|
34636
|
-
|
|
34637
|
-
|
|
34638
|
-
|
|
34639
|
-
|
|
34640
|
-
|
|
34641
|
-
}
|
|
34642
|
-
|
|
34643
|
-
|
|
34644
|
-
|
|
34645
|
-
|
|
34646
|
-
|
|
34647
|
-
|
|
34648
|
-
|
|
34649
|
-
|
|
34650
|
-
|
|
34651
|
-
|
|
34652
|
-
|
|
34653
|
-
|
|
34654
|
-
|
|
34655
|
-
|
|
34656
|
-
|
|
34657
|
-
|
|
34658
|
-
|
|
34659
|
-
|
|
34660
|
-
|
|
34661
|
-
|
|
34662
|
-
|
|
34663
|
-
|
|
34664
|
-
|
|
34665
|
-
|
|
34666
|
-
|
|
34667
|
-
|
|
34668
|
-
|
|
34669
|
-
|
|
34670
|
-
|
|
34671
|
-
|
|
34672
|
-
|
|
34673
|
-
|
|
34674
|
-
|
|
34675
|
-
|
|
34676
|
-
|
|
34677
|
-
|
|
34678
|
-
|
|
34679
|
-
|
|
34680
|
-
|
|
34681
|
-
|
|
34682
|
-
|
|
34683
|
-
|
|
34684
|
-
|
|
34685
|
-
|
|
34686
|
-
|
|
34687
|
-
|
|
34688
|
-
|
|
34689
|
-
|
|
34690
|
-
|
|
34691
|
-
|
|
34692
|
-
|
|
34693
|
-
|
|
34694
|
-
|
|
34695
|
-
|
|
34696
|
-
|
|
34697
|
-
|
|
34698
|
-
|
|
34699
|
-
|
|
34700
|
-
|
|
34701
|
-
|
|
34702
|
-
|
|
34703
|
-
|
|
34704
|
-
|
|
34705
|
-
|
|
34706
|
-
|
|
34707
|
-
|
|
34708
|
-
|
|
34709
|
-
|
|
34192
|
+
const inputId = `${id}_input`;
|
|
34193
|
+
const childController = uiGroupStateController.findChildById(inputId);
|
|
34194
|
+
if (!childController) {
|
|
34195
|
+
return;
|
|
34196
|
+
}
|
|
34197
|
+
const list = ref.current;
|
|
34198
|
+
const allowed = dispatchRequestInteraction(list, e, "unselect");
|
|
34199
|
+
if (!allowed) {
|
|
34200
|
+
e.preventDefault();
|
|
34201
|
+
return;
|
|
34202
|
+
}
|
|
34203
|
+
if (childController.setUIState(false, e)) {
|
|
34204
|
+
dispatchRequestAction(list, {
|
|
34205
|
+
event: e
|
|
34206
|
+
});
|
|
34207
|
+
}
|
|
34208
|
+
},
|
|
34209
|
+
onnavi_request_nav: e => {
|
|
34210
|
+
const {
|
|
34211
|
+
goal
|
|
34212
|
+
} = e.detail;
|
|
34213
|
+
const navigableEls = getNavigableElements();
|
|
34214
|
+
if (navigableEls.length === 0) {
|
|
34215
|
+
return;
|
|
34216
|
+
}
|
|
34217
|
+
const currentId = currentIdRef.current;
|
|
34218
|
+
let currentIndex = -1;
|
|
34219
|
+
if (currentId) {
|
|
34220
|
+
currentIndex = navigableEls.findIndex(el => el.id === currentId);
|
|
34221
|
+
}
|
|
34222
|
+
let targetEl;
|
|
34223
|
+
if (goal === "first") {
|
|
34224
|
+
targetEl = navigableEls[0];
|
|
34225
|
+
} else if (goal === "last") {
|
|
34226
|
+
targetEl = navigableEls[navigableEls.length - 1];
|
|
34227
|
+
} else if (goal === "down") {
|
|
34228
|
+
if (currentIndex === -1) {
|
|
34229
|
+
targetEl = navigableEls[0];
|
|
34230
|
+
} else if (currentIndex < navigableEls.length - 1) {
|
|
34231
|
+
targetEl = navigableEls[currentIndex + 1];
|
|
34232
|
+
} else {
|
|
34233
|
+
targetEl = navigableEls[navigableEls.length - 1];
|
|
34234
|
+
}
|
|
34235
|
+
} else if (goal === "up") {
|
|
34236
|
+
if (currentIndex === -1) {
|
|
34237
|
+
targetEl = navigableEls[0];
|
|
34238
|
+
} else if (currentIndex > 0) {
|
|
34239
|
+
targetEl = navigableEls[currentIndex - 1];
|
|
34240
|
+
} else {
|
|
34241
|
+
targetEl = navigableEls[0];
|
|
34242
|
+
}
|
|
34243
|
+
}
|
|
34244
|
+
if (!targetEl) {
|
|
34245
|
+
return;
|
|
34246
|
+
}
|
|
34247
|
+
setCurrentId(targetEl.id, e);
|
|
34248
|
+
dispatchCustomEvent(ref.current, "navi_request_scroll", {
|
|
34249
|
+
event: e,
|
|
34250
|
+
id: targetEl.id
|
|
34251
|
+
});
|
|
34252
|
+
},
|
|
34253
|
+
onnavi_request_activate: e => {
|
|
34254
|
+
const currentId = currentIdRef.current;
|
|
34255
|
+
if (!currentId) {
|
|
34256
|
+
return;
|
|
34257
|
+
}
|
|
34258
|
+
if (multiple) {
|
|
34259
|
+
const inputId = `${currentId}_input`;
|
|
34260
|
+
const childController = uiGroupStateController.findChildById(inputId);
|
|
34261
|
+
const isSelected = childController && childController.uiState;
|
|
34262
|
+
dispatchCustomEvent(ref.current, isSelected ? "navi_request_unselect" : "navi_request_select", {
|
|
34263
|
+
event: e,
|
|
34264
|
+
id: currentId
|
|
34265
|
+
});
|
|
34266
|
+
return;
|
|
34267
|
+
}
|
|
34268
|
+
dispatchCustomEvent(ref.current, "navi_request_select", {
|
|
34269
|
+
event: e,
|
|
34270
|
+
id: currentId
|
|
34271
|
+
});
|
|
34272
|
+
},
|
|
34273
|
+
children: jsx(ControlgroupChildrenWrapper, {
|
|
34274
|
+
...childrenWrapperProps,
|
|
34275
|
+
children: props.children
|
|
34276
|
+
})
|
|
34277
|
+
});
|
|
34278
|
+
return jsx(SelectableListMultipleContext.Provider, {
|
|
34279
|
+
value: multiple,
|
|
34280
|
+
children: listVnode
|
|
34281
|
+
});
|
|
34710
34282
|
};
|
|
34711
|
-
const
|
|
34712
|
-
|
|
34713
|
-
|
|
34714
|
-
|
|
34715
|
-
|
|
34716
|
-
|
|
34717
|
-
|
|
34718
|
-
if (virtualItemHeightProp && virtualHeightSignal.peek() !== virtualItemHeightProp) {
|
|
34719
|
-
virtualHeightSignal.value = virtualItemHeightProp;
|
|
34283
|
+
const SelectableRealInputContext = createContext(null);
|
|
34284
|
+
const ListItemSelectableResolver = props => {
|
|
34285
|
+
const Next = useNextResolver();
|
|
34286
|
+
if (props.selectable) {
|
|
34287
|
+
return jsx(ListItemSelectable, {
|
|
34288
|
+
...props
|
|
34289
|
+
});
|
|
34720
34290
|
}
|
|
34721
|
-
|
|
34722
|
-
|
|
34723
|
-
return;
|
|
34724
|
-
}
|
|
34725
|
-
const listEl = ref.current?.querySelector(".navi_list");
|
|
34726
|
-
if (!listEl) {
|
|
34727
|
-
return;
|
|
34728
|
-
}
|
|
34729
|
-
const firstListItem = listEl.querySelector(REAL_LIST_ITEM_SELECTOR);
|
|
34730
|
-
if (!firstListItem) {
|
|
34731
|
-
return;
|
|
34732
|
-
}
|
|
34733
|
-
const measuredHeight = firstListItem.getBoundingClientRect().height;
|
|
34734
|
-
virtualHeightSignal.value = measuredHeight;
|
|
34291
|
+
return jsx(Next, {
|
|
34292
|
+
...props
|
|
34735
34293
|
});
|
|
34736
|
-
return virtualHeightSignal;
|
|
34737
34294
|
};
|
|
34738
|
-
|
|
34739
|
-
|
|
34740
|
-
|
|
34741
|
-
|
|
34742
|
-
|
|
34743
|
-
|
|
34744
|
-
|
|
34745
|
-
|
|
34746
|
-
|
|
34747
|
-
|
|
34748
|
-
|
|
34749
|
-
|
|
34750
|
-
|
|
34751
|
-
|
|
34752
|
-
|
|
34753
|
-
|
|
34754
|
-
|
|
34755
|
-
|
|
34756
|
-
|
|
34757
|
-
|
|
34295
|
+
const ListItemSelectable = props => {
|
|
34296
|
+
const Next = useNextResolver();
|
|
34297
|
+
const {
|
|
34298
|
+
index,
|
|
34299
|
+
id,
|
|
34300
|
+
highlight,
|
|
34301
|
+
hidden,
|
|
34302
|
+
filtered,
|
|
34303
|
+
matchScore,
|
|
34304
|
+
defaultSelected,
|
|
34305
|
+
selected,
|
|
34306
|
+
pointed,
|
|
34307
|
+
selectableArea = "all",
|
|
34308
|
+
...rest
|
|
34309
|
+
} = props;
|
|
34310
|
+
const multiple = useContext(SelectableListMultipleContext);
|
|
34311
|
+
const inputRef = useRef();
|
|
34312
|
+
const inputType = multiple ? "checkbox" : "radio";
|
|
34313
|
+
const inputId = `${id}_input`;
|
|
34314
|
+
inputRef.nullCanHappen = true; // virtualization
|
|
34315
|
+
const [checkableProps, remainingProps, ChildrenContextWrapper] = useCheckableProps({
|
|
34316
|
+
readOnlyMessage: naviI18n(`constraint.readonly.option`, props),
|
|
34758
34317
|
...rest,
|
|
34759
|
-
|
|
34760
|
-
|
|
34761
|
-
|
|
34762
|
-
|
|
34763
|
-
|
|
34764
|
-
|
|
34765
|
-
|
|
34766
|
-
|
|
34767
|
-
|
|
34768
|
-
|
|
34769
|
-
|
|
34770
|
-
|
|
34771
|
-
|
|
34772
|
-
|
|
34773
|
-
|
|
34774
|
-
|
|
34775
|
-
|
|
34776
|
-
|
|
34777
|
-
|
|
34318
|
+
ref: inputRef,
|
|
34319
|
+
id: inputId,
|
|
34320
|
+
type: inputType,
|
|
34321
|
+
defaultChecked: defaultSelected,
|
|
34322
|
+
checked: selected,
|
|
34323
|
+
action: (v, {
|
|
34324
|
+
event
|
|
34325
|
+
}) => {
|
|
34326
|
+
const listContainerEl = event.currentTarget.closest(".navi_list_container");
|
|
34327
|
+
dispatchRequestAction(listContainerEl, {
|
|
34328
|
+
event
|
|
34329
|
+
});
|
|
34330
|
+
}
|
|
34331
|
+
});
|
|
34332
|
+
const {
|
|
34333
|
+
checked,
|
|
34334
|
+
value,
|
|
34335
|
+
basePseudoState,
|
|
34336
|
+
children
|
|
34337
|
+
} = checkableProps;
|
|
34338
|
+
const readOnly = basePseudoState[":read-only"];
|
|
34339
|
+
// const disabled = basePseudoState[":disabled"];
|
|
34340
|
+
// const loading = basePseudoState[":-navi-loading"];
|
|
34341
|
+
const realInputContextValue = useMemo(() => {
|
|
34342
|
+
return {
|
|
34343
|
+
id: inputId,
|
|
34344
|
+
type: inputType,
|
|
34345
|
+
checked,
|
|
34346
|
+
readOnly,
|
|
34347
|
+
value
|
|
34348
|
+
};
|
|
34349
|
+
}, [inputId, inputType, checked, readOnly, value]);
|
|
34350
|
+
return jsxs(Next, {
|
|
34351
|
+
id: id,
|
|
34352
|
+
index: index,
|
|
34353
|
+
highlight: highlight,
|
|
34354
|
+
filtered: filtered,
|
|
34355
|
+
hidden: hidden,
|
|
34356
|
+
matchScore: matchScore,
|
|
34357
|
+
"aria-selected": checked,
|
|
34358
|
+
selected: checked,
|
|
34359
|
+
"navi-selectable": "",
|
|
34360
|
+
padding: "m",
|
|
34361
|
+
spacing: "s",
|
|
34362
|
+
flex: true,
|
|
34363
|
+
alignY: "center",
|
|
34364
|
+
...remainingProps,
|
|
34365
|
+
pseudoClasses: SELECTABLE_PSEUDO_CLASSES,
|
|
34366
|
+
basePseudoState: {
|
|
34367
|
+
":-navi-selected": checked,
|
|
34368
|
+
":-navi-pointed": pointed,
|
|
34369
|
+
...basePseudoState
|
|
34370
|
+
},
|
|
34371
|
+
ref: props.ref,
|
|
34372
|
+
selectable: undefined,
|
|
34373
|
+
"navi-selectable-area-all": selectableArea === "all" ? "" : undefined,
|
|
34374
|
+
children: [jsx(SelectableRealInput, {
|
|
34375
|
+
...checkableProps,
|
|
34376
|
+
// eslint-disable-next-line react/no-children-prop
|
|
34377
|
+
children: undefined
|
|
34378
|
+
}), jsx(SelectableRealInputContext.Provider, {
|
|
34379
|
+
value: realInputContextValue,
|
|
34380
|
+
children: jsx(ChildrenContextWrapper, {
|
|
34381
|
+
children: children
|
|
34778
34382
|
})
|
|
34779
|
-
}), jsx(BottomFiller, {
|
|
34780
|
-
virtualItemHeightSignal: virtualItemHeightSignal,
|
|
34781
|
-
renderWindowEnd: renderWindow.end,
|
|
34782
|
-
tracker: tracker
|
|
34783
34383
|
})]
|
|
34784
34384
|
});
|
|
34785
34385
|
};
|
|
34786
|
-
const
|
|
34787
|
-
|
|
34788
|
-
|
|
34789
|
-
|
|
34790
|
-
|
|
34791
|
-
|
|
34792
|
-
|
|
34793
|
-
|
|
34794
|
-
|
|
34795
|
-
|
|
34796
|
-
|
|
34797
|
-
|
|
34798
|
-
|
|
34799
|
-
if (noMatchFallback === undefined) {
|
|
34800
|
-
noMatchFallback = allHidden ? naviI18n("list.no_match") : naviI18n("list.no_match_rest_shown");
|
|
34801
|
-
}
|
|
34802
|
-
if (!showMatchFallback) {
|
|
34803
|
-
return null;
|
|
34804
|
-
}
|
|
34805
|
-
return jsx(ListItem, {
|
|
34806
|
-
role: "presentation",
|
|
34807
|
-
className: "navi_list_item navi_list_no_match_fallback",
|
|
34808
|
-
hidden: !showMatchFallback,
|
|
34809
|
-
"navi-default": typeof noMatchFallback === "string" ? "" : undefined,
|
|
34810
|
-
children: noMatchFallback
|
|
34811
|
-
});
|
|
34812
|
-
};
|
|
34813
|
-
const Fallback = ({
|
|
34814
|
-
tracker,
|
|
34815
|
-
fallback
|
|
34816
|
-
}) => {
|
|
34817
|
-
const itemCount = tracker.countSignal.value;
|
|
34818
|
-
const showFallback = itemCount === 0;
|
|
34819
|
-
if (fallback === undefined) {
|
|
34820
|
-
fallback = naviI18n("list.empty");
|
|
34821
|
-
}
|
|
34822
|
-
if (!showFallback) {
|
|
34823
|
-
return null;
|
|
34824
|
-
}
|
|
34825
|
-
return jsx(ListItem, {
|
|
34826
|
-
role: "presentation",
|
|
34827
|
-
className: "navi_list_item navi_list_fallback",
|
|
34828
|
-
hidden: !showFallback,
|
|
34829
|
-
"navi-default": typeof fallback === "string" ? "" : undefined,
|
|
34830
|
-
children: fallback
|
|
34386
|
+
const SELECTABLE_PSEUDO_CLASSES = [":hover", ":disabled", ":read-only", ":focus-within", ":focus", ":focus-visible", ":-navi-loading", ":-navi-pointed", ":-navi-selected", ":disabled", ":read-only"];
|
|
34387
|
+
const SelectableRealInput = props => {
|
|
34388
|
+
// here for some reason we can't use <Input, so instead we use <Box
|
|
34389
|
+
// ideally we could use <Input but it would interfere with the control props we already create
|
|
34390
|
+
// in the ListItemSelectable
|
|
34391
|
+
return jsx(Box, {
|
|
34392
|
+
as: "input",
|
|
34393
|
+
pseudoClasses: SELECTABLE_INPUT_PSEUDO_CLASSES,
|
|
34394
|
+
...props,
|
|
34395
|
+
"navi-visually-hidden": "",
|
|
34396
|
+
"navi-selectable-real-input": "",
|
|
34397
|
+
"data-callout-arrow-x": "center"
|
|
34398
|
+
// navi-debug
|
|
34831
34399
|
});
|
|
34832
34400
|
};
|
|
34833
|
-
const
|
|
34834
|
-
|
|
34835
|
-
|
|
34836
|
-
|
|
34837
|
-
|
|
34838
|
-
const numberOfItemsAbove = renderWindowStart;
|
|
34839
|
-
const heightToFillAbove = numberOfItemsAbove * virtualItemHeight;
|
|
34840
|
-
if (!heightToFillAbove) {
|
|
34841
|
-
return null;
|
|
34401
|
+
const SELECTABLE_INPUT_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":checked"];
|
|
34402
|
+
const SelectableInputProxy = props => {
|
|
34403
|
+
const selectableRealInputProps = useContext(SelectableRealInputContext);
|
|
34404
|
+
if (!selectableRealInputProps) {
|
|
34405
|
+
throw new Error("Selectable.Input must be used within a Selectable component");
|
|
34842
34406
|
}
|
|
34843
|
-
return jsx("li", {
|
|
34844
|
-
className: "navi_list_virtual_filler"
|
|
34845
|
-
// eslint-disable-next-line react/no-unknown-property
|
|
34846
|
-
,
|
|
34847
34407
|
|
|
34848
|
-
|
|
34849
|
-
|
|
34850
|
-
|
|
34851
|
-
|
|
34852
|
-
|
|
34853
|
-
|
|
34854
|
-
|
|
34855
|
-
|
|
34856
|
-
|
|
34857
|
-
|
|
34858
|
-
|
|
34859
|
-
|
|
34860
|
-
const visibleItemCount = tracker.visibleCountSignal.value;
|
|
34861
|
-
const virtualItemHeight = virtualItemHeightSignal.value;
|
|
34862
|
-
const numberOfItemsBelow = Math.max(visibleItemCount - renderWindowEnd, 0);
|
|
34863
|
-
const heightToFillBelow = numberOfItemsBelow * virtualItemHeight;
|
|
34864
|
-
if (!heightToFillBelow) {
|
|
34865
|
-
return null;
|
|
34866
|
-
}
|
|
34867
|
-
return jsx("li", {
|
|
34868
|
-
className: "navi_list_virtual_filler"
|
|
34869
|
-
// eslint-disable-next-line react/no-unknown-property
|
|
34870
|
-
,
|
|
34408
|
+
// Reset FieldToInterfaceContext to ensure we don't read id or report our
|
|
34409
|
+
// states (real input should take id and report)
|
|
34410
|
+
return jsx(ControlToInterfaceContext.Provider, {
|
|
34411
|
+
value: undefined,
|
|
34412
|
+
children: jsx(Input, {
|
|
34413
|
+
...props,
|
|
34414
|
+
...selectableRealInputProps,
|
|
34415
|
+
id: undefined,
|
|
34416
|
+
"navi-control-proxy-for": selectableRealInputProps.id
|
|
34417
|
+
// give it a specific name to avoid radio name (would unselect others)
|
|
34418
|
+
// (making it unique to the list would be enough, but here it's even more unique)
|
|
34419
|
+
,
|
|
34871
34420
|
|
|
34872
|
-
|
|
34873
|
-
|
|
34874
|
-
|
|
34875
|
-
|
|
34876
|
-
}
|
|
34421
|
+
name: `${selectableRealInputProps.id}_proxy`,
|
|
34422
|
+
"aria-hidden": "true",
|
|
34423
|
+
tabIndex: -1
|
|
34424
|
+
})
|
|
34877
34425
|
});
|
|
34878
34426
|
};
|
|
34427
|
+
const SelectableInput = SelectableInputProxy;
|
|
34879
34428
|
|
|
34880
34429
|
/**
|
|
34881
|
-
*
|
|
34430
|
+
* CSS Highlight API integration for navi-search-match.
|
|
34882
34431
|
*
|
|
34883
|
-
*
|
|
34884
|
-
*
|
|
34885
|
-
* - Early return when outside the render window
|
|
34886
|
-
* - Separator rendering between visible items
|
|
34432
|
+
* Provides a shared `Highlight` instance registered as "navi-search-match"
|
|
34433
|
+
* and a `useSearchHighlight` hook that marks matching text ranges on an element.
|
|
34887
34434
|
*
|
|
34888
|
-
*
|
|
34889
|
-
*
|
|
34890
|
-
*
|
|
34891
|
-
*
|
|
34892
|
-
*
|
|
34893
|
-
*
|
|
34894
|
-
*
|
|
34435
|
+
* The highlight is not specific to ListItem — any element that wants to mark
|
|
34436
|
+
* search match ranges (e.g. Suggestion, custom search results) can use these.
|
|
34437
|
+
*
|
|
34438
|
+
* CSS to paint the highlight:
|
|
34439
|
+
* ::highlight(navi-search-match) {
|
|
34440
|
+
* color: var(--search-match-color);
|
|
34441
|
+
* background-color: var(--search-match-background-color);
|
|
34442
|
+
* }
|
|
34443
|
+
*
|
|
34444
|
+
* The `highlight` prop can be:
|
|
34445
|
+
* - an array of [start, end] pairs — applied to all text nodes under the root element
|
|
34446
|
+
* - an object { [domSelector]: [[start, end], …] } — applied to each sub-element
|
|
34447
|
+
* matched by the selector (the format produced by createSearch)
|
|
34895
34448
|
*/
|
|
34896
|
-
|
|
34897
|
-
|
|
34898
|
-
|
|
34899
|
-
|
|
34900
|
-
|
|
34449
|
+
|
|
34450
|
+
// Module-level shared Highlight instance — created lazily once.
|
|
34451
|
+
let naviSearchHighlight = null;
|
|
34452
|
+
const getNaviSearchHighlight = () => {
|
|
34453
|
+
if (!CSS.highlights) {
|
|
34454
|
+
return null;
|
|
34901
34455
|
}
|
|
34902
|
-
|
|
34903
|
-
|
|
34904
|
-
|
|
34456
|
+
if (!naviSearchHighlight) {
|
|
34457
|
+
naviSearchHighlight = new Highlight();
|
|
34458
|
+
CSS.highlights.set("navi-search-match", naviSearchHighlight);
|
|
34459
|
+
}
|
|
34460
|
+
return naviSearchHighlight;
|
|
34905
34461
|
};
|
|
34906
|
-
|
|
34907
|
-
|
|
34908
|
-
|
|
34909
|
-
|
|
34910
|
-
|
|
34462
|
+
|
|
34463
|
+
/**
|
|
34464
|
+
* useSearchHighlight — registers/unregisters CSS Highlight API ranges for an element.
|
|
34465
|
+
*
|
|
34466
|
+
* @param {import("preact").RefObject} ref - ref to the root element
|
|
34467
|
+
* @param {Array|Object|null|undefined} highlight - ranges to highlight
|
|
34468
|
+
* @param {Array} deps - additional dependencies that should retrigger the effect
|
|
34469
|
+
* (typically [children, hidden] from the consuming component)
|
|
34470
|
+
*/
|
|
34471
|
+
const useSearchHighlight = (ref, highlight, deps = []) => {
|
|
34472
|
+
useLayoutEffect(() => {
|
|
34473
|
+
const el = ref.current;
|
|
34474
|
+
if (!el || !highlight) {
|
|
34475
|
+
return undefined;
|
|
34476
|
+
}
|
|
34477
|
+
return applySearchHighlight(el, highlight);
|
|
34478
|
+
}, [highlight, ...deps]);
|
|
34911
34479
|
};
|
|
34912
|
-
|
|
34913
|
-
|
|
34914
|
-
|
|
34480
|
+
|
|
34481
|
+
const applySearchHighlight = (el, highlight) => {
|
|
34482
|
+
const hl = getNaviSearchHighlight();
|
|
34483
|
+
if (!hl) {
|
|
34484
|
+
return undefined;
|
|
34915
34485
|
}
|
|
34916
|
-
|
|
34917
|
-
|
|
34486
|
+
|
|
34487
|
+
// Normalise highlight to { root, ranges }[] entries.
|
|
34488
|
+
// Flat array → single entry scoped to the whole element.
|
|
34489
|
+
const entries = Array.isArray(highlight)
|
|
34490
|
+
? highlight.length === 0
|
|
34491
|
+
? []
|
|
34492
|
+
: [{ root: el, ranges: highlight }]
|
|
34493
|
+
: Object.entries(highlight).map(([selector, ranges]) => ({
|
|
34494
|
+
root: el.querySelector(selector) ?? el,
|
|
34495
|
+
ranges,
|
|
34496
|
+
}));
|
|
34497
|
+
|
|
34498
|
+
if (entries.length === 0) {
|
|
34499
|
+
return undefined;
|
|
34918
34500
|
}
|
|
34919
|
-
|
|
34920
|
-
|
|
34921
|
-
const
|
|
34922
|
-
|
|
34923
|
-
|
|
34924
|
-
|
|
34925
|
-
|
|
34926
|
-
|
|
34927
|
-
|
|
34928
|
-
|
|
34929
|
-
|
|
34930
|
-
|
|
34931
|
-
|
|
34932
|
-
|
|
34933
|
-
|
|
34934
|
-
|
|
34501
|
+
|
|
34502
|
+
const ownRanges = [];
|
|
34503
|
+
for (const { root, ranges } of entries) {
|
|
34504
|
+
// Collect text nodes under root with cumulative character offsets so that
|
|
34505
|
+
// [start, end] ranges (positions in the field string) map directly to
|
|
34506
|
+
// the correct DOM text node positions without re-searching.
|
|
34507
|
+
const textNodes = [];
|
|
34508
|
+
let totalLength = 0;
|
|
34509
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
34510
|
+
acceptNode: (node) => {
|
|
34511
|
+
// Skip text nodes inside aria-hidden elements (icons, decorative content, etc.)
|
|
34512
|
+
let parent = node.parentElement;
|
|
34513
|
+
while (parent && parent !== root) {
|
|
34514
|
+
if (parent.getAttribute("aria-hidden") === "true") {
|
|
34515
|
+
return NodeFilter.FILTER_REJECT;
|
|
34516
|
+
}
|
|
34517
|
+
parent = parent.parentElement;
|
|
34518
|
+
}
|
|
34519
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
34520
|
+
},
|
|
34935
34521
|
});
|
|
34936
|
-
|
|
34937
|
-
|
|
34938
|
-
|
|
34939
|
-
|
|
34940
|
-
if (visibleIndex < renderWindow.start || visibleIndex >= renderWindow.end) {
|
|
34941
|
-
return jsx(ListItemVoid, {});
|
|
34942
|
-
}
|
|
34943
|
-
const listItemVnode = jsx(ListItemReal, {
|
|
34944
|
-
...props
|
|
34945
|
-
});
|
|
34946
|
-
// For separator decision, we need to know "am I the first visible item?".
|
|
34947
|
-
// We deliberately do NOT use tracker's visibleIndex here because, during a
|
|
34948
|
-
// reorder render pass (e.g. items resorted by search score), other items
|
|
34949
|
-
// still have stale keyToExplicitOrder values — the binary search reads
|
|
34950
|
-
// those stale values and computes wrong indices. The result is that no
|
|
34951
|
-
// item gets visibleIndex === 0 and a spurious <hr> appears at the top.
|
|
34952
|
-
//
|
|
34953
|
-
// Instead we use the parent-provided index, which is race-free:
|
|
34954
|
-
// - global list: props.index === 0 means "first by explicit order"
|
|
34955
|
-
// (parent passes sequential indices starting at 0; filtered items
|
|
34956
|
-
// are already pushed to the end by useSearchText)
|
|
34957
|
-
// - inside a group: each group has its own item tracker and group
|
|
34958
|
-
// items don't reorder, so groupVisibleIndex is reliable
|
|
34959
|
-
const isFirstInList = groupVisibleIndex === null ? props.index === 0 : groupVisibleIndex === 0;
|
|
34960
|
-
if (!separator || isFirstInList) {
|
|
34961
|
-
return listItemVnode;
|
|
34962
|
-
}
|
|
34963
|
-
// separatorIndex is only used as the function-form argument (gap index)
|
|
34964
|
-
const separatorIndex = groupVisibleIndex === null ? visibleIndex : groupVisibleIndex;
|
|
34965
|
-
const separatorVnode = typeof separator === "function" ? separator(separatorIndex - 1) : separator;
|
|
34966
|
-
return jsxs(Fragment$1, {
|
|
34967
|
-
children: [separatorVnode, listItemVnode]
|
|
34968
|
-
});
|
|
34969
|
-
};
|
|
34970
|
-
// When an item is outside the render window it cannot render a DOM node.
|
|
34971
|
-
// If it wants to scroll into view it sets scrollTop so the scroll event
|
|
34972
|
-
// shifts the window; once the item mounts as ListItemReal its layout effect
|
|
34973
|
-
// calls scrollIntoViewWithStickyAwareness to fine-tune the position.
|
|
34974
|
-
const ListItemVoid = () => {
|
|
34975
|
-
return null;
|
|
34976
|
-
};
|
|
34977
|
-
const ListItemReal = props => {
|
|
34978
|
-
const defaultRef = useRef(null);
|
|
34979
|
-
props.ref = props.ref || defaultRef;
|
|
34980
|
-
const {
|
|
34981
|
-
ref,
|
|
34982
|
-
id,
|
|
34983
|
-
hidden,
|
|
34984
|
-
highlight,
|
|
34985
|
-
children,
|
|
34986
|
-
...rest
|
|
34987
|
-
} = props;
|
|
34988
|
-
const pendingScrollRef = useContext(PendingScrollRefContext);
|
|
34989
|
-
const pendingScroll = pendingScrollRef.current;
|
|
34990
|
-
const needScrollOnMount = pendingScroll && pendingScroll.id === id;
|
|
34991
|
-
useLayoutEffect(() => {
|
|
34992
|
-
if (!needScrollOnMount) {
|
|
34993
|
-
return;
|
|
34522
|
+
let node;
|
|
34523
|
+
while ((node = walker.nextNode())) {
|
|
34524
|
+
textNodes.push({ node, offset: totalLength });
|
|
34525
|
+
totalLength += node.textContent.length;
|
|
34994
34526
|
}
|
|
34995
|
-
const
|
|
34996
|
-
|
|
34997
|
-
|
|
34527
|
+
for (const [start, end] of ranges) {
|
|
34528
|
+
for (const { node: textNode, offset: nodeOffset } of textNodes) {
|
|
34529
|
+
const nodeEnd = nodeOffset + textNode.textContent.length;
|
|
34530
|
+
if (nodeEnd <= start || nodeOffset >= end) {
|
|
34531
|
+
continue;
|
|
34532
|
+
}
|
|
34533
|
+
const rangeStart = start - nodeOffset;
|
|
34534
|
+
const rangeEnd = end - nodeOffset;
|
|
34535
|
+
const range = new Range();
|
|
34536
|
+
range.setStart(textNode, rangeStart < 0 ? 0 : rangeStart);
|
|
34537
|
+
range.setEnd(
|
|
34538
|
+
textNode,
|
|
34539
|
+
rangeEnd > textNode.textContent.length
|
|
34540
|
+
? textNode.textContent.length
|
|
34541
|
+
: rangeEnd,
|
|
34542
|
+
);
|
|
34543
|
+
hl.add(range);
|
|
34544
|
+
ownRanges.push(range);
|
|
34545
|
+
}
|
|
34998
34546
|
}
|
|
34999
|
-
pendingScroll.resolve(itemEl);
|
|
35000
|
-
}, [needScrollOnMount]);
|
|
35001
|
-
|
|
35002
|
-
// CSS Highlight API: mark matching text ranges when highlight prop is set.
|
|
35003
|
-
useSearchHighlight(ref, highlight, [children, hidden]);
|
|
35004
|
-
return jsx(Box, {
|
|
35005
|
-
as: "li",
|
|
35006
|
-
baseClassName: "navi_list_item",
|
|
35007
|
-
styleCSSVars: LIST_ITEM_STYLE_CSS_VARS,
|
|
35008
|
-
pseudoClasses: LIST_ITEM_PSEUDO_CLASSES,
|
|
35009
|
-
pseudoElements: LIST_ITEM_PSEUDO_ELEMENTS,
|
|
35010
|
-
id: id,
|
|
35011
|
-
"navi-list-item-real": "",
|
|
35012
|
-
...rest,
|
|
35013
|
-
index: undefined,
|
|
35014
|
-
selected: undefined,
|
|
35015
|
-
matchScore: undefined,
|
|
35016
|
-
hidden: hidden,
|
|
35017
|
-
ref: ref,
|
|
35018
|
-
children: children
|
|
35019
|
-
});
|
|
35020
|
-
};
|
|
35021
|
-
const LIST_ITEM_STYLE_CSS_VARS = {
|
|
35022
|
-
"paddingX": "--list-item-padding-x",
|
|
35023
|
-
"paddingY": "--list-item-padding-y",
|
|
35024
|
-
"padding": "--list-item-padding",
|
|
35025
|
-
"color": "--list-item-color",
|
|
35026
|
-
"backgroundColor": "--list-item-background-color",
|
|
35027
|
-
"fontWeight": "--list-item-font-weight",
|
|
35028
|
-
":-navi-pointed": {
|
|
35029
|
-
color: "--list-item-color-keyboard-pointed",
|
|
35030
|
-
backgroundColor: "--list-item-background-color-keyboard-pointed"
|
|
35031
|
-
},
|
|
35032
|
-
":hover": {
|
|
35033
|
-
color: "--list-item-color-hover",
|
|
35034
|
-
backgroundColor: "--list-item-background-color-hover"
|
|
35035
|
-
},
|
|
35036
|
-
":-navi-selected": {
|
|
35037
|
-
color: "--list-item-color-selected",
|
|
35038
|
-
backgroundColor: "--list-item-background-color-selected"
|
|
35039
|
-
},
|
|
35040
|
-
":disabled": {
|
|
35041
|
-
color: "--list-item-color-disabled",
|
|
35042
|
-
backgroundColor: "--list-item-background-color-disabled"
|
|
35043
|
-
},
|
|
35044
|
-
"::highlight": {
|
|
35045
|
-
color: "--suggestion-color-highlight",
|
|
35046
|
-
backgroundColor: "--suggestion-background-color-highlight"
|
|
35047
34547
|
}
|
|
34548
|
+
return () => {
|
|
34549
|
+
for (const range of ownRanges) {
|
|
34550
|
+
hl.delete(range);
|
|
34551
|
+
}
|
|
34552
|
+
};
|
|
35048
34553
|
};
|
|
35049
|
-
const LIST_ITEM_PSEUDO_CLASSES = [];
|
|
35050
|
-
const LIST_ITEM_PSEUDO_ELEMENTS = ["::highlight"];
|
|
35051
34554
|
|
|
35052
|
-
|
|
35053
|
-
|
|
35054
|
-
|
|
35055
|
-
* Renders a <li role="presentation"> wrapper containing a label span
|
|
35056
|
-
* (accessible via aria-labelledby) and a <ul role="group"> for the items.
|
|
35057
|
-
*
|
|
35058
|
-
* Props:
|
|
35059
|
-
* label — group label content
|
|
35060
|
-
* labelProps — props forwarded to the label <span>
|
|
35061
|
-
* ...rest — forwarded to the outer <li role="presentation">
|
|
35062
|
-
*/
|
|
35063
|
-
const ListItemGroup = ({
|
|
35064
|
-
label,
|
|
35065
|
-
hiddenWhileEmpty,
|
|
35066
|
-
children,
|
|
35067
|
-
...rest
|
|
35068
|
-
}) => {
|
|
35069
|
-
const groupId = useId();
|
|
35070
|
-
const groupTracker = useItemTracker();
|
|
35071
|
-
const groupRef = useRef(null);
|
|
35072
|
-
const labelRef = useRef(null);
|
|
35073
|
-
useDisplayedLayoutEffect(labelRef, labelEl => {
|
|
35074
|
-
const groupEl = groupRef.current;
|
|
35075
|
-
if (!groupEl) {
|
|
35076
|
-
return;
|
|
35077
|
-
}
|
|
35078
|
-
const labelHeight = labelEl.getBoundingClientRect().height;
|
|
35079
|
-
groupEl.style.setProperty("--list-group-label-height", `${labelHeight}px`);
|
|
35080
|
-
}, []);
|
|
35081
|
-
return jsxs(ListItem, {
|
|
35082
|
-
...rest,
|
|
35083
|
-
ref: groupRef,
|
|
35084
|
-
baseClassName: "navi_list_item_group",
|
|
35085
|
-
role: "presentation",
|
|
35086
|
-
"data-hidden-while-empty": hiddenWhileEmpty ? "" : undefined,
|
|
35087
|
-
children: [jsx("span", {
|
|
35088
|
-
ref: labelRef,
|
|
35089
|
-
id: groupId,
|
|
35090
|
-
className: "navi_list_item_group_label",
|
|
35091
|
-
role: "presentation"
|
|
35092
|
-
// eslint-disable-next-line react/no-unknown-property
|
|
35093
|
-
,
|
|
34555
|
+
installImportMetaCssBuild(import.meta);const ListItemTrackerContext = createContext(null);
|
|
34556
|
+
const GroupItemTrackerContext = createContext(null);
|
|
34557
|
+
const PendingScrollRefContext = createContext(null);
|
|
35094
34558
|
|
|
35095
|
-
|
|
35096
|
-
|
|
35097
|
-
|
|
35098
|
-
|
|
35099
|
-
|
|
35100
|
-
"aria-labelledby": groupId,
|
|
35101
|
-
children: jsx(GroupItemTrackerContext.Provider, {
|
|
35102
|
-
value: groupTracker,
|
|
35103
|
-
children: children
|
|
35104
|
-
})
|
|
35105
|
-
})]
|
|
35106
|
-
});
|
|
35107
|
-
};
|
|
35108
|
-
const ListItemHeader = props => {
|
|
35109
|
-
const defaultRef = useRef(null);
|
|
35110
|
-
const ref = props.ref || defaultRef;
|
|
35111
|
-
useDisplayedLayoutEffect(ref, headerEl => {
|
|
35112
|
-
const listContainerEl = headerEl.closest(".navi_list_container");
|
|
35113
|
-
const headerHeight = headerEl.getBoundingClientRect().height;
|
|
35114
|
-
listContainerEl.style.setProperty("--list-header-height", `${headerHeight}px`);
|
|
35115
|
-
}, []);
|
|
35116
|
-
return jsx(ListItem, {
|
|
35117
|
-
...props,
|
|
35118
|
-
ref: ref,
|
|
35119
|
-
role: "presentation",
|
|
35120
|
-
baseClassName: "navi_list_item_header"
|
|
35121
|
-
});
|
|
35122
|
-
};
|
|
35123
|
-
const ListItemFooter = props => {
|
|
35124
|
-
const defaultRef = useRef(null);
|
|
35125
|
-
const ref = props.ref || defaultRef;
|
|
35126
|
-
useDisplayedLayoutEffect(ref, headerEl => {
|
|
35127
|
-
const listContainerEl = headerEl.closest(".navi_list_container");
|
|
35128
|
-
const headerHeight = headerEl.getBoundingClientRect().height;
|
|
35129
|
-
listContainerEl.style.setProperty("--list-footer-height", `${headerHeight}px`);
|
|
35130
|
-
}, []);
|
|
35131
|
-
return jsx(ListItem, {
|
|
35132
|
-
...props,
|
|
35133
|
-
ref: ref,
|
|
35134
|
-
role: "presentation",
|
|
35135
|
-
baseClassName: "navi_list_item_footer"
|
|
35136
|
-
});
|
|
35137
|
-
};
|
|
34559
|
+
// When total rendered items exceeds renderBudget, a render window [start, end)
|
|
34560
|
+
// is activated to cap the number of DOM nodes. Items outside the window return
|
|
34561
|
+
// null. The window slides as the user scrolls, using actual DOM positions
|
|
34562
|
+
// (getBoundingClientRect) to find the first visible item — no height estimation.
|
|
34563
|
+
const RENDER_BUDGET_DEFAULT = 100;
|
|
35138
34564
|
|
|
35139
|
-
|
|
35140
|
-
|
|
35141
|
-
|
|
35142
|
-
|
|
35143
|
-
|
|
35144
|
-
|
|
35145
|
-
|
|
35146
|
-
|
|
35147
|
-
|
|
35148
|
-
|
|
35149
|
-
* mais pilote cet input décoratif
|
|
35150
|
-
*/
|
|
34565
|
+
// Attribute used on <li> elements rendered by ListItemReal so the scroll listener
|
|
34566
|
+
// and filler-height calculation can find real items without matching presentation ones.
|
|
34567
|
+
const REAL_LIST_ITEM_SELECTOR = `[navi-list-item-real]`;
|
|
34568
|
+
|
|
34569
|
+
// Carries the render window {start, end} (or null = render all) from
|
|
34570
|
+
// List down to each ListItem.
|
|
34571
|
+
const RenderWindowContext = createContext(null);
|
|
34572
|
+
// Carries the separator element/function down to each ListItem so separators
|
|
34573
|
+
// are only rendered between items that actually mount (post-filter, post-window).
|
|
34574
|
+
const SeparatorContext = createContext(null);
|
|
35151
34575
|
const css$l = /* css */`
|
|
35152
34576
|
@layer navi {
|
|
35153
34577
|
.navi_list_container {
|
|
35154
|
-
--list-outline-
|
|
35155
|
-
--list-
|
|
35156
|
-
--list-
|
|
35157
|
-
--list-
|
|
35158
|
-
|
|
35159
|
-
--list-
|
|
35160
|
-
--list-item-background-color-hover: light-dark(#f5f5f5, #2a2a2a);
|
|
35161
|
-
/* Pointed by mouse — subtle, just a shade above background */
|
|
35162
|
-
--list-item-color-mouse-pointed: var(--list-item-color);
|
|
35163
|
-
--list-item-background-color-mouse-pointed: light-dark(#ebebeb, #303030);
|
|
35164
|
-
/* Pointed by keyboard — subtle light blue highlight */
|
|
35165
|
-
--list-item-color-keyboard-pointed: var(--list-item-color);
|
|
35166
|
-
--list-item-background-color-keyboard-pointed: light-dark(
|
|
35167
|
-
#c2dcff,
|
|
35168
|
-
#1c3a6e
|
|
35169
|
-
);
|
|
35170
|
-
/* Pointed by proxy */
|
|
35171
|
-
--list-item-color-pointed: var(--list-item-color);
|
|
35172
|
-
--list-item-background-color-pointed: light-dark(#dbeafe, #1c3a6e);
|
|
35173
|
-
/* Selected — vivid blue accent */
|
|
35174
|
-
--list-item-color-selected: white;
|
|
35175
|
-
--list-item-background-color-selected: rgb(3, 30, 60);
|
|
35176
|
-
/* Disabled */
|
|
35177
|
-
--list-item-color-disabled: light-dark(#aaa, #555);
|
|
35178
|
-
--list-item-background-color-disabled: var(--list-item-background-color);
|
|
34578
|
+
--list-outline-width: 1px;
|
|
34579
|
+
--list-border-radius: 4px;
|
|
34580
|
+
--list-border-width: 1px;
|
|
34581
|
+
--list-border-color: light-dark(#ccc, #555);
|
|
34582
|
+
--list-background-color: light-dark(#fff, #1e1e1e);
|
|
34583
|
+
--list-max-height: 220px;
|
|
35179
34584
|
}
|
|
35180
|
-
|
|
35181
|
-
|
|
35182
|
-
|
|
35183
|
-
|
|
35184
|
-
|
|
35185
|
-
|
|
35186
|
-
|
|
35187
|
-
.navi_list_container {
|
|
35188
|
-
--x-list-outline-width: calc(
|
|
35189
|
-
var(--list-outline-width) + var(--list-border-width)
|
|
35190
|
-
);
|
|
35191
|
-
--x-list-outline-offset: calc(-1 * var(--list-border-width));
|
|
34585
|
+
.navi_list_item {
|
|
34586
|
+
--list-item-padding-x: 0px;
|
|
34587
|
+
--list-item-padding-y: 0px;
|
|
34588
|
+
--list-item-padding: var(--list-item-padding-y) var(--list-item-padding-x);
|
|
34589
|
+
--list-item-color: inherit;
|
|
34590
|
+
--list-item-font-weight: inherit;
|
|
34591
|
+
--list-item-background-color: transparent;
|
|
35192
34592
|
|
|
35193
|
-
|
|
35194
|
-
|
|
35195
|
-
|
|
34593
|
+
/* Highlight (CSS Highlight API match) */
|
|
34594
|
+
--list-item-color-highlight: inherit;
|
|
34595
|
+
--list-item-background-color-highlight: #ffe066;
|
|
35196
34596
|
|
|
35197
|
-
|
|
35198
|
-
|
|
35199
|
-
outline-offset: calc(-1 * var(--list-outline-width)); */
|
|
34597
|
+
/* Here to be overridable by box layout props such as flex */
|
|
34598
|
+
display: inline-block;
|
|
35200
34599
|
}
|
|
35201
|
-
|
|
35202
|
-
|
|
34600
|
+
.navi_list_item_group_label {
|
|
34601
|
+
--list-group-label-background-color: var(--list-background-color);
|
|
35203
34602
|
}
|
|
35204
|
-
|
|
35205
|
-
|
|
34603
|
+
.navi_list_item_header {
|
|
34604
|
+
background: var(--list-background-color);
|
|
34605
|
+
}
|
|
34606
|
+
.navi_list_item_footer {
|
|
34607
|
+
background: var(--list-background-color);
|
|
35206
34608
|
}
|
|
35207
34609
|
}
|
|
35208
34610
|
|
|
35209
|
-
.
|
|
35210
|
-
|
|
35211
|
-
|
|
35212
|
-
|
|
35213
|
-
|
|
34611
|
+
.navi_list_container {
|
|
34612
|
+
--x-list-border-radius: var(--list-border-radius);
|
|
34613
|
+
--x-list-border-width: var(--list-border-width);
|
|
34614
|
+
--x-list-border-color: var(--list-border-color);
|
|
34615
|
+
--x-list-background-color: var(--list-background-color);
|
|
34616
|
+
/* When typing inside an input browser tries to keep caret visible */
|
|
34617
|
+
/* For input within a sticky element inside a scrollable container */
|
|
34618
|
+
/* Browser will try to scroll that input into view */
|
|
34619
|
+
/* When that scrollable container has a scroll padding it causes scroll on each keystroke */
|
|
34620
|
+
/* Even putting a scroll margin on the input won't fix */
|
|
34621
|
+
/* The only solution is to use scroll-margins on each item that can scroll */
|
|
34622
|
+
/* This is why these props are named list-scroll-spacing-top and applied via scroll-margin on items */
|
|
34623
|
+
--x-list-scroll-spacing-top: calc(
|
|
34624
|
+
var(--list-header-height, 0px) + var(--list-scroll-padding-top, 0px)
|
|
34625
|
+
);
|
|
34626
|
+
--x-list-scroll-spacing-bottom: calc(
|
|
34627
|
+
var(--list-footer-height, 0px) + var(--list-scroll-padding-bottom, 0px)
|
|
34628
|
+
);
|
|
34629
|
+
--x-list-scroll-spacing-left: calc(
|
|
34630
|
+
var(--list-header-width, 0px) + var(--list-scroll-padding-left, 0px)
|
|
34631
|
+
);
|
|
34632
|
+
--x-list-scroll-spacing-right: calc(
|
|
34633
|
+
var(--list-footer-width, 0px) + var(--list-scroll-padding-right, 0px)
|
|
34634
|
+
);
|
|
35214
34635
|
|
|
35215
|
-
|
|
35216
|
-
|
|
35217
|
-
|
|
34636
|
+
display: flex;
|
|
34637
|
+
min-width: 0;
|
|
34638
|
+
/* fit-content by default, but never wider than the parent */
|
|
34639
|
+
max-width: 100%;
|
|
34640
|
+
flex-direction: column;
|
|
34641
|
+
background-color: var(--x-list-background-color);
|
|
34642
|
+
border: var(--x-list-border-width) solid var(--x-list-border-color);
|
|
34643
|
+
border-radius: var(--x-list-border-radius);
|
|
35218
34644
|
|
|
35219
|
-
|
|
35220
|
-
|
|
35221
|
-
|
|
35222
|
-
|
|
35223
|
-
|
|
35224
|
-
|
|
35225
|
-
|
|
35226
|
-
|
|
35227
|
-
|
|
34645
|
+
transition: opacity 0.2s ease;
|
|
34646
|
+
/* overflow:hidden is required on the container (not the inner scroll element)
|
|
34647
|
+
so that border-radius clips the content correctly. Without it, items near
|
|
34648
|
+
the corners would visually overflow the rounded corners during scroll. */
|
|
34649
|
+
overflow: hidden;
|
|
34650
|
+
|
|
34651
|
+
.navi_list_scroll_container {
|
|
34652
|
+
width: inherit;
|
|
34653
|
+
min-width: inherit;
|
|
34654
|
+
max-width: inherit;
|
|
34655
|
+
max-height: var(--list-max-height);
|
|
34656
|
+
overflow: auto;
|
|
34657
|
+
overscroll-behavior: inherit; /* inherit select behavior */
|
|
35228
34658
|
}
|
|
35229
|
-
|
|
35230
|
-
|
|
35231
|
-
|
|
34659
|
+
|
|
34660
|
+
&[data-expand-x] {
|
|
34661
|
+
width: 100%;
|
|
35232
34662
|
}
|
|
35233
|
-
|
|
35234
|
-
|
|
35235
|
-
|
|
35236
|
-
|
|
35237
|
-
|
|
35238
|
-
|
|
35239
|
-
|
|
35240
|
-
|
|
34663
|
+
&[popover] {
|
|
34664
|
+
position: absolute;
|
|
34665
|
+
inset: unset;
|
|
34666
|
+
display: none;
|
|
34667
|
+
min-width: var(--list-anchor-width, 0px);
|
|
34668
|
+
max-width: 95vw;
|
|
34669
|
+
margin: 0;
|
|
34670
|
+
padding: 0;
|
|
35241
34671
|
|
|
35242
|
-
|
|
35243
|
-
|
|
35244
|
-
|
|
35245
|
-
|
|
35246
|
-
|
|
35247
|
-
);
|
|
35248
|
-
}
|
|
34672
|
+
&:popover-open {
|
|
34673
|
+
display: flex;
|
|
34674
|
+
}
|
|
34675
|
+
.navi_list {
|
|
34676
|
+
width: 100%;
|
|
35249
34677
|
}
|
|
35250
34678
|
|
|
35251
|
-
&[data-
|
|
35252
|
-
|
|
35253
|
-
|
|
35254
|
-
--list-item-background-color-selected
|
|
35255
|
-
);
|
|
35256
|
-
&[data-hover] {
|
|
35257
|
-
--x-list-item-background-color: var(
|
|
35258
|
-
--list-item-background-color-selected,
|
|
35259
|
-
var(--list-item-background-color-mouse-pointed)
|
|
35260
|
-
) !important;
|
|
35261
|
-
}
|
|
34679
|
+
&[data-anchor-hidden] {
|
|
34680
|
+
opacity: 0;
|
|
34681
|
+
pointer-events: none;
|
|
35262
34682
|
}
|
|
35263
34683
|
}
|
|
34684
|
+
}
|
|
35264
34685
|
|
|
35265
|
-
|
|
35266
|
-
|
|
35267
|
-
|
|
35268
|
-
|
|
35269
|
-
|
|
35270
|
-
|
|
35271
|
-
pointer-events: none;
|
|
35272
|
-
}
|
|
35273
|
-
&[data-readonly] {
|
|
35274
|
-
--x-list-item-color: var(--list-item-color-disabled);
|
|
35275
|
-
cursor: default;
|
|
35276
|
-
}
|
|
34686
|
+
.navi_list {
|
|
34687
|
+
box-sizing: border-box;
|
|
34688
|
+
margin: 0;
|
|
34689
|
+
padding: 0;
|
|
34690
|
+
list-style: none;
|
|
34691
|
+
outline: none; /* Focus is displayed on the container */
|
|
35277
34692
|
}
|
|
35278
|
-
`;
|
|
35279
|
-
const SelectableListMultipleContext = createContext(false);
|
|
35280
34693
|
|
|
35281
|
-
|
|
35282
|
-
|
|
35283
|
-
|
|
35284
|
-
|
|
35285
|
-
|
|
35286
|
-
|
|
35287
|
-
|
|
35288
|
-
|
|
35289
|
-
|
|
35290
|
-
|
|
35291
|
-
|
|
35292
|
-
|
|
35293
|
-
|
|
35294
|
-
|
|
35295
|
-
|
|
35296
|
-
|
|
35297
|
-
|
|
35298
|
-
|
|
35299
|
-
|
|
35300
|
-
|
|
35301
|
-
|
|
35302
|
-
|
|
35303
|
-
|
|
35304
|
-
|
|
35305
|
-
|
|
35306
|
-
|
|
35307
|
-
|
|
35308
|
-
|
|
35309
|
-
|
|
35310
|
-
|
|
35311
|
-
|
|
35312
|
-
|
|
35313
|
-
|
|
35314
|
-
|
|
35315
|
-
|
|
35316
|
-
|
|
35317
|
-
|
|
35318
|
-
|
|
35319
|
-
|
|
35320
|
-
|
|
34694
|
+
.navi_list_item {
|
|
34695
|
+
--x-list-item-color: var(--list-item-color);
|
|
34696
|
+
--x-list-item-background-color: var(--list-item-background-color);
|
|
34697
|
+
--x-list-item-font-weight: var(--list-item-font-weight);
|
|
34698
|
+
--x-list-item-border-width: var(--list-item-border-width, 0px);
|
|
34699
|
+
--x-list-item-border-color: var(--list-item-border-color, black);
|
|
34700
|
+
|
|
34701
|
+
box-sizing: border-box;
|
|
34702
|
+
min-width: 0;
|
|
34703
|
+
max-width: 100%;
|
|
34704
|
+
padding: var(--list-item-padding);
|
|
34705
|
+
color: var(--x-list-item-color);
|
|
34706
|
+
font-weight: var(--x-list-item-font-weight);
|
|
34707
|
+
background-color: var(--x-list-item-background-color);
|
|
34708
|
+
border: var(--x-list-item-border-width) solid
|
|
34709
|
+
var(--x-list-item-border-color);
|
|
34710
|
+
/*
|
|
34711
|
+
CSS impossible d'obtenir un layout qui ferait en gros:
|
|
34712
|
+
width = max(min(max-content, 100%), unbreakable-content)
|
|
34713
|
+
Donc 3 options:
|
|
34714
|
+
- Laisser le contenu overflow
|
|
34715
|
+
- moche, background ne suit pas
|
|
34716
|
+
-> NOPE
|
|
34717
|
+
- Force overflow hidden + ellipsis
|
|
34718
|
+
- casse la lisibilité des mots insécables
|
|
34719
|
+
- possible d'optin en utilisant overflowEllipsis sur le ListItem
|
|
34720
|
+
-> Bien mais pas par défaut
|
|
34721
|
+
- Forcer le retour a la ligne des mot inécables
|
|
34722
|
+
- Aucun des inconvénient ci dessus
|
|
34723
|
+
-> Comportement par défaut
|
|
34724
|
+
*/
|
|
34725
|
+
overflow-wrap: anywhere;
|
|
34726
|
+
/* When list has sticky header/footer, put a scroll padding */
|
|
34727
|
+
scroll-margin-top: var(--x-list-scroll-spacing-top);
|
|
34728
|
+
scroll-margin-right: var(--x-list-scroll-spacing-right);
|
|
34729
|
+
scroll-margin-bottom: var(--x-list-scroll-spacing-bottom);
|
|
34730
|
+
scroll-margin-left: var(--x-list-scroll-spacing-left);
|
|
34731
|
+
}
|
|
34732
|
+
|
|
34733
|
+
/* Virtual scroll fillers — must remain invisible.
|
|
34734
|
+
The browser may briefly flash them during scroll before the render window
|
|
34735
|
+
updates, so giving them a visible background would cause visual glitches. */
|
|
34736
|
+
.navi_list_virtual_filler {
|
|
34737
|
+
display: inline-block;
|
|
34738
|
+
height: var(--size-to-fill, 0px);
|
|
34739
|
+
list-style: none;
|
|
34740
|
+
/* background: pink; */
|
|
34741
|
+
}
|
|
34742
|
+
&[data-horizontal] {
|
|
34743
|
+
.navi_list_virtual_filler {
|
|
34744
|
+
width: var(--size-to-fill, 0px);
|
|
34745
|
+
height: 100%;
|
|
34746
|
+
}
|
|
34747
|
+
}
|
|
34748
|
+
|
|
34749
|
+
/* Empty state — hidden by default, shown when no list items are rendered.
|
|
34750
|
+
order: 1 pushes fallbacks after all regular items in flex column layout.
|
|
34751
|
+
The list children are open-ended (headers, presentation items, real items),
|
|
34752
|
+
so we cannot control where the consumer places the fallback nodes in the DOM.
|
|
34753
|
+
Using order ensures fallbacks always appear after items regardless of DOM order.
|
|
34754
|
+
matchFallback intentionally shares the same order as fallback so it appears
|
|
34755
|
+
at the same visual position — after an input if present but before any items
|
|
34756
|
+
still displayed (non-matching items remain in DOM with hidden prop):
|
|
34757
|
+
1. Input (sticky header, order: -2)
|
|
34758
|
+
2. matchFallback (order: -1)
|
|
34759
|
+
3. hidden items (regular order, after DOM flow)
|
|
34760
|
+
4. HOT FIX OF THE DEAD for bottom filler + preact issue: order: 1
|
|
34761
|
+
5. sticky footer (order: 2)
|
|
34762
|
+
*/
|
|
34763
|
+
/* order: 0 keeps the header pinned before fallbacks (order: 1) in flex order,
|
|
34764
|
+
ensuring the header (e.g. a search input) always appears above them. */
|
|
34765
|
+
.navi_list_item_header {
|
|
34766
|
+
position: sticky;
|
|
34767
|
+
top: 0;
|
|
34768
|
+
left: 0;
|
|
34769
|
+
z-index: 1;
|
|
34770
|
+
order: -2;
|
|
34771
|
+
}
|
|
34772
|
+
.navi_list_fallback,
|
|
34773
|
+
.navi_list_no_match_fallback {
|
|
34774
|
+
order: -1;
|
|
34775
|
+
color: light-dark(#888, #aaa);
|
|
34776
|
+
&[navi-default] {
|
|
34777
|
+
display: inline;
|
|
34778
|
+
padding: var(--list-item-padding);
|
|
34779
|
+
text-align: center;
|
|
34780
|
+
user-select: none;
|
|
34781
|
+
}
|
|
34782
|
+
}
|
|
34783
|
+
[navi-virtual-filler="after"] {
|
|
34784
|
+
/* for some reason preact ends up puttin this element before the list items in some scenarios
|
|
34785
|
+
I've noticed that removing the ItemIndexToScrollOnMountRefContext.Provider
|
|
34786
|
+
does fix this issue (I suppose it's because it cause on less render of the list which is the problematic one)
|
|
34787
|
+
this order ENSURE that even when preact hallucinates we are still correctly putting the bottom filler
|
|
34788
|
+
after the list items */
|
|
34789
|
+
order: 1;
|
|
34790
|
+
}
|
|
34791
|
+
/* order: 2 pins the footer after fallbacks (order: 1) and all items. */
|
|
34792
|
+
.navi_list_item_footer {
|
|
34793
|
+
position: sticky;
|
|
34794
|
+
right: 0;
|
|
34795
|
+
bottom: 0;
|
|
34796
|
+
z-index: 1;
|
|
34797
|
+
order: 2;
|
|
34798
|
+
}
|
|
34799
|
+
|
|
34800
|
+
::highlight(navi-search-match) {
|
|
34801
|
+
color: var(--list-item-color-highlight);
|
|
34802
|
+
background-color: var(--list-item-background-color-highlight);
|
|
34803
|
+
}
|
|
34804
|
+
|
|
34805
|
+
/* Hide groups that have no rendered items. */
|
|
34806
|
+
.navi_list_item_group {
|
|
34807
|
+
min-width: 100%;
|
|
34808
|
+
|
|
34809
|
+
.navi_list_item_group_label {
|
|
34810
|
+
position: sticky;
|
|
34811
|
+
top: 0;
|
|
34812
|
+
z-index: 1;
|
|
34813
|
+
display: block;
|
|
34814
|
+
background-color: var(--list-group-label-background-color);
|
|
34815
|
+
user-select: none;
|
|
34816
|
+
|
|
34817
|
+
&[navi-default] {
|
|
34818
|
+
padding: 4px 12px 2px;
|
|
34819
|
+
color: light-dark(#888, #aaa);
|
|
34820
|
+
font-weight: 600;
|
|
34821
|
+
font-size: 0.75em;
|
|
34822
|
+
text-transform: uppercase;
|
|
34823
|
+
letter-spacing: 0.05em;
|
|
35321
34824
|
}
|
|
35322
|
-
|
|
34825
|
+
}
|
|
34826
|
+
.navi_list_item_group_list {
|
|
34827
|
+
display: flex;
|
|
34828
|
+
width: max-content;
|
|
34829
|
+
min-width: 100%;
|
|
34830
|
+
margin: 0;
|
|
34831
|
+
padding: 0;
|
|
34832
|
+
flex-direction: column;
|
|
34833
|
+
list-style: none;
|
|
34834
|
+
|
|
34835
|
+
/* Items inside a group must account for the sticky group label height
|
|
34836
|
+
on top of the list's global header/scroll-padding spacing. */
|
|
34837
|
+
.navi_list_item {
|
|
34838
|
+
scroll-margin-top: calc(
|
|
34839
|
+
var(--x-list-scroll-spacing-top) + var(--list-group-label-height, 0px)
|
|
34840
|
+
);
|
|
34841
|
+
scroll-margin-left: calc(
|
|
34842
|
+
var(--x-list-scroll-spacing-left) + var(--list-group-label-width, 0px)
|
|
34843
|
+
);
|
|
34844
|
+
}
|
|
34845
|
+
}
|
|
34846
|
+
|
|
34847
|
+
&[data-hidden-while-empty]:not(:has([navi-list-item-real])) {
|
|
34848
|
+
display: none;
|
|
34849
|
+
}
|
|
34850
|
+
}
|
|
34851
|
+
`;
|
|
34852
|
+
|
|
34853
|
+
/**
|
|
34854
|
+
* List — generic virtualized scroll container.
|
|
34855
|
+
*
|
|
34856
|
+
* Renders children inside a scrollable container with an optional render budget
|
|
34857
|
+
* for virtual scrolling. Items must use <ListItem> to participate in tracking.
|
|
34858
|
+
*
|
|
34859
|
+
* Props:
|
|
34860
|
+
* keyboardInteractions — when true, attaches arrow/enter/escape keyboard shortcuts
|
|
34861
|
+
* that dispatch navi_list_nav / navi_list_confirm / navi_list_clear
|
|
34862
|
+
* to the list container. Pair with uiAction for a full keyboard-
|
|
34863
|
+
* navigable list.
|
|
34864
|
+
* uiAction — called with the selected value on confirm. When provided
|
|
34865
|
+
* the list becomes interactive: tracks hover and keyboard-
|
|
34866
|
+
* pointed state, handles navi_list_nav / navi_list_clear /
|
|
34867
|
+
* navi_list_confirm custom events via ListInteractionContext.
|
|
34868
|
+
* popover — when true, renders as a managed popover positioned near
|
|
34869
|
+
* an anchor element via navi_list_open / navi_list_close events.
|
|
34870
|
+
* renderBudget — max items in DOM at once (default 100, virtual scroll when exceeded)
|
|
34871
|
+
* virtualItemSize — fixed px size per item (width if horizontal, height otherwise) when all items have the same size.
|
|
34872
|
+
* Enables precise virtual-scroll filler sizing without a DOM
|
|
34873
|
+
* measurement pass. Required when renderBudget is active and
|
|
34874
|
+
* item height is known up-front.
|
|
34875
|
+
* fallback — content shown when no items exist at all
|
|
34876
|
+
* matchFallback — content shown when items exist but all are hidden (e.g. no search match)
|
|
34877
|
+
* separator — element or function(index, { previousItem, currentItem }) inserted between visible items
|
|
34878
|
+
* lockSize — when true, captures the container's dimensions on first render
|
|
34879
|
+
* (always in unfiltered state). Those values become min-width/
|
|
34880
|
+
* min-height so filtering cannot collapse the layout.
|
|
34881
|
+
* ...rest — forwarded to the outer scroll container <Box>
|
|
34882
|
+
*/
|
|
34883
|
+
const ListUI = props => {
|
|
34884
|
+
import.meta.css = [css$l, "@jsenv/navi/src/control/list/list.jsx"];
|
|
34885
|
+
const {
|
|
34886
|
+
ref,
|
|
34887
|
+
renderBudget = RENDER_BUDGET_DEFAULT,
|
|
34888
|
+
renderBudgetSkipCheck,
|
|
34889
|
+
role,
|
|
34890
|
+
fallback,
|
|
34891
|
+
noMatchFallback,
|
|
34892
|
+
separator,
|
|
34893
|
+
children,
|
|
34894
|
+
popover,
|
|
34895
|
+
expandX,
|
|
34896
|
+
expand,
|
|
34897
|
+
maxHeight,
|
|
34898
|
+
onListVisibleItemsChange,
|
|
34899
|
+
virtualItemSize,
|
|
34900
|
+
lockSize,
|
|
34901
|
+
searchText,
|
|
34902
|
+
horizontal,
|
|
34903
|
+
spacing,
|
|
34904
|
+
...rest
|
|
34905
|
+
} = props;
|
|
34906
|
+
if (renderBudget < 30 && !renderBudgetSkipCheck) {
|
|
34907
|
+
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}).`);
|
|
34908
|
+
}
|
|
34909
|
+
|
|
34910
|
+
// lockSize: capture the container's dimensions on first render so filtering
|
|
34911
|
+
// cannot collapse the layout. Measurement happens on the initial (unfiltered)
|
|
34912
|
+
// state because the parent controls hidden props before any search is applied.
|
|
34913
|
+
const sizeLocked = useRef(false);
|
|
34914
|
+
useDisplayedLayoutEffect(ref, listContainerEl => {
|
|
34915
|
+
if (!lockSize) {
|
|
34916
|
+
return undefined;
|
|
34917
|
+
}
|
|
34918
|
+
const observer = new ResizeObserver(entries => {
|
|
34919
|
+
const entry = entries[0];
|
|
34920
|
+
// Use borderBoxSize (outer width) not contentRect (which excludes the
|
|
34921
|
+
// scrollbar width). If we used contentRect, min-width would be set to
|
|
34922
|
+
// outerWidth − scrollbarWidth, and the container would shrink by exactly
|
|
34923
|
+
// the scrollbar width when the scrollbar disappears.
|
|
34924
|
+
const borderBoxEntry = entry.borderBoxSize ? entry.borderBoxSize[0] : null;
|
|
34925
|
+
const width = borderBoxEntry ? borderBoxEntry.inlineSize : entry.contentRect.width;
|
|
34926
|
+
const height = borderBoxEntry ? borderBoxEntry.blockSize : entry.contentRect.height;
|
|
34927
|
+
if (width === 0 && height === 0) {
|
|
34928
|
+
return;
|
|
34929
|
+
}
|
|
34930
|
+
listContainerEl.style.minWidth = `${width}px`;
|
|
34931
|
+
listContainerEl.style.minHeight = `${height}px`;
|
|
34932
|
+
sizeLocked.current = true;
|
|
34933
|
+
observer.disconnect();
|
|
34934
|
+
});
|
|
34935
|
+
observer.observe(listContainerEl);
|
|
34936
|
+
return () => {
|
|
34937
|
+
observer.disconnect();
|
|
34938
|
+
};
|
|
34939
|
+
}, [lockSize]);
|
|
34940
|
+
const tracker = useItemTracker({
|
|
34941
|
+
onChange: () => {
|
|
34942
|
+
onListVisibleItemsChange?.(tracker.visibleItemsSignal.peek());
|
|
35323
34943
|
}
|
|
35324
34944
|
});
|
|
35325
|
-
|
|
35326
|
-
|
|
35327
|
-
|
|
35328
|
-
|
|
35329
|
-
|
|
34945
|
+
const {
|
|
34946
|
+
virtualItemSizeSignal,
|
|
34947
|
+
renderWindow,
|
|
34948
|
+
scrollToItem,
|
|
34949
|
+
pendingScrollRef
|
|
34950
|
+
} = useListScrollSync({
|
|
34951
|
+
ref,
|
|
34952
|
+
tracker,
|
|
34953
|
+
renderBudget,
|
|
34954
|
+
virtualItemSize,
|
|
34955
|
+
searchText,
|
|
34956
|
+
horizontal
|
|
35330
34957
|
});
|
|
35331
|
-
|
|
35332
|
-
|
|
35333
|
-
|
|
35334
|
-
|
|
35335
|
-
|
|
35336
|
-
|
|
35337
|
-
|
|
35338
|
-
|
|
35339
|
-
|
|
35340
|
-
|
|
35341
|
-
|
|
35342
|
-
|
|
34958
|
+
const getItemById = itemId => {
|
|
34959
|
+
return tracker.itemsSignal.peek().find(item => item.id === itemId);
|
|
34960
|
+
};
|
|
34961
|
+
return jsx(Box, {
|
|
34962
|
+
...rest,
|
|
34963
|
+
ref: ref,
|
|
34964
|
+
baseClassName: "navi_list_container",
|
|
34965
|
+
popover: popover,
|
|
34966
|
+
"data-horizontal": horizontal ? "" : undefined,
|
|
34967
|
+
"data-expand-x": expandX || expand ? "" : undefined,
|
|
34968
|
+
expandX: expandX,
|
|
34969
|
+
expand: expand,
|
|
34970
|
+
maxHeight: maxHeight,
|
|
34971
|
+
styleCSSVars: LIST_STYLE_CSS_VARS,
|
|
34972
|
+
pseudoClasses: LIST_PSEUDO_CLASSES,
|
|
34973
|
+
hasChildUsingForwardedProps: true,
|
|
34974
|
+
onnavi_request_scroll: e => {
|
|
34975
|
+
if (!Object.hasOwn(e.detail, "id")) {
|
|
34976
|
+
console.warn(`navi_request_scroll event is missing the "id" property in its detail.`, e);
|
|
34977
|
+
return;
|
|
34978
|
+
}
|
|
34979
|
+
const {
|
|
34980
|
+
id
|
|
34981
|
+
} = e.detail;
|
|
34982
|
+
const item = getItemById(id);
|
|
34983
|
+
scrollToItem(item, {
|
|
34984
|
+
event: e,
|
|
34985
|
+
reason: "navi_request_scroll"
|
|
34986
|
+
});
|
|
34987
|
+
},
|
|
34988
|
+
children: jsx(ListContent, {
|
|
34989
|
+
role: role,
|
|
34990
|
+
fallback: fallback,
|
|
34991
|
+
noMatchFallback: noMatchFallback,
|
|
34992
|
+
searchText: searchText,
|
|
34993
|
+
separator: separator,
|
|
34994
|
+
expandX: expandX,
|
|
34995
|
+
expand: expand,
|
|
34996
|
+
horizontal: horizontal,
|
|
34997
|
+
spacing: spacing,
|
|
34998
|
+
tracker: tracker,
|
|
34999
|
+
renderWindow: renderWindow,
|
|
35000
|
+
virtualItemSizeSignal: virtualItemSizeSignal,
|
|
35001
|
+
pendingScrollRef: pendingScrollRef,
|
|
35002
|
+
children: children
|
|
35003
|
+
})
|
|
35004
|
+
});
|
|
35005
|
+
};
|
|
35006
|
+
const ListFirstResolver = props => {
|
|
35007
|
+
const Next = useNextResolver();
|
|
35008
|
+
const refDefault = useRef(null);
|
|
35009
|
+
props.ref = props.ref || refDefault;
|
|
35010
|
+
const idDefault = useId();
|
|
35011
|
+
props.id = props.id || idDefault;
|
|
35012
|
+
return jsx(Next, {
|
|
35013
|
+
...props
|
|
35014
|
+
});
|
|
35015
|
+
};
|
|
35016
|
+
const List = createComponentResolver([ListFirstResolver, ListSelectableResolver, ListUI]);
|
|
35017
|
+
const ListContent = ({
|
|
35018
|
+
role,
|
|
35019
|
+
fallback,
|
|
35020
|
+
noMatchFallback,
|
|
35021
|
+
searchText,
|
|
35022
|
+
separator,
|
|
35023
|
+
expandX,
|
|
35024
|
+
expand,
|
|
35025
|
+
horizontal,
|
|
35026
|
+
spacing,
|
|
35027
|
+
tracker,
|
|
35028
|
+
renderWindow,
|
|
35029
|
+
virtualItemSizeSignal,
|
|
35030
|
+
pendingScrollRef,
|
|
35031
|
+
children
|
|
35032
|
+
}) => {
|
|
35033
|
+
const listProps = useContext(BoxForwardedPropsContext);
|
|
35034
|
+
return jsx("div", {
|
|
35035
|
+
className: "navi_list_scroll_container",
|
|
35036
|
+
children: jsx(UnorderedList, {
|
|
35037
|
+
role: role,
|
|
35038
|
+
fallback: fallback,
|
|
35039
|
+
noMatchFallback: noMatchFallback,
|
|
35040
|
+
searchText: searchText,
|
|
35041
|
+
separator: separator === true ? jsx(Separator, {
|
|
35042
|
+
margin: "0"
|
|
35043
|
+
}) : separator,
|
|
35044
|
+
expandX: expandX || expand,
|
|
35045
|
+
horizontal: horizontal,
|
|
35046
|
+
spacing: spacing,
|
|
35047
|
+
...listProps,
|
|
35048
|
+
tracker: tracker,
|
|
35049
|
+
renderWindow: renderWindow,
|
|
35050
|
+
virtualItemSizeSignal: virtualItemSizeSignal,
|
|
35051
|
+
children: jsx(PendingScrollRefContext.Provider, {
|
|
35052
|
+
value: pendingScrollRef,
|
|
35053
|
+
children: children
|
|
35054
|
+
})
|
|
35055
|
+
})
|
|
35056
|
+
});
|
|
35057
|
+
};
|
|
35058
|
+
const LIST_STYLE_CSS_VARS = {
|
|
35059
|
+
maxHeight: "--list-max-height",
|
|
35060
|
+
borderColor: "--list-border-color",
|
|
35061
|
+
borderRadius: "--list-border-radius",
|
|
35062
|
+
borderWidth: "--list-border-width"
|
|
35063
|
+
};
|
|
35064
|
+
const LIST_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled", ":-navi-void", ":-navi-expanded"];
|
|
35065
|
+
const useListScrollSync = ({
|
|
35066
|
+
ref,
|
|
35067
|
+
tracker,
|
|
35068
|
+
renderBudget,
|
|
35069
|
+
virtualItemSize,
|
|
35070
|
+
searchText,
|
|
35071
|
+
horizontal
|
|
35072
|
+
}) => {
|
|
35073
|
+
const debugScroll = useDebugScroll();
|
|
35074
|
+
const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal);
|
|
35075
|
+
const [renderWindow, setRenderWindow] = useState({
|
|
35076
|
+
start: 0,
|
|
35077
|
+
end: renderBudget
|
|
35078
|
+
});
|
|
35079
|
+
const renderWindowRef = useRef(null);
|
|
35080
|
+
renderWindowRef.current = renderWindow;
|
|
35081
|
+
const updateRenderWindow = (newStart, newEnd, reason) => {
|
|
35082
|
+
const {
|
|
35083
|
+
start,
|
|
35084
|
+
end
|
|
35085
|
+
} = renderWindowRef.current;
|
|
35086
|
+
if (newStart === start && newEnd === end) {
|
|
35343
35087
|
return;
|
|
35344
35088
|
}
|
|
35345
|
-
|
|
35346
|
-
const
|
|
35347
|
-
|
|
35089
|
+
debugScroll(`updateRenderWindow(${newStart}, ${newEnd}, "${reason}")`);
|
|
35090
|
+
const renderWindow = {
|
|
35091
|
+
start: newStart,
|
|
35092
|
+
end: newEnd
|
|
35093
|
+
};
|
|
35094
|
+
renderWindowRef.current = renderWindow;
|
|
35095
|
+
setRenderWindow(renderWindow);
|
|
35096
|
+
};
|
|
35097
|
+
const pendingScrollRef = useRef();
|
|
35098
|
+
const scrollToItem = (item, {
|
|
35099
|
+
event,
|
|
35100
|
+
reason
|
|
35101
|
+
}) => {
|
|
35102
|
+
if (!item) {
|
|
35348
35103
|
return;
|
|
35349
35104
|
}
|
|
35350
|
-
|
|
35351
|
-
|
|
35105
|
+
const items = tracker.itemsSignal.peek();
|
|
35106
|
+
const itemCount = items.length;
|
|
35107
|
+
if (itemCount === 0) {
|
|
35108
|
+
return;
|
|
35109
|
+
}
|
|
35110
|
+
let index = items.findIndex(i => i.id === item.id);
|
|
35111
|
+
if (index === -1) {
|
|
35112
|
+
return;
|
|
35113
|
+
}
|
|
35114
|
+
if (index >= itemCount) {
|
|
35115
|
+
index = itemCount - 1;
|
|
35116
|
+
}
|
|
35117
|
+
const scrollItemIntoView = itemEl => {
|
|
35118
|
+
const trigger = `"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
|
|
35119
|
+
// When we display the list we prefer to have selected item at the center
|
|
35120
|
+
// otherwise, usually when focused by arrow nav, we want to keep it into view close to the nearest edge
|
|
35121
|
+
const block = event.type === "navi_displayed" ? "center" : "nearest";
|
|
35122
|
+
const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${block}", container: "nearest" })`;
|
|
35123
|
+
const listScrollContainerEl = ref.current.querySelector(`.navi_list_scroll_container`);
|
|
35124
|
+
debugScroll(`${trigger} -> ${scrollToItemCall}`);
|
|
35125
|
+
scrollIntoViewScoped(itemEl, {
|
|
35126
|
+
container: listScrollContainerEl,
|
|
35127
|
+
block
|
|
35128
|
+
});
|
|
35129
|
+
const listEl = ref.current.querySelector(".navi_list");
|
|
35130
|
+
dispatchPublicCustomEvent(listEl, "navi_scroll", {
|
|
35131
|
+
event,
|
|
35132
|
+
item
|
|
35133
|
+
});
|
|
35134
|
+
};
|
|
35135
|
+
const {
|
|
35136
|
+
start,
|
|
35137
|
+
end
|
|
35138
|
+
} = renderWindowRef.current;
|
|
35139
|
+
const isInWindow = index >= start && index < end;
|
|
35140
|
+
if (isInWindow) {
|
|
35141
|
+
const itemEl = document.getElementById(item.id);
|
|
35142
|
+
if (itemEl) {
|
|
35143
|
+
scrollItemIntoView(itemEl);
|
|
35144
|
+
return;
|
|
35145
|
+
}
|
|
35146
|
+
}
|
|
35147
|
+
// Not in DOM — shift the render window. The item will read
|
|
35148
|
+
// pendingScrollRef on mount and scroll into view.
|
|
35149
|
+
pendingScrollRef.current = {
|
|
35150
|
+
id: item.id,
|
|
35151
|
+
resolve: itemEl => {
|
|
35152
|
+
pendingScrollRef.current = null;
|
|
35153
|
+
scrollItemIntoView(itemEl);
|
|
35154
|
+
}
|
|
35155
|
+
};
|
|
35156
|
+
const half = Math.floor(renderBudget / 2);
|
|
35157
|
+
const newStart = Math.max(0, index - half);
|
|
35158
|
+
const newEnd = newStart + renderBudget;
|
|
35159
|
+
updateRenderWindow(newStart, newEnd, `item to scroll (at ${index}) is out of render window`);
|
|
35160
|
+
};
|
|
35161
|
+
const currentScrollRef = useRef(null);
|
|
35162
|
+
const updateCurrentScroll = () => {
|
|
35163
|
+
const listScrollContainerEl = ref.current.querySelector(`.navi_list_scroll_container`);
|
|
35164
|
+
const currentScrollLeft = listScrollContainerEl.scrollLeft;
|
|
35165
|
+
const currentScrollTop = listScrollContainerEl.scrollTop;
|
|
35166
|
+
const renderWindow = renderWindowRef.current;
|
|
35167
|
+
currentScrollRef.current = {
|
|
35168
|
+
left: currentScrollLeft,
|
|
35169
|
+
top: currentScrollTop,
|
|
35170
|
+
renderWindow: {
|
|
35171
|
+
...renderWindow
|
|
35172
|
+
}
|
|
35173
|
+
};
|
|
35174
|
+
debugScroll(`store currentScroll: scrollTop=${currentScrollTop}, renderWindow=[${renderWindow.start}, ${renderWindow.end})`);
|
|
35175
|
+
};
|
|
35176
|
+
const searchTextRef = useRef();
|
|
35177
|
+
let searchTextBecomesActive = false;
|
|
35178
|
+
if (searchTextRef.current === undefined) {
|
|
35179
|
+
searchTextRef.current = searchText;
|
|
35180
|
+
} else {
|
|
35181
|
+
const searchTextPrevious = searchTextRef.current;
|
|
35182
|
+
searchTextRef.current = searchText;
|
|
35183
|
+
if (!searchTextPrevious && searchText) {
|
|
35184
|
+
searchTextBecomesActive = true;
|
|
35185
|
+
}
|
|
35186
|
+
}
|
|
35187
|
+
// Scroll to the selected item only the FIRST time the list is presented on screen,
|
|
35188
|
+
// so the user can see what's selected on initial open. On subsequent re-displays
|
|
35189
|
+
// (e.g. reopening a popover containing the list), we intentionally keep the previous
|
|
35190
|
+
// scroll position — it's less disruptive UX to land where the user last was, even
|
|
35191
|
+
// if that means the selected item isn't currently visible.
|
|
35192
|
+
// Skipped when inside a closed <dialog>/<details> (scrollIntoView is a no-op
|
|
35193
|
+
// on hidden elements); re-runs automatically every time the ancestor opens.
|
|
35194
|
+
const hasBeenDisplayedRef = useRef(false);
|
|
35195
|
+
useDisplayedLayoutEffect(ref, (el, openEvent) => {
|
|
35196
|
+
updateCurrentScroll();
|
|
35197
|
+
if (hasBeenDisplayedRef.current) {
|
|
35198
|
+
return;
|
|
35199
|
+
}
|
|
35200
|
+
hasBeenDisplayedRef.current = true;
|
|
35201
|
+
const items = tracker.itemsSignal.peek();
|
|
35202
|
+
const firstSelected = items.find(i => i.selected);
|
|
35203
|
+
if (firstSelected) {
|
|
35204
|
+
scrollToItem(firstSelected, {
|
|
35205
|
+
event: new CustomEvent("navi_displayed", {
|
|
35206
|
+
detail: {
|
|
35207
|
+
originalEvent: openEvent
|
|
35208
|
+
}
|
|
35209
|
+
}),
|
|
35210
|
+
reason: "scroll to selected"
|
|
35211
|
+
});
|
|
35352
35212
|
} else {
|
|
35353
|
-
|
|
35213
|
+
scrollToItem(items[0], {
|
|
35214
|
+
event: new CustomEvent("navi_displayed", {
|
|
35215
|
+
detail: {
|
|
35216
|
+
originalEvent: openEvent
|
|
35217
|
+
}
|
|
35218
|
+
}),
|
|
35219
|
+
reason: "scroll to top (no selected item)"
|
|
35220
|
+
});
|
|
35354
35221
|
}
|
|
35355
|
-
|
|
35356
|
-
|
|
35357
|
-
|
|
35358
|
-
|
|
35222
|
+
}, []);
|
|
35223
|
+
// Watch scores of the top renderBudget items.
|
|
35224
|
+
// When scores change during an active search, scroll to top to reveal the most relevant items.
|
|
35225
|
+
// When search becomes empty, restore the scroll position from before the search started.
|
|
35226
|
+
// We save the first-visible item ID so restoration is item-precise
|
|
35227
|
+
// and survives render-window shifts or item reordering.
|
|
35228
|
+
|
|
35229
|
+
// NOTE POUR LE JOUR OU ON A LE MULTISELECT:
|
|
35230
|
+
// Lorsqu'on selectionne quelque chose pendant une recherche, alors ensuite meme si on clear
|
|
35231
|
+
// on veut pas revenir a la position scroll précédente car on veut garde l'item qu'on a selectionné visible
|
|
35232
|
+
// (pour l'instant pas grave car on travaille pour le mode select qui fermera le dialog au select)
|
|
35233
|
+
const savedScrollRef = useRef(null);
|
|
35234
|
+
const topMatchScoresKeyRef = useRef("");
|
|
35235
|
+
useLayoutEffect(() => {
|
|
35236
|
+
const listScrollContainerEl = ref.current?.querySelector(`.navi_list_scroll_container`);
|
|
35237
|
+
if (!listScrollContainerEl) {
|
|
35238
|
+
return undefined;
|
|
35239
|
+
}
|
|
35240
|
+
if (!searchText) {
|
|
35241
|
+
// no search -> try to restore scroll position
|
|
35242
|
+
topMatchScoresKeyRef.current = "";
|
|
35243
|
+
const savedScroll = savedScrollRef.current;
|
|
35244
|
+
if (!savedScroll) {
|
|
35245
|
+
// nothing to restore
|
|
35246
|
+
return undefined;
|
|
35247
|
+
}
|
|
35248
|
+
savedScrollRef.current = null;
|
|
35249
|
+
debugScroll("Restoring scroll to", savedScroll);
|
|
35250
|
+
updateRenderWindow(savedScroll.renderWindow.start, savedScroll.renderWindow.end, "restore scroll window");
|
|
35251
|
+
const raf = requestAnimationFrame(() => {
|
|
35252
|
+
const left = savedScroll.left;
|
|
35253
|
+
const top = savedScroll.top;
|
|
35254
|
+
// use scrollTo to respect eventual css scroll-behavior: smooth;
|
|
35255
|
+
debugScroll(`restore scroll: ${getElementSignature(listScrollContainerEl)}.scrollTo({ left: ${left}, top: ${top} })`);
|
|
35256
|
+
// The reliable way to restore scroll is to use scrollTop because otherwise we will estimate the item to scroll
|
|
35257
|
+
// based on virtual item height which can wrongly restore the scroll.
|
|
35258
|
+
// However we have a contract with outside to inside which item is scrolled
|
|
35259
|
+
// (used by keyboard nav to enable anchoring the item for list item nav with arrow keys)
|
|
35260
|
+
// so we do our best to give that item back
|
|
35261
|
+
const {
|
|
35262
|
+
item
|
|
35263
|
+
} = getScrollInfo(savedScroll, listScrollContainerEl, tracker, virtualItemSizeSignal, renderWindowRef, horizontal);
|
|
35264
|
+
listScrollContainerEl.scrollTo({
|
|
35265
|
+
left: savedScroll.left,
|
|
35266
|
+
top: savedScroll.top
|
|
35267
|
+
});
|
|
35268
|
+
const listEl = ref.current.querySelector(".navi_list");
|
|
35269
|
+
dispatchPublicCustomEvent(listEl, "navi_scroll", {
|
|
35270
|
+
item,
|
|
35271
|
+
event: new CustomEvent("navi_scroll_restore")
|
|
35272
|
+
});
|
|
35273
|
+
});
|
|
35274
|
+
return () => {
|
|
35275
|
+
cancelAnimationFrame(raf);
|
|
35276
|
+
};
|
|
35277
|
+
}
|
|
35278
|
+
const visibleItems = tracker.visibleItemsSignal.peek();
|
|
35279
|
+
const topItems = visibleItems.slice(0, renderBudget);
|
|
35280
|
+
const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchScore ?? ""}`).join(",");
|
|
35281
|
+
const currentTopMatchScore = topMatchScoresKeyRef.current;
|
|
35282
|
+
if (topMatchScoresKey === currentTopMatchScore) {
|
|
35283
|
+
// no changes in top matches -> no need to scroll
|
|
35284
|
+
return undefined;
|
|
35285
|
+
}
|
|
35286
|
+
// n items are now more important to see, scrollTop to show them
|
|
35287
|
+
topMatchScoresKeyRef.current = topMatchScoresKey;
|
|
35288
|
+
if (searchTextBecomesActive) {
|
|
35289
|
+
// search just started -> save the currently scrolled item id to restore later
|
|
35290
|
+
const currentScroll = currentScrollRef.current;
|
|
35291
|
+
savedScrollRef.current = currentScroll;
|
|
35292
|
+
debugScroll(`Saving scroll: { top: ${currentScroll.top}, renderWindowStart: ${currentScroll.renderWindow.start}, renderWindowEnd: ${currentScroll.renderWindow.end} }`);
|
|
35293
|
+
}
|
|
35294
|
+
// -> scroll to the top
|
|
35295
|
+
scrollToItem(visibleItems[0], {
|
|
35296
|
+
event: new CustomEvent("navi_list_top_match_change")
|
|
35359
35297
|
});
|
|
35360
|
-
|
|
35361
|
-
|
|
35362
|
-
|
|
35363
|
-
|
|
35364
|
-
|
|
35298
|
+
return undefined;
|
|
35299
|
+
});
|
|
35300
|
+
|
|
35301
|
+
// Scroll listener — slides the window as the user scrolls.
|
|
35302
|
+
useLayoutEffect(() => {
|
|
35303
|
+
const listContainerEl = ref.current;
|
|
35304
|
+
if (!listContainerEl) {
|
|
35305
|
+
return undefined;
|
|
35365
35306
|
}
|
|
35366
|
-
const
|
|
35367
|
-
const
|
|
35368
|
-
|
|
35369
|
-
|
|
35370
|
-
|
|
35307
|
+
const listScrollContainerEl = listContainerEl.querySelector(`.navi_list_scroll_container`);
|
|
35308
|
+
const listEl = listContainerEl.querySelector(".navi_list");
|
|
35309
|
+
const onScroll = () => {
|
|
35310
|
+
updateCurrentScroll();
|
|
35311
|
+
const visibleItemCount = tracker.visibleCountSignal.peek();
|
|
35312
|
+
if (visibleItemCount <= renderBudget) {
|
|
35313
|
+
return;
|
|
35371
35314
|
}
|
|
35372
|
-
const
|
|
35373
|
-
if (!
|
|
35374
|
-
|
|
35315
|
+
const oneRealListItemInDom = Boolean(listEl.querySelector(REAL_LIST_ITEM_SELECTOR));
|
|
35316
|
+
if (!oneRealListItemInDom) {
|
|
35317
|
+
return;
|
|
35375
35318
|
}
|
|
35376
|
-
|
|
35319
|
+
let reason = "";
|
|
35320
|
+
const scrollInfo = getScrollInfo({
|
|
35321
|
+
left: listScrollContainerEl.scrollLeft,
|
|
35322
|
+
top: listScrollContainerEl.scrollTop
|
|
35323
|
+
}, listScrollContainerEl, tracker, virtualItemSizeSignal, renderWindowRef, horizontal);
|
|
35324
|
+
if (!scrollInfo) {
|
|
35325
|
+
return;
|
|
35326
|
+
}
|
|
35327
|
+
const {
|
|
35328
|
+
index,
|
|
35329
|
+
reason: hitReason
|
|
35330
|
+
} = scrollInfo;
|
|
35331
|
+
reason = hitReason;
|
|
35332
|
+
const half = Math.floor(renderBudget / 2);
|
|
35333
|
+
let newStart = Math.max(0, index - half);
|
|
35334
|
+
let newEnd = Math.min(visibleItemCount, newStart + renderBudget);
|
|
35335
|
+
if (newEnd === visibleItemCount) {
|
|
35336
|
+
newStart = Math.max(0, visibleItemCount - renderBudget);
|
|
35337
|
+
}
|
|
35338
|
+
updateRenderWindow(newStart, newEnd, reason);
|
|
35339
|
+
};
|
|
35340
|
+
listScrollContainerEl.addEventListener("scroll", onScroll, {
|
|
35341
|
+
passive: true
|
|
35342
|
+
});
|
|
35343
|
+
return () => {
|
|
35344
|
+
listScrollContainerEl.removeEventListener("scroll", onScroll);
|
|
35345
|
+
};
|
|
35346
|
+
}, [renderBudget]);
|
|
35347
|
+
return {
|
|
35348
|
+
virtualItemSizeSignal,
|
|
35349
|
+
renderWindow,
|
|
35350
|
+
pendingScrollRef,
|
|
35351
|
+
scrollToItem
|
|
35352
|
+
};
|
|
35353
|
+
};
|
|
35354
|
+
// Returns the item located at the current scroll position of a list container.
|
|
35355
|
+
// Uses DOM hit-testing to find visible items/fillers; falls back to index
|
|
35356
|
+
// estimation via virtualItemSize or renderWindow.start.
|
|
35357
|
+
// Returns { index, item, reason } or null if nothing can be determined.
|
|
35358
|
+
const getScrollInfo = (scrollValues, listScrollContainerEl, tracker, virtualItemSizeSignal, renderWindowRef, horizontal) => {
|
|
35359
|
+
const listEl = listScrollContainerEl.querySelector(".navi_list");
|
|
35360
|
+
const items = tracker.itemsSignal.peek();
|
|
35361
|
+
const containerRect = listScrollContainerEl.getBoundingClientRect();
|
|
35362
|
+
let hitEl = null;
|
|
35363
|
+
let hitFiller = null;
|
|
35364
|
+
const scrollPos = horizontal ? scrollValues.left : scrollValues.top;
|
|
35365
|
+
// Start scanning from the center of the viewport along the main axis.
|
|
35366
|
+
// The render window places half its budget before and half after the hit index.
|
|
35367
|
+
// Anchoring to the center maximises how many rendered items fall within the
|
|
35368
|
+
// visible area.
|
|
35369
|
+
const scanStart = horizontal ? (containerRect.left + containerRect.right) / 2 : (containerRect.top + containerRect.bottom) / 2;
|
|
35370
|
+
const scanEnd = horizontal ? containerRect.right : containerRect.bottom;
|
|
35371
|
+
for (let pos = scanStart; pos < scanEnd; pos += 4) {
|
|
35372
|
+
const x = horizontal ? pos : containerRect.left + 1;
|
|
35373
|
+
const y = horizontal ? containerRect.top + 1 : pos;
|
|
35374
|
+
const el = document.elementFromPoint(x, y);
|
|
35375
|
+
if (!el || !listEl.contains(el)) {
|
|
35376
|
+
continue;
|
|
35377
35377
|
}
|
|
35378
|
-
|
|
35378
|
+
const realItem = el.closest(REAL_LIST_ITEM_SELECTOR);
|
|
35379
|
+
if (realItem) {
|
|
35380
|
+
hitEl = realItem;
|
|
35381
|
+
break;
|
|
35382
|
+
}
|
|
35383
|
+
const filler = el.closest("[navi-virtual-filler]");
|
|
35384
|
+
if (filler) {
|
|
35385
|
+
hitFiller = filler;
|
|
35386
|
+
break;
|
|
35387
|
+
}
|
|
35388
|
+
}
|
|
35389
|
+
if (hitFiller) {
|
|
35390
|
+
const virtualItemSize = virtualItemSizeSignal.peek();
|
|
35391
|
+
if (virtualItemSize === 0) {
|
|
35392
|
+
return null;
|
|
35393
|
+
}
|
|
35394
|
+
const estimatedIndex = Math.floor(scrollPos / virtualItemSize);
|
|
35395
|
+
const index = Math.min(items.length - 1, estimatedIndex);
|
|
35396
|
+
return {
|
|
35397
|
+
item: items[index],
|
|
35398
|
+
index,
|
|
35399
|
+
reason: `hit filler, estimated at ${index} (${items[index]?.value})`
|
|
35400
|
+
};
|
|
35401
|
+
}
|
|
35402
|
+
if (hitEl) {
|
|
35403
|
+
const hitId = hitEl.id;
|
|
35404
|
+
const index = items.findIndex(i => i.id === hitId);
|
|
35405
|
+
if (index === -1) {
|
|
35406
|
+
return null;
|
|
35407
|
+
}
|
|
35408
|
+
return {
|
|
35409
|
+
item: items[index],
|
|
35410
|
+
index,
|
|
35411
|
+
reason: `hit item at ${index} (${items[index].value})`
|
|
35412
|
+
};
|
|
35413
|
+
}
|
|
35414
|
+
const fallbackIndex = renderWindowRef.current.start;
|
|
35415
|
+
return {
|
|
35416
|
+
item: items[fallbackIndex],
|
|
35417
|
+
index: fallbackIndex,
|
|
35418
|
+
reason: "no hit"
|
|
35379
35419
|
};
|
|
35380
|
-
|
|
35381
|
-
|
|
35420
|
+
};
|
|
35421
|
+
const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal) => {
|
|
35422
|
+
const virtualSizeSignalRef = useRef(null);
|
|
35423
|
+
if (!virtualSizeSignalRef.current) {
|
|
35424
|
+
virtualSizeSignalRef.current = signal(virtualItemSizeProp);
|
|
35425
|
+
}
|
|
35426
|
+
const virtualSizeSignal = virtualSizeSignalRef.current;
|
|
35427
|
+
// propagate prop changes to the signal
|
|
35428
|
+
if (virtualItemSizeProp && virtualSizeSignal.peek() !== virtualItemSizeProp) {
|
|
35429
|
+
virtualSizeSignal.value = virtualItemSizeProp;
|
|
35430
|
+
}
|
|
35382
35431
|
useLayoutEffect(() => {
|
|
35383
|
-
|
|
35384
|
-
if (navigableEls.length === 0) {
|
|
35432
|
+
if (virtualSizeSignal.peek() !== 0) {
|
|
35385
35433
|
return;
|
|
35386
35434
|
}
|
|
35387
|
-
|
|
35388
|
-
|
|
35389
|
-
|
|
35390
|
-
if (realInput && realInput.checked) {
|
|
35391
|
-
initialEl = el;
|
|
35392
|
-
break;
|
|
35393
|
-
}
|
|
35435
|
+
const listEl = ref.current?.querySelector(".navi_list");
|
|
35436
|
+
if (!listEl) {
|
|
35437
|
+
return;
|
|
35394
35438
|
}
|
|
35395
|
-
|
|
35396
|
-
|
|
35439
|
+
const firstListItem = listEl.querySelector(REAL_LIST_ITEM_SELECTOR);
|
|
35440
|
+
if (!firstListItem) {
|
|
35441
|
+
return;
|
|
35442
|
+
}
|
|
35443
|
+
const rect = firstListItem.getBoundingClientRect();
|
|
35444
|
+
const measuredSize = horizontal ? rect.width : rect.height;
|
|
35445
|
+
virtualSizeSignal.value = measuredSize;
|
|
35446
|
+
});
|
|
35447
|
+
return virtualSizeSignal;
|
|
35448
|
+
};
|
|
35449
|
+
|
|
35450
|
+
// Inner <ul> — hosts the fillers + items.
|
|
35451
|
+
// Creates a virtualItemSize signal so BeforeFiller and AfterFiller can
|
|
35452
|
+
// subscribe to it independently. When virtualItemSize is passed as a prop it
|
|
35453
|
+
// initialises the signal directly; otherwise UnorderedList measures a rendered
|
|
35454
|
+
// item after each commit and writes to the signal, causing only the fillers to
|
|
35455
|
+
// re-render.
|
|
35456
|
+
const UnorderedList = ({
|
|
35457
|
+
tracker,
|
|
35458
|
+
renderWindow,
|
|
35459
|
+
virtualItemSizeSignal,
|
|
35460
|
+
fallback,
|
|
35461
|
+
noMatchFallback,
|
|
35462
|
+
searchText,
|
|
35463
|
+
separator,
|
|
35464
|
+
horizontal,
|
|
35465
|
+
spacing,
|
|
35466
|
+
children,
|
|
35467
|
+
...rest
|
|
35468
|
+
}) => {
|
|
35469
|
+
return jsxs(Box, {
|
|
35470
|
+
as: "ul",
|
|
35471
|
+
...rest,
|
|
35472
|
+
flex: horizontal ? "x" : "y",
|
|
35473
|
+
spacing: spacing,
|
|
35474
|
+
baseClassName: "navi_list",
|
|
35475
|
+
children: [jsx(BeforeFiller, {
|
|
35476
|
+
virtualItemSizeSignal: virtualItemSizeSignal,
|
|
35477
|
+
renderWindowStart: renderWindow.start
|
|
35478
|
+
}), jsx(NoMatchFallback, {
|
|
35479
|
+
noMatchFallback: noMatchFallback,
|
|
35480
|
+
tracker: tracker,
|
|
35481
|
+
searchText: searchText
|
|
35482
|
+
}), jsx(Fallback, {
|
|
35483
|
+
fallback: fallback,
|
|
35484
|
+
tracker: tracker
|
|
35485
|
+
}), jsx(RenderWindowContext.Provider, {
|
|
35486
|
+
value: renderWindow,
|
|
35487
|
+
children: jsx(SeparatorContext.Provider, {
|
|
35488
|
+
value: separator ?? null,
|
|
35489
|
+
children: jsx(ListItemTrackerContext.Provider, {
|
|
35490
|
+
value: tracker,
|
|
35491
|
+
children: children
|
|
35492
|
+
})
|
|
35493
|
+
})
|
|
35494
|
+
}), jsx(AfterFiller, {
|
|
35495
|
+
virtualItemSizeSignal: virtualItemSizeSignal,
|
|
35496
|
+
renderWindowEnd: renderWindow.end,
|
|
35497
|
+
tracker: tracker
|
|
35498
|
+
})]
|
|
35499
|
+
});
|
|
35500
|
+
};
|
|
35501
|
+
const NoMatchFallback = ({
|
|
35502
|
+
tracker,
|
|
35503
|
+
noMatchFallback,
|
|
35504
|
+
searchText
|
|
35505
|
+
}) => {
|
|
35506
|
+
const itemCount = tracker.countSignal.value;
|
|
35507
|
+
const visibleItemCount = tracker.visibleCountSignal.value;
|
|
35508
|
+
const matchCount = tracker.matchCountSignal.value;
|
|
35509
|
+
// Show when all items are filtered out (filtered prop), or when search is
|
|
35510
|
+
// active but no visible item has a positive match score.
|
|
35511
|
+
const allHidden = itemCount > 0 && visibleItemCount === 0;
|
|
35512
|
+
const noneMatch = searchText && visibleItemCount > 0 && matchCount === 0;
|
|
35513
|
+
const showMatchFallback = allHidden || noneMatch;
|
|
35514
|
+
if (noMatchFallback === undefined) {
|
|
35515
|
+
noMatchFallback = allHidden ? naviI18n("list.no_match") : naviI18n("list.no_match_rest_shown");
|
|
35516
|
+
}
|
|
35517
|
+
if (!showMatchFallback) {
|
|
35518
|
+
return null;
|
|
35519
|
+
}
|
|
35520
|
+
return jsx(ListItem, {
|
|
35521
|
+
role: "presentation",
|
|
35522
|
+
className: "navi_list_item navi_list_no_match_fallback",
|
|
35523
|
+
hidden: !showMatchFallback,
|
|
35524
|
+
"navi-default": typeof noMatchFallback === "string" ? "" : undefined,
|
|
35525
|
+
children: noMatchFallback
|
|
35526
|
+
});
|
|
35527
|
+
};
|
|
35528
|
+
const Fallback = ({
|
|
35529
|
+
tracker,
|
|
35530
|
+
fallback
|
|
35531
|
+
}) => {
|
|
35532
|
+
const itemCount = tracker.countSignal.value;
|
|
35533
|
+
const showFallback = itemCount === 0;
|
|
35534
|
+
if (fallback === undefined) {
|
|
35535
|
+
fallback = naviI18n("list.empty");
|
|
35536
|
+
}
|
|
35537
|
+
if (!showFallback) {
|
|
35538
|
+
return null;
|
|
35539
|
+
}
|
|
35540
|
+
return jsx(ListItem, {
|
|
35541
|
+
role: "presentation",
|
|
35542
|
+
className: "navi_list_item navi_list_fallback",
|
|
35543
|
+
hidden: !showFallback,
|
|
35544
|
+
"navi-default": typeof fallback === "string" ? "" : undefined,
|
|
35545
|
+
children: fallback
|
|
35546
|
+
});
|
|
35547
|
+
};
|
|
35548
|
+
const BeforeFiller = ({
|
|
35549
|
+
virtualItemSizeSignal,
|
|
35550
|
+
renderWindowStart
|
|
35551
|
+
}) => {
|
|
35552
|
+
const virtualItemSize = virtualItemSizeSignal.value;
|
|
35553
|
+
const numberOfItemsBefore = renderWindowStart;
|
|
35554
|
+
const sizeToFillBefore = numberOfItemsBefore * virtualItemSize;
|
|
35555
|
+
if (!sizeToFillBefore) {
|
|
35556
|
+
return null;
|
|
35557
|
+
}
|
|
35558
|
+
return jsx("li", {
|
|
35559
|
+
className: "navi_list_virtual_filler"
|
|
35560
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
35561
|
+
,
|
|
35562
|
+
|
|
35563
|
+
"navi-virtual-filler": "before",
|
|
35564
|
+
"aria-hidden": true,
|
|
35565
|
+
style: {
|
|
35566
|
+
"--size-to-fill": `${sizeToFillBefore}px`
|
|
35397
35567
|
}
|
|
35398
|
-
|
|
35399
|
-
|
|
35400
|
-
|
|
35401
|
-
|
|
35402
|
-
|
|
35403
|
-
|
|
35404
|
-
|
|
35405
|
-
|
|
35406
|
-
|
|
35407
|
-
|
|
35408
|
-
|
|
35568
|
+
});
|
|
35569
|
+
};
|
|
35570
|
+
const AfterFiller = ({
|
|
35571
|
+
virtualItemSizeSignal,
|
|
35572
|
+
renderWindowEnd,
|
|
35573
|
+
tracker
|
|
35574
|
+
}) => {
|
|
35575
|
+
const visibleItemCount = tracker.visibleCountSignal.value;
|
|
35576
|
+
const virtualItemSize = virtualItemSizeSignal.value;
|
|
35577
|
+
const numberOfItemsAfter = Math.max(visibleItemCount - renderWindowEnd, 0);
|
|
35578
|
+
const sizeToFillAfter = numberOfItemsAfter * virtualItemSize;
|
|
35579
|
+
if (!sizeToFillAfter) {
|
|
35580
|
+
return null;
|
|
35581
|
+
}
|
|
35582
|
+
return jsx("li", {
|
|
35583
|
+
className: "navi_list_virtual_filler"
|
|
35584
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
35409
35585
|
,
|
|
35410
35586
|
|
|
35411
|
-
|
|
35412
|
-
|
|
35413
|
-
|
|
35414
|
-
|
|
35415
|
-
|
|
35416
|
-
const itemEl = realInput.closest("[navi-list-item-real]");
|
|
35417
|
-
if (itemEl && itemEl.id) {
|
|
35418
|
-
setCurrentId(itemEl.id, e);
|
|
35419
|
-
}
|
|
35420
|
-
},
|
|
35421
|
-
onnavi_request_select: e => {
|
|
35422
|
-
const {
|
|
35423
|
-
id
|
|
35424
|
-
} = e.detail;
|
|
35425
|
-
if (id === undefined) {
|
|
35426
|
-
return;
|
|
35427
|
-
}
|
|
35428
|
-
const inputId = `${id}_input`;
|
|
35429
|
-
const childController = uiGroupStateController.findChildById(inputId);
|
|
35430
|
-
if (!childController) {
|
|
35431
|
-
return;
|
|
35432
|
-
}
|
|
35433
|
-
const list = ref.current;
|
|
35434
|
-
const allowed = dispatchRequestInteraction(list, e, "select");
|
|
35435
|
-
if (!allowed) {
|
|
35436
|
-
e.preventDefault();
|
|
35437
|
-
return;
|
|
35438
|
-
}
|
|
35439
|
-
if (childController.setUIState(true, e)) {
|
|
35440
|
-
dispatchRequestAction(list, {
|
|
35441
|
-
event: e
|
|
35442
|
-
});
|
|
35443
|
-
}
|
|
35444
|
-
},
|
|
35445
|
-
onnavi_request_unselect: e => {
|
|
35446
|
-
const {
|
|
35447
|
-
id
|
|
35448
|
-
} = e.detail;
|
|
35449
|
-
if (id === undefined) {
|
|
35450
|
-
return;
|
|
35451
|
-
}
|
|
35452
|
-
const inputId = `${id}_input`;
|
|
35453
|
-
const childController = uiGroupStateController.findChildById(inputId);
|
|
35454
|
-
if (!childController) {
|
|
35455
|
-
return;
|
|
35456
|
-
}
|
|
35457
|
-
const list = ref.current;
|
|
35458
|
-
const allowed = dispatchRequestInteraction(list, e, "unselect");
|
|
35459
|
-
if (!allowed) {
|
|
35460
|
-
e.preventDefault();
|
|
35461
|
-
return;
|
|
35462
|
-
}
|
|
35463
|
-
if (childController.setUIState(false, e)) {
|
|
35464
|
-
dispatchRequestAction(list, {
|
|
35465
|
-
event: e
|
|
35466
|
-
});
|
|
35467
|
-
}
|
|
35468
|
-
},
|
|
35469
|
-
onnavi_request_nav: e => {
|
|
35470
|
-
const {
|
|
35471
|
-
goal
|
|
35472
|
-
} = e.detail;
|
|
35473
|
-
const navigableEls = getNavigableElements();
|
|
35474
|
-
if (navigableEls.length === 0) {
|
|
35475
|
-
return;
|
|
35476
|
-
}
|
|
35477
|
-
const currentId = currentIdRef.current;
|
|
35478
|
-
let currentIndex = -1;
|
|
35479
|
-
if (currentId) {
|
|
35480
|
-
currentIndex = navigableEls.findIndex(el => el.id === currentId);
|
|
35481
|
-
}
|
|
35482
|
-
let targetEl;
|
|
35483
|
-
if (goal === "first") {
|
|
35484
|
-
targetEl = navigableEls[0];
|
|
35485
|
-
} else if (goal === "last") {
|
|
35486
|
-
targetEl = navigableEls[navigableEls.length - 1];
|
|
35487
|
-
} else if (goal === "down") {
|
|
35488
|
-
if (currentIndex === -1) {
|
|
35489
|
-
targetEl = navigableEls[0];
|
|
35490
|
-
} else if (currentIndex < navigableEls.length - 1) {
|
|
35491
|
-
targetEl = navigableEls[currentIndex + 1];
|
|
35492
|
-
} else {
|
|
35493
|
-
targetEl = navigableEls[navigableEls.length - 1];
|
|
35494
|
-
}
|
|
35495
|
-
} else if (goal === "up") {
|
|
35496
|
-
if (currentIndex === -1) {
|
|
35497
|
-
targetEl = navigableEls[0];
|
|
35498
|
-
} else if (currentIndex > 0) {
|
|
35499
|
-
targetEl = navigableEls[currentIndex - 1];
|
|
35500
|
-
} else {
|
|
35501
|
-
targetEl = navigableEls[0];
|
|
35502
|
-
}
|
|
35503
|
-
}
|
|
35504
|
-
if (!targetEl) {
|
|
35505
|
-
return;
|
|
35506
|
-
}
|
|
35507
|
-
setCurrentId(targetEl.id, e);
|
|
35508
|
-
dispatchCustomEvent(ref.current, "navi_request_scroll", {
|
|
35509
|
-
event: e,
|
|
35510
|
-
id: targetEl.id
|
|
35511
|
-
});
|
|
35512
|
-
},
|
|
35513
|
-
onnavi_request_activate: e => {
|
|
35514
|
-
const currentId = currentIdRef.current;
|
|
35515
|
-
if (!currentId) {
|
|
35516
|
-
return;
|
|
35517
|
-
}
|
|
35518
|
-
if (multiple) {
|
|
35519
|
-
const inputId = `${currentId}_input`;
|
|
35520
|
-
const childController = uiGroupStateController.findChildById(inputId);
|
|
35521
|
-
const isSelected = childController && childController.uiState;
|
|
35522
|
-
dispatchCustomEvent(ref.current, isSelected ? "navi_request_unselect" : "navi_request_select", {
|
|
35523
|
-
event: e,
|
|
35524
|
-
id: currentId
|
|
35525
|
-
});
|
|
35526
|
-
return;
|
|
35527
|
-
}
|
|
35528
|
-
dispatchCustomEvent(ref.current, "navi_request_select", {
|
|
35529
|
-
event: e,
|
|
35530
|
-
id: currentId
|
|
35531
|
-
});
|
|
35532
|
-
},
|
|
35533
|
-
children: jsx(ControlgroupChildrenWrapper, {
|
|
35534
|
-
...childrenWrapperProps,
|
|
35535
|
-
children: props.children
|
|
35536
|
-
})
|
|
35587
|
+
"navi-virtual-filler": "after",
|
|
35588
|
+
"aria-hidden": true,
|
|
35589
|
+
style: {
|
|
35590
|
+
"--size-to-fill": `${sizeToFillAfter}px`
|
|
35591
|
+
}
|
|
35537
35592
|
});
|
|
35538
|
-
|
|
35539
|
-
|
|
35540
|
-
|
|
35593
|
+
};
|
|
35594
|
+
const ListItemFirstResolver = props => {
|
|
35595
|
+
const Next = useNextResolver();
|
|
35596
|
+
const defaultRef = useRef(null);
|
|
35597
|
+
props.ref = props.ref || defaultRef;
|
|
35598
|
+
return jsx(Next, {
|
|
35599
|
+
...props
|
|
35541
35600
|
});
|
|
35542
35601
|
};
|
|
35543
|
-
const
|
|
35544
|
-
const
|
|
35602
|
+
const ListItemPresentationResolver = props => {
|
|
35603
|
+
const Next = useNextResolver();
|
|
35604
|
+
if (props.role === "presentation") {
|
|
35605
|
+
return jsx(ListItemPresentation, {
|
|
35606
|
+
...props
|
|
35607
|
+
});
|
|
35608
|
+
}
|
|
35609
|
+
return jsx(Next, {
|
|
35610
|
+
...props
|
|
35611
|
+
});
|
|
35612
|
+
};
|
|
35613
|
+
const ListItemPresentation = props => {
|
|
35614
|
+
return jsx(Box, {
|
|
35615
|
+
as: "li",
|
|
35616
|
+
...props
|
|
35617
|
+
});
|
|
35618
|
+
};
|
|
35619
|
+
const ListItemUI = props => {
|
|
35620
|
+
if (props.id === undefined) {
|
|
35621
|
+
console.warn("ListItem is missing an explicit id prop. Provide a stable id so pointed/selected state survives search reordering.");
|
|
35622
|
+
}
|
|
35623
|
+
if (props.index === undefined) {
|
|
35624
|
+
console.warn("ListItem is missing an explicit index prop. Provide an index so item ordering is stable regardless of render order.");
|
|
35625
|
+
}
|
|
35626
|
+
const idDefault = useId();
|
|
35627
|
+
props.id = props.id || idDefault;
|
|
35628
|
+
const renderWindow = useContext(RenderWindowContext);
|
|
35629
|
+
const tracker = useContext(ListItemTrackerContext);
|
|
35630
|
+
const item = props;
|
|
35631
|
+
const visibleIndex = tracker.useTrackItem(item);
|
|
35632
|
+
const groupTracker = useContext(GroupItemTrackerContext);
|
|
35633
|
+
const groupVisibleIndex = groupTracker ? groupTracker.useTrackItem(item) : null;
|
|
35634
|
+
const separator = useContext(SeparatorContext);
|
|
35635
|
+
if (props.filtered) {
|
|
35636
|
+
return null;
|
|
35637
|
+
}
|
|
35638
|
+
// html-hidden items: excluded from virtual scroll accounting but always in DOM
|
|
35639
|
+
if (props.hidden) {
|
|
35640
|
+
return jsx(ListItemReal, {
|
|
35641
|
+
...props
|
|
35642
|
+
});
|
|
35643
|
+
}
|
|
35644
|
+
if (visibleIndex === -1) {
|
|
35645
|
+
return null;
|
|
35646
|
+
}
|
|
35647
|
+
if (visibleIndex < renderWindow.start || visibleIndex >= renderWindow.end) {
|
|
35648
|
+
return jsx(ListItemVoid, {});
|
|
35649
|
+
}
|
|
35650
|
+
const listItemVnode = jsx(ListItemReal, {
|
|
35651
|
+
...props
|
|
35652
|
+
});
|
|
35653
|
+
// For separator decision, we need to know "am I the first visible item?".
|
|
35654
|
+
// We deliberately do NOT use tracker's visibleIndex here because, during a
|
|
35655
|
+
// reorder render pass (e.g. items resorted by search score), other items
|
|
35656
|
+
// still have stale keyToExplicitOrder values — the binary search reads
|
|
35657
|
+
// those stale values and computes wrong indices. The result is that no
|
|
35658
|
+
// item gets visibleIndex === 0 and a spurious <hr> appears at the top.
|
|
35659
|
+
//
|
|
35660
|
+
// Instead we use the parent-provided index, which is race-free:
|
|
35661
|
+
// - global list: props.index === 0 means "first by explicit order"
|
|
35662
|
+
// (parent passes sequential indices starting at 0; filtered items
|
|
35663
|
+
// are already pushed to the end by useSearchText)
|
|
35664
|
+
// - inside a group: each group has its own item tracker and group
|
|
35665
|
+
// items don't reorder, so groupVisibleIndex is reliable
|
|
35666
|
+
const isFirstInList = groupVisibleIndex === null ? props.index === 0 : groupVisibleIndex === 0;
|
|
35667
|
+
if (!separator || isFirstInList) {
|
|
35668
|
+
return listItemVnode;
|
|
35669
|
+
}
|
|
35670
|
+
// separatorIndex is only used as the function-form argument (gap index)
|
|
35671
|
+
const separatorIndex = groupVisibleIndex === null ? visibleIndex : groupVisibleIndex;
|
|
35672
|
+
const separatorVnode = typeof separator === "function" ? separator(separatorIndex - 1) : separator;
|
|
35673
|
+
return jsxs(Fragment$1, {
|
|
35674
|
+
children: [separatorVnode, listItemVnode]
|
|
35675
|
+
});
|
|
35676
|
+
};
|
|
35677
|
+
// When an item is outside the render window it cannot render a DOM node.
|
|
35678
|
+
// If it wants to scroll into view it sets scrollTop so the scroll event
|
|
35679
|
+
// shifts the window; once the item mounts as ListItemReal its layout effect
|
|
35680
|
+
// calls scrollIntoViewWithStickyAwareness to fine-tune the position.
|
|
35681
|
+
const ListItemVoid = () => {
|
|
35682
|
+
return null;
|
|
35683
|
+
};
|
|
35684
|
+
const ListItemReal = props => {
|
|
35545
35685
|
const {
|
|
35546
|
-
|
|
35686
|
+
ref,
|
|
35547
35687
|
id,
|
|
35548
|
-
highlight,
|
|
35549
35688
|
hidden,
|
|
35550
|
-
|
|
35551
|
-
|
|
35552
|
-
defaultSelected,
|
|
35553
|
-
selected,
|
|
35554
|
-
pointed,
|
|
35555
|
-
selectableArea,
|
|
35689
|
+
highlight,
|
|
35690
|
+
children,
|
|
35556
35691
|
...rest
|
|
35557
35692
|
} = props;
|
|
35558
|
-
const
|
|
35559
|
-
const
|
|
35560
|
-
const
|
|
35561
|
-
|
|
35562
|
-
|
|
35563
|
-
|
|
35564
|
-
readOnlyMessage: naviI18n(`constraints.readonly.option`, props),
|
|
35565
|
-
...rest,
|
|
35566
|
-
ref: inputRef,
|
|
35567
|
-
id: inputId,
|
|
35568
|
-
type: inputType,
|
|
35569
|
-
defaultChecked: defaultSelected,
|
|
35570
|
-
checked: selected,
|
|
35571
|
-
action: (v, {
|
|
35572
|
-
event
|
|
35573
|
-
}) => {
|
|
35574
|
-
const listContainerEl = event.currentTarget.closest(".navi_list_container");
|
|
35575
|
-
dispatchRequestAction(listContainerEl, {
|
|
35576
|
-
event
|
|
35577
|
-
});
|
|
35693
|
+
const pendingScrollRef = useContext(PendingScrollRefContext);
|
|
35694
|
+
const pendingScroll = pendingScrollRef.current;
|
|
35695
|
+
const needScrollOnMount = pendingScroll && pendingScroll.id === id;
|
|
35696
|
+
useLayoutEffect(() => {
|
|
35697
|
+
if (!needScrollOnMount) {
|
|
35698
|
+
return;
|
|
35578
35699
|
}
|
|
35579
|
-
|
|
35580
|
-
|
|
35581
|
-
|
|
35582
|
-
|
|
35583
|
-
|
|
35584
|
-
|
|
35585
|
-
|
|
35586
|
-
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
|
|
35590
|
-
|
|
35591
|
-
|
|
35592
|
-
|
|
35593
|
-
|
|
35594
|
-
readOnly,
|
|
35595
|
-
value
|
|
35596
|
-
};
|
|
35597
|
-
}, [inputId, inputType, checked, readOnly, value]);
|
|
35598
|
-
return jsx(ListItem, {
|
|
35700
|
+
const itemEl = ref.current;
|
|
35701
|
+
if (!itemEl) {
|
|
35702
|
+
return;
|
|
35703
|
+
}
|
|
35704
|
+
pendingScroll.resolve(itemEl);
|
|
35705
|
+
}, [needScrollOnMount]);
|
|
35706
|
+
|
|
35707
|
+
// CSS Highlight API: mark matching text ranges when highlight prop is set.
|
|
35708
|
+
useSearchHighlight(ref, highlight, [children, hidden]);
|
|
35709
|
+
return jsx(Box, {
|
|
35710
|
+
as: "li",
|
|
35711
|
+
baseClassName: "navi_list_item",
|
|
35712
|
+
styleCSSVars: LIST_ITEM_STYLE_CSS_VARS,
|
|
35713
|
+
pseudoClasses: LIST_ITEM_PSEUDO_CLASSES,
|
|
35714
|
+
pseudoElements: LIST_ITEM_PSEUDO_ELEMENTS,
|
|
35599
35715
|
id: id,
|
|
35600
|
-
|
|
35601
|
-
|
|
35602
|
-
|
|
35716
|
+
"navi-list-item-real": "",
|
|
35717
|
+
...rest,
|
|
35718
|
+
index: undefined,
|
|
35719
|
+
selected: undefined,
|
|
35720
|
+
matchScore: undefined,
|
|
35603
35721
|
hidden: hidden,
|
|
35604
|
-
|
|
35605
|
-
|
|
35606
|
-
basePseudoState: {
|
|
35607
|
-
":-navi-selected": checked,
|
|
35608
|
-
":-navi-pointed": pointed,
|
|
35609
|
-
...basePseudoState
|
|
35610
|
-
},
|
|
35611
|
-
"aria-selected": checked,
|
|
35612
|
-
selected: checked,
|
|
35613
|
-
"navi-selectable": "",
|
|
35614
|
-
children: jsxs(Field, {
|
|
35615
|
-
as: selectableArea === "manual" ? "div" : undefined,
|
|
35616
|
-
padding: "m",
|
|
35617
|
-
flex: true,
|
|
35618
|
-
alignY: "center",
|
|
35619
|
-
spacing: "s",
|
|
35620
|
-
expandX: true,
|
|
35621
|
-
...remainingProps,
|
|
35622
|
-
selectableArea: undefined,
|
|
35623
|
-
basePseudoState: basePseudoState,
|
|
35624
|
-
pseudoStateSelector: "[navi-selectable-real-input]",
|
|
35625
|
-
disabled: disabled,
|
|
35626
|
-
readOnly: readOnly,
|
|
35627
|
-
loading: loading,
|
|
35628
|
-
interactive: true,
|
|
35629
|
-
children: [jsx(SelectableRealInput, {
|
|
35630
|
-
...checkableProps,
|
|
35631
|
-
// eslint-disable-next-line react/no-children-prop
|
|
35632
|
-
children: undefined
|
|
35633
|
-
}), jsx(SelectableRealInputContext.Provider, {
|
|
35634
|
-
value: realInputContextValue,
|
|
35635
|
-
children: jsx(ChildrenContextWrapper, {
|
|
35636
|
-
children: children
|
|
35637
|
-
})
|
|
35638
|
-
})]
|
|
35639
|
-
})
|
|
35640
|
-
});
|
|
35641
|
-
};
|
|
35642
|
-
const SELECTABLE_PSEUDO_CLASSES = [...LIST_ITEM_PSEUDO_CLASSES, ":hover", ":disabled", ":read-only", ":focus-within", ":focus", ":focus-visible", ":-navi-loading", ":-navi-pointed", ":-navi-selected", ":disabled", ":read-only"];
|
|
35643
|
-
const SelectableRealInput = props => {
|
|
35644
|
-
return jsx(Box, {
|
|
35645
|
-
as: "input",
|
|
35646
|
-
...props,
|
|
35647
|
-
"navi-selectable-real-input": "",
|
|
35648
|
-
"navi-visually-hidden": "",
|
|
35649
|
-
"data-callout-arrow-x": "center"
|
|
35650
|
-
// navi-debug
|
|
35722
|
+
ref: ref,
|
|
35723
|
+
children: children
|
|
35651
35724
|
});
|
|
35652
35725
|
};
|
|
35653
|
-
const
|
|
35654
|
-
|
|
35655
|
-
|
|
35656
|
-
|
|
35726
|
+
const LIST_ITEM_STYLE_CSS_VARS = {
|
|
35727
|
+
"paddingX": "--list-item-padding-x",
|
|
35728
|
+
"paddingY": "--list-item-padding-y",
|
|
35729
|
+
"padding": "--list-item-padding",
|
|
35730
|
+
"color": "--list-item-color",
|
|
35731
|
+
"backgroundColor": "--list-item-background-color",
|
|
35732
|
+
"fontWeight": "--list-item-font-weight",
|
|
35733
|
+
"borderWidth": "--list-item-border-width",
|
|
35734
|
+
"borderColor": "--list-item-border-color",
|
|
35735
|
+
":-navi-pointed": {
|
|
35736
|
+
color: "--list-item-color-keyboard-pointed",
|
|
35737
|
+
backgroundColor: "--list-item-background-color-keyboard-pointed"
|
|
35738
|
+
},
|
|
35739
|
+
":hover": {
|
|
35740
|
+
color: "--list-item-color-hover",
|
|
35741
|
+
backgroundColor: "--list-item-background-color-hover"
|
|
35742
|
+
},
|
|
35743
|
+
":-navi-selected": {
|
|
35744
|
+
color: "--list-item-color-selected",
|
|
35745
|
+
backgroundColor: "--list-item-background-color-selected",
|
|
35746
|
+
borderColor: "--list-item-border-color-selected"
|
|
35747
|
+
},
|
|
35748
|
+
":disabled": {
|
|
35749
|
+
color: "--list-item-color-disabled",
|
|
35750
|
+
backgroundColor: "--list-item-background-color-disabled"
|
|
35751
|
+
},
|
|
35752
|
+
"::highlight": {
|
|
35753
|
+
color: "--suggestion-color-highlight",
|
|
35754
|
+
backgroundColor: "--suggestion-background-color-highlight"
|
|
35657
35755
|
}
|
|
35756
|
+
};
|
|
35757
|
+
const LIST_ITEM_PSEUDO_CLASSES = [];
|
|
35758
|
+
const LIST_ITEM_PSEUDO_ELEMENTS = ["::highlight"];
|
|
35658
35759
|
|
|
35659
|
-
|
|
35660
|
-
|
|
35661
|
-
|
|
35662
|
-
|
|
35663
|
-
|
|
35664
|
-
|
|
35665
|
-
|
|
35666
|
-
|
|
35667
|
-
|
|
35668
|
-
|
|
35669
|
-
|
|
35760
|
+
/**
|
|
35761
|
+
* ListItem — a trackable item that participates in virtualization.
|
|
35762
|
+
*
|
|
35763
|
+
* Must be used inside <List>. Handles:
|
|
35764
|
+
* - Registration with item tracker (always runs, even when hidden)
|
|
35765
|
+
* - Early return when outside the render window
|
|
35766
|
+
* - Separator rendering between visible items
|
|
35767
|
+
*
|
|
35768
|
+
* Props:
|
|
35769
|
+
* itemId — stable string id for tracking (auto-generated if omitted)
|
|
35770
|
+
* filtered — when true, item is excluded from visible count and removed from DOM entirely
|
|
35771
|
+
* hidden — when true, item is excluded from visible count (no virtual scroll height)
|
|
35772
|
+
* but stays in DOM with the native HTML hidden attribute
|
|
35773
|
+
* highlight — array of [start, end] ranges to highlight via CSS Highlight API
|
|
35774
|
+
* ...rest — forwarded to the rendered <li> element
|
|
35775
|
+
*/
|
|
35776
|
+
const ListItem = createComponentResolver([ListItemFirstResolver, ListItemSelectableResolver, ListItemHeaderOrFooterResolver, ListItemPresentationResolver, ListItemUI]);
|
|
35777
|
+
List.Item = ListItem;
|
|
35778
|
+
|
|
35779
|
+
/**
|
|
35780
|
+
* ListGroup — a labeled group of list items.
|
|
35781
|
+
*
|
|
35782
|
+
* Renders a <li role="presentation"> wrapper containing a label span
|
|
35783
|
+
* (accessible via aria-labelledby) and a <ul role="group"> for the items.
|
|
35784
|
+
*
|
|
35785
|
+
* Props:
|
|
35786
|
+
* label — group label content
|
|
35787
|
+
* labelProps — props forwarded to the label <span>
|
|
35788
|
+
* ...rest — forwarded to the outer <li role="presentation">
|
|
35789
|
+
*/
|
|
35790
|
+
const ListItemGroup = ({
|
|
35791
|
+
label,
|
|
35792
|
+
hiddenWhileEmpty,
|
|
35793
|
+
children,
|
|
35794
|
+
...rest
|
|
35795
|
+
}) => {
|
|
35796
|
+
const groupId = useId();
|
|
35797
|
+
const groupTracker = useItemTracker();
|
|
35798
|
+
const groupRef = useRef(null);
|
|
35799
|
+
const labelRef = useRef(null);
|
|
35800
|
+
useDisplayedLayoutEffect(labelRef, labelEl => {
|
|
35801
|
+
const groupEl = groupRef.current;
|
|
35802
|
+
if (!groupEl) {
|
|
35803
|
+
return;
|
|
35804
|
+
}
|
|
35805
|
+
const rect = labelEl.getBoundingClientRect();
|
|
35806
|
+
groupEl.style.setProperty("--list-group-label-height", `${rect.height}px`);
|
|
35807
|
+
groupEl.style.setProperty("--list-group-label-width", `${rect.width}px`);
|
|
35808
|
+
}, []);
|
|
35809
|
+
return jsxs(ListItem, {
|
|
35810
|
+
...rest,
|
|
35811
|
+
ref: groupRef,
|
|
35812
|
+
baseClassName: "navi_list_item_group",
|
|
35813
|
+
role: "presentation",
|
|
35814
|
+
"data-hidden-while-empty": hiddenWhileEmpty ? "" : undefined,
|
|
35815
|
+
children: [jsx("span", {
|
|
35816
|
+
ref: labelRef,
|
|
35817
|
+
id: groupId,
|
|
35818
|
+
className: "navi_list_item_group_label",
|
|
35819
|
+
role: "presentation"
|
|
35820
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
35670
35821
|
,
|
|
35671
35822
|
|
|
35672
|
-
|
|
35673
|
-
|
|
35674
|
-
|
|
35675
|
-
|
|
35823
|
+
"navi-default": typeof label === "string" ? "" : undefined,
|
|
35824
|
+
children: label
|
|
35825
|
+
}), jsx("ul", {
|
|
35826
|
+
className: "navi_list_item_group_list",
|
|
35827
|
+
role: "group",
|
|
35828
|
+
"aria-labelledby": groupId,
|
|
35829
|
+
children: jsx(GroupItemTrackerContext.Provider, {
|
|
35830
|
+
value: groupTracker,
|
|
35831
|
+
children: children
|
|
35832
|
+
})
|
|
35833
|
+
})]
|
|
35676
35834
|
});
|
|
35677
35835
|
};
|
|
35678
|
-
Selectable.Input = SelectableInputProxy;
|
|
35679
35836
|
|
|
35680
35837
|
const PickerNaviTime = props => {
|
|
35681
35838
|
const Next = useNextResolver();
|
|
@@ -35690,9 +35847,11 @@ const PickerNaviTime = props => {
|
|
|
35690
35847
|
return jsx(Next, {
|
|
35691
35848
|
...props,
|
|
35692
35849
|
type: "time",
|
|
35693
|
-
children: jsx(
|
|
35850
|
+
children: jsx(List, {
|
|
35851
|
+
selectable: true,
|
|
35694
35852
|
action: "send",
|
|
35695
|
-
children: slots.map((slot, i) => jsx(
|
|
35853
|
+
children: slots.map((slot, i) => jsx(List.Item, {
|
|
35854
|
+
selectable: true,
|
|
35696
35855
|
id: slot,
|
|
35697
35856
|
index: i,
|
|
35698
35857
|
value: slot,
|
|
@@ -36042,13 +36201,6 @@ installImportMetaCssBuild(import.meta);const css$k = /* css */`
|
|
|
36042
36201
|
* children — content to display inside the popup (enables popover/dialog mode)
|
|
36043
36202
|
* mode — "popover" or "dialog"; auto-detected from screen size when omitted
|
|
36044
36203
|
*/
|
|
36045
|
-
const Picker = props => {
|
|
36046
|
-
const defaultRef = useRef(null);
|
|
36047
|
-
props.ref = props.ref || defaultRef;
|
|
36048
|
-
const picker = renderPicker(PickerButton, props);
|
|
36049
|
-
return picker;
|
|
36050
|
-
};
|
|
36051
|
-
const renderPicker = createComponentResolver(pickerResolvers);
|
|
36052
36204
|
const PickerButton = props => {
|
|
36053
36205
|
import.meta.css = [css$k, "@jsenv/navi/src/control/picker/picker.jsx"];
|
|
36054
36206
|
resolveInputProps(props);
|
|
@@ -36214,6 +36366,15 @@ const PickerDefaultUI = () => {
|
|
|
36214
36366
|
children: value
|
|
36215
36367
|
});
|
|
36216
36368
|
};
|
|
36369
|
+
const PickerFirstResolver = props => {
|
|
36370
|
+
const Next = useNextResolver();
|
|
36371
|
+
const defaultRef = useRef(null);
|
|
36372
|
+
props.ref = props.ref || defaultRef;
|
|
36373
|
+
return jsx(Next, {
|
|
36374
|
+
...props
|
|
36375
|
+
});
|
|
36376
|
+
};
|
|
36377
|
+
const Picker = createComponentResolver([PickerFirstResolver, ...pickerResolvers, PickerButton]);
|
|
36217
36378
|
Picker.Placeholder = PickerPlaceholder;
|
|
36218
36379
|
Picker.Value = PickerValue;
|
|
36219
36380
|
Picker.UI = PickerDefaultUI;
|
|
@@ -40252,7 +40413,7 @@ const ButtonCopyToClipboard = ({
|
|
|
40252
40413
|
children,
|
|
40253
40414
|
...props
|
|
40254
40415
|
}) => {
|
|
40255
|
-
import.meta.css = [css$c, "@jsenv/navi/src/control/button_copy_to_clipboard.jsx"];
|
|
40416
|
+
import.meta.css = [css$c, "@jsenv/navi/src/control/input/button_copy_to_clipboard.jsx"];
|
|
40256
40417
|
const [copied, setCopied] = useState(false);
|
|
40257
40418
|
const renderedRef = useRef();
|
|
40258
40419
|
useEffect(() => {
|
|
@@ -42409,5 +42570,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
42409
42570
|
})
|
|
42410
42571
|
});
|
|
42411
42572
|
|
|
42412
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, Details, Dialog, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem,
|
|
42573
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, Details, Dialog, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Thead, Time, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, installCustomConstraintValidation, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, langSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navTo, naviI18n, openCallout, rawUrlPart, reload, removeCustomMessage, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
42413
42574
|
//# sourceMappingURL=jsenv_navi.js.map
|