diff --git a/docs/UI-UX-WINDOWS.md b/docs/UI-UX-WINDOWS.md index e0b77ea..625bfc7 100644 --- a/docs/UI-UX-WINDOWS.md +++ b/docs/UI-UX-WINDOWS.md @@ -111,9 +111,19 @@ openDialog({ - **Campos** — `TextBox`, `NumberBox` (con spinner +/-), `ComboBox` propio (desplegable redondeado, con buscador si la lista tiene **≥10 ítems**; nunca el ` nativo para listas >= 10 items). +// El panel desplegable se porta a document.body y se posiciona con `position: fixed` calculado +// desde el trigger (igual tecnica que Flyout) — evita que un ancestro con overflow:hidden/auto +// (Expander, DetailPopup, cualquier tarjeta con scroll propio) recorte la lista desplegada. // options: [{ value, label }] export function ComboBox({ options, value, onChange, placeholder = '', searchThreshold = 10 }) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); - const rootRef = useRef(null); + const [pos, setPos] = useState(null); + const triggerRef = useRef(null); + const panelRef = useRef(null); + + function openPanel() { + const rect = triggerRef.current.getBoundingClientRect(); + setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width }); + setOpen(true); + } useEffect(() => { - function onDocClick(e) { - if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); + if (!open) return; + function onDocMouseDown(e) { + if (triggerRef.current?.contains(e.target)) return; + if (panelRef.current?.contains(e.target)) return; + setOpen(false); } - document.addEventListener('mousedown', onDocClick); - return () => document.removeEventListener('mousedown', onDocClick); - }, []); + // capture:true para detectar scroll en CUALQUIER ancestro (los eventos de scroll no burbujean). + function onScroll() { setOpen(false); } + document.addEventListener('mousedown', onDocMouseDown); + window.addEventListener('scroll', onScroll, true); + window.addEventListener('resize', onScroll); + return () => { + document.removeEventListener('mousedown', onDocMouseDown); + window.removeEventListener('scroll', onScroll, true); + window.removeEventListener('resize', onScroll); + }; + }, [open]); const selected = options.find((o) => String(o.value) === String(value)); const filtered = query @@ -22,13 +45,19 @@ export function ComboBox({ options, value, onChange, placeholder = '', searchThr : options; return ( -
- - {open && ( -
+ {open && pos && createPortal( +
{options.length >= searchThreshold && (
@@ -52,7 +81,8 @@ export function ComboBox({ options, value, onChange, placeholder = '', searchThr {o.label}
))} -
+
, + document.body )}
); diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index 4742185..eec02f5 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -105,10 +105,13 @@ button { font-family: inherit; } .field-row > .field { flex: 1; min-width: 180px; } .checkbox-row { display: flex; align-items: center; gap: 8px; font-size: .82rem; } -/* --- ComboBox --- */ +/* --- ComboBox --- + El panel es un portal a document.body posicionado con `position: fixed` (top/left/width en + linea, calculados desde el trigger) — NUNCA anidado ni con position:absolute dentro de .combobox, + para no quedar recortado por un ancestro con overflow:hidden/auto (Expander, DetailPopup, etc.). */ .combobox { position: relative; } .combobox-panel { - position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 40; + position: fixed; z-index: 220; background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); box-shadow: 0 8px 24px rgb(0 0 0 / .35); max-height: 240px; overflow-y: auto; padding: 6px; } @@ -213,7 +216,7 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; } /* --- Flyout / menú contextual --- */ .flyout-backdrop { position: fixed; inset: 0; z-index: 90; } .flyout-panel { - position: absolute; z-index: 100; background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); + position: fixed; z-index: 100; background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); box-shadow: 0 10px 28px rgb(0 0 0 / .35); padding: 6px; min-width: 180px; } .flyout-item {