Fix drawer snap geometry: handle at bottom, scroll on content ref

- Reorder drawer so handle sits at the screen bottom (snap shows bottom band)
- Hide dock content when collapsed instead of clipping through search
- Attach scroll-collapse listener directly on campaign-content-scroll ref
This commit is contained in:
Cursor Agent 2026-08-01 11:35:09 +00:00
parent 923a0eb885
commit a056616d4a
No known key found for this signature in database
6 changed files with 50 additions and 59 deletions

View file

@ -8,10 +8,7 @@ import {
CampaignQuickActionsHost,
useQuickActionsChromeActive,
} from '@/components/campaign/shell/CampaignQuickActionsHost'
import {
CampaignQuickActionsScrollCollapse,
CampaignQuickActionsSnapProvider,
} from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
import { CampaignQuickActionsSnapProvider } from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
import type { CampaignRole } from '@/lib/campaignRoles'
export const CampaignAppScrollChrome = ({
@ -30,7 +27,6 @@ export const CampaignAppScrollChrome = ({
return (
<CampaignQuickActionsSnapProvider>
<CampaignGlobalSearchProvider>
<CampaignQuickActionsScrollCollapse />
<CampaignContentScroll quickActionsPeek>{children}</CampaignContentScroll>
<CampaignQuickActionsHost role={role} />
</CampaignGlobalSearchProvider>

View file

@ -14,6 +14,7 @@ import {
type QuickActionsSnapPoint,
} from '@/lib/campaignQuickActionSnap'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import { cn } from '@/lib/utils'
const SNAP_POINTS = [...QUICK_ACTIONS_SNAP_POINTS]
@ -67,20 +68,14 @@ export const CampaignQuickActionsDrawer = ({
className="border-t border-border bg-background text-foreground shadow-[0_-4px_24px_-8px_rgb(0_0_0/0.12)] print:hidden"
>
<DrawerTitle className="sr-only">Ações rápidas</DrawerTitle>
<button
type="button"
onClick={toggleSnap}
aria-expanded={isDock}
aria-controls="quickActionContext"
className="flex w-full shrink-0 cursor-grab flex-col items-center border-0 bg-transparent px-4 pt-2 pb-1 active:cursor-grabbing"
aria-label={isDock ? 'Ocultar ações rápidas' : 'Mostrar ações rápidas'}
>
<span aria-hidden className="mb-1 block h-1 w-12 rounded-full bg-muted" />
</button>
<div
id="quickActionContext"
className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain px-4 pb-[max(1rem,env(safe-area-inset-bottom,0px))]"
className={cn(
'flex min-h-0 flex-col gap-4 overflow-hidden px-4 pt-2',
isDock ? 'flex-1' : 'hidden',
)}
data-snap={isDock ? 'dock' : 'collapsed'}
aria-hidden={!isDock}
>
{showActions ? (
<CampaignHomeActionStrip
@ -96,6 +91,16 @@ export const CampaignQuickActionsDrawer = ({
) : null}
<CampaignGlobalSearchBody />
</div>
<button
type="button"
onClick={toggleSnap}
aria-expanded={isDock}
aria-controls="quickActionContext"
className="flex w-full shrink-0 cursor-grab flex-col items-center border-0 bg-transparent px-4 pt-2 pb-[max(0.25rem,env(safe-area-inset-bottom,0px))] active:cursor-grabbing"
aria-label={isDock ? 'Ocultar ações rápidas' : 'Mostrar ações rápidas'}
>
<span aria-hidden className="mb-1 block h-1 w-12 rounded-full bg-muted" />
</button>
</DrawerContent>
</Drawer>
)

View file

@ -1,8 +1,9 @@
'use client'
import { usePathname } from 'next/navigation'
import type { ReactNode } from 'react'
import { useCallback, useEffect, useRef, type ReactNode } from 'react'
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
import { useCampaignQuickActionContext } from '@/components/campaign/shell/CampaignQuickActionContext'
import { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer'
import { useCampaignQuickActionsSnap } from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
@ -10,6 +11,7 @@ import { useIsMobile } from '@/hooks/use-mobile'
import { shouldMountQuickActionsDrawer } from '@/lib/campaignQuickActionMount'
import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry'
import {
QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
QUICK_ACTIONS_SNAP_COLLAPSED,
QUICK_ACTIONS_SNAP_DOCK,
} from '@/lib/campaignQuickActionSnap'
@ -17,11 +19,35 @@ import type { CampaignRole } from '@/lib/campaignRoles'
import { cn } from '@/lib/utils'
const CampaignContentScrollWithPeek = ({ children }: { children: ReactNode }) => {
const { isDock } = useCampaignQuickActionsSnap()
const scrollRef = useRef<HTMLDivElement>(null)
const { isDock, setSnapPoint } = useCampaignQuickActionsSnap()
const { uiFocused } = useHomeSearch()
const peekHeight = isDock ? QUICK_ACTIONS_SNAP_DOCK : QUICK_ACTIONS_SNAP_COLLAPSED
const collapseIfScrolled = useCallback(() => {
if (uiFocused) return
const scrollport = scrollRef.current
if (!scrollport) return
if (scrollport.scrollTop > QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX) {
setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED)
}
}, [setSnapPoint, uiFocused])
useEffect(() => {
const scrollport = scrollRef.current
if (!scrollport) return
scrollport.addEventListener('scroll', collapseIfScrolled, { passive: true })
return () => {
scrollport.removeEventListener('scroll', collapseIfScrolled)
}
}, [collapseIfScrolled])
return (
<div
ref={scrollRef}
data-slot="campaign-content-scroll"
className={cn(
'min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 md:p-6 print:h-auto print:overflow-visible print:p-0',

View file

@ -3,7 +3,6 @@
import { usePathname } from 'next/navigation'
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
@ -13,10 +12,7 @@ import {
type SetStateAction,
} from 'react'
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
import {
QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
QUICK_ACTIONS_SNAP_COLLAPSED,
QUICK_ACTIONS_SNAP_DOCK,
quickActionsSnapIsDock,
type QuickActionsSnapPoint,
@ -63,32 +59,3 @@ export const useCampaignQuickActionsSnap = (): CampaignQuickActionsSnapContextVa
}
return value
}
/** Collapses the dock when the main content scrollport moves down past the threshold. */
export const CampaignQuickActionsScrollCollapse = () => {
const { setSnapPoint } = useCampaignQuickActionsSnap()
const { uiFocused } = useHomeSearch()
const collapseIfScrolled = useCallback(() => {
if (uiFocused) return
const scrollport = document.querySelector('[data-slot="campaign-content-scroll"]')
if (!(scrollport instanceof HTMLElement)) return
if (scrollport.scrollTop > QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX) {
setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED)
}
}, [setSnapPoint, uiFocused])
useEffect(() => {
const scrollport = document.querySelector('[data-slot="campaign-content-scroll"]')
if (!(scrollport instanceof HTMLElement)) return
scrollport.addEventListener('scroll', collapseIfScrolled, { passive: true })
return () => {
scrollport.removeEventListener('scroll', collapseIfScrolled)
}
}, [collapseIfScrolled])
return null
}

View file

@ -437,8 +437,8 @@ test.describe('Municípios — bottom drawer mobile (B100)', () => {
el.dispatchEvent(new Event('scroll'))
})
await expect(page.getByRole('button', { name: 'Mostrar ações rápidas' })).toBeVisible()
await expect(search).not.toBeInViewport()
await expect(page.getByRole('button', { name: 'Mostrar ações rápidas' })).toBeInViewport()
await scrollport.evaluate((el) => {
el.scrollTop = 0

View file

@ -6,7 +6,6 @@ import { CampaignGlobalSearchProvider } from '@/components/campaign/dashboard/Ca
import { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer'
import { CampaignContentScroll } from '@/components/campaign/shell/CampaignQuickActionsHost'
import {
CampaignQuickActionsScrollCollapse,
CampaignQuickActionsSnapProvider,
useCampaignQuickActionsSnap,
} from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
@ -41,10 +40,7 @@ const idleSuggestPayload = {
const renderQuickActionsChrome = (ui: ReactNode) =>
render(
<CampaignQuickActionsSnapProvider>
<CampaignGlobalSearchProvider>
<CampaignQuickActionsScrollCollapse />
{ui}
</CampaignGlobalSearchProvider>
<CampaignGlobalSearchProvider>{ui}</CampaignGlobalSearchProvider>
</CampaignQuickActionsSnapProvider>,
)
@ -68,7 +64,7 @@ describe('CampaignQuickActionsDrawer (B100)', () => {
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
const context = document.getElementById('quickActionContext')
expect(context?.className).not.toContain('hidden')
expect(context?.className.split(/\s+/)).not.toContain('hidden')
expect(context?.getAttribute('data-snap')).toBe('dock')
expect(screen.getByLabelText('Buscar na campanha')).toBeTruthy()
expect(screen.getByRole('region', { name: 'Resultados da busca' })).toBeTruthy()
@ -106,6 +102,7 @@ describe('CampaignQuickActionsDrawer (B100)', () => {
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe(
'collapsed',
)
expect(document.getElementById('quickActionContext')?.className).toContain('hidden')
expect(screen.getByRole('button', { name: 'Mostrar ações rápidas' })).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Mostrar ações rápidas' }))
@ -113,7 +110,7 @@ describe('CampaignQuickActionsDrawer (B100)', () => {
})
})
describe('CampaignQuickActionsScrollCollapse (B100)', () => {
describe('CampaignContentScroll quick-actions collapse (B100)', () => {
it('collapses on content scroll down but not when scrolling back to top', () => {
renderQuickActionsChrome(
<>