@jsenv/navi 0.29.147 → 0.29.149

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.
5563
+ */
5564
+ const SELF_INTERACTIONS_ATTRIBUTE = "data-self-interactions";
5565
+ const ALL_SELF_INTERACTIONS = "*";
5566
+
5567
+ /**
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.
5506
5571
  */
5507
- const OWN_TARGET_ATTRIBUTE = "data-own-target";
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");
5508
5585
 
5509
5586
  /**
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.
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
 
@@ -31192,7 +31285,9 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
31192
31285
  * 5. The element focused before the container opened
31193
31286
  *
31194
31287
  * [navi-autofocus="restore"] appears in step 1 only: it never claims focus on
31195
- * a fresh open, it only gets it back.
31288
+ * a fresh open, it only gets it back. Unless a caller says the arrival is not a
31289
+ * fresh open but the answer to a press aimed at that very control — see
31290
+ * findFocusTarget's `restoreMayClaim`.
31196
31291
  *
31197
31292
  * A ladder that comes back empty — a container holding nothing focusable yet —
31198
31293
  * places no focus, and says so on the container ([navi-autofocus-unplaced]),
@@ -31340,9 +31435,19 @@ const markAutofocusRestoreOnClose = (
31340
31435
  * it, so landing there scrolls whatever comes before it out of sight.
31341
31436
  * transferFocus turns this on by itself wherever the keyboard is a virtual
31342
31437
  * one — see the reasoning there.
31438
+ * @param {boolean} [options.restoreMayClaim]
31439
+ * Lets `navi-autofocus="restore"` claim the focus like a plain `autoFocus`.
31440
+ * Its "never on a fresh open" is about a surface APPEARING — a sheet whose
31441
+ * field must not raise a phone's keyboard by opening. A press aimed at the
31442
+ * very control being handed the focus is not that: it IS the ask, and
31443
+ * refusing it leaves the focus on the button that was pressed. Only
31444
+ * "restore" is freed; "last-resort" still waits its turn.
31343
31445
  * @returns {{target: HTMLElement, reason: string}|undefined}
31344
31446
  */
31345
- const findFocusTarget = (containerEl, { skipFirstFocusable } = {}) => {
31447
+ const findFocusTarget = (
31448
+ containerEl,
31449
+ { skipFirstFocusable, restoreMayClaim } = {},
31450
+ ) => {
31346
31451
  // Not while there is anything else: what takes the focus only for want of
31347
31452
  // anything better ("last-resort") and what only takes it back ("restore").
31348
31453
  // Neither is dropped, both are simply tried later — step 3 below for the
@@ -31359,8 +31464,15 @@ const findFocusTarget = (containerEl, { skipFirstFocusable } = {}) => {
31359
31464
  const isHiddenFromAssistiveTech = (element) =>
31360
31465
  Boolean(element.closest?.(`[aria-hidden="true"]`));
31361
31466
 
31362
- const skip = (element) =>
31363
- isRestorableAutofocus(element) || isHiddenFromAssistiveTech(element);
31467
+ const skip = (element) => {
31468
+ if (isHiddenFromAssistiveTech(element)) {
31469
+ return true;
31470
+ }
31471
+ if (restoreMayClaim) {
31472
+ return element.getAttribute("navi-autofocus") === "last-resort";
31473
+ }
31474
+ return isRestorableAutofocus(element);
31475
+ };
31364
31476
 
31365
31477
  // Every mark, not just the first: a mark is only worth stopping at if it
31366
31478
  // leads somewhere focusable. One inside a screen waiting its turn (an inert
@@ -35193,10 +35305,11 @@ const useControlProps = (props, {
35193
35305
  return dispatched;
35194
35306
  };
35195
35307
 
35196
- // What the caller wrote runs from inside the gate when this control is an
35197
- // own target: a plain `onClick` is DOM, and it would fire from a cross drawn
35198
- // greyed by the read-only control the affordance sits in (see own_target.js).
35199
- 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);
35200
35313
  const gateCallerHandler = (handler, e) => {
35201
35314
  if (!handler) {
35202
35315
  return undefined;
@@ -35773,12 +35886,13 @@ const useInteractiveProps = (props, {
35773
35886
  } = props;
35774
35887
  const [controlRootProps, controlHostProps] = splitControlProps(props);
35775
35888
  controlRootProps["navi-control"] = controlInfo.controlType;
35776
- if (props.ownTarget) {
35889
+ if (props.selfInteractions) {
35777
35890
  // One attribute, in the DOM, because that is where the claim is read from —
35778
35891
  // by the controls above and by the gesture readers below (see
35779
- // own_target.js). The prop is the ergonomic form of it and nothing more: an
35780
- // element an application draws itself writes the same attribute by hand.
35781
- 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);
35782
35896
  }
35783
35897
  const {
35784
35898
  "navi-control-proxy-for": naviProxyFor
@@ -35830,12 +35944,12 @@ const useInteractiveProps = (props, {
35830
35944
  optimistic
35831
35945
  } = props;
35832
35946
 
35833
- // `ownTarget="always"`: an affordance that writes nothing to the control it
35834
- // sits in has no business inheriting that control's state — a diskette
35835
- // saving a row into the reader's own address book stays pressable on a game
35836
- // nobody may edit. Its own props still hold; only what came from above is
35837
- // dropped (see own_target.js for the three modes).
35838
- 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);
35839
35953
  const controlDisabled = zoneStateApplies && controlDisabledFromAbove;
35840
35954
  const controlReadOnly = zoneStateApplies && controlReadOnlyFromAbove;
35841
35955
  const controlLoading = zoneStateApplies && controlLoadingFromAbove;
@@ -44458,8 +44572,8 @@ const ButtonFirstResolver = props => {
44458
44572
  const Next = useNextResolver();
44459
44573
  const defaultRef = useRef(null);
44460
44574
  props.ref = props.ref || defaultRef;
44461
- const ownTargetHidden = useOwnTargetHidden(props);
44462
- if (ownTargetHidden) {
44575
+ const selfInteractionsHidden = useSelfInteractionsHidden(props);
44576
+ if (selfInteractionsHidden) {
44463
44577
  return null;
44464
44578
  }
44465
44579
  return jsx(Next, {
@@ -44559,7 +44673,8 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
44559
44673
 
44560
44674
  /**
44561
44675
  * @type {import("ignore:preact").FunctionComponent<{
44562
- * ownTarget?: boolean | "refuse" | "always",
44676
+ * selfInteractions?: string,
44677
+ * whenSelfInteractionsBlocked?: "hide" | "refuse" | "ignore",
44563
44678
  * replace?: boolean,
44564
44679
  * [key: string]: any,
44565
44680
  * }>}
@@ -44570,15 +44685,20 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
44570
44685
  * @param {Function} [action] On a button with an `href` or a `route`, the
44571
44686
  * same order as a Link's: it runs on the press, before the navigation, and
44572
44687
  * the navigation does not wait for it (see Link's `action`).
44573
- * @param {boolean|"refuse"|"always"} [ownTarget] A real target inside a zone
44574
- * that belongs to another control — a chip's cross on a picker's façade, an
44575
- * eye on a pressable row, a diskette inside a slide that travels. The press is
44576
- * this button's alone (no travel starts, no popup opens, no navi control above
44577
- * answers) and its `onClick` waits for its own interaction gate instead of
44578
- * firing from the DOM. What it does where the zone is read-only, disabled or
44579
- * busy depends on whether it WRITES to the control it sits in: it goes by
44580
- * default, `"refuse"` keeps it and refuses with a callout, `"always"` ignores
44581
- * the zone's state entirely — for a gesture that never touched that control.
44688
+ * @param {string} [selfInteractions] The interactions this button takes for
44689
+ * itself inside a zone that belongs to another control — a chip's cross on a
44690
+ * picker's façade, an eye on a pressable row, a badge against the edge of a
44691
+ * card one carries. A required list, because a press is not a drag:
44692
+ * `"click"` makes the press this button's alone (no popup opens, no navi
44693
+ * control above answers) and leaves the grab to whatever it sits in,
44694
+ * `"click drag"` takes both, `"*"` takes every gesture there is. Whatever is
44695
+ * claimed, the button's `onClick` waits for its own interaction gate instead
44696
+ * of firing from the DOM.
44697
+ * @param {"hide"|"refuse"|"ignore"} [whenSelfInteractionsBlocked] What becomes
44698
+ * of it where the zone around it is disabled or read-only, which is settled
44699
+ * by whether it WRITES to the control it sits in: it goes (`"hide"`, the
44700
+ * default), `"refuse"` keeps it and refuses with a callout, `"ignore"` lets
44701
+ * it through untouched — for an affordance that never wrote to that control.
44582
44702
  */
44583
44703
  const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
44584
44704
 
@@ -44593,7 +44713,7 @@ installImportMetaCssBuild(import.meta);/**
44593
44713
  * caps are fixed: same place, same size, in both states, and either of them
44594
44714
  * hands the floor to the other side. That is the whole reason the caps sit
44595
44715
  * OUTSIDE the controls rather than being drawn inside them (a picker's façade
44596
- * yields a zone with `ownTarget`, a field has `Input.UI.LeftSlot`): an icon
44716
+ * yields a zone with `selfInteractions`, a field has `Input.UI.LeftSlot`): an icon
44597
44717
  * that lives inside its control while open and becomes a pill once closed is
44598
44718
  * a switch that moves when you flip it: the finger that opened the search has
44599
44719
  * to travel to close it again. Out here, the same pixel does both.
@@ -44619,6 +44739,7 @@ installImportMetaCssBuild(import.meta);/**
44619
44739
  * make the row thinner or thicker.
44620
44740
  */
44621
44741
  const css$O = /* css */`.navi_control_swap {
44742
+ font-size: var(--navi-control-font-size);
44622
44743
  --x-control-swap-size: var(--navi-control-swap-size, calc(var(--navi-control-line-height) + 2 * var(--navi-s) + 2 *
44623
44744
  var(--navi-control-border-width)));
44624
44745
  --x-control-swap-gap: var(--navi-control-swap-gap, var(--navi-xs));
@@ -44627,6 +44748,7 @@ const css$O = /* css */`.navi_control_swap {
44627
44748
  gap: var(--x-control-swap-gap);
44628
44749
 
44629
44750
  & > .navi_control_swap_cap {
44751
+ --button-background-color: var(--navi-surface-color);
44630
44752
  aspect-ratio: 1;
44631
44753
  flex: none;
44632
44754
  justify-content: center;
@@ -44817,7 +44939,7 @@ const ControlSwap = props => {
44817
44939
  id: `${slotIdPrefix}_${index}`,
44818
44940
  className: "navi_control_swap_slot",
44819
44941
  inert: index === activeIndex ? undefined : true,
44820
- children: side.children
44942
+ children: side.control
44821
44943
  }, side.name))
44822
44944
  })
44823
44945
  }), jsx(ControlSwapCap, {
@@ -44832,7 +44954,8 @@ const ControlSwap = props => {
44832
44954
 
44833
44955
  /**
44834
44956
  * One of the two controls, and the cap that speaks for it. Declarative: the row
44835
- * reads these and draws the caps at its ends, the controls between them.
44957
+ * reads these and draws the caps at its ends, the controls between them. The
44958
+ * props below say what the side IS; every other prop describes its cap.
44836
44959
  *
44837
44960
  * @type {import("ignore:preact").FunctionComponent<{
44838
44961
  * name?: string,
@@ -44841,6 +44964,7 @@ const ControlSwap = props => {
44841
44964
  * badge?: boolean | import("ignore:preact").ComponentChildren,
44842
44965
  * autoFocus?: boolean,
44843
44966
  * children?: import("ignore:preact").ComponentChildren,
44967
+ * [key: string]: any,
44844
44968
  * }>}
44845
44969
  * @param name - How `value`/`signal`/`onChange` name this side. Its position
44846
44970
  * ("0" or "1") by default.
@@ -44848,15 +44972,24 @@ const ControlSwap = props => {
44848
44972
  * @param label - What the cap is called — it holds no text, so this is its
44849
44973
  * accessible name. Say what pressing it reveals ("Rechercher"), not what it
44850
44974
  * currently shows: the cap wears `aria-expanded` for the state.
44851
- * @param badge - A mark on the cap saying this collapsed control is still
44852
- * doing something (a filter set, a search typed). `true` draws a dot;
44853
- * anything else is drawn as given (a `<BadgeCount>`, say).
44975
+ * @param badge - A mark on the cap saying the control behind it is still doing
44976
+ * something (a filter set, a search typed). `true` draws a dot; anything else
44977
+ * is drawn as given (a `<BadgeCount>`, say). Drawn only while this side is
44978
+ * the collapsed one — the side holding the floor spells out in full what a
44979
+ * dot could only hint at — so it is read straight off the state
44980
+ * (`badge={Boolean(groupId)}`), with no "and this side is hidden" to add.
44854
44981
  * @param autoFocus - On by default: the focus goes into this control when it
44855
44982
  * takes the floor, where navi's ladder puts it — an `autoFocus` inside the
44856
44983
  * control first, its first focusable otherwise. `false` leaves it on the cap
44857
44984
  * that was pressed, for a control one reads before writing in (and, on a
44858
44985
  * phone, for a keyboard that must not rise). Never on mount, whatever the
44859
44986
  * setting.
44987
+ *
44988
+ * Anything else — `data-testid`, `variant`, `backgroundColor`, `color`, an
44989
+ * `aria-describedby` — goes to the cap, which is a `<Button>`. It is the one
44990
+ * element of the row an application does not render, so nothing else can name
44991
+ * it or dress it; the control it stands for is a vnode of the caller's own and
44992
+ * takes its props directly.
44860
44993
  */
44861
44994
  const ControlSwapSide = () => null;
44862
44995
  const ControlSwapCap = ({
@@ -44869,14 +45002,23 @@ const ControlSwapCap = ({
44869
45002
  const {
44870
45003
  icon,
44871
45004
  label,
44872
- badge
44873
- } = side;
45005
+ badge,
45006
+ ...capProps
45007
+ } = side.capProps;
45008
+ // A badge on a cap can only say one thing: the control you are NOT looking at
45009
+ // holds something. The one holding the floor is on the row saying it itself.
45010
+ const badgeToDraw = active ? null : badge;
44874
45011
  return jsxs(Button, {
44875
- ref: ref,
44876
- className: "navi_control_swap_cap",
44877
45012
  icon: true,
44878
45013
  pressEffect: "none",
44879
45014
  "aria-label": label,
45015
+ ...capProps,
45016
+ ref: ref
45017
+ // After the caller's props, all of them: the class is what the row's own
45018
+ // CSS reaches for, and the rest is the wiring that makes the cap a cap.
45019
+ ,
45020
+
45021
+ className: withPropsClassName("navi_control_swap_cap", capProps.className),
44880
45022
  "aria-expanded": active,
44881
45023
  "aria-controls": slotId,
44882
45024
  onClick: onPress,
@@ -44884,10 +45026,10 @@ const ControlSwapCap = ({
44884
45026
  width: "50%",
44885
45027
  square: true,
44886
45028
  children: icon
44887
- }), badge ? jsx("span", {
45029
+ }), badgeToDraw ? jsx("span", {
44888
45030
  className: "navi_control_swap_badge",
44889
45031
  "aria-hidden": "true",
44890
- children: badge === true ? null : badge
45032
+ children: badgeToDraw === true ? null : badgeToDraw
44891
45033
  }) : null]
44892
45034
  });
44893
45035
  };
@@ -44898,13 +45040,20 @@ const readSides = children => {
44898
45040
  if (!child || child.type !== ControlSwapSide) {
44899
45041
  continue;
44900
45042
  }
45043
+ // What is left over once the side's own vocabulary is taken out describes
45044
+ // the cap: the control is a vnode the caller wrote and dresses itself, so
45045
+ // the cap is the only thing in the row left to describe.
44901
45046
  const {
44902
45047
  name,
44903
- ...rest
45048
+ autoFocus,
45049
+ children: control,
45050
+ ...capProps
44904
45051
  } = child.props;
44905
45052
  sides.push({
44906
45053
  name: name === undefined ? String(sides.length) : name,
44907
- ...rest
45054
+ autoFocus,
45055
+ control,
45056
+ capProps
44908
45057
  });
44909
45058
  }
44910
45059
  return sides;
@@ -44916,14 +45065,22 @@ const readSides = children => {
44916
45065
  //
44917
45066
  // WHERE inside is navi's own ladder (findFocusTarget): an `autoFocus` in the
44918
45067
  // control's own content first, the first focusable otherwise, a last resort
44919
- // after that. Unlike an arriving popup it keeps the first focusable even where
44920
- // the pointer is coarse (see docs/autofocus.md): a cap is pressed to reach the
44921
- // control it names, so a keyboard rising is the answer to that gesture rather
44922
- // than a cost imposed on a screen that merely appeared. A side one reads before
44923
- // writing in says `autoFocus={false}`.
45068
+ // after that. Two of the ladder's reflexes are turned off here, and it is the
45069
+ // same reason both times — they are about a surface APPEARING, and this is a
45070
+ // press aimed at the control being handed the focus:
45071
+ // - the first focusable is kept even where the pointer is coarse (an arriving
45072
+ // popup drops it so a virtual keyboard does not rise over what it just
45073
+ // showed, see docs/autofocus.md); pressing a cap to reach a field is asking
45074
+ // for that keyboard;
45075
+ // - `autoFocus="restore"` may claim it, though it means "never on a fresh
45076
+ // open". A field marked that way so its sheet opens quietly would otherwise
45077
+ // leave the focus on the magnifier that was pressed to reach it.
45078
+ // A side one reads before writing in says `autoFocus={false}`.
44924
45079
  const focusWithTheFloor = (arrivingSlot, arrivingSide, arrivingCap) => {
44925
45080
  if (arrivingSide.autoFocus !== false) {
44926
- const found = findFocusTarget(arrivingSlot);
45081
+ const found = findFocusTarget(arrivingSlot, {
45082
+ restoreMayClaim: true
45083
+ });
44927
45084
  if (found) {
44928
45085
  moveFocusTo(found.target);
44929
45086
  return;
@@ -65506,8 +65663,8 @@ const BadgeStyleCSSVars = {
65506
65663
  fontSize: "--font-size"
65507
65664
  };
65508
65665
  const BadgeButton = props => {
65509
- const ownTargetHidden = useOwnTargetHidden(props);
65510
- if (ownTargetHidden) {
65666
+ const selfInteractionsHidden = useSelfInteractionsHidden(props);
65667
+ if (selfInteractionsHidden) {
65511
65668
  return null;
65512
65669
  }
65513
65670
  return jsx(BadgeButtonUI, {
@@ -66267,7 +66424,7 @@ const PickerArrayUI = asPickerOwnUI(() => {
66267
66424
  * around the chip. `commandFor` is for a chip that stands outside the picker it
66268
66425
  * speaks for.
66269
66426
  *
66270
- * The cross is an own target (see own_target.js), so the press belongs to it
66427
+ * The cross claims the press (see self_interactions.js), so it belongs to it
66271
66428
  * and not to the picker underneath, and it goes when the picker turns read-only
66272
66429
  * — a row being read still says what was picked, it just no longer offers to
66273
66430
  * unpick it.
@@ -66294,7 +66451,7 @@ const PickerChip = ({
66294
66451
  flex: true,
66295
66452
  ...rest,
66296
66453
  children: [children, jsx(Badge.Button, {
66297
- ownTarget: true,
66454
+ selfInteractions: "click",
66298
66455
  command: "--navi-unselect",
66299
66456
  commandFor: commandFor,
66300
66457
  value: value,
@@ -66671,7 +66828,7 @@ installImportMetaCssBuild(import.meta);const css$t = /* css */`@layer navi {
66671
66828
  overflow: visible !important;
66672
66829
  }
66673
66830
 
66674
- & [data-own-target] {
66831
+ & [data-self-interactions~="click"], & [data-self-interactions~="*"] {
66675
66832
  pointer-events: auto;
66676
66833
  position: relative;
66677
66834
  }
@@ -66942,6 +67099,9 @@ const PickerButton = props => {
66942
67099
  // Adds a clear button to the right slot, the same one type="search" puts at
66943
67100
  // the end of an input: a picker holds a value the user chose, and unsetting
66944
67101
  // it should not require reopening the popup to hunt for a "none" entry.
67102
+ // Where navi puts it, which needs a slot to put it in: a variant without
67103
+ // one (bare above all) holds a <Picker.Clear /> in its own drawing instead
67104
+ // — the same cross, placed by whoever draws around it.
66945
67105
  clearable,
66946
67106
  // "Are you sure?" before the cross clears anything. A cross of three
66947
67107
  // millimetres at the edge of a touch screen, right where the chevron is
@@ -66962,6 +67122,17 @@ const PickerButton = props => {
66962
67122
  const isSingleLine = maxLines === 1;
66963
67123
  // Same rule as the root: phrasing content inside a sentence.
66964
67124
  const ContentTag = variant === "text" ? "span" : "div";
67125
+ // Which variants get the right slot — the chevron saying "this opens", and
67126
+ // the cross replacing it once there is something to clear.
67127
+ // Not the ones that draw no value beside it: an icon picker IS its icon, a
67128
+ // headless one draws nothing, a button says what it opens with its label, a
67129
+ // word in a sentence has no room for furniture. Nor a picker rendering the
67130
+ // browser's own control ("default").
67131
+ // Nor a bare one: the picker is that drawing's box to the pixel, so anything
67132
+ // navi adds beside it either grows the box or covers what the caller drew.
67133
+ // The pieces are the caller's to place there instead — a <Picker.Clear /> in
67134
+ // their own layout (see warnOnClearableWithoutSlot).
67135
+ const hasRightSlot = variant !== "icon" && variant !== "headless" && variant !== "button" && variant !== "text" && variant !== "bare" && ui !== "default";
66965
67136
  const inputRef = useRef(null);
66966
67137
  const [pickerRemainingProps, inputProps, facadeChildrenProps] = useControlFacadeProps({
66967
67138
  ...props,
@@ -66990,6 +67161,17 @@ const PickerButton = props => {
66990
67161
  // clearing being a modification like any other.
66991
67162
  const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
66992
67163
  usePickerErrorCallout(uiStateController, error);
67164
+ // What the picker knows about itself, for the pieces it does not place: the
67165
+ // drawings of its value (Picker.UI.*), and the affordances a caller may put
67166
+ // in their own `ui` (Picker.Clear) as much as the ones it puts in its slot.
67167
+ const pickerContext = {
67168
+ value,
67169
+ placeholder,
67170
+ maxLines,
67171
+ id: inputProps.id,
67172
+ interactive,
67173
+ clearConfirm
67174
+ };
66993
67175
  return (
66994
67176
  /* Read-only crosses into everything the picker is made of: what it really
66995
67177
  holds is drawn by controls of their own — in the popup, and on the façade
@@ -67063,112 +67245,108 @@ const PickerButton = props => {
67063
67245
  onnavi_request_unselect: e => {
67064
67246
  requestPickerListEntry(ref.current, inputRef.current, e, "unselect");
67065
67247
  },
67066
- children: [jsxs("span", {
67248
+ children: [jsx("span", {
67067
67249
  className: "navi_picker_box",
67068
- children: [variant === "headless" ? null : jsx(LoadingOutline, {
67069
- loading: loading,
67070
- color: "var(--picker-loader-color)",
67071
- inset: -2
67072
- }), jsx(PickerInput, {
67073
- tabIndex: variant === "headless" ? -1 : undefined,
67074
- "aria-hidden": variant === "headless" ? "true" : undefined,
67075
- ...inputProps,
67076
- // eslint-disable-next-line react/no-children-prop
67077
- children: undefined // we will render children into the div
67078
- ,
67079
-
67080
- ui: ui,
67081
- onCopy: e => {
67082
- const pickerEl = ref.current;
67083
- if (isWithinPickerContent(e.target, pickerEl)) {
67084
- return;
67085
- }
67086
- const uiState = uiStateController.uiState;
67087
- if (uiState === undefined) {
67088
- return;
67089
- }
67090
- e.preventDefault();
67091
- const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
67092
- e.clipboardData.setData("text/plain", displayText);
67093
- e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
67094
- },
67095
- onCut: e => {
67096
- const pickerEl = ref.current;
67097
- if (isWithinPickerContent(e.target, pickerEl)) {
67098
- return;
67099
- }
67100
- const uiState = uiStateController.uiState;
67101
- if (uiState === undefined) {
67102
- return;
67103
- }
67104
- // the copy part don't need control to be interactable
67105
- const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
67106
- e.clipboardData.setData("text/plain", displayText);
67107
- e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
67108
- // the clear ui state part need control to be interactable
67109
- dispatchRequestInteraction(pickerEl, {
67110
- event: e,
67111
- name: "cut",
67112
- allowed: () => {
67113
- dispatchRequestClearUIState(inputRef.current, e);
67250
+ children: jsxs(PickerContext.Provider, {
67251
+ value: pickerContext,
67252
+ children: [variant === "headless" ? null : jsx(LoadingOutline, {
67253
+ loading: loading,
67254
+ color: "var(--picker-loader-color)",
67255
+ inset: -2
67256
+ }), jsx(PickerInput, {
67257
+ tabIndex: variant === "headless" ? -1 : undefined,
67258
+ "aria-hidden": variant === "headless" ? "true" : undefined,
67259
+ ...inputProps,
67260
+ // eslint-disable-next-line react/no-children-prop
67261
+ children: undefined // we will render children into the div
67262
+ ,
67263
+
67264
+ ui: ui,
67265
+ onCopy: e => {
67266
+ const pickerEl = ref.current;
67267
+ if (isWithinPickerContent(e.target, pickerEl)) {
67268
+ return;
67114
67269
  }
67115
- });
67116
- e.preventDefault();
67117
- },
67118
- onPaste: e => {
67119
- const pickerEl = ref.current;
67120
- if (isWithinPickerContent(e.target, pickerEl)) {
67121
- // Don't intercept inside the picker popup content.
67122
- return;
67123
- }
67124
- const naviData = e.clipboardData.getData("application/x-navi");
67125
- let pasteValue;
67126
- if (naviData) {
67127
- try {
67128
- pasteValue = JSON.parse(naviData);
67129
- } catch {
67130
- pasteValue = naviData;
67270
+ const uiState = uiStateController.uiState;
67271
+ if (uiState === undefined) {
67272
+ return;
67131
67273
  }
67132
- } else {
67133
- pasteValue = e.clipboardData.getData("text/plain");
67134
- }
67135
- dispatchRequestInteraction(pickerEl, {
67136
- event: e,
67137
- name: "paste",
67138
- allowed: () => {
67139
- dispatchRequestSetUIState(inputRef.current, pasteValue, {
67140
- event: e
67141
- });
67274
+ e.preventDefault();
67275
+ const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
67276
+ e.clipboardData.setData("text/plain", displayText);
67277
+ e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
67278
+ },
67279
+ onCut: e => {
67280
+ const pickerEl = ref.current;
67281
+ if (isWithinPickerContent(e.target, pickerEl)) {
67282
+ return;
67142
67283
  }
67143
- });
67144
- e.preventDefault();
67145
- }
67146
- }), variant === "headless" || ui === "default" ? null : jsx(Text, {
67147
- className: "navi_picker_value"
67148
- // Tells the caller's own drawing of the control from the value
67149
- // the picker draws itself, so each is written on its own line
67150
- // (see .navi_picker_value in the CSS above).
67151
- ,
67152
-
67153
- "data-picker-facade": ui === undefined ? undefined : ""
67154
- // A placeholder is the picker saying "nothing here yet" about
67155
- // the value navi draws — the default rendering, or the one a
67156
- // typed picker installs for itself. A button's label is not
67157
- // that, however empty the picker behind it is, and neither is a
67158
- // caller's own "ui": an empty value may be exactly what the
67159
- // caller is drawing there ("no filter", "anywhere"), so how it
67160
- // looks empty stays theirs (documented on the ui prop below).
67161
- ,
67162
-
67163
- "navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) && variant !== "button" && variant !== "text" && uiStateHoldsNothing(value) ? "" : undefined,
67164
- maxLines: maxLines,
67165
- children: jsx(PickerOwnContent, {
67166
- children: jsx(PickerContext.Provider, {
67167
- value: {
67168
- value,
67169
- placeholder,
67170
- maxLines
67171
- },
67284
+ const uiState = uiStateController.uiState;
67285
+ if (uiState === undefined) {
67286
+ return;
67287
+ }
67288
+ // the copy part don't need control to be interactable
67289
+ const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
67290
+ e.clipboardData.setData("text/plain", displayText);
67291
+ e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
67292
+ // the clear ui state part need control to be interactable
67293
+ dispatchRequestInteraction(pickerEl, {
67294
+ event: e,
67295
+ name: "cut",
67296
+ allowed: () => {
67297
+ dispatchRequestClearUIState(inputRef.current, e);
67298
+ }
67299
+ });
67300
+ e.preventDefault();
67301
+ },
67302
+ onPaste: e => {
67303
+ const pickerEl = ref.current;
67304
+ if (isWithinPickerContent(e.target, pickerEl)) {
67305
+ // Don't intercept inside the picker popup content.
67306
+ return;
67307
+ }
67308
+ const naviData = e.clipboardData.getData("application/x-navi");
67309
+ let pasteValue;
67310
+ if (naviData) {
67311
+ try {
67312
+ pasteValue = JSON.parse(naviData);
67313
+ } catch {
67314
+ pasteValue = naviData;
67315
+ }
67316
+ } else {
67317
+ pasteValue = e.clipboardData.getData("text/plain");
67318
+ }
67319
+ dispatchRequestInteraction(pickerEl, {
67320
+ event: e,
67321
+ name: "paste",
67322
+ allowed: () => {
67323
+ dispatchRequestSetUIState(inputRef.current, pasteValue, {
67324
+ event: e
67325
+ });
67326
+ }
67327
+ });
67328
+ e.preventDefault();
67329
+ }
67330
+ }), variant === "headless" || ui === "default" ? null : jsx(Text, {
67331
+ className: "navi_picker_value"
67332
+ // Tells the caller's own drawing of the control from the value
67333
+ // the picker draws itself, so each is written on its own line
67334
+ // (see .navi_picker_value in the CSS above).
67335
+ ,
67336
+
67337
+ "data-picker-facade": ui === undefined ? undefined : ""
67338
+ // A placeholder is the picker saying "nothing here yet" about
67339
+ // the value navi draws — the default rendering, or the one a
67340
+ // typed picker installs for itself. A button's label is not
67341
+ // that, however empty the picker behind it is, and neither is a
67342
+ // caller's own "ui": an empty value may be exactly what the
67343
+ // caller is drawing there ("no filter", "anywhere"), so how it
67344
+ // looks empty stays theirs (documented on the ui prop below).
67345
+ ,
67346
+
67347
+ "navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) && variant !== "button" && variant !== "text" && uiStateHoldsNothing(value) ? "" : undefined,
67348
+ maxLines: maxLines,
67349
+ children: jsx(PickerOwnContent, {
67172
67350
  children: jsx(MaxLinesContext.Provider, {
67173
67351
  value: maxLines,
67174
67352
  children: ui === undefined ? variant === "icon" ?
@@ -67183,71 +67361,23 @@ const PickerButton = props => {
67183
67361
  }) : jsx(PickerDefaultUI, {}) : ui
67184
67362
  })
67185
67363
  })
67186
- })
67187
- }), variant === "icon" || variant === "headless" || variant === "button" || variant === "text" || variant === "bare" || ui === "default" ? null : jsx("span", {
67188
- className: "navi_picker_right_slot",
67189
- children: jsx(PickerOwnContent, {
67190
- children: clearable && interactive && value !== undefined && value !== "" && clearConfirm !== undefined ?
67191
- // A picker on the façade of a picker: the cross opens the
67192
- // question, and yes sends the clear to this picker's input.
67193
- // A door only (allowNameless): the form around does not see
67194
- // a field in it.
67195
- jsx(Picker, {
67196
- type: "confirm",
67197
- variant: "icon",
67198
- ui: jsx(Icon, {
67199
- size: rightSlotIconSize,
67200
- lineOverflow: "allow",
67201
- children: jsx(CloseSvg, {})
67202
- }),
67203
- message: clearConfirm,
67204
- command: "--navi-clear",
67205
- commandFor: inputProps.id,
67206
- tabIndex: "-1"
67207
- }) : clearable && interactive && value !== undefined && value !== "" ? jsx(Button, {
67208
- command: "--navi-clear",
67209
- commandFor: inputProps.id,
67210
- tabIndex: "-1"
67211
- // No navi-focus-delegate, unlike the identical button inside an
67212
- // input: handing focus back to the picker's own input is what
67213
- // opens the popup, and clearing is the opposite intention.
67214
- ,
67215
-
67216
- icon: true,
67217
- variant: "discrete"
67218
- // What is busy once the clear is sent is the picker — the value
67219
- // being removed is the whole field's, and the picker already
67220
- // draws the wait around all of it. Two outlines for one wait is
67221
- // one too many.
67222
- ,
67223
-
67224
- loadingOutline: false
67225
- // preventDefault, not just tabIndex="-1": a mousedown focuses
67226
- // its target before any click happens, and this button should
67227
- // never hold focus at all — the field keeps it.
67228
- ,
67229
-
67230
- onMouseDown: e => {
67231
- e.preventDefault();
67232
- },
67233
- flex: true,
67234
- align: "center",
67235
- children: jsx(Icon, {
67364
+ }), hasRightSlot ? jsx("span", {
67365
+ className: "navi_picker_right_slot",
67366
+ children: jsx(PickerOwnContent, {
67367
+ children: clearable && pickerHasSomethingToClear(pickerContext) ? jsx(PickerClear, {
67368
+ size: rightSlotIconSize
67369
+ }) : rightSlot === undefined ?
67370
+ // lineOverflow: what sits in the slot is an affordance, not a
67371
+ // character — a caller asking for a bigger one wants it bigger,
67372
+ // not capped at the height of the line it sits on
67373
+ jsx(Icon, {
67236
67374
  size: rightSlotIconSize,
67237
67375
  lineOverflow: "allow",
67238
- children: jsx(CloseSvg, {})
67239
- })
67240
- }) : rightSlot === undefined ?
67241
- // lineOverflow: what sits in the slot is an affordance, not a
67242
- // character — a caller asking for a bigger one wants it bigger,
67243
- // not capped at the height of the line it sits on
67244
- jsx(Icon, {
67245
- size: rightSlotIconSize,
67246
- lineOverflow: "allow",
67247
- children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
67248
- }) : rightSlot
67249
- })
67250
- })]
67376
+ children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
67377
+ }) : rightSlot
67378
+ })
67379
+ }) : null]
67380
+ })
67251
67381
  }), jsx(ControlFacadeChildrenWrapper, {
67252
67382
  ...facadeChildrenProps,
67253
67383
  children: jsx(ContentTag, {
@@ -67260,6 +67390,123 @@ const PickerButton = props => {
67260
67390
  })
67261
67391
  );
67262
67392
  };
67393
+ /**
67394
+ * The cross that empties the picker it sits in — reached as `<Picker.Clear />`.
67395
+ *
67396
+ * It is the very cross `clearable` puts in the right slot — the same button,
67397
+ * wired the same way — for the drawings navi has no slot to put it in:
67398
+ * `variant="bare"` above all, where the drawing is the caller's and so is the
67399
+ * place the cross belongs in it, after a name or over a corner, at a size and
67400
+ * with an icon of its own.
67401
+ *
67402
+ * It goes when there is nothing to take out and when nothing can be changed, so
67403
+ * a caller places it once and never asks whether to draw it.
67404
+ *
67405
+ * @type {import("ignore:preact").FunctionComponent<{
67406
+ * size?: number | string,
67407
+ * children?: import("ignore:preact").ComponentChildren,
67408
+ * } & Record<string, any>>}
67409
+ * @param {number|string} [size="inherit"] How big the cross is drawn, as
67410
+ * `<Icon size>` reads it — "inherit" takes the size of the text around it.
67411
+ * @param {import("ignore:preact").ComponentChildren} [children] The icon, in place of
67412
+ * the cross. Anything else goes to the `<Button>` underneath (`variant`,
67413
+ * `paddingX`, `aria-label`, …).
67414
+ */
67415
+ const PickerClear = ({
67416
+ size = "inherit",
67417
+ children,
67418
+ ...rest
67419
+ }) => {
67420
+ const pickerContext = useContext(PickerContext);
67421
+ if (!pickerContext) {
67422
+ return null;
67423
+ }
67424
+ if (!pickerHasSomethingToClear(pickerContext)) {
67425
+ return null;
67426
+ }
67427
+ const {
67428
+ id,
67429
+ clearConfirm
67430
+ } = pickerContext;
67431
+ const icon =
67432
+ // lineOverflow: the cross is an affordance, not a character — a caller
67433
+ // asking for a bigger one wants it bigger, not capped at the height of the
67434
+ // line it sits on.
67435
+ jsx(Icon, {
67436
+ size: size,
67437
+ lineOverflow: "allow",
67438
+ children: children === undefined ? jsx(CloseSvg, {}) : children
67439
+ });
67440
+ // The press is aimed AT the cross: clearing is the opposite intention to
67441
+ // opening, and the picker's box answers a press landing anywhere in it (see
67442
+ // self_interactions.js). Only the press: a picker drawn inside something one
67443
+ // carries is still carried by its cross.
67444
+ if (clearConfirm !== undefined) {
67445
+ return (
67446
+ // A picker on the façade of a picker: the cross opens the question, and
67447
+ // yes sends the clear to this picker's input.
67448
+ jsx(Picker, {
67449
+ type: "confirm",
67450
+ variant: "icon",
67451
+ selfInteractions: "click",
67452
+ ui: icon,
67453
+ message: clearConfirm,
67454
+ command: "--navi-clear",
67455
+ commandFor: id,
67456
+ tabIndex: "-1",
67457
+ "aria-label": naviI18n("button.clear"),
67458
+ ...rest
67459
+ })
67460
+ );
67461
+ }
67462
+ return jsx(Button, {
67463
+ command: "--navi-clear",
67464
+ commandFor: id,
67465
+ selfInteractions: "click",
67466
+ tabIndex: "-1"
67467
+ // No navi-focus-delegate, unlike the identical button inside an input:
67468
+ // handing focus back to the picker's own input is what opens the popup,
67469
+ // and clearing is the opposite intention.
67470
+ ,
67471
+
67472
+ icon: true,
67473
+ variant: "discrete"
67474
+ // What is busy once the clear is sent is the picker — the value being
67475
+ // removed is the whole field's, and the picker already draws the wait
67476
+ // around all of it. Two outlines for one wait is one too many.
67477
+ ,
67478
+
67479
+ loadingOutline: false
67480
+ // preventDefault, not just tabIndex="-1": a mousedown focuses its target
67481
+ // before any click happens, and this button should never hold focus at
67482
+ // all — the field keeps it.
67483
+ ,
67484
+
67485
+ onMouseDown: e => {
67486
+ e.preventDefault();
67487
+ },
67488
+ flex: true,
67489
+ align: "center",
67490
+ "aria-label": naviI18n("button.clear"),
67491
+ ...rest,
67492
+ children: icon
67493
+ });
67494
+ };
67495
+ // Clearing is a modification: nothing to offer on a picker whose value cannot
67496
+ // be changed. Said here rather than left to the cross's own gate — the cross is
67497
+ // a control of its own, so the picker's interaction gate finds IT and would let
67498
+ // the press through: the tap aimed where the chevron sits would empty a field
67499
+ // nothing else can touch.
67500
+ const pickerHasSomethingToClear = ({
67501
+ interactive,
67502
+ value
67503
+ }) => {
67504
+ if (!interactive) {
67505
+ return false;
67506
+ }
67507
+ return value !== undefined && value !== "";
67508
+ };
67509
+
67263
67510
  // What the picker draws itself — the value it shows, the furniture in its slot,
67264
67511
  // and whatever a caller puts in either — is not another control of the field
67265
67512
  // around it: none of it may take the id (nor the name) a <Field> hands down,
@@ -67450,6 +67697,7 @@ const PickerFirstResolver = props => {
67450
67697
  };
67451
67698
  const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerConfirmResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
67452
67699
  Picker.Chip = PickerChip;
67700
+ Picker.Clear = PickerClear;
67453
67701
  Picker.UI = PickerDefaultUI;
67454
67702
  Picker.UI.Date = PickerDateUI;
67455
67703
  Picker.UI.Time = PickerTimeUI;