reka-ui
Version:
Vue port for Radix UI Primitives.
70 lines (68 loc) • 2.98 kB
JavaScript
import { getActiveElement } from "./getActiveElement.js";
import { refAutoReset } from "@vueuse/shared";
//#region src/shared/useTypeahead.ts
function useTypeahead(callback) {
const search = refAutoReset("", 1e3);
const handleTypeaheadSearch = (key, items) => {
search.value = search.value + key;
if (callback) callback(key);
else {
const currentItem = getActiveElement();
const itemsWithTextValue = items.map((item) => ({
...item,
textValue: item.value?.textValue ?? item.ref.textContent?.trim() ?? ""
}));
const currentMatch = itemsWithTextValue.find((item) => item.ref === currentItem);
const values = itemsWithTextValue.map((item) => item.textValue);
const nextMatch = getNextMatch(values, search.value, currentMatch?.textValue);
const newItem = itemsWithTextValue.find((item) => item.textValue === nextMatch);
if (newItem) newItem.ref.focus();
return newItem?.ref;
}
};
const resetTypeahead = () => {
search.value = "";
};
return {
search,
handleTypeaheadSearch,
resetTypeahead
};
}
/**
* Wraps an array around itself at a given start index
* Example: `wrapArray(['a', 'b', 'c', 'd'], 2) === ['c', 'd', 'a', 'b']`
*/
function wrapArray(array, startIndex) {
return array.map((_, index) => array[(startIndex + index) % array.length]);
}
/**
* This is the "meat" of the typeahead matching logic. It takes in all the values,
* the search and the current match, and returns the next match (or `undefined`).
*
* We normalize the search because if a user has repeatedly pressed a character,
* we want the exact same behavior as if we only had that one character
* (ie. cycle through options starting with that character)
*
* We also reorder the values by wrapping the array around the current match.
* This is so we always look forward from the current match, and picking the first
* match will always be the correct one.
*
* Finally, if the normalized search is exactly one character, we exclude the
* current match from the values because otherwise it would be the first to match always
* and focus would never move. This is as opposed to the regular case, where we
* don't want focus to move if the current match still matches.
*/
function getNextMatch(values, search, currentMatch) {
const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]);
const normalizedSearch = isRepeated ? search[0] : search;
const currentMatchIndex = currentMatch ? values.indexOf(currentMatch) : -1;
let wrappedValues = wrapArray(values, Math.max(currentMatchIndex, 0));
const excludeCurrentMatch = normalizedSearch.length === 1;
if (excludeCurrentMatch) wrappedValues = wrappedValues.filter((v) => v !== currentMatch);
const nextMatch = wrappedValues.find((value) => value.toLowerCase().startsWith(normalizedSearch.toLowerCase()));
return nextMatch !== currentMatch ? nextMatch : void 0;
}
//#endregion
export { getNextMatch, useTypeahead, wrapArray };
//# sourceMappingURL=useTypeahead.js.map