@jsenv/navi 0.29.47 → 0.29.49

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.
@@ -16993,11 +16993,14 @@ const VISUAL_PROPS = {
16993
16993
  };
16994
16994
  const CONTENT_PROPS = {
16995
16995
  align: applyOnTwoProps("alignX", "alignY"),
16996
+ /* The value the caller named is always written out, even when it matches
16997
+ CSS's own initial value ("start" for justify-content, "stretch" for
16998
+ align-items). Skipping it as a no-op only holds for a bare element: a
16999
+ component whose stylesheet already centers its content (Picker in its icon
17000
+ variant, for one) would keep centering, and alignX="start" would silently
17001
+ do nothing — the one case where saying it explicitly matters most. */
16996
17002
  alignX: (value, { boxFlow }) => {
16997
17003
  if (boxFlow === "flex-y" || boxFlow === "inline-flex-y") {
16998
- if (value === "stretch") {
16999
- return undefined; // this is the default
17000
- }
17001
17004
  return { alignItems: value };
17002
17005
  }
17003
17006
  if (
@@ -17011,18 +17014,12 @@ const CONTENT_PROPS = {
17011
17014
  boxFlow === "grid" ||
17012
17015
  boxFlow === "inline-grid"
17013
17016
  ) {
17014
- if (value === "start") {
17015
- return undefined; // this is the default
17016
- }
17017
17017
  return { justifyContent: value };
17018
17018
  }
17019
17019
  return { textAlign: value };
17020
17020
  },
17021
17021
  alignY: (value, { boxFlow }) => {
17022
17022
  if (boxFlow === "flex-y" || boxFlow === "inline-flex-y") {
17023
- if (value === "start") {
17024
- return undefined;
17025
- }
17026
17023
  return { justifyContent: value };
17027
17024
  }
17028
17025
  if (
@@ -17035,9 +17032,6 @@ const CONTENT_PROPS = {
17035
17032
  boxFlow === "grid" ||
17036
17033
  boxFlow === "inline-grid"
17037
17034
  ) {
17038
- if (value === "stretch") {
17039
- return undefined;
17040
- }
17041
17035
  return { alignItems: value };
17042
17036
  }
17043
17037
  const verticalAlignMap = {
@@ -22147,7 +22141,10 @@ const useNavState = useNavStateBasic;
22147
22141
  * @param {Event} event What the user did.
22148
22142
  * @param {object} [options]
22149
22143
  * @param {boolean} [options.optional] No suitable target is not a warning.
22150
- * @param {any} [options.value] Carried to whoever answers.
22144
+ * @param {any} [options.value] What the command is about, carried to whoever
22145
+ * answers — and, for `--navi-open`/`--navi-toggle`, what the popup is opened
22146
+ * ON: it reaches its `onOpen` before the popup builds anything. Left out, a
22147
+ * source's own value (`<Button value={id}>`) is read instead.
22151
22148
  * @param {Element} [options.anchor] Where a popup this opens should be placed,
22152
22149
  * when that is not the element asking: a menu opened by a press belongs at the
22153
22150
  * point the press happened, and the row that was pressed is not that point.
@@ -22176,6 +22173,10 @@ const triggerNaviCommand = (
22176
22173
  // Whatever followed the colon: "--navi-go-to-slide:edit" → "edit".
22177
22174
  argument: command.includes(":") ? commandArgument(command) : undefined,
22178
22175
  anchor,
22176
+ // What the command is about, said by the caller rather than read off the
22177
+ // source: the attribute form has a `value` to read (`<Button value={id}>`),
22178
+ // a JS decision has none, and both must be able to say the same thing.
22179
+ value,
22179
22180
  });
22180
22181
  if (!execute) {
22181
22182
  if (optional) {
@@ -22812,28 +22813,42 @@ registerNaviCommand("--navi-nav-to", (source, event, { argument }) => {
22812
22813
  };
22813
22814
  });
22814
22815
 
22815
- registerNaviCommand("--navi-toggle", (source, event, { anchor } = {}) => {
22816
- const target =
22817
- resolveExplicitTarget(source) || resolveClosestExpandable(source);
22818
- if (!target) {
22819
- return undefined;
22820
- }
22821
- return {
22822
- target,
22823
- implementation: () => {
22824
- const isExpanded = target.getAttribute("aria-expanded") === "true";
22825
- const customEventName = isExpanded
22826
- ? "navi_request_close"
22827
- : "navi_request_open";
22828
- return dispatchCustomEvent(target, customEventName, {
22829
- event,
22830
- source: resolveCommandProxySource(source),
22831
- anchor,
22832
- });
22833
- },
22834
- };
22835
- });
22836
- registerNaviCommand("--navi-open", (source, event, { anchor } = {}) => {
22816
+ registerNaviCommand(
22817
+ "--navi-toggle",
22818
+ (source, event, { anchor, value } = {}) => {
22819
+ const target =
22820
+ resolveExplicitTarget(source) || resolveClosestExpandable(source);
22821
+ if (!target) {
22822
+ return undefined;
22823
+ }
22824
+ return {
22825
+ target,
22826
+ implementation: () => {
22827
+ const isExpanded = target.getAttribute("aria-expanded") === "true";
22828
+ const customEventName = isExpanded
22829
+ ? "navi_request_close"
22830
+ : "navi_request_open";
22831
+ return dispatchCustomEvent(target, customEventName, {
22832
+ event,
22833
+ source: resolveCommandProxySource(source),
22834
+ anchor,
22835
+ // Same as --navi-open below: the half of the toggle that opens says
22836
+ // what it opens ON, and the half that closes carries it too so the
22837
+ // popup never has to ask which half it just heard.
22838
+ value:
22839
+ value === undefined ? resolveCommandValue(source, event) : value,
22840
+ });
22841
+ },
22842
+ };
22843
+ },
22844
+ );
22845
+ // A popup that edits opens ON something, and the press is the only place that
22846
+ // knows which one. It says it with `value` — "what this is about", as
22847
+ // everywhere else — rather than with an argument, which says what the command
22848
+ // DOES: "open" is already a complete instruction, unlike --navi-go-to-slide.
22849
+ // <Button value={radar.id} command="--navi-open" commandfor="radar-dialog">
22850
+ // The popup is told before it opens — see Dialog/Popover's `onOpen`.
22851
+ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
22837
22852
  const target =
22838
22853
  resolveExplicitTarget(source) || resolveClosestExpandable(source);
22839
22854
  if (!target) {
@@ -22849,6 +22864,7 @@ registerNaviCommand("--navi-open", (source, event, { anchor } = {}) => {
22849
22864
  event,
22850
22865
  source: resolveCommandProxySource(source),
22851
22866
  anchor,
22867
+ value: value === undefined ? resolveCommandValue(source, event) : value,
22852
22868
  });
22853
22869
  },
22854
22870
  };
@@ -27799,7 +27815,8 @@ const getFocusedBeforeTransfer = (e) => {
27799
27815
  * - `onClose(e)`: actually closing, not preventable — final reactions live here.
27800
27816
  *
27801
27817
  * The controller exposes matching action methods:
27802
- * - `open()`: requests opening — runs `openEffect`, then `openHandler`.
27818
+ * - `open()`: requests opening — calls the caller's `onOpen` (see below), then
27819
+ * `mountContent`/`openEffect`, then `openHandler`.
27803
27820
  * - `requestClose()`: requests closing — calls `onRequestClose` then `onClose`,
27804
27821
  * stopping after the first if denied. The popup may choose to stay open.
27805
27822
  * - `close()`: closes for real — calls only `onClose`, skipping
@@ -27969,6 +27986,13 @@ const createOpenController = (
27969
27986
  // content is still waiting for a first open to be built. Called below,
27970
27987
  // before openEffect, so the popup measures and positions the real thing.
27971
27988
  mountContent: null,
27989
+ // The caller's own `onOpen`, set by Dialog/Popover from their props on
27990
+ // every render (like openEffect). Called BEFORE mountContent, so whatever
27991
+ // it decides — which record this dialog is opening on — is already true by
27992
+ // the time the content is built, positioned and shown. That order is the
27993
+ // whole point: learning it afterwards means the content mounted on the
27994
+ // previous subject first.
27995
+ onOpen: null,
27972
27996
  // The counterpart, set only when the popup was told to throw its content
27973
27997
  // away on close (`unmountWhenClosed`). Called from performClose above.
27974
27998
  unmountContent: null,
@@ -28025,6 +28049,10 @@ const createOpenController = (
28025
28049
  }
28026
28050
  };
28027
28051
  };
28052
+ // Before mountContent, which builds the content, and before openEffect,
28053
+ // which shows it: what the popup opens ON has to be known before either
28054
+ // (see `onOpen` above).
28055
+ controller.onOpen?.(requestOpenEvent);
28028
28056
  // After prepareFocusTransfer, which has to record what held the focus
28029
28057
  // before anything inside the popup can claim it, and before openEffect,
28030
28058
  // which measures the popup to place it.
@@ -29035,16 +29063,16 @@ const css$V = /* css */`
29035
29063
  backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
29036
29064
  }
29037
29065
 
29038
- /* backdropAppearance, keyed off the originating element (a
29066
+ /* backdropVariant, keyed off the originating element (a
29039
29067
  pseudo-element carries no attributes of its own — same reasoning as
29040
29068
  the capture rule just above). After the rules it overrides: same
29041
29069
  specificity, so order is what decides. showModal() still makes the
29042
29070
  page inert either way — only the paint goes away. */
29043
- &[data-backdrop-appearance="discrete"]::backdrop {
29071
+ &[data-backdrop-variant="discrete"]::backdrop {
29044
29072
  background: var(--navi-backdrop-discrete-background);
29045
29073
  backdrop-filter: none;
29046
29074
  }
29047
- &[data-backdrop-appearance="none"]::backdrop {
29075
+ &[data-backdrop-variant="invisible"]::backdrop {
29048
29076
  background: transparent;
29049
29077
  backdrop-filter: none;
29050
29078
  }
@@ -29171,11 +29199,11 @@ const css$V = /* css */`
29171
29199
  /* Same override as the via-attribute renderer's own ::backdrop rules
29172
29200
  above, on the real element this renderer uses instead — see them for
29173
29201
  the specificity/ordering reasoning. */
29174
- &[data-backdrop-appearance="discrete"] {
29202
+ &[data-backdrop-variant="discrete"] {
29175
29203
  background: var(--navi-backdrop-discrete-background);
29176
29204
  backdrop-filter: none;
29177
29205
  }
29178
- &[data-backdrop-appearance="none"] {
29206
+ &[data-backdrop-variant="invisible"] {
29179
29207
  background: transparent;
29180
29208
  backdrop-filter: none;
29181
29209
  }
@@ -29254,11 +29282,11 @@ const css$V = /* css */`
29254
29282
  * both just absorb the click without closing (visually dimmed backdrop vs.
29255
29283
  * not) — a dialog is always modal one way or another, so there's always
29256
29284
  * at least a click-absorbing backdrop regardless of this prop.
29257
- * @param {"auto"|"discrete"|"none"} [props.backdropAppearance="auto"] - How
29285
+ * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant="auto"] - How
29258
29286
  * visible the backdrop is, independently of what it does. `"auto"`: the
29259
29287
  * paint `pointerInteractionOutsideEffect` implies (dimmed for
29260
29288
  * `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
29261
- * barely-there dim. `"none"`: fully transparent. The dialog stays modal
29289
+ * barely-there dim. `"invisible"`: fully transparent. The dialog stays modal
29262
29290
  * either way — this only changes how much it insists visually, never what
29263
29291
  * an outside click does or whether the page behind stays reachable.
29264
29292
  * @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
@@ -29314,6 +29342,13 @@ const css$V = /* css */`
29314
29342
  * for the user to see it transition away from. `"interaction"` says the
29315
29343
  * opposite — this dialog is mounted *because* the user just asked for it, so
29316
29344
  * the mount is the opening and the entrance plays like any other.
29345
+ * @param {(openEvent: CustomEvent) => void} [props.onOpen] - Called when it
29346
+ * opens, BEFORE its content is built, positioned or shown. What it opens ON
29347
+ * is in `openEvent.detail.value` — the value of whatever asked
29348
+ * (`<Button value={radar.id} command="--navi-open" commandfor="…">`), or the
29349
+ * `value` given to `triggerNaviCommand`. That order is the point: a dialog that
29350
+ * is "new" or "edit X" depending on the press must know which one it is
29351
+ * before what it holds is rendered.
29317
29352
  * @param {(event: Event) => void} [props.onClose] - Called when the dialog
29318
29353
  * actually closes — not preventable (see `open_controller.js`'s own
29319
29354
  * `onRequestClose`/`onClose` distinction; `onRequestClose` is where you'd
@@ -29388,7 +29423,11 @@ const UncontrolledDialog = props => {
29388
29423
  openController: openController,
29389
29424
  onnavi_request_open: e => {
29390
29425
  openController.open(e, {
29391
- anchor: e.detail?.anchor ?? e.detail?.source
29426
+ anchor: e.detail?.anchor ?? e.detail?.source,
29427
+ // What the command was about — a `<Button value={id}>` that opened
29428
+ // this popup ON that id. Handed to `onOpen` before anything is
29429
+ // built (see open_controller.js).
29430
+ value: e.detail?.value
29392
29431
  });
29393
29432
  },
29394
29433
  onnavi_request_close: e => {
@@ -29512,7 +29551,7 @@ const useDialogProps = props => {
29512
29551
  // *does* (that's pointerInteractionOutsideEffect above). A dialog is
29513
29552
  // always modal, so "none" here never makes the page behind reachable:
29514
29553
  // it only stops the dim from being drawn.
29515
- backdropAppearance = "auto",
29554
+ backdropVariant = "auto",
29516
29555
  scrollCapture: scrollCaptureProp,
29517
29556
  // "auto" (default) → the dialog follows its content. "frozen" → measured
29518
29557
  // once, held at that size while open. See this prop's own JSDoc above.
@@ -29531,11 +29570,20 @@ const useDialogProps = props => {
29531
29570
  // instead, so it's read here rather than left in `rest`.
29532
29571
  autoFocus = "last-resort",
29533
29572
  onKeyDown,
29573
+ // Read here (rather than left in `rest`) for two reasons: it must never
29574
+ // reach the DOM as an `onopen` attribute, and the controller — not this
29575
+ // render — is what calls it, at the one moment that makes it useful (see
29576
+ // openController.onOpen below).
29577
+ onOpen,
29534
29578
  children: childrenProp,
29535
29579
  mountWhenClosed,
29536
29580
  unmountWhenClosed,
29537
29581
  ...rest
29538
29582
  } = props;
29583
+ // Assigned on every render, like openEffect below, so it always closes over
29584
+ // the latest prop. Called by openController.open() before the content is
29585
+ // built: what this popup opens ON is known before anything reads it.
29586
+ openController.onOpen = onOpen || null;
29539
29587
  const children = usePopupContentMount(openController, props.ref, {
29540
29588
  children: childrenProp,
29541
29589
  mountWhenClosed,
@@ -30034,7 +30082,7 @@ const useDialogProps = props => {
30034
30082
  "styleCSSVars": DIALOG_STYLE_CSS_VARS,
30035
30083
  "animationDuration": rest.animationDuration,
30036
30084
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
30037
- "data-backdrop-appearance": backdropAppearance
30085
+ "data-backdrop-variant": backdropVariant
30038
30086
  });
30039
30087
  Object.assign(contentProps, {
30040
30088
  tabIndex,
@@ -30067,7 +30115,7 @@ const useDialogProps = props => {
30067
30115
  // ::backdrop, same "a pseudo-element can't carry attributes" reasoning
30068
30116
  // as the prop just above (and harmless for the custom renderer, whose
30069
30117
  // real backdrop element gets it via backdropProps).
30070
- "data-backdrop-appearance": backdropAppearance,
30118
+ "data-backdrop-variant": backdropVariant,
30071
30119
  "styleCSSVars": DIALOG_STYLE_CSS_VARS,
30072
30120
  ...rest,
30073
30121
  ...autoFocusProps,
@@ -30472,16 +30520,16 @@ const css$U = /* css */`
30472
30520
  backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
30473
30521
  }
30474
30522
 
30475
- /* backdropAppearance overrides whatever the effect above picked — same
30523
+ /* backdropVariant overrides whatever the effect above picked — same
30476
30524
  specificity (class + one attribute), so these have to stay *after*
30477
30525
  them to win. Only the paint changes: the element is still rendered
30478
30526
  and still pointer-events: auto, so an outside click keeps doing
30479
30527
  exactly what pointerInteractionOutsideEffect says. */
30480
- &[data-backdrop-appearance="discrete"] {
30528
+ &[data-backdrop-variant="discrete"] {
30481
30529
  background: var(--navi-backdrop-discrete-background);
30482
30530
  backdrop-filter: none;
30483
30531
  }
30484
- &[data-backdrop-appearance="none"] {
30532
+ &[data-backdrop-variant="invisible"] {
30485
30533
  background: transparent;
30486
30534
  backdrop-filter: none;
30487
30535
  }
@@ -30549,11 +30597,11 @@ const css$U = /* css */`
30549
30597
  * absorbs the click (dims the backdrop) without closing. Note this
30550
30598
  * default differs from `Dialog`'s own (`"close"`) — a popover is
30551
30599
  * typically a lightweight, non-modal affordance.
30552
- * @param {"auto"|"discrete"|"none"} [props.backdropAppearance="auto"] - How
30600
+ * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant="auto"] - How
30553
30601
  * visible the backdrop is, independently of what it does. `"auto"`: the
30554
30602
  * paint `pointerInteractionOutsideEffect` implies (dimmed for
30555
30603
  * `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
30556
- * barely-there dim. `"none"`: fully transparent. The backdrop is still
30604
+ * barely-there dim. `"invisible"`: fully transparent. The backdrop is still
30557
30605
  * rendered and still catches outside clicks in every case — this only
30558
30606
  * changes how much the popover insists on being the thing you deal with.
30559
30607
  * Ignored when `pointerInteractionOutsideEffect="none"` (there is no
@@ -30616,6 +30664,13 @@ const css$U = /* css */`
30616
30664
  * for the user to see it transition away from. `"interaction"` says the
30617
30665
  * opposite — this popover is mounted *because* the user just asked for it, so
30618
30666
  * the mount is the opening and the entrance plays like any other.
30667
+ * @param {(openEvent: CustomEvent) => void} [props.onOpen] - Called when it
30668
+ * opens, BEFORE its content is built, positioned or shown. What it opens ON
30669
+ * is in `openEvent.detail.value` — the value of whatever asked
30670
+ * (`<Button value={radar.id} command="--navi-open" commandfor="…">`), or the
30671
+ * `value` given to `triggerNaviCommand`. That order is the point: a popover that
30672
+ * is "new" or "edit X" depending on the press must know which one it is
30673
+ * before what it holds is rendered.
30619
30674
  * @param {(event: Event) => void} [props.onClose] - Called when the popover
30620
30675
  * actually closes — not preventable (see `open_controller.js`'s own
30621
30676
  * `onRequestClose`/`onClose` distinction; `onRequestClose` is where you'd
@@ -30688,7 +30743,11 @@ const UncontrolledPopover = props => {
30688
30743
  openController: openController,
30689
30744
  onnavi_request_open: e => {
30690
30745
  openController.open(e, {
30691
- anchor: e.detail?.anchor ?? e.detail?.source
30746
+ anchor: e.detail?.anchor ?? e.detail?.source,
30747
+ // What the command was about — a `<Button value={id}>` that opened
30748
+ // this popup ON that id. Handed to `onOpen` before anything is
30749
+ // built (see open_controller.js).
30750
+ value: e.detail?.value
30692
30751
  });
30693
30752
  },
30694
30753
  onnavi_request_close: e => {
@@ -30806,7 +30865,7 @@ const usePopoverProps = props => {
30806
30865
  // *does* (that's pointerInteractionOutsideEffect above). "auto" keeps
30807
30866
  // the paint the effect implies; "discrete"/"none" tone it down or
30808
30867
  // remove it entirely without giving up the outside click.
30809
- backdropAppearance = "auto",
30868
+ backdropVariant = "auto",
30810
30869
  scrollCapture,
30811
30870
  focusCapture,
30812
30871
  // "auto" (default) → the popover follows its content. "frozen" → measured
@@ -30826,11 +30885,20 @@ const usePopoverProps = props => {
30826
30885
  // instead, so it's read here rather than left in `rest`.
30827
30886
  autoFocus = "last-resort",
30828
30887
  onKeyDown,
30888
+ // Read here (rather than left in `rest`) for two reasons: it must never
30889
+ // reach the DOM as an `onopen` attribute, and the controller — not this
30890
+ // render — is what calls it, at the one moment that makes it useful (see
30891
+ // openController.onOpen below).
30892
+ onOpen,
30829
30893
  children: childrenProp,
30830
30894
  mountWhenClosed,
30831
30895
  unmountWhenClosed,
30832
30896
  ...rest
30833
30897
  } = props;
30898
+ // Assigned on every render, like openEffect below, so it always closes over
30899
+ // the latest prop. Called by openController.open() before the content is
30900
+ // built: what this popup opens ON is known before anything reads it.
30901
+ openController.onOpen = onOpen || null;
30834
30902
  const children = usePopupContentMount(openController, props.ref, {
30835
30903
  children: childrenProp,
30836
30904
  mountWhenClosed,
@@ -31480,7 +31548,7 @@ const usePopoverProps = props => {
31480
31548
  "styleCSSVars": POPUP_STYLE_CSS_VARS,
31481
31549
  "animationDuration": rest.animationDuration,
31482
31550
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
31483
- "data-backdrop-appearance": backdropAppearance,
31551
+ "data-backdrop-variant": backdropVariant,
31484
31552
  "onMouseDown": mouseDownEvent => {
31485
31553
  if (mouseDownEvent.button !== 0) {
31486
31554
  return;
@@ -51833,7 +51901,7 @@ const css$z = /* css */`
51833
51901
  * is unavoidably *more* intrusive once it switches to dialog mode than
51834
51902
  * the exact same usage would be as a popover — worth keeping in mind for
51835
51903
  * anything that relies on `Popup` and can end up on a small screen.
51836
- * @param {"auto"|"discrete"|"none"} [props.backdropAppearance] - Forwarded
51904
+ * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant] - Forwarded
51837
51905
  * as-is to whichever component renders (both understand it identically):
51838
51906
  * how visible the backdrop is, independently of what an outside click
51839
51907
  * does. Unlike `pointerInteractionOutsideEffect` above, this one needs no
@@ -52239,9 +52307,16 @@ const PickerCustom = props => {
52239
52307
  const pickerEl = ref.current;
52240
52308
  const inputEl = getPickerInput(pickerEl);
52241
52309
  const valueAtClose = getUIStateFromElement(inputEl);
52242
- if (heldAtOpen && compareTwoJsValues(valueAtClose, valueAtOpen)) {
52243
- // Value unchanged and already held closing on it says nothing
52244
- // new. No action to run, but still allow the close.
52310
+ if (compareTwoJsValues(valueAtClose, valueAtOpen) && (heldAtOpen || valueAtOpen === undefined)) {
52311
+ // Nothing to say on the way out, for one of two reasons. Either the
52312
+ // value was already held and has not moved closing on it repeats
52313
+ // what was already the answer. Or there was never anything to
52314
+ // confirm: a picker holding nothing AND showing nothing (a menu of
52315
+ // gestures — no value, no defaultValue, no signal) has no
52316
+ // suggestion to accept, and confirming `undefined` cannot mean
52317
+ // anything. A picker on a defaultValue is untouched by this: it
52318
+ // shows something, so closing on it still confirms it.
52319
+ // No action to run, but still allow the close.
52245
52320
  return;
52246
52321
  }
52247
52322
  dispatchRequestAction(inputEl, {
@@ -52272,8 +52347,17 @@ const PickerCustom = props => {
52272
52347
  // answer. Say it here — this is the moment the suggestion becomes
52273
52348
  // one. Harmless when the value did change on the way: the state is
52274
52349
  // already what it is, and this only re-runs the same reaction.
52275
- debugPopup(closeEvent, `picker defined a suggestion -> commit it`);
52276
- commitUIStateAsAnswer(getPickerInput(ref.current)?.__uiStateController__, closeEvent);
52350
+ const inputEl = getPickerInput(ref.current);
52351
+ const valueAtClose = getUIStateFromElement(inputEl);
52352
+ if (valueAtOpen === undefined && compareTwoJsValues(valueAtClose, valueAtOpen)) {
52353
+ // Same third case onRequestClose steps around: nothing held,
52354
+ // nothing shown, nothing picked. There is no suggestion here to
52355
+ // turn into an answer.
52356
+ debugPopup(closeEvent, `picker showed nothing -> nothing to commit`);
52357
+ } else {
52358
+ debugPopup(closeEvent, `picker defined a suggestion -> commit it`);
52359
+ commitUIStateAsAnswer(inputEl?.__uiStateController__, closeEvent);
52360
+ }
52277
52361
  }
52278
52362
  leaveExpanded({
52279
52363
  isBack: closeEvent.detail.isCancel
@@ -52561,7 +52645,7 @@ const PickerContentInsidePopup = props => {
52561
52645
  pointerInteractionOutsideEffect = "close",
52562
52646
  // Named/forwarded rather than left in ...rest: rest goes to the picker
52563
52647
  // element itself, not the popup, and this belongs to the popup.
52564
- backdropAppearance,
52648
+ backdropVariant,
52565
52649
  dialogExpand,
52566
52650
  dialogExpandX,
52567
52651
  dialogExpandY,
@@ -52612,7 +52696,7 @@ const PickerContentInsidePopup = props => {
52612
52696
  marginWithContainer: marginWithContainer === undefined && isPopover ? popoverSpacing : marginWithContainer,
52613
52697
  scrollCapture: scrollCapture,
52614
52698
  pointerInteractionOutsideEffect: pointerLock ? "capture" : pointerInteractionOutsideEffect,
52615
- backdropAppearance: backdropAppearance,
52699
+ backdropVariant: backdropVariant,
52616
52700
  focusCapture: isPopover ? focusCapture : undefined,
52617
52701
  expand: isPopover ? undefined : dialogExpand,
52618
52702
  expandX: isPopover ? undefined : dialogExpandX,
@@ -59111,6 +59195,12 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
59111
59195
  transparent
59112
59196
  );
59113
59197
  --picker-icon-color-disabled: var(--picker-icon-color-readonly);
59198
+ /* Where the slots sit INSIDE the box, visible only once the box is bigger
59199
+ than what it holds (a width/height the caller gave it). Distinct from
59200
+ textAlign, which places the text inside the value slot; this places the
59201
+ slots themselves. */
59202
+ --picker-align-x-default: flex-start;
59203
+ --picker-align-y-default: center;
59114
59204
  }
59115
59205
  }
59116
59206
 
@@ -59147,6 +59237,8 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
59147
59237
  );
59148
59238
  --x-picker-color: var(--picker-color);
59149
59239
  --x-picker-icon-color: var(--picker-icon-color);
59240
+ --x-picker-align-x: var(--picker-align-x, var(--picker-align-x-default));
59241
+ --x-picker-align-y: var(--picker-align-y, var(--picker-align-y-default));
59150
59242
 
59151
59243
  /* Deliberately NOT positioned: the popup children live in here, and a
59152
59244
  layer="local" Popover/Dialog takes its nearest positioned ancestor as
@@ -59186,7 +59278,8 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
59186
59278
  padding-left: 0;
59187
59279
  flex: 1 1 auto;
59188
59280
  flex-direction: row;
59189
- align-items: center;
59281
+ align-items: var(--x-picker-align-y);
59282
+ justify-content: var(--x-picker-align-x);
59190
59283
  background-color: var(--x-picker-background-color);
59191
59284
  border-width: var(--picker-border-width);
59192
59285
  border-style: solid;
@@ -59288,7 +59381,7 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
59288
59381
  }
59289
59382
  &[navi-single-line] {
59290
59383
  .navi_picker_right_slot {
59291
- align-self: center;
59384
+ align-self: var(--x-picker-align-y);
59292
59385
  }
59293
59386
  }
59294
59387
  .navi_picker_input {
@@ -59372,6 +59465,10 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
59372
59465
  &[data-variant="icon"] {
59373
59466
  --picker-padding-x-default: 0;
59374
59467
  --picker-padding-y-default: 0;
59468
+ /* Nothing but the icon is drawn here, so a width/height the caller gave
59469
+ it is a target area, not a text column: the icon belongs in its middle.
59470
+ A default, like everything else a variant moves, so alignX still wins. */
59471
+ --picker-align-x-default: center;
59375
59472
  --picker-border-width: 0px; /* must carry a unit (px) — used in calc() to offset the custom input overlay */
59376
59473
  --picker-border-color: transparent;
59377
59474
  --picker-border-color-hover: var(--picker-border-color);
@@ -59473,7 +59570,15 @@ const PickerButton = props => {
59473
59570
  usePickerErrorCallout(uiStateController, error);
59474
59571
  return jsxs(Box, {
59475
59572
  as: "div",
59476
- ref: ref,
59573
+ ref: ref
59574
+ // The flow this element really has (.navi_picker is display:inline-flex).
59575
+ // Left unsaid, Box reads a <div> as block and resolves alignX into a
59576
+ // text-align — which is a different intention entirely (that one is the
59577
+ // textAlign prop, placing the text INSIDE the value slot).
59578
+ ,
59579
+
59580
+ inline: true,
59581
+ flex: "x",
59477
59582
  baseClassName: "navi_picker",
59478
59583
  pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
59479
59584
  "data-variant": variant,
@@ -59731,6 +59836,11 @@ const PickerStyleCSSVars = {
59731
59836
  "popupBorderRadius": "--picker-popup-border-radius",
59732
59837
  "dialogBorderWidth": "--picker-dialog-border-width",
59733
59838
  "slotSpacing": "--picker-slot-spacing",
59839
+ // alignX/alignY resolve to these two on a flex-x box; naming the CSS style
59840
+ // (not the prop) is what styleCSSVars matches, so justifyContent/alignItems
59841
+ // passed directly land in the same variables.
59842
+ "justifyContent": "--picker-align-x",
59843
+ "alignItems": "--picker-align-y",
59734
59844
  "padding": "--picker-padding",
59735
59845
  "paddingX": "--picker-padding-x",
59736
59846
  "paddingY": "--picker-padding-y",