From f7deecc41c5c2fb94fca6499d25d783bd3fad125 Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Sun, 26 Apr 2026 02:01:49 +0500 Subject: [PATCH] =?UTF-8?q?fix(supply-quick-add):=20dropdown=20not=20rende?= =?UTF-8?q?ring=20=E2=80=94=20Portal=20+=20fixed=20position?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Корень проблемы: Section рендерится с класcом overflow-hidden (нужен для скруглений углов) — absolute-позиционированный dropdown обрезался границей карточки и был не виден совсем. Решение: dropdown через React createPortal вынесен в document.body, позиция вычисляется по getBoundingClientRect() input'а на каждый open + window scroll/resize. position: fixed, z-[100] — выше любого sticky-header. Outside-click handler теперь учитывает оба контейнера (input wrap + portal-узел) — клик по элементу dropdown'а больше не закрывает его как «снаружи». Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/SupplyLineQuickAdd.tsx | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/food-market.web/src/components/SupplyLineQuickAdd.tsx b/src/food-market.web/src/components/SupplyLineQuickAdd.tsx index 9f2a1db..333a27a 100644 --- a/src/food-market.web/src/components/SupplyLineQuickAdd.tsx +++ b/src/food-market.web/src/components/SupplyLineQuickAdd.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react' +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' @@ -62,6 +63,28 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) { )>(null) const inputRef = useRef(null) const wrapRef = useRef(null) + const dropdownRef = useRef(null) + const [dropdownPos, setDropdownPos] = useState<{ top: number; left: number; width: number } | null>(null) + + // Считаем позицию dropdown'а по rect input'а — без этого + // он рендерится через portal в body и будет в углу. + const recomputePos = () => { + const el = inputRef.current + if (!el) return + const r = el.getBoundingClientRect() + setDropdownPos({ top: r.bottom + 4, left: r.left, width: r.width }) + } + useLayoutEffect(() => { + if (!open) return + recomputePos() + const onScrollOrResize = () => recomputePos() + window.addEventListener('scroll', onScrollOrResize, true) + window.addEventListener('resize', onScrollOrResize) + return () => { + window.removeEventListener('scroll', onScrollOrResize, true) + window.removeEventListener('resize', onScrollOrResize) + } + }, [open]) // Автофокус при монтировании useEffect(() => { @@ -92,11 +115,15 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) { return () => { cancelled = true } }, [debounced, storeId]) - // Outside click — закрыть dropdown + // Outside click — закрыть dropdown. Dropdown рендерится в Portal, поэтому + // проверяем и wrap (input), и сам dropdown. useEffect(() => { if (!open) return const onDoc = (e: MouseEvent) => { - if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpen(false) + const t = e.target as Node + const inWrap = wrapRef.current?.contains(t) + const inDropdown = dropdownRef.current?.contains(t) + if (!inWrap && !inDropdown) setOpen(false) } document.addEventListener('mousedown', onDoc) return () => document.removeEventListener('mousedown', onDoc) @@ -225,8 +252,12 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) { {hint} )} - {open && (query.trim().length > 0 || items.length > 0) && ( -
+ {open && (query.trim().length > 0 || items.length > 0) && dropdownPos && createPortal( +
{loading && items.length === 0 ? (
Ищу…
) : items.length === 0 ? ( @@ -275,7 +306,8 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
)} -
+ , + document.body, )}