@jsenv/navi 0.29.79 → 0.29.81

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.
@@ -2,8 +2,8 @@
2
2
  * AI reading this file: read ../docs/AI_INSTRUCTIONS.md for context on
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
- import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
- export { coarsePointerSignal, disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
5
+ import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
+ export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
7
  import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, clickIsSuppressed, isTouchDrivenEvent, scrollIntoViewScoped, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
8
8
  export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
@@ -17134,6 +17134,12 @@ const TYPO_PROPS = {
17134
17134
  uppercase: applyToCssPropWhenTruthy("textTransform", "uppercase", "none"),
17135
17135
  lowercase: applyToCssPropWhenTruthy("textTransform", "lowercase", "none"),
17136
17136
  letterSpacing: PASS_THROUGH,
17137
+ // How many lines before truncation, for anything that is not a `Text`.
17138
+ // On a `Text`, `maxLines` is the prop to use and it does more than this
17139
+ // mapping (block display, min-width, white-space per tag) — see
17140
+ // docs/typography.md. `lineClamp` and `overflowEllipsis` below are the raw
17141
+ // one-to-one CSS mappings, kept for an element that opts out of `Text` and
17142
+ // still wants that exact CSS; `lineClamp: 1` is NOT single-line truncation.
17137
17143
  maxLines: (value) => {
17138
17144
  if (!value) {
17139
17145
  return null;
@@ -21128,6 +21134,12 @@ const shouldInjectSpacingBetween = (left, right) => {
21128
21134
  };
21129
21135
 
21130
21136
  /**
21137
+ * The typography primitive: every string an app displays goes through it, or
21138
+ * through something built on it (`Title`, `Paragraph`, `Caption`, `Link`, a
21139
+ * control's label). It accepts every `Box` prop on top of the ones below.
21140
+ * See `docs/typography.md` for the decisions behind it — truncating, rows made
21141
+ * of an icon, a text and an icon, and where a line may break.
21142
+ *
21131
21143
  * @type {import("ignore:preact").FunctionComponent<{
21132
21144
  * children?: import("ignore:preact").ComponentChildren,
21133
21145
  * as?: string,
@@ -21139,6 +21151,7 @@ const shouldInjectSpacingBetween = (left, right) => {
21139
21151
  * spacing?: string | number | import("ignore:preact").ComponentChildren,
21140
21152
  * loading?: boolean,
21141
21153
  * skeleton?: boolean,
21154
+ * attachLastChild?: boolean,
21142
21155
  * preventSpaceUnderlines?: boolean,
21143
21156
  * holdSpaceForStyle?: import("ignore:preact").JSX.CSSProperties,
21144
21157
  * boldStable?: boolean,
@@ -21150,9 +21163,14 @@ const shouldInjectSpacingBetween = (left, right) => {
21150
21163
  * }>}
21151
21164
  *
21152
21165
  * @param {number} [maxLines]
21153
- * Truncates overflowing text with an ellipsis. `maxLines={1}` produces a
21154
- * single-line truncation; `maxLines={n}` (n > 1) uses `-webkit-line-clamp`
21155
- * to allow up to n lines before clipping.
21166
+ * How many lines the text may take before it is truncated with an ellipsis.
21167
+ * `maxLines={1}` truncates on a single line; `maxLines={n}` (n > 1) clamps to
21168
+ * n lines. This is the only prop to use for that — `Box`'s `lineClamp` /
21169
+ * `overflowEllipsis` are raw CSS mappings meant for elements that are not a
21170
+ * `Text`, and `lineClamp={1}` is never the single-line truncation you want.
21171
+ * Truncation only happens if the element may become narrower than its
21172
+ * content: `maxLines` sets `min-width: 0` here, but each `Box` between this
21173
+ * one and the element that carries the width must set it too.
21156
21174
  *
21157
21175
  * @param {string|number} [spacing]
21158
21176
  * Separator injected between child nodes. Accepts a size token (`"s"`, `"m"`, …),
@@ -21168,7 +21186,10 @@ const shouldInjectSpacingBetween = (left, right) => {
21168
21186
  * @param {boolean} [attachLastChild]
21169
21187
  * Keeps the last child on the same line as the word before it — a trailing
21170
21188
  * icon, a unit, an arrow. Without it the browser may break the line right
21171
- * before that child and leave it alone underneath.
21189
+ * before that child and leave it alone underneath, and no character can
21190
+ * prevent that break. For wrapping text; a child that must survive
21191
+ * truncation belongs outside the `Text` instead (see `docs/typography.md`).
21192
+ * `Link` sets it on its own whenever it renders an end icon.
21172
21193
  *
21173
21194
  * @param {boolean} [preventSpaceUnderlines]
21174
21195
  * Replaces real space characters between children with padding-based spaces.
@@ -21678,6 +21699,454 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
21678
21699
  });
21679
21700
  };
21680
21701
 
21702
+ /**
21703
+ * Decides which element receives focus when a container (popover, dialog, …)
21704
+ * opens, and gives it back to where it came from when the container closes.
21705
+ *
21706
+ * The [navi-autofocus] attribute (written by use_auto_focus.js) tunes where
21707
+ * focus lands. Candidates are tried in this order:
21708
+ * 1. The element that held focus when the container was last closed
21709
+ * 2. [navi-autofocus] asking for it ("" for a plain `autoFocus`)
21710
+ * 3. The first focusable element
21711
+ * 4. [navi-autofocus="last-resort"], the container itself included
21712
+ * 5. The element focused before the container opened
21713
+ *
21714
+ * [navi-autofocus="restore"] appears in step 1 only: it never claims focus on
21715
+ * a fresh open, it only gets it back.
21716
+ *
21717
+ * A ladder that comes back empty — a container holding nothing focusable yet —
21718
+ * places no focus, and says so on the container ([navi-autofocus-unplaced]),
21719
+ * because content arriving a moment later would otherwise stand aside for a
21720
+ * transfer that never happened (see claimUnplacedAutofocus).
21721
+ */
21722
+
21723
+ // The element that held focus when a container closed is marked with
21724
+ // [navi-autofocus-last-focused], and its container with
21725
+ // [navi-autofocus-restore]. Both carry the same generated id: containers can
21726
+ // nest (a popover inside a dialog), so the id is what tells a reopening
21727
+ // container which mark among its descendants is its own.
21728
+ let restoreIdCounter = 0;
21729
+
21730
+ // The values that never ASK for the focus: one takes it for want of anything
21731
+ // better ("last-resort"), the other only takes it back ("restore"). What they
21732
+ // have in common is being worth giving back to — a container that was holding
21733
+ // the keyboard itself, a field that said it wants it, are both places one was,
21734
+ // and coming back to where one was is the whole point of a restore.
21735
+ const isRestorableAutofocus = (el) => {
21736
+ const value = el.getAttribute("navi-autofocus");
21737
+ return value === "last-resort" || value === "restore";
21738
+ };
21739
+
21740
+ const clearAutofocusRestore = (containerEl) => {
21741
+ const restoreId = containerEl.getAttribute("navi-autofocus-restore");
21742
+ if (restoreId === null) {
21743
+ return null;
21744
+ }
21745
+ containerEl.removeAttribute("navi-autofocus-restore");
21746
+ const selector = `[navi-autofocus-last-focused="${restoreId}"]`;
21747
+ const lastFocused = containerEl.matches(selector)
21748
+ ? containerEl
21749
+ : containerEl.querySelector(selector);
21750
+ if (lastFocused) {
21751
+ lastFocused.removeAttribute("navi-autofocus-last-focused");
21752
+ }
21753
+ return lastFocused;
21754
+ };
21755
+
21756
+ // An opening whose transfer had nothing to give: the ladder came back empty, or
21757
+ // only found somewhere outside the container to leave the focus (see
21758
+ // transferFocus). Nothing inside was focused, so nothing inside owes the
21759
+ // opening anything either — what mounts or gets displayed right after may
21760
+ // claim the focus with its own autofocus (see use_auto_focus.js), instead of
21761
+ // standing aside for a transfer that placed nothing.
21762
+ const AUTOFOCUS_UNPLACED_ATTRIBUTE = "navi-autofocus-unplaced";
21763
+
21764
+ /**
21765
+ * "Did this container's opening leave the focus unplaced, and may I take it?" —
21766
+ * asked by whatever appears inside it right after. Answering yes settles the
21767
+ * debt: the first to ask is the one the opening was missing, and the ones after
21768
+ * it are content appearing alongside, which has no more claim than usual.
21769
+ *
21770
+ * @param {HTMLElement} containerEl
21771
+ * @returns {boolean}
21772
+ */
21773
+ const claimUnplacedAutofocus = (containerEl) => {
21774
+ if (!containerEl.hasAttribute?.(AUTOFOCUS_UNPLACED_ATTRIBUTE)) {
21775
+ return false;
21776
+ }
21777
+ containerEl.removeAttribute(AUTOFOCUS_UNPLACED_ATTRIBUTE);
21778
+ return true;
21779
+ };
21780
+
21781
+ /**
21782
+ * "When the focus comes back here, put it on this" — what transferFocus reads
21783
+ * first when it next hands the focus to that container.
21784
+ *
21785
+ * Told rather than watched: whoever is about to take the focus away is the one
21786
+ * moment that still knows what was holding it.
21787
+ */
21788
+ const markAutofocusRestore = (containerEl, element) => {
21789
+ clearAutofocusRestore(containerEl);
21790
+ if (!element || !(containerEl === element || containerEl.contains(element))) {
21791
+ return;
21792
+ }
21793
+ const restoreId = `${++restoreIdCounter}`;
21794
+ containerEl.setAttribute("navi-autofocus-restore", restoreId);
21795
+ element.setAttribute("navi-autofocus-last-focused", restoreId);
21796
+ };
21797
+
21798
+ // A popup closing remembers what held the focus, so reopening comes back to
21799
+ // it — re-focusing where the user was takes priority over any autofocus the
21800
+ // contents declare (see transferFocus). One exception: the element the closing
21801
+ // pointer itself pressed (a close button, an option whose click dismissed the
21802
+ // popup) is remembered only if it asked to be (restorable) — reopening a
21803
+ // dialog on the button one pressed to leave it would be surprising. A keyboard
21804
+ // close (Escape) designates no element, so whatever holds the focus is
21805
+ // remembered as where the user was.
21806
+ const markAutofocusRestoreOnClose = (
21807
+ containerEl,
21808
+ closeEvent,
21809
+ // Received rather than read here: by the time the close cleanups run, the
21810
+ // closing itself may have moved the focus already (a native <dialog>.close()
21811
+ // hands it back to what held it at showModal() time) — the caller captured
21812
+ // it when the close was decided.
21813
+ focused = document.activeElement,
21814
+ ) => {
21815
+ clearAutofocusRestore(containerEl);
21816
+ if (!focused || !(containerEl === focused || containerEl.contains(focused))) {
21817
+ return;
21818
+ }
21819
+ if (!isRestorableAutofocus(focused)) {
21820
+ const pointerEvent = closeEvent
21821
+ ? findEvent(closeEvent, "mousedown") || findEvent(closeEvent, "click")
21822
+ : null;
21823
+ if (pointerEvent) {
21824
+ const pointerTarget = pointerEvent.target;
21825
+ if (
21826
+ pointerTarget &&
21827
+ (focused === pointerTarget || focused.contains(pointerTarget))
21828
+ ) {
21829
+ return;
21830
+ }
21831
+ }
21832
+ }
21833
+ markAutofocusRestore(containerEl, focused);
21834
+ };
21835
+
21836
+ /**
21837
+ * Where the focus goes inside a container, in the order candidates are tried:
21838
+ * 1. the first [navi-autofocus] that leads somewhere focusable — "put it here";
21839
+ * 2. the first focusable that asks for nothing in particular — what one came to
21840
+ * do;
21841
+ * 3. the DEEPEST [navi-autofocus="last-resort"], the container itself included
21842
+ * — "not me, unless you have nothing else". Deepest first, because of two
21843
+ * nested ones the inner is the more precise answer: a dialog holding a panel
21844
+ * holding a close button lands on the button, not on the dialog;
21845
+ * 4. nothing, and the caller decides what that means.
21846
+ *
21847
+ * One word covers both readings of "last resort", because they are the same
21848
+ * sentence said by different elements. On a FOCUSABLE — a picker's search box,
21849
+ * a panel's close button, a slide's chevron — it means "prefer anything else in
21850
+ * here to me". On a CONTAINER — a dialog, a popover, a slide — it means the
21851
+ * same about its own contents, and those contents being tried first (step 2
21852
+ * walks them) is exactly what makes the container a last resort.
21853
+ *
21854
+ * @param {HTMLElement} containerEl
21855
+ * @param {object} [options]
21856
+ * @param {boolean} [options.skipFirstFocusable]
21857
+ * Drops step 2 — the focus then goes where something ASKED for it, or to the
21858
+ * last resort, which for a container is itself. For a surface that is read
21859
+ * before it is reached: the first focusable is wherever the content happens
21860
+ * to put it, so landing there scrolls whatever comes before it out of sight
21861
+ * (see open_controller.js, which turns this on wherever the keyboard is a
21862
+ * virtual one).
21863
+ * @returns {{target: HTMLElement, reason: string}|undefined}
21864
+ */
21865
+ const findFocusTarget = (containerEl, { skipFirstFocusable } = {}) => {
21866
+ // Not while there is anything else: what takes the focus only for want of
21867
+ // anything better ("last-resort") and what only takes it back ("restore").
21868
+ // Neither is dropped, both are simply tried later — step 3 below for the
21869
+ // first, and for the second the restore transferFocus does before ever
21870
+ // calling here.
21871
+ //
21872
+ // Skipped for good, unlike the two above: an element hidden from assistive
21873
+ // technology is not a place the focus can land at all. Something aria-hidden
21874
+ // and out of the tab order is a value holder standing behind what one
21875
+ // actually uses — a spin's headless picker behind its slides, say — and
21876
+ // landing there puts a ring on it, raises a phone's keyboard over the panel
21877
+ // that just opened, and has the browser complain about a focused aria-hidden
21878
+ // element. What one came to use is further down the same container.
21879
+ const isHiddenFromAssistiveTech = (element) =>
21880
+ Boolean(element.closest?.(`[aria-hidden="true"]`));
21881
+
21882
+ const skip = (element) =>
21883
+ isRestorableAutofocus(element) || isHiddenFromAssistiveTech(element);
21884
+
21885
+ // Every mark, not just the first: a mark is only worth stopping at if it
21886
+ // leads somewhere focusable. One inside a screen waiting its turn (an inert
21887
+ // slide) says where the focus goes WHEN it arrives there, not now — so it is
21888
+ // passed over here rather than treated as an answer that then fails silently.
21889
+ //
21890
+ // The container's own mark comes last among the asked, and querySelectorAll
21891
+ // does not return it: a surface saying "the keyboard stops on me" is answered
21892
+ // by anything inside it that named itself, the more precise answer winning.
21893
+ const askedList = Array.from(
21894
+ containerEl.querySelectorAll(`[navi-autofocus]`),
21895
+ );
21896
+ if (containerEl.matches?.(`[navi-autofocus]`)) {
21897
+ askedList.push(containerEl);
21898
+ }
21899
+ for (const asked of askedList) {
21900
+ if (skip(asked)) {
21901
+ continue;
21902
+ }
21903
+ // Through findFocusable: the mark is not always ON the focusable itself — a
21904
+ // control puts it on the box it renders, the field inside being what takes
21905
+ // the keyboard — and it is also what answers "can this be focused at all"
21906
+ // (inert, hidden, disabled).
21907
+ const askedFocusable = findFocusable(asked, { exclude: skip });
21908
+ if (askedFocusable) {
21909
+ return { target: askedFocusable, reason: "navi-autofocus" };
21910
+ }
21911
+ }
21912
+ if (!skipFirstFocusable) {
21913
+ const focusable = findFocusable(containerEl, { exclude: skip });
21914
+ if (focusable) {
21915
+ return { target: focusable, reason: "first focusable element" };
21916
+ }
21917
+ }
21918
+ const lastResorts = Array.from(
21919
+ containerEl.querySelectorAll(`[navi-autofocus="last-resort"]`),
21920
+ );
21921
+ if (containerEl.matches?.(`[navi-autofocus="last-resort"]`)) {
21922
+ // Last of all: querySelectorAll only looks at descendants, and the
21923
+ // container is the outermost last resort there is.
21924
+ lastResorts.push(containerEl);
21925
+ }
21926
+ const deepestLastResort = lastResorts.find(
21927
+ (candidate) =>
21928
+ !lastResorts.some(
21929
+ (other) => other !== candidate && candidate.contains(other),
21930
+ ),
21931
+ );
21932
+ if (deepestLastResort) {
21933
+ const lastResortFocusable = findFocusable(deepestLastResort);
21934
+ if (lastResortFocusable) {
21935
+ return {
21936
+ target: lastResortFocusable,
21937
+ reason: "navi-autofocus last-resort",
21938
+ };
21939
+ }
21940
+ }
21941
+ return undefined;
21942
+ };
21943
+
21944
+ const prepareFocusTransfer = (prepareEvent, debugFocus) => {
21945
+ const focusedElement = getFocusedBeforeTransfer(prepareEvent);
21946
+ // Whether what receives the focus shows a ring: the modality of the
21947
+ // interaction asking for the transfer, not the state of the element handing
21948
+ // it over. That element is often no witness at all — a popup opened from a
21949
+ // trigger whose mousedown we prevented keeps a :focus-visible nobody can see,
21950
+ // and a slide handing over to the next one was itself focused programmatically
21951
+ // without a ring, so it would report "no ring" for a travel asked for with
21952
+ // ArrowLeft. The modality answers "was the user on the keyboard when this was
21953
+ // asked for", which is the whole question (see isKeyboardModality).
21954
+ const focusVisible = isKeyboardModality();
21955
+
21956
+ debugFocus(
21957
+ prepareEvent,
21958
+ `prepare focus transfer from`,
21959
+ focusedElement,
21960
+ focusVisible ? " matching :focus-visible" : "not matching :focus-visible",
21961
+ );
21962
+
21963
+ return {
21964
+ focusedElement,
21965
+ focusVisible,
21966
+
21967
+ /**
21968
+ * Moves the focus into `containerEl`, on the element the ladder above
21969
+ * picks.
21970
+ *
21971
+ * `getDelay(target)` — asked once the target is known, answers how many
21972
+ * milliseconds to wait before actually focusing it. The ladder is what
21973
+ * decides WHO gets the focus and it may only run once (it consumes the
21974
+ * autofocus-restore mark), so a caller with a policy about WHEN cannot
21975
+ * resolve the target itself to make up its mind: it is handed the answer
21976
+ * instead. Returns a cancel function when it did delay, so a container
21977
+ * closing before the delay is up takes back a focus it never gave;
21978
+ * undefined when it focused straight away and there is nothing to take
21979
+ * back.
21980
+ */
21981
+ transferFocus: (
21982
+ transferEvent,
21983
+ containerEl,
21984
+ { getDelay, skipFirstFocusable } = {},
21985
+ ) => {
21986
+ let target;
21987
+ let reason;
21988
+ containerEl.removeAttribute(AUTOFOCUS_UNPLACED_ATTRIBUTE);
21989
+ const lastFocused = clearAutofocusRestore(containerEl);
21990
+ if (lastFocused) {
21991
+ // Through findFocusable: what was remembered may have become a wrapper
21992
+ // since (or stopped taking focus at all), and what is inside it is then
21993
+ // what the memory meant.
21994
+ const stillFocusable = findFocusable(lastFocused);
21995
+ if (stillFocusable) {
21996
+ reason = "element focused when it was left (restore)";
21997
+ target = stillFocusable;
21998
+ }
21999
+ }
22000
+ if (!target) {
22001
+ const found = findFocusTarget(containerEl, { skipFirstFocusable });
22002
+ if (found) {
22003
+ reason = found.reason;
22004
+ target = found.target;
22005
+ }
22006
+ }
22007
+ if (!target) {
22008
+ if (focusedElement) {
22009
+ reason = "focused element before open (fallback)";
22010
+ target = focusedElement;
22011
+ }
22012
+ }
22013
+ // Whether the focus ends up inside is what the transfer is asked for; a
22014
+ // container that has to say no leaves the mark saying so, for whatever
22015
+ // appears inside it next (see claimUnplacedAutofocus). Both ways of
22016
+ // saying no count: finding nothing at all, and the fallback above, which
22017
+ // leaves the focus where it already was — outside.
22018
+ const placedInside =
22019
+ target && (containerEl === target || containerEl.contains(target));
22020
+ let cancelRetry;
22021
+ if (!placedInside) {
22022
+ containerEl.setAttribute(AUTOFOCUS_UNPLACED_ATTRIBUTE, "");
22023
+ cancelRetry = retryWhenPlaceable(containerEl, {
22024
+ skipFirstFocusable,
22025
+ focusVisible,
22026
+ debugFocus,
22027
+ transferEvent,
22028
+ });
22029
+ }
22030
+ if (!target) {
22031
+ return cancelRetry;
22032
+ }
22033
+ // The modality speaks for the transfer, but an editable target outranks
22034
+ // it: it draws its ring on any focus (see isMatchingFocusVisible), so
22035
+ // the native :focus-visible is told the same.
22036
+ const targetFocusVisible = focusVisible || isEditableTarget(target);
22037
+ const giveFocus = () => {
22038
+ debugFocus(
22039
+ transferEvent,
22040
+ `Moving focus to ${getElementSignature(target)}.focus({ preventScroll: true, focusVisible: ${targetFocusVisible} }) (reason: ${reason})`,
22041
+ );
22042
+ focusTransferTarget(target, targetFocusVisible);
22043
+ };
22044
+ const delay = getDelay?.(target) || 0;
22045
+ if (!delay) {
22046
+ giveFocus();
22047
+ return cancelRetry;
22048
+ }
22049
+ debugFocus(
22050
+ transferEvent,
22051
+ `Delaying focus to ${getElementSignature(target)} by ${delay}ms`,
22052
+ );
22053
+ const timeout = setTimeout(giveFocus, delay);
22054
+ return () => {
22055
+ clearTimeout(timeout);
22056
+ cancelRetry?.();
22057
+ };
22058
+ },
22059
+
22060
+ restoreFocus: (restoreEvent) => {
22061
+ debugFocus(
22062
+ restoreEvent,
22063
+ `restore focus to previously focused element`,
22064
+ focusedElement,
22065
+ );
22066
+ const restoreFocusVisible =
22067
+ isKeyboardModality() || isEditableTarget(focusedElement);
22068
+ focusedElement.focus({
22069
+ preventScroll: true,
22070
+ focusVisible: restoreFocusVisible,
22071
+ });
22072
+ },
22073
+ };
22074
+ };
22075
+
22076
+ /**
22077
+ * The second and last try at placing a focus the ladder had nowhere to put.
22078
+ *
22079
+ * A container can open on a moment where nothing in it — its own contents, and
22080
+ * itself — can take the focus: content still being built, a screen not yet
22081
+ * interactive. That moment is over almost immediately, and nothing else would
22082
+ * ever come back to it: the opening is the one event there is, and it has
22083
+ * passed. So the transfer keeps its promise one microtask later, still before
22084
+ * the browser paints, and still before anything the user does.
22085
+ *
22086
+ * Whoever settled the debt in between wins — content arriving with an autofocus
22087
+ * of its own claims it through use_auto_focus.js, and finding the mark gone is
22088
+ * how this knows to stand down.
22089
+ */
22090
+ const retryWhenPlaceable = (
22091
+ containerEl,
22092
+ { skipFirstFocusable, focusVisible, debugFocus, transferEvent },
22093
+ ) => {
22094
+ let cancelled = false;
22095
+ queueMicrotask(() => {
22096
+ if (cancelled || !containerEl.isConnected) {
22097
+ return;
22098
+ }
22099
+ if (!claimUnplacedAutofocus(containerEl)) {
22100
+ return;
22101
+ }
22102
+ const found = findFocusTarget(containerEl, { skipFirstFocusable });
22103
+ if (!found) {
22104
+ return;
22105
+ }
22106
+ const { target, reason } = found;
22107
+ debugFocus(
22108
+ transferEvent,
22109
+ `Moving focus to ${getElementSignature(target)} on second try (reason: ${reason})`,
22110
+ );
22111
+ focusTransferTarget(target, focusVisible || isEditableTarget(target));
22112
+ });
22113
+ return () => {
22114
+ cancelled = true;
22115
+ };
22116
+ };
22117
+
22118
+ const focusTransferTarget = (target, focusVisible) => {
22119
+ target.focus({ preventScroll: true, focusVisible });
22120
+ if (target.hasAttribute("navi-autofocus-select")) {
22121
+ target.select();
22122
+ // Keep the beginning of the text visible instead of scrolling to the end
22123
+ target.scrollLeft = 0;
22124
+ }
22125
+ };
22126
+
22127
+ // Get the active element before we transfer focus in the popover/dialog
22128
+ // We don't just use document.activeElement because when dialog is opened by mousedown
22129
+ // we prevent default so browser don't steal focus back from the dialog
22130
+ // meaning the focus did not yet reach the element receiving the mousedown
22131
+ // as a result document.activeElement is not up-to-date (can be document.body for instance)
22132
+ const getFocusedBeforeTransfer = (e) => {
22133
+ // No event at all: a transfer asked for by code (a `current` prop moving a
22134
+ // slide, say) has no interaction to read — whatever holds the focus is all
22135
+ // there is to know.
22136
+ const initiator = e?.detail?.eventChain ? e.detail.eventChain[0] : null;
22137
+ if (initiator) {
22138
+ if (initiator.type === "mousedown") {
22139
+ // if we we had let browser give focus, the element would be the one that would be focused
22140
+ return initiator.currentTarget;
22141
+ }
22142
+ if (initiator.type === "click") {
22143
+ // label use case
22144
+ return initiator.currentTarget;
22145
+ }
22146
+ }
22147
+ return document.activeElement;
22148
+ };
22149
+
21681
22150
  // see also https://github.com/preactjs/preact/issues/1255
21682
22151
 
21683
22152
 
@@ -21760,10 +22229,21 @@ const useAutoFocus = (
21760
22229
  // added to a visible list), nothing else speaks for it — an autofocus it
21761
22230
  // declares is the only word there is, exactly like dialog content saying
21762
22231
  // where the keyboard goes when the dialog's transfer looks for it.
22232
+ //
22233
+ // Unless that owner came back empty-handed: a transfer that found nothing
22234
+ // to focus inside the ancestor placed no focus to steal back, and marks
22235
+ // itself as such (see claimUnplacedAutofocus in focus_transfer.js). What
22236
+ // this element says is then the only word there is after all — this runs
22237
+ // right after the transfer, which is exactly when content the transfer was
22238
+ // too early to see arrives. "last-resort" stays out of it: it means "not me
22239
+ // unless you have nothing else", a question the transfer's own ladder has
22240
+ // already asked and answered.
21763
22241
  const { ancestor, ancestorType, becauseAncestorOpened } = e.detail;
21764
22242
  const isSelfAncestor = ancestor === focusableElement;
21765
22243
  if (becauseAncestorOpened && !isSelfAncestor) {
21766
- return () => {};
22244
+ if (autoFocus === "last-resort" || !claimUnplacedAutofocus(ancestor)) {
22245
+ return () => {};
22246
+ }
21767
22247
  }
21768
22248
  if (autoFocus === "last-resort" && !isSelfAncestor) {
21769
22249
  // "not me, unless you have nothing else" is a question only whoever hands
@@ -28419,337 +28899,6 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
28419
28899
  };
28420
28900
  const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
28421
28901
 
28422
- /**
28423
- * Decides which element receives focus when a container (popover, dialog, …)
28424
- * opens, and gives it back to where it came from when the container closes.
28425
- *
28426
- * The [navi-autofocus] attribute (written by use_auto_focus.js) tunes where
28427
- * focus lands. Candidates are tried in this order:
28428
- * 1. The element that held focus when the container was last closed
28429
- * 2. [navi-autofocus] asking for it ("" for a plain `autoFocus`)
28430
- * 3. The first focusable element
28431
- * 4. [navi-autofocus="last-resort"], the container itself included
28432
- * 5. The element focused before the container opened
28433
- *
28434
- * [navi-autofocus="restore"] appears in step 1 only: it never claims focus on
28435
- * a fresh open, it only gets it back.
28436
- */
28437
-
28438
- // The element that held focus when a container closed is marked with
28439
- // [navi-autofocus-last-focused], and its container with
28440
- // [navi-autofocus-restore]. Both carry the same generated id: containers can
28441
- // nest (a popover inside a dialog), so the id is what tells a reopening
28442
- // container which mark among its descendants is its own.
28443
- let restoreIdCounter = 0;
28444
-
28445
- // The values that never ASK for the focus: one takes it for want of anything
28446
- // better ("last-resort"), the other only takes it back ("restore"). What they
28447
- // have in common is being worth giving back to — a container that was holding
28448
- // the keyboard itself, a field that said it wants it, are both places one was,
28449
- // and coming back to where one was is the whole point of a restore.
28450
- const isRestorableAutofocus = (el) => {
28451
- const value = el.getAttribute("navi-autofocus");
28452
- return value === "last-resort" || value === "restore";
28453
- };
28454
-
28455
- const clearAutofocusRestore = (containerEl) => {
28456
- const restoreId = containerEl.getAttribute("navi-autofocus-restore");
28457
- if (restoreId === null) {
28458
- return null;
28459
- }
28460
- containerEl.removeAttribute("navi-autofocus-restore");
28461
- const selector = `[navi-autofocus-last-focused="${restoreId}"]`;
28462
- const lastFocused = containerEl.matches(selector)
28463
- ? containerEl
28464
- : containerEl.querySelector(selector);
28465
- if (lastFocused) {
28466
- lastFocused.removeAttribute("navi-autofocus-last-focused");
28467
- }
28468
- return lastFocused;
28469
- };
28470
-
28471
- /**
28472
- * "When the focus comes back here, put it on this" — what transferFocus reads
28473
- * first when it next hands the focus to that container.
28474
- *
28475
- * Told rather than watched: whoever is about to take the focus away is the one
28476
- * moment that still knows what was holding it.
28477
- */
28478
- const markAutofocusRestore = (containerEl, element) => {
28479
- clearAutofocusRestore(containerEl);
28480
- if (!element || !(containerEl === element || containerEl.contains(element))) {
28481
- return;
28482
- }
28483
- const restoreId = `${++restoreIdCounter}`;
28484
- containerEl.setAttribute("navi-autofocus-restore", restoreId);
28485
- element.setAttribute("navi-autofocus-last-focused", restoreId);
28486
- };
28487
-
28488
- // A popup closing remembers what held the focus, so reopening comes back to
28489
- // it — re-focusing where the user was takes priority over any autofocus the
28490
- // contents declare (see transferFocus). One exception: the element the closing
28491
- // pointer itself pressed (a close button, an option whose click dismissed the
28492
- // popup) is remembered only if it asked to be (restorable) — reopening a
28493
- // dialog on the button one pressed to leave it would be surprising. A keyboard
28494
- // close (Escape) designates no element, so whatever holds the focus is
28495
- // remembered as where the user was.
28496
- const markAutofocusRestoreOnClose = (
28497
- containerEl,
28498
- closeEvent,
28499
- // Received rather than read here: by the time the close cleanups run, the
28500
- // closing itself may have moved the focus already (a native <dialog>.close()
28501
- // hands it back to what held it at showModal() time) — the caller captured
28502
- // it when the close was decided.
28503
- focused = document.activeElement,
28504
- ) => {
28505
- clearAutofocusRestore(containerEl);
28506
- if (!focused || !(containerEl === focused || containerEl.contains(focused))) {
28507
- return;
28508
- }
28509
- if (!isRestorableAutofocus(focused)) {
28510
- const pointerEvent = closeEvent
28511
- ? findEvent(closeEvent, "mousedown") || findEvent(closeEvent, "click")
28512
- : null;
28513
- if (pointerEvent) {
28514
- const pointerTarget = pointerEvent.target;
28515
- if (
28516
- pointerTarget &&
28517
- (focused === pointerTarget || focused.contains(pointerTarget))
28518
- ) {
28519
- return;
28520
- }
28521
- }
28522
- }
28523
- markAutofocusRestore(containerEl, focused);
28524
- };
28525
-
28526
- /**
28527
- * Where the focus goes inside a container, in the order candidates are tried:
28528
- * 1. the first [navi-autofocus] that leads somewhere focusable — "put it here";
28529
- * 2. the first focusable that asks for nothing in particular — what one came to
28530
- * do;
28531
- * 3. the DEEPEST [navi-autofocus="last-resort"], the container itself included
28532
- * — "not me, unless you have nothing else". Deepest first, because of two
28533
- * nested ones the inner is the more precise answer: a dialog holding a panel
28534
- * holding a close button lands on the button, not on the dialog;
28535
- * 4. nothing, and the caller decides what that means.
28536
- *
28537
- * One word covers both readings of "last resort", because they are the same
28538
- * sentence said by different elements. On a FOCUSABLE — a picker's search box,
28539
- * a panel's close button, a slide's chevron — it means "prefer anything else in
28540
- * here to me". On a CONTAINER — a dialog, a popover, a slide — it means the
28541
- * same about its own contents, and those contents being tried first (step 2
28542
- * walks them) is exactly what makes the container a last resort.
28543
- *
28544
- * @param {HTMLElement} containerEl
28545
- * @returns {{target: HTMLElement, reason: string}|undefined}
28546
- */
28547
- const findFocusTarget = (containerEl) => {
28548
- // Not while there is anything else: what takes the focus only for want of
28549
- // anything better ("last-resort") and what only takes it back ("restore").
28550
- // Neither is dropped, both are simply tried later — step 3 below for the
28551
- // first, and for the second the restore transferFocus does before ever
28552
- // calling here.
28553
- //
28554
- // Skipped for good, unlike the two above: an element hidden from assistive
28555
- // technology is not a place the focus can land at all. Something aria-hidden
28556
- // and out of the tab order is a value holder standing behind what one
28557
- // actually uses — a spin's headless picker behind its slides, say — and
28558
- // landing there puts a ring on it, raises a phone's keyboard over the panel
28559
- // that just opened, and has the browser complain about a focused aria-hidden
28560
- // element. What one came to use is further down the same container.
28561
- const isHiddenFromAssistiveTech = (element) =>
28562
- Boolean(element.closest?.(`[aria-hidden="true"]`));
28563
-
28564
- const skip = (element) =>
28565
- isRestorableAutofocus(element) || isHiddenFromAssistiveTech(element);
28566
-
28567
- // Every mark, not just the first: a mark is only worth stopping at if it
28568
- // leads somewhere focusable. One inside a screen waiting its turn (an inert
28569
- // slide) says where the focus goes WHEN it arrives there, not now — so it is
28570
- // passed over here rather than treated as an answer that then fails silently.
28571
- for (const asked of containerEl.querySelectorAll(`[navi-autofocus]`)) {
28572
- if (skip(asked)) {
28573
- continue;
28574
- }
28575
- // Through findFocusable: the mark is not always ON the focusable itself — a
28576
- // control puts it on the box it renders, the field inside being what takes
28577
- // the keyboard — and it is also what answers "can this be focused at all"
28578
- // (inert, hidden, disabled).
28579
- const askedFocusable = findFocusable(asked, { exclude: skip });
28580
- if (askedFocusable) {
28581
- return { target: askedFocusable, reason: "navi-autofocus" };
28582
- }
28583
- }
28584
- const focusable = findFocusable(containerEl, { exclude: skip });
28585
- if (focusable) {
28586
- return { target: focusable, reason: "first focusable element" };
28587
- }
28588
- const lastResorts = Array.from(
28589
- containerEl.querySelectorAll(`[navi-autofocus="last-resort"]`),
28590
- );
28591
- if (containerEl.matches?.(`[navi-autofocus="last-resort"]`)) {
28592
- // Last of all: querySelectorAll only looks at descendants, and the
28593
- // container is the outermost last resort there is.
28594
- lastResorts.push(containerEl);
28595
- }
28596
- const deepestLastResort = lastResorts.find(
28597
- (candidate) =>
28598
- !lastResorts.some(
28599
- (other) => other !== candidate && candidate.contains(other),
28600
- ),
28601
- );
28602
- if (deepestLastResort) {
28603
- const lastResortFocusable = findFocusable(deepestLastResort);
28604
- if (lastResortFocusable) {
28605
- return {
28606
- target: lastResortFocusable,
28607
- reason: "navi-autofocus last-resort",
28608
- };
28609
- }
28610
- }
28611
- return undefined;
28612
- };
28613
-
28614
- const prepareFocusTransfer = (prepareEvent, debugFocus) => {
28615
- const focusedElement = getFocusedBeforeTransfer(prepareEvent);
28616
- // Whether what receives the focus shows a ring: the modality of the
28617
- // interaction asking for the transfer, not the state of the element handing
28618
- // it over. That element is often no witness at all — a popup opened from a
28619
- // trigger whose mousedown we prevented keeps a :focus-visible nobody can see,
28620
- // and a slide handing over to the next one was itself focused programmatically
28621
- // without a ring, so it would report "no ring" for a travel asked for with
28622
- // ArrowLeft. The modality answers "was the user on the keyboard when this was
28623
- // asked for", which is the whole question (see isKeyboardModality).
28624
- const focusVisible = isKeyboardModality();
28625
-
28626
- debugFocus(
28627
- prepareEvent,
28628
- `prepare focus transfer from`,
28629
- focusedElement,
28630
- focusVisible ? " matching :focus-visible" : "not matching :focus-visible",
28631
- );
28632
-
28633
- return {
28634
- focusedElement,
28635
- focusVisible,
28636
-
28637
- /**
28638
- * Moves the focus into `containerEl`, on the element the ladder above
28639
- * picks.
28640
- *
28641
- * `getDelay(target)` — asked once the target is known, answers how many
28642
- * milliseconds to wait before actually focusing it. The ladder is what
28643
- * decides WHO gets the focus and it may only run once (it consumes the
28644
- * autofocus-restore mark), so a caller with a policy about WHEN cannot
28645
- * resolve the target itself to make up its mind: it is handed the answer
28646
- * instead. Returns a cancel function when it did delay, so a container
28647
- * closing before the delay is up takes back a focus it never gave;
28648
- * undefined when it focused straight away and there is nothing to take
28649
- * back.
28650
- */
28651
- transferFocus: (transferEvent, containerEl, { getDelay } = {}) => {
28652
- let target;
28653
- let reason;
28654
- const lastFocused = clearAutofocusRestore(containerEl);
28655
- if (lastFocused) {
28656
- // Through findFocusable: what was remembered may have become a wrapper
28657
- // since (or stopped taking focus at all), and what is inside it is then
28658
- // what the memory meant.
28659
- const stillFocusable = findFocusable(lastFocused);
28660
- if (stillFocusable) {
28661
- reason = "element focused when it was left (restore)";
28662
- target = stillFocusable;
28663
- }
28664
- }
28665
- if (!target) {
28666
- const found = findFocusTarget(containerEl);
28667
- if (found) {
28668
- reason = found.reason;
28669
- target = found.target;
28670
- }
28671
- }
28672
- if (!target) {
28673
- if (focusedElement) {
28674
- reason = "focused element before open (fallback)";
28675
- target = focusedElement;
28676
- }
28677
- }
28678
- if (!target) {
28679
- return undefined;
28680
- }
28681
- // The modality speaks for the transfer, but an editable target outranks
28682
- // it: it draws its ring on any focus (see isMatchingFocusVisible), so
28683
- // the native :focus-visible is told the same.
28684
- const targetFocusVisible = focusVisible || isEditableTarget(target);
28685
- const giveFocus = () => {
28686
- debugFocus(
28687
- transferEvent,
28688
- `Moving focus to ${getElementSignature(target)}.focus({ preventScroll: true, focusVisible: ${targetFocusVisible} }) (reason: ${reason})`,
28689
- );
28690
- target.focus({
28691
- preventScroll: true,
28692
- focusVisible: targetFocusVisible,
28693
- });
28694
- if (target.hasAttribute("navi-autofocus-select")) {
28695
- target.select();
28696
- target.scrollLeft = 0;
28697
- }
28698
- };
28699
- const delay = getDelay?.(target) || 0;
28700
- if (!delay) {
28701
- giveFocus();
28702
- return undefined;
28703
- }
28704
- debugFocus(
28705
- transferEvent,
28706
- `Delaying focus to ${getElementSignature(target)} by ${delay}ms`,
28707
- );
28708
- const timeout = setTimeout(giveFocus, delay);
28709
- return () => {
28710
- clearTimeout(timeout);
28711
- };
28712
- },
28713
-
28714
- restoreFocus: (restoreEvent) => {
28715
- debugFocus(
28716
- restoreEvent,
28717
- `restore focus to previously focused element`,
28718
- focusedElement,
28719
- );
28720
- const restoreFocusVisible =
28721
- isKeyboardModality() || isEditableTarget(focusedElement);
28722
- focusedElement.focus({
28723
- preventScroll: true,
28724
- focusVisible: restoreFocusVisible,
28725
- });
28726
- },
28727
- };
28728
- };
28729
-
28730
- // Get the active element before we transfer focus in the popover/dialog
28731
- // We don't just use document.activeElement because when dialog is opened by mousedown
28732
- // we prevent default so browser don't steal focus back from the dialog
28733
- // meaning the focus did not yet reach the element receiving the mousedown
28734
- // as a result document.activeElement is not up-to-date (can be document.body for instance)
28735
- const getFocusedBeforeTransfer = (e) => {
28736
- // No event at all: a transfer asked for by code (a `current` prop moving a
28737
- // slide, say) has no interaction to read — whatever holds the focus is all
28738
- // there is to know.
28739
- const initiator = e?.detail?.eventChain ? e.detail.eventChain[0] : null;
28740
- if (initiator) {
28741
- if (initiator.type === "mousedown") {
28742
- // if we we had let browser give focus, the element would be the one that would be focused
28743
- return initiator.currentTarget;
28744
- }
28745
- if (initiator.type === "click") {
28746
- // label use case
28747
- return initiator.currentTarget;
28748
- }
28749
- }
28750
- return document.activeElement;
28751
- };
28752
-
28753
28902
  // How long a popup waits before handing the focus to a field, when giving it
28754
28903
  // is what raises the on-screen keyboard.
28755
28904
  //
@@ -29019,6 +29168,19 @@ const createOpenController = (
29019
29168
  findEvent(requestOpenEvent, isTouchDrivenEvent),
29020
29169
  );
29021
29170
  const cancelPendingFocus = focusTransfer.transferFocus(e, el, {
29171
+ // A popup is READ before it is reached wherever the keyboard is a
29172
+ // virtual one. Landing on the first focusable there costs the top of
29173
+ // the popup twice over: the browser scrolls that element into view,
29174
+ // and a field raises a keyboard that takes a third of what is left —
29175
+ // so the title and the sentence saying what this is about are gone
29176
+ // before the popup has been looked at. Only something that ASKED for
29177
+ // the focus is worth that, and asking is what `autoFocus` is.
29178
+ //
29179
+ // The device, not the opening (unlike the delay below): whether
29180
+ // focusing raises a keyboard over the popup is true of the screen,
29181
+ // and a popup opened by the page loading — no pointer in it at all —
29182
+ // is precisely the one that must not be answered "no keyboard here".
29183
+ skipFirstFocusable: coarsePointerSignal.value,
29022
29184
  getDelay: (target) =>
29023
29185
  openedByTouch && isEditableTarget(target)
29024
29186
  ? FOCUS_DELAY_ON_KEYBOARD_MS
@@ -30668,10 +30830,20 @@ const css$X = /* css */`
30668
30830
  * @param {number} [props.tabIndex=-1] - Set on the dialog element itself so
30669
30831
  * `autoFocus="last-resort"` below has somewhere to land when the dialog has
30670
30832
  * no other focusable descendant of its own.
30671
- * @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] - See
30672
- * `focus_transfer.js` `"last-resort"` focuses the dialog itself only if it
30673
- * has no other focusable descendant, `"restore"` keeps it out of the
30674
- * opening focus chain unless it held focus when the dialog closed.
30833
+ * @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] -
30834
+ * Where the keyboard goes when this dialog opens one rung of the ladder in
30835
+ * `docs/autofocus.md`, which is what to read for the whole of it.
30836
+ * - `true` the dialog element itself takes the keyboard, whatever it holds.
30837
+ * For a dialog whose content is READ before it is filled: the focus starts
30838
+ * at the top of the reading order and no virtual keyboard rises over it.
30839
+ * - `"last-resort"` — the dialog takes the keyboard only if it holds nothing
30840
+ * focusable of its own.
30841
+ * - `"restore"` — the dialog stays out of the opening focus chain unless it
30842
+ * held focus when it closed.
30843
+ * Wherever the keyboard is a virtual one (a touch device), the surface is
30844
+ * already what one arrives on: a popup is read before it is reached there, so
30845
+ * the focus only leaves it for something that asked by name (`autoFocus` on
30846
+ * that element, which outranks whatever the dialog says).
30675
30847
  * @param {boolean} [props.open] - Controlled open state.
30676
30848
  * @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
30677
30849
  * initial open state. `true` plays no entrance animation: the dialog was
@@ -32093,13 +32265,24 @@ const css$W = /* css */`
32093
32265
  * @param {number} [props.tabIndex=-1] - Set on the popover element itself
32094
32266
  * so `autoFocus="last-resort"` below has somewhere to land when the popover
32095
32267
  * has no other focusable descendant of its own.
32096
- * @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] - See
32097
- * `focus_transfer.js` `"last-resort"` focuses the popover itself only if it
32098
- * has no other focusable descendant, `"restore"` keeps it out of the
32099
- * opening focus chain unless it held focus when the popover closed. `false`
32100
- * disables the open-time focus transfer entirely: nothing inside the popover
32101
- * receives focus, whoever had the keyboard keeps it the combobox case,
32102
- * where suggestions open under an input being typed in.
32268
+ * @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] -
32269
+ * Where the keyboard goes when this popover opens one rung of the ladder in
32270
+ * `docs/autofocus.md`, which is what to read for the whole of it.
32271
+ * - `true` the popover element itself takes the keyboard, whatever it
32272
+ * holds. For a popover whose content is READ before it is filled: the focus
32273
+ * starts at the top of the reading order and no virtual keyboard rises over
32274
+ * it.
32275
+ * - `"last-resort"` — the popover takes the keyboard only if it holds nothing
32276
+ * focusable of its own.
32277
+ * - `"restore"` — the popover stays out of the opening focus chain unless it
32278
+ * held focus when it closed.
32279
+ * - `false` — no open-time focus transfer at all: nothing inside the popover
32280
+ * receives focus, whoever had the keyboard keeps it — the combobox case,
32281
+ * where suggestions open under an input being typed in.
32282
+ * Wherever the keyboard is a virtual one (a touch device), the surface is
32283
+ * already what one arrives on: a popup is read before it is reached there, so
32284
+ * the focus only leaves it for something that asked by name (`autoFocus` on
32285
+ * that element, which outranks whatever the popover says).
32103
32286
  * @param {boolean} [props.open] - Controlled open state.
32104
32287
  * @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
32105
32288
  * initial open state. `true` plays no entrance animation: the popover was
@@ -55024,8 +55207,15 @@ installImportMetaCssBuild(import.meta);const css$z = /* css */`
55024
55207
  0px,
55025
55208
  var(--picker-border-radius) - var(--picker-border-width)
55026
55209
  );
55027
- overflow: auto;
55028
55210
  overscroll-behavior: none;
55211
+
55212
+ /* Skipped when the list asks for overflow="visible": that ask is
55213
+ about escaping every box the list sits in, and this selector is
55214
+ specific enough to win over the list's own rules and silently put
55215
+ the scroll back. */
55216
+ &:not([data-overflow-visible]) {
55217
+ overflow: auto;
55218
+ }
55029
55219
  }
55030
55220
  }
55031
55221
 
@@ -55083,8 +55273,13 @@ installImportMetaCssBuild(import.meta);const css$z = /* css */`
55083
55273
  0px,
55084
55274
  var(--picker-border-radius) - var(--picker-border-width)
55085
55275
  );
55086
- overflow: auto;
55087
55276
  overscroll-behavior: none;
55277
+
55278
+ /* See the popover block above: overflow="visible" on the list must not
55279
+ be overridden back into a scroll by this rule. */
55280
+ &:not([data-overflow-visible]) {
55281
+ overflow: auto;
55282
+ }
55088
55283
  }
55089
55284
  }
55090
55285
 
@@ -74963,5 +75158,5 @@ const UserSvg = () => jsx("svg", {
74963
75158
  })
74964
75159
  });
74965
75160
 
74966
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeSpin, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
75161
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeSpin, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
74967
75162
  //# sourceMappingURL=jsenv_navi.js.map