@jsenv/navi 0.29.480 → 0.29.482

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.
@@ -41994,6 +41994,14 @@ const useParallelGuard = (max) => {
41994
41994
  * `getAssociatedLabels` in control_hooks.jsx. Both channels carry the same
41995
41995
  * values and land on the same setters, so a control reachable through both is
41996
41996
  * simply told twice.
41997
+ *
41998
+ * The link also runs the other way, for a control whose role is on its root
41999
+ * (`nameOn: "root"`, a wheel): a label[for] can only point at its host, an
42000
+ * aria-hidden carrier, and names nothing. That control says so in the state it
42001
+ * publishes; its label then drops `for` and publishes its own id under the
42002
+ * control's id instead, and the control names its root with the ids it finds
42003
+ * there (aria-labelledby). A label[for] kept as well would name the carrier
42004
+ * too, and a lookup by label would find two elements.
41997
42005
  */
41998
42006
 
41999
42007
  const stateByControlId = new Map();
@@ -42046,6 +42054,68 @@ const subscribeToControlState = (controlId, callback) => {
42046
42054
  };
42047
42055
  };
42048
42056
 
42057
+ const labelIdSetByControlId = new Map();
42058
+ const labelCallbackSetByControlId = new Map();
42059
+
42060
+ const notifyLabelIds = (controlId) => {
42061
+ const callbackSet = labelCallbackSetByControlId.get(controlId);
42062
+ if (!callbackSet) {
42063
+ return;
42064
+ }
42065
+ const labelIds = readLabelIds(controlId);
42066
+ for (const callback of callbackSet) {
42067
+ callback(labelIds);
42068
+ }
42069
+ };
42070
+ // One string ("label_a label_b"), the shape aria-labelledby takes, and equal
42071
+ // from one call to the next while nothing changed.
42072
+ const readLabelIds = (controlId) => {
42073
+ const labelIdSet = labelIdSetByControlId.get(controlId);
42074
+ return labelIdSet ? Array.from(labelIdSet).join(" ") : undefined;
42075
+ };
42076
+
42077
+ /**
42078
+ * Publishes `labelId` as a label of the control identified by `controlId`.
42079
+ * Returns the teardown.
42080
+ */
42081
+ const publishLabelToControl = (controlId, labelId) => {
42082
+ let labelIdSet = labelIdSetByControlId.get(controlId);
42083
+ if (!labelIdSet) {
42084
+ labelIdSet = new Set();
42085
+ labelIdSetByControlId.set(controlId, labelIdSet);
42086
+ }
42087
+ labelIdSet.add(labelId);
42088
+ notifyLabelIds(controlId);
42089
+ return () => {
42090
+ labelIdSet.delete(labelId);
42091
+ if (labelIdSet.size === 0) {
42092
+ labelIdSetByControlId.delete(controlId);
42093
+ }
42094
+ notifyLabelIds(controlId);
42095
+ };
42096
+ };
42097
+
42098
+ /**
42099
+ * Subscribes to the ids of the labels published for `controlId`. The callback
42100
+ * is called right away (with `undefined` when there is none), then on every
42101
+ * change. Returns the teardown.
42102
+ */
42103
+ const subscribeToControlLabels = (controlId, callback) => {
42104
+ let callbackSet = labelCallbackSetByControlId.get(controlId);
42105
+ if (!callbackSet) {
42106
+ callbackSet = new Set();
42107
+ labelCallbackSetByControlId.set(controlId, callbackSet);
42108
+ }
42109
+ callbackSet.add(callback);
42110
+ callback(readLabelIds(controlId));
42111
+ return () => {
42112
+ callbackSet.delete(callback);
42113
+ if (callbackSet.size === 0) {
42114
+ labelCallbackSetByControlId.delete(controlId);
42115
+ }
42116
+ };
42117
+ };
42118
+
42049
42119
  const FormContext = createContext();
42050
42120
 
42051
42121
  /**
@@ -46342,17 +46412,11 @@ const useControlProps = (props, {
46342
46412
  };
46343
46413
  }
46344
46414
  if (defaultAction === "activate") {
46345
- // activating the control (e.g. space on a button/range)
46346
46415
  return {
46347
46416
  name: `keydown to activate`,
46348
46417
  prevented: () => e.preventDefault(),
46349
46418
  allowed: () => {
46350
46419
  triggerUIAction(e);
46351
- if (controlType === "button" && e.key === " ") {
46352
- // prevent browser dispatching click
46353
- // it could lead to duplicates as we explicitely handled the space to click here
46354
- e.preventDefault();
46355
- }
46356
46420
  }
46357
46421
  };
46358
46422
  }
@@ -46489,7 +46553,15 @@ const useControlProps = (props, {
46489
46553
  runWhenActionSucceeded(completion, deferredCommand);
46490
46554
  };
46491
46555
  return {
46492
- keyDown: keyDownDefault,
46556
+ keyDown: e => {
46557
+ if (getKeyboardEventDefaultAction(e) === "activate") {
46558
+ // The browser presses the button itself — Enter clicks on
46559
+ // keydown, Space on keyup — and the click reaction below answers
46560
+ // that press, once: ui action, action and command together.
46561
+ return null;
46562
+ }
46563
+ return keyDownDefault(e);
46564
+ },
46493
46565
  mouseDown: e => {
46494
46566
  if (actsOnMouseDown) {
46495
46567
  return {
@@ -46508,7 +46580,9 @@ const useControlProps = (props, {
46508
46580
  return null;
46509
46581
  },
46510
46582
  click: e => {
46511
- if (actsOnMouseDown) {
46583
+ // A click no mousedown came before (the keyboard, a label) is a
46584
+ // press the mousedown reaction never saw.
46585
+ if (actsOnMouseDown && e.detail !== 0) {
46512
46586
  return null;
46513
46587
  }
46514
46588
  return {
@@ -47729,7 +47803,8 @@ const useInteractiveProps = (props, {
47729
47803
  const controlState = {
47730
47804
  disabled: disabledResolved,
47731
47805
  readOnly,
47732
- required: requiredResolved
47806
+ required: requiredResolved,
47807
+ nameOn
47733
47808
  };
47734
47809
  for (const label of getAssociatedLabels(element)) {
47735
47810
  label.dispatchEvent(new CustomEvent("navi_control_state", {
@@ -47750,6 +47825,22 @@ const useInteractiveProps = (props, {
47750
47825
  unpublishControlStateToLabels(publishedIdRef.current);
47751
47826
  };
47752
47827
  }, []);
47828
+ // Named on its root, the control is named by its labels' ids: they drop
47829
+ // their `for` for it (see control_label_state.js). A name the caller gives
47830
+ // wins.
47831
+ const [labelledBy, setLabelledBy] = useState();
47832
+ const {
47833
+ id
47834
+ } = props;
47835
+ useLayoutEffect(() => {
47836
+ if (nameOn !== "root" || !id) {
47837
+ return undefined;
47838
+ }
47839
+ return subscribeToControlLabels(id, setLabelledBy);
47840
+ }, [id]);
47841
+ if (labelledBy && controlRootProps["aria-label"] === undefined && controlRootProps["aria-labelledby"] === undefined) {
47842
+ controlRootProps["aria-labelledby"] = labelledBy;
47843
+ }
47753
47844
  }
47754
47845
  {
47755
47846
  const isCheckable = isCheckableInput(uiStateController.controlType, props.type);
@@ -58882,6 +58973,7 @@ const Label = props => {
58882
58973
  const [readOnly, setReadOnly] = useState(false);
58883
58974
  const [required, setRequired] = useState(false);
58884
58975
  const [connected, setConnected] = useState(false);
58976
+ const [namesControlRoot, setNamesControlRoot] = useState(false);
58885
58977
  // Set htmlFor only when we know the correct target id:
58886
58978
  // - caller explicitly provided one (even undefined to opt out)
58887
58979
  // - a parent Field provided one via ControlIdContext
@@ -58908,8 +59000,37 @@ const Label = props => {
58908
59000
  setDisabled(Boolean(controlState?.disabled));
58909
59001
  setReadOnly(Boolean(controlState?.readOnly));
58910
59002
  setRequired(Boolean(controlState?.required));
59003
+ setNamesControlRoot(controlState?.nameOn === "root");
58911
59004
  });
58912
59005
  }, [htmlFor]);
59006
+ // A control whose role is on its root (a wheel) is named by this label's id
59007
+ // rather than by `for`, which can only point at its aria-hidden host (see
59008
+ // control_label_state.js).
59009
+ const labelId = props.id;
59010
+ useLayoutEffect(() => {
59011
+ if (!htmlFor || !namesControlRoot) {
59012
+ return undefined;
59013
+ }
59014
+ return publishLabelToControl(htmlFor, labelId);
59015
+ }, [htmlFor, namesControlRoot, labelId]);
59016
+ // …and without `for`, the press that focuses the control is this label's
59017
+ // to answer. A press on something interactive inside the label stays that
59018
+ // thing's, as the browser does for a label[for].
59019
+ const onClickFocusingControlRoot = e => {
59020
+ props.onClick?.(e);
59021
+ if (e.defaultPrevented) {
59022
+ return;
59023
+ }
59024
+ const interactive = e.target.closest("a[href], button, input, select, textarea, [tabindex]");
59025
+ if (interactive && e.currentTarget.contains(interactive)) {
59026
+ return;
59027
+ }
59028
+ const host = document.getElementById(htmlFor);
59029
+ const controlRoot = host && findControlRoot(host);
59030
+ if (controlRoot) {
59031
+ moveFocusTo(controlRoot);
59032
+ }
59033
+ };
58913
59034
  const [messageProps, remainingProps] = extractMessageAndRemainingProps({
58914
59035
  ...props,
58915
59036
  requiredIndicator: undefined
@@ -58926,6 +59047,8 @@ const Label = props => {
58926
59047
  ":read-only": readOnly
58927
59048
  },
58928
59049
  ...remainingProps,
59050
+ htmlFor: namesControlRoot ? undefined : htmlFor,
59051
+ onClick: namesControlRoot ? onClickFocusingControlRoot : props.onClick,
58929
59052
  onnavi_control_state: e => {
58930
59053
  setConnected(true);
58931
59054
  setDisabled(e.detail.disabled);
@@ -66217,10 +66340,12 @@ const TimeHour = ({
66217
66340
  children: String(children)
66218
66341
  });
66219
66342
  }
66343
+ const dateTime = formatClock(hours * 3600);
66220
66344
  if (format === "timestring") {
66221
66345
  const totalMinutes = Math.round(hours * 60);
66222
66346
  const date = new Date(1970, 0, 1, Math.floor(totalMinutes / 60), totalMinutes % 60, 0);
66223
66347
  return jsx(TimeText, {
66348
+ dateTime: dateTime,
66224
66349
  ...props,
66225
66350
  children: formatTime(date, {
66226
66351
  lang
@@ -66233,6 +66358,7 @@ const TimeHour = ({
66233
66358
  forceUnit
66234
66359
  });
66235
66360
  return jsx(TimeText, {
66361
+ dateTime: dateTime,
66236
66362
  ...props,
66237
66363
  children: text
66238
66364
  });