@jsenv/navi 0.27.3 → 0.27.4
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 +436 -79
- package/dist/jsenv_navi.js.map +48 -39
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -12823,6 +12823,17 @@ const requestPseudoStateCheck = (element, detail) => {
|
|
|
12823
12823
|
"navi_pseudo_state_request_check",
|
|
12824
12824
|
detail,
|
|
12825
12825
|
);
|
|
12826
|
+
// When a control has a visible proxy mirroring its state (e.g. selectable
|
|
12827
|
+
// radio with `navi-control-proxy-for`), re-check the proxy too so it stays
|
|
12828
|
+
// in sync with the real control.
|
|
12829
|
+
const proxy = findControlProxy(element);
|
|
12830
|
+
if (proxy) {
|
|
12831
|
+
dispatchInternalCustomEvent(
|
|
12832
|
+
proxy,
|
|
12833
|
+
"navi_pseudo_state_request_check",
|
|
12834
|
+
detail,
|
|
12835
|
+
);
|
|
12836
|
+
}
|
|
12826
12837
|
};
|
|
12827
12838
|
const NAVI_PSEUDO_STATE_CUSTOM_EVENT = "navi_pseudo_state";
|
|
12828
12839
|
const dispatchPseudoStateCustomEvent = (element, value, oldValue) => {
|
|
@@ -13045,7 +13056,10 @@ definePseudoClass(":active", {
|
|
|
13045
13056
|
// component.
|
|
13046
13057
|
//
|
|
13047
13058
|
// When a controller element (e.g. combobox input) gains or loses focus,
|
|
13048
|
-
// notify the elements it controls via aria-controls so they re-check their
|
|
13059
|
+
// notify the elements it controls via aria-controls so they re-check their
|
|
13060
|
+
// focus state. `requestPseudoStateCheck` also re-checks the controlled
|
|
13061
|
+
// element's proxy (if any), so the visible proxy mirrors the hidden real
|
|
13062
|
+
// input's inherited focus.
|
|
13049
13063
|
const notifyAriaControlled = (el, e) => {
|
|
13050
13064
|
const controlledIds = el.getAttribute("aria-controls");
|
|
13051
13065
|
if (!controlledIds) {
|
|
@@ -13058,53 +13072,104 @@ definePseudoClass(":active", {
|
|
|
13058
13072
|
}
|
|
13059
13073
|
}
|
|
13060
13074
|
};
|
|
13061
|
-
//
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
const
|
|
13067
|
-
|
|
13075
|
+
// Returns true when el holds focus indirectly — either because a controlling
|
|
13076
|
+
// element (aria-controls) has focus, or because el is a proxy whose target
|
|
13077
|
+
// is itself controlled by a focused element.
|
|
13078
|
+
const hasIndirectFocus = (el, { requireFocusVisible = false } = {}) => {
|
|
13079
|
+
const pseudoClass = requireFocusVisible ? ":focus-visible" : ":focus";
|
|
13080
|
+
const isControlledBy = (target) => {
|
|
13081
|
+
const id = target.id;
|
|
13082
|
+
if (!id) {
|
|
13083
|
+
return false;
|
|
13084
|
+
}
|
|
13085
|
+
const controllers = document.querySelectorAll(`[aria-controls~="${id}"]`);
|
|
13086
|
+
for (const controller of controllers) {
|
|
13087
|
+
// If the controller is inside the element it controls, focus is already
|
|
13088
|
+
// native (:focus-within) — no need to inherit it.
|
|
13089
|
+
if (target.contains(controller)) {
|
|
13090
|
+
continue;
|
|
13091
|
+
}
|
|
13092
|
+
if (controller.matches(pseudoClass)) {
|
|
13093
|
+
return true;
|
|
13094
|
+
}
|
|
13095
|
+
}
|
|
13068
13096
|
return false;
|
|
13097
|
+
};
|
|
13098
|
+
if (isControlledBy(el)) {
|
|
13099
|
+
return true;
|
|
13069
13100
|
}
|
|
13070
|
-
const
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
13074
|
-
if (el.contains(controller)) {
|
|
13075
|
-
continue;
|
|
13101
|
+
const proxyTarget = findControlProxyTarget(el);
|
|
13102
|
+
if (proxyTarget) {
|
|
13103
|
+
if (proxyTarget.matches(pseudoClass)) {
|
|
13104
|
+
return true;
|
|
13076
13105
|
}
|
|
13077
|
-
|
|
13078
|
-
if (controller.matches(pseudoClass)) {
|
|
13106
|
+
if (isControlledBy(proxyTarget)) {
|
|
13079
13107
|
return true;
|
|
13080
13108
|
}
|
|
13081
13109
|
}
|
|
13082
13110
|
return false;
|
|
13083
13111
|
};
|
|
13084
13112
|
|
|
13113
|
+
// Shared setup for :focus and :focus-visible. Both need focusin/focusout
|
|
13114
|
+
// listeners + a MutationObserver on aria-controls so that when the attribute
|
|
13115
|
+
// changes while the element is focused, old and new controlled elements are
|
|
13116
|
+
// notified to re-check their own focus state.
|
|
13117
|
+
// extraSetup: optional (el, callback) => teardown for pseudo-class-specific
|
|
13118
|
+
// listeners (e.g. keydown/keyup for :focus-visible).
|
|
13119
|
+
const setupFocus = (el, callback) => {
|
|
13120
|
+
const onFocusChange = (e) => {
|
|
13121
|
+
callback();
|
|
13122
|
+
notifyAriaControlled(el, e);
|
|
13123
|
+
};
|
|
13124
|
+
el.addEventListener("focusin", onFocusChange);
|
|
13125
|
+
el.addEventListener("focusout", onFocusChange);
|
|
13126
|
+
// Only observe aria-controls mutations when the element already has the
|
|
13127
|
+
// attribute at setup time. If aria-controls is guaranteed to be set before
|
|
13128
|
+
// initPseudoStyles runs (e.g. passed as a prop in box.jsx), this covers all
|
|
13129
|
+
// real cases without paying the MutationObserver cost for every element.
|
|
13130
|
+
let observer;
|
|
13131
|
+
// if (el.hasAttribute("aria-controls")) {
|
|
13132
|
+
observer = new MutationObserver((mutations) => {
|
|
13133
|
+
if (!el.matches(":focus-within")) {
|
|
13134
|
+
return;
|
|
13135
|
+
}
|
|
13136
|
+
for (const mutation of mutations) {
|
|
13137
|
+
const oldIds = (mutation.oldValue || "").split(" ").filter(Boolean);
|
|
13138
|
+
for (const id of oldIds) {
|
|
13139
|
+
const controlled = document.getElementById(id);
|
|
13140
|
+
if (controlled) {
|
|
13141
|
+
requestPseudoStateCheck(controlled, {});
|
|
13142
|
+
}
|
|
13143
|
+
}
|
|
13144
|
+
}
|
|
13145
|
+
notifyAriaControlled(el, {});
|
|
13146
|
+
});
|
|
13147
|
+
observer.observe(el, {
|
|
13148
|
+
attributes: true,
|
|
13149
|
+
attributeFilter: ["aria-controls"],
|
|
13150
|
+
attributeOldValue: true,
|
|
13151
|
+
});
|
|
13152
|
+
// }
|
|
13153
|
+
return () => {
|
|
13154
|
+
el.removeEventListener("focusin", onFocusChange);
|
|
13155
|
+
el.removeEventListener("focusout", onFocusChange);
|
|
13156
|
+
observer?.disconnect();
|
|
13157
|
+
};
|
|
13158
|
+
};
|
|
13159
|
+
|
|
13085
13160
|
definePseudoClass(":focus", {
|
|
13086
13161
|
attribute: "data-focus",
|
|
13087
13162
|
setup: (el, callback) => {
|
|
13088
|
-
const
|
|
13089
|
-
callback();
|
|
13090
|
-
notifyAriaControlled(el, e);
|
|
13091
|
-
};
|
|
13092
|
-
el.addEventListener("focusin", onFocusChange);
|
|
13093
|
-
el.addEventListener("focusout", onFocusChange);
|
|
13163
|
+
const cleanup = setupFocus(el, callback);
|
|
13094
13164
|
return () => {
|
|
13095
|
-
|
|
13096
|
-
el.removeEventListener("focusout", onFocusChange);
|
|
13165
|
+
cleanup();
|
|
13097
13166
|
};
|
|
13098
13167
|
},
|
|
13099
13168
|
test: (el) => {
|
|
13100
13169
|
if (el.matches(":focus")) {
|
|
13101
13170
|
return true;
|
|
13102
13171
|
}
|
|
13103
|
-
|
|
13104
|
-
if (proxyTarget && proxyTarget.matches(":focus")) {
|
|
13105
|
-
return true;
|
|
13106
|
-
}
|
|
13107
|
-
if (isControlledByFocusedElement(el)) {
|
|
13172
|
+
if (hasIndirectFocus(el)) {
|
|
13108
13173
|
return true;
|
|
13109
13174
|
}
|
|
13110
13175
|
return false;
|
|
@@ -13113,30 +13178,20 @@ definePseudoClass(":active", {
|
|
|
13113
13178
|
definePseudoClass(":focus-visible", {
|
|
13114
13179
|
attribute: "data-focus-visible",
|
|
13115
13180
|
setup: (el, callback) => {
|
|
13116
|
-
const
|
|
13117
|
-
callback();
|
|
13118
|
-
notifyAriaControlled(el, e);
|
|
13119
|
-
};
|
|
13181
|
+
const cleanup = setupFocus(el, callback);
|
|
13120
13182
|
document.addEventListener("keydown", callback);
|
|
13121
13183
|
document.addEventListener("keyup", callback);
|
|
13122
|
-
el.addEventListener("focusin", onFocusChange);
|
|
13123
|
-
el.addEventListener("focusout", onFocusChange);
|
|
13124
13184
|
return () => {
|
|
13185
|
+
cleanup();
|
|
13125
13186
|
document.removeEventListener("keydown", callback);
|
|
13126
13187
|
document.removeEventListener("keyup", callback);
|
|
13127
|
-
el.removeEventListener("focusin", onFocusChange);
|
|
13128
|
-
el.removeEventListener("focusout", onFocusChange);
|
|
13129
13188
|
};
|
|
13130
13189
|
},
|
|
13131
13190
|
test: (el) => {
|
|
13132
13191
|
if (el.matches(":focus-visible")) {
|
|
13133
13192
|
return true;
|
|
13134
13193
|
}
|
|
13135
|
-
|
|
13136
|
-
if (proxyTarget && proxyTarget.matches(":focus-visible")) {
|
|
13137
|
-
return true;
|
|
13138
|
-
}
|
|
13139
|
-
if (isControlledByFocusedElement(el, { requireFocusVisible: true })) {
|
|
13194
|
+
if (hasIndirectFocus(el, { requireFocusVisible: true })) {
|
|
13140
13195
|
return true;
|
|
13141
13196
|
}
|
|
13142
13197
|
return false;
|
|
@@ -13160,7 +13215,7 @@ definePseudoClass(":active", {
|
|
|
13160
13215
|
if (el.matches(":focus-within")) {
|
|
13161
13216
|
return true;
|
|
13162
13217
|
}
|
|
13163
|
-
if (
|
|
13218
|
+
if (hasIndirectFocus(el)) {
|
|
13164
13219
|
return true;
|
|
13165
13220
|
}
|
|
13166
13221
|
if (el.contains(document.activeElement)) {
|
|
@@ -18945,6 +19000,22 @@ naviI18n.addAll({
|
|
|
18945
19000
|
},
|
|
18946
19001
|
});
|
|
18947
19002
|
|
|
19003
|
+
// List messages — override any key to customize list messages
|
|
19004
|
+
naviI18n.addAll({
|
|
19005
|
+
"list.empty": {
|
|
19006
|
+
en: "No items in this list.",
|
|
19007
|
+
fr: "Aucun élément dans cette liste.",
|
|
19008
|
+
},
|
|
19009
|
+
"list.no_match": {
|
|
19010
|
+
en: "No item matches this search.",
|
|
19011
|
+
fr: "Aucun élément ne correspond à cette recherche.",
|
|
19012
|
+
},
|
|
19013
|
+
"list.no_match_rest_shown": {
|
|
19014
|
+
en: "No item matches this search. The rest is shown below.",
|
|
19015
|
+
fr: "Aucun élément ne correspond à cette recherche. Le reste est affiché ci-dessous.",
|
|
19016
|
+
},
|
|
19017
|
+
});
|
|
19018
|
+
|
|
18948
19019
|
// Constraint validation messages — override any key to customize error messages
|
|
18949
19020
|
naviI18n.addAll({
|
|
18950
19021
|
"constraint.available": {
|
|
@@ -21673,11 +21744,12 @@ const useCustomValidationRef = (
|
|
|
21673
21744
|
}
|
|
21674
21745
|
const element = elementRef.current;
|
|
21675
21746
|
if (!element) {
|
|
21676
|
-
|
|
21677
|
-
|
|
21747
|
+
if (!elementRef.nullExpected) {
|
|
21748
|
+
console.warn(
|
|
21749
|
+
`useCustomValidationRef: elementRef.current is null, make sure to pass a ref to an element
|
|
21678
21750
|
${callSiteRef.current}`,
|
|
21679
|
-
|
|
21680
|
-
|
|
21751
|
+
);
|
|
21752
|
+
/* can happen if the component does this for instance:
|
|
21681
21753
|
const Component = () => {
|
|
21682
21754
|
const ref = useRef(null)
|
|
21683
21755
|
|
|
@@ -21689,6 +21761,7 @@ ${callSiteRef.current}`,
|
|
|
21689
21761
|
|
|
21690
21762
|
usually it's better to split the component in two but hey
|
|
21691
21763
|
*/
|
|
21764
|
+
}
|
|
21692
21765
|
return undefined;
|
|
21693
21766
|
}
|
|
21694
21767
|
let target;
|
|
@@ -22997,10 +23070,7 @@ const applyKeyboardShortcuts = (shortcuts, keyboardEvent) => {
|
|
|
22997
23070
|
} else {
|
|
22998
23071
|
// we need to check if the default action is something that we can allow to be intercepted
|
|
22999
23072
|
const defaultAction = getKeyboardEventDefaultAction(keyboardEvent);
|
|
23000
|
-
canIntercept =
|
|
23001
|
-
!defaultAction ||
|
|
23002
|
-
defaultAction === "scroll" ||
|
|
23003
|
-
defaultAction === "dismiss";
|
|
23073
|
+
canIntercept = !defaultAction || defaultAction === "scroll";
|
|
23004
23074
|
}
|
|
23005
23075
|
if (!canIntercept) {
|
|
23006
23076
|
return null;
|
|
@@ -24518,6 +24588,13 @@ const useAutoFocus = (
|
|
|
24518
24588
|
if (!focusableElement) {
|
|
24519
24589
|
return () => {};
|
|
24520
24590
|
}
|
|
24591
|
+
// Only autofocus when the element is mounted directly on the document.
|
|
24592
|
+
// Any other event type means an expandable (popover, dialog, …) just opened
|
|
24593
|
+
// and revealed this element — the expandable's opening logic already calls
|
|
24594
|
+
// focusFirstAutofocusOrFocusable, so we must not steal focus here.
|
|
24595
|
+
if (e.type !== "navi_displayed_on_document") {
|
|
24596
|
+
return () => {};
|
|
24597
|
+
}
|
|
24521
24598
|
const activeElement = document.activeElement;
|
|
24522
24599
|
const focusDebugCall = `${getElementSignature(focusableElement)}.focus({ preventScroll: ${preventScroll} })`;
|
|
24523
24600
|
if (e.type === "navi_displayed_on_document") {
|
|
@@ -24920,12 +24997,8 @@ const createUICallback = ({ name = "ui callback", event, action }) => {
|
|
|
24920
24997
|
if (event && action) {
|
|
24921
24998
|
return (...args) => {
|
|
24922
24999
|
return routeArgs(args, {
|
|
24923
|
-
event
|
|
24924
|
-
|
|
24925
|
-
},
|
|
24926
|
-
action: (value, actionSecondArg) => {
|
|
24927
|
-
return action(value, actionSecondArg);
|
|
24928
|
-
},
|
|
25000
|
+
event,
|
|
25001
|
+
action,
|
|
24929
25002
|
other: () => {
|
|
24930
25003
|
console.warn(
|
|
24931
25004
|
`${name} unsupported call attempt. It is designed to be called by action.`,
|
|
@@ -24944,9 +25017,7 @@ const createUICallback = ({ name = "ui callback", event, action }) => {
|
|
|
24944
25017
|
);
|
|
24945
25018
|
return false;
|
|
24946
25019
|
},
|
|
24947
|
-
action
|
|
24948
|
-
return action(value, secondArg);
|
|
24949
|
-
},
|
|
25020
|
+
action,
|
|
24950
25021
|
other: () => {
|
|
24951
25022
|
console.warn(
|
|
24952
25023
|
`${name} unsupported call attempt. It is designed to be called by action.`,
|
|
@@ -24959,9 +25030,7 @@ const createUICallback = ({ name = "ui callback", event, action }) => {
|
|
|
24959
25030
|
// event only
|
|
24960
25031
|
return (...args) => {
|
|
24961
25032
|
return routeArgs(args, {
|
|
24962
|
-
event
|
|
24963
|
-
return event(e);
|
|
24964
|
-
},
|
|
25033
|
+
event,
|
|
24965
25034
|
action: (_, { event: eventFromArg }) => {
|
|
24966
25035
|
console.info(
|
|
24967
25036
|
`${name} got called by action. It works but is designed to be called by DOM`,
|
|
@@ -24999,8 +25068,8 @@ const routeArgs = (args, { event, action, other }) => {
|
|
|
24999
25068
|
return other(...args);
|
|
25000
25069
|
};
|
|
25001
25070
|
|
|
25002
|
-
const triggerStringAction = (actionName, event) => {
|
|
25003
|
-
return resolveActionProp(actionName)(event);
|
|
25071
|
+
const triggerStringAction = (actionName, event, options) => {
|
|
25072
|
+
return resolveActionProp(actionName)(event, options);
|
|
25004
25073
|
};
|
|
25005
25074
|
const resolveActionProp = (action) => {
|
|
25006
25075
|
if (typeof action === "string") {
|
|
@@ -25198,11 +25267,13 @@ const requestClosestAction = (event) => {
|
|
|
25198
25267
|
*/
|
|
25199
25268
|
const clear = createUICallback({
|
|
25200
25269
|
name: "clear",
|
|
25201
|
-
event: (event) => {
|
|
25270
|
+
event: (event, { skipClose } = {}) => {
|
|
25202
25271
|
requestUpdate(event, "", { isClear: true });
|
|
25203
|
-
|
|
25204
|
-
|
|
25205
|
-
|
|
25272
|
+
if (!skipClose) {
|
|
25273
|
+
const expandableEl = event.currentTarget.closest("[aria-expanded]");
|
|
25274
|
+
if (expandableEl) {
|
|
25275
|
+
return requestClose(event);
|
|
25276
|
+
}
|
|
25206
25277
|
}
|
|
25207
25278
|
return true;
|
|
25208
25279
|
},
|
|
@@ -25831,7 +25902,8 @@ const useInteractiveProps = (props, {
|
|
|
25831
25902
|
autoSelect
|
|
25832
25903
|
});
|
|
25833
25904
|
Object.assign(controlProps, {
|
|
25834
|
-
"navi-autofocus": autoFocus ? "" : undefined
|
|
25905
|
+
"navi-autofocus": autoFocus ? autoFocus === true ? "" : autoFocus : undefined,
|
|
25906
|
+
"navi-autofocus-select": autoFocus && autoSelect ? "" : undefined
|
|
25835
25907
|
});
|
|
25836
25908
|
}
|
|
25837
25909
|
{
|
|
@@ -29903,6 +29975,78 @@ const Label = props => {
|
|
|
29903
29975
|
};
|
|
29904
29976
|
const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
|
|
29905
29977
|
|
|
29978
|
+
const InputWithList = props => {
|
|
29979
|
+
const Next = useNextResolver();
|
|
29980
|
+
const {
|
|
29981
|
+
"navi-list": naviList,
|
|
29982
|
+
onKeyDown
|
|
29983
|
+
} = props;
|
|
29984
|
+
const getListEl = () => {
|
|
29985
|
+
return document.getElementById(naviList);
|
|
29986
|
+
};
|
|
29987
|
+
const [currentId, setCurrentId] = useState(() => {
|
|
29988
|
+
const listEl = getListEl();
|
|
29989
|
+
return listEl ? listEl.getAttribute("navi-current-id") || null : null;
|
|
29990
|
+
});
|
|
29991
|
+
useLayoutEffect(() => {
|
|
29992
|
+
const listEl = getListEl();
|
|
29993
|
+
if (!listEl) {
|
|
29994
|
+
return undefined;
|
|
29995
|
+
}
|
|
29996
|
+
// Sync in case list updated before our effect ran.
|
|
29997
|
+
setCurrentId(listEl.getAttribute("navi-current-id") || null);
|
|
29998
|
+
const onCurrentChange = e => {
|
|
29999
|
+
setCurrentId(e.detail.id || null);
|
|
30000
|
+
};
|
|
30001
|
+
listEl.addEventListener("navi_current_change", onCurrentChange);
|
|
30002
|
+
return () => {
|
|
30003
|
+
listEl.removeEventListener("navi_current_change", onCurrentChange);
|
|
30004
|
+
};
|
|
30005
|
+
}, [naviList]);
|
|
30006
|
+
const requestListNav = (e, goal) => {
|
|
30007
|
+
const listEl = getListEl();
|
|
30008
|
+
if (!listEl) {
|
|
30009
|
+
return;
|
|
30010
|
+
}
|
|
30011
|
+
e.preventDefault();
|
|
30012
|
+
e.stopPropagation();
|
|
30013
|
+
dispatchCustomEvent(listEl, "navi_request_nav", {
|
|
30014
|
+
event: e,
|
|
30015
|
+
goal
|
|
30016
|
+
});
|
|
30017
|
+
};
|
|
30018
|
+
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
30019
|
+
arrowdown: e => requestListNav(e, "down"),
|
|
30020
|
+
arrowup: e => requestListNav(e, "up"),
|
|
30021
|
+
home: e => requestListNav(e, "first"),
|
|
30022
|
+
end: e => requestListNav(e, "last"),
|
|
30023
|
+
enter: e => {
|
|
30024
|
+
const listEl = getListEl();
|
|
30025
|
+
if (!listEl) {
|
|
30026
|
+
return;
|
|
30027
|
+
}
|
|
30028
|
+
e.preventDefault();
|
|
30029
|
+
e.stopPropagation();
|
|
30030
|
+
dispatchCustomEvent(listEl, "navi_request_activate", {
|
|
30031
|
+
event: e
|
|
30032
|
+
});
|
|
30033
|
+
}
|
|
30034
|
+
});
|
|
30035
|
+
return jsx(Next, {
|
|
30036
|
+
role: "combobox",
|
|
30037
|
+
"aria-haspopup": "listbox",
|
|
30038
|
+
"aria-autocomplete": "list",
|
|
30039
|
+
"aria-controls": currentId ? `${currentId}_input` : undefined,
|
|
30040
|
+
"aria-activedescendant": currentId || undefined,
|
|
30041
|
+
autoComplete: "off",
|
|
30042
|
+
...props,
|
|
30043
|
+
onKeyDown: e => {
|
|
30044
|
+
onKeyDown?.(e);
|
|
30045
|
+
onKeyDownShortcuts(e);
|
|
30046
|
+
}
|
|
30047
|
+
});
|
|
30048
|
+
};
|
|
30049
|
+
|
|
29906
30050
|
installImportMetaCssBuild(import.meta);/**
|
|
29907
30051
|
* Input component for all textual input types.
|
|
29908
30052
|
*
|
|
@@ -30169,6 +30313,11 @@ const InputTextual = props => {
|
|
|
30169
30313
|
};
|
|
30170
30314
|
const InputTextualWithListResolver = props => {
|
|
30171
30315
|
const Next = useNextResolver();
|
|
30316
|
+
if (props["navi-list"]) {
|
|
30317
|
+
return jsx(InputWithList, {
|
|
30318
|
+
...props
|
|
30319
|
+
});
|
|
30320
|
+
}
|
|
30172
30321
|
if (props.suggestions) {
|
|
30173
30322
|
return jsx(InputTextualWithSuggestions, {
|
|
30174
30323
|
...props
|
|
@@ -30441,7 +30590,9 @@ const InputSearchUI = ({
|
|
|
30441
30590
|
const input = e.currentTarget;
|
|
30442
30591
|
const allowed = dispatchRequestInteraction(input, e);
|
|
30443
30592
|
if (allowed) {
|
|
30444
|
-
triggerStringAction("clear", e
|
|
30593
|
+
triggerStringAction("clear", e, {
|
|
30594
|
+
skipClose: true
|
|
30595
|
+
});
|
|
30445
30596
|
}
|
|
30446
30597
|
},
|
|
30447
30598
|
children: jsx(Icon, {
|
|
@@ -31351,23 +31502,76 @@ window.addEventListener("resize", () => {
|
|
|
31351
31502
|
|
|
31352
31503
|
/**
|
|
31353
31504
|
* Mirrors what browsers do when navigating to a page:
|
|
31354
|
-
* 1. Focus the first element with [navi-autofocus] inside the container
|
|
31505
|
+
* 1. Focus the first element with [navi-autofocus] (but not [navi-autofocus="fallback"]) inside the container
|
|
31355
31506
|
* 2. Fall back to the first focusable element
|
|
31507
|
+
* 3. Fall back to the first element with [navi-autofocus="fallback"]
|
|
31356
31508
|
* Does nothing if no candidate is found.
|
|
31357
31509
|
*/
|
|
31510
|
+
const markAutofocusRestoreOnClose = (containerEl) => {
|
|
31511
|
+
const focused = document.activeElement;
|
|
31512
|
+
if (
|
|
31513
|
+
focused &&
|
|
31514
|
+
containerEl.contains(focused) &&
|
|
31515
|
+
focused.getAttribute("navi-autofocus") === "fallback"
|
|
31516
|
+
) {
|
|
31517
|
+
containerEl.setAttribute("navi-autofocus-restore", "");
|
|
31518
|
+
} else {
|
|
31519
|
+
containerEl.removeAttribute("navi-autofocus-restore");
|
|
31520
|
+
}
|
|
31521
|
+
};
|
|
31522
|
+
|
|
31358
31523
|
const focusFirstAutofocusOrFocusable = (containerEl, debugFocus, e) => {
|
|
31359
|
-
let target
|
|
31524
|
+
let target;
|
|
31525
|
+
let reason;
|
|
31526
|
+
if (containerEl.hasAttribute("navi-autofocus-restore")) {
|
|
31527
|
+
containerEl.removeAttribute("navi-autofocus-restore");
|
|
31528
|
+
const naviAutoFocusFallback = containerEl.querySelector(
|
|
31529
|
+
"[navi-autofocus='fallback']",
|
|
31530
|
+
);
|
|
31531
|
+
if (naviAutoFocusFallback) {
|
|
31532
|
+
reason = "navi-autofocus fallback (restore)";
|
|
31533
|
+
target = naviAutoFocusFallback;
|
|
31534
|
+
}
|
|
31535
|
+
}
|
|
31536
|
+
if (!target) {
|
|
31537
|
+
const naviAutoFocus = containerEl.querySelector(
|
|
31538
|
+
"[navi-autofocus]:not([navi-autofocus='fallback'])",
|
|
31539
|
+
);
|
|
31540
|
+
if (naviAutoFocus) {
|
|
31541
|
+
reason = "navi-autofocus";
|
|
31542
|
+
target = naviAutoFocus;
|
|
31543
|
+
}
|
|
31544
|
+
}
|
|
31545
|
+
if (!target) {
|
|
31546
|
+
const focusable = findFocusable(containerEl, {
|
|
31547
|
+
exclude: (el) => el.getAttribute("navi-autofocus") === "fallback",
|
|
31548
|
+
});
|
|
31549
|
+
if (focusable) {
|
|
31550
|
+
reason = "first focusable element";
|
|
31551
|
+
target = focusable;
|
|
31552
|
+
}
|
|
31553
|
+
}
|
|
31360
31554
|
if (!target) {
|
|
31361
|
-
|
|
31555
|
+
const naviAutoFocusFallback = containerEl.querySelector(
|
|
31556
|
+
"[navi-autofocus='fallback']",
|
|
31557
|
+
);
|
|
31558
|
+
if (naviAutoFocusFallback) {
|
|
31559
|
+
reason = "navi-autofocus fallback";
|
|
31560
|
+
target = naviAutoFocusFallback;
|
|
31561
|
+
}
|
|
31362
31562
|
}
|
|
31363
31563
|
if (!target) {
|
|
31364
31564
|
return;
|
|
31365
31565
|
}
|
|
31366
31566
|
debugFocus(
|
|
31367
31567
|
e,
|
|
31368
|
-
`Moving focus to ${getElementSignature(target)}.focus({ preventScroll: true })`,
|
|
31568
|
+
`Moving focus to ${getElementSignature(target)}.focus({ preventScroll: true }) (reason: ${reason})`,
|
|
31369
31569
|
);
|
|
31370
31570
|
target.focus({ preventScroll: true });
|
|
31571
|
+
if (target.hasAttribute("navi-autofocus-select")) {
|
|
31572
|
+
target.select();
|
|
31573
|
+
target.scrollLeft = 0;
|
|
31574
|
+
}
|
|
31371
31575
|
};
|
|
31372
31576
|
|
|
31373
31577
|
const useCleanup = () => {
|
|
@@ -31435,6 +31639,7 @@ const Dialog = props => {
|
|
|
31435
31639
|
const close = e => {
|
|
31436
31640
|
debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> closeDialog`);
|
|
31437
31641
|
const dialogEl = ref.current;
|
|
31642
|
+
markAutofocusRestoreOnClose(dialogEl);
|
|
31438
31643
|
dialogEl.close();
|
|
31439
31644
|
cleanup();
|
|
31440
31645
|
openedRef.current = false;
|
|
@@ -31615,6 +31820,7 @@ const Popover = props => {
|
|
|
31615
31820
|
const close = e => {
|
|
31616
31821
|
debugPopup(e, `closePopover()`);
|
|
31617
31822
|
const popoverEl = ref.current;
|
|
31823
|
+
markAutofocusRestoreOnClose(popoverEl);
|
|
31618
31824
|
popoverEl.hidePopover();
|
|
31619
31825
|
cleanup();
|
|
31620
31826
|
openedRef.current = false;
|
|
@@ -33724,6 +33930,7 @@ const ListUI = props => {
|
|
|
33724
33930
|
const {
|
|
33725
33931
|
ref,
|
|
33726
33932
|
renderBudget = RENDER_BUDGET_DEFAULT,
|
|
33933
|
+
renderBudgetSkipCheck,
|
|
33727
33934
|
role,
|
|
33728
33935
|
fallback,
|
|
33729
33936
|
noMatchFallback,
|
|
@@ -33739,7 +33946,7 @@ const ListUI = props => {
|
|
|
33739
33946
|
searchText,
|
|
33740
33947
|
...rest
|
|
33741
33948
|
} = props;
|
|
33742
|
-
if (renderBudget < 30) {
|
|
33949
|
+
if (renderBudget < 30 && !renderBudgetSkipCheck) {
|
|
33743
33950
|
console.warn(`List: renderBudget=${renderBudget} is too low. A renderBudget below 30 is not supported: on large screens or when the list grows, items outside the window would appear as blank space instead of rendered content. Use a value of at least 30, or omit the prop to use the default (${RENDER_BUDGET_DEFAULT}).`);
|
|
33744
33951
|
}
|
|
33745
33952
|
|
|
@@ -34320,7 +34527,7 @@ const NoMatchFallback = ({
|
|
|
34320
34527
|
const noneMatch = searchText && visibleItemCount > 0 && matchCount === 0;
|
|
34321
34528
|
const showMatchFallback = allHidden || noneMatch;
|
|
34322
34529
|
if (noMatchFallback === undefined) {
|
|
34323
|
-
noMatchFallback = allHidden ? "
|
|
34530
|
+
noMatchFallback = allHidden ? naviI18n("list.no_match") : naviI18n("list.no_match_rest_shown");
|
|
34324
34531
|
}
|
|
34325
34532
|
return jsx(ListItem, {
|
|
34326
34533
|
role: "presentation",
|
|
@@ -34337,7 +34544,7 @@ const Fallback = ({
|
|
|
34337
34544
|
const itemCount = tracker.countSignal.value;
|
|
34338
34545
|
const showFallback = itemCount === 0;
|
|
34339
34546
|
if (fallback === undefined) {
|
|
34340
|
-
fallback = "
|
|
34547
|
+
fallback = naviI18n("list.empty");
|
|
34341
34548
|
}
|
|
34342
34549
|
return jsx(ListItem, {
|
|
34343
34550
|
role: "presentation",
|
|
@@ -34508,6 +34715,7 @@ const ListItemReal = props => {
|
|
|
34508
34715
|
id: id,
|
|
34509
34716
|
"navi-list-item-real": "",
|
|
34510
34717
|
...rest,
|
|
34718
|
+
index: undefined,
|
|
34511
34719
|
hidden: hidden,
|
|
34512
34720
|
ref: ref,
|
|
34513
34721
|
children: children
|
|
@@ -34819,6 +35027,75 @@ const SelectableList = props => {
|
|
|
34819
35027
|
// Up/Down navigate between list items only (the visually-hidden real inputs).
|
|
34820
35028
|
ySelector: "[navi-selectable-real-input]"
|
|
34821
35029
|
});
|
|
35030
|
+
|
|
35031
|
+
// "Current item" tracking — the item that an external controller (e.g. an
|
|
35032
|
+
// <input navi-list>) navigates from. Defaults to the first selected item,
|
|
35033
|
+
// else the first navigable item. Updated when:
|
|
35034
|
+
// - an item's real input gains focus (via Tab, click, etc.)
|
|
35035
|
+
// - the controller dispatches navi_request_list_nav
|
|
35036
|
+
// The current id is announced via navi_list_current_change (bubbling) so a
|
|
35037
|
+
// connected input can update its aria-controls / aria-activedescendant.
|
|
35038
|
+
const currentIdRef = useRef(null);
|
|
35039
|
+
const setCurrentId = (id, event) => {
|
|
35040
|
+
const previousId = currentIdRef.current;
|
|
35041
|
+
if (previousId === id) {
|
|
35042
|
+
return;
|
|
35043
|
+
}
|
|
35044
|
+
currentIdRef.current = id;
|
|
35045
|
+
const listEl = ref.current;
|
|
35046
|
+
if (!listEl) {
|
|
35047
|
+
return;
|
|
35048
|
+
}
|
|
35049
|
+
if (id) {
|
|
35050
|
+
listEl.setAttribute("navi-current-id", id);
|
|
35051
|
+
} else {
|
|
35052
|
+
listEl.removeAttribute("navi-current-id");
|
|
35053
|
+
}
|
|
35054
|
+
dispatchPublicCustomEvent(listEl, "navi_current_change", {
|
|
35055
|
+
event,
|
|
35056
|
+
id,
|
|
35057
|
+
realInputId: id ? `${id}_input` : null
|
|
35058
|
+
});
|
|
35059
|
+
};
|
|
35060
|
+
const getNavigableElements = () => {
|
|
35061
|
+
const listEl = ref.current;
|
|
35062
|
+
if (!listEl) {
|
|
35063
|
+
return [];
|
|
35064
|
+
}
|
|
35065
|
+
const itemEls = Array.from(listEl.querySelectorAll("[navi-list-item-real]"));
|
|
35066
|
+
const navigableEls = [];
|
|
35067
|
+
for (const itemEl of itemEls) {
|
|
35068
|
+
if (itemEl.hidden) {
|
|
35069
|
+
continue;
|
|
35070
|
+
}
|
|
35071
|
+
const realInput = itemEl.querySelector("[navi-selectable-real-input]");
|
|
35072
|
+
if (!realInput || realInput.disabled) {
|
|
35073
|
+
continue;
|
|
35074
|
+
}
|
|
35075
|
+
navigableEls.push(itemEl);
|
|
35076
|
+
}
|
|
35077
|
+
return navigableEls;
|
|
35078
|
+
};
|
|
35079
|
+
// On mount: set the initial current item to the first selected, else the first navigable.
|
|
35080
|
+
// After that, focusin events on the list keep currentIdRef up to date.
|
|
35081
|
+
useLayoutEffect(() => {
|
|
35082
|
+
const navigableEls = getNavigableElements();
|
|
35083
|
+
if (navigableEls.length === 0) {
|
|
35084
|
+
return;
|
|
35085
|
+
}
|
|
35086
|
+
let initialEl;
|
|
35087
|
+
for (const el of navigableEls) {
|
|
35088
|
+
const realInput = el.querySelector("[navi-selectable-real-input]");
|
|
35089
|
+
if (realInput && realInput.checked) {
|
|
35090
|
+
initialEl = el;
|
|
35091
|
+
break;
|
|
35092
|
+
}
|
|
35093
|
+
}
|
|
35094
|
+
if (!initialEl) {
|
|
35095
|
+
initialEl = navigableEls[0];
|
|
35096
|
+
}
|
|
35097
|
+
setCurrentId(initialEl.id);
|
|
35098
|
+
}, []);
|
|
34822
35099
|
const listVnode = jsx(List, {
|
|
34823
35100
|
as: "fieldset",
|
|
34824
35101
|
"navi-has-selected-background": selectedIndicator === "backgroundColor" ? "" : undefined,
|
|
@@ -34826,7 +35103,20 @@ const SelectableList = props => {
|
|
|
34826
35103
|
...remainingProps,
|
|
34827
35104
|
name: undefined,
|
|
34828
35105
|
selectedIndicator: undefined,
|
|
34829
|
-
multiple: undefined
|
|
35106
|
+
multiple: undefined
|
|
35107
|
+
// Track focus inside the list: whichever item gets focus becomes current.
|
|
35108
|
+
,
|
|
35109
|
+
|
|
35110
|
+
onFocusIn: e => {
|
|
35111
|
+
const realInput = e.target.closest("[navi-selectable-real-input]");
|
|
35112
|
+
if (!realInput) {
|
|
35113
|
+
return;
|
|
35114
|
+
}
|
|
35115
|
+
const itemEl = realInput.closest("[navi-list-item-real]");
|
|
35116
|
+
if (itemEl && itemEl.id) {
|
|
35117
|
+
setCurrentId(itemEl.id, e);
|
|
35118
|
+
}
|
|
35119
|
+
},
|
|
34830
35120
|
onnavi_request_select: e => {
|
|
34831
35121
|
const {
|
|
34832
35122
|
id
|
|
@@ -34875,6 +35165,70 @@ const SelectableList = props => {
|
|
|
34875
35165
|
});
|
|
34876
35166
|
}
|
|
34877
35167
|
},
|
|
35168
|
+
onnavi_request_nav: e => {
|
|
35169
|
+
const {
|
|
35170
|
+
goal
|
|
35171
|
+
} = e.detail;
|
|
35172
|
+
const navigableEls = getNavigableElements();
|
|
35173
|
+
if (navigableEls.length === 0) {
|
|
35174
|
+
return;
|
|
35175
|
+
}
|
|
35176
|
+
const currentId = currentIdRef.current;
|
|
35177
|
+
let currentIndex = -1;
|
|
35178
|
+
if (currentId) {
|
|
35179
|
+
currentIndex = navigableEls.findIndex(el => el.id === currentId);
|
|
35180
|
+
}
|
|
35181
|
+
let targetEl;
|
|
35182
|
+
if (goal === "first") {
|
|
35183
|
+
targetEl = navigableEls[0];
|
|
35184
|
+
} else if (goal === "last") {
|
|
35185
|
+
targetEl = navigableEls[navigableEls.length - 1];
|
|
35186
|
+
} else if (goal === "down") {
|
|
35187
|
+
if (currentIndex === -1) {
|
|
35188
|
+
targetEl = navigableEls[0];
|
|
35189
|
+
} else if (currentIndex < navigableEls.length - 1) {
|
|
35190
|
+
targetEl = navigableEls[currentIndex + 1];
|
|
35191
|
+
} else {
|
|
35192
|
+
targetEl = navigableEls[navigableEls.length - 1];
|
|
35193
|
+
}
|
|
35194
|
+
} else if (goal === "up") {
|
|
35195
|
+
if (currentIndex === -1) {
|
|
35196
|
+
targetEl = navigableEls[0];
|
|
35197
|
+
} else if (currentIndex > 0) {
|
|
35198
|
+
targetEl = navigableEls[currentIndex - 1];
|
|
35199
|
+
} else {
|
|
35200
|
+
targetEl = navigableEls[0];
|
|
35201
|
+
}
|
|
35202
|
+
}
|
|
35203
|
+
if (!targetEl) {
|
|
35204
|
+
return;
|
|
35205
|
+
}
|
|
35206
|
+
setCurrentId(targetEl.id, e);
|
|
35207
|
+
dispatchCustomEvent(ref.current, "navi_request_scroll", {
|
|
35208
|
+
event: e,
|
|
35209
|
+
id: targetEl.id
|
|
35210
|
+
});
|
|
35211
|
+
},
|
|
35212
|
+
onnavi_request_activate: e => {
|
|
35213
|
+
const currentId = currentIdRef.current;
|
|
35214
|
+
if (!currentId) {
|
|
35215
|
+
return;
|
|
35216
|
+
}
|
|
35217
|
+
if (multiple) {
|
|
35218
|
+
const inputId = `${currentId}_input`;
|
|
35219
|
+
const childController = uiGroupStateController.findChildById(inputId);
|
|
35220
|
+
const isSelected = childController && childController.uiState;
|
|
35221
|
+
dispatchCustomEvent(ref.current, isSelected ? "navi_request_unselect" : "navi_request_select", {
|
|
35222
|
+
event: e,
|
|
35223
|
+
id: currentId
|
|
35224
|
+
});
|
|
35225
|
+
return;
|
|
35226
|
+
}
|
|
35227
|
+
dispatchCustomEvent(ref.current, "navi_request_select", {
|
|
35228
|
+
event: e,
|
|
35229
|
+
id: currentId
|
|
35230
|
+
});
|
|
35231
|
+
},
|
|
34878
35232
|
children: jsx(ControlgroupChildrenWrapper, {
|
|
34879
35233
|
...childrenWrapperProps,
|
|
34880
35234
|
children: props.children
|
|
@@ -34893,6 +35247,7 @@ const Selectable = props => {
|
|
|
34893
35247
|
highlight,
|
|
34894
35248
|
hidden,
|
|
34895
35249
|
filtered,
|
|
35250
|
+
matchScore,
|
|
34896
35251
|
defaultSelected,
|
|
34897
35252
|
selected,
|
|
34898
35253
|
pointed,
|
|
@@ -34903,6 +35258,7 @@ const Selectable = props => {
|
|
|
34903
35258
|
const inputRef = useRef();
|
|
34904
35259
|
const inputType = multiple ? "checkbox" : "radio";
|
|
34905
35260
|
const inputId = `${id}_input`;
|
|
35261
|
+
inputRef.nullExpected = true; // virtualization
|
|
34906
35262
|
const [checkableProps, remainingProps, ChildrenContextWrapper] = useCheckableProps({
|
|
34907
35263
|
readOnlyMessage: naviI18n(`constraints.readonly.option`, props),
|
|
34908
35264
|
...rest,
|
|
@@ -34944,6 +35300,7 @@ const Selectable = props => {
|
|
|
34944
35300
|
highlight: highlight,
|
|
34945
35301
|
filtered: filtered,
|
|
34946
35302
|
hidden: hidden,
|
|
35303
|
+
matchScore: matchScore,
|
|
34947
35304
|
pseudoClasses: SELECTABLE_PSEUDO_CLASSES,
|
|
34948
35305
|
basePseudoState: {
|
|
34949
35306
|
":-navi-selected": checked,
|