B146 — focus dialog title on open (B42 pattern); consolidate mobile test stub

- Move initial focus to sr-only title instead of void/body (a11y).
- Fold matchMedia mock into stubMobileViewport helper.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-02 23:07:29 +00:00
parent 56e60ce589
commit fced1f3e1d
No known key found for this signature in database
2 changed files with 24 additions and 23 deletions

View file

@ -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<HTMLHeadingElement>(null)
useEffect(() => {
if (!open) clear()
@ -127,10 +128,12 @@ export const CampaignQuickActionsOverlay = ({
<Drawer open={open} onOpenChange={onOpenChange} showSwipeHandle>
<DrawerContent
id="CampaignQuickActionsOverlay"
initialFocus={false}
initialFocus={titleRef}
className="max-h-[85dvh] border-t border-border bg-background text-foreground [--drawer-height:auto]"
>
<DrawerTitle className="sr-only">Ações rápidas</DrawerTitle>
<DrawerTitle ref={titleRef} tabIndex={-1} className="sr-only outline-none">
Ações rápidas
</DrawerTitle>
<DrawerDescription className="sr-only">
Ações do contexto e busca na campanha
</DrawerDescription>
@ -147,10 +150,15 @@ export const CampaignQuickActionsOverlay = ({
<DialogContent
id="CampaignQuickActionsOverlay"
className="flex max-h-[min(85dvh,40rem)] w-[calc(100vw-2rem)] max-w-lg flex-col gap-4 overflow-hidden p-4 sm:p-6"
onOpenAutoFocus={(event) => event.preventDefault()}
onOpenAutoFocus={(event) => {
event.preventDefault()
titleRef.current?.focus()
}}
>
<DialogHeader className="sr-only">
<DialogTitle>Ações rápidas</DialogTitle>
<DialogTitle ref={titleRef} tabIndex={-1} className="outline-none">
Ações rápidas
</DialogTitle>
<DialogDescription>Ações do contexto e busca na campanha</DialogDescription>
</DialogHeader>
<div className="flex flex-col overflow-y-auto">

View file

@ -58,6 +58,15 @@ let innerWidthSpy: ReturnType<typeof vi.spyOn> | 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,