@jsenv/navi 0.27.2 → 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 +448 -85
- package/dist/jsenv_navi.js.map +50 -40
- 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
|
+
}
|
|
31360
31545
|
if (!target) {
|
|
31361
|
-
|
|
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
|
+
}
|
|
31554
|
+
if (!target) {
|
|
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;
|
|
@@ -32096,6 +32302,9 @@ const PickerCustom = props => {
|
|
|
32096
32302
|
// (like enter that would try to request action of closest form otherwise for instance)
|
|
32097
32303
|
if (e.key === "Enter") {
|
|
32098
32304
|
e.stopPropagation();
|
|
32305
|
+
// preventDefault prevents the browser from synthesising a click on the
|
|
32306
|
+
// picker button when focus moves to it synchronously during enterEffect
|
|
32307
|
+
e.preventDefault();
|
|
32099
32308
|
}
|
|
32100
32309
|
}
|
|
32101
32310
|
});
|
|
@@ -32132,6 +32341,7 @@ const PickerContentInsidePopover = props => {
|
|
|
32132
32341
|
popoverMode = "nearby",
|
|
32133
32342
|
popoverSpacing = popoverMode === "nearby" ? 5 : 0,
|
|
32134
32343
|
viewportSpacing = 10,
|
|
32344
|
+
closeOnFocusOut = false,
|
|
32135
32345
|
...rest
|
|
32136
32346
|
} = props;
|
|
32137
32347
|
return jsx(Next, {
|
|
@@ -32139,6 +32349,9 @@ const PickerContentInsidePopover = props => {
|
|
|
32139
32349
|
"navi-popover-mode": popoverMode,
|
|
32140
32350
|
...rest,
|
|
32141
32351
|
onFocusOut: e => {
|
|
32352
|
+
if (!closeOnFocusOut) {
|
|
32353
|
+
return;
|
|
32354
|
+
}
|
|
32142
32355
|
// Close when focus leaves the select entirely (not just moving between internal elements).
|
|
32143
32356
|
// relatedTarget is the element receiving focus; if it's inside the select or the popover, keep open.
|
|
32144
32357
|
const relatedTarget = e.relatedTarget;
|
|
@@ -33717,6 +33930,7 @@ const ListUI = props => {
|
|
|
33717
33930
|
const {
|
|
33718
33931
|
ref,
|
|
33719
33932
|
renderBudget = RENDER_BUDGET_DEFAULT,
|
|
33933
|
+
renderBudgetSkipCheck,
|
|
33720
33934
|
role,
|
|
33721
33935
|
fallback,
|
|
33722
33936
|
noMatchFallback,
|
|
@@ -33732,7 +33946,7 @@ const ListUI = props => {
|
|
|
33732
33946
|
searchText,
|
|
33733
33947
|
...rest
|
|
33734
33948
|
} = props;
|
|
33735
|
-
if (renderBudget < 30) {
|
|
33949
|
+
if (renderBudget < 30 && !renderBudgetSkipCheck) {
|
|
33736
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}).`);
|
|
33737
33951
|
}
|
|
33738
33952
|
|
|
@@ -34313,7 +34527,7 @@ const NoMatchFallback = ({
|
|
|
34313
34527
|
const noneMatch = searchText && visibleItemCount > 0 && matchCount === 0;
|
|
34314
34528
|
const showMatchFallback = allHidden || noneMatch;
|
|
34315
34529
|
if (noMatchFallback === undefined) {
|
|
34316
|
-
noMatchFallback = allHidden ? "
|
|
34530
|
+
noMatchFallback = allHidden ? naviI18n("list.no_match") : naviI18n("list.no_match_rest_shown");
|
|
34317
34531
|
}
|
|
34318
34532
|
return jsx(ListItem, {
|
|
34319
34533
|
role: "presentation",
|
|
@@ -34330,7 +34544,7 @@ const Fallback = ({
|
|
|
34330
34544
|
const itemCount = tracker.countSignal.value;
|
|
34331
34545
|
const showFallback = itemCount === 0;
|
|
34332
34546
|
if (fallback === undefined) {
|
|
34333
|
-
fallback = "
|
|
34547
|
+
fallback = naviI18n("list.empty");
|
|
34334
34548
|
}
|
|
34335
34549
|
return jsx(ListItem, {
|
|
34336
34550
|
role: "presentation",
|
|
@@ -34501,6 +34715,7 @@ const ListItemReal = props => {
|
|
|
34501
34715
|
id: id,
|
|
34502
34716
|
"navi-list-item-real": "",
|
|
34503
34717
|
...rest,
|
|
34718
|
+
index: undefined,
|
|
34504
34719
|
hidden: hidden,
|
|
34505
34720
|
ref: ref,
|
|
34506
34721
|
children: children
|
|
@@ -34642,6 +34857,7 @@ const css$l = /* css */`
|
|
|
34642
34857
|
--list-outline-color: var(--navi-focus-outline-color);
|
|
34643
34858
|
--list-item-outline-color: var(--navi-focus-outline-color);
|
|
34644
34859
|
--list-item-outline-width: 2px;
|
|
34860
|
+
--list-item-outline-offset: calc(-1 * var(--list-item-outline-width));
|
|
34645
34861
|
/* Hover (mouse) */
|
|
34646
34862
|
--list-item-color-hover: var(--list-item-color);
|
|
34647
34863
|
--list-item-background-color-hover: light-dark(#f5f5f5, #2a2a2a);
|
|
@@ -34694,12 +34910,10 @@ const css$l = /* css */`
|
|
|
34694
34910
|
}
|
|
34695
34911
|
|
|
34696
34912
|
.navi_list_item {
|
|
34697
|
-
--x-list-item-border-color: var(--x-list-item-background-color);
|
|
34698
|
-
|
|
34699
34913
|
position: relative;
|
|
34700
|
-
|
|
34701
|
-
|
|
34702
|
-
|
|
34914
|
+
outline-width: var(--list-item-outline-width);
|
|
34915
|
+
outline-color: var(--list-item-outline-color);
|
|
34916
|
+
outline-offset: var(--list-item-outline-offset);
|
|
34703
34917
|
|
|
34704
34918
|
&[data-interactive] {
|
|
34705
34919
|
cursor: pointer;
|
|
@@ -34722,7 +34936,7 @@ const css$l = /* css */`
|
|
|
34722
34936
|
--x-list-item-background-color: var(
|
|
34723
34937
|
--list-item-background-color-keyboard-pointed
|
|
34724
34938
|
);
|
|
34725
|
-
|
|
34939
|
+
outline-style: solid;
|
|
34726
34940
|
|
|
34727
34941
|
/* Selected must win over keyboard-pointed */
|
|
34728
34942
|
&[data-selected] {
|
|
@@ -34813,6 +35027,75 @@ const SelectableList = props => {
|
|
|
34813
35027
|
// Up/Down navigate between list items only (the visually-hidden real inputs).
|
|
34814
35028
|
ySelector: "[navi-selectable-real-input]"
|
|
34815
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
|
+
}, []);
|
|
34816
35099
|
const listVnode = jsx(List, {
|
|
34817
35100
|
as: "fieldset",
|
|
34818
35101
|
"navi-has-selected-background": selectedIndicator === "backgroundColor" ? "" : undefined,
|
|
@@ -34820,7 +35103,20 @@ const SelectableList = props => {
|
|
|
34820
35103
|
...remainingProps,
|
|
34821
35104
|
name: undefined,
|
|
34822
35105
|
selectedIndicator: undefined,
|
|
34823
|
-
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
|
+
},
|
|
34824
35120
|
onnavi_request_select: e => {
|
|
34825
35121
|
const {
|
|
34826
35122
|
id
|
|
@@ -34869,6 +35165,70 @@ const SelectableList = props => {
|
|
|
34869
35165
|
});
|
|
34870
35166
|
}
|
|
34871
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
|
+
},
|
|
34872
35232
|
children: jsx(ControlgroupChildrenWrapper, {
|
|
34873
35233
|
...childrenWrapperProps,
|
|
34874
35234
|
children: props.children
|
|
@@ -34887,6 +35247,7 @@ const Selectable = props => {
|
|
|
34887
35247
|
highlight,
|
|
34888
35248
|
hidden,
|
|
34889
35249
|
filtered,
|
|
35250
|
+
matchScore,
|
|
34890
35251
|
defaultSelected,
|
|
34891
35252
|
selected,
|
|
34892
35253
|
pointed,
|
|
@@ -34897,6 +35258,7 @@ const Selectable = props => {
|
|
|
34897
35258
|
const inputRef = useRef();
|
|
34898
35259
|
const inputType = multiple ? "checkbox" : "radio";
|
|
34899
35260
|
const inputId = `${id}_input`;
|
|
35261
|
+
inputRef.nullExpected = true; // virtualization
|
|
34900
35262
|
const [checkableProps, remainingProps, ChildrenContextWrapper] = useCheckableProps({
|
|
34901
35263
|
readOnlyMessage: naviI18n(`constraints.readonly.option`, props),
|
|
34902
35264
|
...rest,
|
|
@@ -34938,6 +35300,7 @@ const Selectable = props => {
|
|
|
34938
35300
|
highlight: highlight,
|
|
34939
35301
|
filtered: filtered,
|
|
34940
35302
|
hidden: hidden,
|
|
35303
|
+
matchScore: matchScore,
|
|
34941
35304
|
pseudoClasses: SELECTABLE_PSEUDO_CLASSES,
|
|
34942
35305
|
basePseudoState: {
|
|
34943
35306
|
":-navi-selected": checked,
|