@jsenv/navi 0.28.1 → 0.28.2

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.
@@ -21724,8 +21724,12 @@ const createDisplayedEvent = (ancestor) => {
21724
21724
  *
21725
21725
  * @param {import("preact/hooks").Ref<HTMLElement>} focusableElementRef
21726
21726
  * Ref to the element to focus.
21727
- * @param {boolean} autoFocus
21728
- * When false the hook is a no-op.
21727
+ * @param {boolean|"fallback"|"restore"} autoFocus
21728
+ * When false the hook is a no-op. `"fallback"` claims focus only when nothing
21729
+ * more specific already did. `"restore"` never claims focus on open; it only
21730
+ * gets focus back from an ancestor that closed while it was focused (see
21731
+ * focus_transfer.js) — typically a text input that must not pop the mobile
21732
+ * keyboard open every time, but should stay where the user left it.
21729
21733
  * @param {object} [options]
21730
21734
  * @param {boolean} [options.preventScroll]
21731
21735
  * Passed as `preventScroll` to `element.focus()`. Defaults to true to suppress
@@ -21748,6 +21752,11 @@ const useAutoFocus = (
21748
21752
  if (!autoFocus) {
21749
21753
  return () => {};
21750
21754
  }
21755
+ if (autoFocus === "restore") {
21756
+ // "restore" never claims focus on its own; the only way it gets focus is
21757
+ // an ancestor reopening and handing it back (see focus_transfer.js).
21758
+ return () => {};
21759
+ }
21751
21760
  const focusableElement = focusableElementRef.current;
21752
21761
  if (!focusableElement) {
21753
21762
  return () => {};
@@ -36015,22 +36024,61 @@ const renderSafe = (value) => {
36015
36024
  const PickerContext = createContext();
36016
36025
 
36017
36026
  /**
36018
- * Mirrors what browsers do when navigating to a page:
36019
- * 1. Focus the first element with [navi-autofocus] (but not [navi-autofocus="fallback"]) inside the container
36020
- * 2. Fall back to the first focusable element
36021
- * 3. Fall back to the first element with [navi-autofocus="fallback"]
36022
- * Does nothing if no candidate is found.
36027
+ * Decides which element receives focus when a container (popover, dialog, …)
36028
+ * opens, and gives it back to where it came from when the container closes.
36029
+ *
36030
+ * The [navi-autofocus] attribute (written by use_auto_focus.js) tunes where
36031
+ * focus lands. Candidates are tried in this order:
36032
+ * 1. The element that held focus when the container was last closed, if it
36033
+ * opted into that with "fallback" or "restore"
36034
+ * 2. [navi-autofocus] with any other value ("" for a plain `autoFocus`)
36035
+ * 3. The first focusable element
36036
+ * 4. [navi-autofocus="fallback"], the container itself included
36037
+ * 5. The element focused before the container opened
36038
+ *
36039
+ * [navi-autofocus="restore"] appears in step 1 only: it never claims focus on
36040
+ * a fresh open, it only gets it back.
36023
36041
  */
36042
+
36043
+ // The element that held focus when a container closed is marked with
36044
+ // [navi-autofocus-last-focused], and its container with
36045
+ // [navi-autofocus-restore]. Both carry the same generated id: containers can
36046
+ // nest (a popover inside a dialog), so the id is what tells a reopening
36047
+ // container which mark among its descendants is its own.
36048
+ let restoreIdCounter = 0;
36049
+
36050
+ const isRestorableAutofocus = (el) => {
36051
+ const value = el.getAttribute("navi-autofocus");
36052
+ return value === "fallback" || value === "restore";
36053
+ };
36054
+
36055
+ const clearAutofocusRestore = (containerEl) => {
36056
+ const restoreId = containerEl.getAttribute("navi-autofocus-restore");
36057
+ if (restoreId === null) {
36058
+ return null;
36059
+ }
36060
+ containerEl.removeAttribute("navi-autofocus-restore");
36061
+ const selector = `[navi-autofocus-last-focused="${restoreId}"]`;
36062
+ const lastFocused = containerEl.matches(selector)
36063
+ ? containerEl
36064
+ : containerEl.querySelector(selector);
36065
+ if (lastFocused) {
36066
+ lastFocused.removeAttribute("navi-autofocus-last-focused");
36067
+ }
36068
+ return lastFocused;
36069
+ };
36070
+
36024
36071
  const markAutofocusRestoreOnClose = (containerEl) => {
36072
+ clearAutofocusRestore(containerEl);
36025
36073
  const focused = document.activeElement;
36026
36074
  if (
36027
36075
  focused &&
36028
- containerEl.contains(focused) &&
36029
- focused.getAttribute("navi-autofocus") === "fallback"
36076
+ (containerEl === focused || containerEl.contains(focused)) &&
36077
+ isRestorableAutofocus(focused)
36030
36078
  ) {
36031
- containerEl.setAttribute("navi-autofocus-restore", "");
36032
- } else {
36033
- containerEl.removeAttribute("navi-autofocus-restore");
36079
+ const restoreId = `${++restoreIdCounter}`;
36080
+ containerEl.setAttribute("navi-autofocus-restore", restoreId);
36081
+ focused.setAttribute("navi-autofocus-last-focused", restoreId);
36034
36082
  }
36035
36083
  };
36036
36084
 
@@ -36052,19 +36100,14 @@ const prepareFocusTransfer = (prepareEvent, debugFocus) => {
36052
36100
  transferFocus: (transferEvent, containerEl) => {
36053
36101
  let target;
36054
36102
  let reason;
36055
- if (containerEl.hasAttribute("navi-autofocus-restore")) {
36056
- containerEl.removeAttribute("navi-autofocus-restore");
36057
- const naviAutoFocusFallback = containerEl.querySelector(
36058
- "[navi-autofocus='fallback']",
36059
- );
36060
- if (naviAutoFocusFallback) {
36061
- reason = "navi-autofocus fallback (restore)";
36062
- target = naviAutoFocusFallback;
36063
- }
36103
+ const lastFocused = clearAutofocusRestore(containerEl);
36104
+ if (lastFocused) {
36105
+ reason = "element focused when closed (restore)";
36106
+ target = lastFocused;
36064
36107
  }
36065
36108
  if (!target) {
36066
36109
  const naviAutoFocus = containerEl.querySelector(
36067
- "[navi-autofocus]:not([navi-autofocus='fallback'])",
36110
+ `[navi-autofocus]:not([navi-autofocus="fallback"]):not([navi-autofocus="restore"])`,
36068
36111
  );
36069
36112
  if (naviAutoFocus) {
36070
36113
  reason = "navi-autofocus";
@@ -36073,7 +36116,7 @@ const prepareFocusTransfer = (prepareEvent, debugFocus) => {
36073
36116
  }
36074
36117
  if (!target) {
36075
36118
  const focusable = findFocusable(containerEl, {
36076
- exclude: (el) => el.getAttribute("navi-autofocus") === "fallback",
36119
+ exclude: isRestorableAutofocus,
36077
36120
  });
36078
36121
  if (focusable) {
36079
36122
  reason = "first focusable element";
@@ -37329,9 +37372,10 @@ const css$v = /* css */`
37329
37372
  * @param {number} [props.tabIndex=-1] - Set on the dialog element itself so
37330
37373
  * `autoFocus="fallback"` below has somewhere to land when the dialog has
37331
37374
  * no other focusable descendant of its own.
37332
- * @param {boolean|"fallback"} [props.autoFocus="fallback"] - See
37333
- * `use_auto_focus.js` — `"fallback"` focuses the dialog itself if it has
37334
- * no other focusable descendant.
37375
+ * @param {boolean|"fallback"|"restore"} [props.autoFocus="fallback"] - See
37376
+ * `focus_transfer.js` — `"fallback"` focuses the dialog itself if it has
37377
+ * no other focusable descendant, `"restore"` keeps it out of the opening
37378
+ * focus chain unless it held focus when the dialog closed.
37335
37379
  * @param {boolean} [props.open] - Controlled open state.
37336
37380
  * @param {boolean} [props.defaultOpen] - Uncontrolled, mount-only initial
37337
37381
  * open state — plays no entrance animation (nothing was ever shown as
@@ -38317,9 +38361,10 @@ const css$u = /* css */`
38317
38361
  * @param {number} [props.tabIndex=-1] - Set on the popover element itself
38318
38362
  * so `autoFocus="fallback"` below has somewhere to land when the popover
38319
38363
  * has no other focusable descendant of its own.
38320
- * @param {boolean|"fallback"} [props.autoFocus="fallback"] - See
38321
- * `use_auto_focus.js` — `"fallback"` focuses the popover itself if it has
38322
- * no other focusable descendant.
38364
+ * @param {boolean|"fallback"|"restore"} [props.autoFocus="fallback"] - See
38365
+ * `focus_transfer.js` — `"fallback"` focuses the popover itself if it has
38366
+ * no other focusable descendant, `"restore"` keeps it out of the opening
38367
+ * focus chain unless it held focus when the popover closed.
38323
38368
  * @param {boolean} [props.open] - Controlled open state.
38324
38369
  * @param {boolean} [props.defaultOpen] - Uncontrolled, mount-only initial
38325
38370
  * open state — plays no entrance animation (nothing was ever shown as