@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
121 lines • 5.87 kB
JavaScript
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