componentes / modal
Modal
Diálogo sobre um fundo escurecido com foco preso, bloqueio de rolagem, fechamento por Esc/fundo e role="dialog".
Como usar
Renderizado em um portal com `aria-modal`, nomeado por `title`. O foco entra ao abrir e volta ao gatilho ao fechar; o Tab fica dentro e a página atrás não rola. Passe `trigger` no caso não controlado, ou `open` + `onOpenChange` para controlar. Envolva um botão do rodapé em `ModalClose` para fechar.
preview
Modal.tsx
import { Button, Modal, ModalClose } from "vaiperui"; <Modal trigger={<Button>Abrir</Button>} title="Excluir projeto?" footer={ <> <ModalClose><Button variant="outlined">Cancelar</Button></ModalClose> <ModalClose><Button variantColor="danger">Excluir</Button></ModalClose> </> }> Esta ação não pode ser desfeita.</Modal>Tamanhos
preview
tamanhos.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
O pai controla `open` e reage à resposta.
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 | Padrão | Descrição |
|---|---|---|---|
| title | ReactNode | obrigatório | Título e nome acessível (aria-labelledby) |
| footer | ReactNode | nenhum | Botões de ação; sem ele nem o rodapé nem a divisória aparecem |
| trigger | ReactElement | nenhum | Elemento que abre; desnecessário quando controlado |
| open / defaultOpen / onOpenChange | boolean / boolean / (open) => void | nenhum | Estado aberto controlado ou não controlado |
| showClose / closeLabel | boolean / string | true / "Close" | Botão × no cabeçalho e seu nome acessível |
| closeOnEscape / closeOnBackdropClick | boolean | true | Se Esc ou clique no fundo fecham |
| size | sm | md | lg | sm | Largura máxima: 480, 640 ou 800px |
| showHeaderDivider / showFooterDivider | boolean | true | Linhas entre cabeçalho, corpo e rodapé |
| radius / animation | none | sm | md | lg | full / none | smooth | fast | bounce | lg / smooth | Arredondamento e velocidade da transição de entrada |
Acessibilidade
- O painel é renderizado em um portal no `document.body` com `role="dialog"` e `aria-modal="true"`.
- `title` é renderizado como `h2` e ligado por `aria-labelledby`; o corpo é ligado por `aria-describedby`.
- Ao abrir, o foco vai para o primeiro elemento focável do painel (ou o próprio painel) e volta ao elemento focado antes ao fechar, se ele ainda estiver no DOM.
- Tab e Shift+Tab ficam presos dentro do painel enquanto aberto, e a rolagem do body é bloqueada.
- O gatilho recebe `aria-haspopup="dialog"` e `aria-expanded`.
- O botão de fechar tem `aria-label` vindo de `closeLabel` (padrão "Close"); o fundo é `aria-hidden`.
Teclado
| Tecla | Ação |
|---|---|
| Tab / Shift+Tab | Move o foco entre os elementos do painel, voltando ao início/fim |
| Esc | Fecha (desativado com `closeOnEscape={false}`) |
Tenha em mente
- `title` é obrigatório e deve descrever o diálogo; traduza `closeLabel`, pois o padrão é em 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. Clicar no fundo fecha, salvo con `closeOnBackdropClick={false}`.