fix(supply-quick-add): keep input focused after scan / clear on add
Some checks are pending
Some checks are pending
Сценарий — приёмщик подряд сканирует 50 штрихкодов без клика мышью:
- Sticky-bar с input'ом теперь ВНЕ Section'а (overflow-hidden родителя
ломал sticky), прибит к низу скроллируемого тела документа. После
любого добавления строки — input всегда виден.
- Очистка query и refocus вызываются СРАЗУ после клика/Enter, до
await на /products/{id}: пока сеть в полёте, юзер уже может начать
следующий скан. После завершения запроса — повторный refocus
(двойной guarded focus + requestAnimationFrame), чтобы перебить
любые ререндеры родителя, которые могут увести фокус.
- Поиск по quick-search теперь через AbortController — устаревший
ответ при быстром вводе подряд не подменяет свежий список.
- Закрытие ProductQuickCreateModal тоже возвращает фокус в input
(и при «Создать», и при «Отмена»).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c8a7efde47
commit
45f2ce682f
|
|
@ -1,6 +1,5 @@
|
|||
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { BarcodeType, type Product } from '@/lib/types'
|
||||
|
|
@ -97,22 +96,26 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
|
|||
return () => clearTimeout(t)
|
||||
}, [query])
|
||||
|
||||
// Поисковый запрос на каждое изменение debounced query
|
||||
// Поисковый запрос на каждое изменение debounced query. AbortController
|
||||
// обрывает устаревший запрос, чтобы при быстрых последовательных вводах
|
||||
// (или сразу после скана) старый ответ не подменил свежий список.
|
||||
useEffect(() => {
|
||||
const q = debounced.trim()
|
||||
if (q.length === 0) { setItems([]); setOpen(false); return }
|
||||
let cancelled = false
|
||||
const ac = new AbortController()
|
||||
setLoading(true)
|
||||
api.get<QuickSearchItem[]>('/api/catalog/products/quick-search', {
|
||||
params: { search: q, storeId: storeId || undefined, limit: 20 },
|
||||
signal: ac.signal,
|
||||
}).then((res) => {
|
||||
if (cancelled) return
|
||||
setItems(res.data)
|
||||
setOpen(true)
|
||||
setHighlight(0)
|
||||
}).catch(() => { if (!cancelled) setItems([]) })
|
||||
.finally(() => { if (!cancelled) setLoading(false) })
|
||||
return () => { cancelled = true }
|
||||
}).catch((e) => {
|
||||
if ((e as { name?: string }).name === 'CanceledError' || (e as { code?: string }).code === 'ERR_CANCELED') return
|
||||
setItems([])
|
||||
}).finally(() => setLoading(false))
|
||||
return () => ac.abort()
|
||||
}, [debounced, storeId])
|
||||
|
||||
// Outside click — закрыть dropdown. Dropdown рендерится в Portal, поэтому
|
||||
|
|
@ -136,12 +139,22 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
|
|||
return () => clearTimeout(t)
|
||||
}, [hint])
|
||||
|
||||
const fetchProductFull = useMutation({
|
||||
mutationFn: async (id: string) => (await api.get<Product>(`/api/catalog/products/${id}`)).data,
|
||||
})
|
||||
// Возврат фокуса после добавления строки — двойной guarded focus:
|
||||
// сразу + через requestAnimationFrame, чтобы перебить любые ререндеры,
|
||||
// которые могут увести фокус в null. Без этого подряд сканирование
|
||||
// ломается на 2-3 скане, когда DOM подменяет input.
|
||||
const refocus = () => {
|
||||
inputRef.current?.focus()
|
||||
requestAnimationFrame(() => inputRef.current?.focus())
|
||||
}
|
||||
|
||||
const addById = async (id: string) => {
|
||||
const full = await fetchProductFull.mutateAsync(id)
|
||||
// Очищаем поле и возвращаем фокус СРАЗУ — пока сетевой запрос в полёте,
|
||||
// юзер уже может начать ввод следующего штрихкода.
|
||||
setQuery(''); setDebounced(''); setItems([]); setOpen(false)
|
||||
refocus()
|
||||
try {
|
||||
const full = (await api.get<Product>(`/api/catalog/products/${id}`)).data
|
||||
const incremented = onPick({
|
||||
id: full.id,
|
||||
name: full.name,
|
||||
|
|
@ -152,8 +165,9 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
|
|||
prices: full.prices?.map((p) => ({ amount: p.amount })),
|
||||
})
|
||||
if (incremented) setHint('Кол-во увеличено на 1')
|
||||
setQuery(''); setDebounced(''); setItems([]); setOpen(false)
|
||||
inputRef.current?.focus()
|
||||
} finally {
|
||||
refocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Точный barcode-поиск (для сканера: Enter сразу после цифр)
|
||||
|
|
@ -227,7 +241,7 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
|
|||
prices: p.prices?.map((pr) => ({ amount: pr.amount })),
|
||||
})
|
||||
setQuery(''); setDebounced(''); setItems([]); setOpen(false)
|
||||
inputRef.current?.focus()
|
||||
refocus()
|
||||
}
|
||||
|
||||
const showCreateRow = useMemo(() => {
|
||||
|
|
@ -312,7 +326,7 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
|
|||
|
||||
<ProductQuickCreateModal
|
||||
open={createPrefill !== null}
|
||||
onClose={() => setCreatePrefill(null)}
|
||||
onClose={() => { setCreatePrefill(null); refocus() }}
|
||||
prefill={createPrefill}
|
||||
onCreated={onCreated}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -447,8 +447,14 @@ export function SupplyEditPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
</Section>
|
||||
|
||||
{!isPosted && (
|
||||
<div className="mt-3">
|
||||
// Sticky-bottom вне Section (overflow-hidden родителя ломает sticky):
|
||||
// пользователь может подряд сканировать партию штрихкодов — после
|
||||
// каждого скана строка добавляется в таблицу выше, а input остаётся
|
||||
// прибит к низу скроллируемого тела документа и принимает следующий ввод.
|
||||
<div className="sticky bottom-0 -mx-3 sm:-mx-6 px-3 sm:px-6 py-3 bg-white dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 z-10 shadow-[0_-4px_12px_rgba(0,0,0,0.04)]">
|
||||
<SupplyLineQuickAdd
|
||||
storeId={form.storeId}
|
||||
onPick={addOrIncrementLine}
|
||||
|
|
@ -456,7 +462,6 @@ export function SupplyEditPage() {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue