@jsenv/navi 0.29.62 → 0.29.64

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.
@@ -3,7 +3,7 @@
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
- export { coarsePointerSignal } from "./jsenv_navi_side_effects.js";
6
+ export { coarsePointerSignal, disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
7
  import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
8
8
  export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
@@ -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);
@@ -16829,8 +16861,11 @@ const DIMENSION_PROPS = {
16829
16861
  return { flexGrow: expandWeight(value), flexBasis: "0%" };
16830
16862
  }
16831
16863
  if (parentBoxFlow === "flex-y" || parentBoxFlow === "inline-flex-y") {
16864
+ // width 100% is what fills the cross axis; align-self stretch would do
16865
+ // the same but takes the parent's alignX away — a maxWidth-capped item
16866
+ // in an alignX="center" parent would then sit at the start instead of
16867
+ // in the middle.
16832
16868
  return {
16833
- alignSelf: "stretch",
16834
16869
  width: "100%",
16835
16870
  };
16836
16871
  }
@@ -22668,34 +22703,122 @@ registerNaviCommand("--navi-clear", (source, event) => {
22668
22703
  // moment a value is chosen — has nothing that would commit a clear: its
22669
22704
  // action never runs on a ui state change. Left alone, the field goes empty
22670
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__;
22671
22712
  const fromSendOnlyControl = Boolean(
22672
- source.closest?.(`[navi-control=picker]`),
22713
+ source.closest?.(`[navi-control=picker]`) &&
22714
+ !sourceController?.props.action,
22673
22715
  );
22674
22716
 
22675
- return {
22676
- target,
22677
- implementation: () => {
22678
- dispatchRequestInteraction(target, {
22679
- event,
22680
- name: "--navi-clear",
22681
- prevented: () => event.preventDefault(),
22682
- allowed: () => {
22683
- dispatchRequestClearUIState(target, event);
22684
- if (fromSendOnlyControl) {
22685
- // After the clear, never before: the action is bound to the ui
22686
- // state signal, so this sends the value the control now holds.
22687
- triggerNaviCommand(source, "--navi-send", event, {
22688
- optional: true,
22689
- });
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;
22690
22742
  }
22691
- },
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,
22692
22778
  });
22779
+ }
22780
+ };
22693
22781
 
22694
- if (fromInput) ; else {
22695
- triggerNaviCommand(source, "--navi-close", event, {
22696
- optional: true,
22697
- });
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;
22698
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
+ });
22699
22822
  },
22700
22823
  };
22701
22824
  });
@@ -23904,6 +24027,10 @@ const useUIStateController = (
23904
24027
  defaultValue: controlInfo.defaultValue,
23905
24028
 
23906
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,
23907
24034
  getManagedControls: () => {
23908
24035
  if (controller.facadeChild) {
23909
24036
  const child = controller.facadeChild;
@@ -23959,7 +24086,19 @@ const useUIStateController = (
23959
24086
  debugUIState(
23960
24087
  `triggering command "${command}" for "${controlType}"`,
23961
24088
  );
23962
- 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
+ }
23963
24102
  }
23964
24103
  }
23965
24104
  }
@@ -25452,6 +25591,12 @@ const INTERNAL_EVENT_SET = new Set([
25452
25591
  // on registration, and group pushing value/defaultValue to children on registerChild.
25453
25592
  // Equivalent to defaultValue initialization: no uiAction, no commands, no parent notification.
25454
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",
25455
25600
  ]);
25456
25601
  const isInternalEvent = (e) => {
25457
25602
  return INTERNAL_EVENT_SET.has(e.type);
@@ -25787,7 +25932,30 @@ const useControlProps = (props, {
25787
25932
  }
25788
25933
  if (controlType === "button") {
25789
25934
  const onButtonInteractionAllowed = e => {
25790
- 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
+ }
25791
25959
  const control = ref.current;
25792
25960
  if (!control) {
25793
25961
  // What the button just did took the button away: a command that
@@ -25796,11 +25964,36 @@ const useControlProps = (props, {
25796
25964
  // press was for has already happened.
25797
25965
  return;
25798
25966
  }
25799
- tryActionAfterInteractionAllowed(control, {
25967
+ const completion = watchActionCompletion(control, () => tryActionAfterInteractionAllowed(control, {
25800
25968
  event: e,
25801
25969
  action: boundAction,
25802
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;
25803
25993
  });
25994
+ if (succeeded) {
25995
+ deferredCommand();
25996
+ }
25804
25997
  };
25805
25998
  return {
25806
25999
  keyDown: keyDownDefault,
@@ -28321,7 +28514,6 @@ const createOpenController = (
28321
28514
  cancelable: true,
28322
28515
  });
28323
28516
  chainEvent(requestOpenEvent, e);
28324
- controller.opened = true;
28325
28517
  // we prepare focus transfer before actually opening the popover/dialog
28326
28518
  // because opnening dialog makes browser try to transfer focus (which ends up in document.body for instance)
28327
28519
  const focusTransfer = prepareFocusTransfer(
@@ -28369,6 +28561,18 @@ const createOpenController = (
28369
28561
  // before anything inside the popup can claim it, and before openEffect,
28370
28562
  // which measures the popup to place it.
28371
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;
28372
28576
  const openEffectReturnValue =
28373
28577
  controller.openEffect(requestOpenEvent) || null;
28374
28578
  openEffectCleanup = (closeEvent) => {
@@ -29799,6 +30003,11 @@ const css$W = /* css */`
29799
30003
  * triggered the open (`e.detail.anchor`), if any. A string is resolved via
29800
30004
  * `document.getElementById` when the dialog opens — see popover.jsx's own
29801
30005
  * `anchor` doc for why (mainly `defaultOpen`).
30006
+ * @param {"override"|"ignore"} [props.anchorCustomEventDetail="override"] -
30007
+ * Whether an explicit `anchor` prop takes precedence over (`"override"`,
30008
+ * default) or is ignored in favor of (`"ignore"`) whatever anchor the
30009
+ * triggering event carried. Same prop as Popover's, applied to the only
30010
+ * thing an anchor does here: sizing (`--anchor-width`/`--anchor-height`).
29802
30011
  * @param {string} [props.minWidth] - Maps to `--dialog-min-width`; clamped
29803
30012
  * so it can never push the dialog past `--dialog-maxmax-width` (the
29804
30013
  * viewport/container-spacing ceiling) regardless of how large a value is
@@ -30060,6 +30269,10 @@ const useDialogProps = props => {
30060
30269
  // Only ever affects --anchor-width/--anchor-height (see this file's top
30061
30270
  // comment) — Dialog's own positioning is never relative to it.
30062
30271
  anchor,
30272
+ // Same meaning as Popover's own prop, applied to the only thing an anchor
30273
+ // does here: sizing. "ignore" is how a dialog that must not inherit its
30274
+ // trigger's width says so (SidePanel does exactly that).
30275
+ anchorCustomEventDetail = "override",
30063
30276
  // Makes the dialog itself a valid focus target so
30064
30277
  // autoFocus="last-resort" below has somewhere to land when it contains
30065
30278
  // nothing focusable of its own — -1 keeps it out of the normal Tab order (it's only ever reached
@@ -30239,9 +30452,10 @@ const useDialogProps = props => {
30239
30452
  console.warn(`Dialog: anchor="${anchor}" did not match any element`);
30240
30453
  }
30241
30454
  } else if (anchor) {
30242
- // anchor prop is a ref or a DOM element
30455
+ // anchor prop is a ref or a DOM element — always a real anchor,
30456
+ // regardless of anchorCustomEventDetail.
30243
30457
  anchorElement = anchor.current ?? anchor;
30244
- } else if (e.detail.anchor) {
30458
+ } else if (anchorCustomEventDetail === "override") {
30245
30459
  // e.g. the button that triggered a --navi-toggle/--navi-open command,
30246
30460
  // already resolved from detail.anchor/detail.source by the caller
30247
30461
  // (see UncontrolledDialog's onnavi_request_open).
@@ -52743,10 +52957,10 @@ installImportMetaCssBuild(import.meta);/**
52743
52957
  * the hook's own `resetMode` return value, from its own onClose.
52744
52958
  *
52745
52959
  * `layer` (shared by both — picks the top-layer vs. local-container rendering
52746
- * strategy either way) and `anchorCustomEventDetail` (Popover-only, Dialog
52747
- * ignores it Dialog never resolves an anchor for positioning purposes)
52748
- * pass through untouched via `...rest` to whichever of Popover/Dialog
52749
- * actually renders.
52960
+ * strategy either way) and `anchorCustomEventDetail` (shared too: Popover
52961
+ * resolves an anchor to position against, Dialog to size itself from) pass
52962
+ * through untouched via `...rest` to whichever of Popover/Dialog actually
52963
+ * renders.
52750
52964
  */
52751
52965
  const css$A = /* css */`
52752
52966
  @layer navi {
@@ -52786,12 +53000,14 @@ const css$A = /* css */`
52786
53000
  * @param {Element|{current: Element}} [props.anchor] - Forwarded as-is —
52787
53001
  * sizing-only for `Dialog`, positioning for `Popover` (see each
52788
53002
  * component's own doc for what it actually does there).
52789
- * @param {"override"|"ignore"} [props.anchorCustomEventDetail] -
52790
- * **Popover-only** (`Dialog` never resolves an anchor for positioning)
52791
- * never forwarded to `Dialog`, so it can't leak onto the real `<dialog>`
52792
- * element as a stray DOM attribute when `mode="dialog"` is picked.
52793
- * @param {string} [props.marginWithAnchor] - **Popover-only**, same
52794
- * Dialog-leak guard as `anchorCustomEventDetail` above.
53003
+ * @param {"override"|"ignore"} [props.anchorCustomEventDetail] - Forwarded
53004
+ * as-is to both what it governs differs (positioning for `Popover`,
53005
+ * sizing for `Dialog`), but "ignore whatever anchor the triggering event
53006
+ * carried" has to mean the same thing in either mode, or the same
53007
+ * `<Popup>` usage silently picks up its trigger's width on small screens.
53008
+ * @param {string} [props.marginWithAnchor] - **Popover-only**, destructured
53009
+ * out so it can't leak onto the real `<dialog>` element as a stray DOM
53010
+ * attribute when `mode="dialog"` is picked.
52795
53011
  * @param {boolean} [props.focusCapture] - **Popover-only**, same guard.
52796
53012
  * @param {string} [props.positionAreaFixed] - **Popover-only**, same guard.
52797
53013
  * @param {string} [props.positionArea] - Forwarded as-is — `Dialog` and
@@ -52879,7 +53095,6 @@ const Popup = props => {
52879
53095
  // they're never part of ...rest, and therefore never forwarded to
52880
53096
  // Dialog below, where they'd otherwise leak onto the real <dialog>
52881
53097
  // element as stray, unrecognized DOM attributes.
52882
- anchorCustomEventDetail,
52883
53098
  marginWithAnchor,
52884
53099
  focusCapture,
52885
53100
  scrollCapture,
@@ -52914,7 +53129,6 @@ const Popup = props => {
52914
53129
  ...rest,
52915
53130
  maxWidth: maxWidth,
52916
53131
  pointerInteractionOutsideEffect: pointerInteractionOutsideEffect,
52917
- anchorCustomEventDetail: anchorCustomEventDetail,
52918
53132
  marginWithAnchor: marginWithAnchor,
52919
53133
  focusCapture: focusCapture,
52920
53134
  scrollCapture: scrollCapture === "popover" || scrollCapture,
@@ -60637,6 +60851,11 @@ const PickerButton = props => {
60637
60851
  variant,
60638
60852
  rightSlotIcon,
60639
60853
  rightSlotIconSize = "inherit",
60854
+ // What goes in the right slot as-is — no <Icon> around it, so a caller can
60855
+ // put something interactive there. `rightSlotIcon` cannot: it is wrapped in
60856
+ // an <Icon>, which is aria-hidden, and a focusable node under aria-hidden is
60857
+ // invisible to assistive tech while still being reachable by tab.
60858
+ rightSlot,
60640
60859
  placeholder,
60641
60860
  ui,
60642
60861
  maxLines = 1,
@@ -60649,6 +60868,13 @@ const PickerButton = props => {
60649
60868
  // the end of an input: a picker holds a value the user chose, and unsetting
60650
60869
  // it should not require reopening the popup to hunt for a "none" entry.
60651
60870
  clearable,
60871
+ // "Are you sure?" before the cross clears anything. A cross of three
60872
+ // millimetres at the edge of a touch screen, right where the chevron is
60873
+ // aimed at, is the button pressed by accident — and what it removes does
60874
+ // not come back. Asked before the clear, so a "no" leaves the field
60875
+ // untouched (see the --navi-clear command).
60876
+ clearConfirm,
60877
+ clearConfirmPopupContent,
60652
60878
  error
60653
60879
  } = props;
60654
60880
  const isSingleLine = maxLines === 1;
@@ -60691,6 +60917,9 @@ const PickerButton = props => {
60691
60917
  variant: undefined,
60692
60918
  rightSlotIcon: undefined,
60693
60919
  rightSlotIconSize: undefined,
60920
+ rightSlot: undefined,
60921
+ clearConfirm: undefined,
60922
+ clearConfirmPopupContent: undefined,
60694
60923
  ui: undefined,
60695
60924
  maxLines: undefined,
60696
60925
  popupWidthFitContent: undefined,
@@ -60805,40 +61034,59 @@ const PickerButton = props => {
60805
61034
  })
60806
61035
  }), variant === "headless" || ui === "default" ? null : jsx("span", {
60807
61036
  className: "navi_picker_right_slot",
60808
- children: clearable && value !== undefined && value !== "" ? jsx(Button, {
60809
- command: "--navi-clear",
60810
- commandFor: inputProps.id,
60811
- tabIndex: "-1"
60812
- // No navi-focus-delegate, unlike the identical button inside an
60813
- // input: handing focus back to the picker's own input is what
60814
- // opens the popup, and clearing is the opposite intention.
60815
- ,
60816
-
60817
- icon: true,
60818
- variant: "discrete"
60819
- // preventDefault, not just tabIndex="-1": a mousedown focuses
60820
- // its target before any click happens, and this button should
60821
- // never hold focus at all — the field keeps it.
60822
- ,
60823
-
60824
- onMouseDown: e => {
60825
- e.preventDefault();
60826
- },
60827
- flex: true,
60828
- align: "center",
60829
- children: jsx(Icon, {
60830
- size: rightSlotIconSize,
60831
- lineOverflow: "allow",
60832
- children: jsx(CloseSvg, {})
61037
+ children: jsx(ControlIdContext.Provider, {
61038
+ value: undefined,
61039
+ children: jsx(ControlNameContext.Provider, {
61040
+ value: undefined,
61041
+ children: clearable && value !== undefined && value !== "" ? jsx(Button, {
61042
+ command: "--navi-clear",
61043
+ commandFor: inputProps.id
61044
+ // The question, asked before the clear rather than by the
61045
+ // action the clear sends — see the --navi-clear command.
61046
+ ,
61047
+
61048
+ confirm: clearConfirm,
61049
+ confirmPopupContent: clearConfirmPopupContent,
61050
+ tabIndex: "-1"
61051
+ // No navi-focus-delegate, unlike the identical button inside an
61052
+ // input: handing focus back to the picker's own input is what
61053
+ // opens the popup, and clearing is the opposite intention.
61054
+ ,
61055
+
61056
+ icon: true,
61057
+ variant: "discrete"
61058
+ // What is busy once the clear is sent is the picker — the value
61059
+ // being removed is the whole field's, and the picker already
61060
+ // draws the wait around all of it. Two outlines for one wait is
61061
+ // one too many.
61062
+ ,
61063
+
61064
+ loadingOutline: false
61065
+ // preventDefault, not just tabIndex="-1": a mousedown focuses
61066
+ // its target before any click happens, and this button should
61067
+ // never hold focus at all — the field keeps it.
61068
+ ,
61069
+
61070
+ onMouseDown: e => {
61071
+ e.preventDefault();
61072
+ },
61073
+ flex: true,
61074
+ align: "center",
61075
+ children: jsx(Icon, {
61076
+ size: rightSlotIconSize,
61077
+ lineOverflow: "allow",
61078
+ children: jsx(CloseSvg, {})
61079
+ })
61080
+ }) : rightSlot === undefined ?
61081
+ // lineOverflow: what sits in the slot is an affordance, not a
61082
+ // character — a caller asking for a bigger one wants it bigger,
61083
+ // not capped at the height of the line it sits on
61084
+ jsx(Icon, {
61085
+ size: rightSlotIconSize,
61086
+ lineOverflow: "allow",
61087
+ children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
61088
+ }) : rightSlot
60833
61089
  })
60834
- }) :
60835
- // lineOverflow: what sits in the slot is an affordance, not a
60836
- // character — a caller asking for a bigger one wants it bigger,
60837
- // not capped at the height of the line it sits on
60838
- jsx(Icon, {
60839
- size: rightSlotIconSize,
60840
- lineOverflow: "allow",
60841
- children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
60842
61090
  })
60843
61091
  })]
60844
61092
  }), jsx(ControlFacadeChildrenWrapper, {
@@ -72272,19 +72520,13 @@ installImportMetaCssBuild(import.meta);/**
72272
72520
  * `navi-side`/`data-layer` attributes) rather than computed in JS — read
72273
72521
  * the CSS block below instead of expecting a JS equivalent of it here.
72274
72522
  *
72275
- * `anchorCustomEventDetail="ignore"` is required, not cosmetic: without it,
72276
- * Popover would dock next to whatever triggered the open instead of flush
72277
- * against the edge, defeating the point of a side panel.
72523
+ * `anchorCustomEventDetail="ignore"` is required, not cosmetic, and in both
72524
+ * modes: without it Popover docks next to whatever triggered the open instead
72525
+ * of flush against the edge, and Dialog takes that trigger's width as its own
72526
+ * `min-width` floor (`--anchor-width`), overriding the `width` prop.
72278
72527
  */
72279
72528
  const css = /* css */`
72280
72529
  .navi_side_panel {
72281
- /* Dialog's own \`min-width: var(--anchor-width, 0px)\` exists so a
72282
- dialog naturally matches whatever triggered it (picker_custom.jsx's
72283
- dialog mode relies on this) — SidePanel's own "anchor" is just its
72284
- container, so this would otherwise force min-width to the full
72285
- container width, overriding \`width\`/\`height\` below entirely. Popover
72286
- ignores this var. */
72287
- --anchor-width: 0px;
72288
72530
  /* Side panel create a barriere with the content that is full size */
72289
72531
  /* So by default they don't have border-radius */
72290
72532
  --popup-border-radius: 0px;