From bfa07777395c29de73de4ec098e9fe5b3b94a0ee Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Wed, 29 Jul 2026 16:24:05 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B1=D0=B5=D1=81=D0=BA=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D1=87=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BA=D1=80=D0=BE=D0=BB=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ui/AsyncSearchableSelect.tsx | 36 +++++++++++++------ .../ui/SearchableSelect.module.scss | 25 ++++--------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/frontend/src/webparts/approvalAdmin/components/ui/AsyncSearchableSelect.tsx b/frontend/src/webparts/approvalAdmin/components/ui/AsyncSearchableSelect.tsx index f3e5d95..8d74d1b 100644 --- a/frontend/src/webparts/approvalAdmin/components/ui/AsyncSearchableSelect.tsx +++ b/frontend/src/webparts/approvalAdmin/components/ui/AsyncSearchableSelect.tsx @@ -51,6 +51,8 @@ export function AsyncSearchableSelect({ const rootRef = React.useRef(null); const searchRef = React.useRef(null); + const listRef = React.useRef(null); + const sentinelRef = React.useRef(null); // Растущий счётчик запросов: если ответ пришёл не от последнего запроса // (например, пользователь допечатал ещё символ) — просто игнорируем его. const requestIdRef = React.useRef(0); @@ -118,6 +120,27 @@ export function AsyncSearchableSelect({ 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({ value={query} onChange={e => setQuery(e.target.value)} /> -
+
{loading &&
Поиск...
} {!loading && error &&
{error}
} {!loading && !error && items.length === 0 && ( @@ -174,15 +197,8 @@ export function AsyncSearchableSelect({ ); })} {!loading && !error && hasMore && ( -
- +
+ {loadingMore && 'Загрузка...'}
)}
diff --git a/frontend/src/webparts/approvalAdmin/components/ui/SearchableSelect.module.scss b/frontend/src/webparts/approvalAdmin/components/ui/SearchableSelect.module.scss index a43e718..b587b26 100644 --- a/frontend/src/webparts/approvalAdmin/components/ui/SearchableSelect.module.scss +++ b/frontend/src/webparts/approvalAdmin/components/ui/SearchableSelect.module.scss @@ -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 {