UNPKG

@melt-ui/svelte

Version:
66 lines (65 loc) 2.58 kB
import { get, writable } from 'svelte/store'; import { wrapArray } from './array.js'; import { debounce } from './debounce.js'; import { isHTMLElement } from './is.js'; import { handleRovingFocus } from './rovingFocus.js'; import { withGet } from './withGet.js'; /** * Keys to ignore for typeahead so we aren't matching things * like `Shift menu item` or `Control center` or `Alt menu` when * a user presses those keys. */ const ignoredKeys = new Set(['Shift', 'Control', 'Alt', 'Meta', 'CapsLock', 'NumLock']); /** * Default options for the typeahead search. * We default to roving focus when a match is found, but * you can override this with the `onMatch` option. */ const defaults = { onMatch: handleRovingFocus, getCurrentItem: () => document.activeElement, }; export function createTypeaheadSearch(args = {}) { const withDefaults = { ...defaults, ...args }; const typed = withGet(writable([])); const resetTyped = debounce(() => { typed.update(() => []); }); const handleTypeaheadSearch = (key, items) => { if (ignoredKeys.has(key)) return; const currentItem = withDefaults.getCurrentItem(); const $typed = get(typed); if (!Array.isArray($typed)) { return; } $typed.push(key.toLowerCase()); typed.set($typed); const candidateItems = items.filter((item) => { if (item.getAttribute('disabled') === 'true' || item.getAttribute('aria-disabled') === 'true' || item.hasAttribute('data-disabled')) { return false; } return true; }); const isRepeated = $typed.length > 1 && $typed.every((char) => char === $typed[0]); const normalizeSearch = isRepeated ? $typed[0] : $typed.join(''); const currentItemIndex = isHTMLElement(currentItem) ? candidateItems.indexOf(currentItem) : -1; let wrappedItems = wrapArray(candidateItems, Math.max(currentItemIndex, 0)); const excludeCurrentItem = normalizeSearch.length === 1; if (excludeCurrentItem) { wrappedItems = wrappedItems.filter((v) => v !== currentItem); } const nextItem = wrappedItems.find((item) => item?.innerText && item.innerText.toLowerCase().startsWith(normalizeSearch.toLowerCase())); if (isHTMLElement(nextItem) && nextItem !== currentItem) { withDefaults.onMatch(nextItem); } resetTyped(); }; return { typed, resetTyped, handleTypeaheadSearch, }; }