@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, 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, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } from "@jsenv/dom";
6
+ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, 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, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } 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";
@@ -6554,6 +6554,12 @@ const isAimedAtSelfInteractionsBelow = (
6554
6554
  if (!target || typeof target.closest !== "function") {
6555
6555
  return false;
6556
6556
  }
6557
+ if (event.type === "keydown" || event.type === "keyup") {
6558
+ // A key is no press: the claim is the pointer's. An Escape pressed while
6559
+ // the focus rests on the claimer asks the controls around it — the sheet
6560
+ // it sits in closing.
6561
+ return false;
6562
+ }
6557
6563
  const claimer = target.closest(CLICK_SELECTOR);
6558
6564
  if (!claimer) {
6559
6565
  return false;
@@ -16120,6 +16126,17 @@ const createOpenController = (
16120
16126
  { opened: false, event: closeEvent },
16121
16127
  );
16122
16128
  };
16129
+ // A cancel an Escape asked for, read with what Escape means for this popup.
16130
+ const readCloseDetail = (e, detail) => {
16131
+ if (
16132
+ detail?.isCancel &&
16133
+ controller.escapeEffect !== "cancel" &&
16134
+ findEvent(e, isEscapeKeydown)
16135
+ ) {
16136
+ return { ...detail, isCancel: false };
16137
+ }
16138
+ return detail;
16139
+ };
16123
16140
  const controller = {
16124
16141
  opened: false,
16125
16142
  // What the DOM currently shows, as opposed to what the controller has
@@ -16151,6 +16168,11 @@ const createOpenController = (
16151
16168
  // whole point: learning it afterwards means the content mounted on the
16152
16169
  // previous subject first.
16153
16170
  onOpen: null,
16171
+ // What an Escape means for this popup, whoever heard the key — its own
16172
+ // shortcut, its trigger's, the group it stands in, a bubble's: a cancel
16173
+ // (what it holds reverts), unless its owner says otherwise (a Picker's
16174
+ // escapeEffect="close"). Read from the close the key asked for.
16175
+ escapeEffect: "cancel",
16154
16176
  // The counterpart, set only when the popup was told to throw its content
16155
16177
  // away on close (`mount="while-opened"`). Called from performClose above.
16156
16178
  unmountContent: null,
@@ -16320,7 +16342,7 @@ const createOpenController = (
16320
16342
  return true;
16321
16343
  }
16322
16344
  const requestCloseEvent = new CustomEvent("navi_request_close", {
16323
- detail: { event: e, ...detail },
16345
+ detail: { event: e, ...readCloseDetail(e, detail) },
16324
16346
  cancelable: true,
16325
16347
  });
16326
16348
  chainEvent(requestCloseEvent, e);
@@ -16330,10 +16352,7 @@ const createOpenController = (
16330
16352
  // the popup (a PopupGroup, a dialog), closing what this popup kept open,
16331
16353
  // or what is still closing with it (a lifting dialog closes a frame
16332
16354
  // later).
16333
- const escapeKeydown = findEvent(
16334
- requestCloseEvent,
16335
- (event) => event.type === "keydown" && event.key === "Escape",
16336
- );
16355
+ const escapeKeydown = findEvent(requestCloseEvent, isEscapeKeydown);
16337
16356
  if (escapeKeydown) {
16338
16357
  escapeKeydown.preventDefault();
16339
16358
  }
@@ -16355,7 +16374,7 @@ const createOpenController = (
16355
16374
  return;
16356
16375
  }
16357
16376
  const closeEvent = new CustomEvent("navi_close", {
16358
- detail: { event: e, ...detail },
16377
+ detail: { event: e, ...readCloseDetail(e, detail) },
16359
16378
  });
16360
16379
  chainEvent(closeEvent, e);
16361
16380
  // Skips onRequestClose entirely — there is no choice here.
@@ -16366,6 +16385,9 @@ const createOpenController = (
16366
16385
  };
16367
16386
 
16368
16387
  // A click a keyboard made (detail 0) is not one: nothing pressed for it.
16388
+ const isEscapeKeydown = (event) =>
16389
+ event.type === "keydown" && event.key === "Escape";
16390
+
16369
16391
  const isEventOfAPress = (event) =>
16370
16392
  event.type === "pointerdown" ||
16371
16393
  event.type === "mousedown" ||
@@ -18006,8 +18028,10 @@ const openCallout = (
18006
18028
  }
18007
18029
  calloutCloseButton.tabIndex = -1;
18008
18030
  calloutCloseButton.setAttribute("navi-focus-delegate", "");
18031
+ // One Escape closes one popup: an Escape already spent by one in front
18032
+ // of the callout — a menu opened from its content — is not this one's.
18009
18033
  const onAnchorKeydown = (e) => {
18010
- if (e.key === "Escape") {
18034
+ if (e.key === "Escape" && !e.defaultPrevented) {
18011
18035
  requestClose(e, "escape_key");
18012
18036
  e.stopPropagation();
18013
18037
  e.preventDefault();
@@ -18022,7 +18046,7 @@ const openCallout = (
18022
18046
  // Usually callout is inside anchor so the anchor would see the escape being pressed
18023
18047
  // but callout can be appended outside anchor when anchor cannot receive children
18024
18048
  const onCalloutKeydown = (e) => {
18025
- if (e.key === "Escape") {
18049
+ if (e.key === "Escape" && !e.defaultPrevented) {
18026
18050
  requestClose(e, "escape_key");
18027
18051
  e.stopPropagation();
18028
18052
  e.preventDefault();
@@ -18068,21 +18092,23 @@ const openCallout = (
18068
18092
  //
18069
18093
  // Only for the ones that cannot be heard otherwise, and only while they are
18070
18094
  // open: a callout the anchor can hear about is left to the anchor, so
18071
- // Escape goes on meaning what it means where the user is.
18095
+ // Escape goes on meaning what it means where the user is. Heard once the
18096
+ // key has been through what it was pressed in: one that answered it (a
18097
+ // popup closing, a field clearing) spent it.
18072
18098
  if (anchorElement && anchorElement.contains(document.activeElement)) {
18073
18099
  break close_on_escape_from_anywhere;
18074
18100
  }
18075
18101
  const onDocumentKeydown = (e) => {
18076
- if (e.key !== "Escape") {
18102
+ if (e.key !== "Escape" || e.defaultPrevented) {
18077
18103
  return;
18078
18104
  }
18079
18105
  requestClose(e, "escape_key");
18080
18106
  e.stopPropagation();
18081
18107
  e.preventDefault();
18082
18108
  };
18083
- document.addEventListener("keydown", onDocumentKeydown, true);
18109
+ document.addEventListener("keydown", onDocumentKeydown);
18084
18110
  addTeardown(() => {
18085
- document.removeEventListener("keydown", onDocumentKeydown, true);
18111
+ document.removeEventListener("keydown", onDocumentKeydown);
18086
18112
  });
18087
18113
  }
18088
18114
  {
@@ -34354,7 +34380,7 @@ const readOuterScroll = (element) => {
34354
34380
  };
34355
34381
 
34356
34382
  const isDocumentScroller = (node) => {
34357
- return node === document.documentElement || node === document.body;
34383
+ return node === document.documentElement;
34358
34384
  };
34359
34385
 
34360
34386
  const isPinnedToViewport = (node) => {
@@ -37580,12 +37606,12 @@ const whilePageRenders = async (page, change, wait = armRouteRenderWait()) => {
37580
37606
  // (`overflow: clip`) holds nothing and is walked straight through — see
37581
37607
  // is_scrollable.js.
37582
37608
  const pagesScrollTheDocument = element => {
37583
- const scrollContainer = getScrollContainer(element, {
37609
+ // What window.scrollTo moves. A body whose overflow is the viewport's is
37610
+ // walked through to it (see overflowGoesToViewport in @jsenv/dom); a body
37611
+ // the walk stops on scrolls itself, out of the viewport's reach.
37612
+ return getScrollContainer(element, {
37584
37613
  includeHidden: true
37585
- });
37586
- // html and body are one answer: whichever of them the walk stops on, what
37587
- // scrolls is the viewport, which is what window.scrollTo moves.
37588
- return scrollContainer === document.documentElement || scrollContainer === document.body;
37614
+ }) === document.documentElement;
37589
37615
  };
37590
37616
 
37591
37617
  // A page of the row: a route, and the params that say which of its tabs when
@@ -65975,6 +66001,8 @@ const engageGroup = (groupEl, group, {
65975
66001
  const themeColor = readPopupThemeColor(groupEl, group.themeColor);
65976
66002
  const focusedBefore = document.activeElement;
65977
66003
  const focusWasInGroup = Boolean(focusedBefore) && focusedBefore !== groupEl && groupEl.contains(focusedBefore);
66004
+ // What the focus goes back to, when nothing holds it any more at the close.
66005
+ const focusOrigin = !focusWasInGroup && focusedBefore && focusedBefore !== document.body && focusedBefore !== groupEl ? focusedBefore : null;
65978
66006
  const ringBefore = Boolean(focusedBefore?.matches?.(":focus-visible"));
65979
66007
  const {
65980
66008
  scrollTop,
@@ -65982,12 +66010,7 @@ const engageGroup = (groupEl, group, {
65982
66010
  } = groupEl;
65983
66011
  // A gesture on a box fixed in the top layer goes to the viewport: only a
65984
66012
  // place that scrolls with the viewport moves under it.
65985
- const placeScroller = getScrollContainer(groupEl);
65986
- const placeScrollsWithViewport = (placeScroller === document.scrollingElement ||
65987
- // Reported as a scroll container on an app setting `body {
65988
- // overflow-x: hidden }` (its computed overflow-y turns auto), it never
65989
- // scrolls itself: the viewport does.
65990
- placeScroller === document.body && bodyOverflowIsTheViewports()) && !findSelfOrAncestorFixedPosition(groupEl);
66013
+ const placeScrollsWithViewport = getScrollContainer(groupEl) === document.scrollingElement && !findSelfOrAncestorFixedPosition(groupEl);
65991
66014
  const placeholder = createPlaceholder(groupStyle, rect, baselineDescent);
65992
66015
  groupEl.before(placeholder);
65993
66016
  groupEl.style.setProperty("--x-popup-group-padding", `${groupStyle.paddingTop} ${groupStyle.paddingRight} ${groupStyle.paddingBottom} ${groupStyle.paddingLeft}`);
@@ -65995,25 +66018,49 @@ const engageGroup = (groupEl, group, {
65995
66018
  if (silent) {
65996
66019
  groupEl.setAttribute("navi-transitions-off", "");
65997
66020
  }
65998
- // showModal() runs the dialog focusing steps: it focuses the first element
65999
- // marked autofocus, or else the first focusable the group holds — and
66000
- // scrolls the group's own scroller to it. Pointed at what already has the
66001
- // focus, or at the group itself, with any other mark set aside for the
66002
- // call, so nothing moves. A native autofocus is otherwise never used (see
66003
- // the focus skill): this one exists for the length of the call, to steer
66004
- // the browser's own pick, not to transfer anything.
66005
- const autofocusTarget = focusWasInGroup ? focusedBefore : groupEl;
66021
+ // showModal() runs the dialog focusing steps — it focuses the first element
66022
+ // marked autofocus, or else the first focusable the group holds, and
66023
+ // scrolls the group's own scroller to it — and records what held the focus,
66024
+ // for close() to give it back there. A native autofocus is otherwise never
66025
+ // used (see the focus skill): these exist for the length of the call, to
66026
+ // steer the browser's own pick, not to transfer anything.
66027
+ //
66028
+ // From inside the group the pick is what holds the focus already, any other
66029
+ // mark set aside, so nothing moves. From outside, a stop is put first in the
66030
+ // group and focused: it is what showModal() picks and records, and it is
66031
+ // gone by the close, which then moves nothing (see cleanup) — what the focus
66032
+ // came from, a sheet around the group, could not be kept from taking it
66033
+ // back. The focus goes on to the group itself, so no peer is focused on the
66034
+ // way.
66035
+ let focusStop = null;
66036
+ if (!focusWasInGroup) {
66037
+ focusStop = document.createElement("span");
66038
+ focusStop.setAttribute("navi-out-of-flow", "");
66039
+ focusStop.setAttribute("aria-hidden", "true");
66040
+ focusStop.tabIndex = -1;
66041
+ focusStop.style.cssText = "position: absolute; width: 0; height: 0; overflow: hidden";
66042
+ groupEl.prepend(focusStop);
66043
+ moveFocusTo(focusStop, {
66044
+ focusVisible: false
66045
+ });
66046
+ }
66047
+ const autofocusTarget = focusWasInGroup ? focusedBefore : focusStop;
66006
66048
  const autofocusWasSet = autofocusTarget.hasAttribute("autofocus");
66007
- const autofocusSetAside = focusWasInGroup ? [...groupEl.querySelectorAll("[autofocus]")].filter(element => element !== autofocusTarget) : [];
66049
+ const autofocusSetAside = [...groupEl.querySelectorAll("[autofocus]")].filter(element => element !== autofocusTarget);
66008
66050
  for (const element of autofocusSetAside) {
66009
66051
  element.removeAttribute("autofocus");
66010
66052
  }
66011
66053
  if (!autofocusWasSet) {
66012
66054
  autofocusTarget.setAttribute("autofocus", "");
66013
66055
  }
66056
+ // Chromium takes a <dialog> without a tabindex for no focus target. Lent
66057
+ // for the whole engagement: taken back from the group holding the focus,
66058
+ // it would lose it.
66059
+ const tabindexLent = Boolean(focusStop) && !groupEl.hasAttribute("tabindex");
66014
66060
  try {
66015
66061
  groupEl.showModal();
66016
66062
  } catch (error) {
66063
+ focusStop?.remove();
66017
66064
  placeholder.remove();
66018
66065
  clearPlacement(groupEl);
66019
66066
  groupEl.removeAttribute("navi-transitions-off");
@@ -66026,6 +66073,15 @@ const engageGroup = (groupEl, group, {
66026
66073
  element.setAttribute("autofocus", "");
66027
66074
  }
66028
66075
  }
66076
+ if (focusStop) {
66077
+ if (tabindexLent) {
66078
+ groupEl.tabIndex = -1;
66079
+ }
66080
+ moveFocusTo(groupEl, {
66081
+ focusVisible: false
66082
+ });
66083
+ focusStop.remove();
66084
+ }
66029
66085
  // Measured once the group has left the flow, where a parent sized by its
66030
66086
  // content no longer counts it. Only a box given its width by what holds it
66031
66087
  // goes on following that width (a rotation while engaged); one sized by its
@@ -66048,9 +66104,6 @@ const engageGroup = (groupEl, group, {
66048
66104
  focusVisible: ringBefore
66049
66105
  });
66050
66106
  }
66051
- // What showModal() left focused when the focus came from outside: that is
66052
- // the browser's pick, not a restore of navi's, at the close (see cleanup).
66053
- const focusLeftByShowModal = focusWasInGroup ? null : document.activeElement;
66054
66107
  if (silent) {
66055
66108
  // The wall's starting style is resolved with no transition to play from
66056
66109
  // it; giving the transition back afterwards changes no value, so nothing
@@ -66082,33 +66135,55 @@ const engageGroup = (groupEl, group, {
66082
66135
  placeholder.remove();
66083
66136
  if (leavingTree) {
66084
66137
  // Closed once the commit is over, as a Dialog leaving the tree is (see
66085
- // its close cleanup): closing now would hand the focus to what held it
66086
- // at showModal() time, often leaving in this same commit. A park
66087
- // takes the group out of the top layer and leaves its `open`
66088
- // attribute, which the reveal would show.
66138
+ // its close cleanup). A park takes the group out of the top layer and
66139
+ // leaves its `open` attribute, which the reveal would show.
66089
66140
  queueMicrotask(() => {
66090
66141
  if (groupEl.open) {
66091
66142
  groupEl.close();
66092
66143
  }
66144
+ if (tabindexLent) {
66145
+ groupEl.removeAttribute("tabindex");
66146
+ }
66093
66147
  });
66148
+ if (focusOrigin) {
66149
+ // The focus left with the group: back where it was, if that stays.
66150
+ giveFocusBackAfterCommit(focusOrigin, () => {
66151
+ moveFocusTo(focusOrigin, {
66152
+ focusVisible: ringBefore
66153
+ });
66154
+ });
66155
+ }
66094
66156
  return;
66095
66157
  }
66096
- // close() hands the focus back to whatever held it at showModal() time
66097
- // (focusedBefore), undoing where a popup of the group, or the user, put
66098
- // it since. Unless no popup moved the focus since showModal() did, that
66099
- // element is made no focus target for the call, so the focus stays where
66100
- // it is: inert — or, when the focus is inside it (the sheet around the
66101
- // popover around the group, focused by a press on its blank), without
66102
- // its tabindex, inert taking the focus with it. A <dialog> stays
66103
- // focusable without one: around a Dialog sheet, close() focuses it and
66104
- // the moveFocusTo below brings the focus back.
66105
66158
  const focusedBeforeClose = document.activeElement;
66159
+ if (!focusWasInGroup) {
66160
+ // What showModal() recorded is the stop, gone by now: close() moves
66161
+ // nothing, and the focus stays where a popup of the group, or the
66162
+ // user, put it. Only one still on the group, or lost, goes back to
66163
+ // where it came from.
66164
+ if (groupEl.open) {
66165
+ groupEl.close();
66166
+ }
66167
+ if (focusOrigin && focusOrigin.isConnected && (!focusedBeforeClose || focusedBeforeClose === document.body || focusedBeforeClose === groupEl)) {
66168
+ moveFocusTo(focusOrigin, {
66169
+ focusVisible: ringBefore
66170
+ });
66171
+ }
66172
+ if (tabindexLent) {
66173
+ groupEl.removeAttribute("tabindex");
66174
+ }
66175
+ return;
66176
+ }
66177
+ // Coming from inside the group, the focus was recorded where it was,
66178
+ // and close() gives it back there, undoing where a popup of the group,
66179
+ // or the user, put it since. Unless nothing moved it, that element is
66180
+ // made no focus target for the call: inert — or, holding the focus
66181
+ // inside it, without its tabindex, inert taking the focus with it.
66106
66182
  const ringBeforeClose = Boolean(focusedBeforeClose?.matches?.(":focus-visible"));
66107
- const focusMovedSinceShowModal = focusedBeforeClose && focusedBeforeClose !== document.body && focusedBeforeClose !== groupEl && focusedBeforeClose !== focusLeftByShowModal && focusedBeforeClose.isConnected;
66108
- const keepsFocusWhereItIs = focusMovedSinceShowModal && focusedBefore && focusedBefore !== focusedBeforeClose && focusedBefore !== document.body && focusedBefore.isConnected;
66109
- const holdsFocusInside = keepsFocusWhereItIs && focusedBefore.contains(focusedBeforeClose);
66110
- const madeInert = keepsFocusWhereItIs && !holdsFocusInside && !focusedBefore.inert;
66111
- const tabindexSetAside = holdsFocusInside && focusedBefore.tagName !== "DIALOG" ? focusedBefore.getAttribute("tabindex") : null;
66183
+ const focusMovedInGroup = focusWasInGroup && focusedBeforeClose && focusedBeforeClose !== document.body && focusedBeforeClose !== groupEl && focusedBeforeClose !== focusedBefore && focusedBeforeClose.isConnected && focusedBefore.isConnected;
66184
+ const holdsFocusInside = focusMovedInGroup && focusedBefore.contains(focusedBeforeClose);
66185
+ const madeInert = focusMovedInGroup && !holdsFocusInside && !focusedBefore.inert;
66186
+ const tabindexSetAside = holdsFocusInside ? focusedBefore.getAttribute("tabindex") : null;
66112
66187
  if (madeInert) {
66113
66188
  focusedBefore.inert = true;
66114
66189
  }
@@ -66126,10 +66201,13 @@ const engageGroup = (groupEl, group, {
66126
66201
  if (tabindexSetAside !== null) {
66127
66202
  focusedBefore.setAttribute("tabindex", tabindexSetAside);
66128
66203
  }
66204
+ if (tabindexLent) {
66205
+ groupEl.removeAttribute("tabindex");
66206
+ }
66129
66207
  }
66130
- // Where the engine moved it anyway (an element focusable without a
66131
- // tabindex).
66132
- if (focusMovedSinceShowModal && document.activeElement !== focusedBeforeClose) {
66208
+ // Where the engine moved it anyway: an element focusable without a
66209
+ // tabindex.
66210
+ if (focusMovedInGroup && document.activeElement !== focusedBeforeClose) {
66133
66211
  moveFocusTo(focusedBeforeClose, {
66134
66212
  focusVisible: ringBeforeClose
66135
66213
  });
@@ -66313,7 +66391,8 @@ const engageGroup = (groupEl, group, {
66313
66391
  * screen it is synthesized at touchend and hit-tested again, with the group
66314
66392
  * still up, so a finger that scrolled the group (pointercancel, no mousedown)
66315
66393
  * closes nothing. A pointerdown cancelled downstream (a drag source) brings no
66316
- * mousedown: its click decides instead. A popup that captures the outside is
66394
+ * mousedown: its click decides instead, or the start of the drag it turns
66395
+ * into, which brings no click. A popup that captures the outside is
66317
66396
  * answered at the pointerdown itself: nothing of that press may reach a peer.
66318
66397
  */
66319
66398
  const armPressArbiter = (groupEl, group, engagement) => {
@@ -66482,7 +66561,28 @@ const armPressArbiter = (groupEl, group, engagement) => {
66482
66561
  engagement.checkAfterTask();
66483
66562
  }
66484
66563
  };
66485
- const listeners = [["pointerdown", onPointerDown], ["mousedown", onMouseDown], ["click", onClick], ["pointerup", onPointerUp], ["pointercancel", onPointerCancel]];
66564
+ // A press that becomes a drag brings no click: decided at the click, it
66565
+ // would leave the open popup over a card being carried away, its own
66566
+ // trigger maybe. A drag starting outside the open popup closes it, as a
66567
+ // press there does. Nothing is left to hand over to then, so the group goes
66568
+ // back into the page at once — where the drag can land: what is outside the
66569
+ // group is inert while it is engaged.
66570
+ const onDragStart = dragStartEvent => {
66571
+ const openMember = findMemberPressedOutside(dragStartEvent);
66572
+ if (openMember) {
66573
+ const pressOutside = readPressOutside(openMember);
66574
+ if (pressOutside !== "capture") {
66575
+ openMember.openController.requestClose(dragStartEvent, {
66576
+ isCancel: pressOutside === "cancel"
66577
+ });
66578
+ }
66579
+ }
66580
+ if (engagement.pressClosed) {
66581
+ engagement.pressClosed = false;
66582
+ group.disengageIfNothingOpen();
66583
+ }
66584
+ };
66585
+ const listeners = [["pointerdown", onPointerDown], ["mousedown", onMouseDown], ["click", onClick], ["pointerup", onPointerUp], ["pointercancel", onPointerCancel], ["navi_drag_start", onDragStart]];
66486
66586
  for (const [type, listener] of listeners) {
66487
66587
  document.addEventListener(type, listener, {
66488
66588
  capture: true
@@ -66718,8 +66818,6 @@ const readAncestorClip = (element, elementStyle) => {
66718
66818
  if (elementStyle.position === "fixed") {
66719
66819
  return null;
66720
66820
  }
66721
- // Read on body, an overflow that is the viewport's clips nothing.
66722
- const bodyClipsNothing = bodyOverflowIsTheViewports();
66723
66821
  let left = -Infinity;
66724
66822
  let top = -Infinity;
66725
66823
  let right = Infinity;
@@ -66741,9 +66839,10 @@ const readAncestorClip = (element, elementStyle) => {
66741
66839
  overflowY,
66742
66840
  position
66743
66841
  } = ancestorStyle;
66744
- const overflowGoesToViewport = ancestor === document.body && bodyClipsNothing;
66745
- const clipsX = !overflowGoesToViewport && overflowX !== "visible";
66746
- const clipsY = !overflowGoesToViewport && overflowY !== "visible";
66842
+ // Read on a body whose overflow is the viewport's, it clips nothing.
66843
+ const clipsNothing = overflowGoesToViewport(ancestor);
66844
+ const clipsX = !clipsNothing && overflowX !== "visible";
66845
+ const clipsY = !clipsNothing && overflowY !== "visible";
66747
66846
  if (clipsX || clipsY) {
66748
66847
  clipped = true;
66749
66848
  const ancestorRect = ancestor.getBoundingClientRect();
@@ -66783,12 +66882,6 @@ const readAncestorClip = (element, elementStyle) => {
66783
66882
  bottom
66784
66883
  } : null;
66785
66884
  };
66786
- // The overflow of <body> is the viewport's when the root leaves its own
66787
- // visible: body itself then neither scrolls nor clips.
66788
- const bodyOverflowIsTheViewports = () => {
66789
- const rootStyle = getComputedStyle(document.documentElement);
66790
- return rootStyle.overflowX === "visible" && rootStyle.overflowY === "visible";
66791
- };
66792
66885
  const isContainingBlockForAbsolute = style => style.position !== "static" || style.transform !== "none" || style.translate !== "none" || style.rotate !== "none" || style.scale !== "none" || style.perspective !== "none" || style.filter !== "none" ||
66793
66886
  // Prefixed alone before Safari 18.
66794
66887
  (style.backdropFilter ?? style.webkitBackdropFilter ?? "none") !== "none" || /\b(?:layout|paint|strict|content)\b/.test(style.contain) ||
@@ -69886,9 +69979,13 @@ const useDialogProps = props => {
69886
69979
  };
69887
69980
  const onKeyDownShortcuts = createOnKeyDownForShortcuts({
69888
69981
  escape: e => {
69889
- // Only a modal <dialog> fires "cancel" on Escape natively (handled via
69890
- // onCancel below); neither a .show()n nor a showPopover()n one does.
69891
- if (isModal || !openController.opened) {
69982
+ // Answered at the keydown, modal or not: the innermost popup takes the
69983
+ // key and spends it (requestClose prevents it), so one Escape closes
69984
+ // one popup. A modal's native "cancel" comes only once the keydown has
69985
+ // been dispatched, and the key would reach a popup around this one
69986
+ // first — a picker whose drawing holds this dialog. onCancel below is
69987
+ // left the close requests that are no keydown: the back gesture.
69988
+ if (!openController.opened) {
69892
69989
  return null;
69893
69990
  }
69894
69991
  return {
@@ -70069,9 +70166,9 @@ const useDialogProps = props => {
70069
70166
  onKeyDownShortcuts(e);
70070
70167
  },
70071
70168
  "onCancel": e => {
70072
- // The dialog's own "cancel" (Escape on a modal showModal() dialog —
70073
- // the Escape handling for the other two shows lives in
70074
- // onKeyDownShortcuts above) fires on the dialog element itself. But a child
70169
+ // The dialog's own "cancel" — a close request that is no keydown (the
70170
+ // back gesture), an Escape being answered at its keydown (see
70171
+ // onKeyDownShortcuts above) — fires on the dialog element itself. But a child
70075
70172
  // <input type="file"> also fires a BUBBLING "cancel" when the user
70076
70173
  // dismisses the file chooser (per the HTML spec), and it lands here
70077
70174
  // too; only the dialog's own cancel means "close".
@@ -72700,6 +72797,10 @@ const PickerCustom = props => {
72700
72797
  }
72701
72798
  };
72702
72799
  });
72800
+ // Whoever hears the Escape — this picker's trigger, its popup, the group
72801
+ // it stands in, its bubble — asks for a cancel; the controller reads it
72802
+ // with this (see escapeEffect in open_controller.js).
72803
+ openController.escapeEffect = escapeEffect;
72703
72804
  // scroll <button> of the picker into view when opening it
72704
72805
  // -> would be overriden by dialog.jsx or popover.jsx
72705
72806
  // so ideally openEffect should be either protective or a pubSub to allow multiple callbacks
@@ -72925,13 +73026,13 @@ const PickerCustom = props => {
72925
73026
  if (!openController.opened) {
72926
73027
  return null;
72927
73028
  }
72928
- const isCancel = escapeEffect === "cancel";
72929
73029
  return {
72930
- name: isCancel ? "escape_to_cancel" : "escape_to_close",
73030
+ name: "escape_to_cancel",
72931
73031
  intent: "read",
72932
73032
  allowed: () => {
73033
+ // Read with escapeEffect by the controller (see above).
72933
73034
  requestClose(e, {
72934
- isCancel
73035
+ isCancel: true
72935
73036
  });
72936
73037
  e.preventDefault(); // prevent browser from closing the dialog (if any)
72937
73038
  }
@@ -73082,10 +73183,7 @@ const PickerCustom = props => {
73082
73183
  }
73083
73184
  return renderResolver(PickerContentInsidePopup, {
73084
73185
  ...pickerProps,
73085
- mode,
73086
- // What a callout's own Escape means (see PickerCalloutPopup): the bubble
73087
- // answers the key itself when the focus is in it.
73088
- escapeEffect
73186
+ mode
73089
73187
  });
73090
73188
  };
73091
73189
 
@@ -73200,7 +73298,6 @@ const PickerContentInsidePopup = props => {
73200
73298
  calloutIcon,
73201
73299
  calloutCloseButton,
73202
73300
  calloutCloseByScroll,
73203
- escapeEffect,
73204
73301
  ...rest
73205
73302
  } = props;
73206
73303
  const isPopover = mode === "popover";
@@ -73249,7 +73346,6 @@ const PickerContentInsidePopup = props => {
73249
73346
  ,
73250
73347
  backdrop: backdrop,
73251
73348
  pressOutside: pressOutside,
73252
- escapeEffect: escapeEffect,
73253
73349
  children: jsx(PopupModeContext.Provider, {
73254
73350
  value: mode,
73255
73351
  children: children
@@ -73347,7 +73443,6 @@ const PickerCalloutPopup = ({
73347
73443
  closeByScroll,
73348
73444
  backdrop,
73349
73445
  pressOutside,
73350
- escapeEffect,
73351
73446
  onnavi_request_open,
73352
73447
  onnavi_request_close,
73353
73448
  onnavi_request_confirm,
@@ -73429,14 +73524,14 @@ const PickerCalloutPopup = ({
73429
73524
  // Not skipped: the callout moves the focus into the picker when it is
73430
73525
  // elsewhere, which is what lets Escape find the callout right away.
73431
73526
  skipFocus: false,
73432
- // An Escape the callout answered itself (the focus in the bubble) means
73433
- // what the picker's own Escape means.
73527
+ // An Escape the callout answered itself (the focus in the bubble) is
73528
+ // the picker's Escape (see escapeEffect in open_controller.js).
73434
73529
  onClose: ({
73435
73530
  event,
73436
73531
  reason
73437
73532
  }) => {
73438
73533
  openController.close(event, {
73439
- isCancel: reason === "escape_key" && escapeEffect === "cancel"
73534
+ isCancel: reason === "escape_key"
73440
73535
  });
73441
73536
  }
73442
73537
  });