fix(ui): ComboBox y Flyout se portan a document.body con position:fixed

El panel desplegable de ComboBox se posicionaba con position:absolute dentro
de su propio contenedor. Cualquier ancestro con overflow:hidden/auto (un
Expander colapsable, el body de un DetailPopup, una tarjeta con scroll propio)
lo recortaba, dejando la lista invisible o cortada a la mitad -reproducido en
Configuracion > Apariencia, dentro del Expander-.

Fix: igual tecnica que Flyout -createPortal a document.body, position:fixed
calculado desde getBoundingClientRect() del trigger-, con cierre en click
afuera y en scroll de cualquier ancestro (capture:true, el scroll no
burbujea). De paso Flyout pasa de position:absolute a position:fixed por el
mismo motivo (evita un bug latente de desalineacion si la pagina esta
scrolleada).

Regla documentada en project-md-templates/UI-UX-WINDOWS.md (Kit de
componentes + do's/don'ts) para que no se repita en este ni otros proyectos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-05 21:50:09 -05:00
parent 827d798ef8
commit 6b61e5cea7
3 changed files with 62 additions and 16 deletions
+15 -2
View File
@@ -111,9 +111,19 @@ openDialog({
- **Campos** — `TextBox`, `NumberBox` (con spinner +/-), `ComboBox` propio (desplegable redondeado,
con buscador si la lista tiene **≥10 ítems**; nunca el `<select>`/combo nativo del SO — no se puede
redondear ni estilar), `Textarea`, `CheckBox`, `RadioButtons`, `ToggleSwitch` (interruptor), `Slider`.
**Regla dura de implementación (bug real, recurrente):** el panel desplegable de `ComboBox` (y de
cualquier control propio con un panel flotante — `DatePicker`, `Flyout`) **nunca** se posiciona con
`position: absolute` dentro de su propio contenedor. Un ancestro con `overflow: hidden` o `overflow:
auto` (un `Expander` colapsable, el body de un `DetailPopup`, una tarjeta con scroll propio) lo
recorta y la lista queda invisible o cortada a la mitad. Portar el panel a `document.body` con
`createPortal` y posicionarlo con `position: fixed` calculado desde `getBoundingClientRect()` del
trigger (misma técnica que `Flyout`) — así queda fuera del árbol/clipping del contenedor. Cerrar el
panel en scroll (`window.addEventListener('scroll', fn, true)`, con `capture:true` porque el scroll
de un ancestro no burbujea) y en click afuera.
- **DatePicker/TimePicker** — calendario propio en un **flyout** anclado al campo (navegación de mes,
día de hoy marcado, selección resaltada con `accent`); igual criterio que `ComboBox`: no usar el
selector nativo del SO por falta de control visual.
selector nativo del SO por falta de control visual, y el mismo requisito de portal +
`position: fixed` de arriba (no recortarse dentro de un contenedor con overflow).
- **ListView/DataGrid** — tabla con encabezados **ordenables** (clic ordena asc/desc), columna de
**selección múltiple** (checkbox), fila con **hover** y **estado seleccionado** (`accent/.14`), y
**clic derecho** que abre el menú contextual de esa fila (ver §6). Alternativa por fila sin mouse
@@ -304,7 +314,10 @@ Alt+Tab); si es una decisión puntual ligada al contexto actual, es un `ContentD
debajo del listado en pantallas de gestión (usar `DetailPopup`). ❌ que un `DetailPopup` cambie de
tamaño al moverse entre sus pestañas/secciones internas. ❌ montar `ContentDialog`/`DetailPopup` sin
portal a `document.body` (queda atrapado en el stacking context de un ancestro y puede terminar
tapado por otro overlay).
tapado por otro overlay). ❌ panel desplegable de `ComboBox`/`DatePicker`/`Flyout` con
`position: absolute` dentro de su propio contenedor (un `Expander`, el body de un `DetailPopup` o
cualquier tarjeta con `overflow` lo recorta o lo oculta) — portar a `document.body` y usar
`position: fixed` calculado desde el trigger.
> Para que una app "se sienta Windows moderna": misma barra de título, mismo panel de navegación,
> mismos cuatro mecanismos de aviso (`ContentDialog`/`InfoBar`/`Toast`/`Flyout`) bien diferenciados,