@jsenv/navi 0.29.107 → 0.29.109

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.
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
- import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, clickIsSuppressed, watchWheelTravel, scrollRoomTowards, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
10
10
  import { isValidElement, createContext, render, h, Fragment, toChildArray, options, cloneElement } from "preact";
@@ -38,7 +38,7 @@ installImportMetaCssBuild(import.meta);/**
38
38
  * any of these, and a number is the last resort, not the first tool.
39
39
  */
40
40
 
41
- const css$14 = /* css */`
41
+ const css$15 = /* css */`
42
42
  @layer navi {
43
43
  :root {
44
44
  /* A control that overlaps its neighbours (the members of a Group share
@@ -102,7 +102,7 @@ const css$14 = /* css */`
102
102
  }
103
103
  }
104
104
  `;
105
- import.meta.css = [css$14, "@jsenv/navi/src/navi_z_indexes.js"];
105
+ import.meta.css = [css$15, "@jsenv/navi/src/navi_z_indexes.js"];
106
106
 
107
107
  const addIntoArray = (array, ...valuesToAdd) => {
108
108
  if (valuesToAdd.length === 1) {
@@ -371,7 +371,7 @@ installImportMetaCssBuild(import.meta);/**
371
371
  * the very first render and the browser does everything on its own.
372
372
  */
373
373
  const URL_TARGET_ATTRIBUTE = "data-url-target";
374
- const css$13 = /* css */`
374
+ const css$14 = /* css */`
375
375
  @layer navi {
376
376
  [${URL_TARGET_ATTRIBUTE}] {
377
377
  animation: navi_url_target var(--navi-url-target-duration, 2000ms)
@@ -389,7 +389,7 @@ const css$13 = /* css */`
389
389
  }
390
390
  }
391
391
  `;
392
- import.meta.css = [css$13, "@jsenv/navi/src/nav/url_target/url_target.js"];
392
+ import.meta.css = [css$14, "@jsenv/navi/src/nav/url_target/url_target.js"];
393
393
  let urlTargetOptions = {
394
394
  block: "start",
395
395
  behavior: "instant",
@@ -750,7 +750,7 @@ const useActionStatus = (action) => {
750
750
  };
751
751
  };
752
752
 
753
- installImportMetaCssBuild(import.meta);const css$12 = /* css */`
753
+ installImportMetaCssBuild(import.meta);const css$13 = /* css */`
754
754
  .action_error {
755
755
  margin-top: 0;
756
756
  margin-bottom: 20px;
@@ -775,7 +775,7 @@ const ActionRenderer = ({
775
775
  children,
776
776
  disabled
777
777
  }) => {
778
- import.meta.css = [css$12, "@jsenv/navi/src/action/action_renderer.jsx"];
778
+ import.meta.css = [css$13, "@jsenv/navi/src/action/action_renderer.jsx"];
779
779
  if (action === undefined) {
780
780
  throw new Error("ActionRenderer requires an action to render, but none was provided.");
781
781
  }
@@ -3813,6 +3813,9 @@ const CONTROL_PROP_SET = new Set([
3813
3813
  "signal",
3814
3814
  "defaultValue",
3815
3815
  "defaultChecked",
3816
+ // A checked radio pressed again lets its value go — a group allowed to hold
3817
+ // nothing. Answered by the radio's own reactions (see control_hooks.jsx).
3818
+ "deselectable",
3816
3819
  "readOnly", // will depend wether readOnly is supported
3817
3820
 
3818
3821
  "loading",
@@ -4185,15 +4188,26 @@ let renderMessageText = text => text;
4185
4188
  const setCalloutMessageTextRenderer = renderer => {
4186
4189
  renderMessageText = renderer;
4187
4190
  };
4188
- const CalloutRequestCloseContext = createContext();
4191
+
4192
+ // What a callout's own JSX may know about the callout around it: how to close
4193
+ // it, and the element — whose id is what `commandFor` needs when the button
4194
+ // closing it is not inside it (inside, `--navi-close` finds it on its own).
4195
+ const CalloutContext = createContext();
4189
4196
  const useCalloutRequestClose = () => {
4190
- return useContext(CalloutRequestCloseContext);
4197
+ return useContext(CalloutContext)?.requestClose;
4198
+ };
4199
+ const useCalloutElement = () => {
4200
+ return useContext(CalloutContext)?.element;
4191
4201
  };
4192
4202
  const renderIntoCallout = (jsx$1, calloutMessageElement, {
4193
- requestClose
4203
+ requestClose,
4204
+ element
4194
4205
  }) => {
4195
- const calloutJsx = jsx(CalloutRequestCloseContext.Provider, {
4196
- value: requestClose,
4206
+ const calloutJsx = jsx(CalloutContext.Provider, {
4207
+ value: {
4208
+ requestClose,
4209
+ element
4210
+ },
4197
4211
  children: jsx$1
4198
4212
  });
4199
4213
  render(calloutJsx, calloutMessageElement);
@@ -4203,12 +4217,14 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
4203
4217
  // text can go through renderMessageText: an emoji in a validation message must
4204
4218
  // not make the first line taller than the icon and close button beside it.
4205
4219
  const renderHtmlIntoCallout = (html, calloutMessageElement, {
4206
- requestClose
4220
+ requestClose,
4221
+ element
4207
4222
  }) => {
4208
4223
  const template = document.createElement("template");
4209
4224
  template.innerHTML = html;
4210
4225
  renderIntoCallout(domToVNodes(template.content), calloutMessageElement, {
4211
- requestClose
4226
+ requestClose,
4227
+ element
4212
4228
  });
4213
4229
  };
4214
4230
 
@@ -4241,6 +4257,98 @@ const domToVNodes = node => {
4241
4257
  return vnodes;
4242
4258
  };
4243
4259
 
4260
+ installImportMetaCssBuild(import.meta);/**
4261
+ * The icon a callout shows beside its message — a square in the status color
4262
+ * with the "!" glyph — drawable on its own, where the callout is not: on the
4263
+ * trigger that opens one (a picker in callout mode), so what one presses looks
4264
+ * like what it opens. The callout's own template draws from the same glyph
4265
+ * (see calloutTemplate in callout.js).
4266
+ */
4267
+
4268
+ const css$12 = /* css */`
4269
+ .navi_callout_status_icon {
4270
+ --x-callout-status-icon-color: var(--navi-callout-neutral-color);
4271
+
4272
+ display: inline-flex;
4273
+ box-sizing: border-box;
4274
+ aspect-ratio: 1 / 1;
4275
+ width: 1em;
4276
+ height: 1em;
4277
+ flex-shrink: 0;
4278
+ align-items: center;
4279
+ justify-content: center;
4280
+ color: white;
4281
+ vertical-align: middle;
4282
+ background-color: var(--x-callout-status-icon-color);
4283
+ border-radius: 2px;
4284
+
4285
+ &[data-status="success"] {
4286
+ --x-callout-status-icon-color: var(--navi-callout-success-color);
4287
+ }
4288
+ &[data-status="info"] {
4289
+ --x-callout-status-icon-color: var(--navi-callout-info-color);
4290
+ }
4291
+ &[data-status="warning"] {
4292
+ --x-callout-status-icon-color: var(--navi-callout-warning-color);
4293
+ }
4294
+ &[data-status="error"] {
4295
+ --x-callout-status-icon-color: var(--navi-callout-error-color);
4296
+ }
4297
+ &[data-shape="circle"] {
4298
+ border-radius: 50%;
4299
+ }
4300
+
4301
+ svg {
4302
+ width: auto;
4303
+ height: 55%;
4304
+ }
4305
+ }
4306
+ /* Inside an <Icon>, the icon box is the size: fill it. The glyph keeps its
4307
+ own share of it — the Icon's rule sizing any svg it holds to the whole
4308
+ box is for an svg that IS the icon, and this one sits in a square. */
4309
+ .navi_icon > .navi_callout_status_icon {
4310
+ width: 100%;
4311
+ height: 100%;
4312
+ }
4313
+ .navi_icon[data-icon-char] .navi_callout_status_icon svg {
4314
+ width: auto;
4315
+ height: 55%;
4316
+ }
4317
+ `;
4318
+ const CALLOUT_STATUS_GLYPH_VIEWBOX = "0 0 125 300";
4319
+ const CALLOUT_STATUS_GLYPH_PATH = "m25,1 8,196h59l8-196zm37,224a37,37 0 1,0 2,0z";
4320
+
4321
+ /**
4322
+ * @type {import("ignore:preact").FunctionComponent<{
4323
+ * status?: "info" | "warning" | "error" | "success" | "none",
4324
+ * shape?: "square" | "circle",
4325
+ * }>}
4326
+ * @param {"info"|"warning"|"error"|"success"|"none"} [status="info"] The color
4327
+ * — the callout's own for that status. `"none"` is the neutral one.
4328
+ * @param {"square"|"circle"} [shape="square"] Square like the callout's own
4329
+ * icon, or a circle.
4330
+ */
4331
+ const CalloutStatusIcon = ({
4332
+ status = "info",
4333
+ shape = "square"
4334
+ }) => {
4335
+ import.meta.css = [css$12, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
4336
+ return jsx("span", {
4337
+ className: "navi_callout_status_icon",
4338
+ "data-status": status === "none" ? undefined : status,
4339
+ "data-shape": shape === "circle" ? "circle" : undefined,
4340
+ "aria-hidden": "true",
4341
+ children: jsx("svg", {
4342
+ viewBox: CALLOUT_STATUS_GLYPH_VIEWBOX,
4343
+ xmlns: "http://www.w3.org/2000/svg",
4344
+ children: jsx("path", {
4345
+ fill: "currentColor",
4346
+ d: CALLOUT_STATUS_GLYPH_PATH
4347
+ })
4348
+ })
4349
+ });
4350
+ };
4351
+
4244
4352
  installImportMetaCssBuild(import.meta);/**
4245
4353
  * A callout component that mimics native browser validation messages.
4246
4354
  * Features:
@@ -4250,6 +4358,9 @@ installImportMetaCssBuild(import.meta);/**
4250
4358
  * - Arrow automatically shows when pointing at a valid anchor element
4251
4359
  * - Centers in viewport when no anchor element provided or anchor is too big
4252
4360
  */
4361
+
4362
+ // Unique for the page's lifetime: a caller may write the id in a commandfor.
4363
+ let calloutCount = 0;
4253
4364
  const css$11 = /* css */`
4254
4365
  @layer navi {
4255
4366
  .navi_callout {
@@ -4259,19 +4370,37 @@ const css$11 = /* css */`
4259
4370
  copies. */
4260
4371
  user-select: text;
4261
4372
 
4262
- --callout-success-color: #4caf50;
4263
- --callout-info-color: #2196f3;
4264
- --callout-warning-color: #ff9800;
4265
- --callout-error-color: #f44336;
4373
+ --callout-success-color: var(--navi-callout-success-color);
4374
+ --callout-info-color: var(--navi-callout-info-color);
4375
+ --callout-warning-color: var(--navi-callout-warning-color);
4376
+ --callout-error-color: var(--navi-callout-error-color);
4377
+ --callout-neutral-color: var(--navi-callout-neutral-color);
4266
4378
 
4267
4379
  --callout-background-color: white;
4268
4380
  --callout-icon-color: black;
4269
4381
  --callout-padding: 8px;
4270
4382
  --callout-z-index: var(--navi-z-index-callout);
4383
+ /* The callout's own, like its font: the icon and the cross are columns
4384
+ one line tall (1lh), and that line has to be the message's first
4385
+ line — which it is only if both read the same line-height, rather
4386
+ than whatever the element the callout sits in happens to use. */
4387
+ --callout-line-height: 1.5;
4388
+ /* The cross is furniture, not content: quieter than the message beside
4389
+ it, and the size of a glyph on the message's first line. */
4390
+ --callout-close-button-color: color-mix(
4391
+ in srgb,
4392
+ currentColor 45%,
4393
+ transparent
4394
+ );
4395
+ --callout-close-button-color-hover: currentColor;
4396
+ --callout-close-button-size: 0.7em;
4271
4397
  }
4272
4398
  }
4273
4399
 
4274
4400
  .navi_callout {
4401
+ /* No status until one is said (the data-status blocks below): a plain
4402
+ tooltip, framed in the neutral color and shown without an icon. */
4403
+ --x-callout-status-color: var(--callout-neutral-color);
4275
4404
  --x-callout-border-color: var(--x-callout-status-color);
4276
4405
  --x-callout-background-color: var(--callout-background-color);
4277
4406
  --x-callout-icon-color: var(--x-callout-status-color);
@@ -4297,6 +4426,9 @@ const css$11 = /* css */`
4297
4426
  color: revert; /* Do no inherit element color, callout is inside the element it should use document color though */
4298
4427
  font-weight: initial; /* Callout fells disconnected from the element, font weight should be predictible and stable */
4299
4428
  font-size: initial; /* Callout fells disconnected from the element, font size should be predictible and stable */
4429
+ line-height: var(
4430
+ --callout-line-height
4431
+ ); /* Same reason — and what the columns measure their 1lh against */
4300
4432
  background: transparent;
4301
4433
  border: none;
4302
4434
  outline: none; /* programmatic focus may land here briefly before being redirected to close button */
@@ -4326,6 +4458,11 @@ const css$11 = /* css */`
4326
4458
  &[data-status="error"] {
4327
4459
  --x-callout-status-color: var(--callout-error-color);
4328
4460
  }
4461
+ &[data-close-button="none"] {
4462
+ .navi_callout_close_button_column {
4463
+ display: none;
4464
+ }
4465
+ }
4329
4466
 
4330
4467
  .navi_callout_box {
4331
4468
  position: relative;
@@ -4376,6 +4513,15 @@ const css$11 = /* css */`
4376
4513
  background-color: var(--x-callout-icon-color);
4377
4514
  border-radius: 2px;
4378
4515
 
4516
+ /* Not drawn without a status (nothing to say about the message),
4517
+ nor when the message asked for none (icon: false). Nested in
4518
+ here rather than written beside this block: a rule one level up
4519
+ would lose to the display above on specificity alone. */
4520
+ .navi_callout:not([data-status]) &,
4521
+ .navi_callout[data-icon="none"] & {
4522
+ display: none;
4523
+ }
4524
+
4379
4525
  svg {
4380
4526
  width: 16px;
4381
4527
  height: 12px;
@@ -4408,37 +4554,52 @@ const css$11 = /* css */`
4408
4554
  .navi_callout_close_button_column {
4409
4555
  display: flex;
4410
4556
  height: var(--callout-icon-height);
4557
+ /* The button is a square one line tall around a cross of glyph size,
4558
+ so most of it is empty. Taken back on both sides: what is drawn then
4559
+ sits one gap from the text and one padding from the edge — where the
4560
+ icon sits on the other side — instead of that plus the button's own
4561
+ margin. The target keeps its full size; only the space it claims in
4562
+ the row shrinks. */
4563
+ margin-inline: calc(
4564
+ -1 * (var(--callout-icon-height) - var(--callout-close-button-size)) / 2
4565
+ );
4566
+ /* Whatever the cross measures, it sits on the middle of the first line
4567
+ — the line it is read with. */
4568
+ align-items: center;
4411
4569
  align-self: flex-start;
4412
4570
 
4413
4571
  .navi_callout_close_button {
4414
4572
  /* A square filling the column, so the whole first line is the target;
4415
- the padding keeps the cross itself at glyph size. */
4573
+ the cross itself is drawn at glyph size in the middle of it. */
4416
4574
  display: inline-flex;
4417
4575
  box-sizing: border-box;
4418
4576
  aspect-ratio: 1 / 1;
4419
4577
  height: 100%;
4420
- padding: 0.2em;
4578
+ padding: 0;
4421
4579
  align-items: center;
4422
4580
  justify-content: center;
4423
- color: currentColor;
4581
+ color: var(--callout-close-button-color);
4424
4582
  font-size: inherit;
4425
4583
  background: none;
4426
4584
  border: none;
4427
4585
  border-radius: 0.2em;
4586
+ /* transition: color 0.15s ease-in-out; */
4428
4587
  cursor: pointer;
4429
4588
 
4430
4589
  &:hover {
4431
- background: rgba(0, 0, 0, 0.1);
4590
+ color: var(--callout-close-button-color-hover);
4591
+ background: rgba(0, 0, 0, 0.06);
4432
4592
  }
4433
4593
 
4434
4594
  &:focus-visible,
4435
4595
  .navi_callout:focus-visible & {
4596
+ color: var(--callout-close-button-color-hover);
4436
4597
  outline: auto;
4437
4598
  }
4438
4599
 
4439
4600
  .navi_callout_close_button_svg {
4440
- width: 100%;
4441
- height: 100%;
4601
+ width: var(--callout-close-button-size);
4602
+ height: var(--callout-close-button-size);
4442
4603
  }
4443
4604
  }
4444
4605
  }
@@ -4453,6 +4614,11 @@ const css$11 = /* css */`
4453
4614
  * @param {string} [options.status=""] - Callout status: "info" | "warning" | "error" | "success"
4454
4615
  * @param {Function} [options.onClose] - Callback when callout is closed
4455
4616
  * @param {boolean} [options.closeOnClickOutside] - Whether to close on outside clicks (defaults to true for "info" status)
4617
+ * @param {boolean} [options.icon=true] - Whether the status icon is shown beside the message.
4618
+ * Never shown without a status either way (see the CSS).
4619
+ * @param {boolean} [options.closeButton=true] - Whether the cross is shown. Without it the callout
4620
+ * still closes on Escape, a click outside and its own `--navi-close` — for a tooltip that is
4621
+ * read rather than dismissed
4456
4622
  * @param {string} [options.reopen="toggle"] - What to do when the anchor already has an open callout:
4457
4623
  * "toggle" closes it (a second press on what opened it closes it), "update" replaces its message
4458
4624
  * in place, "replace" tears it down and opens a new one
@@ -4499,6 +4665,8 @@ const openCallout = (message, {
4499
4665
  reopen = "toggle",
4500
4666
  showErrorStack,
4501
4667
  skipFocus = false,
4668
+ icon = true,
4669
+ closeButton = true,
4502
4670
  debug = () => {}
4503
4671
  } = {}) => {
4504
4672
  import.meta.css = [css$11, "@jsenv/navi/src/control/rules/callout/callout.js"];
@@ -4655,10 +4823,27 @@ const openCallout = (message, {
4655
4823
  }
4656
4824
  requestClose(e, "click_close_button");
4657
4825
  };
4658
- const calloutId = `navi_callout_${Date.now()}`;
4826
+ calloutCount++;
4827
+ const calloutId = `navi_callout_${calloutCount}`;
4659
4828
  calloutElement.id = calloutId;
4660
4829
  calloutElement.style.opacity = 0;
4661
4830
  const update = (newMessage, options = {}) => {
4831
+ if (Object.hasOwn(options, "icon")) {
4832
+ if (options.icon === false) {
4833
+ calloutElement.setAttribute("data-icon", "none");
4834
+ } else {
4835
+ calloutElement.removeAttribute("data-icon");
4836
+ }
4837
+ }
4838
+ if (Object.hasOwn(options, "closeButton")) {
4839
+ // Per message rather than per callout: what replaces a message (a
4840
+ // constraint taking over a tooltip's callout) brings its own cross back.
4841
+ if (options.closeButton === false) {
4842
+ calloutElement.setAttribute("data-close-button", "none");
4843
+ } else {
4844
+ calloutElement.removeAttribute("data-close-button");
4845
+ }
4846
+ }
4662
4847
  const prevStatus = callout.status;
4663
4848
  // Connect callout with target element for accessibility
4664
4849
  if (options.status && options.status !== callout.status) {
@@ -4678,7 +4863,8 @@ const openCallout = (message, {
4678
4863
  if (isValidElement(newMessage)) {
4679
4864
  debug(`callout update message (jsx)`);
4680
4865
  renderIntoCallout(newMessage, calloutMessageElement, {
4681
- requestClose
4866
+ requestClose,
4867
+ element: calloutElement
4682
4868
  });
4683
4869
  } else if (newMessage instanceof Node) {
4684
4870
  // Handle DOM node (cloned from CSS selector)
@@ -4690,9 +4876,11 @@ const openCallout = (message, {
4690
4876
  clearCalloutMessage(calloutMessageElement);
4691
4877
  newMessage({
4692
4878
  renderIntoCallout: jsx => renderIntoCallout(jsx, calloutMessageElement, {
4693
- requestClose
4879
+ requestClose,
4880
+ element: calloutElement
4694
4881
  }),
4695
- requestClose
4882
+ requestClose,
4883
+ element: calloutElement
4696
4884
  });
4697
4885
  } else {
4698
4886
  if (Error.isError(newMessage)) {
@@ -4717,7 +4905,8 @@ const openCallout = (message, {
4717
4905
  } else {
4718
4906
  debug(`callout update message: ${typeof newMessage === "string" ? newMessage.slice(0, 80) : String(newMessage)}`);
4719
4907
  renderHtmlIntoCallout(String(newMessage), calloutMessageElement, {
4720
- requestClose
4908
+ requestClose,
4909
+ element: calloutElement
4721
4910
  });
4722
4911
  }
4723
4912
  }
@@ -4932,6 +5121,21 @@ const openCallout = (message, {
4932
5121
  };
4933
5122
  calloutElement.addEventListener("navi_request_close", handleCustomCloseEvent);
4934
5123
  }
5124
+ {
5125
+ // What a command said inside the callout is aimed at, once aria-expanded
5126
+ // made the callout its target (see calloutTemplate): run it, the way a
5127
+ // popup runs the commands aimed at it. Inlined rather than onNaviCommand
5128
+ // from commands.js, which already reaches this module through the callout
5129
+ // manager and must not be reached back.
5130
+ calloutElement.addEventListener("navi_command", e => {
5131
+ const {
5132
+ implementation
5133
+ } = e.detail;
5134
+ if (typeof implementation === "function") {
5135
+ implementation();
5136
+ }
5137
+ });
5138
+ }
4935
5139
  Object.assign(callout, {
4936
5140
  element: calloutElement,
4937
5141
  update,
@@ -5030,7 +5234,9 @@ const openCallout = (message, {
5030
5234
  });
5031
5235
  }
5032
5236
  update(message, {
5033
- status
5237
+ status,
5238
+ icon,
5239
+ closeButton
5034
5240
  });
5035
5241
 
5036
5242
  // positionCallout itself handles both "no anchorElement at all" and "a
@@ -5056,21 +5262,25 @@ const ARROW_WIDTH = 16;
5056
5262
  const ARROW_HEIGHT = 8;
5057
5263
  const ARROW_SPACING = 8;
5058
5264
 
5059
- // HTML template for the callout
5265
+ // aria-expanded is what --navi-close/--navi-cancel resolve their target with
5266
+ // (closest "[aria-expanded]", see commands.js): a button inside the callout
5267
+ // closes the callout, not the picker or dialog around it. Never "false" — a
5268
+ // closed callout is removed, not kept.
5060
5269
  const calloutTemplate = /* html */`
5061
5270
  <div
5062
5271
  class="navi_callout"
5063
5272
  popover="manual"
5273
+ aria-expanded="true"
5064
5274
  >
5065
5275
  <div class="navi_callout_box">
5066
5276
  <div class="navi_callout_frame"></div>
5067
5277
  <div class="navi_callout_body">
5068
5278
  <div class="navi_callout_icon">
5069
- <svg viewBox="0 0 125 300" xmlns="http://www.w3.org/2000/svg">
5070
- <path
5071
- fill="currentColor"
5072
- d="m25,1 8,196h59l8-196zm37,224a37,37 0 1,0 2,0z"
5073
- />
5279
+ <svg
5280
+ viewBox="${CALLOUT_STATUS_GLYPH_VIEWBOX}"
5281
+ xmlns="http://www.w3.org/2000/svg"
5282
+ >
5283
+ <path fill="currentColor" d="${CALLOUT_STATUS_GLYPH_PATH}" />
5074
5284
  </svg>
5075
5285
  </div>
5076
5286
  <!-- Keep .navi_callout_message so preact controls it -->
@@ -5719,6 +5929,8 @@ const createCalloutManager = (
5719
5929
  const [, remainingTokenData] = tokens.entries().next().value;
5720
5930
  callout.update(remainingTokenData.message, {
5721
5931
  status: remainingTokenData.status,
5932
+ icon: remainingTokenData.icon,
5933
+ closeButton: remainingTokenData.closeButton,
5722
5934
  });
5723
5935
  }
5724
5936
  return false;
@@ -5740,7 +5952,16 @@ const createCalloutManager = (
5740
5952
 
5741
5953
  const addOpenToken = (
5742
5954
  token,
5743
- { message, status, anchorElement, event, skipFocus, onClose } = {},
5955
+ {
5956
+ message,
5957
+ status,
5958
+ icon,
5959
+ closeButton,
5960
+ anchorElement,
5961
+ event,
5962
+ skipFocus,
5963
+ onClose,
5964
+ } = {},
5744
5965
  ) => {
5745
5966
  if (!message) {
5746
5967
  removeOpenToken(token, event);
@@ -5748,10 +5969,12 @@ const createCalloutManager = (
5748
5969
  }
5749
5970
  const calloutOptions = {
5750
5971
  status,
5972
+ icon,
5973
+ closeButton,
5751
5974
  closeOnClickOutside: status !== "error",
5752
5975
  };
5753
5976
 
5754
- tokens.set(token, { message, status, onClose });
5977
+ tokens.set(token, { message, status, icon, closeButton, onClose });
5755
5978
  if (callout) {
5756
5979
  callout.update(message, calloutOptions);
5757
5980
  return;
@@ -5768,7 +5991,11 @@ const createCalloutManager = (
5768
5991
  openingEvent: event,
5769
5992
  skipFocus,
5770
5993
  debug: debugPopup,
5771
- onClose: ({ event: closeEvent, shouldTransferFocusFromCallout }) => {
5994
+ onClose: ({
5995
+ event: closeEvent,
5996
+ reason,
5997
+ shouldTransferFocusFromCallout,
5998
+ }) => {
5772
5999
  removeCloseOnCleanup?.();
5773
6000
  for (const result of openResults) {
5774
6001
  if (typeof result === "function") {
@@ -5777,8 +6004,10 @@ const createCalloutManager = (
5777
6004
  }
5778
6005
  callout = null;
5779
6006
  // User dismissed the callout — notify all active tokens then clear.
6007
+ // Told what closed it: a token whose content is a popup of its own (a
6008
+ // picker in callout mode) closes that popup on the same event.
5780
6009
  for (const [, tokenData] of tokens) {
5781
- tokenData.onClose?.();
6010
+ tokenData.onClose?.({ event: closeEvent, reason });
5782
6011
  }
5783
6012
  tokens.clear();
5784
6013
  const element = controller.ref.current;
@@ -24826,9 +25055,11 @@ const getHrefTargetInfo = (href) => {
24826
25055
  * leave by where the reader came in, and the swipe (which replaces already, see
24827
25056
  * route_travel.jsx) and the press say the same thing.
24828
25057
  *
24829
- * `<Link replace>` is that, and it travels as an attribute because the click
24830
- * handler sees the anchor, not the component that rendered it — the same mouth
24831
- * as what a link asks of a route transition.
25058
+ * `<Link replace>` is that, and so is `<Button replace>` — on an `href`, or on
25059
+ * a `--navi-nav-to` command. It travels as an attribute because whoever
25060
+ * answers the press sees the element, not the component that rendered it: the
25061
+ * click handler reads it off the anchor, the command off its source — the same
25062
+ * mouth as what a link asks of a route transition.
24832
25063
  */
24833
25064
 
24834
25065
  const LINK_REPLACE_ATTRIBUTE = "data-navi-replace";
@@ -25099,75 +25330,85 @@ const setupBrowserIntegrationViaHistory = ({
25099
25330
  return requestedResult;
25100
25331
  };
25101
25332
 
25102
- // Browser event handlers
25333
+ // A click on a link is answered on the link itself, after the link's own
25334
+ // listeners: a navi control decides there whether the press is allowed — it
25335
+ // refuses with preventDefault from its click reaction — and runs its command
25336
+ // and its action; only then does the routing take the browser's place. This
25337
+ // is the order the Navigation API integration gets for free, where the
25338
+ // navigation starts once the click has been dispatched.
25339
+ const onLinkClick = (e) => {
25340
+ if (e.button !== 0) {
25341
+ // Ignore non-left clicks
25342
+ return;
25343
+ }
25344
+ if (e.metaKey) {
25345
+ // Ignore clicks with meta key (e.g. open in new tab)
25346
+ return;
25347
+ }
25348
+ if (e.defaultPrevented) {
25349
+ // Refused — by the link itself, or by whoever came before it.
25350
+ return;
25351
+ }
25352
+ const linkElement = e.currentTarget;
25353
+ if (linkElement.hasAttribute("data-readonly")) {
25354
+ return;
25355
+ }
25356
+ const href = linkElement.href;
25357
+ const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
25358
+ getHrefTargetInfo(href);
25359
+ if (isEmpty || !isSameOrigin) {
25360
+ // Let link to other origins be handled by the browser
25361
+ return;
25362
+ }
25363
+ if (isAnchor) {
25364
+ // Fragment navigation belongs to the browser: it owns the indicated
25365
+ // part of the document, and taking it over would cost `:target` and the
25366
+ // focus handling that come with it.
25367
+ if (isCurrent) {
25368
+ // Except this one, which the browser answers with a scroll and
25369
+ // nothing else: same pathname, same hash, so no event and no url
25370
+ // change reaches whoever is waiting on the designated element.
25371
+ rearmUrlTarget();
25372
+ }
25373
+ return;
25374
+ }
25375
+ // Nothing here declared a route, so there is nothing to route to: the
25376
+ // page is a plain document and a link in it is a plain link. Taking it
25377
+ // over anyway would push the url and then have nothing to show for it —
25378
+ // the address bar moves and the page does not (see applyRouting's own
25379
+ // "not called yet" branch, which is where that used to end up).
25380
+ if (!isRouting()) {
25381
+ return;
25382
+ }
25383
+ e.preventDefault();
25384
+ handleRoutingTask(href, {
25385
+ reason: `"click" on a[href="${href}"]`,
25386
+ // A link that takes the place of the current entry instead of stacking
25387
+ // on it says so on itself (see link_replace.js).
25388
+ navigationType: linkAsksForReplace(linkElement) ? "replace" : "push",
25389
+ // Who started it. Announced with the navigation because a press
25390
+ // carries things the url does not: what a link asks of a route
25391
+ // transition is the first of them (see route_transition.jsx). Read by
25392
+ // whoever knows what to do with it, and it is the anchor itself —
25393
+ // resolved here, where it already is.
25394
+ element: linkElement,
25395
+ });
25396
+ };
25397
+ // Wired from the capture phase on window, the first place a click is seen,
25398
+ // so that a link is answered whatever handler stops the click on its way up
25399
+ // (a card's own onClick). Wired on every click: a listener an element
25400
+ // already has is not added twice, and a link seen for the first time is
25401
+ // wired before the click reaches it. The click a gesture leaves behind
25402
+ // never gets this far — its suppressor (click_suppression.js in
25403
+ // @jsenv/dom) stops it on window, before anything below.
25103
25404
  window.addEventListener(
25104
25405
  "click",
25105
25406
  (e) => {
25106
- if (e.button !== 0) {
25107
- // Ignore non-left clicks
25108
- return;
25109
- }
25110
- if (e.metaKey) {
25111
- // Ignore clicks with meta key (e.g. open in new tab)
25112
- return;
25113
- }
25114
- if (e.defaultPrevented) {
25115
- return;
25116
- }
25117
- if (clickIsSuppressed()) {
25118
- // The click that ends a gesture (click_suppression.js in @jsenv/dom).
25119
- // Its suppressor also listens on window in capture, so whichever
25120
- // module registered first runs first — asked explicitly, the order
25121
- // stops mattering.
25122
- return;
25123
- }
25124
25407
  const linkElement = e.target.closest("a");
25125
25408
  if (!linkElement) {
25126
25409
  return;
25127
25410
  }
25128
- if (linkElement.hasAttribute("data-readonly")) {
25129
- return;
25130
- }
25131
- const href = linkElement.href;
25132
- const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
25133
- getHrefTargetInfo(href);
25134
- if (isEmpty || !isSameOrigin) {
25135
- // Let link to other origins be handled by the browser
25136
- return;
25137
- }
25138
- if (isAnchor) {
25139
- // Fragment navigation belongs to the browser: it owns the indicated
25140
- // part of the document, and taking it over would cost `:target` and the
25141
- // focus handling that come with it.
25142
- if (isCurrent) {
25143
- // Except this one, which the browser answers with a scroll and
25144
- // nothing else: same pathname, same hash, so no event and no url
25145
- // change reaches whoever is waiting on the designated element.
25146
- rearmUrlTarget();
25147
- }
25148
- return;
25149
- }
25150
- // Nothing here declared a route, so there is nothing to route to: the
25151
- // page is a plain document and a link in it is a plain link. Taking it
25152
- // over anyway would push the url and then have nothing to show for it —
25153
- // the address bar moves and the page does not (see applyRouting's own
25154
- // "not called yet" branch, which is where that used to end up).
25155
- if (!isRouting()) {
25156
- return;
25157
- }
25158
- e.preventDefault();
25159
- handleRoutingTask(href, {
25160
- reason: `"click" on a[href="${href}"]`,
25161
- // A link that takes the place of the current entry instead of stacking
25162
- // on it says so on itself (see link_replace.js).
25163
- navigationType: linkAsksForReplace(linkElement) ? "replace" : "push",
25164
- // Who started it. Announced with the navigation because a press
25165
- // carries things the url does not: what a link asks of a route
25166
- // transition is the first of them (see route_transition.jsx). Read by
25167
- // whoever knows what to do with it, and it is the anchor itself —
25168
- // resolved here, where it already is.
25169
- element: linkElement,
25170
- });
25411
+ linkElement.addEventListener("click", onLinkClick);
25171
25412
  },
25172
25413
  { capture: true },
25173
25414
  );
@@ -29529,7 +29770,25 @@ registerNaviCommand("--navi-nav-to", (source, event, { argument }) => {
29529
29770
  const target = resolveExplicitTarget(source) || source;
29530
29771
  return {
29531
29772
  target,
29532
- implementation: () => navTo(argument),
29773
+ // Which way there is worn by the source, as a link wears it (see
29774
+ // link_replace.js): `<Button command="--navi-nav-to:/done" replace>`.
29775
+ implementation: () =>
29776
+ navTo(
29777
+ argument,
29778
+ linkAsksForReplace(source) ? { replace: true } : undefined,
29779
+ ),
29780
+ };
29781
+ });
29782
+
29783
+ // Back to the screen the reader came from, with somewhere to land when there
29784
+ // is none of ours behind — a url opened cold (see navBack). The fallback is
29785
+ // the argument for the reason --navi-nav-to's destination is: it says WHAT the
29786
+ // command does when there is nothing to go back to.
29787
+ registerNaviCommand("--navi-nav-back", (source, event, { argument }) => {
29788
+ const target = resolveExplicitTarget(source) || source;
29789
+ return {
29790
+ target,
29791
+ implementation: () => navBack({ fallback: argument }),
29533
29792
  };
29534
29793
  });
29535
29794
 
@@ -29589,7 +29848,11 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
29589
29848
  },
29590
29849
  };
29591
29850
  });
29592
- registerNaviCommand("--navi-close", (source, event) => {
29851
+ // "--navi-close:all" closes every expandable above the source, nearest first
29852
+ // a link leaving from a badge shown over a sheet leaves both. A surface that
29853
+ // refuses (a form asking about its changes) keeps what is above it open too:
29854
+ // one cannot be out of the sheet while still in the badge.
29855
+ registerNaviCommand("--navi-close", (source, event, { argument }) => {
29593
29856
  const target =
29594
29857
  resolveExplicitTarget(source) || resolveClosestExpandable(source);
29595
29858
  if (!target) {
@@ -29598,13 +29861,30 @@ registerNaviCommand("--navi-close", (source, event) => {
29598
29861
  return {
29599
29862
  target,
29600
29863
  implementation: () => {
29601
- return dispatchCustomEvent(target, "navi_request_close", {
29602
- event,
29603
- source: resolveCommandProxySource(source),
29604
- });
29864
+ const detail = { event, source: resolveCommandProxySource(source) };
29865
+ if (argument === "all") {
29866
+ return requestCloseUpward(target, detail);
29867
+ }
29868
+ return dispatchCustomEvent(target, "navi_request_close", detail);
29605
29869
  },
29606
29870
  };
29607
29871
  });
29872
+ const requestCloseUpward = (target, detail) => {
29873
+ let expandable = target;
29874
+ while (expandable) {
29875
+ const closing = dispatchCustomEvent(
29876
+ expandable,
29877
+ "navi_request_close",
29878
+ detail,
29879
+ );
29880
+ if (!closing) {
29881
+ return false;
29882
+ }
29883
+ const parent = expandable.parentElement;
29884
+ expandable = parent ? parent.closest("[aria-expanded]") : null;
29885
+ }
29886
+ return true;
29887
+ };
29608
29888
  registerNaviCommand("--navi-cancel", (source, event) => {
29609
29889
  const target =
29610
29890
  resolveExplicitTarget(source) || resolveClosestExpandable(source);
@@ -33296,6 +33576,21 @@ const useControlProps = (props, {
33296
33576
  return defaultAction !== "type" && defaultAction !== "value_change";
33297
33577
  };
33298
33578
  if (controlType === "link") {
33579
+ // Nothing waits for a link's action: the press is a departure, and
33580
+ // the navigation goes whatever the action does — so its command goes
33581
+ // too, unlike a button's, which follows its action (see below).
33582
+ const onLinkInteractionAllowed = e => {
33583
+ triggerUIAction(e);
33584
+ const control = ref.current;
33585
+ if (!control) {
33586
+ return;
33587
+ }
33588
+ tryActionAfterInteractionAllowed(control, {
33589
+ event: e,
33590
+ action: boundAction,
33591
+ requester: control
33592
+ });
33593
+ };
33299
33594
  return {
33300
33595
  keyDown: e => {
33301
33596
  if (isSpaceToActivate(e)) {
@@ -33309,11 +33604,19 @@ const useControlProps = (props, {
33309
33604
  }
33310
33605
  };
33311
33606
  }
33607
+ if (getKeyboardEventDefaultAction(e) === "activate") {
33608
+ // Enter: the browser presses the link itself, with a click that
33609
+ // follows this keydown — and the click reaction below is where
33610
+ // the press is answered, once. A tab for a slide (no href) gets
33611
+ // no such click; Link answers Enter on its own there.
33612
+ return null;
33613
+ }
33312
33614
  return keyDownDefault(e);
33313
33615
  },
33314
33616
  click: e => {
33315
33617
  return {
33316
33618
  name: "click",
33619
+ allowed: () => onLinkInteractionAllowed(e),
33317
33620
  prevented: () => {
33318
33621
  e.preventDefault();
33319
33622
  }
@@ -33541,6 +33844,15 @@ const useControlProps = (props, {
33541
33844
  };
33542
33845
  if (isRadio) {
33543
33846
  if (checked) {
33847
+ if (props.deselectable) {
33848
+ return {
33849
+ name: "enter to uncheck radio",
33850
+ allowed: () => dispatchRequestSetUIState(inputEl, undefined, {
33851
+ event: e
33852
+ }),
33853
+ always
33854
+ };
33855
+ }
33544
33856
  return {
33545
33857
  name: "enter on checked radio",
33546
33858
  allowed: () => triggerUIAction(e),
@@ -33566,6 +33878,14 @@ const useControlProps = (props, {
33566
33878
  if (isRadio && e.key === " ") {
33567
33879
  const inputEl = e.currentTarget;
33568
33880
  if (inputEl.checked) {
33881
+ if (props.deselectable) {
33882
+ return {
33883
+ name: "space to uncheck radio",
33884
+ allowed: () => dispatchRequestSetUIState(inputEl, undefined, {
33885
+ event: e
33886
+ })
33887
+ };
33888
+ }
33569
33889
  // allow space to still trigger uiState and commands
33570
33890
  // on checked radios (won't update the ui state but will notify of interaction)
33571
33891
  return {
@@ -33591,6 +33911,20 @@ const useControlProps = (props, {
33591
33911
  // that lands on the <label> (not the <input>) never fires "mousedown" on
33592
33912
  // the input at all, so a DOM-snapshot-at-mousedown approach misses it.
33593
33913
  if (isRadio && uiStateController.uiState !== undefined) {
33914
+ if (props.deselectable) {
33915
+ // Not prevented: a prevented click has the browser put the
33916
+ // radio back to checked once the listeners are done. Left to
33917
+ // run, it changes nothing (the radio was checked already) and
33918
+ // reports nothing — a radio getting unchecked fires no input
33919
+ // event — so this is the one place the uncheck is said.
33920
+ return {
33921
+ name: `click to uncheck radio`,
33922
+ allowed: () => dispatchRequestSetUIState(ref.current, undefined, {
33923
+ event: e
33924
+ }),
33925
+ prevented: () => e.preventDefault()
33926
+ };
33927
+ }
33594
33928
  return {
33595
33929
  name: `click on checked radio`,
33596
33930
  allowed: () => triggerUIAction(e),
@@ -38584,6 +38918,14 @@ Object.assign(PSEUDO_CLASSES, {
38584
38918
  * way there changes. What a row of tabs wants — the neighbour is a lateral
38585
38919
  * move, not a step deeper, so the whole row weighs one entry and the back
38586
38920
  * button leaves by where the reader came in.
38921
+ * @param {string} [props.command] - What the press asks of a control around
38922
+ * the link — `"--navi-close"` on a link that leaves the sheet it is in.
38923
+ * Triggered on the press, before the navigation.
38924
+ * @param {Function} [props.action] - Work the press runs, before the
38925
+ * navigation. Nothing waits for it — not the navigation, not `command`: what
38926
+ * the next page must find has to be written synchronously (a draft in a
38927
+ * signal), and a request goes on its own while the page changes. Work that
38928
+ * decides the destination navigates itself, from a `<Button action>`.
38587
38929
  * @param {boolean} [props.preventDefault] - Call `event.preventDefault()` on
38588
38930
  * click (navigation suppressed; `onClick` still runs).
38589
38931
  * @param {(event: MouseEvent) => void} [props.onClick]
@@ -38823,9 +39165,12 @@ const LinkPlain = props => {
38823
39165
  replace: undefined,
38824
39166
  "data-navi-route-transition-request": routeTransitionRequest,
38825
39167
  ...replaceRequest,
39168
+ // The control's own handlers first — the interaction gate, the caller's
39169
+ // onClick/onKeyDown, the command and the action — then what only a link
39170
+ // does. Written over the spread above, so they have to be called here.
38826
39171
  onClick: e => {
38827
- onClick?.(e);
38828
- if (slide) {
39172
+ controlHostProps.onClick(e);
39173
+ if (slide && !e.defaultPrevented) {
38829
39174
  goToSlide(e.currentTarget, e);
38830
39175
  }
38831
39176
  if (preventDefault) {
@@ -38838,7 +39183,7 @@ const LinkPlain = props => {
38838
39183
  ,
38839
39184
 
38840
39185
  onKeyDown: e => {
38841
- props.onKeyDown?.(e);
39186
+ controlHostProps.onKeyDown(e);
38842
39187
  if (!slide || e.defaultPrevented) {
38843
39188
  return;
38844
39189
  }
@@ -38884,7 +39229,7 @@ const LinkPlain = props => {
38884
39229
  "data-current-effect-shadow": currentEffectShadow ? "" : undefined,
38885
39230
  "data-current-indicator-position": currentIndicatorPosition,
38886
39231
  "data-anchor": anchor ? "" : undefined,
38887
- "data-interactive": onClick ? "" : undefined,
39232
+ "data-interactive": onClick || props.command || props.action ? "" : undefined,
38888
39233
  "data-reveal-on-interaction": revealOnInteraction ? "" : undefined,
38889
39234
  baseClassName: "navi_link",
38890
39235
  styleCSSVars: LinkStyleCSSVars,
@@ -45160,6 +45505,7 @@ const ButtonUI = props => {
45160
45505
  href,
45161
45506
  target,
45162
45507
  rel,
45508
+ replace,
45163
45509
  // visual
45164
45510
  variant,
45165
45511
  pressEffect,
@@ -45206,6 +45552,12 @@ const ButtonUI = props => {
45206
45552
  ...basePseudoState,
45207
45553
  ":-navi-href-current": innerCurrent
45208
45554
  };
45555
+
45556
+ // Worn as an attribute, like a link's (see link_replace.js): read off the
45557
+ // anchor by the click handler, off the source by --navi-nav-to.
45558
+ const replaceRequest = replace ? {
45559
+ [LINK_REPLACE_ATTRIBUTE]: ""
45560
+ } : null;
45209
45561
  const visualSelector = ".navi_button_content";
45210
45562
  useAccentColorAttributes(ref, null, {
45211
45563
  elementSelector: visualSelector
@@ -45232,11 +45584,11 @@ const ButtonUI = props => {
45232
45584
  as: as,
45233
45585
  href: href,
45234
45586
  target: innerTarget,
45235
- rel: innerRel
45587
+ rel: innerRel,
45588
+ replace: undefined,
45589
+ ...replaceRequest,
45236
45590
  // Respond with the JS prop value directly so callers (e.g. resolveCommandValue)
45237
45591
  // get the original type instead of the DOM-coerced string (e.g. "[object Object]").
45238
- ,
45239
-
45240
45592
  onnavi_get_value: e => {
45241
45593
  e.detail.respondWith(props.value);
45242
45594
  },
@@ -45454,8 +45806,16 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
45454
45806
  * @type {import("ignore:preact").FunctionComponent<{
45455
45807
  * ownTarget?: boolean | "refuse" | "always",
45456
45808
  * emojiAsIcon?: boolean,
45809
+ * replace?: boolean,
45457
45810
  * [key: string]: any,
45458
45811
  * }>}
45812
+ * @param {boolean} [replace] Go where the press leads — an `href`, a
45813
+ * `--navi-nav-to` command — by TAKING THE PLACE of the current history entry
45814
+ * rather than stacking on it: what `<Link replace>` says, for a press drawn
45815
+ * as a button.
45816
+ * @param {Function} [action] On a button with an `href` or a `route`, the
45817
+ * same order as a Link's: it runs on the press, before the navigation, and
45818
+ * the navigation does not wait for it (see Link's `action`).
45459
45819
  * @param {boolean} [emojiAsIcon=true] Renders the emoji of the label as icons
45460
45820
  * so the button keeps the height of its text — `Text`'s prop, on by default
45461
45821
  * here. Pass `false` to let an emoji draw at its natural size.
@@ -51465,7 +51825,8 @@ const FOCUS_DELAY_ON_KEYBOARD_MS = 250;
51465
51825
  * - `open()`: requests opening — calls the caller's `onOpen` (see below), then
51466
51826
  * `mountContent`/`openEffect`, then `openHandler`.
51467
51827
  * - `requestClose()`: requests closing — calls `onRequestClose` then `onClose`,
51468
- * stopping after the first if denied. The popup may choose to stay open.
51828
+ * stopping after the first if denied. The popup may choose to stay open,
51829
+ * which is what a `false` return says (`true`: closed, or closed already).
51469
51830
  * - `close()`: closes for real — calls only `onClose`, skipping
51470
51831
  * `onRequestClose` entirely. Used when there really is no choice (e.g. the
51471
51832
  * popup unmounting).
@@ -51758,7 +52119,7 @@ const createOpenController = (
51758
52119
  detail,
51759
52120
  ) => {
51760
52121
  if (!controller.opened) {
51761
- return;
52122
+ return true;
51762
52123
  }
51763
52124
  const requestCloseEvent = new CustomEvent("navi_request_close", {
51764
52125
  detail: { event: e, ...detail },
@@ -51773,9 +52134,10 @@ const createOpenController = (
51773
52134
  if (nativeCancelEvent) {
51774
52135
  nativeCancelEvent.preventDefault();
51775
52136
  }
51776
- return;
52137
+ return false;
51777
52138
  }
51778
52139
  performClose(requestCloseEvent);
52140
+ return true;
51779
52141
  },
51780
52142
  close: (e = new CustomEvent("programmatic", { detail: {} }), detail) => {
51781
52143
  if (!controller.opened) {
@@ -53342,10 +53704,14 @@ const UncontrolledDialog = props => {
53342
53704
  });
53343
53705
  },
53344
53706
  onnavi_request_close: e => {
53345
- openController.requestClose(e, {
53707
+ const closing = openController.requestClose(e, {
53346
53708
  isCancel: e.detail?.isCancel,
53347
53709
  requester: e.detail?.source
53348
53710
  });
53711
+ if (!closing) {
53712
+ // Said back to whoever asked: --navi-close:all stops climbing here.
53713
+ e.preventDefault();
53714
+ }
53349
53715
  }
53350
53716
  });
53351
53717
  };
@@ -54792,9 +55158,13 @@ const UncontrolledPopover = props => {
54792
55158
  });
54793
55159
  },
54794
55160
  onnavi_request_close: e => {
54795
- openController.requestClose(e, {
55161
+ const closing = openController.requestClose(e, {
54796
55162
  isCancel: e.detail?.isCancel
54797
55163
  });
55164
+ if (!closing) {
55165
+ // Said back to whoever asked: --navi-close:all stops climbing here.
55166
+ e.preventDefault();
55167
+ }
54798
55168
  }
54799
55169
  });
54800
55170
  };
@@ -56105,6 +56475,43 @@ const PickerCustomResolver = props => {
56105
56475
  ...props
56106
56476
  });
56107
56477
  }
56478
+ if (props.mode === "callout") {
56479
+ // A tooltip is an icon one presses, unless told otherwise. Own-property
56480
+ // rather than undefined: an explicit variant={undefined} asks for the
56481
+ // field-like drawing back. "circle" is the icon variant with the status
56482
+ // icon drawn round — a word about the trigger, not a drawing of its own.
56483
+ if (!Object.hasOwn(props, "variant")) {
56484
+ props.variant = "icon";
56485
+ }
56486
+ const circle = props.variant === "circle";
56487
+ if (circle) {
56488
+ props.variant = "icon";
56489
+ }
56490
+ // A door, never a field (see `allowNameless`): the form around it expects
56491
+ // no value from it, and no name.
56492
+ props.allowNameless = true;
56493
+ // A word in a sentence asks for a plain tooltip — no icon in the callout,
56494
+ // no status color; an icon one presses is the callout's own status icon,
56495
+ // and says "info" like the callout it opens.
56496
+ if (props.calloutStatus === undefined) {
56497
+ props.calloutStatus = props.variant === "text" ? "none" : "info";
56498
+ }
56499
+ if (props.calloutIcon === undefined) {
56500
+ props.calloutIcon = props.variant !== "text";
56501
+ }
56502
+ if (props.rightSlotIcon === undefined) {
56503
+ props.rightSlotIcon = jsx(CalloutStatusIcon, {
56504
+ status: props.calloutStatus,
56505
+ shape: circle ? "circle" : "square"
56506
+ });
56507
+ }
56508
+ // The arrow on the middle of what was pressed — an icon, a word — rather
56509
+ // than on where its text starts, which is where a callout points at a
56510
+ // field by default (see the anchor attributes in callout.js).
56511
+ if (props["data-callout-arrow-x"] === undefined) {
56512
+ props["data-callout-arrow-x"] = "center";
56513
+ }
56514
+ }
56108
56515
  if (props.type === undefined) {
56109
56516
  // A picker with a popup of its own holds whatever the control inside it
56110
56517
  // holds — a boolean, a number, an id — and a field with no type is read
@@ -56432,7 +56839,10 @@ const PickerCustom = props => {
56432
56839
  dispatchCustomEvent(popupRef.current, "navi_request_open", e.detail);
56433
56840
  },
56434
56841
  "onnavi_request_close": e => {
56435
- dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
56842
+ const closing = dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
56843
+ if (!closing) {
56844
+ e.preventDefault();
56845
+ }
56436
56846
  },
56437
56847
  children
56438
56848
  });
@@ -56448,7 +56858,9 @@ const PickerCustom = props => {
56448
56858
  // opens anything. Told a value — even an empty one — the picker owns it
56449
56859
  // and pushes it down instead, leaving the popup free to build its
56450
56860
  // content only when it is first opened (see popup_content_mount.js).
56451
- mountWhenClosed: !isControlValueGivenByProps(props),
56861
+ // A caller who knows better says so with the popup's own props.
56862
+ mountWhenClosed: props.mountWhenClosed ?? !isControlValueGivenByProps(props),
56863
+ unmountWhenClosed: props.unmountWhenClosed,
56452
56864
  // Not on pickerProps (the trigger): commands.js's own
56453
56865
  // resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
56454
56866
  // find where to dispatch navi_request_open/navi_request_close — and
@@ -56482,12 +56894,17 @@ const PickerCustom = props => {
56482
56894
  event: e,
56483
56895
  intent: "read",
56484
56896
  allowed: () => {
56485
- requestClose(e, {
56897
+ const closing = requestClose(e, {
56486
56898
  isCancel: e.detail.isCancel
56487
56899
  });
56900
+ if (!closing) {
56901
+ e.preventDefault();
56902
+ }
56488
56903
  },
56489
56904
  prevented: () => {
56490
56905
  confirmEventRef.current = null;
56906
+ // Not closing either way; said back to whoever asked.
56907
+ e.preventDefault();
56491
56908
  }
56492
56909
  });
56493
56910
  },
@@ -56687,6 +57104,9 @@ const PickerContentInsidePopup = props => {
56687
57104
  // but popupProps is built explicitly here, so it only travels if named.
56688
57105
  // "popupLayer" rather than "layer": the picker itself is not the popup.
56689
57106
  popupLayer,
57107
+ // Same reason: a `data-testid` on the picker names the trigger (see
57108
+ // docs/testid.md) — this one names the popup.
57109
+ popupTestId,
56690
57110
  positionArea,
56691
57111
  popoverMode = "nearby",
56692
57112
  popoverSpacing = popoverMode === "nearby" ? 5 : 0,
@@ -56708,13 +57128,26 @@ const PickerContentInsidePopup = props => {
56708
57128
  // marginWithAnchor.
56709
57129
  dockedOnSmallTouchScreen,
56710
57130
  animation,
57131
+ // mode="callout": what the callout says about what it holds, and paints
57132
+ // in its border and icon — "none" for a plain tooltip (see the callout
57133
+ // defaults in PickerCustomResolver). And whether it wears a cross: without
57134
+ // one it still closes on Escape, a click outside, or a --navi-close of the
57135
+ // content's own.
57136
+ calloutStatus,
57137
+ calloutIcon,
57138
+ calloutCloseButton,
56711
57139
  ...rest
56712
57140
  } = props;
56713
57141
  const isPopover = mode === "popover";
57142
+ const isCallout = mode === "callout";
56714
57143
  return jsx(Next, {
56715
57144
  "aria-haspopup": isPopover ? "listbox" : "dialog",
56716
57145
  "navi-popover-mode": isPopover ? popoverMode : undefined,
56717
57146
  ...rest,
57147
+ // On popupProps already (see the picker's popup assembly); they mean
57148
+ // nothing to the picker element.
57149
+ mountWhenClosed: undefined,
57150
+ unmountWhenClosed: undefined,
56718
57151
  onFocusOut: e => {
56719
57152
  if (!isPopover || !closeOnFocusOut) {
56720
57153
  return;
@@ -56739,8 +57172,19 @@ const PickerContentInsidePopup = props => {
56739
57172
  }
56740
57173
  });
56741
57174
  },
56742
- children: jsx(Popup, {
57175
+ children: isCallout ? jsx(PickerCalloutPopup, {
57176
+ ...popupProps,
57177
+ pickerRef: props.ref,
57178
+ status: calloutStatus,
57179
+ icon: calloutIcon,
57180
+ closeButton: calloutCloseButton,
57181
+ children: jsx(PopupModeContext.Provider, {
57182
+ value: mode,
57183
+ children: children
57184
+ })
57185
+ }) : jsx(Popup, {
56743
57186
  ...popupProps,
57187
+ "data-testid": popupTestId,
56744
57188
  mode: mode,
56745
57189
  layer: popupLayer,
56746
57190
  animation: animation,
@@ -56763,6 +57207,112 @@ const PickerContentInsidePopup = props => {
56763
57207
  });
56764
57208
  };
56765
57209
 
57210
+ // One token per picker rather than per instance: a callout manager belongs to
57211
+ // one control, so the key only has to be distinct from the other reasons that
57212
+ // control may have to show a callout (a failing constraint, a busy refusal, its
57213
+ // `error` prop). Those keep working on top of this one: opened while the
57214
+ // content is up they take the callout over, and give it back when they go.
57215
+ const PICKER_CALLOUT_CONTENT_TOKEN = createOpenToken();
57216
+
57217
+ /**
57218
+ * The popup of a `mode="callout"` picker: the picker's own callout — the one
57219
+ * its constraints speak in — showing the picker's children instead of a
57220
+ * message. A speech bubble on the trigger, for a tooltip that opens on a press.
57221
+ *
57222
+ * Wired the way Popover and Dialog are, through `openController.openEffect`:
57223
+ * opening adds a token to the picker's callout manager, whose cleanup removes
57224
+ * it. The callout has ways out of its own (its cross, a click outside, Escape,
57225
+ * focus leaving the picker) and says so through the token's `onClose`, which
57226
+ * closes the controller for real — the popup is already gone, there is no
57227
+ * choice left to offer `requestClose`.
57228
+ *
57229
+ * The content is rendered through a portal into an element this component
57230
+ * owns, handed to the callout as its message (a Node, appended as-is). It is
57231
+ * rendered whether the callout is open or not, so what the content holds
57232
+ * survives a close, the way a popup's `mountWhenClosed` keeps it. The element
57233
+ * carries data-picker-content: the callout is appended inside the picker root,
57234
+ * and a press in there must read as inside the popup, not on the trigger.
57235
+ */
57236
+ const PickerCalloutPopup = ({
57237
+ ref,
57238
+ id,
57239
+ anchor,
57240
+ openController,
57241
+ pickerRef,
57242
+ status,
57243
+ icon,
57244
+ closeButton,
57245
+ onnavi_request_open,
57246
+ onnavi_request_close,
57247
+ onnavi_request_confirm,
57248
+ children
57249
+ }) => {
57250
+ const hostRef = useRef(null);
57251
+ if (!hostRef.current) {
57252
+ const host = document.createElement("div");
57253
+ host.setAttribute("data-picker-content", "");
57254
+ hostRef.current = host;
57255
+ }
57256
+ // Reassigned on every render, like Popover's own, so it closes over the
57257
+ // latest props.
57258
+ openController.openEffect = openEvent => {
57259
+ const pickerEl = pickerRef.current;
57260
+ const calloutManager = getPickerInput(pickerEl).__uiStateController__.rules.callout;
57261
+ // Only an anchor the caller named: left unsaid, the manager anchors on the
57262
+ // picker's own input — which is where the data-callout-* attributes a
57263
+ // caller puts on the picker land, and where the callout reads them.
57264
+ const anchorElement = anchor === pickerRef ? undefined : anchor && "current" in anchor ? anchor.current : anchor;
57265
+ calloutManager.addOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, {
57266
+ message: hostRef.current,
57267
+ // "none" is the picker's word for it; the callout's is no status at all.
57268
+ status: status === "none" ? undefined : status,
57269
+ icon,
57270
+ closeButton,
57271
+ anchorElement,
57272
+ // The request, chained to the press that made it: the callout reads the
57273
+ // mousedown off it to wait for the release before listening for a click
57274
+ // outside — the same gesture's own click would close it otherwise.
57275
+ event: openEvent,
57276
+ // Not skipped: the callout moves the focus into the picker when it is
57277
+ // elsewhere, which is what lets Escape find the callout right away.
57278
+ skipFocus: false,
57279
+ onClose: ({
57280
+ event
57281
+ }) => {
57282
+ openController.close(event);
57283
+ }
57284
+ });
57285
+ // A --navi-confirm said inside the callout is aimed at the callout (its
57286
+ // aria-expanded), not at the element the picker listens on: carried over,
57287
+ // for a confirm picker whose question is a speech bubble.
57288
+ const calloutElement = calloutManager.callout.element;
57289
+ const forwardConfirm = e => {
57290
+ onnavi_request_confirm?.(e);
57291
+ };
57292
+ calloutElement.addEventListener("navi_request_confirm", forwardConfirm);
57293
+ return closeEvent => {
57294
+ calloutElement.removeEventListener("navi_request_confirm", forwardConfirm);
57295
+ calloutManager.removeOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, closeEvent);
57296
+ };
57297
+ };
57298
+ return (
57299
+ // What the picker addresses (aria-controls, the request events it
57300
+ // forwards); the callout itself lives where the callout manager puts it.
57301
+ jsx(Box, {
57302
+ as: "span",
57303
+ ref: ref,
57304
+ id: id,
57305
+ style: {
57306
+ display: "contents"
57307
+ },
57308
+ onnavi_request_open: onnavi_request_open,
57309
+ onnavi_request_close: onnavi_request_close,
57310
+ onnavi_request_confirm: onnavi_request_confirm,
57311
+ children: createPortal(children, hostRef.current)
57312
+ })
57313
+ );
57314
+ };
57315
+
56766
57316
  installImportMetaCssBuild(import.meta);/**
56767
57317
  * A trigger that asks "are you sure?" before doing what it stands for.
56768
57318
  *
@@ -56811,6 +57361,8 @@ const PickerConfirmResolver = props => {
56811
57361
  message,
56812
57362
  confirmLabel,
56813
57363
  cancelLabel,
57364
+ confirmTestId,
57365
+ cancelTestId,
56814
57366
  focusOnOpen = "confirm",
56815
57367
  // A popover whatever the screen: the question is short and about the
56816
57368
  // control it points at, and a sheet sliding up for one sentence is too
@@ -56854,15 +57406,25 @@ const PickerConfirmResolver = props => {
56854
57406
  mode: mode,
56855
57407
  focusCapture: focusCapture,
56856
57408
  openWhileReadOnly: openWhileReadOnly,
56857
- onConfirm: onConfirm,
57409
+ onConfirm: onConfirm
57410
+ // A question holds no value to read before it is asked: the popup is
57411
+ // built on the first open, like that of a picker told its value (see
57412
+ // mountWhenClosed in picker_custom.jsx).
57413
+ ,
57414
+
57415
+ mountWhenClosed: false,
56858
57416
  message: undefined,
56859
57417
  confirmLabel: undefined,
56860
57418
  cancelLabel: undefined,
57419
+ confirmTestId: undefined,
57420
+ cancelTestId: undefined,
56861
57421
  focusOnOpen: undefined,
56862
57422
  children: children === undefined ? jsx(PickerConfirmBody, {
56863
57423
  message: message,
56864
57424
  confirmLabel: confirmLabel,
56865
57425
  cancelLabel: cancelLabel,
57426
+ confirmTestId: confirmTestId,
57427
+ cancelTestId: cancelTestId,
56866
57428
  focusOnOpen: focusOnOpen
56867
57429
  }) : children
56868
57430
  });
@@ -56871,6 +57433,8 @@ const PickerConfirmBody = ({
56871
57433
  message,
56872
57434
  confirmLabel,
56873
57435
  cancelLabel,
57436
+ confirmTestId,
57437
+ cancelTestId,
56874
57438
  focusOnOpen
56875
57439
  }) => {
56876
57440
  return jsxs("div", {
@@ -56882,10 +57446,12 @@ const PickerConfirmBody = ({
56882
57446
  children: [jsx(Button, {
56883
57447
  command: "--navi-cancel",
56884
57448
  autoFocus: focusOnOpen === "cancel",
57449
+ "data-testid": cancelTestId,
56885
57450
  children: cancelLabel
56886
57451
  }), jsx(Button, {
56887
57452
  command: "--navi-confirm",
56888
57453
  autoFocus: focusOnOpen === "confirm",
57454
+ "data-testid": confirmTestId,
56889
57455
  children: confirmLabel
56890
57456
  })]
56891
57457
  })]
@@ -57847,6 +58413,9 @@ installImportMetaCssBuild(import.meta);const css$z = /* css */`
57847
58413
  }
57848
58414
  `;
57849
58415
  const SelectableListMultipleContext = createContext(false);
58416
+ // A single-select list whose selected row, pressed again, lets go: what its
58417
+ // rows' radios need to know to answer the press (see `deselectable` on Input).
58418
+ const SelectableListDeselectableContext = createContext(false);
57850
58419
  // A row of a selectable list is selectable — the list is what decides, and a
57851
58420
  // row says nothing unless it wants out (`selectable={false}` on a row that is
57852
58421
  // only there to be read). Also set to false by a non-selectable list, so a list
@@ -57878,6 +58447,7 @@ const ListSelectable = props => {
57878
58447
  const {
57879
58448
  ref,
57880
58449
  multiple,
58450
+ deselectable,
57881
58451
  focusGroupDirection,
57882
58452
  focusGroupWrap
57883
58453
  } = props;
@@ -58014,6 +58584,7 @@ const ListSelectable = props => {
58014
58584
  defaultValue: undefined,
58015
58585
  selectable: undefined,
58016
58586
  multiple: undefined,
58587
+ deselectable: undefined,
58017
58588
  focusGroupDirection: undefined,
58018
58589
  focusGroupWrap: undefined
58019
58590
  // Track focus inside the list: whichever item gets focus becomes current.
@@ -58136,7 +58707,7 @@ const ListSelectable = props => {
58136
58707
  if (!currentId) {
58137
58708
  return;
58138
58709
  }
58139
- if (multiple) {
58710
+ if (multiple || deselectable) {
58140
58711
  const inputId = `${currentId}_input`;
58141
58712
  const childController = uiGroupStateController.findChildById(inputId);
58142
58713
  const isSelected = childController && childController.uiState;
@@ -58160,7 +58731,10 @@ const ListSelectable = props => {
58160
58731
  value: true,
58161
58732
  children: jsx(SelectableListMultipleContext.Provider, {
58162
58733
  value: multiple,
58163
- children: listVnode
58734
+ children: jsx(SelectableListDeselectableContext.Provider, {
58735
+ value: Boolean(deselectable),
58736
+ children: listVnode
58737
+ })
58164
58738
  })
58165
58739
  });
58166
58740
  };
@@ -58209,6 +58783,8 @@ const ListItemSelectable = props => {
58209
58783
  ...rest
58210
58784
  } = props;
58211
58785
  const multiple = useContext(SelectableListMultipleContext);
58786
+ // A checkbox toggles on its own; only a radio has to be told it may let go.
58787
+ const deselectable = useContext(SelectableListDeselectableContext) && !multiple;
58212
58788
  // Whose reason it is that this row cannot be taken. Read-only reaching it
58213
58789
  // from above is the LIST's, and what is settled is then the whole answer —
58214
58790
  // said as the selection where several things are taken, as the choice where
@@ -58231,6 +58807,7 @@ const ListItemSelectable = props => {
58231
58807
  ref: inputRef,
58232
58808
  id: inputId,
58233
58809
  type: inputType,
58810
+ deselectable,
58234
58811
  defaultChecked: defaultSelected,
58235
58812
  ...(hasSelectedProp ? {
58236
58813
  checked: selected
@@ -58249,9 +58826,10 @@ const ListItemSelectable = props => {
58249
58826
  type: inputType,
58250
58827
  checked,
58251
58828
  readOnly,
58252
- value
58829
+ value,
58830
+ deselectable
58253
58831
  };
58254
- }, [inputId, inputType, checked, readOnly, value]);
58832
+ }, [inputId, inputType, checked, readOnly, value, deselectable]);
58255
58833
  return jsxs(Next, {
58256
58834
  id: id,
58257
58835
  index: index,
@@ -59489,6 +60067,7 @@ const ListFirstResolver = props => {
59489
60067
  * @type {import("ignore:preact").FunctionComponent<{
59490
60068
  * selectable?: boolean,
59491
60069
  * multiple?: boolean,
60070
+ * deselectable?: boolean,
59492
60071
  * maxLength?: number,
59493
60072
  * maxLengthGuard?: number,
59494
60073
  * action?: (value: any) => void,
@@ -59613,6 +60192,11 @@ const ListFirstResolver = props => {
59613
60192
  * Pass `true` for a list whose rows must stay live under the pointer while
59614
60193
  * it scrolls. The trade of the default is the mirror one: right after a
59615
60194
  * scroll, the row under the pointer lights up only once the pointer moves.
60195
+ * @param {boolean} [props.deselectable]
60196
+ * A single-select list allowed to hold nothing: the selected row, pressed
60197
+ * again, lets go. Without it the list is a radio group — a choice, once
60198
+ * made, moves to another row but never goes away. A `multiple` list toggles
60199
+ * its rows already.
59616
60200
  * @param {number} [props.maxLength]
59617
60201
  * How many items a `selectable multiple` list accepts — the same word, and
59618
60202
  * the same behaviour, as `maxLength` on a text field: a rule the list is
@@ -64625,6 +65209,31 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
64625
65209
 
64626
65210
  text-align: center;
64627
65211
  }
65212
+ /* text: a word in a sentence, marked by the dotted line under it — the way
65213
+ a term one can ask about is marked. No box, no slot; the font is the
65214
+ sentence's own, so the word sits in its line like the ones around it. */
65215
+ &[data-variant="text"] {
65216
+ --picker-padding-x-default: 0;
65217
+ --picker-padding-y-default: 0;
65218
+ --picker-border-width: 0px; /* must carry a unit (px) — used in calc() to offset the custom input overlay */
65219
+ --picker-border-color: transparent;
65220
+ --picker-border-color-hover: var(--picker-border-color);
65221
+ --picker-border-color-readonly: var(--picker-border-color);
65222
+ --picker-border-color-disabled: var(--picker-border-color);
65223
+ --picker-background-color: transparent;
65224
+ --picker-background-color-hover: var(--picker-background-color);
65225
+ --picker-background-color-readonly: var(--picker-background-color);
65226
+ --picker-background-color-disabled: var(--picker-background-color);
65227
+
65228
+ font-size: inherit;
65229
+ font-family: inherit;
65230
+ text-decoration: underline dotted;
65231
+ text-underline-offset: 0.2em;
65232
+
65233
+ &[data-hover] {
65234
+ text-decoration-style: solid;
65235
+ }
65236
+ }
64628
65237
  }
64629
65238
  `;
64630
65239
  const PickerButton = props => {
@@ -64668,6 +65277,8 @@ const PickerButton = props => {
64668
65277
  error
64669
65278
  } = props;
64670
65279
  const isSingleLine = maxLines === 1;
65280
+ // Same rule as the root: phrasing content inside a sentence.
65281
+ const ContentTag = variant === "text" ? "span" : "div";
64671
65282
  const inputRef = useRef(null);
64672
65283
  const [pickerRemainingProps, inputProps, facadeChildrenProps] = useControlFacadeProps({
64673
65284
  ...props,
@@ -64705,8 +65316,11 @@ const PickerButton = props => {
64705
65316
  as a value nobody may change. */
64706
65317
  jsx(ReadOnlyContext.Provider, {
64707
65318
  value: readOnlyResolved,
64708
- children: jsxs(Box, {
64709
- as: "div",
65319
+ children: jsxs(Box
65320
+ // A word in a sentence (variant="text") sits in a <p>, where a <div> is
65321
+ // not allowed: phrasing content there, block content elsewhere.
65322
+ , {
65323
+ as: variant === "text" ? "span" : "div",
64710
65324
  ref: ref
64711
65325
  // The flow this element really has (.navi_picker is display:inline-flex).
64712
65326
  // Left unsaid, Box reads a <div> as block and resolves alignX into a
@@ -64856,7 +65470,7 @@ const PickerButton = props => {
64856
65470
  // picker behind it is.
64857
65471
  ,
64858
65472
 
64859
- "navi-placeholder": variant !== "button" && uiStateHoldsNothing(value) ? "" : undefined,
65473
+ "navi-placeholder": variant !== "button" && variant !== "text" && uiStateHoldsNothing(value) ? "" : undefined,
64860
65474
  maxLines: maxLines,
64861
65475
  children: jsx(PickerOwnContent, {
64862
65476
  children: jsx(PickerContext.Provider, {
@@ -64880,7 +65494,7 @@ const PickerButton = props => {
64880
65494
  })
64881
65495
  })
64882
65496
  })
64883
- }), variant === "icon" || variant === "headless" || variant === "button" || ui === "default" ? null : jsx("span", {
65497
+ }), variant === "icon" || variant === "headless" || variant === "button" || variant === "text" || ui === "default" ? null : jsx("span", {
64884
65498
  className: "navi_picker_right_slot",
64885
65499
  children: jsx(PickerOwnContent, {
64886
65500
  children: clearable && interactive && value !== undefined && value !== "" && clearConfirm !== undefined ?
@@ -64946,7 +65560,7 @@ const PickerButton = props => {
64946
65560
  })]
64947
65561
  }), jsx(ControlFacadeChildrenWrapper, {
64948
65562
  ...facadeChildrenProps,
64949
- children: jsx("div", {
65563
+ children: jsx(ContentTag, {
64950
65564
  className: "navi_picker_content",
64951
65565
  "data-picker-content": "",
64952
65566
  children: children
@@ -68027,6 +68641,7 @@ const css$n = /* css */`
68027
68641
  * pointerInteractionOutsideEffect?: "close" | "cancel" | "capture",
68028
68642
  * escapeEffect?: "cancel" | "close",
68029
68643
  * popupLayer?: "top" | "local",
68644
+ * popupTestId?: string,
68030
68645
  * [key: string]: any,
68031
68646
  * }>}
68032
68647
  * @param {Array<{value: any, label: import("ignore:preact").ComponentChildren}>} options
@@ -68068,7 +68683,7 @@ const css$n = /* css */`
68068
68683
  * `dockedOnSmallTouchScreen`, `dialogExpand*`, `dialogMinWidth`/`Height`,
68069
68684
  * `dialogMaxWidth`/`Height`,
68070
68685
  * `marginWithContainer`, `popoverMode`, `popoverSpacing`, `popupLayer`,
68071
- * `popupWidthFitContent`, `popoverMaxHeight`, `backdropVariant`,
68686
+ * `popupTestId`, `popupWidthFitContent`, `popoverMaxHeight`, `backdropVariant`,
68072
68687
  * `pointerInteractionOutsideEffect`, `escapeEffect`, `closeOnFocusOut`,
68073
68688
  * `scrollCapture`, `focusCapture`, `popupBackgroundColor`,
68074
68689
  * `popupBorderRadius`, `animation`. See picker.jsx for what each one says.
@@ -68270,7 +68885,7 @@ const SplitButton = props => {
68270
68885
  // What the Picker's popup answers to — Picker's own popup props, named here so
68271
68886
  // a caller reaches all of them through the split button (see picker.jsx's JSDoc
68272
68887
  // for what each one says).
68273
- const POPUP_PROP_SET = new Set(["mode", "popupLayer", "positionArea", "popoverMode", "popoverSpacing", "popupWidthFitContent", "popoverMaxHeight", "dialogMinWidth", "dialogMinHeight", "dialogMaxWidth", "dialogMaxHeight", "dialogExpand", "dialogExpandX", "dialogExpandY", "dockedOnSmallTouchScreen", "marginWithContainer", "backdropVariant", "pointerInteractionOutsideEffect", "escapeEffect", "closeOnFocusOut", "scrollCapture", "focusCapture", "popupBackgroundColor", "popupBorderRadius", "animation"]);
68888
+ const POPUP_PROP_SET = new Set(["mode", "popupLayer", "popupTestId", "positionArea", "popoverMode", "popoverSpacing", "popupWidthFitContent", "popoverMaxHeight", "dialogMinWidth", "dialogMinHeight", "dialogMaxWidth", "dialogMaxHeight", "dialogExpand", "dialogExpandX", "dialogExpandY", "dockedOnSmallTouchScreen", "marginWithContainer", "backdropVariant", "pointerInteractionOutsideEffect", "escapeEffect", "closeOnFocusOut", "scrollCapture", "focusCapture", "popupBackgroundColor", "popupBorderRadius", "animation"]);
68274
68889
  const splitPopupProps = props => {
68275
68890
  const popupProps = {};
68276
68891
  const boxProps = {};
@@ -75869,7 +76484,7 @@ const ErrorSvg = () => {
75869
76484
  })
75870
76485
  });
75871
76486
  };
75872
- const InfoSvg = () => {
76487
+ const InfoSvg$1 = () => {
75873
76488
  return jsx("svg", {
75874
76489
  viewBox: "0 0 16 16",
75875
76490
  fill: "currentColor",
@@ -75952,7 +76567,7 @@ const MessageBox = ({
75952
76567
  const [hasTitleChild, setHasTitleChild] = useState(false);
75953
76568
  const innerLeftStripe = leftStripe === undefined ? hasTitleChild : leftStripe;
75954
76569
  if (icon === true) {
75955
- icon = status === "info" ? jsx(InfoSvg, {}) : status === "success" ? jsx(SuccessSvg, {}) : status === "warning" ? jsx(WarningSvg, {}) : status === "error" ? jsx(ErrorSvg, {}) : null;
76570
+ icon = status === "info" ? jsx(InfoSvg$1, {}) : status === "success" ? jsx(SuccessSvg, {}) : status === "warning" ? jsx(WarningSvg, {}) : status === "error" ? jsx(ErrorSvg, {}) : null;
75956
76571
  } else if (typeof icon === "function") {
75957
76572
  const Comp = icon;
75958
76573
  icon = jsx(Comp, {});
@@ -78026,6 +78641,29 @@ const ExclamationSvg = () => {
78026
78641
  });
78027
78642
  };
78028
78643
 
78644
+ const InfoSvg = () => {
78645
+ return jsxs("svg", {
78646
+ viewBox: "0 0 24 24",
78647
+ xmlns: "http://www.w3.org/2000/svg",
78648
+ children: [jsx("circle", {
78649
+ cx: "12",
78650
+ cy: "12",
78651
+ r: "10",
78652
+ fill: "none",
78653
+ stroke: "currentColor",
78654
+ strokeWidth: "2"
78655
+ }), jsx("circle", {
78656
+ cx: "12",
78657
+ cy: "7.6",
78658
+ r: "1.4",
78659
+ fill: "currentColor"
78660
+ }), jsx("path", {
78661
+ fill: "currentColor",
78662
+ d: "M10.6 10.6h2.8V18h-2.8z"
78663
+ })]
78664
+ });
78665
+ };
78666
+
78029
78667
  const EyeClosedSvg = () => {
78030
78668
  return jsx("svg", {
78031
78669
  viewBox: "0 0 24 24",
@@ -78101,5 +78739,5 @@ const UserSvg = () => jsx("svg", {
78101
78739
  })
78102
78740
  });
78103
78741
 
78104
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, renderEmojiAsIcon, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
78742
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, renderEmojiAsIcon, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
78105
78743
  //# sourceMappingURL=jsenv_navi.js.map