@jsenv/navi 0.29.36 → 0.29.38
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 +479 -67
- package/dist/jsenv_navi.js.map +29 -18
- package/docs/interactions.md +77 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* using @jsenv/navi as intended.
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
6
|
-
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
6
|
+
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
7
7
|
export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
8
8
|
import { signal, computed, effect, batch, useSignal } from "@preact/signals";
|
|
9
9
|
import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
|
|
@@ -12550,10 +12550,11 @@ defineInteractionDetector({
|
|
|
12550
12550
|
if (canReorder) {
|
|
12551
12551
|
element.setAttribute(REORDERABLE_ATTRIBUTE, "");
|
|
12552
12552
|
}
|
|
12553
|
-
// What @jsenv/dom puts on a drag source: no iOS callout,
|
|
12554
|
-
// the scroll until the press becomes a grab
|
|
12555
|
-
//
|
|
12556
|
-
|
|
12553
|
+
// What @jsenv/dom puts on a drag source: no iOS callout, the touch left to
|
|
12554
|
+
// the scroll until the press becomes a grab, and the listener that lets the
|
|
12555
|
+
// grab take it back. Its argument is the axis the SURROUNDINGS scroll on,
|
|
12556
|
+
// which for a list is the axis the list runs on.
|
|
12557
|
+
const unmarkDragSource = markDragSource(element, axes);
|
|
12557
12558
|
|
|
12558
12559
|
// What a release can mean, which is not all of what was declared: "grab" is a
|
|
12559
12560
|
// moment, not an outcome, and the gesture must not read it as one.
|
|
@@ -12630,7 +12631,7 @@ defineInteractionDetector({
|
|
|
12630
12631
|
|
|
12631
12632
|
return () => {
|
|
12632
12633
|
element.removeAttribute(REORDERABLE_ATTRIBUTE);
|
|
12633
|
-
|
|
12634
|
+
unmarkDragSource();
|
|
12634
12635
|
element.removeEventListener("pointerdown", onPointerDown);
|
|
12635
12636
|
};
|
|
12636
12637
|
},
|
|
@@ -21855,7 +21856,9 @@ registerNaviCommand("--navi-reset", (source, event) => {
|
|
|
21855
21856
|
* - an open popup: it closes. The popup was there for the duration of one
|
|
21856
21857
|
* decision, and the send just made it. A picker already does this when the
|
|
21857
21858
|
* send targets the popup itself (executeNaviDefine); a form inside one is the
|
|
21858
|
-
* same act, a level down
|
|
21859
|
+
* same act, a level down. A popup opened from another popup answers for
|
|
21860
|
+
* itself only — say `data-after-send="--navi-send"` on the sending control
|
|
21861
|
+
* to make the decision travel up to the surface holding it;
|
|
21859
21862
|
* - the document: nothing. A form on a page stays where it is.
|
|
21860
21863
|
*/
|
|
21861
21864
|
// What follows a send that went through — and it is asked of the button that
|
|
@@ -21909,7 +21912,21 @@ registerNaviCommand("--navi-send", (source, event) => {
|
|
|
21909
21912
|
if (target.getAttribute("aria-expanded") === "true") {
|
|
21910
21913
|
return {
|
|
21911
21914
|
target,
|
|
21912
|
-
implementation: () =>
|
|
21915
|
+
implementation: () => {
|
|
21916
|
+
const result = executeNaviDefine(source, event, target);
|
|
21917
|
+
// Only what was explicitly asked for: the surface above a popup is not
|
|
21918
|
+
// answered by a decision taken inside it (see resolveAfterSend), so a
|
|
21919
|
+
// popup opened from another popup closes alone unless the send says
|
|
21920
|
+
// otherwise. Triggered from the target — the popup that just closed no
|
|
21921
|
+
// longer holds the source.
|
|
21922
|
+
const afterSend =
|
|
21923
|
+
source.getAttribute("data-after-send") ||
|
|
21924
|
+
target.getAttribute("data-after-send");
|
|
21925
|
+
if (afterSend) {
|
|
21926
|
+
triggerNaviCommand(target, afterSend, event, { optional: true });
|
|
21927
|
+
}
|
|
21928
|
+
return result;
|
|
21929
|
+
},
|
|
21913
21930
|
};
|
|
21914
21931
|
}
|
|
21915
21932
|
|
|
@@ -23384,7 +23401,8 @@ const useUIStateController = (
|
|
|
23384
23401
|
controller.id = props.id; // never supposed to change, not supported for now
|
|
23385
23402
|
controller.name = props.name;
|
|
23386
23403
|
controller.parentUIStateController = parentUIStateController;
|
|
23387
|
-
const { value, hasStateProp, state, stateInitial } =
|
|
23404
|
+
const { value, hasStateProp, state, stateInitial, stateFromSignal } =
|
|
23405
|
+
controlInfo;
|
|
23388
23406
|
controller.value = value;
|
|
23389
23407
|
if (hasStateProp) {
|
|
23390
23408
|
controller.hasStateProp = true;
|
|
@@ -23393,9 +23411,27 @@ const useUIStateController = (
|
|
|
23393
23411
|
controller.state = state;
|
|
23394
23412
|
controller.setUIState(state, new CustomEvent("state_prop_change"));
|
|
23395
23413
|
}
|
|
23396
|
-
} else
|
|
23397
|
-
controller.hasStateProp
|
|
23398
|
-
|
|
23414
|
+
} else {
|
|
23415
|
+
if (controller.hasStateProp) {
|
|
23416
|
+
controller.hasStateProp = false;
|
|
23417
|
+
controller.state = stateInitial;
|
|
23418
|
+
}
|
|
23419
|
+
if (controlInfo.signal) {
|
|
23420
|
+
// The other half of a bound signal: the control follows it when
|
|
23421
|
+
// something else writes it. Compared against `state` (what the signal
|
|
23422
|
+
// last said), never against the ui state — otherwise typing into an
|
|
23423
|
+
// uncontrolled control would be undone on the next render. A write
|
|
23424
|
+
// coming from the control itself lands here with the value already in
|
|
23425
|
+
// place, and setUIState treats an unchanged state as a no-op.
|
|
23426
|
+
const currentState = controller.state;
|
|
23427
|
+
if (!compareTwoJsValues(stateFromSignal, currentState)) {
|
|
23428
|
+
controller.state = stateFromSignal;
|
|
23429
|
+
controller.setUIState(
|
|
23430
|
+
stateFromSignal,
|
|
23431
|
+
new CustomEvent("state_prop_change"),
|
|
23432
|
+
);
|
|
23433
|
+
}
|
|
23434
|
+
}
|
|
23399
23435
|
}
|
|
23400
23436
|
return {
|
|
23401
23437
|
ref: props.ref,
|
|
@@ -23675,10 +23711,9 @@ const useUIGroupStateController = (
|
|
|
23675
23711
|
const hasValueProp = Object.hasOwn(props, "value");
|
|
23676
23712
|
const hasOwnDefaultValueProp = Object.hasOwn(props, "defaultValue");
|
|
23677
23713
|
// 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.
|
|
23714
|
+
// with the signal's current value as what it starts on. Write-back is handled
|
|
23715
|
+
// by applyState's own boundSignal; the read half is below: children are
|
|
23716
|
+
// placed from it when they register, and again whenever it moves.
|
|
23682
23717
|
const boundSignal = hasValueProp ? undefined : props.signal;
|
|
23683
23718
|
const hasDefaultValueProp = hasOwnDefaultValueProp || Boolean(boundSignal);
|
|
23684
23719
|
const { id, name, value, uiAction } = props;
|
|
@@ -24096,6 +24131,7 @@ const useUIGroupStateController = (
|
|
|
24096
24131
|
const { controller } = s;
|
|
24097
24132
|
const prevValue = controller.value;
|
|
24098
24133
|
const prevHasValueProp = controller.hasValueProp;
|
|
24134
|
+
const prevDefaultValue = controller.defaultValue;
|
|
24099
24135
|
controller.props = props;
|
|
24100
24136
|
controller.ref = ref;
|
|
24101
24137
|
controller.id = id;
|
|
@@ -24124,6 +24160,31 @@ const useUIGroupStateController = (
|
|
|
24124
24160
|
}
|
|
24125
24161
|
controller.syncInternalState(value);
|
|
24126
24162
|
}
|
|
24163
|
+
if (
|
|
24164
|
+
boundSignal &&
|
|
24165
|
+
!hasValueProp &&
|
|
24166
|
+
!compareTwoJsValues(defaultValue, prevDefaultValue)
|
|
24167
|
+
) {
|
|
24168
|
+
// The signal moved from the outside: the children are placed from it
|
|
24169
|
+
// again, exactly as they were when they registered. Without this a
|
|
24170
|
+
// group would answer a write to its own signal by silently writing its
|
|
24171
|
+
// former value back over it on the next child interaction.
|
|
24172
|
+
const propagateDownEvent = new CustomEvent(
|
|
24173
|
+
"propagate_down_set_ui_state",
|
|
24174
|
+
{ detail: {} },
|
|
24175
|
+
);
|
|
24176
|
+
for (const childUIStateController of childUIStateControllerArray) {
|
|
24177
|
+
if (!shouldPropagateStateToChild(childUIStateController)) continue;
|
|
24178
|
+
if (childUIStateController.hasStateProp) continue;
|
|
24179
|
+
const childNewState = controller.distributeChildUIState(
|
|
24180
|
+
defaultValue,
|
|
24181
|
+
childUIStateController,
|
|
24182
|
+
);
|
|
24183
|
+
if (childNewState === CANNOT_DERIVE) continue;
|
|
24184
|
+
childUIStateController.setUIState(childNewState, propagateDownEvent);
|
|
24185
|
+
}
|
|
24186
|
+
controller.syncInternalState(defaultValue);
|
|
24187
|
+
}
|
|
24127
24188
|
|
|
24128
24189
|
return {
|
|
24129
24190
|
ref,
|
|
@@ -25248,6 +25309,13 @@ const createControlInfo = (props, {
|
|
|
25248
25309
|
const signal = Object.hasOwn(props, "signal") ? props.signal : undefined;
|
|
25249
25310
|
// For a checkbox/radio the signal holds the boolean checked state, not the value.
|
|
25250
25311
|
let signalHoldsChecked = false;
|
|
25312
|
+
// What the signal says the control shows, re-read on every render. A signal
|
|
25313
|
+
// carrying a default of its own seeds `defaultValue` (resolveInputProps), so
|
|
25314
|
+
// such a control is uncontrolled — but the binding still has to work in both
|
|
25315
|
+
// directions: whoever writes the signal from the outside expects the control
|
|
25316
|
+
// to move. This is what the controller re-syncs on (see useUIStateController),
|
|
25317
|
+
// and an emptied signal puts the control back on its suggestion.
|
|
25318
|
+
let stateFromSignal;
|
|
25251
25319
|
if (controlType === "input") {
|
|
25252
25320
|
if (typeProp === "checkbox" || typeProp === "radio") {
|
|
25253
25321
|
statePropName = "checked";
|
|
@@ -25259,10 +25327,13 @@ const createControlInfo = (props, {
|
|
|
25259
25327
|
stateInitial = props.checked ? value : undefined;
|
|
25260
25328
|
} else if (props.defaultChecked) {
|
|
25261
25329
|
// resolveInputProps may seed defaultChecked from a bound signal's
|
|
25262
|
-
// default: the control stays uncontrolled and
|
|
25263
|
-
//
|
|
25330
|
+
// default: the control stays uncontrolled and follows the signal
|
|
25331
|
+
// through stateFromSignal.
|
|
25264
25332
|
hasStateProp = false;
|
|
25265
25333
|
stateInitial = value;
|
|
25334
|
+
if (signal) {
|
|
25335
|
+
stateFromSignal = signal.value ? value : undefined;
|
|
25336
|
+
}
|
|
25266
25337
|
} else if (signal) {
|
|
25267
25338
|
// A bound signal with no resolved default: its live value seeds state.
|
|
25268
25339
|
hasStateProp = true;
|
|
@@ -25289,6 +25360,9 @@ const createControlInfo = (props, {
|
|
|
25289
25360
|
// url or set by whoever owns it. Taking the default here would show a
|
|
25290
25361
|
// suggestion in place of the value on every reload.
|
|
25291
25362
|
stateInitial = signal && signal.value !== undefined ? signal.value : props.defaultValue;
|
|
25363
|
+
if (signal) {
|
|
25364
|
+
stateFromSignal = stateInitial;
|
|
25365
|
+
}
|
|
25292
25366
|
} else if (signal) {
|
|
25293
25367
|
// A plain bound signal with no default (e.g. Wheel): its live value
|
|
25294
25368
|
// seeds and controls the state.
|
|
@@ -25322,6 +25396,9 @@ const createControlInfo = (props, {
|
|
|
25322
25396
|
// Same precedence as an input above: the signal's value is the answer,
|
|
25323
25397
|
// defaultValue only the suggestion to start from.
|
|
25324
25398
|
stateInitial = signal && signal.value !== undefined ? signal.value : props.defaultValue;
|
|
25399
|
+
if (signal) {
|
|
25400
|
+
stateFromSignal = stateInitial;
|
|
25401
|
+
}
|
|
25325
25402
|
} else if (signal) {
|
|
25326
25403
|
hasStateProp = true;
|
|
25327
25404
|
stateInitial = signal.value;
|
|
@@ -25345,6 +25422,7 @@ const createControlInfo = (props, {
|
|
|
25345
25422
|
value,
|
|
25346
25423
|
signal,
|
|
25347
25424
|
signalHoldsChecked,
|
|
25425
|
+
stateFromSignal,
|
|
25348
25426
|
readOnlySupported,
|
|
25349
25427
|
disabledSupported
|
|
25350
25428
|
};
|
|
@@ -26824,7 +26902,19 @@ const findFocusTarget = (containerEl) => {
|
|
|
26824
26902
|
// Neither is dropped, both are simply tried later — step 3 below for the
|
|
26825
26903
|
// first, and for the second the restore transferFocus does before ever
|
|
26826
26904
|
// calling here.
|
|
26827
|
-
|
|
26905
|
+
//
|
|
26906
|
+
// Skipped for good, unlike the two above: an element hidden from assistive
|
|
26907
|
+
// technology is not a place the focus can land at all. Something aria-hidden
|
|
26908
|
+
// and out of the tab order is a value holder standing behind what one
|
|
26909
|
+
// actually uses — a spin's headless picker behind its slides, say — and
|
|
26910
|
+
// landing there puts a ring on it, raises a phone's keyboard over the panel
|
|
26911
|
+
// that just opened, and has the browser complain about a focused aria-hidden
|
|
26912
|
+
// element. What one came to use is further down the same container.
|
|
26913
|
+
const isHiddenFromAssistiveTech = (element) =>
|
|
26914
|
+
Boolean(element.closest?.(`[aria-hidden="true"]`));
|
|
26915
|
+
|
|
26916
|
+
const skip = (element) =>
|
|
26917
|
+
isRestorableAutofocus(element) || isHiddenFromAssistiveTech(element);
|
|
26828
26918
|
|
|
26829
26919
|
// Every mark, not just the first: a mark is only worth stopping at if it
|
|
26830
26920
|
// leads somewhere focusable. One inside a screen waiting its turn (an inert
|
|
@@ -36881,12 +36971,19 @@ const css$R = /* css */`
|
|
|
36881
36971
|
travelling. */
|
|
36882
36972
|
.navi_route_travel {
|
|
36883
36973
|
position: relative;
|
|
36884
|
-
|
|
36885
|
-
|
|
36886
|
-
|
|
36887
|
-
|
|
36888
|
-
|
|
36889
|
-
|
|
36974
|
+
|
|
36975
|
+
/* What a touch may do here — only where a touch travels at all
|
|
36976
|
+
(data-travel-by-drag, set from travelByDrag): the axis the pages travel
|
|
36977
|
+
on is taken, the other one is left to the page, so a list still scrolls
|
|
36978
|
+
under the same finger. A box that takes no gesture takes no axis either:
|
|
36979
|
+
it is a plain box around the page, and everything in it scrolls as it
|
|
36980
|
+
would anywhere else. Same reading as SlideContainer's own. */
|
|
36981
|
+
&[data-travel-by-drag="x"] {
|
|
36982
|
+
touch-action: pan-y;
|
|
36983
|
+
}
|
|
36984
|
+
&[data-travel-by-drag="y"] {
|
|
36985
|
+
touch-action: pan-x;
|
|
36986
|
+
}
|
|
36890
36987
|
}
|
|
36891
36988
|
|
|
36892
36989
|
/* Only while a travel of OURS is playing: everything below changes how the
|
|
@@ -37107,6 +37204,11 @@ const css$R = /* css */`
|
|
|
37107
37204
|
* page under six entries. A tab pressed is the other case and pushes, which
|
|
37108
37205
|
* is what its <Link> already does.
|
|
37109
37206
|
*
|
|
37207
|
+
* Every other prop lands on the box itself (`id`, `data-testid`, `className`,
|
|
37208
|
+
* …), which is how one of these is named: a page can hold several — a row of
|
|
37209
|
+
* sections inside the box the whole application travels in — and the class they
|
|
37210
|
+
* share plus their axis are not enough to tell them apart from the outside.
|
|
37211
|
+
*
|
|
37110
37212
|
* The pages are cut at the edge of this box while they travel, which is written
|
|
37111
37213
|
* on the transition's own pseudo-elements — no overflow of the document reaches
|
|
37112
37214
|
* pictures drawn in the top layer. It needs nothing of the browser beyond view
|
|
@@ -46723,12 +46825,13 @@ const NAVI_TYPE_DEFAULTS = {
|
|
|
46723
46825
|
*/
|
|
46724
46826
|
/**
|
|
46725
46827
|
* 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
|
-
*
|
|
46828
|
+
* control: the control starts there and stays uncontrolled, which is what makes
|
|
46829
|
+
* a form read the value shown as a SUGGESTION rather than as something it
|
|
46830
|
+
* already holds. Uncontrolled here is about what the control HOLDS, not about
|
|
46831
|
+
* whether it follows the signal — the binding stays two-way either way (see
|
|
46832
|
+
* stateFromSignal in control_hooks.jsx). Written once and used by everything
|
|
46833
|
+
* that takes a `signal`, so one signal cannot mean two different things
|
|
46834
|
+
* depending on which control it was handed to.
|
|
46732
46835
|
*/
|
|
46733
46836
|
const seedDefaultValueFromSignal = (props) => {
|
|
46734
46837
|
const signalOptions = props.signal?.options;
|
|
@@ -46750,11 +46853,11 @@ const seedDefaultValueFromSignal = (props) => {
|
|
|
46750
46853
|
|
|
46751
46854
|
const resolveInputProps = (props) => {
|
|
46752
46855
|
// `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
|
|
46856
|
+
// `createControlInfo` (control_hooks.jsx) reads it to seed the state and to
|
|
46857
|
+
// follow it, and `onUIAction` (ui_state_controller.js) writes user
|
|
46858
|
+
// interactions back into it. Here we only derive input defaults (type/min/max,
|
|
46859
|
+
// defaultValue/defaultChecked) from the signal's `options`, so the control
|
|
46860
|
+
// ends up uncontrolled-with-default while still bound to the signal.
|
|
46758
46861
|
const signal = props.signal;
|
|
46759
46862
|
if (signal) {
|
|
46760
46863
|
const signalOptions = signal.options;
|
|
@@ -51371,6 +51474,12 @@ installImportMetaCssBuild(import.meta);const css$y = /* css */`
|
|
|
51371
51474
|
--dialog-outline-width: var(--picker-outline-width);
|
|
51372
51475
|
--dialog-outline-color: var(--picker-outline-color);
|
|
51373
51476
|
|
|
51477
|
+
/* No fallback on purpose (same as --popover-max-height above): unset
|
|
51478
|
+
picker props leave these declarations invalid at computed-value
|
|
51479
|
+
time, so the dialog keeps its own ceilings. */
|
|
51480
|
+
--dialog-max-width: var(--picker-dialog-max-width);
|
|
51481
|
+
--dialog-max-height: var(--picker-dialog-max-height);
|
|
51482
|
+
|
|
51374
51483
|
/* Dialog itself already sizes min-width off --anchor-width — only
|
|
51375
51484
|
the cursor reset below is picker-specific here. */
|
|
51376
51485
|
cursor: default; /* Reset pointer cursor within the select */
|
|
@@ -51489,7 +51598,13 @@ const PickerCustom = props => {
|
|
|
51489
51598
|
ref,
|
|
51490
51599
|
mode: modeProp,
|
|
51491
51600
|
open,
|
|
51492
|
-
defaultOpen
|
|
51601
|
+
defaultOpen,
|
|
51602
|
+
// What Escape means for this picker. "cancel" (the default) puts back the
|
|
51603
|
+
// value the picker had at open and, for a dialog, goes back in history —
|
|
51604
|
+
// so everything written to the url while it was open goes back too.
|
|
51605
|
+
// "close" makes Escape say the same thing as clicking outside: keep what
|
|
51606
|
+
// was chosen, close the popup.
|
|
51607
|
+
escapeEffect = "cancel"
|
|
51493
51608
|
} = props;
|
|
51494
51609
|
// Resolve the id the same way useControlProps does (own id > Field's id > generated id)
|
|
51495
51610
|
// before computing popupId below, so two Pickers without an explicit id never collide.
|
|
@@ -51521,6 +51636,7 @@ const PickerCustom = props => {
|
|
|
51521
51636
|
// otherwise leak through PickerContentInsidePopup's own ...rest).
|
|
51522
51637
|
delete pickerProps.open;
|
|
51523
51638
|
delete pickerProps.defaultOpen;
|
|
51639
|
+
delete pickerProps.escapeEffect;
|
|
51524
51640
|
const popupProps = {};
|
|
51525
51641
|
Object.assign(pickerProps, {
|
|
51526
51642
|
popupProps,
|
|
@@ -51789,11 +51905,12 @@ const PickerCustom = props => {
|
|
|
51789
51905
|
if (!openController.opened) {
|
|
51790
51906
|
return null;
|
|
51791
51907
|
}
|
|
51908
|
+
const isCancel = escapeEffect === "cancel";
|
|
51792
51909
|
return {
|
|
51793
|
-
name: "escape_to_cancel",
|
|
51910
|
+
name: isCancel ? "escape_to_cancel" : "escape_to_close",
|
|
51794
51911
|
allowed: () => {
|
|
51795
51912
|
requestClose(e, {
|
|
51796
|
-
isCancel
|
|
51913
|
+
isCancel
|
|
51797
51914
|
});
|
|
51798
51915
|
e.preventDefault(); // prevent browser from closing the dialog (if any)
|
|
51799
51916
|
}
|
|
@@ -51952,7 +52069,7 @@ const PickerContentInsidePopup = props => {
|
|
|
51952
52069
|
animation: animation,
|
|
51953
52070
|
positionArea: isPopover ? positionArea ?? (popoverMode === "nearby" ? "bottom-start" : "inset(top-left)") : positionArea,
|
|
51954
52071
|
marginWithAnchor: isPopover ? popoverSpacing : undefined,
|
|
51955
|
-
marginWithContainer: marginWithContainer === undefined && isPopover ? popoverSpacing : marginWithContainer
|
|
52072
|
+
marginWithContainer: marginWithContainer === undefined && isPopover ? popoverSpacing : marginWithContainer,
|
|
51956
52073
|
scrollCapture: scrollCapture,
|
|
51957
52074
|
pointerInteractionOutsideEffect: pointerLock ? "capture" : pointerInteractionOutsideEffect,
|
|
51958
52075
|
focusCapture: isPopover ? focusCapture : undefined,
|
|
@@ -52845,6 +52962,11 @@ installImportMetaCssBuild(import.meta);const css$w = /* css */`
|
|
|
52845
52962
|
}
|
|
52846
52963
|
`;
|
|
52847
52964
|
const SelectableListMultipleContext = createContext(false);
|
|
52965
|
+
// A row of a selectable list is selectable — the list is what decides, and a
|
|
52966
|
+
// row says nothing unless it wants out (`selectable={false}` on a row that is
|
|
52967
|
+
// only there to be read). Also set to false by a non-selectable list, so a list
|
|
52968
|
+
// nested inside a selectable row does not inherit the outer list's answer.
|
|
52969
|
+
const ListSelectableContext = createContext(false);
|
|
52848
52970
|
// Interactive variant: manages hover/keyboard/selection state and handles the
|
|
52849
52971
|
// navi event protocol. When an action is provided it binds the action to ui state
|
|
52850
52972
|
// and fires it on select. When only uiAction is provided it calls it directly.
|
|
@@ -52855,8 +52977,11 @@ const ListSelectableResolver = props => {
|
|
|
52855
52977
|
...props
|
|
52856
52978
|
});
|
|
52857
52979
|
}
|
|
52858
|
-
return jsx(
|
|
52859
|
-
|
|
52980
|
+
return jsx(ListSelectableContext.Provider, {
|
|
52981
|
+
value: false,
|
|
52982
|
+
children: jsx(Next, {
|
|
52983
|
+
...props
|
|
52984
|
+
})
|
|
52860
52985
|
});
|
|
52861
52986
|
};
|
|
52862
52987
|
const ListSelectable = props => {
|
|
@@ -53146,17 +53271,27 @@ const ListSelectable = props => {
|
|
|
53146
53271
|
children: props.children
|
|
53147
53272
|
})
|
|
53148
53273
|
});
|
|
53149
|
-
return jsx(
|
|
53150
|
-
value:
|
|
53151
|
-
children:
|
|
53274
|
+
return jsx(ListSelectableContext.Provider, {
|
|
53275
|
+
value: true,
|
|
53276
|
+
children: jsx(SelectableListMultipleContext.Provider, {
|
|
53277
|
+
value: multiple,
|
|
53278
|
+
children: listVnode
|
|
53279
|
+
})
|
|
53152
53280
|
});
|
|
53153
53281
|
};
|
|
53154
53282
|
const SelectableRealInputContext = createContext(null);
|
|
53155
53283
|
const ListItemSelectableResolver = props => {
|
|
53156
53284
|
const Next = useNextResolver();
|
|
53157
|
-
|
|
53285
|
+
const listSelectable = useContext(ListSelectableContext);
|
|
53286
|
+
// A header/footer row is a title, not a choice — it stays out even in a
|
|
53287
|
+
// selectable list. (A skeleton row never reaches here: its own resolver runs
|
|
53288
|
+
// before this one.)
|
|
53289
|
+
const isHeaderOrFooter = Boolean(props.header || props.footer);
|
|
53290
|
+
const selectable = props.selectable ?? (isHeaderOrFooter || props.role === "presentation" ? false : listSelectable);
|
|
53291
|
+
if (selectable) {
|
|
53158
53292
|
return jsx(ListItemSelectable, {
|
|
53159
|
-
...props
|
|
53293
|
+
...props,
|
|
53294
|
+
selectable: true
|
|
53160
53295
|
});
|
|
53161
53296
|
}
|
|
53162
53297
|
return jsx(Next, {
|
|
@@ -53182,6 +53317,10 @@ const ListItemSelectable = props => {
|
|
|
53182
53317
|
selected,
|
|
53183
53318
|
pointed,
|
|
53184
53319
|
selectableArea = "all",
|
|
53320
|
+
// The row's own click handler, kept out of the control props below: those
|
|
53321
|
+
// describe the hidden input that carries the selection, and a caller
|
|
53322
|
+
// writing onClick on a <List.Item> is talking about the row they see.
|
|
53323
|
+
onClick,
|
|
53185
53324
|
...rest
|
|
53186
53325
|
} = props;
|
|
53187
53326
|
const multiple = useContext(SelectableListMultipleContext);
|
|
@@ -53236,6 +53375,7 @@ const ListItemSelectable = props => {
|
|
|
53236
53375
|
...basePseudoState
|
|
53237
53376
|
},
|
|
53238
53377
|
ref: props.ref,
|
|
53378
|
+
onClick: onClick,
|
|
53239
53379
|
selectable: undefined,
|
|
53240
53380
|
"navi-selectable-area-all": selectableArea === "all" ? "" : undefined,
|
|
53241
53381
|
children: [jsx(SelectableRealInput, {
|
|
@@ -53514,6 +53654,26 @@ const css$v = /* css */`
|
|
|
53514
53654
|
--list-border-width-default: 0px;
|
|
53515
53655
|
}
|
|
53516
53656
|
|
|
53657
|
+
/* Same reasoning, for the corners: a dialog squares off whatever corner
|
|
53658
|
+
lands on its container's own (see the data-flush-* rules in dialog.jsx —
|
|
53659
|
+
a bottom sheet from dockedOnTouch squares its two bottom ones). A list
|
|
53660
|
+
drawn right against that corner has to square the same one, otherwise its
|
|
53661
|
+
own radius carves a notch out of the popup's square corner. Direct child
|
|
53662
|
+
only: any deeper and the list is presumably inset from the popup's edge,
|
|
53663
|
+
where its own radius is the right one. */
|
|
53664
|
+
.navi_dialog[data-flush-top][data-flush-left] > .navi_list_container {
|
|
53665
|
+
border-top-left-radius: 0;
|
|
53666
|
+
}
|
|
53667
|
+
.navi_dialog[data-flush-top][data-flush-right] > .navi_list_container {
|
|
53668
|
+
border-top-right-radius: 0;
|
|
53669
|
+
}
|
|
53670
|
+
.navi_dialog[data-flush-bottom][data-flush-right] > .navi_list_container {
|
|
53671
|
+
border-bottom-right-radius: 0;
|
|
53672
|
+
}
|
|
53673
|
+
.navi_dialog[data-flush-bottom][data-flush-left] > .navi_list_container {
|
|
53674
|
+
border-bottom-left-radius: 0;
|
|
53675
|
+
}
|
|
53676
|
+
|
|
53517
53677
|
.navi_list_container {
|
|
53518
53678
|
--x-list-border-radius: var(--list-border-radius);
|
|
53519
53679
|
--x-list-border-width: var(
|
|
@@ -56359,7 +56519,8 @@ const LIST_ITEM_STYLE_CSS_VARS = {
|
|
|
56359
56519
|
* @param {boolean} [props.selectable]
|
|
56360
56520
|
* The item participates in selection (radio or checkbox depending on whether
|
|
56361
56521
|
* the parent List has `multiple`). Requires `value` and typically a
|
|
56362
|
-
* <SelectableInput /> child.
|
|
56522
|
+
* <SelectableInput /> child. Inherited from `<List selectable>` — pass
|
|
56523
|
+
* `false` for a row that is only there to be read, and nothing otherwise.
|
|
56363
56524
|
* @param {any} [props.value]
|
|
56364
56525
|
* The JS value emitted by the list's action/uiAction when this item is
|
|
56365
56526
|
* selected. Can be any type (string, number, object…).
|
|
@@ -57661,7 +57822,7 @@ const PickerTypeResolver = props => {
|
|
|
57661
57822
|
const PickerText = props => {
|
|
57662
57823
|
const Next = useNextResolver();
|
|
57663
57824
|
return jsx(Next, {
|
|
57664
|
-
|
|
57825
|
+
rightSlotIcon: jsx(PencilSvg, {}),
|
|
57665
57826
|
...props
|
|
57666
57827
|
});
|
|
57667
57828
|
};
|
|
@@ -57740,7 +57901,7 @@ const PickerColor = props => {
|
|
|
57740
57901
|
const Next = useNextResolver();
|
|
57741
57902
|
return jsx(Next, {
|
|
57742
57903
|
ui: jsx(PickerColorUI, {}),
|
|
57743
|
-
|
|
57904
|
+
rightSlotIcon: jsx(ColorSvg, {}),
|
|
57744
57905
|
type: "color",
|
|
57745
57906
|
...props
|
|
57746
57907
|
});
|
|
@@ -57764,7 +57925,7 @@ const PickerDate = props => {
|
|
|
57764
57925
|
const Next = useNextResolver();
|
|
57765
57926
|
return jsx(Next, {
|
|
57766
57927
|
ui: jsx(PickerDateUI, {}),
|
|
57767
|
-
|
|
57928
|
+
rightSlotIcon: jsx(CalendarSvg, {}),
|
|
57768
57929
|
...props,
|
|
57769
57930
|
type: "date"
|
|
57770
57931
|
});
|
|
@@ -57796,7 +57957,7 @@ const PickerMonth = props => {
|
|
|
57796
57957
|
const Next = useNextResolver();
|
|
57797
57958
|
return jsx(Next, {
|
|
57798
57959
|
ui: jsx(PickerMonthUI, {}),
|
|
57799
|
-
|
|
57960
|
+
rightSlotIcon: jsx(CalendarSvg, {}),
|
|
57800
57961
|
...props,
|
|
57801
57962
|
type: "month"
|
|
57802
57963
|
});
|
|
@@ -57827,7 +57988,7 @@ const PickerWeek = props => {
|
|
|
57827
57988
|
const Next = useNextResolver();
|
|
57828
57989
|
return jsx(Next, {
|
|
57829
57990
|
ui: jsx(PickerWeekUI, {}),
|
|
57830
|
-
|
|
57991
|
+
rightSlotIcon: jsx(CalendarSvg, {}),
|
|
57831
57992
|
...props,
|
|
57832
57993
|
type: "week"
|
|
57833
57994
|
});
|
|
@@ -57858,7 +58019,7 @@ const PickerTime = props => {
|
|
|
57858
58019
|
const Next = useNextResolver();
|
|
57859
58020
|
return jsx(Next, {
|
|
57860
58021
|
ui: jsx(PickerTimeUI, {}),
|
|
57861
|
-
|
|
58022
|
+
rightSlotIcon: jsx(ClockSvg, {}),
|
|
57862
58023
|
...props,
|
|
57863
58024
|
type: "time"
|
|
57864
58025
|
});
|
|
@@ -57888,7 +58049,7 @@ const PickerDuration = props => {
|
|
|
57888
58049
|
const Next = useNextResolver();
|
|
57889
58050
|
return jsx(Next, {
|
|
57890
58051
|
ui: jsx(PickerDurationUI, {}),
|
|
57891
|
-
|
|
58052
|
+
rightSlotIcon: jsx(DurationSvg, {}),
|
|
57892
58053
|
...props,
|
|
57893
58054
|
type: "text",
|
|
57894
58055
|
"navi-input-type": "duration"
|
|
@@ -57919,7 +58080,7 @@ const PickerDatetime = props => {
|
|
|
57919
58080
|
const Next = useNextResolver();
|
|
57920
58081
|
return jsx(Next, {
|
|
57921
58082
|
ui: jsx(PickerDatetimeUI, {}),
|
|
57922
|
-
|
|
58083
|
+
rightSlotIcon: jsx(CalendarSvg, {}),
|
|
57923
58084
|
...props,
|
|
57924
58085
|
type: "datetime-local"
|
|
57925
58086
|
});
|
|
@@ -57948,7 +58109,7 @@ const PickerFile = props => {
|
|
|
57948
58109
|
const Next = useNextResolver();
|
|
57949
58110
|
return jsx(Next, {
|
|
57950
58111
|
ui: jsx(PickerFileUI, {}),
|
|
57951
|
-
|
|
58112
|
+
rightSlotIcon: jsx(FileSvg, {}),
|
|
57952
58113
|
type: "file",
|
|
57953
58114
|
...props
|
|
57954
58115
|
});
|
|
@@ -58288,6 +58449,28 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
|
58288
58449
|
--x-picker-background-color: transparent;
|
|
58289
58450
|
--x-picker-icon-color: currentColor;
|
|
58290
58451
|
}
|
|
58452
|
+
/* discrete: no box at rest, a background on hover — the same word Button
|
|
58453
|
+
uses, and the same drawing. What is read is the value, not the field
|
|
58454
|
+
around it; the chevron in the right slot is what still says it opens. */
|
|
58455
|
+
&[data-variant="discrete"] {
|
|
58456
|
+
--picker-border-width: 0px; /* must carry a unit (px) — used in calc() to offset the custom input overlay */
|
|
58457
|
+
--x-picker-border-color: transparent;
|
|
58458
|
+
--x-picker-background-color: transparent;
|
|
58459
|
+
|
|
58460
|
+
&[data-hover] {
|
|
58461
|
+
--x-picker-border-color: transparent;
|
|
58462
|
+
--x-picker-background-color: color-mix(
|
|
58463
|
+
in srgb,
|
|
58464
|
+
currentColor 8%,
|
|
58465
|
+
transparent
|
|
58466
|
+
);
|
|
58467
|
+
}
|
|
58468
|
+
&[data-readonly],
|
|
58469
|
+
&[data-disabled] {
|
|
58470
|
+
--x-picker-border-color: transparent;
|
|
58471
|
+
--x-picker-background-color: transparent;
|
|
58472
|
+
}
|
|
58473
|
+
}
|
|
58291
58474
|
&[data-variant="headless"] {
|
|
58292
58475
|
--x-picker-padding-top: 0;
|
|
58293
58476
|
--x-picker-padding-right: 0;
|
|
@@ -58318,8 +58501,8 @@ const PickerButton = props => {
|
|
|
58318
58501
|
const {
|
|
58319
58502
|
ref,
|
|
58320
58503
|
variant,
|
|
58321
|
-
|
|
58322
|
-
|
|
58504
|
+
rightSlotIcon,
|
|
58505
|
+
rightSlotIconSize = "inherit",
|
|
58323
58506
|
placeholder,
|
|
58324
58507
|
ui,
|
|
58325
58508
|
maxLines = 1,
|
|
@@ -58364,8 +58547,8 @@ const PickerButton = props => {
|
|
|
58364
58547
|
basePseudoState: basePseudoState,
|
|
58365
58548
|
styleCSSVars: PickerStyleCSSVars,
|
|
58366
58549
|
variant: undefined,
|
|
58367
|
-
|
|
58368
|
-
|
|
58550
|
+
rightSlotIcon: undefined,
|
|
58551
|
+
rightSlotIconSize: undefined,
|
|
58369
58552
|
ui: undefined,
|
|
58370
58553
|
maxLines: undefined,
|
|
58371
58554
|
popupWidthFitContent: undefined,
|
|
@@ -58502,7 +58685,7 @@ const PickerButton = props => {
|
|
|
58502
58685
|
flex: true,
|
|
58503
58686
|
align: "center",
|
|
58504
58687
|
children: jsx(Icon, {
|
|
58505
|
-
size:
|
|
58688
|
+
size: rightSlotIconSize,
|
|
58506
58689
|
lineOverflow: "allow",
|
|
58507
58690
|
children: jsx(CloseSvg, {})
|
|
58508
58691
|
})
|
|
@@ -58511,9 +58694,9 @@ const PickerButton = props => {
|
|
|
58511
58694
|
// character — a caller asking for a bigger one wants it bigger,
|
|
58512
58695
|
// not capped at the height of the line it sits on
|
|
58513
58696
|
jsx(Icon, {
|
|
58514
|
-
size:
|
|
58697
|
+
size: rightSlotIconSize,
|
|
58515
58698
|
lineOverflow: "allow",
|
|
58516
|
-
children:
|
|
58699
|
+
children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
|
|
58517
58700
|
})
|
|
58518
58701
|
})]
|
|
58519
58702
|
}), jsx(ControlFacadeChildrenWrapper, {
|
|
@@ -58604,6 +58787,8 @@ const PickerStyleCSSVars = {
|
|
|
58604
58787
|
"borderWidth": "--picker-border-width",
|
|
58605
58788
|
"borderRadius": "--picker-border-radius",
|
|
58606
58789
|
"popoverMaxHeight": "--picker-popover-max-height",
|
|
58790
|
+
"dialogMaxWidth": "--picker-dialog-max-width",
|
|
58791
|
+
"dialogMaxHeight": "--picker-dialog-max-height",
|
|
58607
58792
|
"popupBackgroundColor": "--picker-popup-background-color",
|
|
58608
58793
|
"popupBorderRadius": "--picker-popup-border-radius",
|
|
58609
58794
|
"dialogBorderWidth": "--picker-dialog-border-width",
|
|
@@ -58683,11 +58868,14 @@ const PickerFirstResolver = props => {
|
|
|
58683
58868
|
* popoverMode?: "nearby" | "overlay",
|
|
58684
58869
|
* positionArea?: string,
|
|
58685
58870
|
* popupWidthFitContent?: boolean,
|
|
58686
|
-
* variant?: "icon" | "headless",
|
|
58687
|
-
*
|
|
58871
|
+
* variant?: "icon" | "headless" | "discrete",
|
|
58872
|
+
* rightSlotIcon?: import("ignore:preact").ComponentChildren,
|
|
58873
|
+
* rightSlotIconSize?: number | string,
|
|
58688
58874
|
* maxLines?: number,
|
|
58689
58875
|
* slotSpacing?: number | string,
|
|
58690
58876
|
* popoverMaxHeight?: number | string,
|
|
58877
|
+
* dialogMaxWidth?: number | string,
|
|
58878
|
+
* dialogMaxHeight?: number | string,
|
|
58691
58879
|
* popupBackgroundColor?: string,
|
|
58692
58880
|
* popupBorderRadius?: number | string,
|
|
58693
58881
|
* clearable?: boolean,
|
|
@@ -58695,6 +58883,9 @@ const PickerFirstResolver = props => {
|
|
|
58695
58883
|
* dialogExpand?: boolean,
|
|
58696
58884
|
* dialogExpandX?: boolean,
|
|
58697
58885
|
* dialogExpandY?: boolean,
|
|
58886
|
+
* marginWithContainer?: number | string,
|
|
58887
|
+
* escapeEffect?: "cancel" | "close",
|
|
58888
|
+
* pointerInteractionOutsideEffect?: "close" | "cancel" | "capture",
|
|
58698
58889
|
* ref?: import("ignore:preact").RefObject<HTMLElement>,
|
|
58699
58890
|
* [key: string]: any,
|
|
58700
58891
|
* }>}
|
|
@@ -58714,6 +58905,13 @@ const PickerFirstResolver = props => {
|
|
|
58714
58905
|
* @param {boolean} [popupWidthFitContent] By default the popup is at least as
|
|
58715
58906
|
* wide as the trigger. Set this to let the content size it instead, so a
|
|
58716
58907
|
* popup narrower than the trigger stays narrow.
|
|
58908
|
+
* @param {import("ignore:preact").ComponentChildren} [rightSlotIcon] What the right
|
|
58909
|
+
* slot draws in place of the chevron. It is the whole slot, not an addition
|
|
58910
|
+
* to it: the picker then no longer says on its own that it opens, so pass
|
|
58911
|
+
* something that does.
|
|
58912
|
+
* @param {number|string} [rightSlotIconSize="inherit"] How big what sits in the
|
|
58913
|
+
* right slot is drawn — the chevron, a `rightSlotIcon`, or the clear button's
|
|
58914
|
+
* cross. "inherit" takes the picker's own font size.
|
|
58717
58915
|
* @param {number|string} [slotSpacing] Gap kept between what sits in the right
|
|
58718
58916
|
* slot (the chevron, or the clear button) and the picker's own edge — same
|
|
58719
58917
|
* prop name Input uses for its own slots. Accepts a spacing token ("s",
|
|
@@ -58721,6 +58919,25 @@ const PickerFirstResolver = props => {
|
|
|
58721
58919
|
* horizontal padding.
|
|
58722
58920
|
* @param {number|string} [popoverMaxHeight] Soft cap on the popover's height
|
|
58723
58921
|
* (default 300px). The popover shrinks below it when space is tight.
|
|
58922
|
+
* @param {number|string} [dialogMaxWidth] Ceiling on the dialog's width, the
|
|
58923
|
+
* one `dialogExpand`/`dialogExpandX` grows up to — what makes an expanded
|
|
58924
|
+
* dialog a large sheet rather than a full screen. Capped in turn by the
|
|
58925
|
+
* container minus `marginWithContainer`.
|
|
58926
|
+
* @param {number|string} [dialogMaxHeight] Same, on the height.
|
|
58927
|
+
* @param {"cancel"|"close"} [escapeEffect="cancel"] What Escape does to an open
|
|
58928
|
+
* picker. "cancel" puts back the value the picker had at open, and a dialog
|
|
58929
|
+
* picker also goes back in history — so anything written to the url while it
|
|
58930
|
+
* was open (a route `stateSignal`, a search param) goes back with it. "close"
|
|
58931
|
+
* makes Escape say what clicking outside says: keep what was chosen, close.
|
|
58932
|
+
* @param {"close"|"cancel"|"capture"} [pointerInteractionOutsideEffect="close"]
|
|
58933
|
+
* What a click outside the popup does: close and keep ("close"), close and
|
|
58934
|
+
* put back the value at open ("cancel"), or nothing at all ("capture").
|
|
58935
|
+
* @param {number|string} [marginWithContainer] Minimum gap kept between the
|
|
58936
|
+
* popup and the edges of what contains it (the viewport, or the picker's own
|
|
58937
|
+
* positioned ancestor for `popupLayer="local"`). Caps the popup's size as
|
|
58938
|
+
* well as its placement, so what an expanded dialog leaves visible around
|
|
58939
|
+
* itself is set here. Defaults to `popoverSpacing` in popover mode, and to
|
|
58940
|
+
* Dialog's own 3vvw in dialog mode.
|
|
58724
58941
|
*/
|
|
58725
58942
|
const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
|
|
58726
58943
|
Picker.UI = PickerDefaultUI;
|
|
@@ -60540,12 +60757,26 @@ const usePlaceholderHeight = (ref, placeholder) => {
|
|
|
60540
60757
|
textareaEl.style.setProperty("--x-textarea-placeholder-height", `${contentHeight}px`);
|
|
60541
60758
|
};
|
|
60542
60759
|
measure();
|
|
60760
|
+
// Measuring writes the variable that sets this element's own height, and
|
|
60761
|
+
// mutating layout from inside a resize callback is what makes the browser
|
|
60762
|
+
// report "ResizeObserver loop completed with undelivered notifications".
|
|
60763
|
+
// So the write waits for the frame that resize produced.
|
|
60764
|
+
let measureFrame = null;
|
|
60765
|
+
const requestMeasure = () => {
|
|
60766
|
+
if (measureFrame !== null) {
|
|
60767
|
+
return;
|
|
60768
|
+
}
|
|
60769
|
+
measureFrame = requestAnimationFrame(() => {
|
|
60770
|
+
measureFrame = null;
|
|
60771
|
+
measure();
|
|
60772
|
+
});
|
|
60773
|
+
};
|
|
60543
60774
|
// The placeholder wraps against the available width, so a new width is a
|
|
60544
60775
|
// new number of lines. Height changes are ignored: this measure is what
|
|
60545
60776
|
// causes them, and reacting to them would be reacting to ourselves.
|
|
60546
60777
|
const resizeObserver = new ResizeObserver(() => {
|
|
60547
60778
|
if (textareaEl.clientWidth !== widthMeasured) {
|
|
60548
|
-
|
|
60779
|
+
requestMeasure();
|
|
60549
60780
|
}
|
|
60550
60781
|
});
|
|
60551
60782
|
resizeObserver.observe(textareaEl);
|
|
@@ -60558,6 +60789,9 @@ const usePlaceholderHeight = (ref, placeholder) => {
|
|
|
60558
60789
|
};
|
|
60559
60790
|
textareaEl.addEventListener("input", onInput);
|
|
60560
60791
|
return () => {
|
|
60792
|
+
if (measureFrame !== null) {
|
|
60793
|
+
cancelAnimationFrame(measureFrame);
|
|
60794
|
+
}
|
|
60561
60795
|
resizeObserver.disconnect();
|
|
60562
60796
|
textareaEl.removeEventListener("input", onInput);
|
|
60563
60797
|
};
|
|
@@ -69586,6 +69820,150 @@ const ViewportLayout = props => {
|
|
|
69586
69820
|
});
|
|
69587
69821
|
};
|
|
69588
69822
|
|
|
69823
|
+
/**
|
|
69824
|
+
* Pushing a side panel back the way it came in closes it: the panel follows
|
|
69825
|
+
* the finger while it travels, and letting go either finishes the departure or
|
|
69826
|
+
* brings it back to rest — whichever the travel was already heading to.
|
|
69827
|
+
*
|
|
69828
|
+
* Reading the pointer is not done here: when a press becomes a gesture, which
|
|
69829
|
+
* axis it leans on, how fast it was going when it was let go, who else has a
|
|
69830
|
+
* claim on it (a field, a scroller with room left, a box inside the panel that
|
|
69831
|
+
* travels the same way) is one gesture system for the whole codebase
|
|
69832
|
+
* (@jsenv/dom's startDragToTravel). A panel closing is a travel of exactly one
|
|
69833
|
+
* box, towards the edge it is docked to and nowhere else — which is all this
|
|
69834
|
+
* file has to say.
|
|
69835
|
+
*
|
|
69836
|
+
* The release travel is driven here (Web Animations) rather than handed to the
|
|
69837
|
+
* exit transition the `animation` prop may install: its pace is what is left to
|
|
69838
|
+
* cover, which a CSS transition written for a full-size travel cannot know, and
|
|
69839
|
+
* two movements on the same property would contradict each other. So CSS
|
|
69840
|
+
* transitions are suppressed on the panel for the whole gesture, release
|
|
69841
|
+
* included, and restored once the panel has either left or come back.
|
|
69842
|
+
*/
|
|
69843
|
+
|
|
69844
|
+
|
|
69845
|
+
// What a full-size release travel takes; a shorter one takes proportionally
|
|
69846
|
+
// less, so the panel keeps the same speed whenever the finger let go.
|
|
69847
|
+
const TRAVEL_DURATION = 220;
|
|
69848
|
+
|
|
69849
|
+
// Which way a panel docked to each side travels — read by the panel itself to
|
|
69850
|
+
// say so in the DOM, which is how a box travelling INSIDE it (a row of slides,
|
|
69851
|
+
// a route travel) knows that axis is already walked.
|
|
69852
|
+
const SWIPE_AXIS_BY_SIDE = {
|
|
69853
|
+
left: "x",
|
|
69854
|
+
right: "x",
|
|
69855
|
+
top: "y",
|
|
69856
|
+
bottom: "y",
|
|
69857
|
+
};
|
|
69858
|
+
// Which way the finger pushes to send the panel back where it came from, in
|
|
69859
|
+
// screen coordinates: positive is right/down, the same sign the gesture reports.
|
|
69860
|
+
const CLOSE_DIRECTION_BY_SIDE = { left: -1, right: 1, top: -1, bottom: 1 };
|
|
69861
|
+
|
|
69862
|
+
/**
|
|
69863
|
+
* Builds the `pointerdown` handler a side panel docked to `side` answers with.
|
|
69864
|
+
*/
|
|
69865
|
+
const createSwipeToClose = (side) => {
|
|
69866
|
+
const axis = SWIPE_AXIS_BY_SIDE[side];
|
|
69867
|
+
const closeDirection = CLOSE_DIRECTION_BY_SIDE[side];
|
|
69868
|
+
|
|
69869
|
+
return (pointerDownEvent) => {
|
|
69870
|
+
const panelEl = pointerDownEvent.currentTarget;
|
|
69871
|
+
if (panelEl.getAttribute("aria-expanded") !== "true") {
|
|
69872
|
+
return;
|
|
69873
|
+
}
|
|
69874
|
+
|
|
69875
|
+
// Where the panel stands, written on it directly: the gesture reports a
|
|
69876
|
+
// distance in screen coordinates, which is exactly what a translate takes.
|
|
69877
|
+
const paint = (distance) => {
|
|
69878
|
+
panelEl.style.translate =
|
|
69879
|
+
axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
|
|
69880
|
+
};
|
|
69881
|
+
const restore = () => {
|
|
69882
|
+
panelEl.style.translate = "";
|
|
69883
|
+
panelEl.style.transitionProperty = "";
|
|
69884
|
+
panelEl.style.userSelect = "";
|
|
69885
|
+
};
|
|
69886
|
+
// The resting value is written first and the animation only covers the way
|
|
69887
|
+
// to it: both end on the same number, so nothing is seen changing when the
|
|
69888
|
+
// animation hands the panel back to its own style.
|
|
69889
|
+
const travelTo = (from, to, onArrival) => {
|
|
69890
|
+
const covered = from > to ? from - to : to - from;
|
|
69891
|
+
paint(to);
|
|
69892
|
+
const animation = panelEl.animate(
|
|
69893
|
+
[
|
|
69894
|
+
{ translate: translateOf(axis, from) },
|
|
69895
|
+
{ translate: translateOf(axis, to) },
|
|
69896
|
+
],
|
|
69897
|
+
{
|
|
69898
|
+
duration: (covered / sizeOf(panelEl, axis)) * TRAVEL_DURATION,
|
|
69899
|
+
easing: "ease-out",
|
|
69900
|
+
},
|
|
69901
|
+
);
|
|
69902
|
+
animation.finished.then(onArrival, () => {});
|
|
69903
|
+
};
|
|
69904
|
+
const close = (event) => {
|
|
69905
|
+
triggerNaviCommand(panelEl, "--navi-close", event);
|
|
69906
|
+
if (panelEl.getAttribute("aria-expanded") === "true") {
|
|
69907
|
+
// Refused: the panel is staying, so it goes back where it was.
|
|
69908
|
+
travelTo(sizeOf(panelEl, axis) * closeDirection, 0, restore);
|
|
69909
|
+
return;
|
|
69910
|
+
}
|
|
69911
|
+
restore();
|
|
69912
|
+
};
|
|
69913
|
+
|
|
69914
|
+
startDragToTravel(pointerDownEvent, {
|
|
69915
|
+
element: panelEl,
|
|
69916
|
+
axes: axis,
|
|
69917
|
+
onStart: ({ sign, target }) => {
|
|
69918
|
+
if (sign !== closeDirection) {
|
|
69919
|
+
// Pushing the panel further in is not a travel: there is nowhere that
|
|
69920
|
+
// way, and reading it as one would make the panel lean at every
|
|
69921
|
+
// gesture that starts by going the wrong way.
|
|
69922
|
+
return false;
|
|
69923
|
+
}
|
|
69924
|
+
const size = sizeOf(panelEl, axis);
|
|
69925
|
+
if (!size) {
|
|
69926
|
+
return false;
|
|
69927
|
+
}
|
|
69928
|
+
// Something else with a better claim on the gesture: a scroller between
|
|
69929
|
+
// the finger and the panel, with room left that way.
|
|
69930
|
+
if (scrollRoomTowards(target, panelEl, axis, sign)) {
|
|
69931
|
+
return false;
|
|
69932
|
+
}
|
|
69933
|
+
panelEl.style.transitionProperty = "none";
|
|
69934
|
+
panelEl.style.userSelect = "none";
|
|
69935
|
+
// What the first pixels of the gesture may have started selecting is
|
|
69936
|
+
// not a selection, it is the beginning of this travel.
|
|
69937
|
+
window.getSelection().removeAllRanges();
|
|
69938
|
+
// The way out is the only way there is anything: pulling the other way
|
|
69939
|
+
// leans against a wall and comes back.
|
|
69940
|
+
return {
|
|
69941
|
+
size,
|
|
69942
|
+
travelBack: closeDirection > 0,
|
|
69943
|
+
travelOn: closeDirection < 0,
|
|
69944
|
+
};
|
|
69945
|
+
},
|
|
69946
|
+
onPull: ({ pulled }) => {
|
|
69947
|
+
paint(pulled);
|
|
69948
|
+
},
|
|
69949
|
+
onEnd: ({ pulled, size, travels, event }) => {
|
|
69950
|
+
if (travels) {
|
|
69951
|
+
travelTo(pulled, size * closeDirection, () => close(event));
|
|
69952
|
+
return;
|
|
69953
|
+
}
|
|
69954
|
+
travelTo(pulled, 0, restore);
|
|
69955
|
+
},
|
|
69956
|
+
});
|
|
69957
|
+
};
|
|
69958
|
+
};
|
|
69959
|
+
|
|
69960
|
+
const sizeOf = (element, axis) => {
|
|
69961
|
+
const rect = element.getBoundingClientRect();
|
|
69962
|
+
return axis === "x" ? rect.width : rect.height;
|
|
69963
|
+
};
|
|
69964
|
+
const translateOf = (axis, distance) =>
|
|
69965
|
+
axis === "x" ? `${distance}px 0px` : `0px ${distance}px`;
|
|
69966
|
+
|
|
69589
69967
|
installImportMetaCssBuild(import.meta);/**
|
|
69590
69968
|
* A drawer docked flush to a viewport (or container) edge, built on top of
|
|
69591
69969
|
* `Popup`. Sizing, the perpendicular-axis fill, and the flush-edge
|
|
@@ -69714,6 +70092,20 @@ const css = /* css */`
|
|
|
69714
70092
|
}
|
|
69715
70093
|
}
|
|
69716
70094
|
|
|
70095
|
+
/* What a touch may do on a panel that closes by being pushed back (see
|
|
70096
|
+
swipe_to_close.js): a left/right panel scrolls its own content
|
|
70097
|
+
downwards and travels sideways, so the two never compete and the
|
|
70098
|
+
browser is told to leave the sideways one alone — which also keeps a
|
|
70099
|
+
swipe near the screen edge from being read as "go back" instead. A
|
|
70100
|
+
top/bottom panel travels the way it scrolls: nothing can be given away
|
|
70101
|
+
there without losing the scroll, so the gesture is settled by
|
|
70102
|
+
swipe_to_close.js's own reading of what is still scrollable, and the
|
|
70103
|
+
browser keeps both axes. */
|
|
70104
|
+
&[data-swipe-to-close][navi-side="left"],
|
|
70105
|
+
&[data-swipe-to-close][navi-side="right"] {
|
|
70106
|
+
touch-action: pan-y;
|
|
70107
|
+
}
|
|
70108
|
+
|
|
69717
70109
|
/* Sticky regardless of side: the panel's own content always stacks
|
|
69718
70110
|
(and scrolls) top-to-bottom, whether the panel itself is docked to
|
|
69719
70111
|
the left/right or the top/bottom of the viewport/container — so
|
|
@@ -69792,6 +70184,11 @@ const css = /* css */`
|
|
|
69792
70184
|
* navigation inside the panel (`focusCapture`) — closing on outside
|
|
69793
70185
|
* interaction only makes sense paired with not letting focus silently
|
|
69794
70186
|
* leave the panel first.
|
|
70187
|
+
* @param {boolean} [props.swipeToClose=true] - Pushing the panel back
|
|
70188
|
+
* towards the edge it is docked to closes it: the panel follows the
|
|
70189
|
+
* pointer and finishes leaving (or comes back to rest) when it is
|
|
70190
|
+
* released. Set to `false` for a panel that must only ever be dismissed
|
|
70191
|
+
* deliberately. See `swipe_to_close.js` for when the gesture is claimed.
|
|
69795
70192
|
* @param {"dialog"|"popover"} [props.mode] - Forwarded to `Popup` — forces
|
|
69796
70193
|
* one underlying renderer instead of its automatic screen-size
|
|
69797
70194
|
* resolution. Note that if `Popup` ends up in dialog mode (small screen,
|
|
@@ -69817,12 +70214,14 @@ const SidePanel = ({
|
|
|
69817
70214
|
minHeight,
|
|
69818
70215
|
animation,
|
|
69819
70216
|
closeOnClickOutside = false,
|
|
70217
|
+
swipeToClose = true,
|
|
69820
70218
|
mode,
|
|
69821
70219
|
layer = "top",
|
|
69822
70220
|
className,
|
|
69823
70221
|
...rest
|
|
69824
70222
|
}) => {
|
|
69825
70223
|
import.meta.css = [css, "@jsenv/navi/src/layout/side_panel.jsx"];
|
|
70224
|
+
const onSwipePointerDown = swipeToClose ? createSwipeToClose(side) : null;
|
|
69826
70225
|
return jsx(Popup, {
|
|
69827
70226
|
mode: mode,
|
|
69828
70227
|
open: open,
|
|
@@ -69843,7 +70242,20 @@ const SidePanel = ({
|
|
|
69843
70242
|
minHeight: toCssLength(minHeight),
|
|
69844
70243
|
className: withPropsClassName("navi_side_panel", className),
|
|
69845
70244
|
"navi-side": side,
|
|
70245
|
+
"data-swipe-to-close": swipeToClose ? "" : undefined
|
|
70246
|
+
// The axis the panel travels on when it is pushed back, said to the
|
|
70247
|
+
// shared gesture layer: it keeps the panel's scrolling from spilling onto
|
|
70248
|
+
// the page, and it is what a box travelling inside the panel reads to
|
|
70249
|
+
// know this axis is already walked (see @jsenv/dom's drag_to_travel).
|
|
70250
|
+
,
|
|
70251
|
+
|
|
70252
|
+
"data-drag-travel": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined,
|
|
70253
|
+
"data-travel-by-drag": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined,
|
|
69846
70254
|
...rest,
|
|
70255
|
+
onPointerDown: pointerDownEvent => {
|
|
70256
|
+
rest.onPointerDown?.(pointerDownEvent);
|
|
70257
|
+
onSwipePointerDown?.(pointerDownEvent);
|
|
70258
|
+
},
|
|
69847
70259
|
style: {
|
|
69848
70260
|
"--navi-side-panel-width": toCssLength(width, "width"),
|
|
69849
70261
|
"--navi-side-panel-height": toCssLength(height, "height"),
|