@jsenv/navi 0.29.465 → 0.29.466

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
5
  import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
- import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, createEventGroupLogger, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, trapScrollInside, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange } from "@jsenv/dom";
6
+ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, createEventGroupLogger, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, trapScrollInside, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, overflowGoesToViewport, parsePositionArea, snapToPixel, trapFocusInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange } from "@jsenv/dom";
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
9
9
  import { options, isValidElement, createContext, render as render$1, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
@@ -6799,6 +6799,12 @@ const isAimedAtSelfInteractionsBelow = (
6799
6799
  if (!target || typeof target.closest !== "function") {
6800
6800
  return false;
6801
6801
  }
6802
+ if (event.type === "keydown" || event.type === "keyup") {
6803
+ // A key is no press: the claim is the pointer's. An Escape pressed while
6804
+ // the focus rests on the claimer asks the controls around it — the sheet
6805
+ // it sits in closing.
6806
+ return false;
6807
+ }
6802
6808
  const claimer = target.closest(CLICK_SELECTOR);
6803
6809
  if (!claimer) {
6804
6810
  return false;
@@ -16715,6 +16721,17 @@ const createOpenController = (
16715
16721
  { opened: false, event: closeEvent },
16716
16722
  );
16717
16723
  };
16724
+ // A cancel an Escape asked for, read with what Escape means for this popup.
16725
+ const readCloseDetail = (e, detail) => {
16726
+ if (
16727
+ detail?.isCancel &&
16728
+ controller.escapeEffect !== "cancel" &&
16729
+ findEvent(e, isEscapeKeydown)
16730
+ ) {
16731
+ return { ...detail, isCancel: false };
16732
+ }
16733
+ return detail;
16734
+ };
16718
16735
  const controller = {
16719
16736
  opened: false,
16720
16737
  // What the DOM currently shows, as opposed to what the controller has
@@ -16746,6 +16763,11 @@ const createOpenController = (
16746
16763
  // whole point: learning it afterwards means the content mounted on the
16747
16764
  // previous subject first.
16748
16765
  onOpen: null,
16766
+ // What an Escape means for this popup, whoever heard the key — its own
16767
+ // shortcut, its trigger's, the group it stands in, a bubble's: a cancel
16768
+ // (what it holds reverts), unless its owner says otherwise (a Picker's
16769
+ // escapeEffect="close"). Read from the close the key asked for.
16770
+ escapeEffect: "cancel",
16749
16771
  // The counterpart, set only when the popup was told to throw its content
16750
16772
  // away on close (`mount="while-opened"`). Called from performClose above.
16751
16773
  unmountContent: null,
@@ -16915,7 +16937,7 @@ const createOpenController = (
16915
16937
  return true;
16916
16938
  }
16917
16939
  const requestCloseEvent = new CustomEvent("navi_request_close", {
16918
- detail: { event: e, ...detail },
16940
+ detail: { event: e, ...readCloseDetail(e, detail) },
16919
16941
  cancelable: true,
16920
16942
  });
16921
16943
  chainEvent(requestCloseEvent, e);
@@ -16925,10 +16947,7 @@ const createOpenController = (
16925
16947
  // the popup (a PopupGroup, a dialog), closing what this popup kept open,
16926
16948
  // or what is still closing with it (a lifting dialog closes a frame
16927
16949
  // later).
16928
- const escapeKeydown = findEvent(
16929
- requestCloseEvent,
16930
- (event) => event.type === "keydown" && event.key === "Escape",
16931
- );
16950
+ const escapeKeydown = findEvent(requestCloseEvent, isEscapeKeydown);
16932
16951
  if (escapeKeydown) {
16933
16952
  escapeKeydown.preventDefault();
16934
16953
  }
@@ -16950,7 +16969,7 @@ const createOpenController = (
16950
16969
  return;
16951
16970
  }
16952
16971
  const closeEvent = new CustomEvent("navi_close", {
16953
- detail: { event: e, ...detail },
16972
+ detail: { event: e, ...readCloseDetail(e, detail) },
16954
16973
  });
16955
16974
  chainEvent(closeEvent, e);
16956
16975
  // Skips onRequestClose entirely — there is no choice here.
@@ -16961,6 +16980,9 @@ const createOpenController = (
16961
16980
  };
16962
16981
 
16963
16982
  // A click a keyboard made (detail 0) is not one: nothing pressed for it.
16983
+ const isEscapeKeydown = (event) =>
16984
+ event.type === "keydown" && event.key === "Escape";
16985
+
16964
16986
  const isEventOfAPress = (event) =>
16965
16987
  event.type === "pointerdown" ||
16966
16988
  event.type === "mousedown" ||
@@ -18617,8 +18639,10 @@ const openCallout = (
18617
18639
  }
18618
18640
  calloutCloseButton.tabIndex = -1;
18619
18641
  calloutCloseButton.setAttribute("navi-focus-delegate", "");
18642
+ // One Escape closes one popup: an Escape already spent by one in front
18643
+ // of the callout — a menu opened from its content — is not this one's.
18620
18644
  const onAnchorKeydown = (e) => {
18621
- if (e.key === "Escape") {
18645
+ if (e.key === "Escape" && !e.defaultPrevented) {
18622
18646
  requestClose(e, "escape_key");
18623
18647
  e.stopPropagation();
18624
18648
  e.preventDefault();
@@ -18633,7 +18657,7 @@ const openCallout = (
18633
18657
  // Usually callout is inside anchor so the anchor would see the escape being pressed
18634
18658
  // but callout can be appended outside anchor when anchor cannot receive children
18635
18659
  const onCalloutKeydown = (e) => {
18636
- if (e.key === "Escape") {
18660
+ if (e.key === "Escape" && !e.defaultPrevented) {
18637
18661
  requestClose(e, "escape_key");
18638
18662
  e.stopPropagation();
18639
18663
  e.preventDefault();
@@ -18679,21 +18703,23 @@ const openCallout = (
18679
18703
  //
18680
18704
  // Only for the ones that cannot be heard otherwise, and only while they are
18681
18705
  // open: a callout the anchor can hear about is left to the anchor, so
18682
- // Escape goes on meaning what it means where the user is.
18706
+ // Escape goes on meaning what it means where the user is. Heard once the
18707
+ // key has been through what it was pressed in: one that answered it (a
18708
+ // popup closing, a field clearing) spent it.
18683
18709
  if (anchorElement && anchorElement.contains(document.activeElement)) {
18684
18710
  break close_on_escape_from_anywhere;
18685
18711
  }
18686
18712
  const onDocumentKeydown = (e) => {
18687
- if (e.key !== "Escape") {
18713
+ if (e.key !== "Escape" || e.defaultPrevented) {
18688
18714
  return;
18689
18715
  }
18690
18716
  requestClose(e, "escape_key");
18691
18717
  e.stopPropagation();
18692
18718
  e.preventDefault();
18693
18719
  };
18694
- document.addEventListener("keydown", onDocumentKeydown, true);
18720
+ document.addEventListener("keydown", onDocumentKeydown);
18695
18721
  addTeardown(() => {
18696
- document.removeEventListener("keydown", onDocumentKeydown, true);
18722
+ document.removeEventListener("keydown", onDocumentKeydown);
18697
18723
  });
18698
18724
  }
18699
18725
  {
@@ -35312,7 +35338,7 @@ const readOuterScroll = (element) => {
35312
35338
  };
35313
35339
 
35314
35340
  const isDocumentScroller = (node) => {
35315
- return node === document.documentElement || node === document.body;
35341
+ return node === document.documentElement;
35316
35342
  };
35317
35343
 
35318
35344
  const isPinnedToViewport = (node) => {
@@ -38538,12 +38564,12 @@ const whilePageRenders = async (page, change, wait = armRouteRenderWait()) => {
38538
38564
  // (`overflow: clip`) holds nothing and is walked straight through — see
38539
38565
  // is_scrollable.js.
38540
38566
  const pagesScrollTheDocument = element => {
38541
- const scrollContainer = getScrollContainer(element, {
38567
+ // What window.scrollTo moves. A body whose overflow is the viewport's is
38568
+ // walked through to it (see overflowGoesToViewport in @jsenv/dom); a body
38569
+ // the walk stops on scrolls itself, out of the viewport's reach.
38570
+ return getScrollContainer(element, {
38542
38571
  includeHidden: true
38543
- });
38544
- // html and body are one answer: whichever of them the walk stops on, what
38545
- // scrolls is the viewport, which is what window.scrollTo moves.
38546
- return scrollContainer === document.documentElement || scrollContainer === document.body;
38572
+ }) === document.documentElement;
38547
38573
  };
38548
38574
 
38549
38575
  // A page of the row: a route, and the params that say which of its tabs when
@@ -67280,6 +67306,8 @@ const engageGroup = (groupEl, group, {
67280
67306
  const themeColor = readPopupThemeColor(groupEl, group.themeColor);
67281
67307
  const focusedBefore = document.activeElement;
67282
67308
  const focusWasInGroup = Boolean(focusedBefore) && focusedBefore !== groupEl && groupEl.contains(focusedBefore);
67309
+ // What the focus goes back to, when nothing holds it any more at the close.
67310
+ const focusOrigin = !focusWasInGroup && focusedBefore && focusedBefore !== document.body && focusedBefore !== groupEl ? focusedBefore : null;
67283
67311
  const ringBefore = Boolean(focusedBefore?.matches?.(":focus-visible"));
67284
67312
  const {
67285
67313
  scrollTop,
@@ -67287,12 +67315,7 @@ const engageGroup = (groupEl, group, {
67287
67315
  } = groupEl;
67288
67316
  // A gesture on a box fixed in the top layer goes to the viewport: only a
67289
67317
  // place that scrolls with the viewport moves under it.
67290
- const placeScroller = getScrollContainer(groupEl);
67291
- const placeScrollsWithViewport = (placeScroller === document.scrollingElement ||
67292
- // Reported as a scroll container on an app setting `body {
67293
- // overflow-x: hidden }` (its computed overflow-y turns auto), it never
67294
- // scrolls itself: the viewport does.
67295
- placeScroller === document.body && bodyOverflowIsTheViewports()) && !findSelfOrAncestorFixedPosition(groupEl);
67318
+ const placeScrollsWithViewport = getScrollContainer(groupEl) === document.scrollingElement && !findSelfOrAncestorFixedPosition(groupEl);
67296
67319
  const placeholder = createPlaceholder(groupStyle, rect, baselineDescent);
67297
67320
  groupEl.before(placeholder);
67298
67321
  groupEl.style.setProperty("--x-popup-group-padding", `${groupStyle.paddingTop} ${groupStyle.paddingRight} ${groupStyle.paddingBottom} ${groupStyle.paddingLeft}`);
@@ -67300,25 +67323,49 @@ const engageGroup = (groupEl, group, {
67300
67323
  if (silent) {
67301
67324
  groupEl.setAttribute("navi-transitions-off", "");
67302
67325
  }
67303
- // showModal() runs the dialog focusing steps: it focuses the first element
67304
- // marked autofocus, or else the first focusable the group holds — and
67305
- // scrolls the group's own scroller to it. Pointed at what already has the
67306
- // focus, or at the group itself, with any other mark set aside for the
67307
- // call, so nothing moves. A native autofocus is otherwise never used (see
67308
- // the focus skill): this one exists for the length of the call, to steer
67309
- // the browser's own pick, not to transfer anything.
67310
- const autofocusTarget = focusWasInGroup ? focusedBefore : groupEl;
67326
+ // showModal() runs the dialog focusing steps — it focuses the first element
67327
+ // marked autofocus, or else the first focusable the group holds, and
67328
+ // scrolls the group's own scroller to it — and records what held the focus,
67329
+ // for close() to give it back there. A native autofocus is otherwise never
67330
+ // used (see the focus skill): these exist for the length of the call, to
67331
+ // steer the browser's own pick, not to transfer anything.
67332
+ //
67333
+ // From inside the group the pick is what holds the focus already, any other
67334
+ // mark set aside, so nothing moves. From outside, a stop is put first in the
67335
+ // group and focused: it is what showModal() picks and records, and it is
67336
+ // gone by the close, which then moves nothing (see cleanup) — what the focus
67337
+ // came from, a sheet around the group, could not be kept from taking it
67338
+ // back. The focus goes on to the group itself, so no peer is focused on the
67339
+ // way.
67340
+ let focusStop = null;
67341
+ if (!focusWasInGroup) {
67342
+ focusStop = document.createElement("span");
67343
+ focusStop.setAttribute("navi-out-of-flow", "");
67344
+ focusStop.setAttribute("aria-hidden", "true");
67345
+ focusStop.tabIndex = -1;
67346
+ focusStop.style.cssText = "position: absolute; width: 0; height: 0; overflow: hidden";
67347
+ groupEl.prepend(focusStop);
67348
+ moveFocusTo(focusStop, {
67349
+ focusVisible: false
67350
+ });
67351
+ }
67352
+ const autofocusTarget = focusWasInGroup ? focusedBefore : focusStop;
67311
67353
  const autofocusWasSet = autofocusTarget.hasAttribute("autofocus");
67312
- const autofocusSetAside = focusWasInGroup ? [...groupEl.querySelectorAll("[autofocus]")].filter(element => element !== autofocusTarget) : [];
67354
+ const autofocusSetAside = [...groupEl.querySelectorAll("[autofocus]")].filter(element => element !== autofocusTarget);
67313
67355
  for (const element of autofocusSetAside) {
67314
67356
  element.removeAttribute("autofocus");
67315
67357
  }
67316
67358
  if (!autofocusWasSet) {
67317
67359
  autofocusTarget.setAttribute("autofocus", "");
67318
67360
  }
67361
+ // Chromium takes a <dialog> without a tabindex for no focus target. Lent
67362
+ // for the whole engagement: taken back from the group holding the focus,
67363
+ // it would lose it.
67364
+ const tabindexLent = Boolean(focusStop) && !groupEl.hasAttribute("tabindex");
67319
67365
  try {
67320
67366
  groupEl.showModal();
67321
67367
  } catch (error) {
67368
+ focusStop?.remove();
67322
67369
  placeholder.remove();
67323
67370
  clearPlacement(groupEl);
67324
67371
  groupEl.removeAttribute("navi-transitions-off");
@@ -67331,6 +67378,15 @@ const engageGroup = (groupEl, group, {
67331
67378
  element.setAttribute("autofocus", "");
67332
67379
  }
67333
67380
  }
67381
+ if (focusStop) {
67382
+ if (tabindexLent) {
67383
+ groupEl.tabIndex = -1;
67384
+ }
67385
+ moveFocusTo(groupEl, {
67386
+ focusVisible: false
67387
+ });
67388
+ focusStop.remove();
67389
+ }
67334
67390
  // Measured once the group has left the flow, where a parent sized by its
67335
67391
  // content no longer counts it. Only a box given its width by what holds it
67336
67392
  // goes on following that width (a rotation while engaged); one sized by its
@@ -67353,9 +67409,6 @@ const engageGroup = (groupEl, group, {
67353
67409
  focusVisible: ringBefore
67354
67410
  });
67355
67411
  }
67356
- // What showModal() left focused when the focus came from outside: that is
67357
- // the browser's pick, not a restore of navi's, at the close (see cleanup).
67358
- const focusLeftByShowModal = focusWasInGroup ? null : document.activeElement;
67359
67412
  if (silent) {
67360
67413
  // The wall's starting style is resolved with no transition to play from
67361
67414
  // it; giving the transition back afterwards changes no value, so nothing
@@ -67387,33 +67440,55 @@ const engageGroup = (groupEl, group, {
67387
67440
  placeholder.remove();
67388
67441
  if (leavingTree) {
67389
67442
  // Closed once the commit is over, as a Dialog leaving the tree is (see
67390
- // its close cleanup): closing now would hand the focus to what held it
67391
- // at showModal() time, often leaving in this same commit. A park
67392
- // takes the group out of the top layer and leaves its `open`
67393
- // attribute, which the reveal would show.
67443
+ // its close cleanup). A park takes the group out of the top layer and
67444
+ // leaves its `open` attribute, which the reveal would show.
67394
67445
  queueMicrotask(() => {
67395
67446
  if (groupEl.open) {
67396
67447
  groupEl.close();
67397
67448
  }
67449
+ if (tabindexLent) {
67450
+ groupEl.removeAttribute("tabindex");
67451
+ }
67398
67452
  });
67453
+ if (focusOrigin) {
67454
+ // The focus left with the group: back where it was, if that stays.
67455
+ giveFocusBackAfterCommit(focusOrigin, () => {
67456
+ moveFocusTo(focusOrigin, {
67457
+ focusVisible: ringBefore
67458
+ });
67459
+ });
67460
+ }
67399
67461
  return;
67400
67462
  }
67401
- // close() hands the focus back to whatever held it at showModal() time
67402
- // (focusedBefore), undoing where a popup of the group, or the user, put
67403
- // it since. Unless no popup moved the focus since showModal() did, that
67404
- // element is made no focus target for the call, so the focus stays where
67405
- // it is: inert — or, when the focus is inside it (the sheet around the
67406
- // popover around the group, focused by a press on its blank), without
67407
- // its tabindex, inert taking the focus with it. A <dialog> stays
67408
- // focusable without one: around a Dialog sheet, close() focuses it and
67409
- // the moveFocusTo below brings the focus back.
67410
67463
  const focusedBeforeClose = document.activeElement;
67464
+ if (!focusWasInGroup) {
67465
+ // What showModal() recorded is the stop, gone by now: close() moves
67466
+ // nothing, and the focus stays where a popup of the group, or the
67467
+ // user, put it. Only one still on the group, or lost, goes back to
67468
+ // where it came from.
67469
+ if (groupEl.open) {
67470
+ groupEl.close();
67471
+ }
67472
+ if (focusOrigin && focusOrigin.isConnected && (!focusedBeforeClose || focusedBeforeClose === document.body || focusedBeforeClose === groupEl)) {
67473
+ moveFocusTo(focusOrigin, {
67474
+ focusVisible: ringBefore
67475
+ });
67476
+ }
67477
+ if (tabindexLent) {
67478
+ groupEl.removeAttribute("tabindex");
67479
+ }
67480
+ return;
67481
+ }
67482
+ // Coming from inside the group, the focus was recorded where it was,
67483
+ // and close() gives it back there, undoing where a popup of the group,
67484
+ // or the user, put it since. Unless nothing moved it, that element is
67485
+ // made no focus target for the call: inert — or, holding the focus
67486
+ // inside it, without its tabindex, inert taking the focus with it.
67411
67487
  const ringBeforeClose = Boolean(focusedBeforeClose?.matches?.(":focus-visible"));
67412
- const focusMovedSinceShowModal = focusedBeforeClose && focusedBeforeClose !== document.body && focusedBeforeClose !== groupEl && focusedBeforeClose !== focusLeftByShowModal && focusedBeforeClose.isConnected;
67413
- const keepsFocusWhereItIs = focusMovedSinceShowModal && focusedBefore && focusedBefore !== focusedBeforeClose && focusedBefore !== document.body && focusedBefore.isConnected;
67414
- const holdsFocusInside = keepsFocusWhereItIs && focusedBefore.contains(focusedBeforeClose);
67415
- const madeInert = keepsFocusWhereItIs && !holdsFocusInside && !focusedBefore.inert;
67416
- const tabindexSetAside = holdsFocusInside && focusedBefore.tagName !== "DIALOG" ? focusedBefore.getAttribute("tabindex") : null;
67488
+ const focusMovedInGroup = focusWasInGroup && focusedBeforeClose && focusedBeforeClose !== document.body && focusedBeforeClose !== groupEl && focusedBeforeClose !== focusedBefore && focusedBeforeClose.isConnected && focusedBefore.isConnected;
67489
+ const holdsFocusInside = focusMovedInGroup && focusedBefore.contains(focusedBeforeClose);
67490
+ const madeInert = focusMovedInGroup && !holdsFocusInside && !focusedBefore.inert;
67491
+ const tabindexSetAside = holdsFocusInside ? focusedBefore.getAttribute("tabindex") : null;
67417
67492
  if (madeInert) {
67418
67493
  focusedBefore.inert = true;
67419
67494
  }
@@ -67431,10 +67506,13 @@ const engageGroup = (groupEl, group, {
67431
67506
  if (tabindexSetAside !== null) {
67432
67507
  focusedBefore.setAttribute("tabindex", tabindexSetAside);
67433
67508
  }
67509
+ if (tabindexLent) {
67510
+ groupEl.removeAttribute("tabindex");
67511
+ }
67434
67512
  }
67435
- // Where the engine moved it anyway (an element focusable without a
67436
- // tabindex).
67437
- if (focusMovedSinceShowModal && document.activeElement !== focusedBeforeClose) {
67513
+ // Where the engine moved it anyway: an element focusable without a
67514
+ // tabindex.
67515
+ if (focusMovedInGroup && document.activeElement !== focusedBeforeClose) {
67438
67516
  moveFocusTo(focusedBeforeClose, {
67439
67517
  focusVisible: ringBeforeClose
67440
67518
  });
@@ -67618,7 +67696,8 @@ const engageGroup = (groupEl, group, {
67618
67696
  * screen it is synthesized at touchend and hit-tested again, with the group
67619
67697
  * still up, so a finger that scrolled the group (pointercancel, no mousedown)
67620
67698
  * closes nothing. A pointerdown cancelled downstream (a drag source) brings no
67621
- * mousedown: its click decides instead. A popup that captures the outside is
67699
+ * mousedown: its click decides instead, or the start of the drag it turns
67700
+ * into, which brings no click. A popup that captures the outside is
67622
67701
  * answered at the pointerdown itself: nothing of that press may reach a peer.
67623
67702
  */
67624
67703
  const armPressArbiter = (groupEl, group, engagement) => {
@@ -67787,7 +67866,28 @@ const armPressArbiter = (groupEl, group, engagement) => {
67787
67866
  engagement.checkAfterTask();
67788
67867
  }
67789
67868
  };
67790
- const listeners = [["pointerdown", onPointerDown], ["mousedown", onMouseDown], ["click", onClick], ["pointerup", onPointerUp], ["pointercancel", onPointerCancel]];
67869
+ // A press that becomes a drag brings no click: decided at the click, it
67870
+ // would leave the open popup over a card being carried away, its own
67871
+ // trigger maybe. A drag starting outside the open popup closes it, as a
67872
+ // press there does. Nothing is left to hand over to then, so the group goes
67873
+ // back into the page at once — where the drag can land: what is outside the
67874
+ // group is inert while it is engaged.
67875
+ const onDragStart = dragStartEvent => {
67876
+ const openMember = findMemberPressedOutside(dragStartEvent);
67877
+ if (openMember) {
67878
+ const pressOutside = readPressOutside(openMember);
67879
+ if (pressOutside !== "capture") {
67880
+ openMember.openController.requestClose(dragStartEvent, {
67881
+ isCancel: pressOutside === "cancel"
67882
+ });
67883
+ }
67884
+ }
67885
+ if (engagement.pressClosed) {
67886
+ engagement.pressClosed = false;
67887
+ group.disengageIfNothingOpen();
67888
+ }
67889
+ };
67890
+ const listeners = [["pointerdown", onPointerDown], ["mousedown", onMouseDown], ["click", onClick], ["pointerup", onPointerUp], ["pointercancel", onPointerCancel], ["navi_drag_start", onDragStart]];
67791
67891
  for (const [type, listener] of listeners) {
67792
67892
  document.addEventListener(type, listener, {
67793
67893
  capture: true
@@ -68023,8 +68123,6 @@ const readAncestorClip = (element, elementStyle) => {
68023
68123
  if (elementStyle.position === "fixed") {
68024
68124
  return null;
68025
68125
  }
68026
- // Read on body, an overflow that is the viewport's clips nothing.
68027
- const bodyClipsNothing = bodyOverflowIsTheViewports();
68028
68126
  let left = -Infinity;
68029
68127
  let top = -Infinity;
68030
68128
  let right = Infinity;
@@ -68046,9 +68144,10 @@ const readAncestorClip = (element, elementStyle) => {
68046
68144
  overflowY,
68047
68145
  position
68048
68146
  } = ancestorStyle;
68049
- const overflowGoesToViewport = ancestor === document.body && bodyClipsNothing;
68050
- const clipsX = !overflowGoesToViewport && overflowX !== "visible";
68051
- const clipsY = !overflowGoesToViewport && overflowY !== "visible";
68147
+ // Read on a body whose overflow is the viewport's, it clips nothing.
68148
+ const clipsNothing = overflowGoesToViewport(ancestor);
68149
+ const clipsX = !clipsNothing && overflowX !== "visible";
68150
+ const clipsY = !clipsNothing && overflowY !== "visible";
68052
68151
  if (clipsX || clipsY) {
68053
68152
  clipped = true;
68054
68153
  const ancestorRect = ancestor.getBoundingClientRect();
@@ -68088,12 +68187,6 @@ const readAncestorClip = (element, elementStyle) => {
68088
68187
  bottom
68089
68188
  } : null;
68090
68189
  };
68091
- // The overflow of <body> is the viewport's when the root leaves its own
68092
- // visible: body itself then neither scrolls nor clips.
68093
- const bodyOverflowIsTheViewports = () => {
68094
- const rootStyle = getComputedStyle(document.documentElement);
68095
- return rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
68096
- };
68097
68190
  const isContainingBlockForAbsolute = style => style.position !== "static" || style.transform !== "none" || style.translate !== "none" || style.rotate !== "none" || style.scale !== "none" || style.perspective !== "none" || style.filter !== "none" ||
68098
68191
  // Prefixed alone before Safari 18.
68099
68192
  (style.backdropFilter ?? style.webkitBackdropFilter ?? "none") !== "none" || /\b(?:layout|paint|strict|content)\b/.test(style.contain) ||
@@ -71253,9 +71346,13 @@ const useDialogProps = props => {
71253
71346
  };
71254
71347
  const onKeyDownShortcuts = createOnKeyDownForShortcuts({
71255
71348
  escape: e => {
71256
- // Only a modal <dialog> fires "cancel" on Escape natively (handled via
71257
- // onCancel below); neither a .show()n nor a showPopover()n one does.
71258
- if (isModal || !openController.opened) {
71349
+ // Answered at the keydown, modal or not: the innermost popup takes the
71350
+ // key and spends it (requestClose prevents it), so one Escape closes
71351
+ // one popup. A modal's native "cancel" comes only once the keydown has
71352
+ // been dispatched, and the key would reach a popup around this one
71353
+ // first — a picker whose drawing holds this dialog. onCancel below is
71354
+ // left the close requests that are no keydown: the back gesture.
71355
+ if (!openController.opened) {
71259
71356
  return null;
71260
71357
  }
71261
71358
  return {
@@ -71436,9 +71533,9 @@ const useDialogProps = props => {
71436
71533
  onKeyDownShortcuts(e);
71437
71534
  },
71438
71535
  "onCancel": e => {
71439
- // The dialog's own "cancel" (Escape on a modal showModal() dialog —
71440
- // the Escape handling for the other two shows lives in
71441
- // onKeyDownShortcuts above) fires on the dialog element itself. But a child
71536
+ // The dialog's own "cancel" — a close request that is no keydown (the
71537
+ // back gesture), an Escape being answered at its keydown (see
71538
+ // onKeyDownShortcuts above) — fires on the dialog element itself. But a child
71442
71539
  // <input type="file"> also fires a BUBBLING "cancel" when the user
71443
71540
  // dismisses the file chooser (per the HTML spec), and it lands here
71444
71541
  // too; only the dialog's own cancel means "close".
@@ -74119,6 +74216,10 @@ const PickerCustom = props => {
74119
74216
  }
74120
74217
  };
74121
74218
  });
74219
+ // Whoever hears the Escape — this picker's trigger, its popup, the group
74220
+ // it stands in, its bubble — asks for a cancel; the controller reads it
74221
+ // with this (see escapeEffect in open_controller.js).
74222
+ openController.escapeEffect = escapeEffect;
74122
74223
  // scroll <button> of the picker into view when opening it
74123
74224
  // -> would be overriden by dialog.jsx or popover.jsx
74124
74225
  // so ideally openEffect should be either protective or a pubSub to allow multiple callbacks
@@ -74344,13 +74445,13 @@ const PickerCustom = props => {
74344
74445
  if (!openController.opened) {
74345
74446
  return null;
74346
74447
  }
74347
- const isCancel = escapeEffect === "cancel";
74348
74448
  return {
74349
- name: isCancel ? "escape_to_cancel" : "escape_to_close",
74449
+ name: "escape_to_cancel",
74350
74450
  intent: "read",
74351
74451
  allowed: () => {
74452
+ // Read with escapeEffect by the controller (see above).
74352
74453
  requestClose(e, {
74353
- isCancel
74454
+ isCancel: true
74354
74455
  });
74355
74456
  e.preventDefault(); // prevent browser from closing the dialog (if any)
74356
74457
  }
@@ -74502,10 +74603,7 @@ const PickerCustom = props => {
74502
74603
  }
74503
74604
  return renderResolver(PickerContentInsidePopup, {
74504
74605
  ...pickerProps,
74505
- mode,
74506
- // What a callout's own Escape means (see PickerCalloutPopup): the bubble
74507
- // answers the key itself when the focus is in it.
74508
- escapeEffect
74606
+ mode
74509
74607
  });
74510
74608
  };
74511
74609
 
@@ -74620,7 +74718,6 @@ const PickerContentInsidePopup = props => {
74620
74718
  calloutIcon,
74621
74719
  calloutCloseButton,
74622
74720
  calloutCloseByScroll,
74623
- escapeEffect,
74624
74721
  ...rest
74625
74722
  } = props;
74626
74723
  const isPopover = mode === "popover";
@@ -74669,7 +74766,6 @@ const PickerContentInsidePopup = props => {
74669
74766
  ,
74670
74767
  backdrop: backdrop,
74671
74768
  pressOutside: pressOutside,
74672
- escapeEffect: escapeEffect,
74673
74769
  children: jsx(PopupModeContext.Provider, {
74674
74770
  value: mode,
74675
74771
  children: children
@@ -74767,7 +74863,6 @@ const PickerCalloutPopup = ({
74767
74863
  closeByScroll,
74768
74864
  backdrop,
74769
74865
  pressOutside,
74770
- escapeEffect,
74771
74866
  onnavi_request_open,
74772
74867
  onnavi_request_close,
74773
74868
  onnavi_request_confirm,
@@ -74849,14 +74944,14 @@ const PickerCalloutPopup = ({
74849
74944
  // Not skipped: the callout moves the focus into the picker when it is
74850
74945
  // elsewhere, which is what lets Escape find the callout right away.
74851
74946
  skipFocus: false,
74852
- // An Escape the callout answered itself (the focus in the bubble) means
74853
- // what the picker's own Escape means.
74947
+ // An Escape the callout answered itself (the focus in the bubble) is
74948
+ // the picker's Escape (see escapeEffect in open_controller.js).
74854
74949
  onClose: ({
74855
74950
  event,
74856
74951
  reason
74857
74952
  }) => {
74858
74953
  openController.close(event, {
74859
- isCancel: reason === "escape_key" && escapeEffect === "cancel"
74954
+ isCancel: reason === "escape_key"
74860
74955
  });
74861
74956
  }
74862
74957
  });