From 23e71d2de325a1943b6f111c1fdbab8a72f78103 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 23:03:04 +0000 Subject: [PATCH 1/5] =?UTF-8?q?B146=20=E2=80=94=20FAB=20overlay=20idle=20o?= =?UTF-8?q?n=20open;=20close/handle=20chrome=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Block Dialog onOpenAutoFocus and Drawer initialFocus so search does not steal focus on open (actions stay visible per B132 idle state). - Reserve md:pr-10 on search chrome so desktop close button does not overlap. - Enable showSwipeHandle on mobile drawer (no X button). - Unit tests for idle state, swipe handle, and close spacing. Co-authored-by: Francisco Solla --- ...fab-overlay-sem-autofocus-e-fechar-impl.md | 73 +++++++++++++++++++ .../shell/CampaignQuickActionsOverlay.tsx | 9 ++- .../campaignQuickActionsDrawer.unit.spec.tsx | 61 ++++++++++++++++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md diff --git a/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md b/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md new file mode 100644 index 00000000..8bef33a4 --- /dev/null +++ b/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md @@ -0,0 +1,73 @@ +# Impl: FAB — overlay abre sem foco na busca; fechar não cobre o campo + +Status: aprovado +Atualizado em: 2026-08-02 +Issue: #317 +Intenção: docs/plans/fab-overlay-sem-autofocus-e-fechar.md +Appetite restante: herdado (~0,5 dia eng) + +## Leitura da intenção + +- **Outcome:** Ao abrir o FAB, o overlay inicia em estado idle — ações visíveis, busca sem foco. Desktop: X não sobrepõe a busca. Mobile: handle de swipe, sem X. +- **O que NÃO negociar:** Ritual B132 (foco deliberado na busca ainda recolhe ações); catálogo/lockdown/limpeza de query inalterados; sem migration/Consent/RBAC. +- **O que reavaliar:** A hipótese de `autoFocus` explícito no input estava errada — o foco vem do trap de foco padrão do Dialog (Radix `onOpenAutoFocus`) e do Drawer (base-ui `initialFocus` default). + +## Abordagem recomendada + +```mermaid +flowchart LR + FAB[FAB click] --> Open[Overlay opens] + Open --> NoFocus[Block open autofocus] + NoFocus --> Idle[Idle: actions visible] + Idle -->|user taps search| Retract[B132 retract] + Open --> Desktop[md+: pr on search + default X] + Open --> Mobile[mobile: showSwipeHandle, no X] +``` + +**Opções consideradas:** + +| Opção | Descrição | Veredito | +| ----- | --------- | -------- | +| A | `onOpenAutoFocus` / `initialFocus={false}` no overlay + `md:pr-10` na busca + `showSwipeHandle` no drawer | **Recomendada** — mínima, escopada ao dono | +| B | Header dedicado com X em flow no dialog | Rejeitada — mais chrome, fora do appetite | +| C | `showCloseButton={false}` + close custom em todo dialog kit | Rejeitada — polui kit global | + +**Recomendação:** A — três props no `CampaignQuickActionsOverlay`, sem alterar `dialog.tsx`/`Drawer.tsx` globalmente. + +### Componentes / mudanças + +- **`CampaignQuickActionsOverlay`** (`src/components/campaign/shell/CampaignQuickActionsOverlay.tsx`): + - Dialog: `onOpenAutoFocus={(e) => e.preventDefault()}` em `DialogContent` + - Dialog: `md:pr-10` em `OverlaySearchChrome` (reserva faixa para X `right-3 size-8`) + - Drawer: `showSwipeHandle` no `Drawer`; `initialFocus={false}` em `DrawerContent` +- **Migration:** sem migration +- **Access / Consent:** N/A +- **UI:** Impeccable B — polimento do chrome existente + +### Dados → forma + +N/A + +## Fases verificáveis + +1. **UI** — props de foco + padding + handle; testes unitários no spec existente +2. **Gates** — `pnpm gate:fast`; push via `pnpm push` + +## Rabbit holes / Não escopo (engenharia) + +- Alterar `DialogContent` default global +- Desligar recolhimento B132 ao focar +- Redesenhar grade ou catálogo + +## Riscos e mitigação + +| Risco | Mitigação | +| ----- | --------- | +| `initialFocus={false}` quebra a11y teclado | Tab ainda alcança busca/ações; trap permanece | +| `pr-10` insuficiente em telas estreitas | `md:` só no breakpoint dialog; mobile usa drawer | + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto +- [x] Invariantes AGENTS/engineering-standards +- [x] Testes unitários previstos em `campaignQuickActionsDrawer.unit.spec.tsx` diff --git a/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx b/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx index 891238a8..348ed427 100644 --- a/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx +++ b/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx @@ -63,7 +63,10 @@ const OverlayActionsChrome = ({ } const OverlaySearchChrome = ({ className }: { className?: string }) => ( -
+
) @@ -121,9 +124,10 @@ export const CampaignQuickActionsOverlay = ({ if (isMobile) { return ( - + Ações rápidas @@ -143,6 +147,7 @@ export const CampaignQuickActionsOverlay = ({ event.preventDefault()} > Ações rápidas diff --git a/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx b/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx index 03fa1dde..6d36a643 100644 --- a/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx +++ b/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx @@ -331,6 +331,67 @@ describe('CampaignQuickActionsOverlay (B126)', () => { expect((screen.getByLabelText('Buscar na campanha') as HTMLInputElement).value).toBe('') }) + it('opens idle without autofocus and keeps actions visible (B146)', () => { + vi.mocked(postCampaignJson).mockResolvedValue({ + ok: true, + payload: idleSuggestPayload, + }) + + renderOverlay({ + actions: [ + { + id: 'test', + label: 'Registrar', + icon: BarChart3, + description: 'Teste.', + href: '/campanha', + }, + ], + }) + + const input = screen.getByLabelText('Buscar na campanha') + const actionsChrome = document.querySelector('[data-slot="quick-actions-chrome"]') + + expect(document.activeElement).not.toBe(input) + expect(actionsChrome?.getAttribute('data-retracted')).toBeNull() + expect(actionsChrome?.className).not.toContain('grid-rows-[0fr]') + }) + + it('shows swipe handle and no dialog close on mobile (B146)', () => { + matchMediaMock.mockImplementation((query: string) => ({ + matches: query.includes('max-width'), + media: query, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn(), + })) + + vi.mocked(postCampaignJson).mockResolvedValue({ + ok: true, + payload: idleSuggestPayload, + }) + + renderOverlay() + + expect(document.querySelector('[data-slot="drawer-swipe-handle"]')).not.toBeNull() + expect(screen.queryByRole('button', { name: 'Fechar' })).toBeNull() + }) + + it('reserves space for close button beside search on desktop (B146)', () => { + vi.mocked(postCampaignJson).mockResolvedValue({ + ok: true, + payload: idleSuggestPayload, + }) + + renderOverlay() + + const searchChrome = document.querySelector('[data-slot="quick-actions-search"]') + expect(searchChrome?.className).toContain('md:pr-10') + expect(screen.getByRole('button', { name: 'Fechar' })).toBeTruthy() + }) + it('uses mobile drawer with actions above search', () => { matchMediaMock.mockImplementation((query: string) => ({ matches: query.includes('max-width'), From 56e60ce589e2dac66975f86f4bdd640b1b70e7d2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 23:04:32 +0000 Subject: [PATCH 2/5] =?UTF-8?q?B146=20=E2=80=94=20stub=20innerWidth=20in?= =?UTF-8?q?=20mobile=20overlay=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Solla --- tests/unit/campaignQuickActionsDrawer.unit.spec.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx b/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx index 6d36a643..773528d6 100644 --- a/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx +++ b/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx @@ -54,6 +54,12 @@ const searchWithPriorityPayload: HomeSearchSuccessResponse = { const matchMediaMock = vi.fn() +let innerWidthSpy: ReturnType | undefined + +const stubMobileViewport = () => { + innerWidthSpy = vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(375) +} + beforeEach(() => { matchMediaMock.mockImplementation((query: string) => ({ matches: false, @@ -69,6 +75,8 @@ beforeEach(() => { afterEach(() => { cleanup() + innerWidthSpy?.mockRestore() + innerWidthSpy = undefined vi.mocked(postCampaignJson).mockReset() vi.unstubAllGlobals() }) @@ -358,6 +366,7 @@ describe('CampaignQuickActionsOverlay (B126)', () => { }) it('shows swipe handle and no dialog close on mobile (B146)', () => { + stubMobileViewport() matchMediaMock.mockImplementation((query: string) => ({ matches: query.includes('max-width'), media: query, @@ -393,6 +402,7 @@ describe('CampaignQuickActionsOverlay (B126)', () => { }) it('uses mobile drawer with actions above search', () => { + stubMobileViewport() matchMediaMock.mockImplementation((query: string) => ({ matches: query.includes('max-width'), media: query, From fced1f3e1d1aa5ba7fe6bd1882b2541b431ee7ba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 23:07:29 +0000 Subject: [PATCH 3/5] =?UTF-8?q?B146=20=E2=80=94=20focus=20dialog=20title?= =?UTF-8?q?=20on=20open=20(B42=20pattern);=20consolidate=20mobile=20test?= =?UTF-8?q?=20stub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move initial focus to sr-only title instead of void/body (a11y). - Fold matchMedia mock into stubMobileViewport helper. Co-authored-by: Francisco Solla --- .../shell/CampaignQuickActionsOverlay.tsx | 18 ++++++++---- .../campaignQuickActionsDrawer.unit.spec.tsx | 29 +++++++------------ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx b/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx index 348ed427..4ede26fd 100644 --- a/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx +++ b/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx @@ -1,7 +1,7 @@ 'use client' import { usePathname } from 'next/navigation' -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { CampaignGlobalSearchBody } from '@/components/campaign/dashboard/CampaignGlobalSearchMount' import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/CampaignHomeActionStrip' @@ -117,6 +117,7 @@ export const CampaignQuickActionsOverlay = ({ }) => { const isMobile = useIsMobile() const { clear } = useHomeSearch() + const titleRef = useRef(null) useEffect(() => { if (!open) clear() @@ -127,10 +128,12 @@ export const CampaignQuickActionsOverlay = ({ - Ações rápidas + + Ações rápidas + Ações do contexto e busca na campanha @@ -147,10 +150,15 @@ export const CampaignQuickActionsOverlay = ({ event.preventDefault()} + onOpenAutoFocus={(event) => { + event.preventDefault() + titleRef.current?.focus() + }} > - Ações rápidas + + Ações rápidas + Ações do contexto e busca na campanha
diff --git a/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx b/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx index 773528d6..160bf305 100644 --- a/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx +++ b/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx @@ -58,6 +58,15 @@ let innerWidthSpy: ReturnType | undefined const stubMobileViewport = () => { innerWidthSpy = vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(375) + matchMediaMock.mockImplementation((query: string) => ({ + matches: query.includes('max-width'), + media: query, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn(), + })) } beforeEach(() => { @@ -359,23 +368,16 @@ describe('CampaignQuickActionsOverlay (B126)', () => { const input = screen.getByLabelText('Buscar na campanha') const actionsChrome = document.querySelector('[data-slot="quick-actions-chrome"]') + const title = screen.getByText('Ações rápidas') expect(document.activeElement).not.toBe(input) + expect(document.activeElement).toBe(title) expect(actionsChrome?.getAttribute('data-retracted')).toBeNull() expect(actionsChrome?.className).not.toContain('grid-rows-[0fr]') }) it('shows swipe handle and no dialog close on mobile (B146)', () => { stubMobileViewport() - matchMediaMock.mockImplementation((query: string) => ({ - matches: query.includes('max-width'), - media: query, - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - addListener: vi.fn(), - removeListener: vi.fn(), - dispatchEvent: vi.fn(), - })) vi.mocked(postCampaignJson).mockResolvedValue({ ok: true, @@ -403,15 +405,6 @@ describe('CampaignQuickActionsOverlay (B126)', () => { it('uses mobile drawer with actions above search', () => { stubMobileViewport() - matchMediaMock.mockImplementation((query: string) => ({ - matches: query.includes('max-width'), - media: query, - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - addListener: vi.fn(), - removeListener: vi.fn(), - dispatchEvent: vi.fn(), - })) vi.mocked(postCampaignJson).mockResolvedValue({ ok: true, From f17f4583b3356baa123ca02e6412a730d9e4a8ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 23:07:33 +0000 Subject: [PATCH 4/5] =?UTF-8?q?B146=20=E2=80=94=20update=20impl=20plan=20w?= =?UTF-8?q?ith=20title-focus=20approach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Solla --- docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md b/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md index 8bef33a4..230c2e73 100644 --- a/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md +++ b/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md @@ -59,11 +59,13 @@ N/A - Desligar recolhimento B132 ao focar - Redesenhar grade ou catálogo +**Defer (gatilho):** extrair `HomeChromeRetractionShell` compartilhado com `CampaignHomeLayout` quando um 3º chrome retrátil for tocado de novo (já é o 3º — `quick-actions-chrome`; extrair em issue futura, fora do appetite B146). + ## Riscos e mitigação | Risco | Mitigação | | ----- | --------- | -| `initialFocus={false}` quebra a11y teclado | Tab ainda alcança busca/ações; trap permanece | +| `initialFocus={false}` quebra a11y teclado | Foco no título sr-only (B42), não na busca nem em void | | `pr-10` insuficiente em telas estreitas | `md:` só no breakpoint dialog; mobile usa drawer | ## Aceite de engenharia From 8437874bd8e0a75610362ce935019f36cd419adb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 23:08:57 +0000 Subject: [PATCH 5/5] =?UTF-8?q?B146=20=E2=80=94=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...fab-overlay-sem-autofocus-e-fechar-impl.md | 20 +++++++++---------- .../shell/CampaignQuickActionsOverlay.tsx | 5 +---- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md b/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md index 230c2e73..50ac5584 100644 --- a/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md +++ b/docs/plans/fab-overlay-sem-autofocus-e-fechar-impl.md @@ -26,20 +26,20 @@ flowchart LR **Opções consideradas:** -| Opção | Descrição | Veredito | -| ----- | --------- | -------- | -| A | `onOpenAutoFocus` / `initialFocus={false}` no overlay + `md:pr-10` na busca + `showSwipeHandle` no drawer | **Recomendada** — mínima, escopada ao dono | -| B | Header dedicado com X em flow no dialog | Rejeitada — mais chrome, fora do appetite | -| C | `showCloseButton={false}` + close custom em todo dialog kit | Rejeitada — polui kit global | +| Opção | Descrição | Veredito | +| ----- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------ | +| A | `onOpenAutoFocus` / `initialFocus={false}` no overlay + `md:pr-10` na busca + `showSwipeHandle` no drawer | **Recomendada** — mínima, escopada ao dono | +| B | Header dedicado com X em flow no dialog | Rejeitada — mais chrome, fora do appetite | +| C | `showCloseButton={false}` + close custom em todo dialog kit | Rejeitada — polui kit global | **Recomendação:** A — três props no `CampaignQuickActionsOverlay`, sem alterar `dialog.tsx`/`Drawer.tsx` globalmente. ### Componentes / mudanças - **`CampaignQuickActionsOverlay`** (`src/components/campaign/shell/CampaignQuickActionsOverlay.tsx`): - - Dialog: `onOpenAutoFocus={(e) => e.preventDefault()}` em `DialogContent` + - Dialog: `onOpenAutoFocus` → `preventDefault` + foco no `DialogTitle` (`titleRef`, padrão B42) - Dialog: `md:pr-10` em `OverlaySearchChrome` (reserva faixa para X `right-3 size-8`) - - Drawer: `showSwipeHandle` no `Drawer`; `initialFocus={false}` em `DrawerContent` + - Drawer: `showSwipeHandle` no `Drawer`; `initialFocus={titleRef}` em `DrawerContent` - **Migration:** sem migration - **Access / Consent:** N/A - **UI:** Impeccable B — polimento do chrome existente @@ -63,10 +63,10 @@ N/A ## Riscos e mitigação -| Risco | Mitigação | -| ----- | --------- | +| Risco | Mitigação | +| ------------------------------------------ | ------------------------------------------------------ | | `initialFocus={false}` quebra a11y teclado | Foco no título sr-only (B42), não na busca nem em void | -| `pr-10` insuficiente em telas estreitas | `md:` só no breakpoint dialog; mobile usa drawer | +| `pr-10` insuficiente em telas estreitas | `md:` só no breakpoint dialog; mobile usa drawer | ## Aceite de engenharia diff --git a/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx b/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx index 4ede26fd..d45689d3 100644 --- a/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx +++ b/src/components/campaign/shell/CampaignQuickActionsOverlay.tsx @@ -63,10 +63,7 @@ const OverlayActionsChrome = ({ } const OverlaySearchChrome = ({ className }: { className?: string }) => ( -
+
)