@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.
- package/README.md +1 -1
- package/dist/jsenv_navi.js +547 -352
- package/dist/jsenv_navi.js.map +10 -10
- package/docs/AI_INSTRUCTIONS.md +14 -0
- package/docs/autofocus.md +136 -0
- package/docs/popup_open.md +3 -0
- package/docs/typography.md +176 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
21154
|
-
* single
|
|
21155
|
-
*
|
|
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
|
-
|
|
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"] -
|
|
30672
|
-
*
|
|
30673
|
-
*
|
|
30674
|
-
*
|
|
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"] -
|
|
32097
|
-
*
|
|
32098
|
-
*
|
|
32099
|
-
*
|
|
32100
|
-
*
|
|
32101
|
-
*
|
|
32102
|
-
*
|
|
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
|