@jsenv/navi 0.29.63 → 0.29.65

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.
@@ -693,9 +693,33 @@ const getConfirmParams = (element) => {
693
693
  if (!element) {
694
694
  return undefined;
695
695
  }
696
+ if (confirmAnsweredSet.has(element)) {
697
+ return undefined;
698
+ }
696
699
  return confirmParamsWeakMap.get(element);
697
700
  };
698
701
 
702
+ const confirmAnsweredSet = new WeakSet();
703
+
704
+ /**
705
+ * One press asks its question once. What a press sets off can come back through
706
+ * the action path with the same element as requester — clearing a picker sends
707
+ * it, and the send reads the question off whoever asked for it — so whoever
708
+ * already asked says so, for as long as that press lasts.
709
+ *
710
+ * @param {Element} element - The one that was asked.
711
+ * @returns {() => void} Call it when the press is over.
712
+ */
713
+ const suspendConfirmParams = (element) => {
714
+ if (!element) {
715
+ return () => {};
716
+ }
717
+ confirmAnsweredSet.add(element);
718
+ return () => {
719
+ confirmAnsweredSet.delete(element);
720
+ };
721
+ };
722
+
699
723
  /**
700
724
  * @param {object} params
701
725
  * @param {string|import("preact").ComponentChildren} [params.message]
@@ -15727,6 +15751,14 @@ const useExecuteAction = (
15727
15751
  // so the validation message appears on the button, not the form.
15728
15752
  const element = elementRef.current;
15729
15753
  let target = requester;
15754
+ // A requester that is no longer on the page is not a place to show
15755
+ // anything: the clear cross leaves with the value it optimistically
15756
+ // cleared, and by the time the refusal comes back there is nothing left to
15757
+ // point at — the callout would anchor on a detached node. The control that
15758
+ // ran the action is still there, and the error is about it, so it takes it.
15759
+ if (target && !target.isConnected) {
15760
+ target = undefined;
15761
+ }
15730
15762
  let message;
15731
15763
  if (errorMapping) {
15732
15764
  const errorMappingResult = errorMapping(error);
@@ -22671,34 +22703,122 @@ registerNaviCommand("--navi-clear", (source, event) => {
22671
22703
  // moment a value is chosen — has nothing that would commit a clear: its
22672
22704
  // action never runs on a ui state change. Left alone, the field goes empty
22673
22705
  // while the caller still holds the value it gave, and renders it right back.
22706
+ //
22707
+ // Unless the source committed the clear itself: a clear button given its own
22708
+ // action (`<Button action={remove} command="--navi-clear">`) already made the
22709
+ // request — the ui state is being brought in line with what just happened, and
22710
+ // sending the picker's own action on top would ask twice.
22711
+ const sourceController = source.__uiStateController__;
22674
22712
  const fromSendOnlyControl = Boolean(
22675
- source.closest?.(`[navi-control=picker]`),
22713
+ source.closest?.(`[navi-control=picker]`) &&
22714
+ !sourceController?.props.action,
22676
22715
  );
22677
22716
 
22678
- return {
22679
- target,
22680
- implementation: () => {
22681
- dispatchRequestInteraction(target, {
22682
- event,
22683
- name: "--navi-clear",
22684
- prevented: () => event.preventDefault(),
22685
- allowed: () => {
22686
- dispatchRequestClearUIState(target, event);
22687
- if (fromSendOnlyControl) {
22688
- // After the clear, never before: the action is bound to the ui
22689
- // state signal, so this sends the value the control now holds.
22690
- triggerNaviCommand(source, "--navi-send", event, {
22691
- optional: true,
22692
- });
22717
+ const performClear = (clearEvent) => {
22718
+ dispatchRequestInteraction(target, {
22719
+ event: clearEvent,
22720
+ name: "--navi-clear",
22721
+ prevented: () => clearEvent.preventDefault(),
22722
+ allowed: () => {
22723
+ // What the control holds, before it holds nothing: the clear is
22724
+ // optimistic the field empties now and the send that commits it may
22725
+ // still fail — so what it emptied has to be kept to be put back.
22726
+ const uiStateBefore = getUIStateFromElement(target);
22727
+ dispatchRequestClearUIState(target, clearEvent);
22728
+ if (!fromSendOnlyControl) {
22729
+ return;
22730
+ }
22731
+ // After the clear, never before: the action is bound to the ui
22732
+ // state signal, so this sends the value the control now holds.
22733
+ const actionHost = findControlHost(target) || target;
22734
+ const completion = watchActionCompletion(actionHost, () => {
22735
+ triggerNaviCommand(source, "--navi-send", clearEvent, {
22736
+ optional: true,
22737
+ });
22738
+ });
22739
+ completion.whenSettled(({ error, aborted }) => {
22740
+ if (!error && !aborted) {
22741
+ return;
22693
22742
  }
22694
- },
22743
+ // The removal did not happen, so the field must stop saying it did.
22744
+ // The error itself stays where the action put it — on the control,
22745
+ // which is still there, unlike the cross that has just gone with the
22746
+ // value it cleared (see addErrorMessage in use_execute_action.js).
22747
+ //
22748
+ // Put back from the inside ("clear_rollback" is an internal event
22749
+ // type, see ui_state_controller.js) rather than asked for the way a
22750
+ // user would: nobody acted, the control is being returned to the
22751
+ // state its caller still holds. Asked from the outside it would be
22752
+ // answered with "this element is busy" — the action is still
22753
+ // settling — over the very error that explains why the value is back.
22754
+ const controller = actionHost.__uiStateController__;
22755
+ if (!controller) {
22756
+ return;
22757
+ }
22758
+ const rollbackEvent = new CustomEvent("clear_rollback", {
22759
+ detail: {},
22760
+ });
22761
+ chainEvent(rollbackEvent, clearEvent);
22762
+ controller.setUIState(uiStateBefore, rollbackEvent);
22763
+ });
22764
+ },
22765
+ });
22766
+
22767
+ if (fromInput) ; else if (
22768
+ // Only what is open: the clear cross of a picker sits on the closed
22769
+ // trigger, and asking that trigger to close is asking it to do nothing —
22770
+ // except that the ask goes through the interaction gate, which turns it
22771
+ // down while the clear it just sent is still running and says so out loud
22772
+ // ("this element is busy"). A clear pressed INSIDE an open popup is the
22773
+ // one this is for: it answers the popup, so the popup goes away.
22774
+ resolveClosestExpandable(source)?.getAttribute("aria-expanded") === "true"
22775
+ ) {
22776
+ triggerNaviCommand(source, "--navi-close", clearEvent, {
22777
+ optional: true,
22695
22778
  });
22779
+ }
22780
+ };
22696
22781
 
22697
- if (fromInput) ; else {
22698
- triggerNaviCommand(source, "--navi-close", event, {
22699
- optional: true,
22700
- });
22782
+ return {
22783
+ target,
22784
+ implementation: () => {
22785
+ // "Are you sure?" comes before anything is cleared. It is asked here
22786
+ // rather than by the action the clear ends up sending (which is where a
22787
+ // confirmation is normally asked, see use_execute_action) because that
22788
+ // one only runs AFTER the ui state was emptied: the field would go blank
22789
+ // behind the question, and answering "no" would leave it blank over a
22790
+ // value that was never removed.
22791
+ const confirmParams = getConfirmParams(source);
22792
+ if (!confirmParams) {
22793
+ performClear(event);
22794
+ return;
22701
22795
  }
22796
+ requestConfirmation({
22797
+ ...confirmParams,
22798
+ anchor: source,
22799
+ }).then((confirmed) => {
22800
+ if (!confirmed) {
22801
+ return;
22802
+ }
22803
+ // A new event, chained to the press: the press itself is over and has
22804
+ // been consumed — the control that took the click cancelled it on its
22805
+ // way out so the region around it would not read it as "unfold me" (see
22806
+ // click_to_expand.js), and navi refuses an interaction on a cancelled
22807
+ // event. What happens now happens BECAUSE of that press, which is what
22808
+ // the chain says, but it is no longer that press.
22809
+ const clearConfirmedEvent = new CustomEvent("navi_clear_confirmed", {
22810
+ detail: {},
22811
+ });
22812
+ chainEvent(clearConfirmedEvent, event);
22813
+ // Answered — and the send that follows must not ask it again: it reads
22814
+ // the same question off this same element (getConfirmParams(requester)).
22815
+ const restoreConfirmParams = suspendConfirmParams(source);
22816
+ try {
22817
+ performClear(clearConfirmedEvent);
22818
+ } finally {
22819
+ restoreConfirmParams();
22820
+ }
22821
+ });
22702
22822
  },
22703
22823
  };
22704
22824
  });
@@ -23907,6 +24027,10 @@ const useUIStateController = (
23907
24027
  defaultValue: controlInfo.defaultValue,
23908
24028
 
23909
24029
  facadeChild: null,
24030
+ // Set for the duration of one interaction by whatever wants the
24031
+ // command to wait for something (a button's own action) — see the
24032
+ // command trigger in onUIAction below.
24033
+ commandDeferral: null,
23910
24034
  getManagedControls: () => {
23911
24035
  if (controller.facadeChild) {
23912
24036
  const child = controller.facadeChild;
@@ -23962,7 +24086,19 @@ const useUIStateController = (
23962
24086
  debugUIState(
23963
24087
  `triggering command "${command}" for "${controlType}"`,
23964
24088
  );
23965
- triggerNaviCommand(element, command, e);
24089
+ const runCommand = () => {
24090
+ triggerNaviCommand(element, command, e);
24091
+ };
24092
+ // What the press means may not be due yet: a button with an
24093
+ // action of its own runs the work first and lets its command
24094
+ // follow only once that work succeeded (see control_hooks).
24095
+ // The command is handed over rather than run; nobody claiming
24096
+ // it means now.
24097
+ if (controller.commandDeferral) {
24098
+ controller.commandDeferral(runCommand);
24099
+ } else {
24100
+ runCommand();
24101
+ }
23966
24102
  }
23967
24103
  }
23968
24104
  }
@@ -25455,6 +25591,12 @@ const INTERNAL_EVENT_SET = new Set([
25455
25591
  // on registration, and group pushing value/defaultValue to children on registerChild.
25456
25592
  // Equivalent to defaultValue initialization: no uiAction, no commands, no parent notification.
25457
25593
  "initial_state_push",
25594
+ // navi undoing its own optimistic write: the clear cross emptied the control
25595
+ // before the send that commits it, the send failed, and the value it emptied
25596
+ // goes back where it was (see the --navi-clear command). Nothing acted — the
25597
+ // control is being put back on the state the caller still holds — so this
25598
+ // must not fire a uiAction, a command, or a report on the way.
25599
+ "clear_rollback",
25458
25600
  ]);
25459
25601
  const isInternalEvent = (e) => {
25460
25602
  return INTERNAL_EVENT_SET.has(e.type);
@@ -25790,7 +25932,30 @@ const useControlProps = (props, {
25790
25932
  }
25791
25933
  if (controlType === "button") {
25792
25934
  const onButtonInteractionAllowed = e => {
25793
- triggerUIAction(e);
25935
+ // A command that follows the control's OWN action has to wait for it:
25936
+ // `<Button action={remove} command="--navi-clear">` means "remove it,
25937
+ // then clear the field" — clearing first would empty the field over a
25938
+ // request that can still fail, and a confirm popup refusing the action
25939
+ // would leave the clear standing. Same rule the form counterpart
25940
+ // already has (data-after-send, see resolveAfterSend in commands.js).
25941
+ //
25942
+ // Armed here, around the ui action, because the command fires from
25943
+ // there (see the command trigger in ui_state_controller). A button is
25944
+ // where this arises: its press IS the ui action, the action and the
25945
+ // command, in one breath. A field's ui action and its action are two
25946
+ // different moments (typing, then change/blur), and its command
25947
+ // belongs to the first — there is nothing to wait for.
25948
+ let deferredCommand = null;
25949
+ if (props.action && props.command) {
25950
+ uiStateController.commandDeferral = runCommand => {
25951
+ deferredCommand = runCommand;
25952
+ };
25953
+ }
25954
+ try {
25955
+ triggerUIAction(e);
25956
+ } finally {
25957
+ uiStateController.commandDeferral = null;
25958
+ }
25794
25959
  const control = ref.current;
25795
25960
  if (!control) {
25796
25961
  // What the button just did took the button away: a command that
@@ -25799,11 +25964,36 @@ const useControlProps = (props, {
25799
25964
  // press was for has already happened.
25800
25965
  return;
25801
25966
  }
25802
- tryActionAfterInteractionAllowed(control, {
25967
+ const completion = watchActionCompletion(control, () => tryActionAfterInteractionAllowed(control, {
25803
25968
  event: e,
25804
25969
  action: boundAction,
25805
25970
  requester: control
25971
+ }));
25972
+ if (!deferredCommand) {
25973
+ return;
25974
+ }
25975
+ if (completion.result === false) {
25976
+ // The action was turned down (a failing constraint, a gate saying
25977
+ // no) — nothing happened, so nothing follows.
25978
+ return;
25979
+ }
25980
+ if (completion.isRunning) {
25981
+ completion.whenSucceeded(deferredCommand);
25982
+ return;
25983
+ }
25984
+ // Synchronous: already settled, and how it ended still decides. An
25985
+ // action that never started (nothing to run) leaves no outcome, and
25986
+ // the command runs as it always did.
25987
+ let succeeded = true;
25988
+ completion.whenSettled(({
25989
+ error,
25990
+ aborted
25991
+ }) => {
25992
+ succeeded = !error && !aborted;
25806
25993
  });
25994
+ if (succeeded) {
25995
+ deferredCommand();
25996
+ }
25807
25997
  };
25808
25998
  return {
25809
25999
  keyDown: keyDownDefault,
@@ -28324,7 +28514,6 @@ const createOpenController = (
28324
28514
  cancelable: true,
28325
28515
  });
28326
28516
  chainEvent(requestOpenEvent, e);
28327
- controller.opened = true;
28328
28517
  // we prepare focus transfer before actually opening the popover/dialog
28329
28518
  // because opnening dialog makes browser try to transfer focus (which ends up in document.body for instance)
28330
28519
  const focusTransfer = prepareFocusTransfer(
@@ -28372,6 +28561,18 @@ const createOpenController = (
28372
28561
  // before anything inside the popup can claim it, and before openEffect,
28373
28562
  // which measures the popup to place it.
28374
28563
  controller.mountContent?.();
28564
+ // Only now — after the content has been built, before openEffect shows
28565
+ // it. Dialog/Popover recompute aria-expanded and navi-hidden from this
28566
+ // flag on every render, and mountContent above renders synchronously:
28567
+ // flipping it any earlier commits an already-open DOM (aria-expanded
28568
+ // "true", navi-hidden gone) before openEffect has run a single
28569
+ // statement, so the "closed" frame it pins to transition from is in
28570
+ // fact the open one and the entrance animation has nothing to play.
28571
+ // It also gives the content it just built the opening it is documented
28572
+ // to observe — mounted while the popup reads as closed, told it opened
28573
+ // right after (see popup_content_mount.js and
28574
+ // use_displayed_layout_effect.js).
28575
+ controller.opened = true;
28375
28576
  const openEffectReturnValue =
28376
28577
  controller.openEffect(requestOpenEvent) || null;
28377
28578
  openEffectCleanup = (closeEvent) => {
@@ -29696,9 +29897,12 @@ const css$W = /* css */`
29696
29897
  backdrop-filter: none;
29697
29898
  }
29698
29899
 
29900
+ /* overlay is a no-op here (this backdrop is a plain div, never a
29901
+ top-layer element) but stays in the list so all four transition lists in
29902
+ these files agree — display and overlay always travel together. */
29699
29903
  &[navi-animation] {
29700
29904
  opacity: 1;
29701
- transition-property: display, opacity;
29905
+ transition-property: display, overlay, opacity;
29702
29906
  transition-duration: var(--popup-animation-duration);
29703
29907
  transition-timing-function: ease;
29704
29908
  transition-behavior: allow-discrete;
@@ -29802,6 +30006,11 @@ const css$W = /* css */`
29802
30006
  * triggered the open (`e.detail.anchor`), if any. A string is resolved via
29803
30007
  * `document.getElementById` when the dialog opens — see popover.jsx's own
29804
30008
  * `anchor` doc for why (mainly `defaultOpen`).
30009
+ * @param {"override"|"ignore"} [props.anchorCustomEventDetail="override"] -
30010
+ * Whether an explicit `anchor` prop takes precedence over (`"override"`,
30011
+ * default) or is ignored in favor of (`"ignore"`) whatever anchor the
30012
+ * triggering event carried. Same prop as Popover's, applied to the only
30013
+ * thing an anchor does here: sizing (`--anchor-width`/`--anchor-height`).
29805
30014
  * @param {string} [props.minWidth] - Maps to `--dialog-min-width`; clamped
29806
30015
  * so it can never push the dialog past `--dialog-maxmax-width` (the
29807
30016
  * viewport/container-spacing ceiling) regardless of how large a value is
@@ -30063,6 +30272,10 @@ const useDialogProps = props => {
30063
30272
  // Only ever affects --anchor-width/--anchor-height (see this file's top
30064
30273
  // comment) — Dialog's own positioning is never relative to it.
30065
30274
  anchor,
30275
+ // Same meaning as Popover's own prop, applied to the only thing an anchor
30276
+ // does here: sizing. "ignore" is how a dialog that must not inherit its
30277
+ // trigger's width says so (SidePanel does exactly that).
30278
+ anchorCustomEventDetail = "override",
30066
30279
  // Makes the dialog itself a valid focus target so
30067
30280
  // autoFocus="last-resort" below has somewhere to land when it contains
30068
30281
  // nothing focusable of its own — -1 keeps it out of the normal Tab order (it's only ever reached
@@ -30242,9 +30455,10 @@ const useDialogProps = props => {
30242
30455
  console.warn(`Dialog: anchor="${anchor}" did not match any element`);
30243
30456
  }
30244
30457
  } else if (anchor) {
30245
- // anchor prop is a ref or a DOM element
30458
+ // anchor prop is a ref or a DOM element — always a real anchor,
30459
+ // regardless of anchorCustomEventDetail.
30246
30460
  anchorElement = anchor.current ?? anchor;
30247
- } else if (e.detail.anchor) {
30461
+ } else if (anchorCustomEventDetail === "override") {
30248
30462
  // e.g. the button that triggered a --navi-toggle/--navi-open command,
30249
30463
  // already resolved from detail.anchor/detail.source by the caller
30250
30464
  // (see UncontrolledDialog's onnavi_request_open).
@@ -31094,12 +31308,16 @@ const css$V = /* css */`
31094
31308
  /* navi-animation mirrors the content popover's own resolved value (set
31095
31309
  imperatively in openEffect) — the backdrop only ever fades, regardless
31096
31310
  of which kind it is (translate/scale wouldn't mean anything on it).
31097
- display is included alongside opacity (+ allow-discrete) for the same
31098
- reason popup_css.js's own .navi_popover rule includes it — see this
31099
- file's top comment. */
31311
+ display and overlay are included alongside opacity (+ allow-discrete)
31312
+ for the same reason popup_css.js's own .navi_popover rule includes them
31313
+ — see this file's top comment. They go together: display keeps the
31314
+ backdrop rendered during the exit, overlay keeps it in the top layer
31315
+ (hidePopover() is called synchronously, so without it the backdrop is
31316
+ evicted immediately and, being z-index: unset, loses to FixedBar and to
31317
+ any positioned element later in DOM order for the whole fade). */
31100
31318
  &[navi-animation] {
31101
31319
  opacity: 1;
31102
- transition-property: display, opacity;
31320
+ transition-property: display, overlay, opacity;
31103
31321
  transition-duration: var(--popup-animation-duration);
31104
31322
  transition-timing-function: ease;
31105
31323
  transition-behavior: allow-discrete;
@@ -52746,10 +52964,10 @@ installImportMetaCssBuild(import.meta);/**
52746
52964
  * the hook's own `resetMode` return value, from its own onClose.
52747
52965
  *
52748
52966
  * `layer` (shared by both — picks the top-layer vs. local-container rendering
52749
- * strategy either way) and `anchorCustomEventDetail` (Popover-only, Dialog
52750
- * ignores it Dialog never resolves an anchor for positioning purposes)
52751
- * pass through untouched via `...rest` to whichever of Popover/Dialog
52752
- * actually renders.
52967
+ * strategy either way) and `anchorCustomEventDetail` (shared too: Popover
52968
+ * resolves an anchor to position against, Dialog to size itself from) pass
52969
+ * through untouched via `...rest` to whichever of Popover/Dialog actually
52970
+ * renders.
52753
52971
  */
52754
52972
  const css$A = /* css */`
52755
52973
  @layer navi {
@@ -52789,12 +53007,14 @@ const css$A = /* css */`
52789
53007
  * @param {Element|{current: Element}} [props.anchor] - Forwarded as-is —
52790
53008
  * sizing-only for `Dialog`, positioning for `Popover` (see each
52791
53009
  * component's own doc for what it actually does there).
52792
- * @param {"override"|"ignore"} [props.anchorCustomEventDetail] -
52793
- * **Popover-only** (`Dialog` never resolves an anchor for positioning)
52794
- * never forwarded to `Dialog`, so it can't leak onto the real `<dialog>`
52795
- * element as a stray DOM attribute when `mode="dialog"` is picked.
52796
- * @param {string} [props.marginWithAnchor] - **Popover-only**, same
52797
- * Dialog-leak guard as `anchorCustomEventDetail` above.
53010
+ * @param {"override"|"ignore"} [props.anchorCustomEventDetail] - Forwarded
53011
+ * as-is to both what it governs differs (positioning for `Popover`,
53012
+ * sizing for `Dialog`), but "ignore whatever anchor the triggering event
53013
+ * carried" has to mean the same thing in either mode, or the same
53014
+ * `<Popup>` usage silently picks up its trigger's width on small screens.
53015
+ * @param {string} [props.marginWithAnchor] - **Popover-only**, destructured
53016
+ * out so it can't leak onto the real `<dialog>` element as a stray DOM
53017
+ * attribute when `mode="dialog"` is picked.
52798
53018
  * @param {boolean} [props.focusCapture] - **Popover-only**, same guard.
52799
53019
  * @param {string} [props.positionAreaFixed] - **Popover-only**, same guard.
52800
53020
  * @param {string} [props.positionArea] - Forwarded as-is — `Dialog` and
@@ -52882,7 +53102,6 @@ const Popup = props => {
52882
53102
  // they're never part of ...rest, and therefore never forwarded to
52883
53103
  // Dialog below, where they'd otherwise leak onto the real <dialog>
52884
53104
  // element as stray, unrecognized DOM attributes.
52885
- anchorCustomEventDetail,
52886
53105
  marginWithAnchor,
52887
53106
  focusCapture,
52888
53107
  scrollCapture,
@@ -52917,7 +53136,6 @@ const Popup = props => {
52917
53136
  ...rest,
52918
53137
  maxWidth: maxWidth,
52919
53138
  pointerInteractionOutsideEffect: pointerInteractionOutsideEffect,
52920
- anchorCustomEventDetail: anchorCustomEventDetail,
52921
53139
  marginWithAnchor: marginWithAnchor,
52922
53140
  focusCapture: focusCapture,
52923
53141
  scrollCapture: scrollCapture === "popover" || scrollCapture,
@@ -60640,6 +60858,11 @@ const PickerButton = props => {
60640
60858
  variant,
60641
60859
  rightSlotIcon,
60642
60860
  rightSlotIconSize = "inherit",
60861
+ // What goes in the right slot as-is — no <Icon> around it, so a caller can
60862
+ // put something interactive there. `rightSlotIcon` cannot: it is wrapped in
60863
+ // an <Icon>, which is aria-hidden, and a focusable node under aria-hidden is
60864
+ // invisible to assistive tech while still being reachable by tab.
60865
+ rightSlot,
60643
60866
  placeholder,
60644
60867
  ui,
60645
60868
  maxLines = 1,
@@ -60652,6 +60875,13 @@ const PickerButton = props => {
60652
60875
  // the end of an input: a picker holds a value the user chose, and unsetting
60653
60876
  // it should not require reopening the popup to hunt for a "none" entry.
60654
60877
  clearable,
60878
+ // "Are you sure?" before the cross clears anything. A cross of three
60879
+ // millimetres at the edge of a touch screen, right where the chevron is
60880
+ // aimed at, is the button pressed by accident — and what it removes does
60881
+ // not come back. Asked before the clear, so a "no" leaves the field
60882
+ // untouched (see the --navi-clear command).
60883
+ clearConfirm,
60884
+ clearConfirmPopupContent,
60655
60885
  error
60656
60886
  } = props;
60657
60887
  const isSingleLine = maxLines === 1;
@@ -60694,6 +60924,9 @@ const PickerButton = props => {
60694
60924
  variant: undefined,
60695
60925
  rightSlotIcon: undefined,
60696
60926
  rightSlotIconSize: undefined,
60927
+ rightSlot: undefined,
60928
+ clearConfirm: undefined,
60929
+ clearConfirmPopupContent: undefined,
60697
60930
  ui: undefined,
60698
60931
  maxLines: undefined,
60699
60932
  popupWidthFitContent: undefined,
@@ -60808,40 +61041,59 @@ const PickerButton = props => {
60808
61041
  })
60809
61042
  }), variant === "headless" || ui === "default" ? null : jsx("span", {
60810
61043
  className: "navi_picker_right_slot",
60811
- children: clearable && value !== undefined && value !== "" ? jsx(Button, {
60812
- command: "--navi-clear",
60813
- commandFor: inputProps.id,
60814
- tabIndex: "-1"
60815
- // No navi-focus-delegate, unlike the identical button inside an
60816
- // input: handing focus back to the picker's own input is what
60817
- // opens the popup, and clearing is the opposite intention.
60818
- ,
60819
-
60820
- icon: true,
60821
- variant: "discrete"
60822
- // preventDefault, not just tabIndex="-1": a mousedown focuses
60823
- // its target before any click happens, and this button should
60824
- // never hold focus at all — the field keeps it.
60825
- ,
60826
-
60827
- onMouseDown: e => {
60828
- e.preventDefault();
60829
- },
60830
- flex: true,
60831
- align: "center",
60832
- children: jsx(Icon, {
60833
- size: rightSlotIconSize,
60834
- lineOverflow: "allow",
60835
- children: jsx(CloseSvg, {})
61044
+ children: jsx(ControlIdContext.Provider, {
61045
+ value: undefined,
61046
+ children: jsx(ControlNameContext.Provider, {
61047
+ value: undefined,
61048
+ children: clearable && value !== undefined && value !== "" ? jsx(Button, {
61049
+ command: "--navi-clear",
61050
+ commandFor: inputProps.id
61051
+ // The question, asked before the clear rather than by the
61052
+ // action the clear sends — see the --navi-clear command.
61053
+ ,
61054
+
61055
+ confirm: clearConfirm,
61056
+ confirmPopupContent: clearConfirmPopupContent,
61057
+ tabIndex: "-1"
61058
+ // No navi-focus-delegate, unlike the identical button inside an
61059
+ // input: handing focus back to the picker's own input is what
61060
+ // opens the popup, and clearing is the opposite intention.
61061
+ ,
61062
+
61063
+ icon: true,
61064
+ variant: "discrete"
61065
+ // What is busy once the clear is sent is the picker — the value
61066
+ // being removed is the whole field's, and the picker already
61067
+ // draws the wait around all of it. Two outlines for one wait is
61068
+ // one too many.
61069
+ ,
61070
+
61071
+ loadingOutline: false
61072
+ // preventDefault, not just tabIndex="-1": a mousedown focuses
61073
+ // its target before any click happens, and this button should
61074
+ // never hold focus at all — the field keeps it.
61075
+ ,
61076
+
61077
+ onMouseDown: e => {
61078
+ e.preventDefault();
61079
+ },
61080
+ flex: true,
61081
+ align: "center",
61082
+ children: jsx(Icon, {
61083
+ size: rightSlotIconSize,
61084
+ lineOverflow: "allow",
61085
+ children: jsx(CloseSvg, {})
61086
+ })
61087
+ }) : rightSlot === undefined ?
61088
+ // lineOverflow: what sits in the slot is an affordance, not a
61089
+ // character — a caller asking for a bigger one wants it bigger,
61090
+ // not capped at the height of the line it sits on
61091
+ jsx(Icon, {
61092
+ size: rightSlotIconSize,
61093
+ lineOverflow: "allow",
61094
+ children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
61095
+ }) : rightSlot
60836
61096
  })
60837
- }) :
60838
- // lineOverflow: what sits in the slot is an affordance, not a
60839
- // character — a caller asking for a bigger one wants it bigger,
60840
- // not capped at the height of the line it sits on
60841
- jsx(Icon, {
60842
- size: rightSlotIconSize,
60843
- lineOverflow: "allow",
60844
- children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
60845
61097
  })
60846
61098
  })]
60847
61099
  }), jsx(ControlFacadeChildrenWrapper, {
@@ -72275,19 +72527,13 @@ installImportMetaCssBuild(import.meta);/**
72275
72527
  * `navi-side`/`data-layer` attributes) rather than computed in JS — read
72276
72528
  * the CSS block below instead of expecting a JS equivalent of it here.
72277
72529
  *
72278
- * `anchorCustomEventDetail="ignore"` is required, not cosmetic: without it,
72279
- * Popover would dock next to whatever triggered the open instead of flush
72280
- * against the edge, defeating the point of a side panel.
72530
+ * `anchorCustomEventDetail="ignore"` is required, not cosmetic, and in both
72531
+ * modes: without it Popover docks next to whatever triggered the open instead
72532
+ * of flush against the edge, and Dialog takes that trigger's width as its own
72533
+ * `min-width` floor (`--anchor-width`), overriding the `width` prop.
72281
72534
  */
72282
72535
  const css = /* css */`
72283
72536
  .navi_side_panel {
72284
- /* Dialog's own \`min-width: var(--anchor-width, 0px)\` exists so a
72285
- dialog naturally matches whatever triggered it (picker_custom.jsx's
72286
- dialog mode relies on this) — SidePanel's own "anchor" is just its
72287
- container, so this would otherwise force min-width to the full
72288
- container width, overriding \`width\`/\`height\` below entirely. Popover
72289
- ignores this var. */
72290
- --anchor-width: 0px;
72291
72537
  /* Side panel create a barriere with the content that is full size */
72292
72538
  /* So by default they don't have border-radius */
72293
72539
  --popup-border-radius: 0px;