@jsenv/navi 0.29.35 → 0.29.37
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 +459 -60
- package/dist/jsenv_navi.js.map +27 -16
- package/docs/interactions.md +77 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -21855,7 +21855,9 @@ registerNaviCommand("--navi-reset", (source, event) => {
|
|
|
21855
21855
|
* - an open popup: it closes. The popup was there for the duration of one
|
|
21856
21856
|
* decision, and the send just made it. A picker already does this when the
|
|
21857
21857
|
* send targets the popup itself (executeNaviDefine); a form inside one is the
|
|
21858
|
-
* same act, a level down
|
|
21858
|
+
* same act, a level down. A popup opened from another popup answers for
|
|
21859
|
+
* itself only — say `data-after-send="--navi-send"` on the sending control
|
|
21860
|
+
* to make the decision travel up to the surface holding it;
|
|
21859
21861
|
* - the document: nothing. A form on a page stays where it is.
|
|
21860
21862
|
*/
|
|
21861
21863
|
// What follows a send that went through — and it is asked of the button that
|
|
@@ -21909,7 +21911,21 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
21909
21911
|
if (target.getAttribute("aria-expanded") === "true") {
|
|
21910
21912
|
return {
|
|
21911
21913
|
target,
|
|
21912
|
-
implementation: () =>
|
|
21914
|
+
implementation: () => {
|
|
21915
|
+
const result = executeNaviDefine(source, event, target);
|
|
21916
|
+
// Only what was explicitly asked for: the surface above a popup is not
|
|
21917
|
+
// answered by a decision taken inside it (see resolveAfterSend), so a
|
|
21918
|
+
// popup opened from another popup closes alone unless the send says
|
|
21919
|
+
// otherwise. Triggered from the target — the popup that just closed no
|
|
21920
|
+
// longer holds the source.
|
|
21921
|
+
const afterSend =
|
|
21922
|
+
source.getAttribute("data-after-send") ||
|
|
21923
|
+
target.getAttribute("data-after-send");
|
|
21924
|
+
if (afterSend) {
|
|
21925
|
+
triggerNaviCommand(target, afterSend, event, { optional: true });
|
|
21926
|
+
}
|
|
21927
|
+
return result;
|
|
21928
|
+
},
|
|
21913
21929
|
};
|
|
21914
21930
|
}
|
|
21915
21931
|
|
|
@@ -23384,7 +23400,8 @@ const useUIStateController = (
|
|
|
23384
23400
|
controller.id = props.id; // never supposed to change, not supported for now
|
|
23385
23401
|
controller.name = props.name;
|
|
23386
23402
|
controller.parentUIStateController = parentUIStateController;
|
|
23387
|
-
const { value, hasStateProp, state, stateInitial } =
|
|
23403
|
+
const { value, hasStateProp, state, stateInitial, stateFromSignal } =
|
|
23404
|
+
controlInfo;
|
|
23388
23405
|
controller.value = value;
|
|
23389
23406
|
if (hasStateProp) {
|
|
23390
23407
|
controller.hasStateProp = true;
|
|
@@ -23393,9 +23410,27 @@ const useUIStateController = (
|
|
|
23393
23410
|
controller.state = state;
|
|
23394
23411
|
controller.setUIState(state, new CustomEvent("state_prop_change"));
|
|
23395
23412
|
}
|
|
23396
|
-
} else
|
|
23397
|
-
controller.hasStateProp
|
|
23398
|
-
|
|
23413
|
+
} else {
|
|
23414
|
+
if (controller.hasStateProp) {
|
|
23415
|
+
controller.hasStateProp = false;
|
|
23416
|
+
controller.state = stateInitial;
|
|
23417
|
+
}
|
|
23418
|
+
if (controlInfo.signal) {
|
|
23419
|
+
// The other half of a bound signal: the control follows it when
|
|
23420
|
+
// something else writes it. Compared against `state` (what the signal
|
|
23421
|
+
// last said), never against the ui state — otherwise typing into an
|
|
23422
|
+
// uncontrolled control would be undone on the next render. A write
|
|
23423
|
+
// coming from the control itself lands here with the value already in
|
|
23424
|
+
// place, and setUIState treats an unchanged state as a no-op.
|
|
23425
|
+
const currentState = controller.state;
|
|
23426
|
+
if (!compareTwoJsValues(stateFromSignal, currentState)) {
|
|
23427
|
+
controller.state = stateFromSignal;
|
|
23428
|
+
controller.setUIState(
|
|
23429
|
+
stateFromSignal,
|
|
23430
|
+
new CustomEvent("state_prop_change"),
|
|
23431
|
+
);
|
|
23432
|
+
}
|
|
23433
|
+
}
|
|
23399
23434
|
}
|
|
23400
23435
|
return {
|
|
23401
23436
|
ref: props.ref,
|
|
@@ -23675,10 +23710,9 @@ const useUIGroupStateController = (
|
|
|
23675
23710
|
const hasValueProp = Object.hasOwn(props, "value");
|
|
23676
23711
|
const hasOwnDefaultValueProp = Object.hasOwn(props, "defaultValue");
|
|
23677
23712
|
// A bound signal seeds the group the way `defaultValue` does — uncontrolled,
|
|
23678
|
-
// with the signal's current value as what it starts on. Write-back is
|
|
23679
|
-
//
|
|
23680
|
-
//
|
|
23681
|
-
// register, exactly as they would be from a defaultValue.
|
|
23713
|
+
// with the signal's current value as what it starts on. Write-back is handled
|
|
23714
|
+
// by applyState's own boundSignal; the read half is below: children are
|
|
23715
|
+
// placed from it when they register, and again whenever it moves.
|
|
23682
23716
|
const boundSignal = hasValueProp ? undefined : props.signal;
|
|
23683
23717
|
const hasDefaultValueProp = hasOwnDefaultValueProp || Boolean(boundSignal);
|
|
23684
23718
|
const { id, name, value, uiAction } = props;
|
|
@@ -24096,6 +24130,7 @@ const useUIGroupStateController = (
|
|
|
24096
24130
|
const { controller } = s;
|
|
24097
24131
|
const prevValue = controller.value;
|
|
24098
24132
|
const prevHasValueProp = controller.hasValueProp;
|
|
24133
|
+
const prevDefaultValue = controller.defaultValue;
|
|
24099
24134
|
controller.props = props;
|
|
24100
24135
|
controller.ref = ref;
|
|
24101
24136
|
controller.id = id;
|
|
@@ -24124,6 +24159,31 @@ const useUIGroupStateController = (
|
|
|
24124
24159
|
}
|
|
24125
24160
|
controller.syncInternalState(value);
|
|
24126
24161
|
}
|
|
24162
|
+
if (
|
|
24163
|
+
boundSignal &&
|
|
24164
|
+
!hasValueProp &&
|
|
24165
|
+
!compareTwoJsValues(defaultValue, prevDefaultValue)
|
|
24166
|
+
) {
|
|
24167
|
+
// The signal moved from the outside: the children are placed from it
|
|
24168
|
+
// again, exactly as they were when they registered. Without this a
|
|
24169
|
+
// group would answer a write to its own signal by silently writing its
|
|
24170
|
+
// former value back over it on the next child interaction.
|
|
24171
|
+
const propagateDownEvent = new CustomEvent(
|
|
24172
|
+
"propagate_down_set_ui_state",
|
|
24173
|
+
{ detail: {} },
|
|
24174
|
+
);
|
|
24175
|
+
for (const childUIStateController of childUIStateControllerArray) {
|
|
24176
|
+
if (!shouldPropagateStateToChild(childUIStateController)) continue;
|
|
24177
|
+
if (childUIStateController.hasStateProp) continue;
|
|
24178
|
+
const childNewState = controller.distributeChildUIState(
|
|
24179
|
+
defaultValue,
|
|
24180
|
+
childUIStateController,
|
|
24181
|
+
);
|
|
24182
|
+
if (childNewState === CANNOT_DERIVE) continue;
|
|
24183
|
+
childUIStateController.setUIState(childNewState, propagateDownEvent);
|
|
24184
|
+
}
|
|
24185
|
+
controller.syncInternalState(defaultValue);
|
|
24186
|
+
}
|
|
24127
24187
|
|
|
24128
24188
|
return {
|
|
24129
24189
|
ref,
|
|
@@ -25248,6 +25308,13 @@ const createControlInfo = (props, {
|
|
|
25248
25308
|
const signal = Object.hasOwn(props, "signal") ? props.signal : undefined;
|
|
25249
25309
|
// For a checkbox/radio the signal holds the boolean checked state, not the value.
|
|
25250
25310
|
let signalHoldsChecked = false;
|
|
25311
|
+
// What the signal says the control shows, re-read on every render. A signal
|
|
25312
|
+
// carrying a default of its own seeds `defaultValue` (resolveInputProps), so
|
|
25313
|
+
// such a control is uncontrolled — but the binding still has to work in both
|
|
25314
|
+
// directions: whoever writes the signal from the outside expects the control
|
|
25315
|
+
// to move. This is what the controller re-syncs on (see useUIStateController),
|
|
25316
|
+
// and an emptied signal puts the control back on its suggestion.
|
|
25317
|
+
let stateFromSignal;
|
|
25251
25318
|
if (controlType === "input") {
|
|
25252
25319
|
if (typeProp === "checkbox" || typeProp === "radio") {
|
|
25253
25320
|
statePropName = "checked";
|
|
@@ -25259,10 +25326,13 @@ const createControlInfo = (props, {
|
|
|
25259
25326
|
stateInitial = props.checked ? value : undefined;
|
|
25260
25327
|
} else if (props.defaultChecked) {
|
|
25261
25328
|
// resolveInputProps may seed defaultChecked from a bound signal's
|
|
25262
|
-
// default: the control stays uncontrolled and
|
|
25263
|
-
//
|
|
25329
|
+
// default: the control stays uncontrolled and follows the signal
|
|
25330
|
+
// through stateFromSignal.
|
|
25264
25331
|
hasStateProp = false;
|
|
25265
25332
|
stateInitial = value;
|
|
25333
|
+
if (signal) {
|
|
25334
|
+
stateFromSignal = signal.value ? value : undefined;
|
|
25335
|
+
}
|
|
25266
25336
|
} else if (signal) {
|
|
25267
25337
|
// A bound signal with no resolved default: its live value seeds state.
|
|
25268
25338
|
hasStateProp = true;
|
|
@@ -25289,6 +25359,9 @@ const createControlInfo = (props, {
|
|
|
25289
25359
|
// url or set by whoever owns it. Taking the default here would show a
|
|
25290
25360
|
// suggestion in place of the value on every reload.
|
|
25291
25361
|
stateInitial = signal && signal.value !== undefined ? signal.value : props.defaultValue;
|
|
25362
|
+
if (signal) {
|
|
25363
|
+
stateFromSignal = stateInitial;
|
|
25364
|
+
}
|
|
25292
25365
|
} else if (signal) {
|
|
25293
25366
|
// A plain bound signal with no default (e.g. Wheel): its live value
|
|
25294
25367
|
// seeds and controls the state.
|
|
@@ -25322,6 +25395,9 @@ const createControlInfo = (props, {
|
|
|
25322
25395
|
// Same precedence as an input above: the signal's value is the answer,
|
|
25323
25396
|
// defaultValue only the suggestion to start from.
|
|
25324
25397
|
stateInitial = signal && signal.value !== undefined ? signal.value : props.defaultValue;
|
|
25398
|
+
if (signal) {
|
|
25399
|
+
stateFromSignal = stateInitial;
|
|
25400
|
+
}
|
|
25325
25401
|
} else if (signal) {
|
|
25326
25402
|
hasStateProp = true;
|
|
25327
25403
|
stateInitial = signal.value;
|
|
@@ -25345,6 +25421,7 @@ const createControlInfo = (props, {
|
|
|
25345
25421
|
value,
|
|
25346
25422
|
signal,
|
|
25347
25423
|
signalHoldsChecked,
|
|
25424
|
+
stateFromSignal,
|
|
25348
25425
|
readOnlySupported,
|
|
25349
25426
|
disabledSupported
|
|
25350
25427
|
};
|
|
@@ -36881,12 +36958,19 @@ const css$R = /* css */`
|
|
|
36881
36958
|
travelling. */
|
|
36882
36959
|
.navi_route_travel {
|
|
36883
36960
|
position: relative;
|
|
36884
|
-
|
|
36885
|
-
|
|
36886
|
-
|
|
36887
|
-
|
|
36888
|
-
|
|
36889
|
-
|
|
36961
|
+
|
|
36962
|
+
/* What a touch may do here — only where a touch travels at all
|
|
36963
|
+
(data-travel-by-drag, set from travelByDrag): the axis the pages travel
|
|
36964
|
+
on is taken, the other one is left to the page, so a list still scrolls
|
|
36965
|
+
under the same finger. A box that takes no gesture takes no axis either:
|
|
36966
|
+
it is a plain box around the page, and everything in it scrolls as it
|
|
36967
|
+
would anywhere else. Same reading as SlideContainer's own. */
|
|
36968
|
+
&[data-travel-by-drag="x"] {
|
|
36969
|
+
touch-action: pan-y;
|
|
36970
|
+
}
|
|
36971
|
+
&[data-travel-by-drag="y"] {
|
|
36972
|
+
touch-action: pan-x;
|
|
36973
|
+
}
|
|
36890
36974
|
}
|
|
36891
36975
|
|
|
36892
36976
|
/* Only while a travel of OURS is playing: everything below changes how the
|
|
@@ -37107,6 +37191,11 @@ const css$R = /* css */`
|
|
|
37107
37191
|
* page under six entries. A tab pressed is the other case and pushes, which
|
|
37108
37192
|
* is what its <Link> already does.
|
|
37109
37193
|
*
|
|
37194
|
+
* Every other prop lands on the box itself (`id`, `data-testid`, `className`,
|
|
37195
|
+
* …), which is how one of these is named: a page can hold several — a row of
|
|
37196
|
+
* sections inside the box the whole application travels in — and the class they
|
|
37197
|
+
* share plus their axis are not enough to tell them apart from the outside.
|
|
37198
|
+
*
|
|
37110
37199
|
* The pages are cut at the edge of this box while they travel, which is written
|
|
37111
37200
|
* on the transition's own pseudo-elements — no overflow of the document reaches
|
|
37112
37201
|
* pictures drawn in the top layer. It needs nothing of the browser beyond view
|
|
@@ -46723,12 +46812,13 @@ const NAVI_TYPE_DEFAULTS = {
|
|
|
46723
46812
|
*/
|
|
46724
46813
|
/**
|
|
46725
46814
|
* A bound signal that carries a default of its own says the same thing on every
|
|
46726
|
-
* control: the control starts there and is
|
|
46727
|
-
*
|
|
46728
|
-
*
|
|
46729
|
-
*
|
|
46730
|
-
*
|
|
46731
|
-
*
|
|
46815
|
+
* control: the control starts there and stays uncontrolled, which is what makes
|
|
46816
|
+
* a form read the value shown as a SUGGESTION rather than as something it
|
|
46817
|
+
* already holds. Uncontrolled here is about what the control HOLDS, not about
|
|
46818
|
+
* whether it follows the signal — the binding stays two-way either way (see
|
|
46819
|
+
* stateFromSignal in control_hooks.jsx). Written once and used by everything
|
|
46820
|
+
* that takes a `signal`, so one signal cannot mean two different things
|
|
46821
|
+
* depending on which control it was handed to.
|
|
46732
46822
|
*/
|
|
46733
46823
|
const seedDefaultValueFromSignal = (props) => {
|
|
46734
46824
|
const signalOptions = props.signal?.options;
|
|
@@ -46750,11 +46840,11 @@ const seedDefaultValueFromSignal = (props) => {
|
|
|
46750
46840
|
|
|
46751
46841
|
const resolveInputProps = (props) => {
|
|
46752
46842
|
// `signal` carries a bound state signal. It is left on `props` on purpose:
|
|
46753
|
-
// `createControlInfo` (control_hooks.jsx) reads it to seed the state and
|
|
46754
|
-
// `onUIAction` (ui_state_controller.js) writes user
|
|
46755
|
-
// it. Here we only derive input defaults (type/min/max,
|
|
46756
|
-
// defaultChecked) from the signal's `options`, so the control
|
|
46757
|
-
// uncontrolled-with-default while still
|
|
46843
|
+
// `createControlInfo` (control_hooks.jsx) reads it to seed the state and to
|
|
46844
|
+
// follow it, and `onUIAction` (ui_state_controller.js) writes user
|
|
46845
|
+
// interactions back into it. Here we only derive input defaults (type/min/max,
|
|
46846
|
+
// defaultValue/defaultChecked) from the signal's `options`, so the control
|
|
46847
|
+
// ends up uncontrolled-with-default while still bound to the signal.
|
|
46758
46848
|
const signal = props.signal;
|
|
46759
46849
|
if (signal) {
|
|
46760
46850
|
const signalOptions = signal.options;
|
|
@@ -51371,6 +51461,12 @@ installImportMetaCssBuild(import.meta);const css$y = /* css */`
|
|
|
51371
51461
|
--dialog-outline-width: var(--picker-outline-width);
|
|
51372
51462
|
--dialog-outline-color: var(--picker-outline-color);
|
|
51373
51463
|
|
|
51464
|
+
/* No fallback on purpose (same as --popover-max-height above): unset
|
|
51465
|
+
picker props leave these declarations invalid at computed-value
|
|
51466
|
+
time, so the dialog keeps its own ceilings. */
|
|
51467
|
+
--dialog-max-width: var(--picker-dialog-max-width);
|
|
51468
|
+
--dialog-max-height: var(--picker-dialog-max-height);
|
|
51469
|
+
|
|
51374
51470
|
/* Dialog itself already sizes min-width off --anchor-width — only
|
|
51375
51471
|
the cursor reset below is picker-specific here. */
|
|
51376
51472
|
cursor: default; /* Reset pointer cursor within the select */
|
|
@@ -51489,7 +51585,13 @@ const PickerCustom = props => {
|
|
|
51489
51585
|
ref,
|
|
51490
51586
|
mode: modeProp,
|
|
51491
51587
|
open,
|
|
51492
|
-
defaultOpen
|
|
51588
|
+
defaultOpen,
|
|
51589
|
+
// What Escape means for this picker. "cancel" (the default) puts back the
|
|
51590
|
+
// value the picker had at open and, for a dialog, goes back in history —
|
|
51591
|
+
// so everything written to the url while it was open goes back too.
|
|
51592
|
+
// "close" makes Escape say the same thing as clicking outside: keep what
|
|
51593
|
+
// was chosen, close the popup.
|
|
51594
|
+
escapeEffect = "cancel"
|
|
51493
51595
|
} = props;
|
|
51494
51596
|
// Resolve the id the same way useControlProps does (own id > Field's id > generated id)
|
|
51495
51597
|
// before computing popupId below, so two Pickers without an explicit id never collide.
|
|
@@ -51521,6 +51623,7 @@ const PickerCustom = props => {
|
|
|
51521
51623
|
// otherwise leak through PickerContentInsidePopup's own ...rest).
|
|
51522
51624
|
delete pickerProps.open;
|
|
51523
51625
|
delete pickerProps.defaultOpen;
|
|
51626
|
+
delete pickerProps.escapeEffect;
|
|
51524
51627
|
const popupProps = {};
|
|
51525
51628
|
Object.assign(pickerProps, {
|
|
51526
51629
|
popupProps,
|
|
@@ -51789,11 +51892,12 @@ const PickerCustom = props => {
|
|
|
51789
51892
|
if (!openController.opened) {
|
|
51790
51893
|
return null;
|
|
51791
51894
|
}
|
|
51895
|
+
const isCancel = escapeEffect === "cancel";
|
|
51792
51896
|
return {
|
|
51793
|
-
name: "escape_to_cancel",
|
|
51897
|
+
name: isCancel ? "escape_to_cancel" : "escape_to_close",
|
|
51794
51898
|
allowed: () => {
|
|
51795
51899
|
requestClose(e, {
|
|
51796
|
-
isCancel
|
|
51900
|
+
isCancel
|
|
51797
51901
|
});
|
|
51798
51902
|
e.preventDefault(); // prevent browser from closing the dialog (if any)
|
|
51799
51903
|
}
|
|
@@ -51952,7 +52056,7 @@ const PickerContentInsidePopup = props => {
|
|
|
51952
52056
|
animation: animation,
|
|
51953
52057
|
positionArea: isPopover ? positionArea ?? (popoverMode === "nearby" ? "bottom-start" : "inset(top-left)") : positionArea,
|
|
51954
52058
|
marginWithAnchor: isPopover ? popoverSpacing : undefined,
|
|
51955
|
-
marginWithContainer: marginWithContainer === undefined && isPopover ? popoverSpacing : marginWithContainer
|
|
52059
|
+
marginWithContainer: marginWithContainer === undefined && isPopover ? popoverSpacing : marginWithContainer,
|
|
51956
52060
|
scrollCapture: scrollCapture,
|
|
51957
52061
|
pointerInteractionOutsideEffect: pointerLock ? "capture" : pointerInteractionOutsideEffect,
|
|
51958
52062
|
focusCapture: isPopover ? focusCapture : undefined,
|
|
@@ -52845,6 +52949,11 @@ installImportMetaCssBuild(import.meta);const css$w = /* css */`
|
|
|
52845
52949
|
}
|
|
52846
52950
|
`;
|
|
52847
52951
|
const SelectableListMultipleContext = createContext(false);
|
|
52952
|
+
// A row of a selectable list is selectable — the list is what decides, and a
|
|
52953
|
+
// row says nothing unless it wants out (`selectable={false}` on a row that is
|
|
52954
|
+
// only there to be read). Also set to false by a non-selectable list, so a list
|
|
52955
|
+
// nested inside a selectable row does not inherit the outer list's answer.
|
|
52956
|
+
const ListSelectableContext = createContext(false);
|
|
52848
52957
|
// Interactive variant: manages hover/keyboard/selection state and handles the
|
|
52849
52958
|
// navi event protocol. When an action is provided it binds the action to ui state
|
|
52850
52959
|
// and fires it on select. When only uiAction is provided it calls it directly.
|
|
@@ -52855,8 +52964,11 @@ const ListSelectableResolver = props => {
|
|
|
52855
52964
|
...props
|
|
52856
52965
|
});
|
|
52857
52966
|
}
|
|
52858
|
-
return jsx(
|
|
52859
|
-
|
|
52967
|
+
return jsx(ListSelectableContext.Provider, {
|
|
52968
|
+
value: false,
|
|
52969
|
+
children: jsx(Next, {
|
|
52970
|
+
...props
|
|
52971
|
+
})
|
|
52860
52972
|
});
|
|
52861
52973
|
};
|
|
52862
52974
|
const ListSelectable = props => {
|
|
@@ -53146,17 +53258,27 @@ const ListSelectable = props => {
|
|
|
53146
53258
|
children: props.children
|
|
53147
53259
|
})
|
|
53148
53260
|
});
|
|
53149
|
-
return jsx(
|
|
53150
|
-
value:
|
|
53151
|
-
children:
|
|
53261
|
+
return jsx(ListSelectableContext.Provider, {
|
|
53262
|
+
value: true,
|
|
53263
|
+
children: jsx(SelectableListMultipleContext.Provider, {
|
|
53264
|
+
value: multiple,
|
|
53265
|
+
children: listVnode
|
|
53266
|
+
})
|
|
53152
53267
|
});
|
|
53153
53268
|
};
|
|
53154
53269
|
const SelectableRealInputContext = createContext(null);
|
|
53155
53270
|
const ListItemSelectableResolver = props => {
|
|
53156
53271
|
const Next = useNextResolver();
|
|
53157
|
-
|
|
53272
|
+
const listSelectable = useContext(ListSelectableContext);
|
|
53273
|
+
// A header/footer row is a title, not a choice — it stays out even in a
|
|
53274
|
+
// selectable list. (A skeleton row never reaches here: its own resolver runs
|
|
53275
|
+
// before this one.)
|
|
53276
|
+
const isHeaderOrFooter = Boolean(props.header || props.footer);
|
|
53277
|
+
const selectable = props.selectable ?? (isHeaderOrFooter || props.role === "presentation" ? false : listSelectable);
|
|
53278
|
+
if (selectable) {
|
|
53158
53279
|
return jsx(ListItemSelectable, {
|
|
53159
|
-
...props
|
|
53280
|
+
...props,
|
|
53281
|
+
selectable: true
|
|
53160
53282
|
});
|
|
53161
53283
|
}
|
|
53162
53284
|
return jsx(Next, {
|
|
@@ -53182,6 +53304,10 @@ const ListItemSelectable = props => {
|
|
|
53182
53304
|
selected,
|
|
53183
53305
|
pointed,
|
|
53184
53306
|
selectableArea = "all",
|
|
53307
|
+
// The row's own click handler, kept out of the control props below: those
|
|
53308
|
+
// describe the hidden input that carries the selection, and a caller
|
|
53309
|
+
// writing onClick on a <List.Item> is talking about the row they see.
|
|
53310
|
+
onClick,
|
|
53185
53311
|
...rest
|
|
53186
53312
|
} = props;
|
|
53187
53313
|
const multiple = useContext(SelectableListMultipleContext);
|
|
@@ -53236,6 +53362,7 @@ const ListItemSelectable = props => {
|
|
|
53236
53362
|
...basePseudoState
|
|
53237
53363
|
},
|
|
53238
53364
|
ref: props.ref,
|
|
53365
|
+
onClick: onClick,
|
|
53239
53366
|
selectable: undefined,
|
|
53240
53367
|
"navi-selectable-area-all": selectableArea === "all" ? "" : undefined,
|
|
53241
53368
|
children: [jsx(SelectableRealInput, {
|
|
@@ -53514,6 +53641,26 @@ const css$v = /* css */`
|
|
|
53514
53641
|
--list-border-width-default: 0px;
|
|
53515
53642
|
}
|
|
53516
53643
|
|
|
53644
|
+
/* Same reasoning, for the corners: a dialog squares off whatever corner
|
|
53645
|
+
lands on its container's own (see the data-flush-* rules in dialog.jsx —
|
|
53646
|
+
a bottom sheet from dockedOnTouch squares its two bottom ones). A list
|
|
53647
|
+
drawn right against that corner has to square the same one, otherwise its
|
|
53648
|
+
own radius carves a notch out of the popup's square corner. Direct child
|
|
53649
|
+
only: any deeper and the list is presumably inset from the popup's edge,
|
|
53650
|
+
where its own radius is the right one. */
|
|
53651
|
+
.navi_dialog[data-flush-top][data-flush-left] > .navi_list_container {
|
|
53652
|
+
border-top-left-radius: 0;
|
|
53653
|
+
}
|
|
53654
|
+
.navi_dialog[data-flush-top][data-flush-right] > .navi_list_container {
|
|
53655
|
+
border-top-right-radius: 0;
|
|
53656
|
+
}
|
|
53657
|
+
.navi_dialog[data-flush-bottom][data-flush-right] > .navi_list_container {
|
|
53658
|
+
border-bottom-right-radius: 0;
|
|
53659
|
+
}
|
|
53660
|
+
.navi_dialog[data-flush-bottom][data-flush-left] > .navi_list_container {
|
|
53661
|
+
border-bottom-left-radius: 0;
|
|
53662
|
+
}
|
|
53663
|
+
|
|
53517
53664
|
.navi_list_container {
|
|
53518
53665
|
--x-list-border-radius: var(--list-border-radius);
|
|
53519
53666
|
--x-list-border-width: var(
|
|
@@ -56359,7 +56506,8 @@ const LIST_ITEM_STYLE_CSS_VARS = {
|
|
|
56359
56506
|
* @param {boolean} [props.selectable]
|
|
56360
56507
|
* The item participates in selection (radio or checkbox depending on whether
|
|
56361
56508
|
* the parent List has `multiple`). Requires `value` and typically a
|
|
56362
|
-
* <SelectableInput /> child.
|
|
56509
|
+
* <SelectableInput /> child. Inherited from `<List selectable>` — pass
|
|
56510
|
+
* `false` for a row that is only there to be read, and nothing otherwise.
|
|
56363
56511
|
* @param {any} [props.value]
|
|
56364
56512
|
* The JS value emitted by the list's action/uiAction when this item is
|
|
56365
56513
|
* selected. Can be any type (string, number, object…).
|
|
@@ -57661,7 +57809,7 @@ const PickerTypeResolver = props => {
|
|
|
57661
57809
|
const PickerText = props => {
|
|
57662
57810
|
const Next = useNextResolver();
|
|
57663
57811
|
return jsx(Next, {
|
|
57664
|
-
|
|
57812
|
+
rightSlotIcon: jsx(PencilSvg, {}),
|
|
57665
57813
|
...props
|
|
57666
57814
|
});
|
|
57667
57815
|
};
|
|
@@ -57740,7 +57888,7 @@ const PickerColor = props => {
|
|
|
57740
57888
|
const Next = useNextResolver();
|
|
57741
57889
|
return jsx(Next, {
|
|
57742
57890
|
ui: jsx(PickerColorUI, {}),
|
|
57743
|
-
|
|
57891
|
+
rightSlotIcon: jsx(ColorSvg, {}),
|
|
57744
57892
|
type: "color",
|
|
57745
57893
|
...props
|
|
57746
57894
|
});
|
|
@@ -57764,7 +57912,7 @@ const PickerDate = props => {
|
|
|
57764
57912
|
const Next = useNextResolver();
|
|
57765
57913
|
return jsx(Next, {
|
|
57766
57914
|
ui: jsx(PickerDateUI, {}),
|
|
57767
|
-
|
|
57915
|
+
rightSlotIcon: jsx(CalendarSvg, {}),
|
|
57768
57916
|
...props,
|
|
57769
57917
|
type: "date"
|
|
57770
57918
|
});
|
|
@@ -57796,7 +57944,7 @@ const PickerMonth = props => {
|
|
|
57796
57944
|
const Next = useNextResolver();
|
|
57797
57945
|
return jsx(Next, {
|
|
57798
57946
|
ui: jsx(PickerMonthUI, {}),
|
|
57799
|
-
|
|
57947
|
+
rightSlotIcon: jsx(CalendarSvg, {}),
|
|
57800
57948
|
...props,
|
|
57801
57949
|
type: "month"
|
|
57802
57950
|
});
|
|
@@ -57827,7 +57975,7 @@ const PickerWeek = props => {
|
|
|
57827
57975
|
const Next = useNextResolver();
|
|
57828
57976
|
return jsx(Next, {
|
|
57829
57977
|
ui: jsx(PickerWeekUI, {}),
|
|
57830
|
-
|
|
57978
|
+
rightSlotIcon: jsx(CalendarSvg, {}),
|
|
57831
57979
|
...props,
|
|
57832
57980
|
type: "week"
|
|
57833
57981
|
});
|
|
@@ -57858,7 +58006,7 @@ const PickerTime = props => {
|
|
|
57858
58006
|
const Next = useNextResolver();
|
|
57859
58007
|
return jsx(Next, {
|
|
57860
58008
|
ui: jsx(PickerTimeUI, {}),
|
|
57861
|
-
|
|
58009
|
+
rightSlotIcon: jsx(ClockSvg, {}),
|
|
57862
58010
|
...props,
|
|
57863
58011
|
type: "time"
|
|
57864
58012
|
});
|
|
@@ -57888,7 +58036,7 @@ const PickerDuration = props => {
|
|
|
57888
58036
|
const Next = useNextResolver();
|
|
57889
58037
|
return jsx(Next, {
|
|
57890
58038
|
ui: jsx(PickerDurationUI, {}),
|
|
57891
|
-
|
|
58039
|
+
rightSlotIcon: jsx(DurationSvg, {}),
|
|
57892
58040
|
...props,
|
|
57893
58041
|
type: "text",
|
|
57894
58042
|
"navi-input-type": "duration"
|
|
@@ -57919,7 +58067,7 @@ const PickerDatetime = props => {
|
|
|
57919
58067
|
const Next = useNextResolver();
|
|
57920
58068
|
return jsx(Next, {
|
|
57921
58069
|
ui: jsx(PickerDatetimeUI, {}),
|
|
57922
|
-
|
|
58070
|
+
rightSlotIcon: jsx(CalendarSvg, {}),
|
|
57923
58071
|
...props,
|
|
57924
58072
|
type: "datetime-local"
|
|
57925
58073
|
});
|
|
@@ -57948,7 +58096,7 @@ const PickerFile = props => {
|
|
|
57948
58096
|
const Next = useNextResolver();
|
|
57949
58097
|
return jsx(Next, {
|
|
57950
58098
|
ui: jsx(PickerFileUI, {}),
|
|
57951
|
-
|
|
58099
|
+
rightSlotIcon: jsx(FileSvg, {}),
|
|
57952
58100
|
type: "file",
|
|
57953
58101
|
...props
|
|
57954
58102
|
});
|
|
@@ -58288,6 +58436,28 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
|
58288
58436
|
--x-picker-background-color: transparent;
|
|
58289
58437
|
--x-picker-icon-color: currentColor;
|
|
58290
58438
|
}
|
|
58439
|
+
/* discrete: no box at rest, a background on hover — the same word Button
|
|
58440
|
+
uses, and the same drawing. What is read is the value, not the field
|
|
58441
|
+
around it; the chevron in the right slot is what still says it opens. */
|
|
58442
|
+
&[data-variant="discrete"] {
|
|
58443
|
+
--picker-border-width: 0px; /* must carry a unit (px) — used in calc() to offset the custom input overlay */
|
|
58444
|
+
--x-picker-border-color: transparent;
|
|
58445
|
+
--x-picker-background-color: transparent;
|
|
58446
|
+
|
|
58447
|
+
&[data-hover] {
|
|
58448
|
+
--x-picker-border-color: transparent;
|
|
58449
|
+
--x-picker-background-color: color-mix(
|
|
58450
|
+
in srgb,
|
|
58451
|
+
currentColor 8%,
|
|
58452
|
+
transparent
|
|
58453
|
+
);
|
|
58454
|
+
}
|
|
58455
|
+
&[data-readonly],
|
|
58456
|
+
&[data-disabled] {
|
|
58457
|
+
--x-picker-border-color: transparent;
|
|
58458
|
+
--x-picker-background-color: transparent;
|
|
58459
|
+
}
|
|
58460
|
+
}
|
|
58291
58461
|
&[data-variant="headless"] {
|
|
58292
58462
|
--x-picker-padding-top: 0;
|
|
58293
58463
|
--x-picker-padding-right: 0;
|
|
@@ -58318,8 +58488,8 @@ const PickerButton = props => {
|
|
|
58318
58488
|
const {
|
|
58319
58489
|
ref,
|
|
58320
58490
|
variant,
|
|
58321
|
-
|
|
58322
|
-
|
|
58491
|
+
rightSlotIcon,
|
|
58492
|
+
rightSlotIconSize = "inherit",
|
|
58323
58493
|
placeholder,
|
|
58324
58494
|
ui,
|
|
58325
58495
|
maxLines = 1,
|
|
@@ -58364,8 +58534,8 @@ const PickerButton = props => {
|
|
|
58364
58534
|
basePseudoState: basePseudoState,
|
|
58365
58535
|
styleCSSVars: PickerStyleCSSVars,
|
|
58366
58536
|
variant: undefined,
|
|
58367
|
-
|
|
58368
|
-
|
|
58537
|
+
rightSlotIcon: undefined,
|
|
58538
|
+
rightSlotIconSize: undefined,
|
|
58369
58539
|
ui: undefined,
|
|
58370
58540
|
maxLines: undefined,
|
|
58371
58541
|
popupWidthFitContent: undefined,
|
|
@@ -58502,7 +58672,7 @@ const PickerButton = props => {
|
|
|
58502
58672
|
flex: true,
|
|
58503
58673
|
align: "center",
|
|
58504
58674
|
children: jsx(Icon, {
|
|
58505
|
-
size:
|
|
58675
|
+
size: rightSlotIconSize,
|
|
58506
58676
|
lineOverflow: "allow",
|
|
58507
58677
|
children: jsx(CloseSvg, {})
|
|
58508
58678
|
})
|
|
@@ -58511,9 +58681,9 @@ const PickerButton = props => {
|
|
|
58511
58681
|
// character — a caller asking for a bigger one wants it bigger,
|
|
58512
58682
|
// not capped at the height of the line it sits on
|
|
58513
58683
|
jsx(Icon, {
|
|
58514
|
-
size:
|
|
58684
|
+
size: rightSlotIconSize,
|
|
58515
58685
|
lineOverflow: "allow",
|
|
58516
|
-
children:
|
|
58686
|
+
children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
|
|
58517
58687
|
})
|
|
58518
58688
|
})]
|
|
58519
58689
|
}), jsx(ControlFacadeChildrenWrapper, {
|
|
@@ -58604,6 +58774,8 @@ const PickerStyleCSSVars = {
|
|
|
58604
58774
|
"borderWidth": "--picker-border-width",
|
|
58605
58775
|
"borderRadius": "--picker-border-radius",
|
|
58606
58776
|
"popoverMaxHeight": "--picker-popover-max-height",
|
|
58777
|
+
"dialogMaxWidth": "--picker-dialog-max-width",
|
|
58778
|
+
"dialogMaxHeight": "--picker-dialog-max-height",
|
|
58607
58779
|
"popupBackgroundColor": "--picker-popup-background-color",
|
|
58608
58780
|
"popupBorderRadius": "--picker-popup-border-radius",
|
|
58609
58781
|
"dialogBorderWidth": "--picker-dialog-border-width",
|
|
@@ -58683,11 +58855,14 @@ const PickerFirstResolver = props => {
|
|
|
58683
58855
|
* popoverMode?: "nearby" | "overlay",
|
|
58684
58856
|
* positionArea?: string,
|
|
58685
58857
|
* popupWidthFitContent?: boolean,
|
|
58686
|
-
* variant?: "icon" | "headless",
|
|
58687
|
-
*
|
|
58858
|
+
* variant?: "icon" | "headless" | "discrete",
|
|
58859
|
+
* rightSlotIcon?: import("ignore:preact").ComponentChildren,
|
|
58860
|
+
* rightSlotIconSize?: number | string,
|
|
58688
58861
|
* maxLines?: number,
|
|
58689
58862
|
* slotSpacing?: number | string,
|
|
58690
58863
|
* popoverMaxHeight?: number | string,
|
|
58864
|
+
* dialogMaxWidth?: number | string,
|
|
58865
|
+
* dialogMaxHeight?: number | string,
|
|
58691
58866
|
* popupBackgroundColor?: string,
|
|
58692
58867
|
* popupBorderRadius?: number | string,
|
|
58693
58868
|
* clearable?: boolean,
|
|
@@ -58695,6 +58870,9 @@ const PickerFirstResolver = props => {
|
|
|
58695
58870
|
* dialogExpand?: boolean,
|
|
58696
58871
|
* dialogExpandX?: boolean,
|
|
58697
58872
|
* dialogExpandY?: boolean,
|
|
58873
|
+
* marginWithContainer?: number | string,
|
|
58874
|
+
* escapeEffect?: "cancel" | "close",
|
|
58875
|
+
* pointerInteractionOutsideEffect?: "close" | "cancel" | "capture",
|
|
58698
58876
|
* ref?: import("ignore:preact").RefObject<HTMLElement>,
|
|
58699
58877
|
* [key: string]: any,
|
|
58700
58878
|
* }>}
|
|
@@ -58714,6 +58892,13 @@ const PickerFirstResolver = props => {
|
|
|
58714
58892
|
* @param {boolean} [popupWidthFitContent] By default the popup is at least as
|
|
58715
58893
|
* wide as the trigger. Set this to let the content size it instead, so a
|
|
58716
58894
|
* popup narrower than the trigger stays narrow.
|
|
58895
|
+
* @param {import("ignore:preact").ComponentChildren} [rightSlotIcon] What the right
|
|
58896
|
+
* slot draws in place of the chevron. It is the whole slot, not an addition
|
|
58897
|
+
* to it: the picker then no longer says on its own that it opens, so pass
|
|
58898
|
+
* something that does.
|
|
58899
|
+
* @param {number|string} [rightSlotIconSize="inherit"] How big what sits in the
|
|
58900
|
+
* right slot is drawn — the chevron, a `rightSlotIcon`, or the clear button's
|
|
58901
|
+
* cross. "inherit" takes the picker's own font size.
|
|
58717
58902
|
* @param {number|string} [slotSpacing] Gap kept between what sits in the right
|
|
58718
58903
|
* slot (the chevron, or the clear button) and the picker's own edge — same
|
|
58719
58904
|
* prop name Input uses for its own slots. Accepts a spacing token ("s",
|
|
@@ -58721,6 +58906,25 @@ const PickerFirstResolver = props => {
|
|
|
58721
58906
|
* horizontal padding.
|
|
58722
58907
|
* @param {number|string} [popoverMaxHeight] Soft cap on the popover's height
|
|
58723
58908
|
* (default 300px). The popover shrinks below it when space is tight.
|
|
58909
|
+
* @param {number|string} [dialogMaxWidth] Ceiling on the dialog's width, the
|
|
58910
|
+
* one `dialogExpand`/`dialogExpandX` grows up to — what makes an expanded
|
|
58911
|
+
* dialog a large sheet rather than a full screen. Capped in turn by the
|
|
58912
|
+
* container minus `marginWithContainer`.
|
|
58913
|
+
* @param {number|string} [dialogMaxHeight] Same, on the height.
|
|
58914
|
+
* @param {"cancel"|"close"} [escapeEffect="cancel"] What Escape does to an open
|
|
58915
|
+
* picker. "cancel" puts back the value the picker had at open, and a dialog
|
|
58916
|
+
* picker also goes back in history — so anything written to the url while it
|
|
58917
|
+
* was open (a route `stateSignal`, a search param) goes back with it. "close"
|
|
58918
|
+
* makes Escape say what clicking outside says: keep what was chosen, close.
|
|
58919
|
+
* @param {"close"|"cancel"|"capture"} [pointerInteractionOutsideEffect="close"]
|
|
58920
|
+
* What a click outside the popup does: close and keep ("close"), close and
|
|
58921
|
+
* put back the value at open ("cancel"), or nothing at all ("capture").
|
|
58922
|
+
* @param {number|string} [marginWithContainer] Minimum gap kept between the
|
|
58923
|
+
* popup and the edges of what contains it (the viewport, or the picker's own
|
|
58924
|
+
* positioned ancestor for `popupLayer="local"`). Caps the popup's size as
|
|
58925
|
+
* well as its placement, so what an expanded dialog leaves visible around
|
|
58926
|
+
* itself is set here. Defaults to `popoverSpacing` in popover mode, and to
|
|
58927
|
+
* Dialog's own 3vvw in dialog mode.
|
|
58724
58928
|
*/
|
|
58725
58929
|
const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
|
|
58726
58930
|
Picker.UI = PickerDefaultUI;
|
|
@@ -60540,12 +60744,26 @@ const usePlaceholderHeight = (ref, placeholder) => {
|
|
|
60540
60744
|
textareaEl.style.setProperty("--x-textarea-placeholder-height", `${contentHeight}px`);
|
|
60541
60745
|
};
|
|
60542
60746
|
measure();
|
|
60747
|
+
// Measuring writes the variable that sets this element's own height, and
|
|
60748
|
+
// mutating layout from inside a resize callback is what makes the browser
|
|
60749
|
+
// report "ResizeObserver loop completed with undelivered notifications".
|
|
60750
|
+
// So the write waits for the frame that resize produced.
|
|
60751
|
+
let measureFrame = null;
|
|
60752
|
+
const requestMeasure = () => {
|
|
60753
|
+
if (measureFrame !== null) {
|
|
60754
|
+
return;
|
|
60755
|
+
}
|
|
60756
|
+
measureFrame = requestAnimationFrame(() => {
|
|
60757
|
+
measureFrame = null;
|
|
60758
|
+
measure();
|
|
60759
|
+
});
|
|
60760
|
+
};
|
|
60543
60761
|
// The placeholder wraps against the available width, so a new width is a
|
|
60544
60762
|
// new number of lines. Height changes are ignored: this measure is what
|
|
60545
60763
|
// causes them, and reacting to them would be reacting to ourselves.
|
|
60546
60764
|
const resizeObserver = new ResizeObserver(() => {
|
|
60547
60765
|
if (textareaEl.clientWidth !== widthMeasured) {
|
|
60548
|
-
|
|
60766
|
+
requestMeasure();
|
|
60549
60767
|
}
|
|
60550
60768
|
});
|
|
60551
60769
|
resizeObserver.observe(textareaEl);
|
|
@@ -60558,6 +60776,9 @@ const usePlaceholderHeight = (ref, placeholder) => {
|
|
|
60558
60776
|
};
|
|
60559
60777
|
textareaEl.addEventListener("input", onInput);
|
|
60560
60778
|
return () => {
|
|
60779
|
+
if (measureFrame !== null) {
|
|
60780
|
+
cancelAnimationFrame(measureFrame);
|
|
60781
|
+
}
|
|
60561
60782
|
resizeObserver.disconnect();
|
|
60562
60783
|
textareaEl.removeEventListener("input", onInput);
|
|
60563
60784
|
};
|
|
@@ -69586,6 +69807,150 @@ const ViewportLayout = props => {
|
|
|
69586
69807
|
});
|
|
69587
69808
|
};
|
|
69588
69809
|
|
|
69810
|
+
/**
|
|
69811
|
+
* Pushing a side panel back the way it came in closes it: the panel follows
|
|
69812
|
+
* the finger while it travels, and letting go either finishes the departure or
|
|
69813
|
+
* brings it back to rest — whichever the travel was already heading to.
|
|
69814
|
+
*
|
|
69815
|
+
* Reading the pointer is not done here: when a press becomes a gesture, which
|
|
69816
|
+
* axis it leans on, how fast it was going when it was let go, who else has a
|
|
69817
|
+
* claim on it (a field, a scroller with room left, a box inside the panel that
|
|
69818
|
+
* travels the same way) is one gesture system for the whole codebase
|
|
69819
|
+
* (@jsenv/dom's startDragToTravel). A panel closing is a travel of exactly one
|
|
69820
|
+
* box, towards the edge it is docked to and nowhere else — which is all this
|
|
69821
|
+
* file has to say.
|
|
69822
|
+
*
|
|
69823
|
+
* The release travel is driven here (Web Animations) rather than handed to the
|
|
69824
|
+
* exit transition the `animation` prop may install: its pace is what is left to
|
|
69825
|
+
* cover, which a CSS transition written for a full-size travel cannot know, and
|
|
69826
|
+
* two movements on the same property would contradict each other. So CSS
|
|
69827
|
+
* transitions are suppressed on the panel for the whole gesture, release
|
|
69828
|
+
* included, and restored once the panel has either left or come back.
|
|
69829
|
+
*/
|
|
69830
|
+
|
|
69831
|
+
|
|
69832
|
+
// What a full-size release travel takes; a shorter one takes proportionally
|
|
69833
|
+
// less, so the panel keeps the same speed whenever the finger let go.
|
|
69834
|
+
const TRAVEL_DURATION = 220;
|
|
69835
|
+
|
|
69836
|
+
// Which way a panel docked to each side travels — read by the panel itself to
|
|
69837
|
+
// say so in the DOM, which is how a box travelling INSIDE it (a row of slides,
|
|
69838
|
+
// a route travel) knows that axis is already walked.
|
|
69839
|
+
const SWIPE_AXIS_BY_SIDE = {
|
|
69840
|
+
left: "x",
|
|
69841
|
+
right: "x",
|
|
69842
|
+
top: "y",
|
|
69843
|
+
bottom: "y",
|
|
69844
|
+
};
|
|
69845
|
+
// Which way the finger pushes to send the panel back where it came from, in
|
|
69846
|
+
// screen coordinates: positive is right/down, the same sign the gesture reports.
|
|
69847
|
+
const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
69848
|
+
|
|
69849
|
+
/**
|
|
69850
|
+
* Builds the `pointerdown` handler a side panel docked to `side` answers with.
|
|
69851
|
+
*/
|
|
69852
|
+
const createSwipeToClose = (side) => {
|
|
69853
|
+
const axis = SWIPE_AXIS_BY_SIDE[side];
|
|
69854
|
+
const closeDirection = CLOSE_DIRECTION_BY_SIDE[side];
|
|
69855
|
+
|
|
69856
|
+
return (pointerDownEvent) => {
|
|
69857
|
+
const panelEl = pointerDownEvent.currentTarget;
|
|
69858
|
+
if (panelEl.getAttribute("aria-expanded") !== "true") {
|
|
69859
|
+
return;
|
|
69860
|
+
}
|
|
69861
|
+
|
|
69862
|
+
// Where the panel stands, written on it directly: the gesture reports a
|
|
69863
|
+
// distance in screen coordinates, which is exactly what a translate takes.
|
|
69864
|
+
const paint = (distance) => {
|
|
69865
|
+
panelEl.style.translate =
|
|
69866
|
+
axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
|
|
69867
|
+
};
|
|
69868
|
+
const restore = () => {
|
|
69869
|
+
panelEl.style.translate = "";
|
|
69870
|
+
panelEl.style.transitionProperty = "";
|
|
69871
|
+
panelEl.style.userSelect = "";
|
|
69872
|
+
};
|
|
69873
|
+
// The resting value is written first and the animation only covers the way
|
|
69874
|
+
// to it: both end on the same number, so nothing is seen changing when the
|
|
69875
|
+
// animation hands the panel back to its own style.
|
|
69876
|
+
const travelTo = (from, to, onArrival) => {
|
|
69877
|
+
const covered = from > to ? from - to : to - from;
|
|
69878
|
+
paint(to);
|
|
69879
|
+
const animation = panelEl.animate(
|
|
69880
|
+
[
|
|
69881
|
+
{ translate: translateOf(axis, from) },
|
|
69882
|
+
{ translate: translateOf(axis, to) },
|
|
69883
|
+
],
|
|
69884
|
+
{
|
|
69885
|
+
duration: (covered / sizeOf(panelEl, axis)) * TRAVEL_DURATION,
|
|
69886
|
+
easing: "ease-out",
|
|
69887
|
+
},
|
|
69888
|
+
);
|
|
69889
|
+
animation.finished.then(onArrival, () => {});
|
|
69890
|
+
};
|
|
69891
|
+
const close = (event) => {
|
|
69892
|
+
triggerNaviCommand(panelEl, "--navi-close", event);
|
|
69893
|
+
if (panelEl.getAttribute("aria-expanded") === "true") {
|
|
69894
|
+
// Refused: the panel is staying, so it goes back where it was.
|
|
69895
|
+
travelTo(sizeOf(panelEl, axis) * closeDirection, 0, restore);
|
|
69896
|
+
return;
|
|
69897
|
+
}
|
|
69898
|
+
restore();
|
|
69899
|
+
};
|
|
69900
|
+
|
|
69901
|
+
startDragToTravel(pointerDownEvent, {
|
|
69902
|
+
element: panelEl,
|
|
69903
|
+
axes: axis,
|
|
69904
|
+
onStart: ({ sign, target }) => {
|
|
69905
|
+
if (sign !== closeDirection) {
|
|
69906
|
+
// Pushing the panel further in is not a travel: there is nowhere that
|
|
69907
|
+
// way, and reading it as one would make the panel lean at every
|
|
69908
|
+
// gesture that starts by going the wrong way.
|
|
69909
|
+
return false;
|
|
69910
|
+
}
|
|
69911
|
+
const size = sizeOf(panelEl, axis);
|
|
69912
|
+
if (!size) {
|
|
69913
|
+
return false;
|
|
69914
|
+
}
|
|
69915
|
+
// Something else with a better claim on the gesture: a scroller between
|
|
69916
|
+
// the finger and the panel, with room left that way.
|
|
69917
|
+
if (scrollRoomTowards(target, panelEl, axis, sign)) {
|
|
69918
|
+
return false;
|
|
69919
|
+
}
|
|
69920
|
+
panelEl.style.transitionProperty = "none";
|
|
69921
|
+
panelEl.style.userSelect = "none";
|
|
69922
|
+
// What the first pixels of the gesture may have started selecting is
|
|
69923
|
+
// not a selection, it is the beginning of this travel.
|
|
69924
|
+
window.getSelection().removeAllRanges();
|
|
69925
|
+
// The way out is the only way there is anything: pulling the other way
|
|
69926
|
+
// leans against a wall and comes back.
|
|
69927
|
+
return {
|
|
69928
|
+
size,
|
|
69929
|
+
travelBack: closeDirection > 0,
|
|
69930
|
+
travelOn: closeDirection < 0,
|
|
69931
|
+
};
|
|
69932
|
+
},
|
|
69933
|
+
onPull: ({ pulled }) => {
|
|
69934
|
+
paint(pulled);
|
|
69935
|
+
},
|
|
69936
|
+
onEnd: ({ pulled, size, travels, event }) => {
|
|
69937
|
+
if (travels) {
|
|
69938
|
+
travelTo(pulled, size * closeDirection, () => close(event));
|
|
69939
|
+
return;
|
|
69940
|
+
}
|
|
69941
|
+
travelTo(pulled, 0, restore);
|
|
69942
|
+
},
|
|
69943
|
+
});
|
|
69944
|
+
};
|
|
69945
|
+
};
|
|
69946
|
+
|
|
69947
|
+
const sizeOf = (element, axis) => {
|
|
69948
|
+
const rect = element.getBoundingClientRect();
|
|
69949
|
+
return axis === "x" ? rect.width : rect.height;
|
|
69950
|
+
};
|
|
69951
|
+
const translateOf = (axis, distance) =>
|
|
69952
|
+
axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
|
|
69953
|
+
|
|
69589
69954
|
installImportMetaCssBuild(import.meta);/**
|
|
69590
69955
|
* A drawer docked flush to a viewport (or container) edge, built on top of
|
|
69591
69956
|
* `Popup`. Sizing, the perpendicular-axis fill, and the flush-edge
|
|
@@ -69714,6 +70079,20 @@ const css = /* css */`
|
|
|
69714
70079
|
}
|
|
69715
70080
|
}
|
|
69716
70081
|
|
|
70082
|
+
/* What a touch may do on a panel that closes by being pushed back (see
|
|
70083
|
+
swipe_to_close.js): a left/right panel scrolls its own content
|
|
70084
|
+
downwards and travels sideways, so the two never compete and the
|
|
70085
|
+
browser is told to leave the sideways one alone — which also keeps a
|
|
70086
|
+
swipe near the screen edge from being read as "go back" instead. A
|
|
70087
|
+
top/bottom panel travels the way it scrolls: nothing can be given away
|
|
70088
|
+
there without losing the scroll, so the gesture is settled by
|
|
70089
|
+
swipe_to_close.js's own reading of what is still scrollable, and the
|
|
70090
|
+
browser keeps both axes. */
|
|
70091
|
+
&[data-swipe-to-close][navi-side="left"],
|
|
70092
|
+
&[data-swipe-to-close][navi-side="right"] {
|
|
70093
|
+
touch-action: pan-y;
|
|
70094
|
+
}
|
|
70095
|
+
|
|
69717
70096
|
/* Sticky regardless of side: the panel's own content always stacks
|
|
69718
70097
|
(and scrolls) top-to-bottom, whether the panel itself is docked to
|
|
69719
70098
|
the left/right or the top/bottom of the viewport/container — so
|
|
@@ -69792,6 +70171,11 @@ const css = /* css */`
|
|
|
69792
70171
|
* navigation inside the panel (`focusCapture`) — closing on outside
|
|
69793
70172
|
* interaction only makes sense paired with not letting focus silently
|
|
69794
70173
|
* leave the panel first.
|
|
70174
|
+
* @param {boolean} [props.swipeToClose=true] - Pushing the panel back
|
|
70175
|
+
* towards the edge it is docked to closes it: the panel follows the
|
|
70176
|
+
* pointer and finishes leaving (or comes back to rest) when it is
|
|
70177
|
+
* released. Set to `false` for a panel that must only ever be dismissed
|
|
70178
|
+
* deliberately. See `swipe_to_close.js` for when the gesture is claimed.
|
|
69795
70179
|
* @param {"dialog"|"popover"} [props.mode] - Forwarded to `Popup` — forces
|
|
69796
70180
|
* one underlying renderer instead of its automatic screen-size
|
|
69797
70181
|
* resolution. Note that if `Popup` ends up in dialog mode (small screen,
|
|
@@ -69817,12 +70201,14 @@ const SidePanel = ({
|
|
|
69817
70201
|
minHeight,
|
|
69818
70202
|
animation,
|
|
69819
70203
|
closeOnClickOutside = false,
|
|
70204
|
+
swipeToClose = true,
|
|
69820
70205
|
mode,
|
|
69821
70206
|
layer = "top",
|
|
69822
70207
|
className,
|
|
69823
70208
|
...rest
|
|
69824
70209
|
}) => {
|
|
69825
70210
|
import.meta.css = [css, "@jsenv/navi/src/layout/side_panel.jsx"];
|
|
70211
|
+
const onSwipePointerDown = swipeToClose ? createSwipeToClose(side) : null;
|
|
69826
70212
|
return jsx(Popup, {
|
|
69827
70213
|
mode: mode,
|
|
69828
70214
|
open: open,
|
|
@@ -69843,7 +70229,20 @@ const SidePanel = ({
|
|
|
69843
70229
|
minHeight: toCssLength(minHeight),
|
|
69844
70230
|
className: withPropsClassName("navi_side_panel", className),
|
|
69845
70231
|
"navi-side": side,
|
|
70232
|
+
"data-swipe-to-close": swipeToClose ? "" : undefined
|
|
70233
|
+
// The axis the panel travels on when it is pushed back, said to the
|
|
70234
|
+
// shared gesture layer: it keeps the panel's scrolling from spilling onto
|
|
70235
|
+
// the page, and it is what a box travelling inside the panel reads to
|
|
70236
|
+
// know this axis is already walked (see @jsenv/dom's drag_to_travel).
|
|
70237
|
+
,
|
|
70238
|
+
|
|
70239
|
+
"data-drag-travel": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined,
|
|
70240
|
+
"data-travel-by-drag": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined,
|
|
69846
70241
|
...rest,
|
|
70242
|
+
onPointerDown: pointerDownEvent => {
|
|
70243
|
+
rest.onPointerDown?.(pointerDownEvent);
|
|
70244
|
+
onSwipePointerDown?.(pointerDownEvent);
|
|
70245
|
+
},
|
|
69847
70246
|
style: {
|
|
69848
70247
|
"--navi-side-panel-width": toCssLength(width, "width"),
|
|
69849
70248
|
"--navi-side-panel-height": toCssLength(height, "height"),
|