solid-suggest
Version:
Headless search suggestion dropdown UI library for SolidJS
141 lines (138 loc) • 4.74 kB
JavaScript
import { template, addEventListener, insert, memo, effect, setAttribute } from 'solid-js/web';
import { createSignal, createMemo, createEffect } from 'solid-js';
var _tmpl$ = /*#__PURE__*/template(`<div class=s-sug-container role=combobox aria-haspopup=listbox><input type=search class=s-sug-search aria-autocomplete=list>`),
_tmpl$2 = /*#__PURE__*/template(`<ul class=s-sug-suggestions role=listbox>`),
_tmpl$3 = /*#__PURE__*/template(`<li class=s-sug-suggestion role=option>`);
function Suggest(props) {
// Suggestions to be rendered
const [suggestions, setSuggestions] = createSignal([]);
const numSuggestions = createMemo(() => suggestions().length);
// Basic signals for component state
const [query, setQuery] = createSignal('');
const [staged, setStaged] = createSignal(null);
// Internal signal for user input, separate from query to handle optional debounce
const [debouncedQuery, setDebouncedQuery] = createSignal('');
// Just a variable to store debounce timeout, if applicable
let debounceTimeout = null;
// Effect to update suggestions when debouncedQuery changes
createEffect(() => {
const result = props.onQuery(debouncedQuery());
if (result instanceof Promise) {
result.then(setSuggestions);
} else {
setSuggestions(result);
}
});
function handleInput(e) {
const value = e.currentTarget.value;
setQuery(value);
if (typeof props.debounceMs === 'number' && props.debounceMs > 0) {
if (debounceTimeout) clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
setDebouncedQuery(value);
}, props.debounceMs);
} else {
setDebouncedQuery(value);
}
}
function stageSuggestion(index) {
setStaged(index);
}
function stageNextSuggestion() {
setStaged(s => {
if (suggestions().length === 0) return null;
const current = s ?? -1;
return (current + 1) % numSuggestions();
});
}
function stagePrevSuggestion() {
setStaged(s => {
if (numSuggestions() === 0) return null;
const current = s ?? 0;
return (current - 1 + numSuggestions()) % numSuggestions();
});
}
function selectStagedSuggestion() {
const index = staged();
if (index !== null) {
props.onSelect(suggestions()[index]);
reset();
}
}
function reset() {
setQuery('');
setDebouncedQuery('');
setStaged(null);
if (debounceTimeout) clearTimeout(debounceTimeout);
}
// Handles special input for suggestion behavior
function handleKeyDown(e) {
const keyInputReversed = props.reverseKeyInput ?? false;
if (e.key === 'ArrowDown') {
e.preventDefault();
if (keyInputReversed) {
stagePrevSuggestion();
} else {
stageNextSuggestion();
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (keyInputReversed) {
stageNextSuggestion();
} else {
stagePrevSuggestion();
}
} else if (e.key === 'Enter') {
e.preventDefault();
selectStagedSuggestion();
} else if (e.key === 'Escape') {
e.preventDefault();
reset();
}
// else -> propagates up to allow input
}
return (() => {
var _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
addEventListener(_el$2, "keydown", handleKeyDown);
addEventListener(_el$2, "input", handleInput);
insert(_el$, (() => {
var _c$ = memo(() => numSuggestions() > 0);
return () => _c$() && (() => {
var _el$3 = _tmpl$2();
insert(_el$3, () => suggestions().map((s, i) => (() => {
var _el$4 = _tmpl$3();
addEventListener(_el$4, "click", selectStagedSuggestion);
addEventListener(_el$4, "mouseenter", () => stageSuggestion(i));
insert(_el$4, () => props.renderSuggestion(s));
effect(_p$ => {
var _v$3 = staged() === i,
_v$4 = staged() === i ? 'true' : 'false';
_v$3 !== _p$.e && setAttribute(_el$4, "data-staged", _p$.e = _v$3);
_v$4 !== _p$.t && setAttribute(_el$4, "aria-selected", _p$.t = _v$4);
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$4;
})()));
return _el$3;
})();
})(), null);
effect(_p$ => {
var _v$ = numSuggestions() > 0,
_v$2 = props.placeholder ?? '';
_v$ !== _p$.e && setAttribute(_el$, "aria-expanded", _p$.e = _v$);
_v$2 !== _p$.t && setAttribute(_el$2, "placeholder", _p$.t = _v$2);
return _p$;
}, {
e: undefined,
t: undefined
});
effect(() => _el$2.value = query());
return _el$;
})();
}
export { Suggest as default };
//# sourceMappingURL=index.js.map