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