@versini/ui-menu 6.1.1 → 6.2.0
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/index.js +73 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-menu v6.
|
|
2
|
+
@versini/ui-menu v6.2.0
|
|
3
3
|
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -43,6 +43,34 @@ function getNextEnabledIndex(items, currentIndex, direction) {
|
|
|
43
43
|
}
|
|
44
44
|
/* v8 ignore start - all items disabled edge case */ return currentIndex;
|
|
45
45
|
/* v8 ignore stop */ }
|
|
46
|
+
const TYPEAHEAD_TIMEOUT_MS = 500;
|
|
47
|
+
function getTypeaheadMatchIndex(items, searchString, startIndex) {
|
|
48
|
+
const count = items.length;
|
|
49
|
+
const normalized = searchString.toLowerCase();
|
|
50
|
+
// When the same single character is repeated (e.g. "aaa"), cycle
|
|
51
|
+
// through items starting with that character instead of trying to
|
|
52
|
+
// match the full repeated string.
|
|
53
|
+
const isRepeatedChar = normalized.length > 1 && [
|
|
54
|
+
...normalized
|
|
55
|
+
].every((c)=>c === normalized[0]);
|
|
56
|
+
const matchStr = isRepeatedChar ? normalized[0] : normalized;
|
|
57
|
+
// For a single character (including repeated single-char), start
|
|
58
|
+
// searching from the item *after* the current one so we always
|
|
59
|
+
// advance to the next match. For multi-character strings, start
|
|
60
|
+
// from the current item so the user can refine the match in-place.
|
|
61
|
+
const offset = normalized.length === 1 || isRepeatedChar ? 1 : 0;
|
|
62
|
+
for(let i = 0; i < count; i++){
|
|
63
|
+
const index = (startIndex + offset + i) % count;
|
|
64
|
+
if (items[index].disabled) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const text = /* v8 ignore next -- textContent is always a string in DOM */ (items[index].element.textContent ?? "").trim().toLowerCase();
|
|
68
|
+
if (text.startsWith(matchStr)) {
|
|
69
|
+
return index;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return -1;
|
|
73
|
+
}
|
|
46
74
|
/* v8 ignore start - Home/End handlers with disabled item edge cases */ function getFirstEnabledIndex(items) {
|
|
47
75
|
for(let i = 0; i < items.length; i++){
|
|
48
76
|
if (!items[i].disabled) {
|
|
@@ -60,6 +88,27 @@ function getLastEnabledIndex(items) {
|
|
|
60
88
|
return -1;
|
|
61
89
|
}
|
|
62
90
|
/* v8 ignore stop */ function useMenuKeyboard({ menuRef, isOpen, activeIndex, setActiveIndex, getItems, onClose, isSubMenu = false, onOpenSubMenu, onCloseToParent }) {
|
|
91
|
+
const searchBufferRef = useRef("");
|
|
92
|
+
const searchTimeoutRef = useRef(undefined);
|
|
93
|
+
// Clear the typeahead buffer when the menu closes.
|
|
94
|
+
useEffect(()=>{
|
|
95
|
+
if (!isOpen) {
|
|
96
|
+
searchBufferRef.current = "";
|
|
97
|
+
if (searchTimeoutRef.current) {
|
|
98
|
+
clearTimeout(searchTimeoutRef.current);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, [
|
|
102
|
+
isOpen
|
|
103
|
+
]);
|
|
104
|
+
// Clean up timeout on unmount.
|
|
105
|
+
useEffect(()=>{
|
|
106
|
+
return ()=>{
|
|
107
|
+
if (searchTimeoutRef.current) {
|
|
108
|
+
clearTimeout(searchTimeoutRef.current);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}, []);
|
|
63
112
|
/* v8 ignore start - focusItem bounds guard */ const focusItem = useCallback((index)=>{
|
|
64
113
|
const items = getItems();
|
|
65
114
|
if (index >= 0 && index < items.length) {
|
|
@@ -77,7 +126,7 @@ function getLastEnabledIndex(items) {
|
|
|
77
126
|
/* v8 ignore start - items always exist when menu is open */ if (items.length === 0) {
|
|
78
127
|
return;
|
|
79
128
|
}
|
|
80
|
-
/* v8 ignore stop */ /* v8 ignore start - switch
|
|
129
|
+
/* v8 ignore stop */ /* v8 ignore start - switch internal branch */ switch(event.key){
|
|
81
130
|
/* v8 ignore stop */ case "ArrowDown":
|
|
82
131
|
{
|
|
83
132
|
event.preventDefault();
|
|
@@ -150,6 +199,28 @@ function getLastEnabledIndex(items) {
|
|
|
150
199
|
onClose();
|
|
151
200
|
break;
|
|
152
201
|
}
|
|
202
|
+
/* v8 ignore stop */ default:
|
|
203
|
+
{
|
|
204
|
+
// Typeahead: printable single characters (ignore modifier-only
|
|
205
|
+
// or functional keys such as Shift, Control, Alt, Meta, etc.).
|
|
206
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
// Reset the debounce timer.
|
|
209
|
+
if (searchTimeoutRef.current) {
|
|
210
|
+
clearTimeout(searchTimeoutRef.current);
|
|
211
|
+
}
|
|
212
|
+
searchBufferRef.current += event.key;
|
|
213
|
+
const matchIndex = getTypeaheadMatchIndex(items, searchBufferRef.current, /* v8 ignore next */ activeIndex >= 0 ? activeIndex : 0);
|
|
214
|
+
if (matchIndex >= 0) {
|
|
215
|
+
setActiveIndex(matchIndex);
|
|
216
|
+
focusItem(matchIndex);
|
|
217
|
+
}
|
|
218
|
+
searchTimeoutRef.current = setTimeout(()=>{
|
|
219
|
+
searchBufferRef.current = "";
|
|
220
|
+
}, TYPEAHEAD_TIMEOUT_MS);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
153
224
|
}
|
|
154
225
|
};
|
|
155
226
|
const menuElement = menuRef.current;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-menu",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"sideEffects": [
|
|
50
50
|
"**/*.css"
|
|
51
51
|
],
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "97a8939e0f65fe7c37b265d10a8956f6b6df42d9"
|
|
53
53
|
}
|