@jsenv/navi 0.29.481 → 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.
- package/dist/dev/jsenv_navi.js +135 -9
- package/dist/dev/jsenv_navi.js.map +5 -5
- package/dist/jsenv_navi.js +135 -9
- package/dist/jsenv_navi.js.map +5 -5
- package/docs/testid.md +3 -1
- package/package.json +3 -3
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -43058,6 +43058,14 @@ const useParallelGuard = (max) => {
|
|
|
43058
43058
|
* `getAssociatedLabels` in control_hooks.jsx. Both channels carry the same
|
|
43059
43059
|
* values and land on the same setters, so a control reachable through both is
|
|
43060
43060
|
* simply told twice.
|
|
43061
|
+
*
|
|
43062
|
+
* The link also runs the other way, for a control whose role is on its root
|
|
43063
|
+
* (`nameOn: "root"`, a wheel): a label[for] can only point at its host, an
|
|
43064
|
+
* aria-hidden carrier, and names nothing. That control says so in the state it
|
|
43065
|
+
* publishes; its label then drops `for` and publishes its own id under the
|
|
43066
|
+
* control's id instead, and the control names its root with the ids it finds
|
|
43067
|
+
* there (aria-labelledby). A label[for] kept as well would name the carrier
|
|
43068
|
+
* too, and a lookup by label would find two elements.
|
|
43061
43069
|
*/
|
|
43062
43070
|
|
|
43063
43071
|
const stateByControlId = new Map();
|
|
@@ -43110,6 +43118,68 @@ const subscribeToControlState = (controlId, callback) => {
|
|
|
43110
43118
|
};
|
|
43111
43119
|
};
|
|
43112
43120
|
|
|
43121
|
+
const labelIdSetByControlId = new Map();
|
|
43122
|
+
const labelCallbackSetByControlId = new Map();
|
|
43123
|
+
|
|
43124
|
+
const notifyLabelIds = (controlId) => {
|
|
43125
|
+
const callbackSet = labelCallbackSetByControlId.get(controlId);
|
|
43126
|
+
if (!callbackSet) {
|
|
43127
|
+
return;
|
|
43128
|
+
}
|
|
43129
|
+
const labelIds = readLabelIds(controlId);
|
|
43130
|
+
for (const callback of callbackSet) {
|
|
43131
|
+
callback(labelIds);
|
|
43132
|
+
}
|
|
43133
|
+
};
|
|
43134
|
+
// One string ("label_a label_b"), the shape aria-labelledby takes, and equal
|
|
43135
|
+
// from one call to the next while nothing changed.
|
|
43136
|
+
const readLabelIds = (controlId) => {
|
|
43137
|
+
const labelIdSet = labelIdSetByControlId.get(controlId);
|
|
43138
|
+
return labelIdSet ? Array.from(labelIdSet).join(" ") : undefined;
|
|
43139
|
+
};
|
|
43140
|
+
|
|
43141
|
+
/**
|
|
43142
|
+
* Publishes `labelId` as a label of the control identified by `controlId`.
|
|
43143
|
+
* Returns the teardown.
|
|
43144
|
+
*/
|
|
43145
|
+
const publishLabelToControl = (controlId, labelId) => {
|
|
43146
|
+
let labelIdSet = labelIdSetByControlId.get(controlId);
|
|
43147
|
+
if (!labelIdSet) {
|
|
43148
|
+
labelIdSet = new Set();
|
|
43149
|
+
labelIdSetByControlId.set(controlId, labelIdSet);
|
|
43150
|
+
}
|
|
43151
|
+
labelIdSet.add(labelId);
|
|
43152
|
+
notifyLabelIds(controlId);
|
|
43153
|
+
return () => {
|
|
43154
|
+
labelIdSet.delete(labelId);
|
|
43155
|
+
if (labelIdSet.size === 0) {
|
|
43156
|
+
labelIdSetByControlId.delete(controlId);
|
|
43157
|
+
}
|
|
43158
|
+
notifyLabelIds(controlId);
|
|
43159
|
+
};
|
|
43160
|
+
};
|
|
43161
|
+
|
|
43162
|
+
/**
|
|
43163
|
+
* Subscribes to the ids of the labels published for `controlId`. The callback
|
|
43164
|
+
* is called right away (with `undefined` when there is none), then on every
|
|
43165
|
+
* change. Returns the teardown.
|
|
43166
|
+
*/
|
|
43167
|
+
const subscribeToControlLabels = (controlId, callback) => {
|
|
43168
|
+
let callbackSet = labelCallbackSetByControlId.get(controlId);
|
|
43169
|
+
if (!callbackSet) {
|
|
43170
|
+
callbackSet = new Set();
|
|
43171
|
+
labelCallbackSetByControlId.set(controlId, callbackSet);
|
|
43172
|
+
}
|
|
43173
|
+
callbackSet.add(callback);
|
|
43174
|
+
callback(readLabelIds(controlId));
|
|
43175
|
+
return () => {
|
|
43176
|
+
callbackSet.delete(callback);
|
|
43177
|
+
if (callbackSet.size === 0) {
|
|
43178
|
+
labelCallbackSetByControlId.delete(controlId);
|
|
43179
|
+
}
|
|
43180
|
+
};
|
|
43181
|
+
};
|
|
43182
|
+
|
|
43113
43183
|
const FormContext = createContext();
|
|
43114
43184
|
|
|
43115
43185
|
/**
|
|
@@ -47522,17 +47592,11 @@ const useControlProps = (props, {
|
|
|
47522
47592
|
};
|
|
47523
47593
|
}
|
|
47524
47594
|
if (defaultAction === "activate") {
|
|
47525
|
-
// activating the control (e.g. space on a button/range)
|
|
47526
47595
|
return {
|
|
47527
47596
|
name: `keydown to activate`,
|
|
47528
47597
|
prevented: () => e.preventDefault(),
|
|
47529
47598
|
allowed: () => {
|
|
47530
47599
|
triggerUIAction(e);
|
|
47531
|
-
if (controlType === "button" && e.key === " ") {
|
|
47532
|
-
// prevent browser dispatching click
|
|
47533
|
-
// it could lead to duplicates as we explicitely handled the space to click here
|
|
47534
|
-
e.preventDefault();
|
|
47535
|
-
}
|
|
47536
47600
|
}
|
|
47537
47601
|
};
|
|
47538
47602
|
}
|
|
@@ -47669,7 +47733,15 @@ const useControlProps = (props, {
|
|
|
47669
47733
|
runWhenActionSucceeded(completion, deferredCommand);
|
|
47670
47734
|
};
|
|
47671
47735
|
return {
|
|
47672
|
-
keyDown:
|
|
47736
|
+
keyDown: e => {
|
|
47737
|
+
if (getKeyboardEventDefaultAction(e) === "activate") {
|
|
47738
|
+
// The browser presses the button itself — Enter clicks on
|
|
47739
|
+
// keydown, Space on keyup — and the click reaction below answers
|
|
47740
|
+
// that press, once: ui action, action and command together.
|
|
47741
|
+
return null;
|
|
47742
|
+
}
|
|
47743
|
+
return keyDownDefault(e);
|
|
47744
|
+
},
|
|
47673
47745
|
mouseDown: e => {
|
|
47674
47746
|
if (actsOnMouseDown) {
|
|
47675
47747
|
return {
|
|
@@ -47688,7 +47760,9 @@ const useControlProps = (props, {
|
|
|
47688
47760
|
return null;
|
|
47689
47761
|
},
|
|
47690
47762
|
click: e => {
|
|
47691
|
-
|
|
47763
|
+
// A click no mousedown came before (the keyboard, a label) is a
|
|
47764
|
+
// press the mousedown reaction never saw.
|
|
47765
|
+
if (actsOnMouseDown && e.detail !== 0) {
|
|
47692
47766
|
return null;
|
|
47693
47767
|
}
|
|
47694
47768
|
return {
|
|
@@ -48932,7 +49006,8 @@ const useInteractiveProps = (props, {
|
|
|
48932
49006
|
const controlState = {
|
|
48933
49007
|
disabled: disabledResolved,
|
|
48934
49008
|
readOnly,
|
|
48935
|
-
required: requiredResolved
|
|
49009
|
+
required: requiredResolved,
|
|
49010
|
+
nameOn
|
|
48936
49011
|
};
|
|
48937
49012
|
for (const label of getAssociatedLabels(element)) {
|
|
48938
49013
|
label.dispatchEvent(new CustomEvent("navi_control_state", {
|
|
@@ -48953,6 +49028,22 @@ const useInteractiveProps = (props, {
|
|
|
48953
49028
|
unpublishControlStateToLabels(publishedIdRef.current);
|
|
48954
49029
|
};
|
|
48955
49030
|
}, []);
|
|
49031
|
+
// Named on its root, the control is named by its labels' ids: they drop
|
|
49032
|
+
// their `for` for it (see control_label_state.js). A name the caller gives
|
|
49033
|
+
// wins.
|
|
49034
|
+
const [labelledBy, setLabelledBy] = useState();
|
|
49035
|
+
const {
|
|
49036
|
+
id
|
|
49037
|
+
} = props;
|
|
49038
|
+
useLayoutEffect(() => {
|
|
49039
|
+
if (nameOn !== "root" || !id) {
|
|
49040
|
+
return undefined;
|
|
49041
|
+
}
|
|
49042
|
+
return subscribeToControlLabels(id, setLabelledBy);
|
|
49043
|
+
}, [id]);
|
|
49044
|
+
if (labelledBy && controlRootProps["aria-label"] === undefined && controlRootProps["aria-labelledby"] === undefined) {
|
|
49045
|
+
controlRootProps["aria-labelledby"] = labelledBy;
|
|
49046
|
+
}
|
|
48956
49047
|
}
|
|
48957
49048
|
{
|
|
48958
49049
|
const isCheckable = isCheckableInput(uiStateController.controlType, props.type);
|
|
@@ -60123,6 +60214,7 @@ const Label = props => {
|
|
|
60123
60214
|
const [readOnly, setReadOnly] = useState(false);
|
|
60124
60215
|
const [required, setRequired] = useState(false);
|
|
60125
60216
|
const [connected, setConnected] = useState(false);
|
|
60217
|
+
const [namesControlRoot, setNamesControlRoot] = useState(false);
|
|
60126
60218
|
// Set htmlFor only when we know the correct target id:
|
|
60127
60219
|
// - caller explicitly provided one (even undefined to opt out)
|
|
60128
60220
|
// - a parent Field provided one via ControlIdContext
|
|
@@ -60149,8 +60241,37 @@ const Label = props => {
|
|
|
60149
60241
|
setDisabled(Boolean(controlState?.disabled));
|
|
60150
60242
|
setReadOnly(Boolean(controlState?.readOnly));
|
|
60151
60243
|
setRequired(Boolean(controlState?.required));
|
|
60244
|
+
setNamesControlRoot(controlState?.nameOn === "root");
|
|
60152
60245
|
});
|
|
60153
60246
|
}, [htmlFor]);
|
|
60247
|
+
// A control whose role is on its root (a wheel) is named by this label's id
|
|
60248
|
+
// rather than by `for`, which can only point at its aria-hidden host (see
|
|
60249
|
+
// control_label_state.js).
|
|
60250
|
+
const labelId = props.id;
|
|
60251
|
+
useLayoutEffect(() => {
|
|
60252
|
+
if (!htmlFor || !namesControlRoot) {
|
|
60253
|
+
return undefined;
|
|
60254
|
+
}
|
|
60255
|
+
return publishLabelToControl(htmlFor, labelId);
|
|
60256
|
+
}, [htmlFor, namesControlRoot, labelId]);
|
|
60257
|
+
// …and without `for`, the press that focuses the control is this label's
|
|
60258
|
+
// to answer. A press on something interactive inside the label stays that
|
|
60259
|
+
// thing's, as the browser does for a label[for].
|
|
60260
|
+
const onClickFocusingControlRoot = e => {
|
|
60261
|
+
props.onClick?.(e);
|
|
60262
|
+
if (e.defaultPrevented) {
|
|
60263
|
+
return;
|
|
60264
|
+
}
|
|
60265
|
+
const interactive = e.target.closest("a[href], button, input, select, textarea, [tabindex]");
|
|
60266
|
+
if (interactive && e.currentTarget.contains(interactive)) {
|
|
60267
|
+
return;
|
|
60268
|
+
}
|
|
60269
|
+
const host = document.getElementById(htmlFor);
|
|
60270
|
+
const controlRoot = host && findControlRoot(host);
|
|
60271
|
+
if (controlRoot) {
|
|
60272
|
+
moveFocusTo(controlRoot);
|
|
60273
|
+
}
|
|
60274
|
+
};
|
|
60154
60275
|
const [messageProps, remainingProps] = extractMessageAndRemainingProps({
|
|
60155
60276
|
...props,
|
|
60156
60277
|
requiredIndicator: undefined
|
|
@@ -60167,6 +60288,8 @@ const Label = props => {
|
|
|
60167
60288
|
":read-only": readOnly
|
|
60168
60289
|
},
|
|
60169
60290
|
...remainingProps,
|
|
60291
|
+
htmlFor: namesControlRoot ? undefined : htmlFor,
|
|
60292
|
+
onClick: namesControlRoot ? onClickFocusingControlRoot : props.onClick,
|
|
60170
60293
|
onnavi_control_state: e => {
|
|
60171
60294
|
setConnected(true);
|
|
60172
60295
|
setDisabled(e.detail.disabled);
|
|
@@ -67508,10 +67631,12 @@ const TimeHour = ({
|
|
|
67508
67631
|
children: String(children)
|
|
67509
67632
|
});
|
|
67510
67633
|
}
|
|
67634
|
+
const dateTime = formatClock(hours * 3600);
|
|
67511
67635
|
if (format === "timestring") {
|
|
67512
67636
|
const totalMinutes = Math.round(hours * 60);
|
|
67513
67637
|
const date = new Date(1970, 0, 1, Math.floor(totalMinutes / 60), totalMinutes % 60, 0);
|
|
67514
67638
|
return jsx(TimeText, {
|
|
67639
|
+
dateTime: dateTime,
|
|
67515
67640
|
...props,
|
|
67516
67641
|
children: formatTime(date, {
|
|
67517
67642
|
lang
|
|
@@ -67524,6 +67649,7 @@ const TimeHour = ({
|
|
|
67524
67649
|
forceUnit
|
|
67525
67650
|
});
|
|
67526
67651
|
return jsx(TimeText, {
|
|
67652
|
+
dateTime: dateTime,
|
|
67527
67653
|
...props,
|
|
67528
67654
|
children: text
|
|
67529
67655
|
});
|