Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/6
This commit was merged in pull request #6.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import styles from './SearchableSelect.module.scss';
|
||||
import { DropdownPortal } from './DropdownPortal';
|
||||
import { Icon } from './Icon';
|
||||
import { texts } from '../../texts';
|
||||
|
||||
@@ -12,6 +13,8 @@ export interface IAsyncSearchableSelectProps<T> {
|
||||
value: string;
|
||||
/** Название выбранного значения — показываем в триггере без похода на сервер. */
|
||||
selectedLabel?: string;
|
||||
/** Вторая строка триггера мельче и серым — например, url под названием сайта. */
|
||||
selectedSublabel?: string;
|
||||
onChange: (value: string, item: T | undefined) => void;
|
||||
search: (query: string, page: number) => Promise<IAsyncSearchResult<T>>;
|
||||
getValue: (item: T) => string;
|
||||
@@ -29,6 +32,7 @@ export interface IAsyncSearchableSelectProps<T> {
|
||||
export function AsyncSearchableSelect<T>({
|
||||
value,
|
||||
selectedLabel,
|
||||
selectedSublabel,
|
||||
onChange,
|
||||
search,
|
||||
getValue,
|
||||
@@ -51,6 +55,7 @@ export function AsyncSearchableSelect<T>({
|
||||
const [error, setError] = React.useState<string | undefined>();
|
||||
|
||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||
const menuRef = React.useRef<HTMLDivElement>(null);
|
||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||
const listRef = React.useRef<HTMLDivElement>(null);
|
||||
const sentinelRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -103,7 +108,10 @@ export function AsyncSearchableSelect<T>({
|
||||
if (!open) return;
|
||||
|
||||
const onDocumentMouseDown = (e: MouseEvent): void => {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
|
||||
const target = e.target as Node;
|
||||
if (rootRef.current?.contains(target)) return;
|
||||
if (menuRef.current?.contains(target)) return;
|
||||
setOpen(false);
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent): void => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
@@ -161,57 +169,62 @@ export function AsyncSearchableSelect<T>({
|
||||
onClick={toggle}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className={`${styles.value} ${value ? '' : styles.placeholder}`}>
|
||||
{value ? (selectedLabel ?? value) : placeholder}
|
||||
</span>
|
||||
{value && selectedSublabel ? (
|
||||
<span className={styles.valueStack}>
|
||||
<span className={styles.valueMain}>{selectedLabel ?? value}</span>
|
||||
<span className={styles.valueSub}>{selectedSublabel}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className={`${styles.value} ${value ? '' : styles.placeholder}`}>
|
||||
{value ? (selectedLabel ?? value) : placeholder}
|
||||
</span>
|
||||
)}
|
||||
<Icon name="chevron-down" size="sm" className={styles.chevron} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className={styles.menu}>
|
||||
<input
|
||||
ref={searchRef}
|
||||
className={styles.search}
|
||||
placeholder={searchPlaceholder}
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
/>
|
||||
<div className={styles.list} ref={listRef}>
|
||||
{loading && <div className={styles.empty}>{texts.select.searching}</div>}
|
||||
{!loading && error && <div className={styles.errorText}>{error}</div>}
|
||||
{!loading && !error && items.length === 0 && (
|
||||
<div className={styles.empty}>{texts.select.nothingFound}</div>
|
||||
)}
|
||||
{!loading && !error && items.map(item => {
|
||||
const itemValue = getValue(item);
|
||||
const sub = getSublabel ? getSublabel(item) : undefined;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={itemValue}
|
||||
className={`${styles.item} ${itemValue === value ? styles.selected : ''}`}
|
||||
onClick={() => pick(item)}
|
||||
>
|
||||
{getLabel(item)}
|
||||
{sub && <span className={styles.itemSub}>{sub}</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!loading && !error && hasMore && (
|
||||
<div ref={sentinelRef} className={styles.sentinel}>
|
||||
{loadingMore && 'Загрузка...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{allowClear && value && (
|
||||
<div className={styles.clear}>
|
||||
<button type="button" className={styles.item} onClick={() => pick(undefined)}>
|
||||
{clearLabel}
|
||||
<DropdownPortal anchorRef={rootRef} open={open} className={styles.menu} menuRef={menuRef}>
|
||||
<input
|
||||
ref={searchRef}
|
||||
className={styles.search}
|
||||
placeholder={searchPlaceholder}
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
/>
|
||||
<div className={styles.list} ref={listRef}>
|
||||
{loading && <div className={styles.empty}>{texts.select.searching}</div>}
|
||||
{!loading && error && <div className={styles.errorText}>{error}</div>}
|
||||
{!loading && !error && items.length === 0 && (
|
||||
<div className={styles.empty}>{texts.select.nothingFound}</div>
|
||||
)}
|
||||
{!loading && !error && items.map(item => {
|
||||
const itemValue = getValue(item);
|
||||
const sub = getSublabel ? getSublabel(item) : undefined;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={itemValue}
|
||||
className={`${styles.item} ${itemValue === value ? styles.selected : ''}`}
|
||||
onClick={() => pick(item)}
|
||||
>
|
||||
{getLabel(item)}
|
||||
{sub && <span className={styles.itemSub}>{sub}</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!loading && !error && hasMore && (
|
||||
<div ref={sentinelRef} className={styles.sentinel}>
|
||||
{loadingMore && 'Загрузка...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{allowClear && value && (
|
||||
<div className={styles.clear}>
|
||||
<button type="button" className={styles.item} onClick={() => pick(undefined)}>
|
||||
{clearLabel}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</DropdownPortal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user