componentes / modal
Modal
Diálogo sobre un fondo oscurecido con foco atrapado, bloqueo de scroll, cierre por Esc/fondo y role="dialog".
Cómo usar
Renderizado en un portal con `aria-modal`, nombrado por `title`. El foco entra al abrir y vuelve al disparador al cerrar; el Tab queda dentro y la página de atrás no se desplaza. Pasa `trigger` en el caso no controlado, o `open` + `onOpenChange` para controlarlo. Envuelve un botón del pie en `ModalClose` para cerrar.
preview
Modal.tsx
import { Button, Modal, ModalClose } from "vaiperui"; <Modal trigger={<Button>Abrir</Button>} title="¿Eliminar proyecto?" footer={ <> <ModalClose><Button variant="outlined">Cancelar</Button></ModalClose> <ModalClose><Button variantColor="danger">Eliminar</Button></ModalClose> </> }> Esta acción no se puede deshacer.</Modal>Tamaños
preview
tamanos.tsx
<Modal size="sm" title="sm" trigger={<Button>sm</Button>}>...</Modal><Modal size="md" title="md" trigger={<Button>md</Button>}>...</Modal><Modal size="lg" title="lg" trigger={<Button>lg</Button>}>...</Modal>Controlado
El padre controla `open` y reacciona a la respuesta.
preview
controlado.tsx
const [open, setOpen] = useState(false); <Button onClick={() => setOpen(true)}>Open</Button><Modal open={open} onOpenChange={setOpen} title="..." footer={...}>...</Modal>Props
| Prop | Valores | Predeterminado | Descripción |
|---|---|---|---|
| title | ReactNode | obligatorio | Título y nombre accesible (aria-labelledby) |
| footer | ReactNode | ninguno | Botones de acción; sin él no se muestran ni el pie ni su divisor |
| trigger | ReactElement | ninguno | Elemento que lo abre; innecesario si es controlado |
| open / defaultOpen / onOpenChange | boolean / boolean / (open) => void | ninguno | Estado abierto controlado o no controlado |
| showClose / closeLabel | boolean / string | true / "Close" | Botón × en el encabezado y su nombre accesible |
| closeOnEscape / closeOnBackdropClick | boolean | true | Si Esc o clic en el fondo lo cierran |
| size | sm | md | lg | sm | Ancho máximo: 480, 640 u 800px |
| showHeaderDivider / showFooterDivider | boolean | true | Líneas entre encabezado, cuerpo y pie |
| radius / animation | none | sm | md | lg | full / none | smooth | fast | bounce | lg / smooth | Redondeo y velocidad de la transición de entrada |
Accesibilidad
- El panel se renderiza en un portal en `document.body` con `role="dialog"` y `aria-modal="true"`.
- `title` se renderiza como `h2` y se enlaza con `aria-labelledby`; el cuerpo se enlaza con `aria-describedby`.
- Al abrir, el foco va al primer elemento enfocable del panel (o al propio panel) y vuelve al elemento enfocado antes al cerrar, si sigue en el DOM.
- Tab y Shift+Tab quedan atrapados dentro del panel mientras está abierto, y el scroll del body se bloquea.
- El disparador recibe `aria-haspopup="dialog"` y `aria-expanded`.
- El botón de cerrar tiene `aria-label` de `closeLabel` (por defecto "Close"); el fondo es `aria-hidden`.
Teclado
| Tecla | Acción |
|---|---|
| Tab / Shift+Tab | Mueve el foco entre los elementos del panel, volviendo al inicio/fin |
| Esc | Cierra (se desactiva con `closeOnEscape={false}`) |
Ten en cuenta
- `title` es obligatorio y debe describir el diálogo; traduce `closeLabel`, ya que su valor por defecto está en inglés.
- La página de fondo no se marca como `inert` ni `aria-hidden`; depende de `aria-modal` y de la trampa de Tab. Hacer clic en el fondo cierra, salvo con `closeOnBackdropClick={false}`.