бесконечный скролл
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -115,25 +115,14 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadMoreRow {
|
||||
padding: 4px 12px 2px;
|
||||
.sentinel {
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.loadMoreBtn {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
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; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $gray-400;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.clear {
|
||||
|
||||
Reference in New Issue
Block a user