fix(searchable-select): dropdown opens as floating overlay (Portal + absolute)
Some checks are pending
Some checks are pending
Старый dropdown использовал position:absolute внутри Section (а у того overflow-hidden для скруглений), из-за чего он клипался границами карточки. На некоторых страницах визуально это смотрелось так, будто список «раздвигает» layout. Решение — тот же Portal-паттерн что у SupplyLineQuickAdd и DateField: - dropdown рендерится через createPortal в document.body - position: fixed, координаты по getBoundingClientRect() trigger'а - z-[100], sticky-headers/секции не перекроют - Auto-flip: если внизу <240px и сверху больше — открываем вверх (anchor через bottom: window.innerHeight - rect.top) - Outside-click учитывает обе ноды (wrap + dropdown в portal) - На window.scroll/resize dropdown закрывается чтобы не «уплывать» относительно trigger'а Фиксит сразу все Select'ы в проекте (тип штрихкода, валюта, страна, группа товара, тип цены и т.д.) — компонент один. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
37bacc196e
commit
d447f431ba
|
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
Children, isValidElement, useEffect, useMemo, useRef, useState,
|
||||
Children, isValidElement, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
type InputHTMLAttributes, type SelectHTMLAttributes, type ReactNode, type TextareaHTMLAttributes,
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||
|
|
@ -88,6 +89,37 @@ export function Select({
|
|||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
const wrapRef = useRef<HTMLDivElement>(null)
|
||||
const searchRef = useRef<HTMLInputElement>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const [pos, setPos] = useState<{ top: number; left: number; width: number; openUp: boolean } | null>(null)
|
||||
|
||||
// Считаем позицию dropdown'а: предпочтительно вниз, но если внизу < 240px,
|
||||
// а вверху больше — открываем вверх (auto-flip). Через portal в body
|
||||
// — overflow-hidden родителей не режет.
|
||||
const recomputePos = () => {
|
||||
const el = wrapRef.current
|
||||
if (!el) return
|
||||
const r = el.getBoundingClientRect()
|
||||
const spaceBelow = window.innerHeight - r.bottom
|
||||
const spaceAbove = r.top
|
||||
const openUp = spaceBelow < 240 && spaceAbove > spaceBelow
|
||||
setPos({
|
||||
top: openUp ? r.top - 4 : r.bottom + 4,
|
||||
left: r.left,
|
||||
width: r.width,
|
||||
openUp,
|
||||
})
|
||||
}
|
||||
useLayoutEffect(() => {
|
||||
if (!open) return
|
||||
recomputePos()
|
||||
const onScrollOrResize = () => setOpen(false)
|
||||
window.addEventListener('scroll', onScrollOrResize, true)
|
||||
window.addEventListener('resize', onScrollOrResize)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScrollOrResize, true)
|
||||
window.removeEventListener('resize', onScrollOrResize)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const current = String(value ?? '')
|
||||
const selected = options.find((o) => o.value === current)
|
||||
|
|
@ -102,9 +134,10 @@ export function Select({
|
|||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
|
||||
setOpen(false); setQuery('')
|
||||
}
|
||||
const t = e.target as Node
|
||||
const inWrap = wrapRef.current?.contains(t)
|
||||
const inDropdown = dropdownRef.current?.contains(t)
|
||||
if (!inWrap && !inDropdown) { setOpen(false); setQuery('') }
|
||||
}
|
||||
document.addEventListener('mousedown', onDoc)
|
||||
return () => document.removeEventListener('mousedown', onDoc)
|
||||
|
|
@ -156,8 +189,17 @@ export function Select({
|
|||
<span className={cn('truncate', !selected && 'text-slate-400')}>{triggerLabel}</span>
|
||||
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute z-50 mt-1 w-full max-h-72 overflow-hidden rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-lg flex flex-col">
|
||||
{open && pos && createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: pos.left,
|
||||
width: pos.width,
|
||||
...(pos.openUp ? { bottom: window.innerHeight - pos.top } : { top: pos.top }),
|
||||
}}
|
||||
className="z-[100] max-h-72 overflow-hidden rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-lg flex flex-col"
|
||||
>
|
||||
<div className="p-2 border-b border-slate-100 dark:border-slate-800">
|
||||
<input
|
||||
ref={searchRef}
|
||||
|
|
@ -219,7 +261,8 @@ export function Select({
|
|||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue