бесконечный скролл

This commit is contained in:
gorbenko.pavel
2026-07-29 16:24:05 +05:00
parent 535741a031
commit bfa0777739
2 changed files with 33 additions and 28 deletions
@@ -51,6 +51,8 @@ export function AsyncSearchableSelect<T>({
const rootRef = React.useRef<HTMLDivElement>(null);
const searchRef = React.useRef<HTMLInputElement>(null);
const listRef = React.useRef<HTMLDivElement>(null);
const sentinelRef = React.useRef<HTMLDivElement>(null);
// Растущий счётчик запросов: если ответ пришёл не от последнего запроса
// (например, пользователь допечатал ещё символ) — просто игнорируем его.
const requestIdRef = React.useRef(0);
@@ -118,6 +120,27 @@ export function AsyncSearchableSelect<T>({
if (open && searchRef.current) searchRef.current.focus();
}, [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 => {
if (disabled) return;
setQuery('');
@@ -152,7 +175,7 @@ export function AsyncSearchableSelect<T>({
value={query}
onChange={e => setQuery(e.target.value)}
/>
<div className={styles.list}>
<div className={styles.list} ref={listRef}>
{loading && <div className={styles.empty}>Поиск...</div>}
{!loading && error && <div className={styles.errorText}>{error}</div>}
{!loading && !error && items.length === 0 && (
@@ -174,15 +197,8 @@ export function AsyncSearchableSelect<T>({
);
})}
{!loading && !error && hasMore && (
<div className={styles.loadMoreRow}>
<button
type="button"
className={styles.loadMoreBtn}
disabled={loadingMore}
onClick={() => runSearch(query, page + 1, true)}
>
{loadingMore ? 'Загрузка...' : 'Показать ещё'}
</button>
<div ref={sentinelRef} className={styles.sentinel}>
{loadingMore && 'Загрузка...'}
</div>
)}
</div>