@jsenv/navi 0.29.91 → 0.29.92
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 +492 -182
- package/dist/jsenv_navi.js.map +39 -25
- package/docs/AI_INSTRUCTIONS.md +9 -0
- package/docs/actions.md +4 -4
- package/docs/control_object.md +12 -0
- package/docs/control_value.md +50 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -342,11 +342,11 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
342
342
|
*
|
|
343
343
|
* Two decisions worth knowing before reading:
|
|
344
344
|
*
|
|
345
|
-
* - **navi places the target itself, every time.**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
345
|
+
* - **navi places the target itself, every time.** The scroll the browser
|
|
346
|
+
* would have done — target against the top edge, instantly — is applied
|
|
347
|
+
* here, after layout, so one rule holds whether the target was there all
|
|
348
|
+
* along or arrived late; `setUrlTargetOptions` lets an app pick another
|
|
349
|
+
* alignment or a smooth behavior. A page with nothing to scroll simply
|
|
350
350
|
* does not move, which is the whole of the "the list already fits on screen"
|
|
351
351
|
* case — the transient mark alone then says which one was meant.
|
|
352
352
|
*
|
|
@@ -381,8 +381,8 @@ const css$12 = /* css */`
|
|
|
381
381
|
`;
|
|
382
382
|
import.meta.css = [css$12, "@jsenv/navi/src/nav/url_target/url_target.js"];
|
|
383
383
|
let urlTargetOptions = {
|
|
384
|
-
block: "
|
|
385
|
-
behavior: "
|
|
384
|
+
block: "start",
|
|
385
|
+
behavior: "instant",
|
|
386
386
|
markDuration: 2000,
|
|
387
387
|
graceAfterIdle: 1000,
|
|
388
388
|
maxWait: 10_000
|
|
@@ -392,12 +392,12 @@ let urlTargetOptions = {
|
|
|
392
392
|
* Adjusts how navi answers the element designated by the URL hash.
|
|
393
393
|
*
|
|
394
394
|
* @param {object} options
|
|
395
|
-
* @param {"start"|"center"|"end"|"nearest"} [options.block="
|
|
396
|
-
* Vertical alignment of the scroll. "
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
395
|
+
* @param {"start"|"center"|"end"|"nearest"} [options.block="start"]
|
|
396
|
+
* Vertical alignment of the scroll. "start" by default — the alignment the
|
|
397
|
+
* browser itself uses when it answers a fragment.
|
|
398
|
+
* @param {ScrollBehavior} [options.behavior="instant"]
|
|
399
|
+
* "instant" by default, like the browser. When set to "smooth", it is
|
|
400
|
+
* overridden with "instant" under `prefers-reduced-motion: reduce`.
|
|
401
401
|
* @param {number} [options.markDuration=2000]
|
|
402
402
|
* How long, in ms, the element carries `data-url-target`. Published to CSS as
|
|
403
403
|
* `--navi-url-target-duration`.
|
|
@@ -23905,7 +23905,15 @@ const useUIStateController = (
|
|
|
23905
23905
|
// Uses the in-memory registry instead of DOM queries so this works even
|
|
23906
23906
|
// when sibling items are virtualized (not in the DOM).
|
|
23907
23907
|
// Form scoping is preserved by comparing parentUIStateController references.
|
|
23908
|
-
|
|
23908
|
+
// Checked, not truthy: a radio holding `false` (two rows asking a
|
|
23909
|
+
// yes/no) is as checked as one holding a name, and the row that was
|
|
23910
|
+
// checked before has to let go all the same.
|
|
23911
|
+
if (
|
|
23912
|
+
isRadio &&
|
|
23913
|
+
newUIState !== undefined &&
|
|
23914
|
+
controller.name &&
|
|
23915
|
+
!controlProxyFor
|
|
23916
|
+
) {
|
|
23909
23917
|
const siblings = getRadioSiblings(controller);
|
|
23910
23918
|
if (siblings) {
|
|
23911
23919
|
const siblingUncheckEvent = new CustomEvent(
|
|
@@ -24485,6 +24493,7 @@ const useUIGroupStateController = (
|
|
|
24485
24493
|
childControlFilter,
|
|
24486
24494
|
aggregateChildStates,
|
|
24487
24495
|
distributeChildUIState,
|
|
24496
|
+
distributeChildStates,
|
|
24488
24497
|
wantRequesterButtonState,
|
|
24489
24498
|
uiActionInternal,
|
|
24490
24499
|
allowCapture = false,
|
|
@@ -24501,7 +24510,7 @@ const useUIGroupStateController = (
|
|
|
24501
24510
|
// whatever IT says — a "HH:MM", an ISO duration — and the shape checks in
|
|
24502
24511
|
// setUIState below are about the default shape, not about that one.
|
|
24503
24512
|
const stateShapeIsTheDefaultOne =
|
|
24504
|
-
!aggregateChildStates && !distributeChildUIState;
|
|
24513
|
+
!aggregateChildStates && !distributeChildUIState && !distributeChildStates;
|
|
24505
24514
|
const defaults = GROUP_DEFAULTS[controlType] ?? GROUP_DEFAULTS[stateType];
|
|
24506
24515
|
const resolvedChildControlFilter =
|
|
24507
24516
|
childControlFilter ?? defaults?.childControlFilter ?? null;
|
|
@@ -24509,6 +24518,12 @@ const useUIGroupStateController = (
|
|
|
24509
24518
|
aggregateChildStates ?? defaults?.aggregateChildStates;
|
|
24510
24519
|
const resolvedDistributeChildUIState =
|
|
24511
24520
|
distributeChildUIState ?? defaults?.distributeChildUIState;
|
|
24521
|
+
// The plural half of the pair: `aggregateChildStates` already sees all the
|
|
24522
|
+
// children at once, and a group whose value is not one key per child usually
|
|
24523
|
+
// needs the same view on the way down — which of four seats each player takes
|
|
24524
|
+
// cannot be decided one seat at a time. Given both, the plural one wins.
|
|
24525
|
+
const resolvedDistributeChildStates =
|
|
24526
|
+
distributeChildStates ?? defaults?.distributeChildStates;
|
|
24512
24527
|
if (
|
|
24513
24528
|
typeof resolvedAggregateChildStates !== "function" ||
|
|
24514
24529
|
typeof resolvedDistributeChildUIState !== "function"
|
|
@@ -24763,6 +24778,48 @@ const useUIGroupStateController = (
|
|
|
24763
24778
|
// Where the group puts a value on ONE child: the only place that knows
|
|
24764
24779
|
// what each child gets, and the only one that sees a child it cannot
|
|
24765
24780
|
// place — see warnChildAnswersForItself.
|
|
24781
|
+
// One pass over every child, which is what a plural distribute needs:
|
|
24782
|
+
// it is asked once, sees the whole group, and answers for all of them.
|
|
24783
|
+
placeChildrenUIState: (groupUIState, e) => {
|
|
24784
|
+
if (!resolvedDistributeChildStates) {
|
|
24785
|
+
for (const childUIStateController of childUIStateControllerArray) {
|
|
24786
|
+
controller.placeChildUIState(
|
|
24787
|
+
childUIStateController,
|
|
24788
|
+
groupUIState,
|
|
24789
|
+
e,
|
|
24790
|
+
);
|
|
24791
|
+
}
|
|
24792
|
+
return;
|
|
24793
|
+
}
|
|
24794
|
+
const monitoredChildren = childUIStateControllerArray.filter(
|
|
24795
|
+
shouldPropagateStateToChild,
|
|
24796
|
+
);
|
|
24797
|
+
const stateByChild = resolvedDistributeChildStates(
|
|
24798
|
+
groupUIState,
|
|
24799
|
+
monitoredChildren,
|
|
24800
|
+
);
|
|
24801
|
+
if (!stateByChild) {
|
|
24802
|
+
return;
|
|
24803
|
+
}
|
|
24804
|
+
for (const childUIStateController of monitoredChildren) {
|
|
24805
|
+
if (!stateByChild.has(childUIStateController)) {
|
|
24806
|
+
// Not named by the answer: left where it is, the way
|
|
24807
|
+
// CANNOT_DERIVE leaves a child a per-child distribute says
|
|
24808
|
+
// nothing about.
|
|
24809
|
+
continue;
|
|
24810
|
+
}
|
|
24811
|
+
if (
|
|
24812
|
+
childUIStateController.hasStateProp &&
|
|
24813
|
+
!childUIStateController.props.signal
|
|
24814
|
+
) {
|
|
24815
|
+
continue;
|
|
24816
|
+
}
|
|
24817
|
+
childUIStateController.setUIState(
|
|
24818
|
+
stateByChild.get(childUIStateController),
|
|
24819
|
+
e,
|
|
24820
|
+
);
|
|
24821
|
+
}
|
|
24822
|
+
},
|
|
24766
24823
|
placeChildUIState: (childUIStateController, groupUIState, e) => {
|
|
24767
24824
|
if (!shouldPropagateStateToChild(childUIStateController)) {
|
|
24768
24825
|
return;
|
|
@@ -24824,13 +24881,7 @@ const useUIGroupStateController = (
|
|
|
24824
24881
|
detail: {},
|
|
24825
24882
|
});
|
|
24826
24883
|
chainEvent(propagateDownEvent, e);
|
|
24827
|
-
|
|
24828
|
-
controller.placeChildUIState(
|
|
24829
|
-
childUIStateController,
|
|
24830
|
-
newUIState,
|
|
24831
|
-
propagateDownEvent,
|
|
24832
|
-
);
|
|
24833
|
-
}
|
|
24884
|
+
controller.placeChildrenUIState(newUIState, propagateDownEvent);
|
|
24834
24885
|
const groupUIState = aggregateGroupUIState(newUIState);
|
|
24835
24886
|
if (e.type === "initial_state_push") {
|
|
24836
24887
|
controller.syncInternalState(groupUIState);
|
|
@@ -24905,11 +24956,21 @@ const useUIGroupStateController = (
|
|
|
24905
24956
|
const initialEvent = new CustomEvent("initial_state_push", {
|
|
24906
24957
|
detail: {},
|
|
24907
24958
|
});
|
|
24908
|
-
|
|
24909
|
-
|
|
24910
|
-
|
|
24911
|
-
|
|
24912
|
-
|
|
24959
|
+
if (resolvedDistributeChildStates) {
|
|
24960
|
+
// A group answering for all its children at once has to be asked
|
|
24961
|
+
// again now that there is one more: what each of them shows may
|
|
24962
|
+
// depend on who else is there.
|
|
24963
|
+
controller.placeChildrenUIState(
|
|
24964
|
+
stateToPlaceChildFrom,
|
|
24965
|
+
initialEvent,
|
|
24966
|
+
);
|
|
24967
|
+
} else {
|
|
24968
|
+
controller.placeChildUIState(
|
|
24969
|
+
childUIStateController,
|
|
24970
|
+
stateToPlaceChildFrom,
|
|
24971
|
+
initialEvent,
|
|
24972
|
+
);
|
|
24973
|
+
}
|
|
24913
24974
|
}
|
|
24914
24975
|
onChange(new CustomEvent(`${childControlType}_mount`), {
|
|
24915
24976
|
notifyExternal: "silent",
|
|
@@ -25084,13 +25145,7 @@ const useUIGroupStateController = (
|
|
|
25084
25145
|
"propagate_down_set_ui_state",
|
|
25085
25146
|
{ detail: {} },
|
|
25086
25147
|
);
|
|
25087
|
-
|
|
25088
|
-
controller.placeChildUIState(
|
|
25089
|
-
childUIStateController,
|
|
25090
|
-
groupUIState,
|
|
25091
|
-
propagateDownEvent,
|
|
25092
|
-
);
|
|
25093
|
-
}
|
|
25148
|
+
controller.placeChildrenUIState(groupUIState, propagateDownEvent);
|
|
25094
25149
|
controller.syncInternalState(groupUIState);
|
|
25095
25150
|
};
|
|
25096
25151
|
if (
|
|
@@ -25381,6 +25436,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25381
25436
|
return {
|
|
25382
25437
|
controller: facadeUIStateController,
|
|
25383
25438
|
realUIStateController,
|
|
25439
|
+
props,
|
|
25384
25440
|
};
|
|
25385
25441
|
},
|
|
25386
25442
|
// ── update: runs every render after the first ─────────────────────────
|
|
@@ -25394,6 +25450,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25394
25450
|
|
|
25395
25451
|
return {
|
|
25396
25452
|
realUIStateController,
|
|
25453
|
+
props,
|
|
25397
25454
|
};
|
|
25398
25455
|
},
|
|
25399
25456
|
);
|
|
@@ -26444,7 +26501,11 @@ const createControlInfo = (props, {
|
|
|
26444
26501
|
if (typeProp === "checkbox" || typeProp === "radio") {
|
|
26445
26502
|
statePropName = "checked";
|
|
26446
26503
|
defaultStatePropName = "defaultChecked";
|
|
26447
|
-
|
|
26504
|
+
// "on" is what HTML sends for a checkbox given no value of its own — the
|
|
26505
|
+
// default of an ABSENT prop, not of a falsy one: `value={false}` and
|
|
26506
|
+
// `value={0}` are values, and two rows holding true and false are how a
|
|
26507
|
+
// list asks a yes/no question.
|
|
26508
|
+
value = props.value === undefined ? "on" : props.value;
|
|
26448
26509
|
signalHoldsChecked = true;
|
|
26449
26510
|
if (signal) {
|
|
26450
26511
|
if (props.defaultChecked) {
|
|
@@ -26629,6 +26690,7 @@ const useControlgroupProps = (props, {
|
|
|
26629
26690
|
childControlFilter,
|
|
26630
26691
|
aggregateChildStates,
|
|
26631
26692
|
distributeChildUIState,
|
|
26693
|
+
distributeChildStates,
|
|
26632
26694
|
wantRequesterButtonState,
|
|
26633
26695
|
uiActionInternal,
|
|
26634
26696
|
allowCapture = false,
|
|
@@ -26642,6 +26704,7 @@ const useControlgroupProps = (props, {
|
|
|
26642
26704
|
childControlFilter,
|
|
26643
26705
|
aggregateChildStates,
|
|
26644
26706
|
distributeChildUIState,
|
|
26707
|
+
distributeChildStates,
|
|
26645
26708
|
wantRequesterButtonState,
|
|
26646
26709
|
uiActionInternal,
|
|
26647
26710
|
allowCapture,
|
|
@@ -45833,7 +45896,6 @@ const useFocusGroup = (
|
|
|
45833
45896
|
};
|
|
45834
45897
|
|
|
45835
45898
|
installImportMetaCssBuild(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
|
|
45836
|
-
const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
|
|
45837
45899
|
const css$L = /* css */`
|
|
45838
45900
|
.navi_summary_marker {
|
|
45839
45901
|
width: 1em;
|
|
@@ -45858,22 +45920,31 @@ const css$L = /* css */`
|
|
|
45858
45920
|
}
|
|
45859
45921
|
}
|
|
45860
45922
|
|
|
45861
|
-
|
|
45862
|
-
|
|
45863
|
-
|
|
45864
|
-
|
|
45865
|
-
|
|
45866
|
-
animation-fill-mode: forwards;
|
|
45923
|
+
/* One chevron, rotated: the transition only ever plays on a direction
|
|
45924
|
+
change, so the first paint shows the resting direction with no
|
|
45925
|
+
movement. */
|
|
45926
|
+
.navi_summary_marker_arrow_group {
|
|
45927
|
+
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
45867
45928
|
|
|
45868
|
-
&[data-
|
|
45869
|
-
|
|
45929
|
+
&[data-direction="right"] {
|
|
45930
|
+
transform: rotate(0deg);
|
|
45870
45931
|
}
|
|
45871
|
-
|
|
45872
|
-
|
|
45873
|
-
|
|
45932
|
+
&[data-direction="down"] {
|
|
45933
|
+
transform: rotate(90deg);
|
|
45934
|
+
}
|
|
45935
|
+
&[data-direction="up"] {
|
|
45936
|
+
transform: rotate(-90deg);
|
|
45937
|
+
}
|
|
45938
|
+
&[data-direction="left"] {
|
|
45939
|
+
transform: rotate(180deg);
|
|
45874
45940
|
}
|
|
45875
45941
|
}
|
|
45876
45942
|
|
|
45943
|
+
.navi_summary_marker_arrow {
|
|
45944
|
+
opacity: 1;
|
|
45945
|
+
transition: opacity 0.3s ease-in-out;
|
|
45946
|
+
}
|
|
45947
|
+
|
|
45877
45948
|
&[data-loading] {
|
|
45878
45949
|
.navi_summary_marker_loading_container {
|
|
45879
45950
|
transform: scale(1);
|
|
@@ -45898,39 +45969,25 @@ const css$L = /* css */`
|
|
|
45898
45969
|
stroke-dashoffset: -2010;
|
|
45899
45970
|
}
|
|
45900
45971
|
}
|
|
45901
|
-
@keyframes morph-to-down {
|
|
45902
|
-
from {
|
|
45903
|
-
d: path("${rightArrowPath}");
|
|
45904
|
-
}
|
|
45905
|
-
to {
|
|
45906
|
-
d: path("${downArrowPath}");
|
|
45907
|
-
}
|
|
45908
|
-
}
|
|
45909
|
-
@keyframes morph-to-right {
|
|
45910
|
-
from {
|
|
45911
|
-
d: path("${downArrowPath}");
|
|
45912
|
-
}
|
|
45913
|
-
to {
|
|
45914
|
-
d: path("${rightArrowPath}");
|
|
45915
|
-
}
|
|
45916
|
-
}
|
|
45917
45972
|
`;
|
|
45973
|
+
|
|
45974
|
+
/**
|
|
45975
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
45976
|
+
* open?: boolean,
|
|
45977
|
+
* loading?: boolean,
|
|
45978
|
+
* openDirection?: "down" | "up" | "left",
|
|
45979
|
+
* }>}
|
|
45980
|
+
* @param openDirection - Where the chevron points while open; closed always
|
|
45981
|
+
* points right. "down" fits content revealed below (the <details> shape),
|
|
45982
|
+
* "up" content revealed above, "left" content revealed beside.
|
|
45983
|
+
*/
|
|
45918
45984
|
const SummaryMarker = ({
|
|
45919
45985
|
open,
|
|
45920
|
-
loading
|
|
45986
|
+
loading,
|
|
45987
|
+
openDirection = "down"
|
|
45921
45988
|
}) => {
|
|
45922
45989
|
import.meta.css = [css$L, "@jsenv/navi/src/control/details/summary_marker.jsx"];
|
|
45923
45990
|
const showLoading = useDebounceTrue(loading, 300);
|
|
45924
|
-
const mountedRef = useRef(false);
|
|
45925
|
-
const prevOpenRef = useRef(open);
|
|
45926
|
-
useLayoutEffect(() => {
|
|
45927
|
-
mountedRef.current = true;
|
|
45928
|
-
return () => {
|
|
45929
|
-
mountedRef.current = false;
|
|
45930
|
-
};
|
|
45931
|
-
}, []);
|
|
45932
|
-
const shouldAnimate = mountedRef.current && prevOpenRef.current !== open;
|
|
45933
|
-
prevOpenRef.current = open;
|
|
45934
45991
|
return jsx("span", {
|
|
45935
45992
|
className: "navi_summary_marker",
|
|
45936
45993
|
"data-loading": showLoading ? "" : undefined,
|
|
@@ -45961,12 +46018,13 @@ const SummaryMarker = ({
|
|
|
45961
46018
|
strokeDasharray: "503 1507"
|
|
45962
46019
|
})]
|
|
45963
46020
|
}), jsx("g", {
|
|
46021
|
+
className: "navi_summary_marker_arrow_group",
|
|
46022
|
+
"data-direction": open ? openDirection : "right",
|
|
45964
46023
|
"transform-origin": "480px -480px",
|
|
45965
46024
|
children: jsx("path", {
|
|
45966
46025
|
className: "navi_summary_marker_arrow",
|
|
45967
46026
|
fill: "currentColor",
|
|
45968
|
-
|
|
45969
|
-
d: open ? downArrowPath : rightArrowPath
|
|
46027
|
+
d: rightArrowPath
|
|
45970
46028
|
})
|
|
45971
46029
|
})]
|
|
45972
46030
|
})
|
|
@@ -46196,21 +46254,32 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
46196
46254
|
* </Expandable>
|
|
46197
46255
|
*
|
|
46198
46256
|
* Content after UI expands below (the <details> shape), Content before UI
|
|
46199
|
-
* expands above; `layout="column"` puts the parts side by side
|
|
46200
|
-
* content then expanding horizontally. The
|
|
46201
|
-
*
|
|
46202
|
-
*
|
|
46203
|
-
*
|
|
46204
|
-
*
|
|
46205
|
-
*
|
|
46257
|
+
* expands above; `layout="column"` puts the parts side by side (sharing
|
|
46258
|
+
* their height), the content then expanding horizontally. The marker
|
|
46259
|
+
* chevron follows: it points right while closed and toward where the
|
|
46260
|
+
* content went while open (down, up, or left). The common shape has a
|
|
46261
|
+
* shorthand: `ui` prop + children as content.
|
|
46262
|
+
* - the UI part is the focusable toggle itself (role button, Space/Enter,
|
|
46263
|
+
* arrow keys) and accepts any markup: controls inside it keep their own
|
|
46264
|
+
* behavior, the marker is purely decorative.
|
|
46265
|
+
*
|
|
46266
|
+
* Reach for it knowingly: expanding in-flow SHIFTS the layout — everything
|
|
46267
|
+
* below (or beside) moves when it opens. A Popover, Dialog, Picker or Callout
|
|
46268
|
+
* answers the same click on its own layer, moving nothing, which is usually
|
|
46269
|
+
* the better UX: a layout that stays where it is reads and operates better —
|
|
46270
|
+
* all the more on small screens, mobile first of all, where the shift can
|
|
46271
|
+
* push most of the page away. Expandable is for content that genuinely
|
|
46272
|
+
* belongs in the flow (a tree, a changelog, a settings group read top to
|
|
46273
|
+
* bottom).
|
|
46206
46274
|
*
|
|
46207
46275
|
* What <details> gives for free is rebuilt here:
|
|
46208
46276
|
* - a "toggle" event (a real ToggleEvent when the browser has it) dispatched on
|
|
46209
46277
|
* the root whenever the state actually changes — but never on mount, unlike
|
|
46210
46278
|
* the native one (see the workaround comment in details.jsx);
|
|
46211
46279
|
* - `--navi-toggle`/`--navi-open`/`--navi-close` commands work against it: the
|
|
46212
|
-
* root
|
|
46213
|
-
* the `navi_command`/`navi_request_open`/`navi_request_close`
|
|
46280
|
+
* root and the UI part carry `aria-expanded` (what the command system reads)
|
|
46281
|
+
* and answer the `navi_command`/`navi_request_open`/`navi_request_close`
|
|
46282
|
+
* events.
|
|
46214
46283
|
*
|
|
46215
46284
|
* Content is not built until the first expansion and stays built afterwards —
|
|
46216
46285
|
* same policy, same prop names as popups (see popup_content_mount.js):
|
|
@@ -46218,12 +46287,15 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
46218
46287
|
* once the collapse settles (so a closing animation still plays on real
|
|
46219
46288
|
* content).
|
|
46220
46289
|
*
|
|
46221
|
-
* The
|
|
46222
|
-
*
|
|
46223
|
-
*
|
|
46224
|
-
*
|
|
46225
|
-
*
|
|
46226
|
-
*
|
|
46290
|
+
* The animation is a REVEAL, not a resize: the expandable's own footprint
|
|
46291
|
+
* grows/shrinks progressively (the content's grid track interpolates
|
|
46292
|
+
* 0fr <-> 1fr — rows for the stacked layout, columns for `layout="column"`),
|
|
46293
|
+
* but the content inside is laid out at its final size for the whole movement
|
|
46294
|
+
* (its animated dimension is frozen to the measured final value, see the
|
|
46295
|
+
* [opened] effect) and the container simply uncovers it. Text never rewraps
|
|
46296
|
+
* mid-animation. The content is revealed from its UI side (pinned against the
|
|
46297
|
+
* UI when it comes first). Once settled open the clipping is released, so a
|
|
46298
|
+
* popover or focus ring inside is not cut at the edges.
|
|
46227
46299
|
*/
|
|
46228
46300
|
const css$J = /* css */`
|
|
46229
46301
|
.navi_expandable {
|
|
@@ -46241,27 +46313,16 @@ const css$J = /* css */`
|
|
|
46241
46313
|
cursor: pointer;
|
|
46242
46314
|
user-select: none;
|
|
46243
46315
|
|
|
46244
|
-
|
|
46245
|
-
only the marker button actually holds it. */
|
|
46246
|
-
&:has(> .navi_expandable_toggle:focus-visible) {
|
|
46316
|
+
&:focus-visible {
|
|
46247
46317
|
border-radius: 4px;
|
|
46248
46318
|
outline: 2px solid AccentColor;
|
|
46249
46319
|
outline-offset: 1px;
|
|
46250
46320
|
}
|
|
46251
46321
|
|
|
46252
|
-
> .
|
|
46322
|
+
> .navi_expandable_marker {
|
|
46253
46323
|
display: flex;
|
|
46254
|
-
|
|
46324
|
+
flex-shrink: 0;
|
|
46255
46325
|
align-items: center;
|
|
46256
|
-
color: inherit;
|
|
46257
|
-
font: inherit;
|
|
46258
|
-
background: none;
|
|
46259
|
-
border: none;
|
|
46260
|
-
cursor: pointer;
|
|
46261
|
-
|
|
46262
|
-
&:focus-visible {
|
|
46263
|
-
outline: none;
|
|
46264
|
-
}
|
|
46265
46326
|
}
|
|
46266
46327
|
|
|
46267
46328
|
> .navi_expandable_ui_label {
|
|
@@ -46273,25 +46334,57 @@ const css$J = /* css */`
|
|
|
46273
46334
|
}
|
|
46274
46335
|
|
|
46275
46336
|
> .navi_expandable_content_container {
|
|
46337
|
+
position: relative;
|
|
46276
46338
|
display: grid;
|
|
46277
46339
|
grid-template-rows: 0fr;
|
|
46278
|
-
|
|
46279
|
-
|
|
46340
|
+
/* The clip lives here, on the moving box, because the content inside
|
|
46341
|
+
keeps its final size during the animation (see the top comment) and
|
|
46342
|
+
overflows the track on purpose. One-sided (a clip-path with the free
|
|
46343
|
+
sides pushed far out) rather than overflow: hidden: only the side
|
|
46344
|
+
being revealed hides anything, so a badge sticking out of the other
|
|
46345
|
+
sides is visible from the very first frame of the movement. */
|
|
46346
|
+
clip-path: inset(-9999px -9999px 0 -9999px);
|
|
46347
|
+
|
|
46348
|
+
/* The sizer is what lets the track actually collapse: min-height 0 on
|
|
46349
|
+
an auto-sized item zeroes its min-content contribution. The frozen
|
|
46350
|
+
content cannot play that role itself — a definite height makes the
|
|
46351
|
+
contribution definite too, and the track then never goes below it. */
|
|
46352
|
+
> .navi_expandable_content_sizer {
|
|
46353
|
+
display: grid;
|
|
46280
46354
|
min-height: 0;
|
|
46281
|
-
overflow: hidden;
|
|
46282
46355
|
}
|
|
46283
46356
|
}
|
|
46284
46357
|
&[aria-expanded="true"] > .navi_expandable_content_container {
|
|
46285
46358
|
grid-template-rows: 1fr;
|
|
46286
46359
|
}
|
|
46360
|
+
/* Content before the UI: revealed against the UI side — the edge touching
|
|
46361
|
+
the UI stays, the far edge is what gets uncovered. Said at BOTH levels:
|
|
46362
|
+
a transitioning fr resolves once for the container's own size and once
|
|
46363
|
+
more inside it (the row is fraction² high), so the row must be glued to
|
|
46364
|
+
the container's UI edge and the oversized frozen content to the row's —
|
|
46365
|
+
anchoring only the inner one leaves the content following the drifting
|
|
46366
|
+
fraction² row. */
|
|
46367
|
+
&[data-content-first]:not([data-layout="column"])
|
|
46368
|
+
> .navi_expandable_content_container {
|
|
46369
|
+
align-content: end;
|
|
46370
|
+
clip-path: inset(0 -9999px -9999px -9999px);
|
|
46287
46371
|
|
|
46288
|
-
|
|
46289
|
-
|
|
46372
|
+
> .navi_expandable_content_sizer {
|
|
46373
|
+
align-content: end;
|
|
46374
|
+
}
|
|
46375
|
+
}
|
|
46376
|
+
|
|
46377
|
+
/* The parts sit side by side, sharing their height: the UI part becomes a
|
|
46378
|
+
vertical strip and the content expands horizontally, on the columns
|
|
46379
|
+
track — the rows track collapses too, so a closed expandable is only as
|
|
46380
|
+
tall as its UI (the content, unmounted or 0-wide, says nothing about
|
|
46381
|
+
the height it will bring). */
|
|
46290
46382
|
&[data-layout="column"] {
|
|
46291
46383
|
flex-direction: row;
|
|
46292
46384
|
|
|
46293
46385
|
> .navi_expandable_ui {
|
|
46294
46386
|
flex-direction: column;
|
|
46387
|
+
align-items: center;
|
|
46295
46388
|
|
|
46296
46389
|
> .navi_expandable_ui_label {
|
|
46297
46390
|
flex-direction: column;
|
|
@@ -46299,17 +46392,34 @@ const css$J = /* css */`
|
|
|
46299
46392
|
}
|
|
46300
46393
|
> .navi_expandable_content_container {
|
|
46301
46394
|
grid-template-columns: 0fr;
|
|
46302
|
-
grid-template-rows:
|
|
46395
|
+
grid-template-rows: 0fr;
|
|
46396
|
+
/* Both tracks reveal: clip the far side of each (right and bottom),
|
|
46397
|
+
the UI side and the top stay free. */
|
|
46398
|
+
clip-path: inset(-9999px 0 0 -9999px);
|
|
46303
46399
|
|
|
46304
|
-
> .
|
|
46400
|
+
> .navi_expandable_content_sizer {
|
|
46305
46401
|
min-width: 0;
|
|
46306
|
-
min-height:
|
|
46402
|
+
min-height: 0;
|
|
46307
46403
|
}
|
|
46308
46404
|
}
|
|
46309
46405
|
&[aria-expanded="true"] > .navi_expandable_content_container {
|
|
46310
46406
|
grid-template-columns: 1fr;
|
|
46407
|
+
grid-template-rows: 1fr;
|
|
46408
|
+
}
|
|
46409
|
+
/* mountWhenClosed: the content is built and width-frozen while closed
|
|
46410
|
+
(see the component), so it can size the height at all times — the
|
|
46411
|
+
expandable then keeps one stable height and only the width reveals. */
|
|
46412
|
+
&[data-closed-content-sized] > .navi_expandable_content_container {
|
|
46311
46413
|
grid-template-rows: none;
|
|
46312
46414
|
}
|
|
46415
|
+
&[data-content-first] > .navi_expandable_content_container {
|
|
46416
|
+
justify-content: end;
|
|
46417
|
+
clip-path: inset(-9999px -9999px 0 0);
|
|
46418
|
+
|
|
46419
|
+
> .navi_expandable_content_sizer {
|
|
46420
|
+
justify-content: end;
|
|
46421
|
+
}
|
|
46422
|
+
}
|
|
46313
46423
|
}
|
|
46314
46424
|
|
|
46315
46425
|
&[data-animation] > .navi_expandable_content_container {
|
|
@@ -46323,16 +46433,21 @@ const css$J = /* css */`
|
|
|
46323
46433
|
transition: none;
|
|
46324
46434
|
}
|
|
46325
46435
|
}
|
|
46326
|
-
/* Settled open: stop clipping, so a popover, a focus ring or a
|
|
46327
|
-
element inside the content can spill out
|
|
46328
|
-
|
|
46329
|
-
|
|
46330
|
-
|
|
46331
|
-
|
|
46332
|
-
|
|
46436
|
+
/* Settled open: stop clipping entirely, so a popover, a focus ring or a
|
|
46437
|
+
dragged element inside the content can spill out on any side. Settled
|
|
46438
|
+
closed: clip every side — the collapsed box must show nothing, a
|
|
46439
|
+
stick-out included. In between (any movement, opening or closing) the
|
|
46440
|
+
one-sided clips above apply. */
|
|
46441
|
+
&[aria-expanded="true"][data-settled] > .navi_expandable_content_container {
|
|
46442
|
+
clip-path: none;
|
|
46443
|
+
}
|
|
46444
|
+
&:not([aria-expanded="true"])[data-settled]
|
|
46445
|
+
> .navi_expandable_content_container {
|
|
46446
|
+
clip-path: inset(0 0 0 0);
|
|
46333
46447
|
}
|
|
46334
46448
|
&[data-content-scrolls]
|
|
46335
46449
|
> .navi_expandable_content_container
|
|
46450
|
+
> .navi_expandable_content_sizer
|
|
46336
46451
|
> .navi_expandable_content {
|
|
46337
46452
|
max-height: var(--navi-expandable-max-content-height);
|
|
46338
46453
|
overflow-y: auto;
|
|
@@ -46387,21 +46502,22 @@ const useExpandableContext = partName => {
|
|
|
46387
46502
|
* object (`{ loading, error, completed, ... }`) — see ActionRenderer.
|
|
46388
46503
|
* @param loading - Shows the loading spinner on the marker regardless of
|
|
46389
46504
|
* `action`'s own loading state.
|
|
46390
|
-
* @param animation - Off by default. `true` plays the
|
|
46391
|
-
*
|
|
46392
|
-
* (0.3s).
|
|
46505
|
+
* @param animation - Off by default. `true` plays the reveal transition;
|
|
46506
|
+
* duration comes from `--navi-expandable-animation-duration` (0.3s).
|
|
46393
46507
|
* @param layout - `"row"` (default): the parts stack, the content expands
|
|
46394
|
-
* vertically. `"column"`: the parts sit side by side
|
|
46395
|
-
* horizontally next to the UI part.
|
|
46396
|
-
* @param autoFocus - Off by default (the focus stays on the
|
|
46508
|
+
* vertically. `"column"`: the parts sit side by side sharing their height,
|
|
46509
|
+
* the content expands horizontally next to the UI part.
|
|
46510
|
+
* @param autoFocus - Off by default (the focus stays on the UI part when
|
|
46397
46511
|
* opening). `true` moves the focus into the content on open — the
|
|
46398
46512
|
* `[autofocus]` element if any, the first focusable otherwise. Whatever the
|
|
46399
46513
|
* setting, closing while the focus is inside the content hands it back to
|
|
46400
|
-
* the
|
|
46514
|
+
* the UI part (it would otherwise be lost to the closed, inert content).
|
|
46401
46515
|
* @param maxContentHeight - Caps the content height; taller content scrolls
|
|
46402
46516
|
* inside the expandable instead of growing it.
|
|
46403
46517
|
* @param mountWhenClosed - Builds the content right away instead of on first
|
|
46404
|
-
* expansion.
|
|
46518
|
+
* expansion. In layout="column" it also gives the closed expandable its
|
|
46519
|
+
* content's height (the content is kept laid out at its open width), so
|
|
46520
|
+
* opening only reveals the width instead of changing the height too.
|
|
46405
46521
|
* @param unmountWhenClosed - Throws the content away once the collapse
|
|
46406
46522
|
* settles — after the closing animation, so it still plays on real content —
|
|
46407
46523
|
* and rebuilds it from scratch on every expansion.
|
|
@@ -46431,10 +46547,10 @@ const Expandable = props => {
|
|
|
46431
46547
|
const defaultRef = useRef();
|
|
46432
46548
|
const rootRef = ref || defaultRef;
|
|
46433
46549
|
const uiRef = useRef();
|
|
46434
|
-
const toggleButtonRef = useRef();
|
|
46435
46550
|
const contentContainerRef = useRef();
|
|
46436
46551
|
const contentId = useId();
|
|
46437
|
-
const
|
|
46552
|
+
const isColumn = layout === "column";
|
|
46553
|
+
const closedContentSized = Boolean(isColumn && mountWhenClosed);
|
|
46438
46554
|
// Reading .value during render is what subscribes the expandable to it.
|
|
46439
46555
|
const openRequested = signal ? signal.value : open;
|
|
46440
46556
|
const [opened, setOpened] = useState(() => Boolean(openRequested === undefined ? defaultOpen : openRequested));
|
|
@@ -46455,16 +46571,44 @@ const Expandable = props => {
|
|
|
46455
46571
|
const [settled, setSettled] = useState(true);
|
|
46456
46572
|
|
|
46457
46573
|
// Read before the close touches the DOM: flipping the content to inert can
|
|
46458
|
-
// blur what it held, so by effect time the focus to hand back to the
|
|
46574
|
+
// blur what it held, so by effect time the focus to hand back to the UI part
|
|
46459
46575
|
// could already be gone.
|
|
46460
46576
|
const focusedBeforeCloseRef = useRef(null);
|
|
46461
|
-
// The pointer press that is about to toggle
|
|
46462
|
-
// the click ever fires
|
|
46463
|
-
//
|
|
46577
|
+
// The pointer press that is about to toggle can blur the focused field
|
|
46578
|
+
// before the click ever fires — so what held the focus has to be remembered
|
|
46579
|
+
// at pointerdown time.
|
|
46464
46580
|
const focusedAtPointerDownRef = useRef(null);
|
|
46465
46581
|
const onUIPointerDown = () => {
|
|
46466
46582
|
focusedAtPointerDownRef.current = document.activeElement;
|
|
46467
46583
|
};
|
|
46584
|
+
|
|
46585
|
+
// The content keeps its final size while the track animates (see the top
|
|
46586
|
+
// comment): its animated dimension is pinned to a measured pixel value, and
|
|
46587
|
+
// released once the movement settles.
|
|
46588
|
+
const freezeContentSize = () => {
|
|
46589
|
+
const contentContainer = contentContainerRef.current;
|
|
46590
|
+
const contentElement = contentContainer ? contentContainer.firstElementChild.firstElementChild : null;
|
|
46591
|
+
if (!contentElement) {
|
|
46592
|
+
return;
|
|
46593
|
+
}
|
|
46594
|
+
const rect = contentElement.getBoundingClientRect();
|
|
46595
|
+
if (isColumn) {
|
|
46596
|
+
// Both, not just the width: a max-height-capped content otherwise
|
|
46597
|
+
// follows the collapsing rows track down instead of holding its size.
|
|
46598
|
+
contentElement.style.width = `${rect.width}px`;
|
|
46599
|
+
contentElement.style.height = `${rect.height}px`;
|
|
46600
|
+
} else {
|
|
46601
|
+
contentElement.style.height = `${rect.height}px`;
|
|
46602
|
+
}
|
|
46603
|
+
};
|
|
46604
|
+
|
|
46605
|
+
// Where the last paint left the track, measured before the toggle commits:
|
|
46606
|
+
// 0 when fully closed, partway when reopening during a collapse. Read here
|
|
46607
|
+
// rather than in the effect — a layout read after the commit would also be
|
|
46608
|
+
// the first style recalc of the open state, starting the track transition
|
|
46609
|
+
// right there; once canceled (to measure the final size), a new transition
|
|
46610
|
+
// to the same end value refuses to start and the reveal jumps.
|
|
46611
|
+
const revealStartSizeRef = useRef(null);
|
|
46468
46612
|
const toggleTo = nextOpen => {
|
|
46469
46613
|
nextOpen = Boolean(nextOpen);
|
|
46470
46614
|
if (nextOpen === openedRef.current) {
|
|
@@ -46472,10 +46616,20 @@ const Expandable = props => {
|
|
|
46472
46616
|
}
|
|
46473
46617
|
openedRef.current = nextOpen;
|
|
46474
46618
|
if (nextOpen) {
|
|
46619
|
+
if (animation) {
|
|
46620
|
+
const contentContainer = contentContainerRef.current;
|
|
46621
|
+
revealStartSizeRef.current = contentContainer ? contentContainer.getBoundingClientRect() : null;
|
|
46622
|
+
}
|
|
46475
46623
|
setContentMounted(true);
|
|
46476
46624
|
} else {
|
|
46477
46625
|
const activeElement = document.activeElement;
|
|
46478
46626
|
focusedBeforeCloseRef.current = !activeElement || activeElement === document.body ? focusedAtPointerDownRef.current : activeElement;
|
|
46627
|
+
if (animation) {
|
|
46628
|
+
// Now, while the content is still fully laid out — by effect time the
|
|
46629
|
+
// track is already heading to 0 (the opening case measures in the
|
|
46630
|
+
// effect instead, where the just-mounted content exists).
|
|
46631
|
+
freezeContentSize();
|
|
46632
|
+
}
|
|
46479
46633
|
}
|
|
46480
46634
|
focusedAtPointerDownRef.current = null;
|
|
46481
46635
|
setOpened(nextOpen);
|
|
@@ -46524,10 +46678,9 @@ const Expandable = props => {
|
|
|
46524
46678
|
}, [openRequested]);
|
|
46525
46679
|
|
|
46526
46680
|
// A state change: tell the world (the "toggle" event), move the focus, and
|
|
46527
|
-
//
|
|
46528
|
-
//
|
|
46529
|
-
//
|
|
46530
|
-
// open).
|
|
46681
|
+
// set up the reveal. Skipped on mount — nothing changed, so neither the
|
|
46682
|
+
// event nor a transition exists (and a page must not have its focus stolen
|
|
46683
|
+
// by an expandable that was simply already open).
|
|
46531
46684
|
const isFirstOpenedRunRef = useRef(true);
|
|
46532
46685
|
useLayoutEffect(() => {
|
|
46533
46686
|
if (isFirstOpenedRunRef.current) {
|
|
@@ -46547,13 +46700,57 @@ const Expandable = props => {
|
|
|
46547
46700
|
const focusedBeforeClose = focusedBeforeCloseRef.current;
|
|
46548
46701
|
focusedBeforeCloseRef.current = null;
|
|
46549
46702
|
if (focusedBeforeClose && contentContainerRef.current && contentContainerRef.current.contains(focusedBeforeClose)) {
|
|
46550
|
-
|
|
46703
|
+
uiRef.current.focus();
|
|
46551
46704
|
}
|
|
46552
46705
|
}
|
|
46553
46706
|
if (!animation) {
|
|
46554
46707
|
return undefined;
|
|
46555
46708
|
}
|
|
46556
|
-
const
|
|
46709
|
+
const contentContainer = contentContainerRef.current;
|
|
46710
|
+
const contentElement = contentContainer.firstElementChild.firstElementChild;
|
|
46711
|
+
if (opened) {
|
|
46712
|
+
// The reveal needs the content at its final size before the track
|
|
46713
|
+
// starts moving, and the final size only exists in the open state —
|
|
46714
|
+
// the reflow trick (see instructions.md, CSS section), with transitions
|
|
46715
|
+
// suppressed BEFORE the first layout read: this effect runs pre-paint,
|
|
46716
|
+
// so any earlier read would itself be the first recalc of the open
|
|
46717
|
+
// state and would start the track transition (see revealStartSizeRef).
|
|
46718
|
+
contentContainer.style.transitionProperty = "none";
|
|
46719
|
+
const finalRect = contentElement.getBoundingClientRect();
|
|
46720
|
+
if (isColumn) {
|
|
46721
|
+
contentElement.style.width = `${finalRect.width}px`;
|
|
46722
|
+
contentElement.style.height = `${finalRect.height}px`;
|
|
46723
|
+
} else {
|
|
46724
|
+
contentElement.style.height = `${finalRect.height}px`;
|
|
46725
|
+
}
|
|
46726
|
+
// Put the tracks back where the last paint left them and let the
|
|
46727
|
+
// transition play from there. In fr — px does not interpolate with fr.
|
|
46728
|
+
const startRect = revealStartSizeRef.current;
|
|
46729
|
+
revealStartSizeRef.current = null;
|
|
46730
|
+
const startFrOf = (startSize, finalSize) => finalSize > 0 ? startSize / finalSize : 0;
|
|
46731
|
+
if (isColumn) {
|
|
46732
|
+
contentContainer.style.gridTemplateColumns = `${startFrOf(startRect ? startRect.width : 0, finalRect.width)}fr`;
|
|
46733
|
+
if (!closedContentSized) {
|
|
46734
|
+
// The height opens alongside the width (a closed column expandable
|
|
46735
|
+
// is only as tall as its UI) — unless the closed content already
|
|
46736
|
+
// sizes it, where only the width has anywhere to go.
|
|
46737
|
+
contentContainer.style.gridTemplateRows = `${startFrOf(startRect ? startRect.height : 0, finalRect.height)}fr`;
|
|
46738
|
+
}
|
|
46739
|
+
} else {
|
|
46740
|
+
contentContainer.style.gridTemplateRows = `${startFrOf(startRect ? startRect.height : 0, finalRect.height)}fr`;
|
|
46741
|
+
}
|
|
46742
|
+
// That starting frame must be genuinely rendered to transition from it,
|
|
46743
|
+
// and transitions re-enabled BEFORE the flip back to the open value —
|
|
46744
|
+
// same order as popover.jsx's own reflow trick.
|
|
46745
|
+
contentContainer.getBoundingClientRect();
|
|
46746
|
+
contentContainer.style.transitionProperty = "";
|
|
46747
|
+
contentContainer.style.gridTemplateColumns = "";
|
|
46748
|
+
contentContainer.style.gridTemplateRows = "";
|
|
46749
|
+
}
|
|
46750
|
+
// (closing froze the content in toggleTo, while it was still laid out)
|
|
46751
|
+
const cancel = whenTransitionSettles(contentContainer, () => {
|
|
46752
|
+
contentElement.style.width = "";
|
|
46753
|
+
contentElement.style.height = "";
|
|
46557
46754
|
setSettled(true);
|
|
46558
46755
|
});
|
|
46559
46756
|
return cancel;
|
|
@@ -46569,6 +46766,30 @@ const Expandable = props => {
|
|
|
46569
46766
|
}
|
|
46570
46767
|
}, [mountWhenClosed]);
|
|
46571
46768
|
|
|
46769
|
+
// closedContentSized (column + mountWhenClosed): the closed content sizes
|
|
46770
|
+
// the height (see the CSS), which is only right if it lies at its OPEN
|
|
46771
|
+
// width — at its natural closed width (a 0-wide track) it would wrap
|
|
46772
|
+
// against nothing and stack word by word. So while closed, its width is
|
|
46773
|
+
// frozen to a silently measured open width.
|
|
46774
|
+
useLayoutEffect(() => {
|
|
46775
|
+
if (!closedContentSized || opened || !settled || !contentMounted) {
|
|
46776
|
+
return;
|
|
46777
|
+
}
|
|
46778
|
+
const contentContainer = contentContainerRef.current;
|
|
46779
|
+
const contentElement = contentContainer.firstElementChild.firstElementChild;
|
|
46780
|
+
contentContainer.style.transitionProperty = "none";
|
|
46781
|
+
contentContainer.style.gridTemplateColumns = "1fr";
|
|
46782
|
+
contentElement.style.width = "";
|
|
46783
|
+
const openRect = contentElement.getBoundingClientRect();
|
|
46784
|
+
contentElement.style.width = `${openRect.width}px`;
|
|
46785
|
+
contentContainer.style.gridTemplateColumns = "";
|
|
46786
|
+
// The closed frame must be committed while transitions are still off —
|
|
46787
|
+
// re-enabled in the same recalc, the 1fr-to-0fr trip back from the silent
|
|
46788
|
+
// measurement above would play as a second closing animation.
|
|
46789
|
+
contentContainer.getBoundingClientRect();
|
|
46790
|
+
contentContainer.style.transitionProperty = "";
|
|
46791
|
+
}, [closedContentSized, opened, settled, contentMounted]);
|
|
46792
|
+
|
|
46572
46793
|
// Mounted already open: the content is visible, its data is due.
|
|
46573
46794
|
useEffect(() => {
|
|
46574
46795
|
if (openedRef.current && hasAction) {
|
|
@@ -46594,7 +46815,7 @@ const Expandable = props => {
|
|
|
46594
46815
|
key
|
|
46595
46816
|
} = keyboardEvent;
|
|
46596
46817
|
if (key === openKeyShortcut) {
|
|
46597
|
-
if (document.activeElement !==
|
|
46818
|
+
if (document.activeElement !== uiRef.current) {
|
|
46598
46819
|
return;
|
|
46599
46820
|
}
|
|
46600
46821
|
if (!openedRef.current) {
|
|
@@ -46614,19 +46835,19 @@ const Expandable = props => {
|
|
|
46614
46835
|
if (!openedRef.current) {
|
|
46615
46836
|
return;
|
|
46616
46837
|
}
|
|
46617
|
-
const
|
|
46618
|
-
if (document.activeElement ===
|
|
46838
|
+
const uiElement = uiRef.current;
|
|
46839
|
+
if (document.activeElement === uiElement) {
|
|
46619
46840
|
keyboardEvent.preventDefault();
|
|
46620
46841
|
toggleTo(false);
|
|
46621
46842
|
} else {
|
|
46622
46843
|
keyboardEvent.preventDefault();
|
|
46623
|
-
|
|
46844
|
+
uiElement.focus();
|
|
46624
46845
|
}
|
|
46625
46846
|
}
|
|
46626
46847
|
};
|
|
46627
46848
|
const onUIClick = clickEvent => {
|
|
46628
46849
|
// A navi control inside the UI part cancels the click it consumed (see
|
|
46629
|
-
// click_to_expand.js — the
|
|
46850
|
+
// click_to_expand.js — the UI part's own aria-expanded is what it finds).
|
|
46630
46851
|
if (clickEvent.defaultPrevented) {
|
|
46631
46852
|
return;
|
|
46632
46853
|
}
|
|
@@ -46635,32 +46856,62 @@ const Expandable = props => {
|
|
|
46635
46856
|
} = clickEvent;
|
|
46636
46857
|
if (target.nodeType === 1) {
|
|
46637
46858
|
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
46638
|
-
if (interactiveElement && interactiveElement !==
|
|
46859
|
+
if (interactiveElement && interactiveElement !== uiRef.current && uiRef.current.contains(interactiveElement)) {
|
|
46639
46860
|
return;
|
|
46640
46861
|
}
|
|
46641
46862
|
}
|
|
46642
46863
|
toggleTo(!openedRef.current);
|
|
46643
46864
|
};
|
|
46865
|
+
|
|
46866
|
+
// Space/Enter on the UI part itself (role button) — a key pressed on a
|
|
46867
|
+
// control inside it belongs to that control.
|
|
46868
|
+
const onUIKeyDown = keyboardEvent => {
|
|
46869
|
+
if (keyboardEvent.defaultPrevented) {
|
|
46870
|
+
return;
|
|
46871
|
+
}
|
|
46872
|
+
if (keyboardEvent.target !== uiRef.current) {
|
|
46873
|
+
return;
|
|
46874
|
+
}
|
|
46875
|
+
const {
|
|
46876
|
+
key
|
|
46877
|
+
} = keyboardEvent;
|
|
46878
|
+
if (key === " " || key === "Enter") {
|
|
46879
|
+
keyboardEvent.preventDefault();
|
|
46880
|
+
toggleTo(!openedRef.current);
|
|
46881
|
+
}
|
|
46882
|
+
};
|
|
46883
|
+
|
|
46884
|
+
// Where the content went, so the marker can point at it while open (closed
|
|
46885
|
+
// always points right): below by default, above when the content part comes
|
|
46886
|
+
// first, beside for layout="column" (the chevron then points back toward
|
|
46887
|
+
// the UI: left).
|
|
46888
|
+
const childArray = toChildArray(children);
|
|
46889
|
+
const firstPart = childArray.find(child => child && (child.type === ExpandableUI || child.type === ExpandableContent));
|
|
46890
|
+
const hasParts = Boolean(firstPart);
|
|
46891
|
+
const contentFirst = hasParts && firstPart.type === ExpandableContent;
|
|
46892
|
+
const openDirection = isColumn ? "left" : contentFirst ? "up" : "down";
|
|
46644
46893
|
const expandableContextValue = {
|
|
46645
46894
|
opened,
|
|
46646
46895
|
loading: loading || hasAction && actionLoading,
|
|
46647
46896
|
contentMounted,
|
|
46648
46897
|
hasAction,
|
|
46649
46898
|
effectiveAction,
|
|
46899
|
+
openDirection,
|
|
46900
|
+
toggleTo,
|
|
46650
46901
|
onUIClick,
|
|
46651
46902
|
onUIPointerDown,
|
|
46903
|
+
onUIKeyDown,
|
|
46652
46904
|
uiRef,
|
|
46653
|
-
toggleButtonRef,
|
|
46654
46905
|
contentContainerRef,
|
|
46655
|
-
contentId
|
|
46656
|
-
uiId
|
|
46906
|
+
contentId
|
|
46657
46907
|
};
|
|
46658
46908
|
|
|
46659
46909
|
// Explicit parts win; the `ui` prop + children is the shorthand for the
|
|
46660
|
-
// common shape (UI above, content below).
|
|
46661
|
-
|
|
46662
|
-
|
|
46663
|
-
|
|
46910
|
+
// common shape (UI above, content below). Parts are cloned on every render:
|
|
46911
|
+
// reference-stable children would be bailed out of the commit, leaving
|
|
46912
|
+
// their context subscription to re-render them asynchronously — after the
|
|
46913
|
+
// [opened] effect above, which measures the content they render.
|
|
46914
|
+
const body = hasParts ? childArray.map(child => child && (child.type === ExpandableUI || child.type === ExpandableContent) ? cloneElement(child) : child) : jsxs(Fragment$1, {
|
|
46664
46915
|
children: [jsx(ExpandableUI, {
|
|
46665
46916
|
children: ui
|
|
46666
46917
|
}), jsx(ExpandableContent, {
|
|
@@ -46671,10 +46922,12 @@ const Expandable = props => {
|
|
|
46671
46922
|
ref: rootRef,
|
|
46672
46923
|
baseClassName: "navi_expandable",
|
|
46673
46924
|
"aria-expanded": opened ? "true" : "false",
|
|
46674
|
-
"data-layout":
|
|
46925
|
+
"data-layout": isColumn ? "column" : undefined,
|
|
46926
|
+
"data-content-first": contentFirst ? "" : undefined,
|
|
46675
46927
|
"data-animation": animation ? "" : undefined,
|
|
46676
46928
|
"data-settled": settled ? "" : undefined,
|
|
46677
46929
|
"data-content-scrolls": maxContentHeight === undefined ? undefined : "",
|
|
46930
|
+
"data-closed-content-sized": closedContentSized ? "" : undefined,
|
|
46678
46931
|
...rest,
|
|
46679
46932
|
// The protocol every command target answers (see commands.js): a
|
|
46680
46933
|
// `--navi-toggle`/`--navi-open`/`--navi-close` lands here as a
|
|
@@ -46707,10 +46960,12 @@ const Expandable = props => {
|
|
|
46707
46960
|
};
|
|
46708
46961
|
|
|
46709
46962
|
/**
|
|
46710
|
-
* The always-visible part that reveals the content: the
|
|
46711
|
-
*
|
|
46712
|
-
*
|
|
46713
|
-
*
|
|
46963
|
+
* The always-visible part that reveals the content: the focusable toggle
|
|
46964
|
+
* itself (role button — click, Space/Enter, arrow keys), holding the marker
|
|
46965
|
+
* plus whatever it is given — any markup, a function of `{ open }` included.
|
|
46966
|
+
* Controls inside it keep their own behavior and do not toggle. Its position
|
|
46967
|
+
* among the parts decides where the content goes (before the content: content
|
|
46968
|
+
* below/right; after it: content above/left).
|
|
46714
46969
|
*
|
|
46715
46970
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
46716
46971
|
* children?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
|
|
@@ -46723,32 +46978,50 @@ const ExpandableUI = ({
|
|
|
46723
46978
|
const {
|
|
46724
46979
|
opened,
|
|
46725
46980
|
loading,
|
|
46981
|
+
openDirection,
|
|
46982
|
+
toggleTo,
|
|
46726
46983
|
onUIClick,
|
|
46727
46984
|
onUIPointerDown,
|
|
46985
|
+
onUIKeyDown,
|
|
46728
46986
|
uiRef,
|
|
46729
|
-
|
|
46730
|
-
contentId,
|
|
46731
|
-
uiId
|
|
46987
|
+
contentId
|
|
46732
46988
|
} = useExpandableContext("UI");
|
|
46733
46989
|
return jsxs("div", {
|
|
46734
46990
|
ref: uiRef,
|
|
46735
46991
|
className: "navi_expandable_ui",
|
|
46992
|
+
role: "button",
|
|
46993
|
+
tabIndex: 0,
|
|
46994
|
+
"aria-expanded": opened,
|
|
46995
|
+
"aria-controls": contentId,
|
|
46736
46996
|
onClick: onUIClick,
|
|
46737
46997
|
onPointerDown: onUIPointerDown,
|
|
46998
|
+
onKeyDown: onUIKeyDown
|
|
46999
|
+
// A command from a control inside the UI part resolves its target to
|
|
47000
|
+
// the closest [aria-expanded] — this very element (see commands.js's
|
|
47001
|
+
// resolveClosestExpandable) — so it answers the protocol too. Spread as
|
|
47002
|
+
// an object: eslint's known-DOM-property check doesn't apply to navi's
|
|
47003
|
+
// own custom events.
|
|
47004
|
+
,
|
|
47005
|
+
|
|
47006
|
+
onnavi_command: e => {
|
|
47007
|
+
onNaviCommand(e);
|
|
47008
|
+
},
|
|
47009
|
+
onnavi_request_open: () => {
|
|
47010
|
+
toggleTo(true);
|
|
47011
|
+
},
|
|
47012
|
+
onnavi_request_close: () => {
|
|
47013
|
+
toggleTo(false);
|
|
47014
|
+
},
|
|
46738
47015
|
...rest,
|
|
46739
|
-
children: [jsx("
|
|
46740
|
-
|
|
46741
|
-
|
|
46742
|
-
className: "navi_expandable_toggle",
|
|
46743
|
-
"aria-expanded": opened,
|
|
46744
|
-
"aria-controls": contentId,
|
|
46745
|
-
"aria-labelledby": uiId,
|
|
47016
|
+
children: [jsx("span", {
|
|
47017
|
+
className: "navi_expandable_marker",
|
|
47018
|
+
"aria-hidden": "true",
|
|
46746
47019
|
children: jsx(SummaryMarker, {
|
|
46747
47020
|
open: opened,
|
|
46748
|
-
loading: loading
|
|
47021
|
+
loading: loading,
|
|
47022
|
+
openDirection: openDirection
|
|
46749
47023
|
})
|
|
46750
47024
|
}), jsx("div", {
|
|
46751
|
-
id: uiId,
|
|
46752
47025
|
className: "navi_expandable_ui_label",
|
|
46753
47026
|
children: typeof children === "function" ? children({
|
|
46754
47027
|
open: opened
|
|
@@ -46789,8 +47062,11 @@ const ExpandableContent = ({
|
|
|
46789
47062
|
inert: opened ? undefined : true,
|
|
46790
47063
|
...rest,
|
|
46791
47064
|
children: jsx("div", {
|
|
46792
|
-
className: "
|
|
46793
|
-
children:
|
|
47065
|
+
className: "navi_expandable_content_sizer",
|
|
47066
|
+
children: jsx("div", {
|
|
47067
|
+
className: "navi_expandable_content",
|
|
47068
|
+
children: contentMounted ? content : null
|
|
47069
|
+
})
|
|
46794
47070
|
})
|
|
46795
47071
|
});
|
|
46796
47072
|
};
|
|
@@ -46798,8 +47074,8 @@ Expandable.UI = ExpandableUI;
|
|
|
46798
47074
|
Expandable.Content = ExpandableContent;
|
|
46799
47075
|
|
|
46800
47076
|
// What a click inside the UI part must not toggle: it was aimed at the
|
|
46801
|
-
// control, not at the row. The
|
|
46802
|
-
// the call site.
|
|
47077
|
+
// control, not at the row. The UI part itself matches [role='button'] and is
|
|
47078
|
+
// the one exception, excluded at the call site.
|
|
46803
47079
|
const UI_INTERACTIVE_SELECTOR = ["a[href]", "button", "input", "select", "textarea", "label", "[role='button']", "[contenteditable='']", "[contenteditable='true']", "audio[controls]", "video[controls]"].join(", ");
|
|
46804
47080
|
const createToggleEvent = open => {
|
|
46805
47081
|
const newState = open ? "open" : "closed";
|
|
@@ -46884,7 +47160,8 @@ const ControlGroup = props => {
|
|
|
46884
47160
|
stateType: "object",
|
|
46885
47161
|
cascadeValidationToChildren: true,
|
|
46886
47162
|
aggregateChildStates: props.aggregateChildStates,
|
|
46887
|
-
distributeChildUIState: props.distributeChildUIState
|
|
47163
|
+
distributeChildUIState: props.distributeChildUIState,
|
|
47164
|
+
distributeChildStates: props.distributeChildStates
|
|
46888
47165
|
});
|
|
46889
47166
|
const {
|
|
46890
47167
|
children
|
|
@@ -46898,6 +47175,7 @@ const ControlGroup = props => {
|
|
|
46898
47175
|
,
|
|
46899
47176
|
aggregateChildStates: undefined,
|
|
46900
47177
|
distributeChildUIState: undefined,
|
|
47178
|
+
distributeChildStates: undefined,
|
|
46901
47179
|
pseudoClasses: CONTROL_GROUP_PSEUDO_CLASSES,
|
|
46902
47180
|
children: jsx(ControlgroupChildrenWrapper, {
|
|
46903
47181
|
...childrenWrapperProps,
|
|
@@ -50335,7 +50613,7 @@ const seedDefaultValueFromSignal = (props) => {
|
|
|
50335
50613
|
}
|
|
50336
50614
|
};
|
|
50337
50615
|
|
|
50338
|
-
const resolveInputProps = (props) => {
|
|
50616
|
+
const resolveInputProps = (props, { controlType = "input" } = {}) => {
|
|
50339
50617
|
// `signal` carries a bound state signal. It is left on `props` on purpose:
|
|
50340
50618
|
// `createControlInfo` (control_hooks.jsx) reads it to seed the state and to
|
|
50341
50619
|
// follow it, and `onUIAction` (ui_state_controller.js) writes user
|
|
@@ -50352,8 +50630,20 @@ const resolveInputProps = (props) => {
|
|
|
50352
50630
|
}
|
|
50353
50631
|
}
|
|
50354
50632
|
if (props.type === undefined && signalOptions.type !== undefined) {
|
|
50355
|
-
|
|
50633
|
+
const typeFromSignal =
|
|
50356
50634
|
VALIDITY_TYPE_TO_INPUT_TYPE[signalOptions.type] ?? signalOptions.type;
|
|
50635
|
+
// What a signal says is what its value IS; what a control's `type` says
|
|
50636
|
+
// is what the control is. They usually agree — a date-typed signal wants
|
|
50637
|
+
// a date field — but a boolean one maps to a checkbox, and a picker made
|
|
50638
|
+
// into a checkbox is not a picker with a different look: it is another
|
|
50639
|
+
// control, with no popup to open. A picker asked to hold a yes/no keeps
|
|
50640
|
+
// its two rows and stays itself.
|
|
50641
|
+
const wouldChangeWhatTheControlIs =
|
|
50642
|
+
controlType === "picker" &&
|
|
50643
|
+
(typeFromSignal === "checkbox" || typeFromSignal === "radio");
|
|
50644
|
+
if (!wouldChangeWhatTheControlIs) {
|
|
50645
|
+
props.type = typeFromSignal;
|
|
50646
|
+
}
|
|
50357
50647
|
}
|
|
50358
50648
|
}
|
|
50359
50649
|
|
|
@@ -55179,6 +55469,20 @@ const PickerCustomResolver = props => {
|
|
|
55179
55469
|
...props
|
|
55180
55470
|
});
|
|
55181
55471
|
}
|
|
55472
|
+
if (props.type === undefined) {
|
|
55473
|
+
// A picker with a popup of its own holds whatever the control inside it
|
|
55474
|
+
// holds — a boolean, a number, an id — and a field with no type is read
|
|
55475
|
+
// back off the DOM, where every value is a string. "false" then matches no
|
|
55476
|
+
// row, the popup empties, and that emptiness climbs back into the picker:
|
|
55477
|
+
// a value survives its own round trip only while it is text. "navi_js" is
|
|
55478
|
+
// how a field says its value is a JS one, kept beside the DOM (see
|
|
55479
|
+
// controller_registry.js) — the same thing type="array"/"object" already
|
|
55480
|
+
// say for their shapes.
|
|
55481
|
+
return jsx(PickerCustom, {
|
|
55482
|
+
...props,
|
|
55483
|
+
type: "navi_js"
|
|
55484
|
+
});
|
|
55485
|
+
}
|
|
55182
55486
|
return jsx(PickerCustom, {
|
|
55183
55487
|
...props
|
|
55184
55488
|
});
|
|
@@ -62190,7 +62494,7 @@ const PickerObjectUI = () => {
|
|
|
62190
62494
|
value,
|
|
62191
62495
|
placeholder
|
|
62192
62496
|
} = useContext(PickerContext);
|
|
62193
|
-
if (
|
|
62497
|
+
if (uiStateHoldsNothing(value)) {
|
|
62194
62498
|
if (!placeholder) {
|
|
62195
62499
|
return null;
|
|
62196
62500
|
}
|
|
@@ -62227,7 +62531,7 @@ const PickerArrayUI = () => {
|
|
|
62227
62531
|
placeholder,
|
|
62228
62532
|
maxLines
|
|
62229
62533
|
} = useContext(PickerContext);
|
|
62230
|
-
if (
|
|
62534
|
+
if (uiStateHoldsNothing(value)) {
|
|
62231
62535
|
if (!placeholder) {
|
|
62232
62536
|
return null;
|
|
62233
62537
|
}
|
|
@@ -63259,7 +63563,9 @@ const PickerFirstResolver = props => {
|
|
|
63259
63563
|
const Next = useNextResolver();
|
|
63260
63564
|
const defaultRef = useRef(null);
|
|
63261
63565
|
props.ref = props.ref || defaultRef;
|
|
63262
|
-
resolveInputProps(props
|
|
63566
|
+
resolveInputProps(props, {
|
|
63567
|
+
controlType: "picker"
|
|
63568
|
+
});
|
|
63263
63569
|
return jsx(Next, {
|
|
63264
63570
|
...props
|
|
63265
63571
|
});
|
|
@@ -64335,7 +64641,8 @@ const SpinGroup = props => {
|
|
|
64335
64641
|
stateType: "object",
|
|
64336
64642
|
cascadeValidationToChildren: true,
|
|
64337
64643
|
aggregateChildStates: props.aggregateChildStates,
|
|
64338
|
-
distributeChildUIState: props.distributeChildUIState
|
|
64644
|
+
distributeChildUIState: props.distributeChildUIState,
|
|
64645
|
+
distributeChildStates: props.distributeChildStates
|
|
64339
64646
|
});
|
|
64340
64647
|
const {
|
|
64341
64648
|
children
|
|
@@ -64352,6 +64659,7 @@ const SpinGroup = props => {
|
|
|
64352
64659
|
// not reach the DOM as unknown attributes.
|
|
64353
64660
|
aggregateChildStates: undefined,
|
|
64354
64661
|
distributeChildUIState: undefined,
|
|
64662
|
+
distributeChildStates: undefined,
|
|
64355
64663
|
baseClassName: "navi_spin_group",
|
|
64356
64664
|
pseudoClasses: SPIN_GROUP_PSEUDO_CLASSES
|
|
64357
64665
|
// What the frame and what sits between the spins are drawn from: the
|
|
@@ -68863,7 +69171,8 @@ const WheelGroup = props => {
|
|
|
68863
69171
|
stateType: "object",
|
|
68864
69172
|
cascadeValidationToChildren: true,
|
|
68865
69173
|
aggregateChildStates: props.aggregateChildStates,
|
|
68866
|
-
distributeChildUIState: props.distributeChildUIState
|
|
69174
|
+
distributeChildUIState: props.distributeChildUIState,
|
|
69175
|
+
distributeChildStates: props.distributeChildStates
|
|
68867
69176
|
});
|
|
68868
69177
|
const {
|
|
68869
69178
|
children
|
|
@@ -68922,6 +69231,7 @@ const WheelGroup = props => {
|
|
|
68922
69231
|
zoom: undefined,
|
|
68923
69232
|
aggregateChildStates: undefined,
|
|
68924
69233
|
distributeChildUIState: undefined,
|
|
69234
|
+
distributeChildStates: undefined,
|
|
68925
69235
|
baseClassName: "navi_wheel_group",
|
|
68926
69236
|
"data-horizontal": horizontal ? "" : undefined,
|
|
68927
69237
|
style: groupStyle,
|