teqo-backup/docs/plans/quick-edit-sheet-mobile-quebrado.md
Francisco Solla 4e920ee064 feat(C107): sheet de criação de atualização usável no mobile — altura auto até o topo, form sem labels com divisórias full-bleed, placeholders, polaridade em toggle, checks à direita
- MunicipalityUpdateFields ganha layout 'labeled'|'list'; polaridade vira ToggleGroup Ruim|Neutra|Boa nas duas resoluções (mesmo dado, hidden input)
- layout 'list': labels sr-only, textarea sem borda, divisórias divide-y full-bleed, Urgente/Sinalizar adversário como linha tocável com check à direita
- StrictCombobox: prop placeholder (backward-compatible)
- Sheet do feed: max-h-dvh (teto no topo, rola por dentro), título sr-only, form lista com placeholders descritivos; desktop preservado
- Quick-edit B42 herda o visual no sheet mobile via layout
- unit/e2e atualizados para o toggle (radio)
2026-08-10 01:11:24 -03:00

72 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Impl: Quick-edit sheet mobile da lista de municípios quebra ao abrir (DialogClose fora do contexto Drawer)
Status: registrado
Atualizado em: 2026-08-10
Issue: (a registrar — C109)
Priority: P1
Model: composer-2.5
Model-local: deepseek-v4-flash-high
Impeccable: A — defect de render, sem superfície nova
Appetite: ~2–4 h eng; um outcome verificável — no celular, tocar em "Registrar atualização" (ou qualquer quick-edit sheet) no card de município abre o sheet e registra sem console error.
## Intenção (defect)
No mobile, os quick-edits em sheet dos cards de `/campanha/municipios` (B42: `MunicipalityListUpdateControl` variant `sheet`, E14 "Registrar movimento", etc.) **não abrem**: o clique dispara o erro `TypeError: Cannot destructure property 'store' of useDialogRootContext() as it is undefined` em `DialogClose` (base-ui), capturado pelo error boundary — o sheet nunca renderiza.
**Evidência:** reproduz em `main` puro (commit 8896dcb9, stash testado em 2026-08-10); console do browser no dev server; `[data-slot="drawer-popup"]` ausente após o clique; `role="dialog"` ausente.
## Root cause (teorizado)
O footer custom do `CampaignCellEditOverlay` variant `sheet` com `CampaignListSheetProvider` é renderizado na árvore da **célula** (`MunicipalityListUpdateControl` — sem `Drawer.Root` ancestral) e enviado via `createPortal(footer, footerTarget)` para o `DrawerFooter` do shared-sheet. `createPortal` **preserva o contexto React da origem** (a célula), não o do alvo — então o `DrawerCloseButton` (`DrawerClose` → base-ui `DialogClose`) dentro do footer custom resolve `useDialogRootContext()` como `undefined` → crash.
O fallback sem provider (um `Drawer` por célula) renderiza o footer **dentro** do `DrawerContent` — contexto OK; é por isso que o unit test B32+ F5 (`campaignCellEditOverlay.unit.spec.ts`) passa e o shared path quebra.
## Abordagem recomendada
**Opções:** A) renderizar o footer custom no host (não portalado da célula) — o `CampaignListSheetHost` ganha um slot de footer por estado (ex.: contexto com `footer: ReactNode` setado via `openSheet`, como já faz com `chrome.title/description`), e a célula para de portalar o footer | B) envolver o footer portalado num `Drawer`-provider wrapper (hack) | C) mover `DrawerCloseButton` para fora do footer custom (degradar o affordance de fechar).
**Recomendação:** A — o footer vira parte do `chrome` do shared-sheet (mesmo mecanismo do título/descrição); a célula envia o footer como valor no `openSheet` e o host renderiza dentro do `DrawerFooter` (contexto correto). O fallback sem provider continua como está. Alinha com o design existente (`hasCustomFooter` já existe no chrome) e remove o portal do footer por completo.
**Rejeitadas:** B porque mascara o problema e adiciona contexto duplicado; C porque remove o botão "Cancelar" do rodapé (regressão de affordance — B32 decidiu footer custom com submit).
### Componentes / mudanças
- **`CampaignListSheetHost.tsx`**: `CampaignListSheetChrome` ganha `footer?: ReactNode`; `openSheet` aceita o footer; `DrawerFooter` renderiza `chrome.footer` (além do `DrawerCloseButton` default quando `!hasCustomFooter`); o ref do portal de footer pode ser removido ou mantido vazio (verificar call sites).
- **`CampaignCellEditOverlay.tsx`**: para de portalar o footer no shared path — passa `footer` no `openSheet({ ... footer })`; mantém o portal/path de fallback.
- **Call sites com footer custom**: `MunicipalityListUpdateControl` (B42) e `MunicipalityListLevelControl` (E14 "Registrar movimento") — sem mudança de props (o footer continua sendo passado como prop do overlay).
- **Sem migration / access / Consent.**
## Fases verificáveis
1. **Root cause confirmado no browser** — reproduzir com o fix mínimo; verificar que `DialogClose` resolve o contexto.
2. **Fix no host/overlay** — mover o footer para o chrome; remover portal.
3. **Gates** — `pnpm gate:fast`; verificação manual no browser mobile (abrir quick-edit sheet de município, registrar, fechar); e2e do `campaignMunicipalities` (popover intocado) + unit `campaignCellEditOverlay`.
4. **Cobertura nova (opcional mas recomendada)**: um unit test do shared path (renderizar `CampaignListSheetHost` + célula) para o footer custom — hoje o bug não é coberto por nenhum teste.
## Rabbit holes / Não escopo
- Redesenhar o sheet/footer dos quick-edits (o fix restaura o comportamento existente, sem redesenho).
- Outros drawers/shared-sheets (agenda, convites) — fora.
- O crash afeta TODOS os footers custom no shared path — verificar os call sites antes de fechar.
## Riscos e mitigação
- **E14 "Registrar movimento"** usa o mesmo shared path com footer custom — o fix cobre os dois; verificar ambos no browser.
- **Remover o portal pode quebrar o `portalRevision`/timing** do host — manter o `portalRevision` para o body; testar reabertura.
- **`chrome.footer` como ReactNode** pode carregar contexto da célula (ex.: `useActionState` do submit) — o footer continua sendo CRIADO na célula (só renderizado no host), então o contexto de estado do form é preservado; o que muda é apenas o `Drawer`-provider, que passa a estar presente.
## Aceite de engenharia
- [ ] Quick-edit sheet abre no mobile sem console error e registra (verificado no browser)
- [ ] Unit test do shared path cobre o footer custom (novo) ou fallback documentado
- [ ] Popover desktop e fallback sem provider intactos; e2e/unit verdes
## Já resolvido no simplify/critique (não reabrir)
- N/A (defect detectado na verificação manual do C107, fora do escopo daquela entrega).
## Explicitamente fora (deste triage)
- Swipe handle no topo quando o sheet atinge `dvh` (C107, defer com gatilho: item PWA/fullscreen).
- Estilo `accent` do ToggleGroup vs precedentes (C107, defer com gatilho: 3º uso).
- Wizard `/acoes/registrar-atualizacao` com select vs toggle (C107, defer com gatilho: consistência pedida pela mesa).