@jsenv/navi 0.29.148 → 0.29.150

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.
@@ -3108,6 +3108,14 @@ const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
3108
3108
  *
3109
3109
  * An action run never throws: failures land in errorSignal and are reported
3110
3110
  * once, by one rule, in action_error_report.js (see the comment in onRunError).
3111
+ *
3112
+ * Blast radius: this file is the substrate of routing (route_action.js,
3113
+ * action_run_effect.js), resources (state/rest) and every control's `action`
3114
+ * prop. Its own unit tests cover almost none of that, so a change here is
3115
+ * verified against the consumers' nets: tests/route_tabs and
3116
+ * tests/route_transition_list_revisit (browser), src/state/rest/tests and
3117
+ * src/nav/tests (node) — src/action/*.test.js alone proves nothing about
3118
+ * routing or lists.
3111
3119
  */
3112
3120
 
3113
3121
  let DEBUG$2 = false;
@@ -3554,6 +3562,14 @@ ${lines.join("\n")}`);
3554
3562
 
3555
3563
  const NO_PARAMS = { __no_params__: true };
3556
3564
  const mergeActionParams = (currentParams, newParams) => {
3565
+ // The order of these two checks is load-bearing. Checking `undefined` first
3566
+ // looks symmetric — "no new params, keep whatever is there, NO_PARAMS
3567
+ // included" — and breaks routing: merge(NO_PARAMS, undefined) must yield
3568
+ // `undefined` so the proxy targets a runnable child, because NO_PARAMS means
3569
+ // "no params yet, nothing to run" to _updateTarget and the run-effect
3570
+ // machinery. With NO_PARAMS kept, a route action bound to a not-yet-matching
3571
+ // route resolves to no target and the page renders nothing
3572
+ // (tests/route_tabs is the net that catches it).
3557
3573
  if (currentParams === NO_PARAMS) {
3558
3574
  return newParams;
3559
3575
  }
@@ -3719,6 +3735,16 @@ const createAction = (callback, rootOptions = {}) => {
3719
3735
  });
3720
3736
  }
3721
3737
  const combinedParams = mergeActionParams(params, newParamsOrSignal);
3738
+ if (combinedParams === params) {
3739
+ // Binding added nothing (mergeTwoJsValues returns the current
3740
+ // params by reference when the new ones change no key): equal
3741
+ // params must give the same instance, and the instance holding
3742
+ // these exact params is this action. A separate child here would
3743
+ // run with its own signals, invisible to whoever holds this one —
3744
+ // a <Button action={A}> runs through this path (its UI state
3745
+ // contributes no params), and useActionStatus(A) must see that run.
3746
+ return action;
3747
+ }
3722
3748
  return createChildAction({
3723
3749
  ...options,
3724
3750
  params: combinedParams,
@@ -3758,7 +3784,13 @@ const createAction = (callback, rootOptions = {}) => {
3758
3784
  }
3759
3785
  const childAction = _bindParams(newParamsOrSignal, options);
3760
3786
  childActionWeakMap.set(newParamsOrSignal, childAction);
3761
- childActionWeakSet.add(childAction);
3787
+ if (childAction !== action) {
3788
+ // binding that added nothing resolves to the action itself; it must
3789
+ // not enter its own child set or matchAllSelfOrDescendant would
3790
+ // traverse it forever (the cache above may still hold it: a plain
3791
+ // lookup, never traversed)
3792
+ childActionWeakSet.add(childAction);
3793
+ }
3762
3794
 
3763
3795
  return childAction;
3764
3796
  };
@@ -5395,9 +5427,11 @@ const CONTROL_PROP_SET = new Set([
5395
5427
  // same thing for every control at once.
5396
5428
  "constraints",
5397
5429
 
5398
- // A real target inside a zone that belongs to another control — see
5399
- // own_target.js.
5400
- "ownTarget",
5430
+ // The interactions this element takes for itself inside a zone that belongs
5431
+ // to another control, and what becomes of it where they are blocked — see
5432
+ // self_interactions.js.
5433
+ "selfInteractions",
5434
+ "whenSelfInteractionsBlocked",
5401
5435
 
5402
5436
  "autoFocus",
5403
5437
  "autoFocusVisible",
@@ -5459,35 +5493,55 @@ const ActionContext = createContext();
5459
5493
  const ActionRequesterContext = createContext();
5460
5494
 
5461
5495
  /**
5462
- * A target of its own inside a zone that belongs to another control.
5496
+ * The interactions an element takes for itself inside a zone that belongs to
5497
+ * another control.
5463
5498
  *
5464
5499
  * A pressable row, a picker's façade, a slide that travels under the finger:
5465
- * each of them answers a press that lands anywhere in its box. An affordance an
5466
- * application draws in there — a chip's cross, an eye that opens a profile, a
5467
- * diskette that saves a guest — is aimed AT, not merely inside, and the press
5468
- * belongs to it alone.
5469
- *
5470
- * Saying that by hand takes three guards, one per moment of the same press: the
5471
- * pointerdown where gestures are arbitrated, the mousedown where a picker opens,
5472
- * the click where it opens too when a gesture disputed the press. All three are
5473
- * navi's own knowledge of navi's own event flow, and an application that gets
5474
- * one wrong finds out by opening a popup it meant to keep shut. `ownTarget` is
5475
- * that knowledge, said once.
5476
- *
5477
- * The other half is interactivity, and the question that settles it is: **does
5478
- * this affordance write to the control it sits in?**
5500
+ * each of them answers a gesture that lands anywhere in its box. An affordance
5501
+ * an application draws in there — a chip's cross, an eye that opens a profile, a
5502
+ * diskette that saves a guest — is aimed AT, not merely inside.
5503
+ *
5504
+ * But aimed at for WHAT. A press is not a drag: a drag announces itself (a few
5505
+ * pixels of travel with a mouse, a long hold with a finger), a click is the
5506
+ * absence of both, and the DOM tells them apart without anyone guessing at
5507
+ * pointerdown. So an affordance drawn against the edge of a card can take the
5508
+ * click and leave the grab: the finger that carries the card is allowed to start
5509
+ * on it. A claim over every gesture at once would be a hole in the card, at
5510
+ * precisely the place one grabs.
5511
+ *
5512
+ * Hence a list, and hence it is required: what is named belongs to this element,
5513
+ * what is not stays the zone's.
5514
+ *
5515
+ * selfInteractions="click" the press is mine, the grab is the card's
5516
+ * selfInteractions="click drag" both — an affordance carried on its own
5517
+ * selfInteractions="*" everything; say it only when it is true
5518
+ *
5519
+ * Saying "the press is mine" by hand takes three guards, one per moment of the
5520
+ * same press: the pointerdown where gestures are arbitrated, the mousedown where
5521
+ * a picker opens, the click where it opens too when a gesture disputed the
5522
+ * press. All three are navi's own knowledge of navi's own event flow, and an
5523
+ * application that gets one wrong finds out by opening a popup it meant to keep
5524
+ * shut. `selfInteractions` is that knowledge, said once.
5525
+ *
5526
+ * The other half is `whenSelfInteractionsBlocked`: what becomes of the
5527
+ * affordance where the zone around it is disabled or read-only. The question
5528
+ * that settles it is **does this affordance write to the control it sits in?**
5479
5529
  *
5480
5530
  * - it does, and once the value cannot be changed it has nothing left to offer:
5481
- * it GOES. A remove cross that still removes is worse than no cross, and one
5482
- * that refuses politely still says "there is something to remove here" on a
5483
- * row that is only being read. That is the default.
5531
+ * it GOES (`"hide"`, the default). A remove cross that still removes is worse
5532
+ * than no cross, and one that refuses politely still says "there is something
5533
+ * to remove here" on a row that is only being read.
5484
5534
  * - it does, but its presence is information in itself: `"refuse"` keeps it and
5485
5535
  * refuses in its own words, like every other navi control.
5486
5536
  * - it does NOT — a diskette saving a row into the reader's own address book, a
5487
- * badge explaining why a placement is odd. The read-only around it is about a
5488
- * value it never touches, so `"always"` ignores it: the affordance stays lit
5489
- * and stays pressable. It is on the caller to use it only for a gesture that
5490
- * genuinely writes nothing to the control around it.
5537
+ * badge explaining why a placement is odd. The block is about a value it never
5538
+ * touches, so `"ignore"` lets it through: the affordance stays lit and stays
5539
+ * pressable. It is on the caller to use it only for a gesture that genuinely
5540
+ * writes nothing to the control around it.
5541
+ *
5542
+ * That question is scoped to the claimed interactions and to them alone — the
5543
+ * ones left to the zone were never this element's to block, and are arbitrated
5544
+ * by the zone's own state.
5491
5545
  *
5492
5546
  * Whichever mode, the affordance's own handler runs from inside its own gate
5493
5547
  * rather than from the DOM: a caller's `onClick` fires before any of this, and
@@ -5500,65 +5554,100 @@ const ActionRequesterContext = createContext();
5500
5554
  * draws itself as much as a navi control. It is read from the outside and
5501
5555
  * nowhere else: by the controls above (below), and by the gesture readers
5502
5556
  * (@jsenv/dom's DRAG_EXCLUDED_SELECTOR and DRAG_IGNORED_SELECTOR), which is why
5503
- * one attribute is enough and the `ownTarget` prop only writes it.
5557
+ * one attribute is enough and the `selfInteractions` prop only writes it.
5504
5558
  *
5505
- * Its value is the mode, when there is one to say: `data-own-target="always"`.
5559
+ * A space-separated list, so every reader picks its own word out of it with the
5560
+ * attribute selector the DOM already has for that: `[data-self-interactions~=
5561
+ * "drag"]`. Nobody parses anything, and a reader that knows nothing of the other
5562
+ * words is unaffected by them.
5506
5563
  */
5507
- const OWN_TARGET_ATTRIBUTE = "data-own-target";
5564
+ const SELF_INTERACTIONS_ATTRIBUTE = "data-self-interactions";
5565
+ const ALL_SELF_INTERACTIONS = "*";
5508
5566
 
5509
5567
  /**
5510
- * Whether `event` was aimed at an own target sitting below this control — in
5511
- * which case the control is not what the press was for and must not answer it.
5568
+ * The value to write in the DOM, and the one place a malformed claim is caught:
5569
+ * a word nobody reads is silent forever otherwise, which is exactly the trap
5570
+ * this API exists to remove.
5571
+ */
5572
+ const selfInteractionsAttributeValue = (selfInteractions) => {
5573
+ return typeof selfInteractions === "string"
5574
+ ? selfInteractions
5575
+ : ALL_SELF_INTERACTIONS;
5576
+ };
5577
+
5578
+ const selfInteractionSelector = (interaction) =>
5579
+ `[${SELF_INTERACTIONS_ATTRIBUTE}~="${interaction}"],[${SELF_INTERACTIONS_ATTRIBUTE}~="${ALL_SELF_INTERACTIONS}"]`;
5580
+
5581
+ // What navi's own interaction gate answers to. The press-to-act chain is one
5582
+ // thing said at three moments (pointerdown, mousedown, click), and a keyboard
5583
+ // activation is a click too — so one word covers the lot.
5584
+ const CLICK_SELECTOR = selfInteractionSelector("click");
5585
+
5586
+ /**
5587
+ * Whether `event` was aimed at an element below this control that claims the
5588
+ * press — in which case the control is not what the press was for and must not
5589
+ * answer it.
5512
5590
  *
5513
5591
  * Read from the event's target rather than from a mark left by a handler: the
5514
5592
  * question is "who is this press for", and the DOM between the pointer and the
5515
- * control is the whole answer. Nothing is asked of the own target itself, which
5516
- * is what lets it be anything — a button, a link, a field, a bare element.
5593
+ * control is the whole answer. Nothing is asked of the element itself, which is
5594
+ * what lets it be anything — a button, a link, a field, a bare element.
5595
+ *
5596
+ * The closest element CLAIMING THE PRESS wins, not the closest one claiming
5597
+ * anything: an affordance that took only the drag is transparent here, exactly
5598
+ * as it is to the drag readers when it took only the click.
5517
5599
  */
5518
- const isAimedAtOwnTargetBelow = (event, controlHost) => {
5600
+ const isAimedAtSelfInteractionsBelow = (event, controlHost) => {
5519
5601
  const target = event?.target;
5520
5602
  if (!target || typeof target.closest !== "function") {
5521
5603
  return false;
5522
5604
  }
5523
- const ownTarget = target.closest(`[${OWN_TARGET_ATTRIBUTE}]`);
5524
- if (!ownTarget) {
5605
+ const claimer = target.closest(CLICK_SELECTOR);
5606
+ if (!claimer) {
5525
5607
  return false;
5526
5608
  }
5527
- // The claim is against what is ABOVE it: the control that IS the own target,
5528
- // and any control living inside it, are being aimed at like anything else.
5529
- if (ownTarget === controlHost || ownTarget.contains(controlHost)) {
5609
+ // The claim is against what is ABOVE it: the control that IS the claimer, and
5610
+ // any control living inside it, are being aimed at like anything else.
5611
+ if (claimer === controlHost || claimer.contains(controlHost)) {
5530
5612
  return false;
5531
5613
  }
5532
5614
  // From the root rather than the host: a layered control (a picker holding an
5533
5615
  // input) has its gate on the input, and what the application drew sits beside
5534
5616
  // it, not in it.
5535
5617
  const controlRoot = controlHost.closest("[navi-control]") || controlHost;
5536
- return controlRoot.contains(ownTarget);
5618
+ return controlRoot.contains(claimer);
5537
5619
  };
5538
5620
 
5539
5621
  /**
5540
- * Whether the read-only, disabled and busy of the zone around this own target
5541
- * are about it at all — see the modes at the top of this file.
5622
+ * Whether the disabled and read-only of the zone around this element are about
5623
+ * its own interactions at all — see the modes at the top of this file.
5542
5624
  */
5543
- const ownTargetIgnoresZoneState = (ownTarget) => ownTarget === "always";
5625
+ const selfInteractionsIgnoreBlock = (props) =>
5626
+ props.whenSelfInteractionsBlocked === "ignore";
5544
5627
 
5545
5628
  /**
5546
- * Whether an own target has nothing to offer where it sits: it writes to the
5547
- * control around it, and that control cannot be changed, so the affordance goes.
5629
+ * Whether the affordance has nothing left to offer where it sits: it writes to
5630
+ * the control around it, and that control cannot be written to, so it goes.
5631
+ *
5632
+ * Read from disabled and read-only alone. Busy is not a block — it is read-only
5633
+ * that blocks, which a running action sets on its way (see readOnlyBase in
5634
+ * control_hooks.jsx); a zone busy without being read-only is a zone whose
5635
+ * interactions still work.
5548
5636
  */
5549
- const useOwnTargetHidden = (props) => {
5637
+ const useSelfInteractionsHidden = (props) => {
5550
5638
  const disabled = useContext(DisabledContext);
5551
5639
  const readOnly = useContext(ReadOnlyContext);
5552
- const loading = useContext(LoadingContext$1);
5553
- const { ownTarget } = props;
5640
+ const { selfInteractions, whenSelfInteractionsBlocked } = props;
5641
+ if (!selfInteractions) {
5642
+ return false;
5643
+ }
5554
5644
  if (
5555
- !ownTarget ||
5556
- ownTarget === "refuse" ||
5557
- ownTargetIgnoresZoneState(ownTarget)
5645
+ whenSelfInteractionsBlocked &&
5646
+ whenSelfInteractionsBlocked !== "hide" // "refuse" stays and says why, "ignore" is not concerned
5558
5647
  ) {
5559
5648
  return false;
5560
5649
  }
5561
- return Boolean(disabled || readOnly || loading);
5650
+ return Boolean(disabled || readOnly);
5562
5651
  };
5563
5652
 
5564
5653
  /**
@@ -9766,14 +9855,18 @@ const onRequestInteraction = (
9766
9855
  const controlHost = findControlHost(currentTarget) || currentTarget;
9767
9856
 
9768
9857
  // Aimed at something else that lives in this control's box: a chip's cross, an
9769
- // eye, a diskette. The press is that affordance's alone (see own_target.js),
9770
- // and stepping back here — rather than stopping the propagation over there —
9771
- // is what leaves the event whole for everything that is not a navi
9772
- // interaction. Stepping back, not refusing: the reaction never happened, so
9773
- // its `prevented`/`always` (an `e.preventDefault()`, for most of them) have
9774
- // nothing to undo and would take the press from the affordance itself.
9775
- if (isAimedAtOwnTargetBelow(event, controlHost)) {
9776
- debugInteraction(event, `"${name}" is for an own target below`);
9858
+ // eye, a diskette, claiming the press for itself (see self_interactions.js).
9859
+ // The press is that element's alone, and stepping back here — rather than
9860
+ // stopping the propagation over there — is what leaves the event whole for
9861
+ // everything that is not a navi interaction. Stepping back, not refusing: the
9862
+ // reaction never happened, so its `prevented`/`always` (an
9863
+ // `e.preventDefault()`, for most of them) have nothing to undo and would take
9864
+ // the press from the affordance itself.
9865
+ if (isAimedAtSelfInteractionsBelow(event, controlHost)) {
9866
+ debugInteraction(
9867
+ event,
9868
+ `"${name}" is for a self-interactions element below`,
9869
+ );
9777
9870
  requestInteractionCustomEvent.preventDefault();
9778
9871
  return false;
9779
9872
  }
@@ -20402,7 +20495,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
20402
20495
  * childPropSet?: Set<string>,
20403
20496
  * preventInitialTransition?: boolean,
20404
20497
  * separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
20405
- * ownTarget?: boolean | "refuse" | "always",
20498
+ * selfInteractions?: string,
20406
20499
  * children?: import("ignore:preact").ComponentChildren,
20407
20500
  * [key: string]: any,
20408
20501
  * }>}
@@ -20536,13 +20629,13 @@ const computeBox = (props, parentBoxFlow) => {
20536
20629
  // cross an application draws in a card's corner, a badge on a row that
20537
20630
  // travels. Writing the attribute is the whole of it here: it is read off
20538
20631
  // the DOM by the controls above and by the gesture readers, and what an
20539
- // affordance makes of the read-only around it is a question only a control
20540
- // can answer (see own_target.js).
20541
- ownTarget,
20632
+ // affordance makes of the block around it is a question only a control can
20633
+ // answer (see self_interactions.js).
20634
+ selfInteractions,
20542
20635
  ...rest
20543
20636
  } = props;
20544
- if (ownTarget) {
20545
- rest[OWN_TARGET_ATTRIBUTE] = typeof ownTarget === "string" ? ownTarget : "";
20637
+ if (selfInteractions) {
20638
+ rest[SELF_INTERACTIONS_ATTRIBUTE] = selfInteractionsAttributeValue(selfInteractions);
20546
20639
  }
20547
20640
  let as = asProp;
20548
20641
 
@@ -35212,10 +35305,11 @@ const useControlProps = (props, {
35212
35305
  return dispatched;
35213
35306
  };
35214
35307
 
35215
- // What the caller wrote runs from inside the gate when this control is an
35216
- // own target: a plain `onClick` is DOM, and it would fire from a cross drawn
35217
- // greyed by the read-only control the affordance sits in (see own_target.js).
35218
- const callerHandlerIsGated = Boolean(props.ownTarget);
35308
+ // What the caller wrote runs from inside the gate when this control claims
35309
+ // the press: a plain `onClick` is DOM, and it would fire from a cross drawn
35310
+ // greyed by the read-only control the affordance sits in (see
35311
+ // self_interactions.js).
35312
+ const callerHandlerIsGated = Boolean(props.selfInteractions);
35219
35313
  const gateCallerHandler = (handler, e) => {
35220
35314
  if (!handler) {
35221
35315
  return undefined;
@@ -35792,12 +35886,13 @@ const useInteractiveProps = (props, {
35792
35886
  } = props;
35793
35887
  const [controlRootProps, controlHostProps] = splitControlProps(props);
35794
35888
  controlRootProps["navi-control"] = controlInfo.controlType;
35795
- if (props.ownTarget) {
35889
+ if (props.selfInteractions) {
35796
35890
  // One attribute, in the DOM, because that is where the claim is read from —
35797
35891
  // by the controls above and by the gesture readers below (see
35798
- // own_target.js). The prop is the ergonomic form of it and nothing more: an
35799
- // element an application draws itself writes the same attribute by hand.
35800
- controlRootProps[OWN_TARGET_ATTRIBUTE] = typeof props.ownTarget === "string" ? props.ownTarget : "";
35892
+ // self_interactions.js). The prop is the ergonomic form of it and nothing
35893
+ // more: an element an application draws itself writes the same attribute by
35894
+ // hand.
35895
+ controlRootProps[SELF_INTERACTIONS_ATTRIBUTE] = selfInteractionsAttributeValue(props.selfInteractions);
35801
35896
  }
35802
35897
  const {
35803
35898
  "navi-control-proxy-for": naviProxyFor
@@ -35849,12 +35944,12 @@ const useInteractiveProps = (props, {
35849
35944
  optimistic
35850
35945
  } = props;
35851
35946
 
35852
- // `ownTarget="always"`: an affordance that writes nothing to the control it
35853
- // sits in has no business inheriting that control's state — a diskette
35854
- // saving a row into the reader's own address book stays pressable on a game
35855
- // nobody may edit. Its own props still hold; only what came from above is
35856
- // dropped (see own_target.js for the three modes).
35857
- const zoneStateApplies = !ownTargetIgnoresZoneState(props.ownTarget);
35947
+ // `whenSelfInteractionsBlocked="ignore"`: an affordance that writes nothing
35948
+ // to the control it sits in has no business inheriting that control's state
35949
+ // — a diskette saving a row into the reader's own address book stays
35950
+ // pressable on a game nobody may edit. Its own props still hold; only what
35951
+ // came from above is dropped (see self_interactions.js for the three modes).
35952
+ const zoneStateApplies = !selfInteractionsIgnoreBlock(props);
35858
35953
  const controlDisabled = zoneStateApplies && controlDisabledFromAbove;
35859
35954
  const controlReadOnly = zoneStateApplies && controlReadOnlyFromAbove;
35860
35955
  const controlLoading = zoneStateApplies && controlLoadingFromAbove;
@@ -42739,6 +42834,34 @@ const DetailsFieldContent = ({
42739
42834
  });
42740
42835
  };
42741
42836
 
42837
+ /**
42838
+ * Runs `fn` and commits whatever it re-renders before returning, instead of
42839
+ * letting Preact batch it into the next microtask. Layout effects of what gets
42840
+ * mounted run inside the call too, exactly as they would on any other commit.
42841
+ *
42842
+ * For the caller that has to read the DOM it just asked for — measuring an
42843
+ * element whose content it mounts in the same breath — and cannot wait a tick
42844
+ * to do it, because what comes after is a browser event still in flight
42845
+ * (preventDefault, focus placement) that no longer accepts being answered late.
42846
+ *
42847
+ * `options.debounceRendering` is Preact's own hook for deciding *when* the
42848
+ * render queue drains; swapping it for "right now" for the duration of the call
42849
+ * is exactly how preact/compat implements React's flushSync. Reserve it for the
42850
+ * case above: rendering synchronously in the middle of an event gives up the
42851
+ * batching that makes several state changes one commit.
42852
+ */
42853
+ const flushSyncRendering = (fn) => {
42854
+ const debounceRenderingPrevious = options.debounceRendering;
42855
+ options.debounceRendering = (drainRenderQueue) => {
42856
+ drainRenderQueue();
42857
+ };
42858
+ try {
42859
+ fn();
42860
+ } finally {
42861
+ options.debounceRendering = debounceRenderingPrevious;
42862
+ }
42863
+ };
42864
+
42742
42865
  /**
42743
42866
  * Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
42744
42867
  * (non-top-layer) renderers — operate on a plain DOM element, no knowledge
@@ -42952,6 +43075,96 @@ const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
42952
43075
  return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
42953
43076
  };
42954
43077
 
43078
+ /**
43079
+ * When a popup builds what it holds, and when it throws it away.
43080
+ *
43081
+ * A closed popup shows nothing, focuses nothing, and answers nothing: what it
43082
+ * holds is out of reach until it opens. Building that content at mount time
43083
+ * means a page carrying a handful of closed popups pays, on the very render
43084
+ * that decides how fast it appears, for content nobody has asked for — and
43085
+ * pays again on every subsequent measurement, since each of those nodes makes
43086
+ * the document the rest of the page queries bigger.
43087
+ *
43088
+ * So the content is built when the popup first opens, and stays built from
43089
+ * then on: closing is not throwing away, and a reopened popup finds its scroll
43090
+ * position, its half-typed form and its list state where it left them.
43091
+ *
43092
+ * It is built synchronously, from inside `openController.open()` and before
43093
+ * `openEffect` runs (see open_controller.js), so the popup still measures real
43094
+ * content when it positions and animates itself, and so anything inside it
43095
+ * still observes the opening the way it always did — mounted while the popup
43096
+ * reads as closed, told it opened right after (see
43097
+ * use_displayed_layout_effect.js).
43098
+ *
43099
+ * The `mount` prop moves that line. "closed" is two states, not one — never
43100
+ * opened yet, and closed again after an opening — and the three values answer
43101
+ * both at once:
43102
+ *
43103
+ * | mount | before the first open | after a close |
43104
+ * | ----------------- | --------------------- | ------------- |
43105
+ * | "always" | mounted | mounted |
43106
+ * | "from-first-open" | not mounted | mounted |
43107
+ * | "while-opened" | not mounted | not mounted |
43108
+ *
43109
+ * "always" is for content something else depends on before any opening: a
43110
+ * value the popup's owner reads off its own children, fields a form around it
43111
+ * collects on submit, a size measured from outside.
43112
+ *
43113
+ * "while-opened" is the opposite end: content that must be rebuilt from
43114
+ * scratch every time, because what it shows is read once at build time and can
43115
+ * change while the popup is closed — an uncontrolled field seeded from a
43116
+ * `defaultValue`, a form whose fresh state is its initial state.
43117
+ */
43118
+
43119
+
43120
+ const MOUNT_DEFAULT = "from-first-open";
43121
+
43122
+ const usePopupContentMount = (
43123
+ openController,
43124
+ ref,
43125
+ { children, mount = MOUNT_DEFAULT },
43126
+ ) => {
43127
+ const mountedAlways = mount === "always";
43128
+ const [contentMounted, setContentMounted] = useState(
43129
+ () => mountedAlways || openController.opened,
43130
+ );
43131
+ openController.mountContent = contentMounted
43132
+ ? null
43133
+ : () => {
43134
+ flushSyncRendering(() => {
43135
+ setContentMounted(true);
43136
+ });
43137
+ };
43138
+ openController.unmountContent =
43139
+ mount === "while-opened"
43140
+ ? () => {
43141
+ const element = ref?.current;
43142
+ if (!element) {
43143
+ setContentMounted(false);
43144
+ return;
43145
+ }
43146
+ // The popup is still on screen while it plays its exit transition;
43147
+ // emptying it right away would show that transition running on a
43148
+ // blank surface.
43149
+ whenTransitionSettles(element, () => {
43150
+ if (openController.opened) {
43151
+ // reopened while it was leaving — the content it holds is the
43152
+ // one that open just asked for
43153
+ return;
43154
+ }
43155
+ setContentMounted(false);
43156
+ });
43157
+ }
43158
+ : null;
43159
+ useLayoutEffect(() => {
43160
+ if (mountedAlways) {
43161
+ setContentMounted(true);
43162
+ }
43163
+ }, [mountedAlways]);
43164
+
43165
+ return contentMounted ? children : null;
43166
+ };
43167
+
42955
43168
  installImportMetaCssBuild(import.meta);/**
42956
43169
  * Expandable: an in-flow disclosure — a UI part that reveals a content part.
42957
43170
  * It covers the same ground as <Details> with structural differences:
@@ -42992,10 +43205,9 @@ installImportMetaCssBuild(import.meta);/**
42992
43205
  * events.
42993
43206
  *
42994
43207
  * Content is not built until the first expansion and stays built afterwards —
42995
- * same policy, same prop names as popups (see popup_content_mount.js):
42996
- * `mountWhenClosed` builds it right away, `unmountWhenClosed` throws it away
42997
- * once the collapse settles (so a closing animation still plays on real
42998
- * content).
43208
+ * same policy, same `mount` prop as popups (see popup_content_mount.js):
43209
+ * `"always"` builds it right away, `"while-opened"` throws it away once the
43210
+ * collapse settles (so a closing animation still plays on real content).
42999
43211
  *
43000
43212
  * The animation is a REVEAL, not a resize: the expandable's own footprint
43001
43213
  * grows/shrinks progressively (the content's grid track interpolates
@@ -43157,8 +43369,7 @@ const useExpandableContext = partName => {
43157
43369
  * layout?: "row" | "column",
43158
43370
  * autoFocus?: boolean,
43159
43371
  * maxContentHeight?: string | number,
43160
- * mountWhenClosed?: boolean,
43161
- * unmountWhenClosed?: boolean,
43372
+ * mount?: "always" | "from-first-open" | "while-opened",
43162
43373
  * arrowKeyShortcuts?: boolean,
43163
43374
  * openKeyShortcut?: string,
43164
43375
  * closeKeyShortcut?: string,
@@ -43195,13 +43406,15 @@ const useExpandableContext = partName => {
43195
43406
  * the UI part (it would otherwise be lost to the closed, inert content).
43196
43407
  * @param maxContentHeight - Caps the content height; taller content scrolls
43197
43408
  * inside the expandable instead of growing it.
43198
- * @param mountWhenClosed - Builds the content right away instead of on first
43199
- * expansion. In layout="column" it also gives the closed expandable its
43200
- * content's height (the content is kept laid out at its open width), so
43201
- * opening only reveals the width instead of changing the height too.
43202
- * @param unmountWhenClosed - Throws the content away once the collapse
43203
- * settles — after the closing animation, so it still plays on real content —
43204
- * and rebuilds it from scratch on every expansion.
43409
+ * @param mount - When the content is built and thrown away, same three values
43410
+ * as a popup's (see popup_content_mount.js). `"from-first-open"` (the
43411
+ * default) builds it on the first expansion and keeps it afterwards.
43412
+ * `"always"` builds it right away; in layout="column" it also gives the
43413
+ * closed expandable its content's height (the content is kept laid out at
43414
+ * its open width), so opening only reveals the width instead of changing the
43415
+ * height too. `"while-opened"` throws the content away once the collapse
43416
+ * settles — after the closing animation, so it still plays on real
43417
+ * content — and rebuilds it from scratch on every expansion.
43205
43418
  */
43206
43419
  const Expandable = props => {
43207
43420
  import.meta.css = [css$Q, "@jsenv/navi/src/control/expandable/expandable.jsx"];
@@ -43217,8 +43430,7 @@ const Expandable = props => {
43217
43430
  layout,
43218
43431
  autoFocus,
43219
43432
  maxContentHeight,
43220
- mountWhenClosed,
43221
- unmountWhenClosed,
43433
+ mount = MOUNT_DEFAULT,
43222
43434
  arrowKeyShortcuts = true,
43223
43435
  openKeyShortcut = "ArrowRight",
43224
43436
  closeKeyShortcut = "ArrowLeft",
@@ -43231,7 +43443,9 @@ const Expandable = props => {
43231
43443
  const contentContainerRef = useRef();
43232
43444
  const contentId = useId();
43233
43445
  const isColumn = layout === "column";
43234
- const closedContentSized = Boolean(isColumn && mountWhenClosed);
43446
+ const mountedAlways = mount === "always";
43447
+ const mountedWhileOpened = mount === "while-opened";
43448
+ const closedContentSized = isColumn && mountedAlways;
43235
43449
  // Reading .value during render is what subscribes the expandable to it.
43236
43450
  const openRequested = signal ? signal.value : open;
43237
43451
  const [opened, setOpened] = useState(() => Boolean(openRequested === undefined ? defaultOpen : openRequested));
@@ -43242,13 +43456,10 @@ const Expandable = props => {
43242
43456
  const {
43243
43457
  loading: actionLoading
43244
43458
  } = useActionStatus(effectiveAction);
43245
- const [contentMounted, setContentMounted] = useState(() => Boolean(mountWhenClosed) || opened);
43246
- // Same exclusion as popup_content_mount.js: content that must exist while
43247
- // closed cannot also be thrown away on close.
43248
- const effectiveUnmountWhenClosed = unmountWhenClosed && !mountWhenClosed;
43459
+ const [contentMounted, setContentMounted] = useState(() => mountedAlways || opened);
43249
43460
 
43250
43461
  // Fully open and no longer moving — what allows overflow to become visible
43251
- // (see the CSS) and what unmountWhenClosed waits for before emptying.
43462
+ // (see the CSS) and what mount="while-opened" waits for before emptying.
43252
43463
  const [settled, setSettled] = useState(true);
43253
43464
 
43254
43465
  // Read before the close touches the DOM: flipping the content to inert can
@@ -43315,7 +43526,7 @@ const Expandable = props => {
43315
43526
  focusedAtPointerDownRef.current = null;
43316
43527
  setOpened(nextOpen);
43317
43528
  // Flipped here, before the closing/opening commit, so effects of that very
43318
- // commit already see the movement as started — unmountWhenClosed must not
43529
+ // commit already see the movement as started — mount="while-opened" must not
43319
43530
  // read a stale "settled" and empty the content under a closing animation.
43320
43531
  setSettled(!animation);
43321
43532
  if (signal) {
@@ -43437,17 +43648,17 @@ const Expandable = props => {
43437
43648
  return cancel;
43438
43649
  }, [opened]);
43439
43650
  useLayoutEffect(() => {
43440
- if (settled && !opened && effectiveUnmountWhenClosed) {
43651
+ if (settled && !opened && mountedWhileOpened) {
43441
43652
  setContentMounted(false);
43442
43653
  }
43443
- }, [settled, opened, effectiveUnmountWhenClosed]);
43654
+ }, [settled, opened, mountedWhileOpened]);
43444
43655
  useLayoutEffect(() => {
43445
- if (mountWhenClosed) {
43656
+ if (mountedAlways) {
43446
43657
  setContentMounted(true);
43447
43658
  }
43448
- }, [mountWhenClosed]);
43659
+ }, [mountedAlways]);
43449
43660
 
43450
- // closedContentSized (column + mountWhenClosed): the closed content sizes
43661
+ // closedContentSized (column + mount="always"): the closed content sizes
43451
43662
  // the height (see the CSS), which is only right if it lies at its OPEN
43452
43663
  // width — at its natural closed width (a 0-wide track) it would wrap
43453
43664
  // against nothing and stack word by word. So while closed, its width is
@@ -44477,8 +44688,8 @@ const ButtonFirstResolver = props => {
44477
44688
  const Next = useNextResolver();
44478
44689
  const defaultRef = useRef(null);
44479
44690
  props.ref = props.ref || defaultRef;
44480
- const ownTargetHidden = useOwnTargetHidden(props);
44481
- if (ownTargetHidden) {
44691
+ const selfInteractionsHidden = useSelfInteractionsHidden(props);
44692
+ if (selfInteractionsHidden) {
44482
44693
  return null;
44483
44694
  }
44484
44695
  return jsx(Next, {
@@ -44578,7 +44789,8 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
44578
44789
 
44579
44790
  /**
44580
44791
  * @type {import("ignore:preact").FunctionComponent<{
44581
- * ownTarget?: boolean | "refuse" | "always",
44792
+ * selfInteractions?: string,
44793
+ * whenSelfInteractionsBlocked?: "hide" | "refuse" | "ignore",
44582
44794
  * replace?: boolean,
44583
44795
  * [key: string]: any,
44584
44796
  * }>}
@@ -44589,15 +44801,20 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
44589
44801
  * @param {Function} [action] On a button with an `href` or a `route`, the
44590
44802
  * same order as a Link's: it runs on the press, before the navigation, and
44591
44803
  * the navigation does not wait for it (see Link's `action`).
44592
- * @param {boolean|"refuse"|"always"} [ownTarget] A real target inside a zone
44593
- * that belongs to another control — a chip's cross on a picker's façade, an
44594
- * eye on a pressable row, a diskette inside a slide that travels. The press is
44595
- * this button's alone (no travel starts, no popup opens, no navi control above
44596
- * answers) and its `onClick` waits for its own interaction gate instead of
44597
- * firing from the DOM. What it does where the zone is read-only, disabled or
44598
- * busy depends on whether it WRITES to the control it sits in: it goes by
44599
- * default, `"refuse"` keeps it and refuses with a callout, `"always"` ignores
44600
- * the zone's state entirely — for a gesture that never touched that control.
44804
+ * @param {string} [selfInteractions] The interactions this button takes for
44805
+ * itself inside a zone that belongs to another control — a chip's cross on a
44806
+ * picker's façade, an eye on a pressable row, a badge against the edge of a
44807
+ * card one carries. A required list, because a press is not a drag:
44808
+ * `"click"` makes the press this button's alone (no popup opens, no navi
44809
+ * control above answers) and leaves the grab to whatever it sits in,
44810
+ * `"click drag"` takes both, `"*"` takes every gesture there is. Whatever is
44811
+ * claimed, the button's `onClick` waits for its own interaction gate instead
44812
+ * of firing from the DOM.
44813
+ * @param {"hide"|"refuse"|"ignore"} [whenSelfInteractionsBlocked] What becomes
44814
+ * of it where the zone around it is disabled or read-only, which is settled
44815
+ * by whether it WRITES to the control it sits in: it goes (`"hide"`, the
44816
+ * default), `"refuse"` keeps it and refuses with a callout, `"ignore"` lets
44817
+ * it through untouched — for an affordance that never wrote to that control.
44601
44818
  */
44602
44819
  const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
44603
44820
 
@@ -44612,7 +44829,7 @@ installImportMetaCssBuild(import.meta);/**
44612
44829
  * caps are fixed: same place, same size, in both states, and either of them
44613
44830
  * hands the floor to the other side. That is the whole reason the caps sit
44614
44831
  * OUTSIDE the controls rather than being drawn inside them (a picker's façade
44615
- * yields a zone with `ownTarget`, a field has `Input.UI.LeftSlot`): an icon
44832
+ * yields a zone with `selfInteractions`, a field has `Input.UI.LeftSlot`): an icon
44616
44833
  * that lives inside its control while open and becomes a pill once closed is
44617
44834
  * a switch that moves when you flip it: the finger that opened the search has
44618
44835
  * to travel to close it again. Out here, the same pixel does both.
@@ -44871,9 +45088,12 @@ const ControlSwap = props => {
44871
45088
  * @param label - What the cap is called — it holds no text, so this is its
44872
45089
  * accessible name. Say what pressing it reveals ("Rechercher"), not what it
44873
45090
  * currently shows: the cap wears `aria-expanded` for the state.
44874
- * @param badge - A mark on the cap saying this collapsed control is still
44875
- * doing something (a filter set, a search typed). `true` draws a dot;
44876
- * anything else is drawn as given (a `<BadgeCount>`, say).
45091
+ * @param badge - A mark on the cap saying the control behind it is still doing
45092
+ * something (a filter set, a search typed). `true` draws a dot; anything else
45093
+ * is drawn as given (a `<BadgeCount>`, say). Drawn only while this side is
45094
+ * the collapsed one — the side holding the floor spells out in full what a
45095
+ * dot could only hint at — so it is read straight off the state
45096
+ * (`badge={Boolean(groupId)}`), with no "and this side is hidden" to add.
44877
45097
  * @param autoFocus - On by default: the focus goes into this control when it
44878
45098
  * takes the floor, where navi's ladder puts it — an `autoFocus` inside the
44879
45099
  * control first, its first focusable otherwise. `false` leaves it on the cap
@@ -44901,6 +45121,9 @@ const ControlSwapCap = ({
44901
45121
  badge,
44902
45122
  ...capProps
44903
45123
  } = side.capProps;
45124
+ // A badge on a cap can only say one thing: the control you are NOT looking at
45125
+ // holds something. The one holding the floor is on the row saying it itself.
45126
+ const badgeToDraw = active ? null : badge;
44904
45127
  return jsxs(Button, {
44905
45128
  icon: true,
44906
45129
  pressEffect: "none",
@@ -44911,7 +45134,7 @@ const ControlSwapCap = ({
44911
45134
  // CSS reaches for, and the rest is the wiring that makes the cap a cap.
44912
45135
  ,
44913
45136
 
44914
- className: capProps.className ? `navi_control_swap_cap ${capProps.className}` : "navi_control_swap_cap",
45137
+ className: withPropsClassName("navi_control_swap_cap", capProps.className),
44915
45138
  "aria-expanded": active,
44916
45139
  "aria-controls": slotId,
44917
45140
  onClick: onPress,
@@ -44919,10 +45142,10 @@ const ControlSwapCap = ({
44919
45142
  width: "50%",
44920
45143
  square: true,
44921
45144
  children: icon
44922
- }), badge ? jsx("span", {
45145
+ }), badgeToDraw ? jsx("span", {
44923
45146
  className: "navi_control_swap_badge",
44924
45147
  "aria-hidden": "true",
44925
- children: badge === true ? null : badge
45148
+ children: badgeToDraw === true ? null : badgeToDraw
44926
45149
  }) : null]
44927
45150
  });
44928
45151
  };
@@ -53795,7 +54018,7 @@ const createOpenController = (
53795
54018
  // previous subject first.
53796
54019
  onOpen: null,
53797
54020
  // The counterpart, set only when the popup was told to throw its content
53798
- // away on close (`unmountWhenClosed`). Called from performClose above.
54021
+ // away on close (`mount="while-opened"`). Called from performClose above.
53799
54022
  unmountContent: null,
53800
54023
  // Told whenever `opened` actually changes, whatever asked for it — an
53801
54024
  // interaction, a command, a prop — with the event that asked. What lets a
@@ -54267,111 +54490,6 @@ const getAvailableWidth = (layer, element) => {
54267
54490
  return visualViewportWidthSignal.peek();
54268
54491
  };
54269
54492
 
54270
- /**
54271
- * Runs `fn` and commits whatever it re-renders before returning, instead of
54272
- * letting Preact batch it into the next microtask. Layout effects of what gets
54273
- * mounted run inside the call too, exactly as they would on any other commit.
54274
- *
54275
- * For the caller that has to read the DOM it just asked for — measuring an
54276
- * element whose content it mounts in the same breath — and cannot wait a tick
54277
- * to do it, because what comes after is a browser event still in flight
54278
- * (preventDefault, focus placement) that no longer accepts being answered late.
54279
- *
54280
- * `options.debounceRendering` is Preact's own hook for deciding *when* the
54281
- * render queue drains; swapping it for "right now" for the duration of the call
54282
- * is exactly how preact/compat implements React's flushSync. Reserve it for the
54283
- * case above: rendering synchronously in the middle of an event gives up the
54284
- * batching that makes several state changes one commit.
54285
- */
54286
- const flushSyncRendering = (fn) => {
54287
- const debounceRenderingPrevious = options.debounceRendering;
54288
- options.debounceRendering = (drainRenderQueue) => {
54289
- drainRenderQueue();
54290
- };
54291
- try {
54292
- fn();
54293
- } finally {
54294
- options.debounceRendering = debounceRenderingPrevious;
54295
- }
54296
- };
54297
-
54298
- /**
54299
- * When a popup builds what it holds, and when it throws it away.
54300
- *
54301
- * A closed popup shows nothing, focuses nothing, and answers nothing: what it
54302
- * holds is out of reach until it opens. Building that content at mount time
54303
- * means a page carrying a handful of closed popups pays, on the very render
54304
- * that decides how fast it appears, for content nobody has asked for — and
54305
- * pays again on every subsequent measurement, since each of those nodes makes
54306
- * the document the rest of the page queries bigger.
54307
- *
54308
- * So the content is built when the popup first opens, and stays built from
54309
- * then on: closing is not throwing away, and a reopened popup finds its scroll
54310
- * position, its half-typed form and its list state where it left them.
54311
- *
54312
- * It is built synchronously, from inside `openController.open()` and before
54313
- * `openEffect` runs (see open_controller.js), so the popup still measures real
54314
- * content when it positions and animates itself, and so anything inside it
54315
- * still observes the opening the way it always did — mounted while the popup
54316
- * reads as closed, told it opened right after (see
54317
- * use_displayed_layout_effect.js).
54318
- *
54319
- * `mountWhenClosed` is for content something else depends on before any of
54320
- * this: a value the popup's owner reads off its own children, fields a form
54321
- * around it collects on submit, a size measured from outside.
54322
- *
54323
- * `unmountWhenClosed` is the opposite end: content that must be rebuilt from
54324
- * scratch every time, because what it shows is read once at build time and can
54325
- * change while the popup is closed — an uncontrolled field seeded from a
54326
- * `defaultValue`, a form whose fresh state is its initial state.
54327
- */
54328
-
54329
-
54330
- const usePopupContentMount = (
54331
- openController,
54332
- ref,
54333
- { children, mountWhenClosed, unmountWhenClosed },
54334
- ) => {
54335
- const [contentMounted, setContentMounted] = useState(
54336
- () => Boolean(mountWhenClosed) || openController.opened,
54337
- );
54338
- openController.mountContent = contentMounted
54339
- ? null
54340
- : () => {
54341
- flushSyncRendering(() => {
54342
- setContentMounted(true);
54343
- });
54344
- };
54345
- openController.unmountContent =
54346
- unmountWhenClosed && !mountWhenClosed
54347
- ? () => {
54348
- const element = ref?.current;
54349
- if (!element) {
54350
- setContentMounted(false);
54351
- return;
54352
- }
54353
- // The popup is still on screen while it plays its exit transition;
54354
- // emptying it right away would show that transition running on a
54355
- // blank surface.
54356
- whenTransitionSettles(element, () => {
54357
- if (openController.opened) {
54358
- // reopened while it was leaving — the content it holds is the
54359
- // one that open just asked for
54360
- return;
54361
- }
54362
- setContentMounted(false);
54363
- });
54364
- }
54365
- : null;
54366
- useLayoutEffect(() => {
54367
- if (mountWhenClosed) {
54368
- setContentMounted(true);
54369
- }
54370
- }, [mountWhenClosed]);
54371
-
54372
- return contentMounted ? children : null;
54373
- };
54374
-
54375
54493
  /**
54376
54494
  * Entry/exit animation CSS shared by Popover and Dialog.
54377
54495
  *
@@ -55562,16 +55680,15 @@ const css$E = /* css */`
55562
55680
  * open controller (see `open_controller.js`) for a caller that wants to
55563
55681
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
55564
55682
  * by `picker_custom.jsx`).
55565
- * @param {boolean} [props.mountWhenClosed] - Builds `children` right away
55566
- * instead of waiting for the first open (see popup_content_mount.js). For
55567
- * content something depends on while the popup is still closed: a value read
55568
- * off it, fields a surrounding form collects on submit, a size measured from
55569
- * outside.
55570
- * @param {boolean} [props.unmountWhenClosed] - Throws `children` away once the
55571
- * popup has finished closing (see popup_content_mount.js). For content whose
55572
- * fresh state is its initial state: an uncontrolled field seeded from a
55573
- * `defaultValue` that changed while the popup was closed. Ignored when
55574
- * `mountWhenClosed` is set.
55683
+ * @param {"always"|"from-first-open"|"while-opened"} [props.mount] - When
55684
+ * `children` are built and thrown away (see popup_content_mount.js).
55685
+ * `"from-first-open"` (the default) builds them on the first open and keeps
55686
+ * them afterwards. `"always"` builds them right away, for content something
55687
+ * depends on while the popup is still closed: a value read off it, fields a
55688
+ * surrounding form collects on submit, a size measured from outside.
55689
+ * `"while-opened"` throws them away once the popup has finished closing, for
55690
+ * content whose fresh state is its initial state: an uncontrolled field
55691
+ * seeded from a `defaultValue` that changed while the popup was closed.
55575
55692
  * @param {import("ignore:preact").ComponentChildren} props.children
55576
55693
  */
55577
55694
  const Dialog = props => {
@@ -55808,8 +55925,7 @@ const useDialogProps = props => {
55808
55925
  // openController.onOpen below).
55809
55926
  onOpen,
55810
55927
  children: childrenProp,
55811
- mountWhenClosed,
55812
- unmountWhenClosed,
55928
+ mount,
55813
55929
  ...rest
55814
55930
  } = props;
55815
55931
  // Assigned on every render, like openEffect below, so it always closes over
@@ -55818,8 +55934,7 @@ const useDialogProps = props => {
55818
55934
  openController.onOpen = onOpen || null;
55819
55935
  const children = usePopupContentMount(openController, props.ref, {
55820
55936
  children: childrenProp,
55821
- mountWhenClosed,
55822
- unmountWhenClosed
55937
+ mount
55823
55938
  });
55824
55939
  const isModal = layer === "top";
55825
55940
  const ref = props.ref;
@@ -57032,16 +57147,15 @@ const css$D = /* css */`
57032
57147
  * open controller (see `open_controller.js`) for a caller that wants to
57033
57148
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
57034
57149
  * by `picker_custom.jsx`/`side_panel.jsx`).
57035
- * @param {boolean} [props.mountWhenClosed] - Builds `children` right away
57036
- * instead of waiting for the first open (see popup_content_mount.js). For
57037
- * content something depends on while the popup is still closed: a value read
57038
- * off it, fields a surrounding form collects on submit, a size measured from
57039
- * outside.
57040
- * @param {boolean} [props.unmountWhenClosed] - Throws `children` away once the
57041
- * popup has finished closing (see popup_content_mount.js). For content whose
57042
- * fresh state is its initial state: an uncontrolled field seeded from a
57043
- * `defaultValue` that changed while the popup was closed. Ignored when
57044
- * `mountWhenClosed` is set.
57150
+ * @param {"always"|"from-first-open"|"while-opened"} [props.mount] - When
57151
+ * `children` are built and thrown away (see popup_content_mount.js).
57152
+ * `"from-first-open"` (the default) builds them on the first open and keeps
57153
+ * them afterwards. `"always"` builds them right away, for content something
57154
+ * depends on while the popup is still closed: a value read off it, fields a
57155
+ * surrounding form collects on submit, a size measured from outside.
57156
+ * `"while-opened"` throws them away once the popup has finished closing, for
57157
+ * content whose fresh state is its initial state: an uncontrolled field
57158
+ * seeded from a `defaultValue` that changed while the popup was closed.
57045
57159
  * @param {import("ignore:preact").ComponentChildren} props.children
57046
57160
  */
57047
57161
  const Popover = props => {
@@ -57256,8 +57370,7 @@ const usePopoverProps = props => {
57256
57370
  // openController.onOpen below).
57257
57371
  onOpen,
57258
57372
  children: childrenProp,
57259
- mountWhenClosed,
57260
- unmountWhenClosed,
57373
+ mount,
57261
57374
  ...rest
57262
57375
  } = props;
57263
57376
  // Assigned on every render, like openEffect below, so it always closes over
@@ -57266,8 +57379,7 @@ const usePopoverProps = props => {
57266
57379
  openController.onOpen = onOpen || null;
57267
57380
  const children = usePopupContentMount(openController, props.ref, {
57268
57381
  children: childrenProp,
57269
- mountWhenClosed,
57270
- unmountWhenClosed
57382
+ mount
57271
57383
  });
57272
57384
  const isTopLayer = layer === "top";
57273
57385
  const ref = props.ref;
@@ -58206,16 +58318,15 @@ const css$C = /* css */`@layer navi {
58206
58318
  * @param {string} [props.className] - Merged with the shared
58207
58319
  * `"navi_popup"` class (see this file's own CSS) rather than replacing
58208
58320
  * it.
58209
- * @param {boolean} [props.mountWhenClosed] - Builds `children` right away
58210
- * instead of waiting for the first open (see popup_content_mount.js). For
58211
- * content something depends on while the popup is still closed: a value read
58212
- * off it, fields a surrounding form collects on submit, a size measured from
58213
- * outside.
58214
- * @param {boolean} [props.unmountWhenClosed] - Throws `children` away once the
58215
- * popup has finished closing (see popup_content_mount.js). For content whose
58216
- * fresh state is its initial state: an uncontrolled field seeded from a
58217
- * `defaultValue` that changed while the popup was closed. Ignored when
58218
- * `mountWhenClosed` is set.
58321
+ * @param {"always"|"from-first-open"|"while-opened"} [props.mount] - When
58322
+ * `children` are built and thrown away (see popup_content_mount.js).
58323
+ * `"from-first-open"` (the default) builds them on the first open and keeps
58324
+ * them afterwards. `"always"` builds them right away, for content something
58325
+ * depends on while the popup is still closed: a value read off it, fields a
58326
+ * surrounding form collects on submit, a size measured from outside.
58327
+ * `"while-opened"` throws them away once the popup has finished closing, for
58328
+ * content whose fresh state is its initial state: an uncontrolled field
58329
+ * seeded from a `defaultValue` that changed while the popup was closed.
58219
58330
  * @param {import("ignore:preact").ComponentChildren} props.children
58220
58331
  */
58221
58332
  const Popup = props => {
@@ -58745,8 +58856,7 @@ const PickerCustom = props => {
58745
58856
  // and pushes it down instead, leaving the popup free to build its
58746
58857
  // content only when it is first opened (see popup_content_mount.js).
58747
58858
  // A caller who knows better says so with the popup's own props.
58748
- mountWhenClosed: props.mountWhenClosed ?? !isControlValueGivenByProps(props),
58749
- unmountWhenClosed: props.unmountWhenClosed,
58859
+ mount: props.mount ?? (isControlValueGivenByProps(props) ? MOUNT_DEFAULT : "always"),
58750
58860
  // Not on pickerProps (the trigger): commands.js's own
58751
58861
  // resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
58752
58862
  // find where to dispatch navi_request_open/navi_request_close — and
@@ -59032,8 +59142,7 @@ const PickerContentInsidePopup = props => {
59032
59142
  ...rest,
59033
59143
  // On popupProps already (see the picker's popup assembly); they mean
59034
59144
  // nothing to the picker element.
59035
- mountWhenClosed: undefined,
59036
- unmountWhenClosed: undefined,
59145
+ mount: undefined,
59037
59146
  onFocusOut: e => {
59038
59147
  if (!isPopover || !closeOnFocusOut) {
59039
59148
  return;
@@ -59115,7 +59224,7 @@ const PICKER_CALLOUT_CONTENT_TOKEN = createOpenToken();
59115
59224
  * The content is rendered through a portal into an element this component
59116
59225
  * owns, handed to the callout as its message (a Node, appended as-is). It is
59117
59226
  * rendered whether the callout is open or not, so what the content holds
59118
- * survives a close, the way a popup's `mountWhenClosed` keeps it. The element
59227
+ * survives a close, the way a popup's `mount="always"` keeps it. The element
59119
59228
  * carries data-picker-content: the callout is appended inside the picker root,
59120
59229
  * and a press in there must read as inside the popup, not on the trigger.
59121
59230
  */
@@ -59294,10 +59403,10 @@ const PickerConfirmResolver = props => {
59294
59403
  onConfirm: onConfirm
59295
59404
  // A question holds no value to read before it is asked: the popup is
59296
59405
  // built on the first open, like that of a picker told its value (see
59297
- // mountWhenClosed in picker_custom.jsx).
59406
+ // the `mount` resolution in picker_custom.jsx).
59298
59407
  ,
59299
59408
 
59300
- mountWhenClosed: false,
59409
+ mount: MOUNT_DEFAULT,
59301
59410
  message: undefined,
59302
59411
  confirmLabel: undefined,
59303
59412
  cancelLabel: undefined,
@@ -65556,8 +65665,8 @@ const BadgeStyleCSSVars = {
65556
65665
  fontSize: "--font-size"
65557
65666
  };
65558
65667
  const BadgeButton = props => {
65559
- const ownTargetHidden = useOwnTargetHidden(props);
65560
- if (ownTargetHidden) {
65668
+ const selfInteractionsHidden = useSelfInteractionsHidden(props);
65669
+ if (selfInteractionsHidden) {
65561
65670
  return null;
65562
65671
  }
65563
65672
  return jsx(BadgeButtonUI, {
@@ -66317,7 +66426,7 @@ const PickerArrayUI = asPickerOwnUI(() => {
66317
66426
  * around the chip. `commandFor` is for a chip that stands outside the picker it
66318
66427
  * speaks for.
66319
66428
  *
66320
- * The cross is an own target (see own_target.js), so the press belongs to it
66429
+ * The cross claims the press (see self_interactions.js), so it belongs to it
66321
66430
  * and not to the picker underneath, and it goes when the picker turns read-only
66322
66431
  * — a row being read still says what was picked, it just no longer offers to
66323
66432
  * unpick it.
@@ -66344,7 +66453,7 @@ const PickerChip = ({
66344
66453
  flex: true,
66345
66454
  ...rest,
66346
66455
  children: [children, jsx(Badge.Button, {
66347
- ownTarget: true,
66456
+ selfInteractions: "click",
66348
66457
  command: "--navi-unselect",
66349
66458
  commandFor: commandFor,
66350
66459
  value: value,
@@ -66721,7 +66830,7 @@ installImportMetaCssBuild(import.meta);const css$t = /* css */`@layer navi {
66721
66830
  overflow: visible !important;
66722
66831
  }
66723
66832
 
66724
- & [data-own-target] {
66833
+ & [data-self-interactions~="click"], & [data-self-interactions~="*"] {
66725
66834
  pointer-events: auto;
66726
66835
  position: relative;
66727
66836
  }
@@ -66992,6 +67101,9 @@ const PickerButton = props => {
66992
67101
  // Adds a clear button to the right slot, the same one type="search" puts at
66993
67102
  // the end of an input: a picker holds a value the user chose, and unsetting
66994
67103
  // it should not require reopening the popup to hunt for a "none" entry.
67104
+ // Where navi puts it, which needs a slot to put it in: a variant without
67105
+ // one (bare above all) holds a <Picker.Clear /> in its own drawing instead
67106
+ // — the same cross, placed by whoever draws around it.
66995
67107
  clearable,
66996
67108
  // "Are you sure?" before the cross clears anything. A cross of three
66997
67109
  // millimetres at the edge of a touch screen, right where the chevron is
@@ -67012,6 +67124,17 @@ const PickerButton = props => {
67012
67124
  const isSingleLine = maxLines === 1;
67013
67125
  // Same rule as the root: phrasing content inside a sentence.
67014
67126
  const ContentTag = variant === "text" ? "span" : "div";
67127
+ // Which variants get the right slot — the chevron saying "this opens", and
67128
+ // the cross replacing it once there is something to clear.
67129
+ // Not the ones that draw no value beside it: an icon picker IS its icon, a
67130
+ // headless one draws nothing, a button says what it opens with its label, a
67131
+ // word in a sentence has no room for furniture. Nor a picker rendering the
67132
+ // browser's own control ("default").
67133
+ // Nor a bare one: the picker is that drawing's box to the pixel, so anything
67134
+ // navi adds beside it either grows the box or covers what the caller drew.
67135
+ // The pieces are the caller's to place there instead — a <Picker.Clear /> in
67136
+ // their own layout (see warnOnClearableWithoutSlot).
67137
+ const hasRightSlot = variant !== "icon" && variant !== "headless" && variant !== "button" && variant !== "text" && variant !== "bare" && ui !== "default";
67015
67138
  const inputRef = useRef(null);
67016
67139
  const [pickerRemainingProps, inputProps, facadeChildrenProps] = useControlFacadeProps({
67017
67140
  ...props,
@@ -67040,6 +67163,17 @@ const PickerButton = props => {
67040
67163
  // clearing being a modification like any other.
67041
67164
  const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
67042
67165
  usePickerErrorCallout(uiStateController, error);
67166
+ // What the picker knows about itself, for the pieces it does not place: the
67167
+ // drawings of its value (Picker.UI.*), and the affordances a caller may put
67168
+ // in their own `ui` (Picker.Clear) as much as the ones it puts in its slot.
67169
+ const pickerContext = {
67170
+ value,
67171
+ placeholder,
67172
+ maxLines,
67173
+ id: inputProps.id,
67174
+ interactive,
67175
+ clearConfirm
67176
+ };
67043
67177
  return (
67044
67178
  /* Read-only crosses into everything the picker is made of: what it really
67045
67179
  holds is drawn by controls of their own — in the popup, and on the façade
@@ -67113,112 +67247,108 @@ const PickerButton = props => {
67113
67247
  onnavi_request_unselect: e => {
67114
67248
  requestPickerListEntry(ref.current, inputRef.current, e, "unselect");
67115
67249
  },
67116
- children: [jsxs("span", {
67250
+ children: [jsx("span", {
67117
67251
  className: "navi_picker_box",
67118
- children: [variant === "headless" ? null : jsx(LoadingOutline, {
67119
- loading: loading,
67120
- color: "var(--picker-loader-color)",
67121
- inset: -2
67122
- }), jsx(PickerInput, {
67123
- tabIndex: variant === "headless" ? -1 : undefined,
67124
- "aria-hidden": variant === "headless" ? "true" : undefined,
67125
- ...inputProps,
67126
- // eslint-disable-next-line react/no-children-prop
67127
- children: undefined // we will render children into the div
67128
- ,
67129
-
67130
- ui: ui,
67131
- onCopy: e => {
67132
- const pickerEl = ref.current;
67133
- if (isWithinPickerContent(e.target, pickerEl)) {
67134
- return;
67135
- }
67136
- const uiState = uiStateController.uiState;
67137
- if (uiState === undefined) {
67138
- return;
67139
- }
67140
- e.preventDefault();
67141
- const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
67142
- e.clipboardData.setData("text/plain", displayText);
67143
- e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
67144
- },
67145
- onCut: e => {
67146
- const pickerEl = ref.current;
67147
- if (isWithinPickerContent(e.target, pickerEl)) {
67148
- return;
67149
- }
67150
- const uiState = uiStateController.uiState;
67151
- if (uiState === undefined) {
67152
- return;
67153
- }
67154
- // the copy part don't need control to be interactable
67155
- const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
67156
- e.clipboardData.setData("text/plain", displayText);
67157
- e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
67158
- // the clear ui state part need control to be interactable
67159
- dispatchRequestInteraction(pickerEl, {
67160
- event: e,
67161
- name: "cut",
67162
- allowed: () => {
67163
- dispatchRequestClearUIState(inputRef.current, e);
67252
+ children: jsxs(PickerContext.Provider, {
67253
+ value: pickerContext,
67254
+ children: [variant === "headless" ? null : jsx(LoadingOutline, {
67255
+ loading: loading,
67256
+ color: "var(--picker-loader-color)",
67257
+ inset: -2
67258
+ }), jsx(PickerInput, {
67259
+ tabIndex: variant === "headless" ? -1 : undefined,
67260
+ "aria-hidden": variant === "headless" ? "true" : undefined,
67261
+ ...inputProps,
67262
+ // eslint-disable-next-line react/no-children-prop
67263
+ children: undefined // we will render children into the div
67264
+ ,
67265
+
67266
+ ui: ui,
67267
+ onCopy: e => {
67268
+ const pickerEl = ref.current;
67269
+ if (isWithinPickerContent(e.target, pickerEl)) {
67270
+ return;
67164
67271
  }
67165
- });
67166
- e.preventDefault();
67167
- },
67168
- onPaste: e => {
67169
- const pickerEl = ref.current;
67170
- if (isWithinPickerContent(e.target, pickerEl)) {
67171
- // Don't intercept inside the picker popup content.
67172
- return;
67173
- }
67174
- const naviData = e.clipboardData.getData("application/x-navi");
67175
- let pasteValue;
67176
- if (naviData) {
67177
- try {
67178
- pasteValue = JSON.parse(naviData);
67179
- } catch {
67180
- pasteValue = naviData;
67272
+ const uiState = uiStateController.uiState;
67273
+ if (uiState === undefined) {
67274
+ return;
67181
67275
  }
67182
- } else {
67183
- pasteValue = e.clipboardData.getData("text/plain");
67184
- }
67185
- dispatchRequestInteraction(pickerEl, {
67186
- event: e,
67187
- name: "paste",
67188
- allowed: () => {
67189
- dispatchRequestSetUIState(inputRef.current, pasteValue, {
67190
- event: e
67191
- });
67276
+ e.preventDefault();
67277
+ const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
67278
+ e.clipboardData.setData("text/plain", displayText);
67279
+ e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
67280
+ },
67281
+ onCut: e => {
67282
+ const pickerEl = ref.current;
67283
+ if (isWithinPickerContent(e.target, pickerEl)) {
67284
+ return;
67192
67285
  }
67193
- });
67194
- e.preventDefault();
67195
- }
67196
- }), variant === "headless" || ui === "default" ? null : jsx(Text, {
67197
- className: "navi_picker_value"
67198
- // Tells the caller's own drawing of the control from the value
67199
- // the picker draws itself, so each is written on its own line
67200
- // (see .navi_picker_value in the CSS above).
67201
- ,
67202
-
67203
- "data-picker-facade": ui === undefined ? undefined : ""
67204
- // A placeholder is the picker saying "nothing here yet" about
67205
- // the value navi draws — the default rendering, or the one a
67206
- // typed picker installs for itself. A button's label is not
67207
- // that, however empty the picker behind it is, and neither is a
67208
- // caller's own "ui": an empty value may be exactly what the
67209
- // caller is drawing there ("no filter", "anywhere"), so how it
67210
- // looks empty stays theirs (documented on the ui prop below).
67211
- ,
67212
-
67213
- "navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) && variant !== "button" && variant !== "text" && uiStateHoldsNothing(value) ? "" : undefined,
67214
- maxLines: maxLines,
67215
- children: jsx(PickerOwnContent, {
67216
- children: jsx(PickerContext.Provider, {
67217
- value: {
67218
- value,
67219
- placeholder,
67220
- maxLines
67221
- },
67286
+ const uiState = uiStateController.uiState;
67287
+ if (uiState === undefined) {
67288
+ return;
67289
+ }
67290
+ // the copy part don't need control to be interactable
67291
+ const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
67292
+ e.clipboardData.setData("text/plain", displayText);
67293
+ e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
67294
+ // the clear ui state part need control to be interactable
67295
+ dispatchRequestInteraction(pickerEl, {
67296
+ event: e,
67297
+ name: "cut",
67298
+ allowed: () => {
67299
+ dispatchRequestClearUIState(inputRef.current, e);
67300
+ }
67301
+ });
67302
+ e.preventDefault();
67303
+ },
67304
+ onPaste: e => {
67305
+ const pickerEl = ref.current;
67306
+ if (isWithinPickerContent(e.target, pickerEl)) {
67307
+ // Don't intercept inside the picker popup content.
67308
+ return;
67309
+ }
67310
+ const naviData = e.clipboardData.getData("application/x-navi");
67311
+ let pasteValue;
67312
+ if (naviData) {
67313
+ try {
67314
+ pasteValue = JSON.parse(naviData);
67315
+ } catch {
67316
+ pasteValue = naviData;
67317
+ }
67318
+ } else {
67319
+ pasteValue = e.clipboardData.getData("text/plain");
67320
+ }
67321
+ dispatchRequestInteraction(pickerEl, {
67322
+ event: e,
67323
+ name: "paste",
67324
+ allowed: () => {
67325
+ dispatchRequestSetUIState(inputRef.current, pasteValue, {
67326
+ event: e
67327
+ });
67328
+ }
67329
+ });
67330
+ e.preventDefault();
67331
+ }
67332
+ }), variant === "headless" || ui === "default" ? null : jsx(Text, {
67333
+ className: "navi_picker_value"
67334
+ // Tells the caller's own drawing of the control from the value
67335
+ // the picker draws itself, so each is written on its own line
67336
+ // (see .navi_picker_value in the CSS above).
67337
+ ,
67338
+
67339
+ "data-picker-facade": ui === undefined ? undefined : ""
67340
+ // A placeholder is the picker saying "nothing here yet" about
67341
+ // the value navi draws — the default rendering, or the one a
67342
+ // typed picker installs for itself. A button's label is not
67343
+ // that, however empty the picker behind it is, and neither is a
67344
+ // caller's own "ui": an empty value may be exactly what the
67345
+ // caller is drawing there ("no filter", "anywhere"), so how it
67346
+ // looks empty stays theirs (documented on the ui prop below).
67347
+ ,
67348
+
67349
+ "navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) && variant !== "button" && variant !== "text" && uiStateHoldsNothing(value) ? "" : undefined,
67350
+ maxLines: maxLines,
67351
+ children: jsx(PickerOwnContent, {
67222
67352
  children: jsx(MaxLinesContext.Provider, {
67223
67353
  value: maxLines,
67224
67354
  children: ui === undefined ? variant === "icon" ?
@@ -67233,71 +67363,23 @@ const PickerButton = props => {
67233
67363
  }) : jsx(PickerDefaultUI, {}) : ui
67234
67364
  })
67235
67365
  })
67236
- })
67237
- }), variant === "icon" || variant === "headless" || variant === "button" || variant === "text" || variant === "bare" || ui === "default" ? null : jsx("span", {
67238
- className: "navi_picker_right_slot",
67239
- children: jsx(PickerOwnContent, {
67240
- children: clearable && interactive && value !== undefined && value !== "" && clearConfirm !== undefined ?
67241
- // A picker on the façade of a picker: the cross opens the
67242
- // question, and yes sends the clear to this picker's input.
67243
- // A door only (allowNameless): the form around does not see
67244
- // a field in it.
67245
- jsx(Picker, {
67246
- type: "confirm",
67247
- variant: "icon",
67248
- ui: jsx(Icon, {
67249
- size: rightSlotIconSize,
67250
- lineOverflow: "allow",
67251
- children: jsx(CloseSvg, {})
67252
- }),
67253
- message: clearConfirm,
67254
- command: "--navi-clear",
67255
- commandFor: inputProps.id,
67256
- tabIndex: "-1"
67257
- }) : clearable && interactive && value !== undefined && value !== "" ? jsx(Button, {
67258
- command: "--navi-clear",
67259
- commandFor: inputProps.id,
67260
- tabIndex: "-1"
67261
- // No navi-focus-delegate, unlike the identical button inside an
67262
- // input: handing focus back to the picker's own input is what
67263
- // opens the popup, and clearing is the opposite intention.
67264
- ,
67265
-
67266
- icon: true,
67267
- variant: "discrete"
67268
- // What is busy once the clear is sent is the picker — the value
67269
- // being removed is the whole field's, and the picker already
67270
- // draws the wait around all of it. Two outlines for one wait is
67271
- // one too many.
67272
- ,
67273
-
67274
- loadingOutline: false
67275
- // preventDefault, not just tabIndex="-1": a mousedown focuses
67276
- // its target before any click happens, and this button should
67277
- // never hold focus at all — the field keeps it.
67278
- ,
67279
-
67280
- onMouseDown: e => {
67281
- e.preventDefault();
67282
- },
67283
- flex: true,
67284
- align: "center",
67285
- children: jsx(Icon, {
67366
+ }), hasRightSlot ? jsx("span", {
67367
+ className: "navi_picker_right_slot",
67368
+ children: jsx(PickerOwnContent, {
67369
+ children: clearable && pickerHasSomethingToClear(pickerContext) ? jsx(PickerClear, {
67370
+ size: rightSlotIconSize
67371
+ }) : rightSlot === undefined ?
67372
+ // lineOverflow: what sits in the slot is an affordance, not a
67373
+ // character — a caller asking for a bigger one wants it bigger,
67374
+ // not capped at the height of the line it sits on
67375
+ jsx(Icon, {
67286
67376
  size: rightSlotIconSize,
67287
67377
  lineOverflow: "allow",
67288
- children: jsx(CloseSvg, {})
67289
- })
67290
- }) : rightSlot === undefined ?
67291
- // lineOverflow: what sits in the slot is an affordance, not a
67292
- // character — a caller asking for a bigger one wants it bigger,
67293
- // not capped at the height of the line it sits on
67294
- jsx(Icon, {
67295
- size: rightSlotIconSize,
67296
- lineOverflow: "allow",
67297
- children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
67298
- }) : rightSlot
67299
- })
67300
- })]
67378
+ children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
67379
+ }) : rightSlot
67380
+ })
67381
+ }) : null]
67382
+ })
67301
67383
  }), jsx(ControlFacadeChildrenWrapper, {
67302
67384
  ...facadeChildrenProps,
67303
67385
  children: jsx(ContentTag, {
@@ -67310,6 +67392,123 @@ const PickerButton = props => {
67310
67392
  })
67311
67393
  );
67312
67394
  };
67395
+ /**
67396
+ * The cross that empties the picker it sits in — reached as `<Picker.Clear />`.
67397
+ *
67398
+ * It is the very cross `clearable` puts in the right slot — the same button,
67399
+ * wired the same way — for the drawings navi has no slot to put it in:
67400
+ * `variant="bare"` above all, where the drawing is the caller's and so is the
67401
+ * place the cross belongs in it, after a name or over a corner, at a size and
67402
+ * with an icon of its own.
67403
+ *
67404
+ * It goes when there is nothing to take out and when nothing can be changed, so
67405
+ * a caller places it once and never asks whether to draw it.
67406
+ *
67407
+ * @type {import("ignore:preact").FunctionComponent<{
67408
+ * size?: number | string,
67409
+ * children?: import("ignore:preact").ComponentChildren,
67410
+ * } & Record<string, any>>}
67411
+ * @param {number|string} [size="inherit"] How big the cross is drawn, as
67412
+ * `<Icon size>` reads it — "inherit" takes the size of the text around it.
67413
+ * @param {import("ignore:preact").ComponentChildren} [children] The icon, in place of
67414
+ * the cross. Anything else goes to the `<Button>` underneath (`variant`,
67415
+ * `paddingX`, `aria-label`, …).
67416
+ */
67417
+ const PickerClear = ({
67418
+ size = "inherit",
67419
+ children,
67420
+ ...rest
67421
+ }) => {
67422
+ const pickerContext = useContext(PickerContext);
67423
+ if (!pickerContext) {
67424
+ return null;
67425
+ }
67426
+ if (!pickerHasSomethingToClear(pickerContext)) {
67427
+ return null;
67428
+ }
67429
+ const {
67430
+ id,
67431
+ clearConfirm
67432
+ } = pickerContext;
67433
+ const icon =
67434
+ // lineOverflow: the cross is an affordance, not a character — a caller
67435
+ // asking for a bigger one wants it bigger, not capped at the height of the
67436
+ // line it sits on.
67437
+ jsx(Icon, {
67438
+ size: size,
67439
+ lineOverflow: "allow",
67440
+ children: children === undefined ? jsx(CloseSvg, {}) : children
67441
+ });
67442
+ // The press is aimed AT the cross: clearing is the opposite intention to
67443
+ // opening, and the picker's box answers a press landing anywhere in it (see
67444
+ // self_interactions.js). Only the press: a picker drawn inside something one
67445
+ // carries is still carried by its cross.
67446
+ if (clearConfirm !== undefined) {
67447
+ return (
67448
+ // A picker on the façade of a picker: the cross opens the question, and
67449
+ // yes sends the clear to this picker's input.
67450
+ jsx(Picker, {
67451
+ type: "confirm",
67452
+ variant: "icon",
67453
+ selfInteractions: "click",
67454
+ ui: icon,
67455
+ message: clearConfirm,
67456
+ command: "--navi-clear",
67457
+ commandFor: id,
67458
+ tabIndex: "-1",
67459
+ "aria-label": naviI18n("button.clear"),
67460
+ ...rest
67461
+ })
67462
+ );
67463
+ }
67464
+ return jsx(Button, {
67465
+ command: "--navi-clear",
67466
+ commandFor: id,
67467
+ selfInteractions: "click",
67468
+ tabIndex: "-1"
67469
+ // No navi-focus-delegate, unlike the identical button inside an input:
67470
+ // handing focus back to the picker's own input is what opens the popup,
67471
+ // and clearing is the opposite intention.
67472
+ ,
67473
+
67474
+ icon: true,
67475
+ variant: "discrete"
67476
+ // What is busy once the clear is sent is the picker — the value being
67477
+ // removed is the whole field's, and the picker already draws the wait
67478
+ // around all of it. Two outlines for one wait is one too many.
67479
+ ,
67480
+
67481
+ loadingOutline: false
67482
+ // preventDefault, not just tabIndex="-1": a mousedown focuses its target
67483
+ // before any click happens, and this button should never hold focus at
67484
+ // all — the field keeps it.
67485
+ ,
67486
+
67487
+ onMouseDown: e => {
67488
+ e.preventDefault();
67489
+ },
67490
+ flex: true,
67491
+ align: "center",
67492
+ "aria-label": naviI18n("button.clear"),
67493
+ ...rest,
67494
+ children: icon
67495
+ });
67496
+ };
67497
+ // Clearing is a modification: nothing to offer on a picker whose value cannot
67498
+ // be changed. Said here rather than left to the cross's own gate — the cross is
67499
+ // a control of its own, so the picker's interaction gate finds IT and would let
67500
+ // the press through: the tap aimed where the chevron sits would empty a field
67501
+ // nothing else can touch.
67502
+ const pickerHasSomethingToClear = ({
67503
+ interactive,
67504
+ value
67505
+ }) => {
67506
+ if (!interactive) {
67507
+ return false;
67508
+ }
67509
+ return value !== undefined && value !== "";
67510
+ };
67511
+
67313
67512
  // What the picker draws itself — the value it shows, the furniture in its slot,
67314
67513
  // and whatever a caller puts in either — is not another control of the field
67315
67514
  // around it: none of it may take the id (nor the name) a <Field> hands down,
@@ -67500,6 +67699,7 @@ const PickerFirstResolver = props => {
67500
67699
  };
67501
67700
  const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerConfirmResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
67502
67701
  Picker.Chip = PickerChip;
67702
+ Picker.Clear = PickerClear;
67503
67703
  Picker.UI = PickerDefaultUI;
67504
67704
  Picker.UI.Date = PickerDateUI;
67505
67705
  Picker.UI.Time = PickerTimeUI;