UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

121 lines 5.87 kB
import { Shade, createComponent } from '@furystack/shades'; import { cssVariableTheme } from '../../services/css-variable-theme.js'; import { promisifyAnimation } from '../../utils/promisify-animation.js'; import { Icon } from '../icons/icon.js'; import { close } from '../icons/icon-definitions.js'; import { Loader } from '../loader.js'; import { searchableInputStyles } from '../searchable-input-styles.js'; import { SuggestInput } from './suggest-input.js'; import { SuggestManager } from './suggest-manager.js'; import { SuggestionList } from './suggestion-list.js'; export * from './suggest-input.js'; export * from './suggest-manager.js'; export * from './suggestion-list.js'; export * from './suggestion-result.js'; const isSyncProps = (props) => { return 'suggestions' in props; }; export const Suggest = Shade({ customElementName: 'shade-suggest', css: { ...searchableInputStyles, fontFamily: cssVariableTheme.typography.fontFamily, '& .suggest-wrapper': { display: 'flex', flexDirection: 'column', }, }, render: ({ props, injector, useDisposable, useRef, useHostProps, useObservable }) => { let getEntries; let getSuggestionEntry; if (isSyncProps(props)) { const { suggestions } = props; getEntries = async (term) => { const lower = term.toLowerCase(); return suggestions.filter((s) => s.toLowerCase().includes(lower)); }; getSuggestionEntry = (entry) => ({ element: createComponent(createComponent, null, entry), score: 1, }); } else { ; ({ getEntries, getSuggestionEntry } = props); } const manager = useDisposable('manager', () => new SuggestManager(getEntries, getSuggestionEntry)); const wrapperRef = useRef('wrapper'); const loaderRef = useRef('loader'); // Keep manager.element in sync for click-outside detection queueMicrotask(() => { const hostEl = wrapperRef.current?.closest('shade-suggest'); if (hostEl) manager.element = hostEl; }); const [isOpened] = useObservable('isOpened', manager.isOpened); useHostProps({ 'data-opened': isOpened ? '' : undefined, tabIndex: -1, 'data-spatial-nav-target': '', onfocus: (ev) => { const host = ev.currentTarget; const input = host.querySelector('input'); if (input) { input.focus(); } }, }); useDisposable('isLoadingSubscription', () => manager.isLoading.subscribe((isLoading) => { const loader = loaderRef.current; if (!loader) return; if (isLoading) { void promisifyAnimation(loader, [{ opacity: 0 }, { opacity: 1 }], { duration: 100, fill: 'forwards', }); } else { void promisifyAnimation(loader, [{ opacity: 1 }, { opacity: 0 }], { duration: 100, fill: 'forwards', }); } })); useDisposable('onSelectSuggestion', () => manager.subscribe('onSelectSuggestion', props.onSelectSuggestion)); return (createComponent("div", { ref: wrapperRef, className: "suggest-wrapper", onkeydown: (ev) => { const hasSuggestions = manager.isOpened.getValue() && manager.currentSuggestions.getValue().length > 0; if (!hasSuggestions) return; if (ev.key === 'Enter') { ev.preventDefault(); manager.selectSuggestion(); return; } if (ev.key === 'ArrowUp') { ev.preventDefault(); manager.selectedIndex.setValue(Math.max(0, manager.selectedIndex.getValue() - 1)); } if (ev.key === 'ArrowDown') { ev.preventDefault(); manager.selectedIndex.setValue(Math.min(manager.selectedIndex.getValue() + 1, manager.currentSuggestions.getValue().length - 1)); } }, oninput: (ev) => { void manager.getSuggestion({ injector, term: ev.target.value }); } }, createComponent("div", { className: "input-container", style: props.style }, createComponent("div", { className: "term-icon", onclick: () => manager.isOpened.setValue(true) }, props.defaultPrefix), createComponent(SuggestInput, { manager: manager }), createComponent("div", { className: "post-controls" }, createComponent("span", { ref: loaderRef, style: { display: 'inline-flex' } }, createComponent(Loader // eslint-disable-next-line furystack/no-direct-get-value-in-render -- Initial opacity only; animated transitions handled by isLoadingSubscription via DOM , { // eslint-disable-next-line furystack/no-direct-get-value-in-render -- Initial opacity only; animated transitions handled by isLoadingSubscription via DOM style: { width: '20px', height: '20px', opacity: manager.isLoading.getValue() ? '1' : '0' }, delay: 0, borderWidth: 4 })), createComponent("div", { className: "close-suggestions", onclick: () => manager.isOpened.setValue(false) }, createComponent(Icon, { icon: close, size: 14 })))), createComponent(SuggestionList, { manager: manager }))); }, }); //# sourceMappingURL=index.js.map