бесконечный скролл
This commit is contained in:
@@ -51,6 +51,8 @@ export function AsyncSearchableSelect<T>({
|
|||||||
|
|
||||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const listRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const sentinelRef = React.useRef<HTMLDivElement>(null);
|
||||||
// Растущий счётчик запросов: если ответ пришёл не от последнего запроса
|
// Растущий счётчик запросов: если ответ пришёл не от последнего запроса
|
||||||
// (например, пользователь допечатал ещё символ) — просто игнорируем его.
|
// (например, пользователь допечатал ещё символ) — просто игнорируем его.
|
||||||
const requestIdRef = React.useRef(0);
|
const requestIdRef = React.useRef(0);
|
||||||
@@ -118,6 +120,27 @@ export function AsyncSearchableSelect<T>({
|
|||||||
if (open && searchRef.current) searchRef.current.focus();
|
if (open && searchRef.current) searchRef.current.focus();
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
// Бесконечная прокрутка: как только сентинел внизу списка попадает в
|
||||||
|
// видимую область скролл-контейнера — подгружаем следующую страницу.
|
||||||
|
// root указываем явно (сам .list, а не viewport) — иначе браузер считает
|
||||||
|
// видимость относительно всей страницы, а не прокручиваемого дропдауна.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!open || !hasMore || loading || loadingMore) return;
|
||||||
|
|
||||||
|
const rootEl = listRef.current;
|
||||||
|
const sentinelEl = sentinelRef.current;
|
||||||
|
if (!rootEl || !sentinelEl) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
if (entries[0]?.isIntersecting) runSearch(query, page + 1, true);
|
||||||
|
},
|
||||||
|
{ root: rootEl, rootMargin: '64px' }
|
||||||
|
);
|
||||||
|
observer.observe(sentinelEl);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [open, hasMore, loading, loadingMore, page, query, runSearch]);
|
||||||
|
|
||||||
const toggle = (): void => {
|
const toggle = (): void => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
setQuery('');
|
setQuery('');
|
||||||
@@ -152,7 +175,7 @@ export function AsyncSearchableSelect<T>({
|
|||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className={styles.list}>
|
<div className={styles.list} ref={listRef}>
|
||||||
{loading && <div className={styles.empty}>Поиск...</div>}
|
{loading && <div className={styles.empty}>Поиск...</div>}
|
||||||
{!loading && error && <div className={styles.errorText}>{error}</div>}
|
{!loading && error && <div className={styles.errorText}>{error}</div>}
|
||||||
{!loading && !error && items.length === 0 && (
|
{!loading && !error && items.length === 0 && (
|
||||||
@@ -174,15 +197,8 @@ export function AsyncSearchableSelect<T>({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!loading && !error && hasMore && (
|
{!loading && !error && hasMore && (
|
||||||
<div className={styles.loadMoreRow}>
|
<div ref={sentinelRef} className={styles.sentinel}>
|
||||||
<button
|
{loadingMore && 'Загрузка...'}
|
||||||
type="button"
|
|
||||||
className={styles.loadMoreBtn}
|
|
||||||
disabled={loadingMore}
|
|
||||||
onClick={() => runSearch(query, page + 1, true)}
|
|
||||||
>
|
|
||||||
{loadingMore ? 'Загрузка...' : 'Показать ещё'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -115,25 +115,14 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadMoreRow {
|
.sentinel {
|
||||||
padding: 4px 12px 2px;
|
height: 30px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
.loadMoreBtn {
|
justify-content: center;
|
||||||
width: 100%;
|
color: $gray-400;
|
||||||
padding: 8px 12px;
|
font-size: 11.5px;
|
||||||
border-radius: $r-sm;
|
|
||||||
border: 1px dashed $gray-300;
|
|
||||||
background: transparent;
|
|
||||||
color: $navy-700;
|
|
||||||
font-family: $font-sans;
|
|
||||||
font-size: 12.5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: .15s;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) { border-color: $gold-500; color: $gold-600; background: $gold-50; }
|
|
||||||
&:disabled { opacity: .5; cursor: not-allowed; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear {
|
.clear {
|
||||||
|
|||||||
Reference in New Issue
Block a user