@jsenv/navi 0.29.147 → 0.29.148

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.
@@ -31192,7 +31192,9 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
31192
31192
  * 5. The element focused before the container opened
31193
31193
  *
31194
31194
  * [navi-autofocus="restore"] appears in step 1 only: it never claims focus on
31195
- * a fresh open, it only gets it back.
31195
+ * a fresh open, it only gets it back. Unless a caller says the arrival is not a
31196
+ * fresh open but the answer to a press aimed at that very control — see
31197
+ * findFocusTarget's `restoreMayClaim`.
31196
31198
  *
31197
31199
  * A ladder that comes back empty — a container holding nothing focusable yet —
31198
31200
  * places no focus, and says so on the container ([navi-autofocus-unplaced]),
@@ -31340,9 +31342,19 @@ const markAutofocusRestoreOnClose = (
31340
31342
  * it, so landing there scrolls whatever comes before it out of sight.
31341
31343
  * transferFocus turns this on by itself wherever the keyboard is a virtual
31342
31344
  * one — see the reasoning there.
31345
+ * @param {boolean} [options.restoreMayClaim]
31346
+ * Lets `navi-autofocus="restore"` claim the focus like a plain `autoFocus`.
31347
+ * Its "never on a fresh open" is about a surface APPEARING — a sheet whose
31348
+ * field must not raise a phone's keyboard by opening. A press aimed at the
31349
+ * very control being handed the focus is not that: it IS the ask, and
31350
+ * refusing it leaves the focus on the button that was pressed. Only
31351
+ * "restore" is freed; "last-resort" still waits its turn.
31343
31352
  * @returns {{target: HTMLElement, reason: string}|undefined}
31344
31353
  */
31345
- const findFocusTarget = (containerEl, { skipFirstFocusable } = {}) => {
31354
+ const findFocusTarget = (
31355
+ containerEl,
31356
+ { skipFirstFocusable, restoreMayClaim } = {},
31357
+ ) => {
31346
31358
  // Not while there is anything else: what takes the focus only for want of
31347
31359
  // anything better ("last-resort") and what only takes it back ("restore").
31348
31360
  // Neither is dropped, both are simply tried later — step 3 below for the
@@ -31359,8 +31371,15 @@ const findFocusTarget = (containerEl, { skipFirstFocusable } = {}) => {
31359
31371
  const isHiddenFromAssistiveTech = (element) =>
31360
31372
  Boolean(element.closest?.(`[aria-hidden="true"]`));
31361
31373
 
31362
- const skip = (element) =>
31363
- isRestorableAutofocus(element) || isHiddenFromAssistiveTech(element);
31374
+ const skip = (element) => {
31375
+ if (isHiddenFromAssistiveTech(element)) {
31376
+ return true;
31377
+ }
31378
+ if (restoreMayClaim) {
31379
+ return element.getAttribute("navi-autofocus") === "last-resort";
31380
+ }
31381
+ return isRestorableAutofocus(element);
31382
+ };
31364
31383
 
31365
31384
  // Every mark, not just the first: a mark is only worth stopping at if it
31366
31385
  // leads somewhere focusable. One inside a screen waiting its turn (an inert
@@ -44619,6 +44638,7 @@ installImportMetaCssBuild(import.meta);/**
44619
44638
  * make the row thinner or thicker.
44620
44639
  */
44621
44640
  const css$O = /* css */`.navi_control_swap {
44641
+ font-size: var(--navi-control-font-size);
44622
44642
  --x-control-swap-size: var(--navi-control-swap-size, calc(var(--navi-control-line-height) + 2 * var(--navi-s) + 2 *
44623
44643
  var(--navi-control-border-width)));
44624
44644
  --x-control-swap-gap: var(--navi-control-swap-gap, var(--navi-xs));
@@ -44627,6 +44647,7 @@ const css$O = /* css */`.navi_control_swap {
44627
44647
  gap: var(--x-control-swap-gap);
44628
44648
 
44629
44649
  & > .navi_control_swap_cap {
44650
+ --button-background-color: var(--navi-surface-color);
44630
44651
  aspect-ratio: 1;
44631
44652
  flex: none;
44632
44653
  justify-content: center;
@@ -44817,7 +44838,7 @@ const ControlSwap = props => {
44817
44838
  id: `${slotIdPrefix}_${index}`,
44818
44839
  className: "navi_control_swap_slot",
44819
44840
  inert: index === activeIndex ? undefined : true,
44820
- children: side.children
44841
+ children: side.control
44821
44842
  }, side.name))
44822
44843
  })
44823
44844
  }), jsx(ControlSwapCap, {
@@ -44832,7 +44853,8 @@ const ControlSwap = props => {
44832
44853
 
44833
44854
  /**
44834
44855
  * One of the two controls, and the cap that speaks for it. Declarative: the row
44835
- * reads these and draws the caps at its ends, the controls between them.
44856
+ * reads these and draws the caps at its ends, the controls between them. The
44857
+ * props below say what the side IS; every other prop describes its cap.
44836
44858
  *
44837
44859
  * @type {import("ignore:preact").FunctionComponent<{
44838
44860
  * name?: string,
@@ -44841,6 +44863,7 @@ const ControlSwap = props => {
44841
44863
  * badge?: boolean | import("ignore:preact").ComponentChildren,
44842
44864
  * autoFocus?: boolean,
44843
44865
  * children?: import("ignore:preact").ComponentChildren,
44866
+ * [key: string]: any,
44844
44867
  * }>}
44845
44868
  * @param name - How `value`/`signal`/`onChange` name this side. Its position
44846
44869
  * ("0" or "1") by default.
@@ -44857,6 +44880,12 @@ const ControlSwap = props => {
44857
44880
  * that was pressed, for a control one reads before writing in (and, on a
44858
44881
  * phone, for a keyboard that must not rise). Never on mount, whatever the
44859
44882
  * setting.
44883
+ *
44884
+ * Anything else — `data-testid`, `variant`, `backgroundColor`, `color`, an
44885
+ * `aria-describedby` — goes to the cap, which is a `<Button>`. It is the one
44886
+ * element of the row an application does not render, so nothing else can name
44887
+ * it or dress it; the control it stands for is a vnode of the caller's own and
44888
+ * takes its props directly.
44860
44889
  */
44861
44890
  const ControlSwapSide = () => null;
44862
44891
  const ControlSwapCap = ({
@@ -44869,14 +44898,20 @@ const ControlSwapCap = ({
44869
44898
  const {
44870
44899
  icon,
44871
44900
  label,
44872
- badge
44873
- } = side;
44901
+ badge,
44902
+ ...capProps
44903
+ } = side.capProps;
44874
44904
  return jsxs(Button, {
44875
- ref: ref,
44876
- className: "navi_control_swap_cap",
44877
44905
  icon: true,
44878
44906
  pressEffect: "none",
44879
44907
  "aria-label": label,
44908
+ ...capProps,
44909
+ ref: ref
44910
+ // After the caller's props, all of them: the class is what the row's own
44911
+ // CSS reaches for, and the rest is the wiring that makes the cap a cap.
44912
+ ,
44913
+
44914
+ className: capProps.className ? `navi_control_swap_cap ${capProps.className}` : "navi_control_swap_cap",
44880
44915
  "aria-expanded": active,
44881
44916
  "aria-controls": slotId,
44882
44917
  onClick: onPress,
@@ -44898,13 +44933,20 @@ const readSides = children => {
44898
44933
  if (!child || child.type !== ControlSwapSide) {
44899
44934
  continue;
44900
44935
  }
44936
+ // What is left over once the side's own vocabulary is taken out describes
44937
+ // the cap: the control is a vnode the caller wrote and dresses itself, so
44938
+ // the cap is the only thing in the row left to describe.
44901
44939
  const {
44902
44940
  name,
44903
- ...rest
44941
+ autoFocus,
44942
+ children: control,
44943
+ ...capProps
44904
44944
  } = child.props;
44905
44945
  sides.push({
44906
44946
  name: name === undefined ? String(sides.length) : name,
44907
- ...rest
44947
+ autoFocus,
44948
+ control,
44949
+ capProps
44908
44950
  });
44909
44951
  }
44910
44952
  return sides;
@@ -44916,14 +44958,22 @@ const readSides = children => {
44916
44958
  //
44917
44959
  // WHERE inside is navi's own ladder (findFocusTarget): an `autoFocus` in the
44918
44960
  // control's own content first, the first focusable otherwise, a last resort
44919
- // after that. Unlike an arriving popup it keeps the first focusable even where
44920
- // the pointer is coarse (see docs/autofocus.md): a cap is pressed to reach the
44921
- // control it names, so a keyboard rising is the answer to that gesture rather
44922
- // than a cost imposed on a screen that merely appeared. A side one reads before
44923
- // writing in says `autoFocus={false}`.
44961
+ // after that. Two of the ladder's reflexes are turned off here, and it is the
44962
+ // same reason both times — they are about a surface APPEARING, and this is a
44963
+ // press aimed at the control being handed the focus:
44964
+ // - the first focusable is kept even where the pointer is coarse (an arriving
44965
+ // popup drops it so a virtual keyboard does not rise over what it just
44966
+ // showed, see docs/autofocus.md); pressing a cap to reach a field is asking
44967
+ // for that keyboard;
44968
+ // - `autoFocus="restore"` may claim it, though it means "never on a fresh
44969
+ // open". A field marked that way so its sheet opens quietly would otherwise
44970
+ // leave the focus on the magnifier that was pressed to reach it.
44971
+ // A side one reads before writing in says `autoFocus={false}`.
44924
44972
  const focusWithTheFloor = (arrivingSlot, arrivingSide, arrivingCap) => {
44925
44973
  if (arrivingSide.autoFocus !== false) {
44926
- const found = findFocusTarget(arrivingSlot);
44974
+ const found = findFocusTarget(arrivingSlot, {
44975
+ restoreMayClaim: true
44976
+ });
44927
44977
  if (found) {
44928
44978
  moveFocusTo(found.target);
44929
44979
  return;