- 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)
5.9 KiB
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:CampaignListSheetChromeganhafooter?: ReactNode;openSheetaceita o footer;DrawerFooterrenderizachrome.footer(além doDrawerCloseButtondefault 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 — passafooternoopenSheet({ ... footer }); mantém o portal/path de fallback.- Call sites com footer custom:
MunicipalityListUpdateControl(B42) eMunicipalityListLevelControl(E14 "Registrar movimento") — sem mudança de props (o footer continua sendo passado como prop do overlay). - Sem migration / access / Consent.
Fases verificáveis
- Root cause confirmado no browser — reproduzir com o fix mínimo; verificar que
DialogCloseresolve o contexto. - Fix no host/overlay — mover o footer para o chrome; remover portal.
- Gates —
pnpm gate:fast; verificação manual no browser mobile (abrir quick-edit sheet de município, registrar, fechar); e2e docampaignMunicipalities(popover intocado) + unitcampaignCellEditOverlay. - 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 oportalRevisionpara o body; testar reabertura. chrome.footercomo ReactNode pode carregar contexto da célula (ex.:useActionStatedo 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 oDrawer-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
accentdo ToggleGroup vs precedentes (C107, defer com gatilho: 3º uso). - Wizard
/acoes/registrar-atualizacaocom select vs toggle (C107, defer com gatilho: consistência pedida pela mesa).