@jsenv/navi 0.29.148 → 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.
- package/dist/jsenv_navi.js +467 -269
- package/dist/jsenv_navi.js.map +27 -19
- package/docs/AI_INSTRUCTIONS.md +8 -6
- package/docs/actions.md +39 -5
- package/docs/error_handling.md +5 -0
- package/docs/interactions.md +83 -38
- package/docs/resource.md +19 -0
- package/package.json +3 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
5399
|
-
//
|
|
5400
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
5468
|
-
*
|
|
5469
|
-
*
|
|
5470
|
-
*
|
|
5471
|
-
*
|
|
5472
|
-
*
|
|
5473
|
-
*
|
|
5474
|
-
*
|
|
5475
|
-
*
|
|
5476
|
-
*
|
|
5477
|
-
*
|
|
5478
|
-
*
|
|
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
|
|
5482
|
-
* that refuses politely still says "there is something
|
|
5483
|
-
* row that is only being read.
|
|
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
|
|
5488
|
-
*
|
|
5489
|
-
*
|
|
5490
|
-
*
|
|
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 `
|
|
5557
|
+
* one attribute is enough and the `selfInteractions` prop only writes it.
|
|
5504
5558
|
*
|
|
5505
|
-
*
|
|
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
|
|
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
|
|
5511
|
-
* which case the control is not what the press was for and must not
|
|
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
|
|
5516
|
-
*
|
|
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
|
|
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
|
|
5524
|
-
if (!
|
|
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
|
|
5528
|
-
//
|
|
5529
|
-
if (
|
|
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(
|
|
5618
|
+
return controlRoot.contains(claimer);
|
|
5537
5619
|
};
|
|
5538
5620
|
|
|
5539
5621
|
/**
|
|
5540
|
-
* Whether the
|
|
5541
|
-
*
|
|
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
|
|
5625
|
+
const selfInteractionsIgnoreBlock = (props) =>
|
|
5626
|
+
props.whenSelfInteractionsBlocked === "ignore";
|
|
5544
5627
|
|
|
5545
5628
|
/**
|
|
5546
|
-
* Whether
|
|
5547
|
-
* control around it, and that control cannot be
|
|
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
|
|
5637
|
+
const useSelfInteractionsHidden = (props) => {
|
|
5550
5638
|
const disabled = useContext(DisabledContext);
|
|
5551
5639
|
const readOnly = useContext(ReadOnlyContext);
|
|
5552
|
-
const
|
|
5553
|
-
|
|
5640
|
+
const { selfInteractions, whenSelfInteractionsBlocked } = props;
|
|
5641
|
+
if (!selfInteractions) {
|
|
5642
|
+
return false;
|
|
5643
|
+
}
|
|
5554
5644
|
if (
|
|
5555
|
-
|
|
5556
|
-
|
|
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
|
|
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
|
|
9770
|
-
// and stepping back here — rather than
|
|
9771
|
-
// is what leaves the event whole for
|
|
9772
|
-
// interaction. Stepping back, not refusing: the
|
|
9773
|
-
// its `prevented`/`always` (an
|
|
9774
|
-
// nothing to undo and would take
|
|
9775
|
-
|
|
9776
|
-
|
|
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
|
-
*
|
|
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
|
|
20540
|
-
//
|
|
20541
|
-
|
|
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 (
|
|
20545
|
-
rest[
|
|
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
|
|
35216
|
-
//
|
|
35217
|
-
// greyed by the read-only control the affordance sits in (see
|
|
35218
|
-
|
|
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.
|
|
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
|
-
//
|
|
35799
|
-
// element an application draws itself writes the same attribute by
|
|
35800
|
-
|
|
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
|
-
// `
|
|
35853
|
-
// sits in has no business inheriting that control's state
|
|
35854
|
-
// saving a row into the reader's own address book stays
|
|
35855
|
-
// nobody may edit. Its own props still hold; only what
|
|
35856
|
-
// dropped (see
|
|
35857
|
-
const zoneStateApplies = !
|
|
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;
|
|
@@ -44477,8 +44572,8 @@ const ButtonFirstResolver = props => {
|
|
|
44477
44572
|
const Next = useNextResolver();
|
|
44478
44573
|
const defaultRef = useRef(null);
|
|
44479
44574
|
props.ref = props.ref || defaultRef;
|
|
44480
|
-
const
|
|
44481
|
-
if (
|
|
44575
|
+
const selfInteractionsHidden = useSelfInteractionsHidden(props);
|
|
44576
|
+
if (selfInteractionsHidden) {
|
|
44482
44577
|
return null;
|
|
44483
44578
|
}
|
|
44484
44579
|
return jsx(Next, {
|
|
@@ -44578,7 +44673,8 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
44578
44673
|
|
|
44579
44674
|
/**
|
|
44580
44675
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
44581
|
-
*
|
|
44676
|
+
* selfInteractions?: string,
|
|
44677
|
+
* whenSelfInteractionsBlocked?: "hide" | "refuse" | "ignore",
|
|
44582
44678
|
* replace?: boolean,
|
|
44583
44679
|
* [key: string]: any,
|
|
44584
44680
|
* }>}
|
|
@@ -44589,15 +44685,20 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
44589
44685
|
* @param {Function} [action] On a button with an `href` or a `route`, the
|
|
44590
44686
|
* same order as a Link's: it runs on the press, before the navigation, and
|
|
44591
44687
|
* the navigation does not wait for it (see Link's `action`).
|
|
44592
|
-
* @param {
|
|
44593
|
-
* that belongs to another control — a chip's cross on a
|
|
44594
|
-
* eye on a pressable row, a
|
|
44595
|
-
*
|
|
44596
|
-
*
|
|
44597
|
-
*
|
|
44598
|
-
*
|
|
44599
|
-
*
|
|
44600
|
-
*
|
|
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.
|
|
44601
44702
|
*/
|
|
44602
44703
|
const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
|
|
44603
44704
|
|
|
@@ -44612,7 +44713,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
44612
44713
|
* caps are fixed: same place, same size, in both states, and either of them
|
|
44613
44714
|
* hands the floor to the other side. That is the whole reason the caps sit
|
|
44614
44715
|
* OUTSIDE the controls rather than being drawn inside them (a picker's façade
|
|
44615
|
-
* yields a zone with `
|
|
44716
|
+
* yields a zone with `selfInteractions`, a field has `Input.UI.LeftSlot`): an icon
|
|
44616
44717
|
* that lives inside its control while open and becomes a pill once closed is
|
|
44617
44718
|
* a switch that moves when you flip it: the finger that opened the search has
|
|
44618
44719
|
* to travel to close it again. Out here, the same pixel does both.
|
|
@@ -44871,9 +44972,12 @@ const ControlSwap = props => {
|
|
|
44871
44972
|
* @param label - What the cap is called — it holds no text, so this is its
|
|
44872
44973
|
* accessible name. Say what pressing it reveals ("Rechercher"), not what it
|
|
44873
44974
|
* currently shows: the cap wears `aria-expanded` for the state.
|
|
44874
|
-
* @param badge - A mark on the cap saying
|
|
44875
|
-
*
|
|
44876
|
-
*
|
|
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.
|
|
44877
44981
|
* @param autoFocus - On by default: the focus goes into this control when it
|
|
44878
44982
|
* takes the floor, where navi's ladder puts it — an `autoFocus` inside the
|
|
44879
44983
|
* control first, its first focusable otherwise. `false` leaves it on the cap
|
|
@@ -44901,6 +45005,9 @@ const ControlSwapCap = ({
|
|
|
44901
45005
|
badge,
|
|
44902
45006
|
...capProps
|
|
44903
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;
|
|
44904
45011
|
return jsxs(Button, {
|
|
44905
45012
|
icon: true,
|
|
44906
45013
|
pressEffect: "none",
|
|
@@ -44911,7 +45018,7 @@ const ControlSwapCap = ({
|
|
|
44911
45018
|
// CSS reaches for, and the rest is the wiring that makes the cap a cap.
|
|
44912
45019
|
,
|
|
44913
45020
|
|
|
44914
|
-
className:
|
|
45021
|
+
className: withPropsClassName("navi_control_swap_cap", capProps.className),
|
|
44915
45022
|
"aria-expanded": active,
|
|
44916
45023
|
"aria-controls": slotId,
|
|
44917
45024
|
onClick: onPress,
|
|
@@ -44919,10 +45026,10 @@ const ControlSwapCap = ({
|
|
|
44919
45026
|
width: "50%",
|
|
44920
45027
|
square: true,
|
|
44921
45028
|
children: icon
|
|
44922
|
-
}),
|
|
45029
|
+
}), badgeToDraw ? jsx("span", {
|
|
44923
45030
|
className: "navi_control_swap_badge",
|
|
44924
45031
|
"aria-hidden": "true",
|
|
44925
|
-
children:
|
|
45032
|
+
children: badgeToDraw === true ? null : badgeToDraw
|
|
44926
45033
|
}) : null]
|
|
44927
45034
|
});
|
|
44928
45035
|
};
|
|
@@ -65556,8 +65663,8 @@ const BadgeStyleCSSVars = {
|
|
|
65556
65663
|
fontSize: "--font-size"
|
|
65557
65664
|
};
|
|
65558
65665
|
const BadgeButton = props => {
|
|
65559
|
-
const
|
|
65560
|
-
if (
|
|
65666
|
+
const selfInteractionsHidden = useSelfInteractionsHidden(props);
|
|
65667
|
+
if (selfInteractionsHidden) {
|
|
65561
65668
|
return null;
|
|
65562
65669
|
}
|
|
65563
65670
|
return jsx(BadgeButtonUI, {
|
|
@@ -66317,7 +66424,7 @@ const PickerArrayUI = asPickerOwnUI(() => {
|
|
|
66317
66424
|
* around the chip. `commandFor` is for a chip that stands outside the picker it
|
|
66318
66425
|
* speaks for.
|
|
66319
66426
|
*
|
|
66320
|
-
* The cross
|
|
66427
|
+
* The cross claims the press (see self_interactions.js), so it belongs to it
|
|
66321
66428
|
* and not to the picker underneath, and it goes when the picker turns read-only
|
|
66322
66429
|
* — a row being read still says what was picked, it just no longer offers to
|
|
66323
66430
|
* unpick it.
|
|
@@ -66344,7 +66451,7 @@ const PickerChip = ({
|
|
|
66344
66451
|
flex: true,
|
|
66345
66452
|
...rest,
|
|
66346
66453
|
children: [children, jsx(Badge.Button, {
|
|
66347
|
-
|
|
66454
|
+
selfInteractions: "click",
|
|
66348
66455
|
command: "--navi-unselect",
|
|
66349
66456
|
commandFor: commandFor,
|
|
66350
66457
|
value: value,
|
|
@@ -66721,7 +66828,7 @@ installImportMetaCssBuild(import.meta);const css$t = /* css */`@layer navi {
|
|
|
66721
66828
|
overflow: visible !important;
|
|
66722
66829
|
}
|
|
66723
66830
|
|
|
66724
|
-
& [data-
|
|
66831
|
+
& [data-self-interactions~="click"], & [data-self-interactions~="*"] {
|
|
66725
66832
|
pointer-events: auto;
|
|
66726
66833
|
position: relative;
|
|
66727
66834
|
}
|
|
@@ -66992,6 +67099,9 @@ const PickerButton = props => {
|
|
|
66992
67099
|
// Adds a clear button to the right slot, the same one type="search" puts at
|
|
66993
67100
|
// the end of an input: a picker holds a value the user chose, and unsetting
|
|
66994
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.
|
|
66995
67105
|
clearable,
|
|
66996
67106
|
// "Are you sure?" before the cross clears anything. A cross of three
|
|
66997
67107
|
// millimetres at the edge of a touch screen, right where the chevron is
|
|
@@ -67012,6 +67122,17 @@ const PickerButton = props => {
|
|
|
67012
67122
|
const isSingleLine = maxLines === 1;
|
|
67013
67123
|
// Same rule as the root: phrasing content inside a sentence.
|
|
67014
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";
|
|
67015
67136
|
const inputRef = useRef(null);
|
|
67016
67137
|
const [pickerRemainingProps, inputProps, facadeChildrenProps] = useControlFacadeProps({
|
|
67017
67138
|
...props,
|
|
@@ -67040,6 +67161,17 @@ const PickerButton = props => {
|
|
|
67040
67161
|
// clearing being a modification like any other.
|
|
67041
67162
|
const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
|
|
67042
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
|
+
};
|
|
67043
67175
|
return (
|
|
67044
67176
|
/* Read-only crosses into everything the picker is made of: what it really
|
|
67045
67177
|
holds is drawn by controls of their own — in the popup, and on the façade
|
|
@@ -67113,112 +67245,108 @@ const PickerButton = props => {
|
|
|
67113
67245
|
onnavi_request_unselect: e => {
|
|
67114
67246
|
requestPickerListEntry(ref.current, inputRef.current, e, "unselect");
|
|
67115
67247
|
},
|
|
67116
|
-
children: [
|
|
67248
|
+
children: [jsx("span", {
|
|
67117
67249
|
className: "navi_picker_box",
|
|
67118
|
-
children:
|
|
67119
|
-
|
|
67120
|
-
|
|
67121
|
-
|
|
67122
|
-
|
|
67123
|
-
|
|
67124
|
-
|
|
67125
|
-
|
|
67126
|
-
|
|
67127
|
-
|
|
67128
|
-
|
|
67129
|
-
|
|
67130
|
-
|
|
67131
|
-
|
|
67132
|
-
|
|
67133
|
-
|
|
67134
|
-
|
|
67135
|
-
|
|
67136
|
-
|
|
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);
|
|
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;
|
|
67164
67269
|
}
|
|
67165
|
-
|
|
67166
|
-
|
|
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;
|
|
67270
|
+
const uiState = uiStateController.uiState;
|
|
67271
|
+
if (uiState === undefined) {
|
|
67272
|
+
return;
|
|
67181
67273
|
}
|
|
67182
|
-
|
|
67183
|
-
|
|
67184
|
-
|
|
67185
|
-
|
|
67186
|
-
|
|
67187
|
-
|
|
67188
|
-
|
|
67189
|
-
|
|
67190
|
-
|
|
67191
|
-
});
|
|
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;
|
|
67192
67283
|
}
|
|
67193
|
-
|
|
67194
|
-
|
|
67195
|
-
|
|
67196
|
-
|
|
67197
|
-
|
|
67198
|
-
|
|
67199
|
-
|
|
67200
|
-
|
|
67201
|
-
|
|
67202
|
-
|
|
67203
|
-
|
|
67204
|
-
|
|
67205
|
-
|
|
67206
|
-
|
|
67207
|
-
|
|
67208
|
-
|
|
67209
|
-
|
|
67210
|
-
|
|
67211
|
-
|
|
67212
|
-
|
|
67213
|
-
|
|
67214
|
-
|
|
67215
|
-
|
|
67216
|
-
|
|
67217
|
-
|
|
67218
|
-
|
|
67219
|
-
|
|
67220
|
-
|
|
67221
|
-
|
|
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, {
|
|
67222
67350
|
children: jsx(MaxLinesContext.Provider, {
|
|
67223
67351
|
value: maxLines,
|
|
67224
67352
|
children: ui === undefined ? variant === "icon" ?
|
|
@@ -67233,71 +67361,23 @@ const PickerButton = props => {
|
|
|
67233
67361
|
}) : jsx(PickerDefaultUI, {}) : ui
|
|
67234
67362
|
})
|
|
67235
67363
|
})
|
|
67236
|
-
})
|
|
67237
|
-
|
|
67238
|
-
|
|
67239
|
-
|
|
67240
|
-
|
|
67241
|
-
|
|
67242
|
-
|
|
67243
|
-
|
|
67244
|
-
|
|
67245
|
-
|
|
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, {
|
|
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, {
|
|
67286
67374
|
size: rightSlotIconSize,
|
|
67287
67375
|
lineOverflow: "allow",
|
|
67288
|
-
children: jsx(
|
|
67289
|
-
})
|
|
67290
|
-
})
|
|
67291
|
-
|
|
67292
|
-
|
|
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
|
-
})]
|
|
67376
|
+
children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
|
|
67377
|
+
}) : rightSlot
|
|
67378
|
+
})
|
|
67379
|
+
}) : null]
|
|
67380
|
+
})
|
|
67301
67381
|
}), jsx(ControlFacadeChildrenWrapper, {
|
|
67302
67382
|
...facadeChildrenProps,
|
|
67303
67383
|
children: jsx(ContentTag, {
|
|
@@ -67310,6 +67390,123 @@ const PickerButton = props => {
|
|
|
67310
67390
|
})
|
|
67311
67391
|
);
|
|
67312
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
|
+
|
|
67313
67510
|
// What the picker draws itself — the value it shows, the furniture in its slot,
|
|
67314
67511
|
// and whatever a caller puts in either — is not another control of the field
|
|
67315
67512
|
// around it: none of it may take the id (nor the name) a <Field> hands down,
|
|
@@ -67500,6 +67697,7 @@ const PickerFirstResolver = props => {
|
|
|
67500
67697
|
};
|
|
67501
67698
|
const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerConfirmResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
|
|
67502
67699
|
Picker.Chip = PickerChip;
|
|
67700
|
+
Picker.Clear = PickerClear;
|
|
67503
67701
|
Picker.UI = PickerDefaultUI;
|
|
67504
67702
|
Picker.UI.Date = PickerDateUI;
|
|
67505
67703
|
Picker.UI.Time = PickerTimeUI;
|