# Modal

> Componentes / Estado e aviso · Estado: **Em andamento** — Implementado e utilizável, mas ainda sem a validação completa de acessibilidade.

Janela sobre a página que pede atenção ou decisão: prende o foco dentro dela, torna o resto da página `inert`, trava a rolagem do fundo, fecha com Esc, com o X ou com clique fora (que se pode desligar) e devolve o foco a quem abriu. O título é obrigatório; `role="alertdialog"` para a confirmação que interrompe.

- Função: Estado e aviso
- Também chamado de: diálogo, janela, pop-up, modal, caixa de diálogo
- Mora em: `app/components/molecules/Modal.tsx`
- Página: [https://phorte-design-system.vercel.app/styleguide/peca/Modal](https://phorte-design-system.vercel.app/styleguide/peca/Modal)
- Ficha no Storybook: [components-complex-modal](https://main--69b4847d498a9bd8659db9ba.chromatic.com/?path=/docs/components-complex-modal--docs)
- Prévia ao vivo, sozinha: [https://phorte-design-system.vercel.app/previa/Modal/0](https://phorte-design-system.vercel.app/previa/Modal/0)
- Todas as peças: [https://phorte-design-system.vercel.app/llms.txt](https://phorte-design-system.vercel.app/llms.txt)

## Como usar no código

```tsx
import { Modal } from '@/app/components/molecules/Modal';

<Modal isOpen={…} onClose={…} title={…}>…</Modal>
```

O `…` marca as props obrigatórias — ver *Propriedades*.

## Uso

### Variantes na ficha

1. [Aberto](https://phorte-design-system.vercel.app/previa/Modal/0)
2. [Interativo](https://phorte-design-system.vercel.app/previa/Modal/1)
3. [Foco inicial no primeiro campo](https://phorte-design-system.vercel.app/previa/Modal/2)
4. [Sem elemento focável](https://phorte-design-system.vercel.app/previa/Modal/3)
5. [Retorno de foco ao acionador](https://phorte-design-system.vercel.app/previa/Modal/4)
6. [Confirmação destrutiva](https://phorte-design-system.vercel.app/previa/Modal/5)

Para uma decisão ou um formulário curto que precisa ser resolvido antes de seguir — confirmar a matrícula, excluir algo.

### Quando não usar

Para uma mensagem que não pede ação, use o Alert. Para escolher entre ações, o Dropdown. Para conteúdo longo, uma página.

### Faça

- Escreva um `title` que diga o que se decide ("Confirme sua matrícula").
- Ponha as ações no `footer`, com a principal por último.
- Deixe o botão de fechar (`showCloseButton`), a menos que a decisão seja obrigatória.
- Use `size` pela quantidade de conteúdo: `sm` a `xl`, ou `full`.
- Em confirmação destrutiva, `role="alertdialog"` e `closeOnBackdrop={false}`: um clique errado no fundo não decide por ninguém (E63).
- Use a origem do `onClose` (`botao`, `fundo`, `escape`) quando o fechamento precisa de tratamento — perguntar antes de descartar um rascunho, por exemplo.

### Evite

- Modal dentro de modal — um de cada vez.
- Fluxo de várias etapas dentro do diálogo: aí é página.
- Tirar toda saída: sem X, o Esc e uma ação de cancelar continuam obrigatórios.
- Abrir sem ação da pessoa (ao carregar a página).
- Renderizar solto no styleguide: ele toma a página — por isso a prévia dele abre num documento próprio.

### Nota de uso

Prende o foco, devolve-o a quem abriu e torna o resto da página `inert`. **Como (corrigido na E63):** o diálogo vai por portal direto ao `<body>`, e o `inert` cai em todos os irmãos dele — o fundo inteiro, sem precisar subir ancestrais; a nota antiga descrevia uma cadeia de ancestrais que o código não usa. Um de cada vez, sempre com saída explícita (Esc e uma ação de cancelar, mesmo sem o X), e nunca fluxo de várias etapas. `role="alertdialog"` e `closeOnBackdrop={false}` para confirmação destrutiva. A entrada — esmaece e cresce de 95% a 100% em 200ms — só anima com movimento liberado no sistema; até a E46 a classe vinha de um plugin nunca instalado e o painel aparecia seco. **Desde a E113** essa mecânica mora em `app/lib/dialogo-modal.ts`, que a Gaveta também usa: uma cópia só, com os testes das duas peças travando.

### Anatomia

1. **Fundo** — Escurece a página a 60% com desfoque. É só desenho: o clique fora do painel fecha pelo documento — o fechamento leve do `<dialog>` nativo (E137) —, salvo com `closeOnBackdrop={false}`.
2. **Painel** — `--card`, cantos `rounded-xl`, na largura do `size`.
3. **Cabeçalho** — Título (`h2`), descrição opcional e o botão "Fechar" de 44×44.
4. **Conteúdo** — O `children`, com rolagem própria quando passa da altura.
5. **Rodapé** — O `footer`, com as ações.

### Estados

| Estado | O que muda |
| --- | --- |
| Fechado | Nada é renderizado. |
| Aberto | Portal no `<body>`, `role="dialog"` com `aria-modal`, resto da página `inert` e rolagem travada. Entra esmaecendo e crescendo de 95% em 200ms, só com movimento liberado. |
| Sem nada focável | O foco vai ao próprio painel, e o Tab não sai dele — conferido no teste da ficha. |
| Alerta | `role="alertdialog"`: o leitor anuncia como alerta e lê a descrição junto (E63). |

### Comportamento

| Situação | O que acontece |
| --- | --- |
| Abre | Guarda quem tinha o foco e leva o foco ao primeiro elemento do conteúdo, pulando o "Fechar" — conferido nos testes da ficha: "Voltar" num diálogo de confirmação, o primeiro campo num formulário. |
| Fecha | Devolve o foco a quem abriu — pelo Esc e pelo fundo, conferido no teste da ficha —, destrava a rolagem e tira o `inert` da página. |
| Clique no fundo | Chama `onClose("fundo")`; com `closeOnBackdrop={false}`, não fecha, e o foco fica onde estava, dentro do diálogo — conferido no teste da ficha. Até a medição da E63 esse clique mandava o foco ao `<body>`. |
| Esc ou X | Chama `onClose("escape")` ou `onClose("botao")` (E63). |
| Elemento escondido dentro | Fica fora da volta do Tab (o filtro descarta o que não tem `offsetParent`). |

### Relacionados

- [Alert](https://phorte-design-system.vercel.app/styleguide/md/Alert.md) — Mensagem que não pede decisão.
- [Dropdown](https://phorte-design-system.vercel.app/styleguide/md/Dropdown.md) — Para escolher entre ações.
- [Button](https://phorte-design-system.vercel.app/styleguide/md/Button.md) — As ações do rodapé.

## Especificação

### Propriedades

| Prop | Tipo | Padrão | Obrigatória | Descrição |
| --- | --- | --- | --- | --- |
| `size` | `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | não | Variante do `cva`. |
| `isOpen` | `boolean` | — | sim |  |
| `onClose` | `(origem: OrigemDoFechamento) => void` | — | sim | Recebe a origem (E63): `'botao'` (o X), `'fundo'` ou `'escape'`. Quem fecha por um botão do rodapé chama o próprio estado — não passa por aqui. |
| `role` | `'dialog' \| 'alertdialog'` | `'dialog'` | não | `alertdialog` (E63) para confirmação que interrompe — excluir, sair sem salvar. O leitor anuncia como alerta, e a descrição é lida junto. |
| `closeOnBackdrop` | `boolean` | `true` | não | Clique no fundo fecha (padrão). Desligue em confirmação destrutiva e em formulário longo, em que um clique errado perde o que se escreveu (E63). O Esc e o X continuam fechando: saída explícita sempre existe. |
| `title` | `string` | — | sim | Obrigatório de propósito. Um diálogo sem nome é anunciado pelo leitor de tela apenas como "diálogo" — a pessoa sabe que algo abriu, mas não o quê. Isso é defeito de conteúdo, não opção de API, e o tipo é o lugar certo para dizer isso: o erro aparece na hora de escrever a chamada, não numa auditoria seis meses depois. |
| `description` | `string` | — | não |  |
| `children` | `React.ReactNode` | — | sim |  |
| `footer` | `React.ReactNode` | — | não |  |
| `showCloseButton` | `boolean` | `true` | não |  |

### Tokens de cor

| Token | Valor | Usado como |
| --- | --- | --- |
| `--border` | #E5E5E5 | `border-border` |
| `--card` | #FFFFFF | `bg-card` |
| `--foreground` | #1A1A1A | `text-ink` |
| `--muted` | #F4F6F9 | `bg-muted` |
| `--muted-foreground` | #595959 | `text-muted-foreground` |

## Acessibilidade

### Teclado

| Tecla | Faz |
| --- | --- |
| Tab | Anda pelos elementos do painel; do último volta ao primeiro — medido: Cancelar, Confirmar Ação, Fechar, Cancelar… |
| Shift / Tab | Anda para trás; do primeiro vai ao último. |
| Esc | Fecha e devolve o foco ao botão que abriu — medido. |

### Leitor de tela

- O nome do diálogo é o título (`aria-labelledby`), e a descrição, se houver, é ligada por `aria-describedby`.
- O `inert` no resto da página tira o fundo do Tab e da árvore de acessibilidade — o leitor não "escapa" do diálogo. O diálogo vai por portal ao `<body>`, e o `inert` cai nos irmãos dele — conferido no teste da ficha.

### O que sustenta o estado

Usa os padrões: dialogo. Falta ouvir com leitor de tela: dialogo.

## Status e recursos

| Tipo | Recurso | Status |
| --- | --- | --- |
| Código | `app/components/molecules/Modal.tsx` | Disponível |
| Ficha | https://main--69b4847d498a9bd8659db9ba.chromatic.com/?path=/docs/components-complex-modal--docs | Disponível |
| Teste de acessibilidade | axe em `npm run test` | No gate de todo commit |
| Leitor de tela | Passada da E33 | Falta ouvir |
| Design | Figma | Desativado até a Fase 2 |

### Histórico

- `251416b` 2026-10-01 — E159: Testimonials — o vídeo toca dentro do cartão, YouTube e Vimeo
- Histórico completo no GitHub: https://github.com/srbunnie/Pos-Phorte-Design-System/commits/main/app/components/molecules/Modal.tsx
