@jsenv/navi 0.29.79 → 0.29.80
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/dist/jsenv_navi.js +453 -347
- package/dist/jsenv_navi.js.map +9 -8
- package/docs/AI_INSTRUCTIONS.md +8 -0
- package/docs/autofocus.md +115 -0
- package/docs/popup_open.md +3 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -21678,6 +21678,393 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
|
|
|
21678
21678
|
});
|
|
21679
21679
|
};
|
|
21680
21680
|
|
|
21681
|
+
/**
|
|
21682
|
+
* Decides which element receives focus when a container (popover, dialog, …)
|
|
21683
|
+
* opens, and gives it back to where it came from when the container closes.
|
|
21684
|
+
*
|
|
21685
|
+
* The [navi-autofocus] attribute (written by use_auto_focus.js) tunes where
|
|
21686
|
+
* focus lands. Candidates are tried in this order:
|
|
21687
|
+
* 1. The element that held focus when the container was last closed
|
|
21688
|
+
* 2. [navi-autofocus] asking for it ("" for a plain `autoFocus`)
|
|
21689
|
+
* 3. The first focusable element
|
|
21690
|
+
* 4. [navi-autofocus="last-resort"], the container itself included
|
|
21691
|
+
* 5. The element focused before the container opened
|
|
21692
|
+
*
|
|
21693
|
+
* [navi-autofocus="restore"] appears in step 1 only: it never claims focus on
|
|
21694
|
+
* a fresh open, it only gets it back.
|
|
21695
|
+
*
|
|
21696
|
+
* A ladder that comes back empty — a container holding nothing focusable yet —
|
|
21697
|
+
* places no focus, and says so on the container ([navi-autofocus-unplaced]),
|
|
21698
|
+
* because content arriving a moment later would otherwise stand aside for a
|
|
21699
|
+
* transfer that never happened (see claimUnplacedAutofocus).
|
|
21700
|
+
*/
|
|
21701
|
+
|
|
21702
|
+
// The element that held focus when a container closed is marked with
|
|
21703
|
+
// [navi-autofocus-last-focused], and its container with
|
|
21704
|
+
// [navi-autofocus-restore]. Both carry the same generated id: containers can
|
|
21705
|
+
// nest (a popover inside a dialog), so the id is what tells a reopening
|
|
21706
|
+
// container which mark among its descendants is its own.
|
|
21707
|
+
let restoreIdCounter = 0;
|
|
21708
|
+
|
|
21709
|
+
// The values that never ASK for the focus: one takes it for want of anything
|
|
21710
|
+
// better ("last-resort"), the other only takes it back ("restore"). What they
|
|
21711
|
+
// have in common is being worth giving back to — a container that was holding
|
|
21712
|
+
// the keyboard itself, a field that said it wants it, are both places one was,
|
|
21713
|
+
// and coming back to where one was is the whole point of a restore.
|
|
21714
|
+
const isRestorableAutofocus = (el) => {
|
|
21715
|
+
const value = el.getAttribute("navi-autofocus");
|
|
21716
|
+
return value === "last-resort" || value === "restore";
|
|
21717
|
+
};
|
|
21718
|
+
|
|
21719
|
+
const clearAutofocusRestore = (containerEl) => {
|
|
21720
|
+
const restoreId = containerEl.getAttribute("navi-autofocus-restore");
|
|
21721
|
+
if (restoreId === null) {
|
|
21722
|
+
return null;
|
|
21723
|
+
}
|
|
21724
|
+
containerEl.removeAttribute("navi-autofocus-restore");
|
|
21725
|
+
const selector = `[navi-autofocus-last-focused="${restoreId}"]`;
|
|
21726
|
+
const lastFocused = containerEl.matches(selector)
|
|
21727
|
+
? containerEl
|
|
21728
|
+
: containerEl.querySelector(selector);
|
|
21729
|
+
if (lastFocused) {
|
|
21730
|
+
lastFocused.removeAttribute("navi-autofocus-last-focused");
|
|
21731
|
+
}
|
|
21732
|
+
return lastFocused;
|
|
21733
|
+
};
|
|
21734
|
+
|
|
21735
|
+
// An opening whose transfer had nothing to give: the ladder came back empty, or
|
|
21736
|
+
// only found somewhere outside the container to leave the focus (see
|
|
21737
|
+
// transferFocus). Nothing inside was focused, so nothing inside owes the
|
|
21738
|
+
// opening anything either — what mounts or gets displayed right after may
|
|
21739
|
+
// claim the focus with its own autofocus (see use_auto_focus.js), instead of
|
|
21740
|
+
// standing aside for a transfer that placed nothing.
|
|
21741
|
+
const AUTOFOCUS_UNPLACED_ATTRIBUTE = "navi-autofocus-unplaced";
|
|
21742
|
+
|
|
21743
|
+
/**
|
|
21744
|
+
* "Did this container's opening leave the focus unplaced, and may I take it?" —
|
|
21745
|
+
* asked by whatever appears inside it right after. Answering yes settles the
|
|
21746
|
+
* debt: the first to ask is the one the opening was missing, and the ones after
|
|
21747
|
+
* it are content appearing alongside, which has no more claim than usual.
|
|
21748
|
+
*
|
|
21749
|
+
* @param {HTMLElement} containerEl
|
|
21750
|
+
* @returns {boolean}
|
|
21751
|
+
*/
|
|
21752
|
+
const claimUnplacedAutofocus = (containerEl) => {
|
|
21753
|
+
if (!containerEl.hasAttribute?.(AUTOFOCUS_UNPLACED_ATTRIBUTE)) {
|
|
21754
|
+
return false;
|
|
21755
|
+
}
|
|
21756
|
+
containerEl.removeAttribute(AUTOFOCUS_UNPLACED_ATTRIBUTE);
|
|
21757
|
+
return true;
|
|
21758
|
+
};
|
|
21759
|
+
|
|
21760
|
+
/**
|
|
21761
|
+
* "When the focus comes back here, put it on this" — what transferFocus reads
|
|
21762
|
+
* first when it next hands the focus to that container.
|
|
21763
|
+
*
|
|
21764
|
+
* Told rather than watched: whoever is about to take the focus away is the one
|
|
21765
|
+
* moment that still knows what was holding it.
|
|
21766
|
+
*/
|
|
21767
|
+
const markAutofocusRestore = (containerEl, element) => {
|
|
21768
|
+
clearAutofocusRestore(containerEl);
|
|
21769
|
+
if (!element || !(containerEl === element || containerEl.contains(element))) {
|
|
21770
|
+
return;
|
|
21771
|
+
}
|
|
21772
|
+
const restoreId = `${++restoreIdCounter}`;
|
|
21773
|
+
containerEl.setAttribute("navi-autofocus-restore", restoreId);
|
|
21774
|
+
element.setAttribute("navi-autofocus-last-focused", restoreId);
|
|
21775
|
+
};
|
|
21776
|
+
|
|
21777
|
+
// A popup closing remembers what held the focus, so reopening comes back to
|
|
21778
|
+
// it — re-focusing where the user was takes priority over any autofocus the
|
|
21779
|
+
// contents declare (see transferFocus). One exception: the element the closing
|
|
21780
|
+
// pointer itself pressed (a close button, an option whose click dismissed the
|
|
21781
|
+
// popup) is remembered only if it asked to be (restorable) — reopening a
|
|
21782
|
+
// dialog on the button one pressed to leave it would be surprising. A keyboard
|
|
21783
|
+
// close (Escape) designates no element, so whatever holds the focus is
|
|
21784
|
+
// remembered as where the user was.
|
|
21785
|
+
const markAutofocusRestoreOnClose = (
|
|
21786
|
+
containerEl,
|
|
21787
|
+
closeEvent,
|
|
21788
|
+
// Received rather than read here: by the time the close cleanups run, the
|
|
21789
|
+
// closing itself may have moved the focus already (a native <dialog>.close()
|
|
21790
|
+
// hands it back to what held it at showModal() time) — the caller captured
|
|
21791
|
+
// it when the close was decided.
|
|
21792
|
+
focused = document.activeElement,
|
|
21793
|
+
) => {
|
|
21794
|
+
clearAutofocusRestore(containerEl);
|
|
21795
|
+
if (!focused || !(containerEl === focused || containerEl.contains(focused))) {
|
|
21796
|
+
return;
|
|
21797
|
+
}
|
|
21798
|
+
if (!isRestorableAutofocus(focused)) {
|
|
21799
|
+
const pointerEvent = closeEvent
|
|
21800
|
+
? findEvent(closeEvent, "mousedown") || findEvent(closeEvent, "click")
|
|
21801
|
+
: null;
|
|
21802
|
+
if (pointerEvent) {
|
|
21803
|
+
const pointerTarget = pointerEvent.target;
|
|
21804
|
+
if (
|
|
21805
|
+
pointerTarget &&
|
|
21806
|
+
(focused === pointerTarget || focused.contains(pointerTarget))
|
|
21807
|
+
) {
|
|
21808
|
+
return;
|
|
21809
|
+
}
|
|
21810
|
+
}
|
|
21811
|
+
}
|
|
21812
|
+
markAutofocusRestore(containerEl, focused);
|
|
21813
|
+
};
|
|
21814
|
+
|
|
21815
|
+
/**
|
|
21816
|
+
* Where the focus goes inside a container, in the order candidates are tried:
|
|
21817
|
+
* 1. the first [navi-autofocus] that leads somewhere focusable — "put it here";
|
|
21818
|
+
* 2. the first focusable that asks for nothing in particular — what one came to
|
|
21819
|
+
* do;
|
|
21820
|
+
* 3. the DEEPEST [navi-autofocus="last-resort"], the container itself included
|
|
21821
|
+
* — "not me, unless you have nothing else". Deepest first, because of two
|
|
21822
|
+
* nested ones the inner is the more precise answer: a dialog holding a panel
|
|
21823
|
+
* holding a close button lands on the button, not on the dialog;
|
|
21824
|
+
* 4. nothing, and the caller decides what that means.
|
|
21825
|
+
*
|
|
21826
|
+
* One word covers both readings of "last resort", because they are the same
|
|
21827
|
+
* sentence said by different elements. On a FOCUSABLE — a picker's search box,
|
|
21828
|
+
* a panel's close button, a slide's chevron — it means "prefer anything else in
|
|
21829
|
+
* here to me". On a CONTAINER — a dialog, a popover, a slide — it means the
|
|
21830
|
+
* same about its own contents, and those contents being tried first (step 2
|
|
21831
|
+
* walks them) is exactly what makes the container a last resort.
|
|
21832
|
+
*
|
|
21833
|
+
* @param {HTMLElement} containerEl
|
|
21834
|
+
* @param {object} [options]
|
|
21835
|
+
* @param {boolean} [options.avoidEditable]
|
|
21836
|
+
* Keeps step 2 off anything the virtual keyboard comes up for. Step 1 is
|
|
21837
|
+
* untouched: a field that says `autoFocus` still gets the keyboard, because
|
|
21838
|
+
* asking for it is the one way to mean it (see open_controller.js, which
|
|
21839
|
+
* turns this on for a surface docked on a small touch screen).
|
|
21840
|
+
* @returns {{target: HTMLElement, reason: string}|undefined}
|
|
21841
|
+
*/
|
|
21842
|
+
const findFocusTarget = (containerEl, { avoidEditable } = {}) => {
|
|
21843
|
+
// Not while there is anything else: what takes the focus only for want of
|
|
21844
|
+
// anything better ("last-resort") and what only takes it back ("restore").
|
|
21845
|
+
// Neither is dropped, both are simply tried later — step 3 below for the
|
|
21846
|
+
// first, and for the second the restore transferFocus does before ever
|
|
21847
|
+
// calling here.
|
|
21848
|
+
//
|
|
21849
|
+
// Skipped for good, unlike the two above: an element hidden from assistive
|
|
21850
|
+
// technology is not a place the focus can land at all. Something aria-hidden
|
|
21851
|
+
// and out of the tab order is a value holder standing behind what one
|
|
21852
|
+
// actually uses — a spin's headless picker behind its slides, say — and
|
|
21853
|
+
// landing there puts a ring on it, raises a phone's keyboard over the panel
|
|
21854
|
+
// that just opened, and has the browser complain about a focused aria-hidden
|
|
21855
|
+
// element. What one came to use is further down the same container.
|
|
21856
|
+
const isHiddenFromAssistiveTech = (element) =>
|
|
21857
|
+
Boolean(element.closest?.(`[aria-hidden="true"]`));
|
|
21858
|
+
|
|
21859
|
+
const skip = (element) =>
|
|
21860
|
+
isRestorableAutofocus(element) || isHiddenFromAssistiveTech(element);
|
|
21861
|
+
|
|
21862
|
+
// Every mark, not just the first: a mark is only worth stopping at if it
|
|
21863
|
+
// leads somewhere focusable. One inside a screen waiting its turn (an inert
|
|
21864
|
+
// slide) says where the focus goes WHEN it arrives there, not now — so it is
|
|
21865
|
+
// passed over here rather than treated as an answer that then fails silently.
|
|
21866
|
+
for (const asked of containerEl.querySelectorAll(`[navi-autofocus]`)) {
|
|
21867
|
+
if (skip(asked)) {
|
|
21868
|
+
continue;
|
|
21869
|
+
}
|
|
21870
|
+
// Through findFocusable: the mark is not always ON the focusable itself — a
|
|
21871
|
+
// control puts it on the box it renders, the field inside being what takes
|
|
21872
|
+
// the keyboard — and it is also what answers "can this be focused at all"
|
|
21873
|
+
// (inert, hidden, disabled).
|
|
21874
|
+
const askedFocusable = findFocusable(asked, { exclude: skip });
|
|
21875
|
+
if (askedFocusable) {
|
|
21876
|
+
return { target: askedFocusable, reason: "navi-autofocus" };
|
|
21877
|
+
}
|
|
21878
|
+
}
|
|
21879
|
+
const focusable = findFocusable(containerEl, {
|
|
21880
|
+
exclude: avoidEditable
|
|
21881
|
+
? (element) => skip(element) || isEditableTarget(element)
|
|
21882
|
+
: skip,
|
|
21883
|
+
});
|
|
21884
|
+
if (focusable) {
|
|
21885
|
+
return { target: focusable, reason: "first focusable element" };
|
|
21886
|
+
}
|
|
21887
|
+
const lastResorts = Array.from(
|
|
21888
|
+
containerEl.querySelectorAll(`[navi-autofocus="last-resort"]`),
|
|
21889
|
+
);
|
|
21890
|
+
if (containerEl.matches?.(`[navi-autofocus="last-resort"]`)) {
|
|
21891
|
+
// Last of all: querySelectorAll only looks at descendants, and the
|
|
21892
|
+
// container is the outermost last resort there is.
|
|
21893
|
+
lastResorts.push(containerEl);
|
|
21894
|
+
}
|
|
21895
|
+
const deepestLastResort = lastResorts.find(
|
|
21896
|
+
(candidate) =>
|
|
21897
|
+
!lastResorts.some(
|
|
21898
|
+
(other) => other !== candidate && candidate.contains(other),
|
|
21899
|
+
),
|
|
21900
|
+
);
|
|
21901
|
+
if (deepestLastResort) {
|
|
21902
|
+
const lastResortFocusable = findFocusable(deepestLastResort);
|
|
21903
|
+
if (lastResortFocusable) {
|
|
21904
|
+
return {
|
|
21905
|
+
target: lastResortFocusable,
|
|
21906
|
+
reason: "navi-autofocus last-resort",
|
|
21907
|
+
};
|
|
21908
|
+
}
|
|
21909
|
+
}
|
|
21910
|
+
return undefined;
|
|
21911
|
+
};
|
|
21912
|
+
|
|
21913
|
+
const prepareFocusTransfer = (prepareEvent, debugFocus) => {
|
|
21914
|
+
const focusedElement = getFocusedBeforeTransfer(prepareEvent);
|
|
21915
|
+
// Whether what receives the focus shows a ring: the modality of the
|
|
21916
|
+
// interaction asking for the transfer, not the state of the element handing
|
|
21917
|
+
// it over. That element is often no witness at all — a popup opened from a
|
|
21918
|
+
// trigger whose mousedown we prevented keeps a :focus-visible nobody can see,
|
|
21919
|
+
// and a slide handing over to the next one was itself focused programmatically
|
|
21920
|
+
// without a ring, so it would report "no ring" for a travel asked for with
|
|
21921
|
+
// ArrowLeft. The modality answers "was the user on the keyboard when this was
|
|
21922
|
+
// asked for", which is the whole question (see isKeyboardModality).
|
|
21923
|
+
const focusVisible = isKeyboardModality();
|
|
21924
|
+
|
|
21925
|
+
debugFocus(
|
|
21926
|
+
prepareEvent,
|
|
21927
|
+
`prepare focus transfer from`,
|
|
21928
|
+
focusedElement,
|
|
21929
|
+
focusVisible ? " matching :focus-visible" : "not matching :focus-visible",
|
|
21930
|
+
);
|
|
21931
|
+
|
|
21932
|
+
return {
|
|
21933
|
+
focusedElement,
|
|
21934
|
+
focusVisible,
|
|
21935
|
+
|
|
21936
|
+
/**
|
|
21937
|
+
* Moves the focus into `containerEl`, on the element the ladder above
|
|
21938
|
+
* picks.
|
|
21939
|
+
*
|
|
21940
|
+
* `getDelay(target)` — asked once the target is known, answers how many
|
|
21941
|
+
* milliseconds to wait before actually focusing it. The ladder is what
|
|
21942
|
+
* decides WHO gets the focus and it may only run once (it consumes the
|
|
21943
|
+
* autofocus-restore mark), so a caller with a policy about WHEN cannot
|
|
21944
|
+
* resolve the target itself to make up its mind: it is handed the answer
|
|
21945
|
+
* instead. Returns a cancel function when it did delay, so a container
|
|
21946
|
+
* closing before the delay is up takes back a focus it never gave;
|
|
21947
|
+
* undefined when it focused straight away and there is nothing to take
|
|
21948
|
+
* back.
|
|
21949
|
+
*/
|
|
21950
|
+
transferFocus: (
|
|
21951
|
+
transferEvent,
|
|
21952
|
+
containerEl,
|
|
21953
|
+
{ getDelay, avoidEditable } = {},
|
|
21954
|
+
) => {
|
|
21955
|
+
let target;
|
|
21956
|
+
let reason;
|
|
21957
|
+
containerEl.removeAttribute(AUTOFOCUS_UNPLACED_ATTRIBUTE);
|
|
21958
|
+
const lastFocused = clearAutofocusRestore(containerEl);
|
|
21959
|
+
if (lastFocused) {
|
|
21960
|
+
// Through findFocusable: what was remembered may have become a wrapper
|
|
21961
|
+
// since (or stopped taking focus at all), and what is inside it is then
|
|
21962
|
+
// what the memory meant.
|
|
21963
|
+
const stillFocusable = findFocusable(lastFocused);
|
|
21964
|
+
if (stillFocusable) {
|
|
21965
|
+
reason = "element focused when it was left (restore)";
|
|
21966
|
+
target = stillFocusable;
|
|
21967
|
+
}
|
|
21968
|
+
}
|
|
21969
|
+
if (!target) {
|
|
21970
|
+
const found = findFocusTarget(containerEl, { avoidEditable });
|
|
21971
|
+
if (found) {
|
|
21972
|
+
reason = found.reason;
|
|
21973
|
+
target = found.target;
|
|
21974
|
+
}
|
|
21975
|
+
}
|
|
21976
|
+
if (!target) {
|
|
21977
|
+
if (focusedElement) {
|
|
21978
|
+
reason = "focused element before open (fallback)";
|
|
21979
|
+
target = focusedElement;
|
|
21980
|
+
}
|
|
21981
|
+
}
|
|
21982
|
+
// Whether the focus ends up inside is what the transfer is asked for; a
|
|
21983
|
+
// container that has to say no leaves the mark saying so, for whatever
|
|
21984
|
+
// appears inside it next (see claimUnplacedAutofocus). Both ways of
|
|
21985
|
+
// saying no count: finding nothing at all, and the fallback above, which
|
|
21986
|
+
// leaves the focus where it already was — outside.
|
|
21987
|
+
if (
|
|
21988
|
+
!target ||
|
|
21989
|
+
!(containerEl === target || containerEl.contains(target))
|
|
21990
|
+
) {
|
|
21991
|
+
containerEl.setAttribute(AUTOFOCUS_UNPLACED_ATTRIBUTE, "");
|
|
21992
|
+
}
|
|
21993
|
+
if (!target) {
|
|
21994
|
+
return undefined;
|
|
21995
|
+
}
|
|
21996
|
+
// The modality speaks for the transfer, but an editable target outranks
|
|
21997
|
+
// it: it draws its ring on any focus (see isMatchingFocusVisible), so
|
|
21998
|
+
// the native :focus-visible is told the same.
|
|
21999
|
+
const targetFocusVisible = focusVisible || isEditableTarget(target);
|
|
22000
|
+
const giveFocus = () => {
|
|
22001
|
+
debugFocus(
|
|
22002
|
+
transferEvent,
|
|
22003
|
+
`Moving focus to ${getElementSignature(target)}.focus({ preventScroll: true, focusVisible: ${targetFocusVisible} }) (reason: ${reason})`,
|
|
22004
|
+
);
|
|
22005
|
+
target.focus({
|
|
22006
|
+
preventScroll: true,
|
|
22007
|
+
focusVisible: targetFocusVisible,
|
|
22008
|
+
});
|
|
22009
|
+
if (target.hasAttribute("navi-autofocus-select")) {
|
|
22010
|
+
target.select();
|
|
22011
|
+
target.scrollLeft = 0;
|
|
22012
|
+
}
|
|
22013
|
+
};
|
|
22014
|
+
const delay = getDelay?.(target) || 0;
|
|
22015
|
+
if (!delay) {
|
|
22016
|
+
giveFocus();
|
|
22017
|
+
return undefined;
|
|
22018
|
+
}
|
|
22019
|
+
debugFocus(
|
|
22020
|
+
transferEvent,
|
|
22021
|
+
`Delaying focus to ${getElementSignature(target)} by ${delay}ms`,
|
|
22022
|
+
);
|
|
22023
|
+
const timeout = setTimeout(giveFocus, delay);
|
|
22024
|
+
return () => {
|
|
22025
|
+
clearTimeout(timeout);
|
|
22026
|
+
};
|
|
22027
|
+
},
|
|
22028
|
+
|
|
22029
|
+
restoreFocus: (restoreEvent) => {
|
|
22030
|
+
debugFocus(
|
|
22031
|
+
restoreEvent,
|
|
22032
|
+
`restore focus to previously focused element`,
|
|
22033
|
+
focusedElement,
|
|
22034
|
+
);
|
|
22035
|
+
const restoreFocusVisible =
|
|
22036
|
+
isKeyboardModality() || isEditableTarget(focusedElement);
|
|
22037
|
+
focusedElement.focus({
|
|
22038
|
+
preventScroll: true,
|
|
22039
|
+
focusVisible: restoreFocusVisible,
|
|
22040
|
+
});
|
|
22041
|
+
},
|
|
22042
|
+
};
|
|
22043
|
+
};
|
|
22044
|
+
|
|
22045
|
+
// Get the active element before we transfer focus in the popover/dialog
|
|
22046
|
+
// We don't just use document.activeElement because when dialog is opened by mousedown
|
|
22047
|
+
// we prevent default so browser don't steal focus back from the dialog
|
|
22048
|
+
// meaning the focus did not yet reach the element receiving the mousedown
|
|
22049
|
+
// as a result document.activeElement is not up-to-date (can be document.body for instance)
|
|
22050
|
+
const getFocusedBeforeTransfer = (e) => {
|
|
22051
|
+
// No event at all: a transfer asked for by code (a `current` prop moving a
|
|
22052
|
+
// slide, say) has no interaction to read — whatever holds the focus is all
|
|
22053
|
+
// there is to know.
|
|
22054
|
+
const initiator = e?.detail?.eventChain ? e.detail.eventChain[0] : null;
|
|
22055
|
+
if (initiator) {
|
|
22056
|
+
if (initiator.type === "mousedown") {
|
|
22057
|
+
// if we we had let browser give focus, the element would be the one that would be focused
|
|
22058
|
+
return initiator.currentTarget;
|
|
22059
|
+
}
|
|
22060
|
+
if (initiator.type === "click") {
|
|
22061
|
+
// label use case
|
|
22062
|
+
return initiator.currentTarget;
|
|
22063
|
+
}
|
|
22064
|
+
}
|
|
22065
|
+
return document.activeElement;
|
|
22066
|
+
};
|
|
22067
|
+
|
|
21681
22068
|
// see also https://github.com/preactjs/preact/issues/1255
|
|
21682
22069
|
|
|
21683
22070
|
|
|
@@ -21760,10 +22147,21 @@ const useAutoFocus = (
|
|
|
21760
22147
|
// added to a visible list), nothing else speaks for it — an autofocus it
|
|
21761
22148
|
// declares is the only word there is, exactly like dialog content saying
|
|
21762
22149
|
// where the keyboard goes when the dialog's transfer looks for it.
|
|
22150
|
+
//
|
|
22151
|
+
// Unless that owner came back empty-handed: a transfer that found nothing
|
|
22152
|
+
// to focus inside the ancestor placed no focus to steal back, and marks
|
|
22153
|
+
// itself as such (see claimUnplacedAutofocus in focus_transfer.js). What
|
|
22154
|
+
// this element says is then the only word there is after all — this runs
|
|
22155
|
+
// right after the transfer, which is exactly when content the transfer was
|
|
22156
|
+
// too early to see arrives. "last-resort" stays out of it: it means "not me
|
|
22157
|
+
// unless you have nothing else", a question the transfer's own ladder has
|
|
22158
|
+
// already asked and answered.
|
|
21763
22159
|
const { ancestor, ancestorType, becauseAncestorOpened } = e.detail;
|
|
21764
22160
|
const isSelfAncestor = ancestor === focusableElement;
|
|
21765
22161
|
if (becauseAncestorOpened && !isSelfAncestor) {
|
|
21766
|
-
|
|
22162
|
+
if (autoFocus === "last-resort" || !claimUnplacedAutofocus(ancestor)) {
|
|
22163
|
+
return () => {};
|
|
22164
|
+
}
|
|
21767
22165
|
}
|
|
21768
22166
|
if (autoFocus === "last-resort" && !isSelfAncestor) {
|
|
21769
22167
|
// "not me, unless you have nothing else" is a question only whoever hands
|
|
@@ -28419,337 +28817,6 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
28419
28817
|
};
|
|
28420
28818
|
const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
|
|
28421
28819
|
|
|
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
28820
|
// How long a popup waits before handing the focus to a field, when giving it
|
|
28754
28821
|
// is what raises the on-screen keyboard.
|
|
28755
28822
|
//
|
|
@@ -28992,7 +29059,7 @@ const createOpenController = (
|
|
|
28992
29059
|
requestOpenEvent,
|
|
28993
29060
|
debugInteraction,
|
|
28994
29061
|
);
|
|
28995
|
-
controller.transferFocusOnOpen = (el) => {
|
|
29062
|
+
controller.transferFocusOnOpen = (el, { avoidEditable } = {}) => {
|
|
28996
29063
|
// requestOpenEvent, not the raw `e` — getFocusedBeforeTransfer needs
|
|
28997
29064
|
// e.detail.eventChain (built by chainEvent above) to recover the
|
|
28998
29065
|
// element a mousedown/click landed on. `e` itself is usually the raw
|
|
@@ -29019,6 +29086,7 @@ const createOpenController = (
|
|
|
29019
29086
|
findEvent(requestOpenEvent, isTouchDrivenEvent),
|
|
29020
29087
|
);
|
|
29021
29088
|
const cancelPendingFocus = focusTransfer.transferFocus(e, el, {
|
|
29089
|
+
avoidEditable,
|
|
29022
29090
|
getDelay: (target) =>
|
|
29023
29091
|
openedByTouch && isEditableTarget(target)
|
|
29024
29092
|
? FOCUS_DELAY_ON_KEYBOARD_MS
|
|
@@ -30668,10 +30736,20 @@ const css$X = /* css */`
|
|
|
30668
30736
|
* @param {number} [props.tabIndex=-1] - Set on the dialog element itself so
|
|
30669
30737
|
* `autoFocus="last-resort"` below has somewhere to land when the dialog has
|
|
30670
30738
|
* no other focusable descendant of its own.
|
|
30671
|
-
* @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] -
|
|
30672
|
-
*
|
|
30673
|
-
*
|
|
30674
|
-
*
|
|
30739
|
+
* @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] -
|
|
30740
|
+
* Where the keyboard goes when this dialog opens — one rung of the ladder in
|
|
30741
|
+
* `docs/autofocus.md`, which is what to read for the whole of it.
|
|
30742
|
+
* - `true` — the dialog element itself takes the keyboard, whatever it holds.
|
|
30743
|
+
* For a dialog whose content is READ before it is filled: the focus starts
|
|
30744
|
+
* at the top of the reading order and no virtual keyboard rises over it.
|
|
30745
|
+
* - `"last-resort"` — the dialog takes the keyboard only if it holds nothing
|
|
30746
|
+
* focusable of its own.
|
|
30747
|
+
* - `"restore"` — the dialog stays out of the opening focus chain unless it
|
|
30748
|
+
* held focus when it closed.
|
|
30749
|
+
* Docked on a small touch screen, the default already withdraws fields from
|
|
30750
|
+
* the choice: a bottom sheet is read before it is typed in, so the keyboard
|
|
30751
|
+
* goes to a field only when that field asks for it by name (`autoFocus` on
|
|
30752
|
+
* the field, which outranks whatever the dialog says).
|
|
30675
30753
|
* @param {boolean} [props.open] - Controlled open state.
|
|
30676
30754
|
* @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
|
|
30677
30755
|
* initial open state. `true` plays no entrance animation: the dialog was
|
|
@@ -31368,7 +31446,16 @@ const useDialogProps = props => {
|
|
|
31368
31446
|
// entrance to be over. Decided by transferFocusOnOpen, the only place that
|
|
31369
31447
|
// knows WHICH element is about to be focused (open_controller.js and its
|
|
31370
31448
|
// FOCUS_DELAY_ON_KEYBOARD_MS).
|
|
31371
|
-
|
|
31449
|
+
//
|
|
31450
|
+
// Docked, the keyboard costs more than a wait: it takes a third of a phone
|
|
31451
|
+
// screen from a dialog that starts at the bottom edge, pushing whatever
|
|
31452
|
+
// comes before the field — the title, the sentence saying why it is asked
|
|
31453
|
+
// for — above the top edge before the dialog has even been looked at. So a
|
|
31454
|
+
// docked dialog is READ first: the transfer only reaches a field that asked
|
|
31455
|
+
// for the keyboard by name (see findFocusTarget's `avoidEditable`).
|
|
31456
|
+
const restoreFocus = openController.transferFocusOnOpen(dialogEl, {
|
|
31457
|
+
avoidEditable: isDocked
|
|
31458
|
+
});
|
|
31372
31459
|
|
|
31373
31460
|
// isModal outside-click detection (see this file's top comment for why
|
|
31374
31461
|
// this is a plain document-level listener rather than anything
|
|
@@ -32093,13 +32180,20 @@ const css$W = /* css */`
|
|
|
32093
32180
|
* @param {number} [props.tabIndex=-1] - Set on the popover element itself
|
|
32094
32181
|
* so `autoFocus="last-resort"` below has somewhere to land when the popover
|
|
32095
32182
|
* has no other focusable descendant of its own.
|
|
32096
|
-
* @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] -
|
|
32097
|
-
*
|
|
32098
|
-
*
|
|
32099
|
-
*
|
|
32100
|
-
*
|
|
32101
|
-
*
|
|
32102
|
-
*
|
|
32183
|
+
* @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] -
|
|
32184
|
+
* Where the keyboard goes when this popover opens — one rung of the ladder in
|
|
32185
|
+
* `docs/autofocus.md`, which is what to read for the whole of it.
|
|
32186
|
+
* - `true` — the popover element itself takes the keyboard, whatever it
|
|
32187
|
+
* holds. For a popover whose content is READ before it is filled: the focus
|
|
32188
|
+
* starts at the top of the reading order and no virtual keyboard rises over
|
|
32189
|
+
* it.
|
|
32190
|
+
* - `"last-resort"` — the popover takes the keyboard only if it holds nothing
|
|
32191
|
+
* focusable of its own.
|
|
32192
|
+
* - `"restore"` — the popover stays out of the opening focus chain unless it
|
|
32193
|
+
* held focus when it closed.
|
|
32194
|
+
* - `false` — no open-time focus transfer at all: nothing inside the popover
|
|
32195
|
+
* receives focus, whoever had the keyboard keeps it — the combobox case,
|
|
32196
|
+
* where suggestions open under an input being typed in.
|
|
32103
32197
|
* @param {boolean} [props.open] - Controlled open state.
|
|
32104
32198
|
* @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
|
|
32105
32199
|
* initial open state. `true` plays no entrance animation: the popover was
|
|
@@ -55024,8 +55118,15 @@ installImportMetaCssBuild(import.meta);const css$z = /* css */`
|
|
|
55024
55118
|
0px,
|
|
55025
55119
|
var(--picker-border-radius) - var(--picker-border-width)
|
|
55026
55120
|
);
|
|
55027
|
-
overflow: auto;
|
|
55028
55121
|
overscroll-behavior: none;
|
|
55122
|
+
|
|
55123
|
+
/* Skipped when the list asks for overflow="visible": that ask is
|
|
55124
|
+
about escaping every box the list sits in, and this selector is
|
|
55125
|
+
specific enough to win over the list's own rules and silently put
|
|
55126
|
+
the scroll back. */
|
|
55127
|
+
&:not([data-overflow-visible]) {
|
|
55128
|
+
overflow: auto;
|
|
55129
|
+
}
|
|
55029
55130
|
}
|
|
55030
55131
|
}
|
|
55031
55132
|
|
|
@@ -55083,8 +55184,13 @@ installImportMetaCssBuild(import.meta);const css$z = /* css */`
|
|
|
55083
55184
|
0px,
|
|
55084
55185
|
var(--picker-border-radius) - var(--picker-border-width)
|
|
55085
55186
|
);
|
|
55086
|
-
overflow: auto;
|
|
55087
55187
|
overscroll-behavior: none;
|
|
55188
|
+
|
|
55189
|
+
/* See the popover block above: overflow="visible" on the list must not
|
|
55190
|
+
be overridden back into a scroll by this rule. */
|
|
55191
|
+
&:not([data-overflow-visible]) {
|
|
55192
|
+
overflow: auto;
|
|
55193
|
+
}
|
|
55088
55194
|
}
|
|
55089
55195
|
}
|
|
55090
55196
|
|