@jsenv/navi 0.29.190 → 0.29.192

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, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
10
  import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, Fragment as Fragment$1 } from "preact";
@@ -5904,7 +5904,13 @@ const css$12 = /* css */ `
5904
5904
  To ensure ti goes above we put a z-index: 1, I hope it won't bite use in the future */
5905
5905
  z-index: 1;
5906
5906
  /* Callout styles */
5907
- display: block;
5907
+ /* No display declared here on purpose: the UA rule hiding a closed popover
5908
+ is the only thing that takes a hidden callout off screen, and any author
5909
+ display beats it (author origin wins over the UA stylesheet whatever the
5910
+ specificity). Overridden, hidePopover() leaves the box painted — out of
5911
+ the top layer, so laid out against the nearest positioned ancestor while
5912
+ its placement is written in document coordinates: drawn at the wrong
5913
+ place instead of not at all. */
5908
5914
  height: auto; /* User agent reset */
5909
5915
  margin: 0;
5910
5916
  padding: 0; /* User agent reset */
@@ -10358,17 +10364,27 @@ const swipeTypeOf = (axis, pulled) => {
10358
10364
  * two, refuse.
10359
10365
  *
10360
10366
  * interactions={{ land: (event) => {
10361
- * const { fromId, toId, syncCloneWithDropTarget } = event.detail;
10367
+ * const { fromId, toId, x, y, syncCloneWithDropTarget } = event.detail;
10362
10368
  * …
10363
10369
  * }}}
10364
10370
  *
10365
10371
  * `toId` is an element and never null: a copy over nothing is a release that meant
10366
- * nothing, and the interaction does not happen at all.
10372
+ * nothing, and the interaction does not happen at all — unless the element says
10373
+ * `data-toss-by="release-outside"` (see below), which gives that release a
10374
+ * meaning of its own.
10375
+ *
10376
+ * WHERE ON IT, in `x`, `y`, `width`, `height`: the box the copy came down in,
10377
+ * measured inside the place — its border and its scroll taken out. A place that is
10378
+ * a SURFACE — a plan, a map, a floor — has no element under the copy to name and
10379
+ * nothing to swap with: where it came down IS the answer, and `toId` says which
10380
+ * surface rather than which piece.
10367
10381
  *
10368
10382
  * `syncCloneWithDropTarget` takes an element here, which `reorder` has no use for:
10369
10383
  * a place of a board can be larger than what stands on it (a quarter of a court, a
10370
10384
  * square holding a smaller piece), and the copy has to come down where the piece
10371
- * will be rather than filling the place. Left alone, it lands on the place itself.
10385
+ * will be rather than filling the place. Left alone, it lands on the place itself —
10386
+ * so a surface either passes what the thing becomes or does not call it at all,
10387
+ * which leaves the copy where the hand put it, over the thing appearing there.
10372
10388
  *
10373
10389
  * WHICH ELEMENTS ARE PLACES: those marked `data-droppable`, and only those.
10374
10390
  * Declaring `land` says an element can be CARRIED, which on a board is a different
@@ -10377,9 +10393,30 @@ const swipeTypeOf = (axis, pulled) => {
10377
10393
  * case (dropped on a piece, the two swap). A list has no such distinction — every
10378
10394
  * row is both, which is why `reorder` needs no marker in the markup.
10379
10395
  *
10380
- * The set of places is looked for inside the carried element's PARENT, so a place
10381
- * and a piece are siblings: a piece nested inside its place would see only that
10382
- * one, and have nowhere else to go.
10396
+ * WHERE THE PLACES ARE LOOKED FOR: inside the carried element's PARENT, so a place
10397
+ * and a piece are siblings — and `data-drop-container` on an ancestor says
10398
+ * otherwise. Two arrangements need it, and both are the same one: what is carried
10399
+ * does not stand among the places. A palette is a strip BESIDE the surface it
10400
+ * fills, a marker already placed is drawn INSIDE the surface it can be put back
10401
+ * on, and in each case the search must cover what holds both. It holds the places
10402
+ * rather than being one — a surface carrying it would never be found from inside
10403
+ * itself. It is also what lets the copy travel there: named, the area it may cross
10404
+ * is the page rather than the source's own scroll area.
10405
+ *
10406
+ * <div data-drop-container>
10407
+ * <aside>{shapes.map((shape) => <Shape interactions={{ land: add }} />)}</aside>
10408
+ * <Plan id="plan" data-droppable>…</Plan>
10409
+ * </div>
10410
+ *
10411
+ * HOW A TOSS IS MADE: `data-toss-by`, beside `toss`. A THROW (`"throw"`, the
10412
+ * default) is far AND fast — the flick that gets rid of something, a list's
10413
+ * gesture, and it is judged before any landing. A RELEASE OUTSIDE
10414
+ * (`"release-outside"`) has no place under it — dragging a marker off a plan and
10415
+ * letting go, deliberate and never a flick, a surface's gesture. Each is named on
10416
+ * its own because each is wrong where the other is right: on a plan a fast drag
10417
+ * that ends ON it has not asked for the thing to go, and on a list a row let go of
10418
+ * beside it is a row put back. Both at once is
10419
+ * `data-toss-by="throw release-outside"`.
10383
10420
  *
10384
10421
  * Nothing of the gesture is decided here. `startDragTo` owns all of it — the
10385
10422
  * copy carried above the page while the original keeps its place in the layout, the
@@ -10469,6 +10506,8 @@ const REORDERABLE_ATTRIBUTE = "data-reorderable";
10469
10506
  // The same for `land`, except the markup is what writes it: a place of a board is
10470
10507
  // not the same thing as a piece of it (see the top of this file).
10471
10508
  const DROPPABLE_ATTRIBUTE = "data-droppable";
10509
+ // What holds the places, when the carried element does not stand among them.
10510
+ const DROP_CONTAINER_ATTRIBUTE = "data-drop-container";
10472
10511
  // Which axes the drag walks: "x", "y" or "xy". Its default is not the same for
10473
10512
  // every outcome — a list runs one way, and something being put somewhere goes
10474
10513
  // wherever it is put.
@@ -10478,6 +10517,10 @@ const SLOP_ATTRIBUTE = "data-drag-slop";
10478
10517
  const THRESHOLD_ATTRIBUTE = "data-drag-threshold";
10479
10518
  const TOSS_DISTANCE_ATTRIBUTE = "data-toss-distance";
10480
10519
  const TOSS_SPEED_ATTRIBUTE = "data-toss-speed";
10520
+ // How a toss is made here: "throw" (far and fast), "release-outside" (let go of
10521
+ // away from every place), or both.
10522
+ const TOSS_BY_ATTRIBUTE = "data-toss-by";
10523
+ const TOSS_BY_DEFAULT = "throw";
10481
10524
 
10482
10525
  defineInteractionDetector({
10483
10526
  name: "drag",
@@ -10508,6 +10551,16 @@ defineInteractionDetector({
10508
10551
  // the hand takes it: a board has places all around, a thing put somewhere has
10509
10552
  // two axes to be put along, and a throw goes where it was thrown.
10510
10553
  (canReorder && !canLand && !canToss ? "y" : "xy");
10554
+ // Where the places are, and what a release away from all of them means —
10555
+ // both read at setup for the same reason as the axes: they are what the
10556
+ // gesture is about, and it is what holds the two sides of it that says them.
10557
+ const dropContainer = element.closest(`[${DROP_CONTAINER_ATTRIBUTE}]`);
10558
+ const tossByHolder = element.closest(`[${TOSS_BY_ATTRIBUTE}]`);
10559
+ const tossBy = (
10560
+ tossByHolder?.getAttribute(TOSS_BY_ATTRIBUTE) || TOSS_BY_DEFAULT
10561
+ )
10562
+ .trim()
10563
+ .split(/\s+/);
10511
10564
 
10512
10565
  if (canReorder) {
10513
10566
  element.setAttribute(REORDERABLE_ATTRIBUTE, "");
@@ -10534,23 +10587,29 @@ defineInteractionDetector({
10534
10587
  : canReorder
10535
10588
  ? `[${REORDERABLE_ATTRIBUTE}]`
10536
10589
  : undefined,
10590
+ containerElement: dropContainer || undefined,
10537
10591
  getItemId: (itemElement) => itemElement.id,
10538
10592
  direction: { x: axes.includes("x"), y: axes.includes("y") },
10539
10593
  // Where it may go, said in the DOM. A thing that is put somewhere stays
10540
10594
  // inside what one can SEE of its container ("scrollport", not "scroll":
10541
10595
  // the scrollable area can be far larger than the box, and "inside the box"
10542
- // is what a hand expects). `data-drag-free` lifts that. Left alone for a
10543
- // throw, which frees the area on its own — it has to be able to leave.
10544
- areaConstraint: element.closest(`[data-drag-free]`)
10545
- ? "none"
10546
- : canMove
10547
- ? "scrollport"
10548
- : undefined,
10596
+ // is what a hand expects). `data-drag-free` lifts that, and so does a
10597
+ // named container of places: they are somewhere else by construction, and
10598
+ // a copy kept in its own scroll area could never reach them. Left alone
10599
+ // for a throw, which frees the area on its own — it has to be able to
10600
+ // leave.
10601
+ areaConstraint:
10602
+ dropContainer || element.closest(`[data-drag-free]`)
10603
+ ? "none"
10604
+ : canMove
10605
+ ? "scrollport"
10606
+ : undefined,
10549
10607
  threshold: readConfig(THRESHOLD_ATTRIBUTE, undefined),
10550
10608
  longPressDelay: readConfig(DELAY_ATTRIBUTE, undefined),
10551
10609
  longPressSlop: readConfig(SLOP_ATTRIBUTE, undefined),
10552
10610
  tossDistance: readConfig(TOSS_DISTANCE_ATTRIBUTE, undefined),
10553
10611
  tossSpeed: readConfig(TOSS_SPEED_ATTRIBUTE, undefined),
10612
+ tossBy,
10554
10613
  // The one moment the gesture has that is not a release. Said here rather
10555
10614
  // than from the press that led to it, because the press is only one of the
10556
10615
  // two ways in: a finger holds still, a mouse travels a few pixels, and it
@@ -10573,12 +10632,7 @@ defineInteractionDetector({
10573
10632
  toId,
10574
10633
  syncCloneWithDropTarget,
10575
10634
  }),
10576
- onLand: (fromId, toId, syncCloneWithDropTarget) =>
10577
- trigger(LAND, pointerDownEvent, {
10578
- fromId,
10579
- toId,
10580
- syncCloneWithDropTarget,
10581
- }),
10635
+ onLand: (detail) => trigger(LAND, pointerDownEvent, detail),
10582
10636
  onToss: ({ gestureInfo }) =>
10583
10637
  trigger(TOSS, pointerDownEvent, {
10584
10638
  id: element.id,
@@ -19670,6 +19724,7 @@ import.meta.css = /* css */[`[data-scrollable] > .navi_loading_outline_wrapper,
19670
19724
 
19671
19725
  [data-scrollable] {
19672
19726
  overflow: var(--x-scrollable-overflow, auto);
19727
+ scroll-padding: var(--navi-scroll-padding, var(--navi-s));
19673
19728
  --box-header-z-index: var(--navi-z-index-sticky);
19674
19729
  --box-footer-z-index: var(--navi-z-index-sticky);
19675
19730
  isolation: isolate;
@@ -19714,8 +19769,9 @@ import.meta.css = /* css */[`[data-scrollable] > .navi_loading_outline_wrapper,
19714
19769
  border-bottom-left-radius: inherit;
19715
19770
  outline-width: var(--navi-focus-outline-width);
19716
19771
  outline-offset: calc(-1 * var(--navi-focus-outline-width));
19717
- flex: 0 auto;
19718
19772
  min-height: 0;
19773
+ scroll-padding: var(--navi-scroll-padding, var(--navi-s));
19774
+ flex: 0 auto;
19719
19775
  overflow: auto;
19720
19776
 
19721
19777
  &:focus-visible {
@@ -44401,6 +44457,7 @@ const mayHaveHiddenFocus = (event) => {
44401
44457
  );
44402
44458
  };
44403
44459
 
44460
+ const scrollportHeightMap = new WeakMap();
44404
44461
  /**
44405
44462
  * Scrolls whatever holds focus inside `popupEl` back into view, if the popup
44406
44463
  * getting shorter has pushed it out.
@@ -44415,14 +44472,27 @@ const mayHaveHiddenFocus = (event) => {
44415
44472
  * past its bottom edge — visually, swallowed by the footer. The browser does
44416
44473
  * not redo a scroll-into-view it already answered, so this does.
44417
44474
  *
44475
+ * Only what the shrink itself hid, though: with the keyboard up the user reads
44476
+ * the rest of the popup by scrolling the field away — to reach the submit under
44477
+ * it, typically — and the room keeps changing while they do (a keyboard settling
44478
+ * in two steps, a suggestion strip, a browser bar). Answering each of those by
44479
+ * scrolling the field back takes the popup away from wherever they had just
44480
+ * scrolled it, over and over: what they were reading cannot be reached at all
44481
+ * without blurring the field first, and the popup reads as unscrollable. So the
44482
+ * field is brought back only when it was in view before the room shrank, which
44483
+ * one remembered number answers: a resize moves neither scrollTop nor the
44484
+ * element's offset inside the scrolled content, so measuring against the height
44485
+ * the scrollport HAD is measuring the state before the change.
44486
+ *
44418
44487
  * Scoped to the field's own scroll container (never the page): a popup traps
44419
44488
  * scrolling precisely so the document underneath cannot move, and a plain
44420
44489
  * scrollIntoView walks past a container whose scrollbar isn't visible — see
44421
44490
  * scrollIntoViewScoped's own doc.
44422
44491
  *
44423
- * "nearest": the smallest scroll that makes it visible, and none at all when
44424
- * it already is — so this is free to call on every resize, and never fights
44425
- * where the user had scrolled to.
44492
+ * "nearest": the smallest scroll that makes it visible, and none at all when it
44493
+ * already is. Where it lands is the container's own business — a navi scroller
44494
+ * keeps a band free at its edges so a field never comes back glued to one (see
44495
+ * scroll-padding in box.jsx).
44426
44496
  */
44427
44497
  const keepFocusedElementVisible = (popupEl) => {
44428
44498
  const { activeElement } = document;
@@ -44432,7 +44502,49 @@ const keepFocusedElementVisible = (popupEl) => {
44432
44502
  if (!popupEl.contains(activeElement)) {
44433
44503
  return;
44434
44504
  }
44435
- scrollIntoViewScoped(activeElement, { block: "nearest" });
44505
+ const scrollContainer = getScrollContainer(activeElement);
44506
+ if (!scrollContainer || !popupEl.contains(scrollContainer)) {
44507
+ // What scrolls the field is outside the popup, which means the page: a
44508
+ // popup holds it still on purpose (trapScrollInside), so there is nothing
44509
+ // here to scroll back.
44510
+ return;
44511
+ }
44512
+ const scrollportHeight = scrollContainer.clientHeight;
44513
+ const scrollportHeightBefore = scrollportHeightMap.get(scrollContainer);
44514
+ scrollportHeightMap.set(scrollContainer, scrollportHeight);
44515
+ if (scrollportHeightBefore !== undefined) {
44516
+ if (scrollportHeight >= scrollportHeightBefore) {
44517
+ // Nothing was taken away, so nothing was hidden by this.
44518
+ return;
44519
+ }
44520
+ if (
44521
+ !isVisibleInScrollport(
44522
+ activeElement,
44523
+ scrollContainer,
44524
+ scrollportHeightBefore,
44525
+ )
44526
+ ) {
44527
+ return;
44528
+ }
44529
+ }
44530
+ scrollIntoViewScoped(activeElement, {
44531
+ container: scrollContainer,
44532
+ block: "nearest",
44533
+ });
44534
+ };
44535
+
44536
+ // Whether any part of `el` was showing in `container` back when its scrollport
44537
+ // was `scrollportHeight` tall. Both boxes are read now: the container may have
44538
+ // moved as well as shrunk (a centered dialog re-centers itself), and the
44539
+ // difference between the two tops is what that move leaves alone.
44540
+ const isVisibleInScrollport = (el, container, scrollportHeight) => {
44541
+ const elRect = el.getBoundingClientRect();
44542
+ const containerRect = container.getBoundingClientRect();
44543
+ const elTop = elRect.top - containerRect.top + container.scrollTop;
44544
+ const elBottom = elTop + elRect.height;
44545
+ const scrollportTop = container.scrollTop;
44546
+ const scrollportBottom = scrollportTop + scrollportHeight;
44547
+ return elBottom > scrollportTop && elTop < scrollportBottom;
44436
44548
  };
44437
44549
 
44438
44550
  /**
@@ -44479,6 +44591,32 @@ const whenTransitionSettles = (el, onSettled) => {
44479
44591
  };
44480
44592
  };
44481
44593
 
44594
+ /**
44595
+ * Drops the document's text selection when it lives inside `el`, leaving a
44596
+ * selection made elsewhere on the page alone.
44597
+ *
44598
+ * A popup being closed takes its content with it, and a selection is a claim
44599
+ * on content the user can still act on: kept, it would outlive the surface it
44600
+ * was made on — painted on the box for the length of its exit transition, with
44601
+ * the handles and the copy toolbar a phone draws from the live selection
44602
+ * hanging over something that is going away — and reappear with the box on
44603
+ * the next opening. Dropping the selection is the only way to remove that
44604
+ * chrome: a `user-select: none` on the closing box hides the highlight in some
44605
+ * browsers only, and the handles are the browser's own, drawn from the
44606
+ * selection object rather than from any style.
44607
+ */
44608
+ const clearTextSelectionInside = (el) => {
44609
+ const selection = window.getSelection();
44610
+ if (!selection || selection.rangeCount === 0) {
44611
+ return;
44612
+ }
44613
+ const range = selection.getRangeAt(0);
44614
+ if (!el.contains(range.commonAncestorContainer)) {
44615
+ return;
44616
+ }
44617
+ selection.removeAllRanges();
44618
+ };
44619
+
44482
44620
  /**
44483
44621
  * Disables pointer-events on `el` until its current CSS transition settles —
44484
44622
  * avoids the cursor changing/something becoming clickable while the popup is
@@ -56135,6 +56273,15 @@ const css$E = /* css */`
56135
56273
  scroll (a short body, an axis with nowhere to go) must not travel to
56136
56274
  whatever is behind. */
56137
56275
  overscroll-behavior: none;
56276
+ /* What the backdrop paints, resolved on the dialog rather than on the
56277
+ pseudo-element that paints it: ::backdrop takes no inline style and
56278
+ carries no attribute of its own, so the backdropColor/backdropFilter
56279
+ props land here — inline, beating every rule below — and the
56280
+ pseudo-element inherits whichever won. A colour and a filter travel
56281
+ together everywhere a backdrop is painted: see navi_css_vars.js for
56282
+ why a blur is not tied to what an outside click does. */
56283
+ --backdrop-background: var(--navi-backdrop-close-background);
56284
+ --backdrop-filter: var(--navi-backdrop-close-backdrop-filter);
56138
56285
 
56139
56286
  /* Docking answers a different question than --dialog-max-width: a sheet
56140
56287
  spans its container's full width, flush against the two side edges —
@@ -56185,6 +56332,7 @@ const css$E = /* css */`
56185
56332
  &[data-flush-bottom][data-flush-left] {
56186
56333
  border-bottom-left-radius: 0;
56187
56334
  }
56335
+
56188
56336
  /* The placement is a translate, so the translate property is spoken for
56189
56337
  here (see applyNewPosition in visible_rect.js, which owns it and animates
56190
56338
  it itself through the Web Animations API rather than through this file's
@@ -56192,26 +56340,26 @@ const css$E = /* css */`
56192
56340
  to filter). An entrance animation moves the dialog through scale and
56193
56341
  transform instead, which compose under it: see popup_css.js. */
56194
56342
 
56195
- &::backdrop {
56196
- background: var(--navi-backdrop-close-background);
56343
+ &[data-pointer-interaction-outside="capture"] {
56344
+ --backdrop-background: var(--navi-backdrop-capture-background);
56345
+ --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
56197
56346
  }
56198
- &[data-pointer-interaction-outside="capture"]::backdrop {
56199
- background: var(--navi-backdrop-capture-background);
56200
- backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
56347
+ /* backdropVariant, after the rules it overrides: same specificity, so
56348
+ order is what decides. showModal() still makes the page inert either
56349
+ way — only the paint goes away. */
56350
+ &[data-backdrop-variant="discrete"] {
56351
+ --backdrop-background: var(--navi-backdrop-discrete-background);
56352
+ --backdrop-filter: var(--navi-backdrop-discrete-backdrop-filter);
56201
56353
  }
56202
-
56203
- /* backdropVariant, keyed off the originating element (a
56204
- pseudo-element carries no attributes of its own — same reasoning as
56205
- the capture rule just above). After the rules it overrides: same
56206
- specificity, so order is what decides. showModal() still makes the
56207
- page inert either way — only the paint goes away. */
56208
- &[data-backdrop-variant="discrete"]::backdrop {
56209
- background: var(--navi-backdrop-discrete-background);
56210
- backdrop-filter: none;
56354
+ /* The one backdrop with nothing to blur through: "invisible" paints
56355
+ nothing at all, and a filter would still be seen. */
56356
+ &[data-backdrop-variant="invisible"] {
56357
+ --backdrop-background: transparent;
56358
+ --backdrop-filter: none;
56211
56359
  }
56212
- &[data-backdrop-variant="invisible"]::backdrop {
56213
- background: transparent;
56214
- backdrop-filter: none;
56360
+ &::backdrop {
56361
+ background: var(--backdrop-background);
56362
+ backdrop-filter: var(--backdrop-filter);
56215
56363
  }
56216
56364
 
56217
56365
  /* Nested under &[navi-animation] (not the other way around) so every
@@ -56321,6 +56469,17 @@ const css$E = /* css */`
56321
56469
  in openEffect) gets pointer-events: none mid-transition. */
56322
56470
  pointer-events: auto;
56323
56471
 
56472
+ /* Painted through the same two variables as the via-attribute renderer's
56473
+ ::backdrop (see them for what each rule is for) — here they resolve on
56474
+ the element that paints, which is also the element the
56475
+ backdropColor/backdropFilter props are set on. Declared unconditionally
56476
+ first so a variable inherited from a dialog this popup was opened from
56477
+ can never reach the paint. */
56478
+ --backdrop-background: transparent;
56479
+ --backdrop-filter: none;
56480
+ background: var(--backdrop-background);
56481
+ backdrop-filter: var(--backdrop-filter);
56482
+
56324
56483
  /* A plain div, unlike dialogEl itself (a real <dialog>, natively hidden
56325
56484
  by default until .show()/.showModal() adds [open]) — needs its own
56326
56485
  starting-hidden mechanism. [navi-hidden] is set from useDialogProps'
@@ -56337,23 +56496,20 @@ const css$E = /* css */`
56337
56496
  /* Makes pointerInteractionOutsideEffect have a visible impact on backdrop */
56338
56497
  &[data-pointer-interaction-outside="close"],
56339
56498
  &[data-pointer-interaction-outside="cancel"] {
56340
- background: var(--navi-backdrop-close-background);
56499
+ --backdrop-background: var(--navi-backdrop-close-background);
56500
+ --backdrop-filter: var(--navi-backdrop-close-backdrop-filter);
56341
56501
  }
56342
56502
  &[data-pointer-interaction-outside="capture"] {
56343
- background: var(--navi-backdrop-capture-background);
56344
- backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
56503
+ --backdrop-background: var(--navi-backdrop-capture-background);
56504
+ --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
56345
56505
  }
56346
-
56347
- /* Same override as the via-attribute renderer's own ::backdrop rules
56348
- above, on the real element this renderer uses instead — see them for
56349
- the specificity/ordering reasoning. */
56350
56506
  &[data-backdrop-variant="discrete"] {
56351
- background: var(--navi-backdrop-discrete-background);
56352
- backdrop-filter: none;
56507
+ --backdrop-background: var(--navi-backdrop-discrete-background);
56508
+ --backdrop-filter: var(--navi-backdrop-discrete-backdrop-filter);
56353
56509
  }
56354
56510
  &[data-backdrop-variant="invisible"] {
56355
- background: transparent;
56356
- backdrop-filter: none;
56511
+ --backdrop-background: transparent;
56512
+ --backdrop-filter: none;
56357
56513
  }
56358
56514
 
56359
56515
  /* overlay is a no-op here (this backdrop is a plain div, never a
@@ -56480,6 +56636,17 @@ const css$E = /* css */`
56480
56636
  * barely-there dim. `"invisible"`: fully transparent. The dialog stays modal
56481
56637
  * either way — this only changes how much it insists visually, never what
56482
56638
  * an outside click does or whether the page behind stays reachable.
56639
+ * @param {string} [props.backdropColor] - The wash painted over what is
56640
+ * behind, for this popup alone: any CSS color (`"rgb(6 10 20 / 88%)"`).
56641
+ * Wins over `backdropVariant` and over the theme tokens
56642
+ * (`--navi-backdrop-*-background`), which is where the same choice goes
56643
+ * when it is the whole app's.
56644
+ * @param {string} [props.backdropFilter] - What that same wash does to the
56645
+ * picture underneath: `"blur(4px)"` to keep the page behind recognisable
56646
+ * while it stops being readable. A `backdrop-filter` value, so
56647
+ * `saturate()`/`grayscale()` work too. How far what is behind withdraws is
56648
+ * a question of paint, not of what an outside click does — a backdrop that
56649
+ * closes can blur.
56483
56650
  * @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
56484
56651
  * dialog so the page/container behind it can't scroll while it's open.
56485
56652
  * A `layer="local"` dialog always locks its own positioned ancestor's
@@ -56816,6 +56983,13 @@ const useDialogProps = props => {
56816
56983
  // always modal, so "none" here never makes the page behind reachable:
56817
56984
  // it only stops the dim from being drawn.
56818
56985
  backdropVariant = "auto",
56986
+ // The paint itself, when the tokens behind backdropVariant are not what
56987
+ // this one dialog wants. Named/forwarded rather than left in ...rest:
56988
+ // rest lands on the dialog element, and these belong to the backdrop —
56989
+ // which is the pseudo-element of one renderer and a sibling of the other,
56990
+ // so both objects below get them (see this file's CSS).
56991
+ backdropColor,
56992
+ backdropFilter,
56819
56993
  scrollCapture: scrollCaptureProp,
56820
56994
  // "auto" (default) → the dialog follows its content. "frozen" → measured
56821
56995
  // once, held at that size while open. See this prop's own JSDoc above.
@@ -57244,6 +57418,33 @@ const useDialogProps = props => {
57244
57418
  // recheck its own position whenever this dialog itself moves is
57245
57419
  // handled generically by applyNewPosition itself (dispatches
57246
57420
  // navi_position_change on every call) — nothing to do here.
57421
+ logPlacement(triggerEvent, position);
57422
+ };
57423
+ // The whole picture in one line: where the dialog ended up, and what it
57424
+ // believed about the room when it decided. Both are needed together — a box
57425
+ // that looks wrong on screen while these numbers are right means something
57426
+ // covers the screen that no API describes (a phone's autofill/suggestion
57427
+ // strip), and the absence of a line after the keyboard arrives means the
57428
+ // placement was never asked to run again.
57429
+ const logPlacement = (triggerEvent, position) => {
57430
+ // The decided target, never the current rect: the placement animates, so
57431
+ // a box read right after applying one is somewhere between the two and
57432
+ // reads like a decision nobody took.
57433
+ const {
57434
+ visualViewport
57435
+ } = window;
57436
+ // Reads nothing that brings the layout up to date — no getComputedStyle,
57437
+ // no window.innerWidth/innerHeight. That is the very thing this line is
57438
+ // here to observe (see getVisibleViewportRect in @jsenv/dom): a log that
57439
+ // forced it would hand the NEXT placement a fresh viewport and hide the
57440
+ // bug it was printed to show. Everything below is either already computed
57441
+ // (the pick's own numbers, "room" being the room it had) or free to read.
57442
+ const message = `placed on "${triggerEvent ? triggerEvent.type : "open"}": ${Math.round(position.width)}x${Math.round(position.height)} at ${Math.round(position.top)}->${Math.round(position.top + position.height)} | room ${Math.round(position.containerHeightAvailable)} | viewport ${Math.round(visualViewport.width)}x${Math.round(visualViewport.height)} +${Math.round(visualViewport.offsetLeft)},${Math.round(visualViewport.offsetTop)} | keyboard ${getVirtualKeyboardOverlayHeight()}`;
57443
+ if (triggerEvent) {
57444
+ debugPopup(triggerEvent, message);
57445
+ } else {
57446
+ debugPopup(message);
57447
+ }
57247
57448
  };
57248
57449
  // Cleared here rather than on close, where the box is deliberately left
57249
57450
  // frozen at the size it was closing at (see the closing function's own
@@ -57391,6 +57592,7 @@ const useDialogProps = props => {
57391
57592
  }
57392
57593
  return closeEvent => {
57393
57594
  debugPopup(`"${closeEvent.type}" on ${getElementSignature(closeEvent.target)} -> closeDialog`);
57595
+ clearTextSelectionInside(dialogEl);
57394
57596
  dialogEl.setAttribute("aria-expanded", "false");
57395
57597
  if (!isModal) {
57396
57598
  openLocalDialogCount = Math.max(0, openLocalDialogCount - 1);
@@ -57478,7 +57680,9 @@ const useDialogProps = props => {
57478
57680
  "styleCSSVars": DIALOG_STYLE_CSS_VARS,
57479
57681
  "animationDuration": rest.animationDuration,
57480
57682
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
57481
- "data-backdrop-variant": backdropVariant
57683
+ "data-backdrop-variant": backdropVariant,
57684
+ backdropColor,
57685
+ backdropFilter
57482
57686
  });
57483
57687
  Object.assign(contentProps, {
57484
57688
  tabIndex,
@@ -57512,6 +57716,11 @@ const useDialogProps = props => {
57512
57716
  // as the prop just above (and harmless for the custom renderer, whose
57513
57717
  // real backdrop element gets it via backdropProps).
57514
57718
  "data-backdrop-variant": backdropVariant,
57719
+ // Read by the native ::backdrop, which inherits them from here (see this
57720
+ // file's CSS) — the custom renderer's own backdrop element gets them via
57721
+ // backdropProps above.
57722
+ backdropColor,
57723
+ backdropFilter,
57515
57724
  "styleCSSVars": DIALOG_STYLE_CSS_VARS,
57516
57725
  ...rest,
57517
57726
  ...autoFocusProps,
@@ -57628,6 +57837,8 @@ const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible",
57628
57837
  // an inline border-radius would outrank them.
57629
57838
  const DIALOG_STYLE_CSS_VARS = {
57630
57839
  animationDuration: "--popup-animation-duration",
57840
+ backdropColor: "--backdrop-background",
57841
+ backdropFilter: "--backdrop-filter",
57631
57842
  borderRadius: "--dialog-border-radius",
57632
57843
  minWidth: "--dialog-min-width",
57633
57844
  maxWidth: "--dialog-max-width",
@@ -57861,8 +58072,18 @@ const css$D = /* css */`
57861
58072
  z-index: calc(var(--navi-z-index-popup) + var(--popover-stack-order, 0));
57862
58073
  margin: 0;
57863
58074
  padding: 0;
57864
- background: transparent;
58075
+ /* What this backdrop paints, in two variables the rules below write as
58076
+ defaults — so the backdropColor/backdropFilter props, inline on this
58077
+ very element, beat all of them. Declared unconditionally first so a
58078
+ variable inherited from a dialog this popover was opened from can never
58079
+ reach the paint. A colour and a filter travel together everywhere a
58080
+ backdrop is painted: see navi_css_vars.js for why a blur is not tied to
58081
+ what an outside click does. */
58082
+ --backdrop-background: transparent;
58083
+ --backdrop-filter: none;
58084
+ background: var(--backdrop-background);
57865
58085
  border: none;
58086
+ backdrop-filter: var(--backdrop-filter);
57866
58087
  /* Always clickable while actually rendered (display: none/hidePopover()
57867
58088
  while genuinely closed already makes it non-interactive on its own)
57868
58089
  — an outside click should close the popover even while it's still
@@ -57893,11 +58114,12 @@ const css$D = /* css */`
57893
58114
  /* Makes pointerInteractionOutsideEffect have a visible impact on backdrop */
57894
58115
  &[data-pointer-interaction-outside="close"],
57895
58116
  &[data-pointer-interaction-outside="cancel"] {
57896
- background: var(--navi-backdrop-close-background);
58117
+ --backdrop-background: var(--navi-backdrop-close-background);
58118
+ --backdrop-filter: var(--navi-backdrop-close-backdrop-filter);
57897
58119
  }
57898
58120
  &[data-pointer-interaction-outside="capture"] {
57899
- background: var(--navi-backdrop-capture-background);
57900
- backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
58121
+ --backdrop-background: var(--navi-backdrop-capture-background);
58122
+ --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
57901
58123
  }
57902
58124
 
57903
58125
  /* backdropVariant overrides whatever the effect above picked — same
@@ -57906,12 +58128,14 @@ const css$D = /* css */`
57906
58128
  and still pointer-events: auto, so an outside click keeps doing
57907
58129
  exactly what pointerInteractionOutsideEffect says. */
57908
58130
  &[data-backdrop-variant="discrete"] {
57909
- background: var(--navi-backdrop-discrete-background);
57910
- backdrop-filter: none;
58131
+ --backdrop-background: var(--navi-backdrop-discrete-background);
58132
+ --backdrop-filter: var(--navi-backdrop-discrete-backdrop-filter);
57911
58133
  }
58134
+ /* The one backdrop with nothing to blur through: "invisible" paints
58135
+ nothing at all, and a filter would still be seen. */
57912
58136
  &[data-backdrop-variant="invisible"] {
57913
- background: transparent;
57914
- backdrop-filter: none;
58137
+ --backdrop-background: transparent;
58138
+ --backdrop-filter: none;
57915
58139
  }
57916
58140
 
57917
58141
  /* navi-animation mirrors the content popover's own resolved value (set
@@ -57991,6 +58215,17 @@ const css$D = /* css */`
57991
58215
  * changes how much the popover insists on being the thing you deal with.
57992
58216
  * Ignored when `pointerInteractionOutsideEffect="none"` (there is no
57993
58217
  * backdrop at all then, and outside clicks pass through).
58218
+ * @param {string} [props.backdropColor] - The wash painted over what is
58219
+ * behind, for this popup alone: any CSS color (`"rgb(6 10 20 / 88%)"`).
58220
+ * Wins over `backdropVariant` and over the theme tokens
58221
+ * (`--navi-backdrop-*-background`), which is where the same choice goes
58222
+ * when it is the whole app's.
58223
+ * @param {string} [props.backdropFilter] - What that same wash does to the
58224
+ * picture underneath: `"blur(4px)"` to keep the page behind recognisable
58225
+ * while it stops being readable. A `backdrop-filter` value, so
58226
+ * `saturate()`/`grayscale()` work too. How far what is behind withdraws is
58227
+ * a question of paint, not of what an outside click does — a backdrop that
58228
+ * closes can blur.
57994
58229
  * @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
57995
58230
  * popover so the page/container behind it can't scroll while it's open.
57996
58231
  * @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
@@ -58302,6 +58537,12 @@ const usePopoverProps = props => {
58302
58537
  // the paint the effect implies; "discrete"/"none" tone it down or
58303
58538
  // remove it entirely without giving up the outside click.
58304
58539
  backdropVariant = "auto",
58540
+ // The paint itself, when the tokens behind backdropVariant are not what
58541
+ // this one popover wants. Named/forwarded rather than left in ...rest:
58542
+ // rest lands on the popover element, and these belong to its backdrop,
58543
+ // which is a sibling element (see this file's top comment).
58544
+ backdropColor,
58545
+ backdropFilter,
58305
58546
  scrollCapture,
58306
58547
  focusCapture,
58307
58548
  // "auto" (default) → the popover follows its content. "frozen" → measured
@@ -58896,6 +59137,7 @@ const usePopoverProps = props => {
58896
59137
  // reason: only ever built here.
58897
59138
  return closeEvent => {
58898
59139
  debugPopup(closeEvent, `closePopover()`);
59140
+ clearTextSelectionInside(popoverEl);
58899
59141
  popoverEl.setAttribute("aria-expanded", "false");
58900
59142
  // Set regardless of isTopLayer — see the open side's own identical
58901
59143
  // comment (openEffect above) for why hidePopover() alone isn't
@@ -59011,6 +59253,8 @@ const usePopoverProps = props => {
59011
59253
  "animationDuration": rest.animationDuration,
59012
59254
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
59013
59255
  "data-backdrop-variant": backdropVariant,
59256
+ backdropColor,
59257
+ backdropFilter,
59014
59258
  "onMouseDown": mouseDownEvent => {
59015
59259
  if (mouseDownEvent.button !== 0) {
59016
59260
  return;
@@ -59115,6 +59359,8 @@ const POPOVER_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible",
59115
59359
  // handling).
59116
59360
  const POPUP_STYLE_CSS_VARS = {
59117
59361
  animationDuration: "--popup-animation-duration",
59362
+ backdropColor: "--backdrop-background",
59363
+ backdropFilter: "--backdrop-filter",
59118
59364
  minWidth: "--popover-min-width",
59119
59365
  maxWidth: "--popover-max-width",
59120
59366
  minHeight: "--popover-min-height",
@@ -59233,6 +59479,10 @@ const css$C = /* css */`@layer navi {
59233
59479
  * how visible the backdrop is, independently of what an outside click
59234
59480
  * does. Unlike `pointerInteractionOutsideEffect` above, this one needs no
59235
59481
  * default here — `"auto"` already means the same thing on both sides.
59482
+ * @param {string} [props.backdropColor] - Forwarded as-is (both understand it
59483
+ * identically): the wash the backdrop paints over what is behind.
59484
+ * @param {string} [props.backdropFilter] - Forwarded as-is: what that wash
59485
+ * does to the picture underneath, `"blur(4px)"` and the like.
59236
59486
  * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"expanding"|`slide-from-${string}`|`expand-${string}`} [props.animation]
59237
59487
  * - Forwarded as-is.
59238
59488
  * @param {string} [props.animationDuration] - Forwarded as-is.
@@ -60101,6 +60351,8 @@ const PickerContentInsidePopup = props => {
60101
60351
  // Named/forwarded rather than left in ...rest: rest goes to the picker
60102
60352
  // element itself, not the popup, and this belongs to the popup.
60103
60353
  backdropVariant,
60354
+ backdropColor,
60355
+ backdropFilter,
60104
60356
  dialogExpand,
60105
60357
  dialogExpandX,
60106
60358
  dialogExpandY,
@@ -60176,6 +60428,8 @@ const PickerContentInsidePopup = props => {
60176
60428
  scrollCapture: scrollCapture,
60177
60429
  pointerInteractionOutsideEffect: pointerLock ? "capture" : pointerInteractionOutsideEffect,
60178
60430
  backdropVariant: backdropVariant,
60431
+ backdropColor: backdropColor,
60432
+ backdropFilter: backdropFilter,
60179
60433
  focusCapture: isPopover ? focusCapture : undefined,
60180
60434
  expand: isPopover ? undefined : dialogExpand,
60181
60435
  expandX: isPopover ? undefined : dialogExpandX,
@@ -71251,6 +71505,8 @@ const css$n = /* css */`.navi_split_button {
71251
71505
  * dockedOnSmallTouchScreen?: boolean,
71252
71506
  * marginWithContainer?: number | string,
71253
71507
  * backdropVariant?: "auto" | "discrete" | "invisible",
71508
+ * backdropColor?: string,
71509
+ * backdropFilter?: string,
71254
71510
  * pointerInteractionOutsideEffect?: "close" | "cancel" | "capture",
71255
71511
  * escapeEffect?: "cancel" | "close",
71256
71512
  * popupLayer?: "top" | "local",
@@ -71297,6 +71553,7 @@ const css$n = /* css */`.navi_split_button {
71297
71553
  * `dialogMaxWidth`/`Height`,
71298
71554
  * `marginWithContainer`, `popoverMode`, `popoverSpacing`, `popupLayer`,
71299
71555
  * `popupTestId`, `popupWidthFitContent`, `popoverMaxHeight`, `backdropVariant`,
71556
+ * `backdropColor`, `backdropFilter`,
71300
71557
  * `pointerInteractionOutsideEffect`, `escapeEffect`, `closeOnFocusOut`,
71301
71558
  * `scrollCapture`, `focusCapture`, `popupBackgroundColor`,
71302
71559
  * `popupBorderRadius`, `animation`. See picker.jsx for what each one says.
@@ -71497,7 +71754,7 @@ const SplitButton = props => {
71497
71754
  // What the Picker's popup answers to — Picker's own popup props, named here so
71498
71755
  // a caller reaches all of them through the split button (see picker.jsx's JSDoc
71499
71756
  // for what each one says).
71500
- 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"]);
71757
+ const POPUP_PROP_SET = new Set(["mode", "popupLayer", "popupTestId", "positionArea", "popoverMode", "popoverSpacing", "popupWidthFitContent", "popoverMaxHeight", "dialogMinWidth", "dialogMinHeight", "dialogMaxWidth", "dialogMaxHeight", "dialogExpand", "dialogExpandX", "dialogExpandY", "dockedOnSmallTouchScreen", "marginWithContainer", "backdropVariant", "backdropColor", "backdropFilter", "pointerInteractionOutsideEffect", "escapeEffect", "closeOnFocusOut", "scrollCapture", "focusCapture", "popupBackgroundColor", "popupBorderRadius", "animation"]);
71501
71758
  const splitPopupProps = props => {
71502
71759
  const popupProps = {};
71503
71760
  const boxProps = {};