mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
Merge pull request #139 from fsolla/cursor/pool-i132-dfca371b-fa84
B105 — Bottom drawer — handle, gesto, scroll, peek discreto, labels 2 linhas
This commit is contained in:
commit
7e99bce9a5
9 changed files with 199 additions and 59 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Bottom drawer — handle, gesto, scroll, peek discreto, labels 2 linhas
|
||||
|
||||
Status: registrado
|
||||
Status: implementado (PR)
|
||||
Atualizado em: 2026-08-01
|
||||
Issue: #132
|
||||
Priority: P1
|
||||
|
|
@ -82,8 +82,8 @@ Dados: N/A — chrome; hits = B48+ via provider existente.
|
|||
|
||||
## Questões em aberto
|
||||
|
||||
- **Limiar scroll-up para abrir?** **Opções:** A) espelhar 24 px de delta ↑ | B) `scrollTop < 24` (só perto do topo). **Recomendação:** **A** (simétrico ao collapse). _(assumido)_
|
||||
- **Focus no input collapsed:** expande para dock automaticamente? **Opções:** A) sim | B) só digitar. **Recomendação:** **A**. _(assumido)_
|
||||
- **Limiar scroll-up para abrir?** **Opções:** A) espelhar 24 px de delta ↑ | B) `scrollTop < 24` (só perto do topo). **Recomendação:** **A** (simétrico ao collapse). ✅ travado em craft (`quickActionsScrollDirection`).
|
||||
- **Focus no input collapsed:** expande para dock automaticamente? **Opções:** A) sim | B) só digitar. **Recomendação:** **A**. ✅ travado em craft (`uiFocused` → dock).
|
||||
|
||||
## Abordagem proposta
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,21 @@ import { HOME_SEARCH_QUERY_MAX_LENGTH } from '@/lib/schemas/homeSearch'
|
|||
|
||||
const HOME_SEARCH_INPUT_ID = 'homeSearchQuery'
|
||||
const HOME_SEARCH_LABEL = 'Buscar na campanha'
|
||||
const HOME_SEARCH_PLACEHOLDER = 'Município, liderança, atividade…'
|
||||
|
||||
export const CampaignHomeSearch = ({
|
||||
children,
|
||||
resultsBusy,
|
||||
placeholder = HOME_SEARCH_PLACEHOLDER,
|
||||
showResults = true,
|
||||
}: {
|
||||
children?: ReactNode
|
||||
/** When set, must already fold debounce (see `useHomeSearchResultsState.isFetching`). */
|
||||
resultsBusy?: boolean
|
||||
/** Empty string = discreet peek (collapsed drawer); sr-only label stays. */
|
||||
placeholder?: string
|
||||
/** When false, keeps the live region mounted but visually hidden (collapsed peek). */
|
||||
showResults?: boolean
|
||||
}) => {
|
||||
const { query, setRaw, clear, isDebouncing, setInputFocused } = useHomeSearch()
|
||||
const ariaBusy = (resultsBusy ?? isDebouncing) || undefined
|
||||
|
|
@ -25,7 +32,7 @@ export const CampaignHomeSearch = ({
|
|||
<CampaignSearchInput
|
||||
id={HOME_SEARCH_INPUT_ID}
|
||||
label={HOME_SEARCH_LABEL}
|
||||
placeholder="Município, liderança, atividade…"
|
||||
placeholder={placeholder}
|
||||
value={query.raw}
|
||||
onChange={(event) => setRaw(event.target.value)}
|
||||
onFocus={() => setInputFocused(true)}
|
||||
|
|
@ -52,6 +59,7 @@ export const CampaignHomeSearch = ({
|
|||
aria-busy={ariaBusy}
|
||||
className="min-w-0"
|
||||
data-slot="home-search-results"
|
||||
hidden={!showResults}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,11 +14,22 @@ import { HomeSearchResultsLayout } from '@/components/campaign/dashboard/HomeSea
|
|||
import { HomeSearchResultsShell } from '@/components/campaign/dashboard/HomeSearchResultsShell'
|
||||
import { HomeSearchStateDeputyGroup } from '@/components/campaign/dashboard/HomeSearchStateDeputyGroup'
|
||||
|
||||
const CampaignStaffGlobalSearchResults = () => {
|
||||
export const CampaignStaffGlobalSearchBody = ({
|
||||
placeholder,
|
||||
showResults = true,
|
||||
}: {
|
||||
/** Forwarded to `CampaignHomeSearch` — empty string for collapsed discreet peek. */
|
||||
placeholder?: string
|
||||
showResults?: boolean
|
||||
}) => {
|
||||
const searchResultsState = useHomeSearchResultsState()
|
||||
|
||||
return (
|
||||
<CampaignHomeSearch resultsBusy={searchResultsState.isFetching}>
|
||||
<CampaignHomeSearch
|
||||
resultsBusy={searchResultsState.isFetching}
|
||||
placeholder={placeholder}
|
||||
showResults={showResults}
|
||||
>
|
||||
<HomeSearchResultsProvider value={searchResultsState}>
|
||||
<HomeSearchResultsShell>
|
||||
<HomeSearchResultsLayout>
|
||||
|
|
@ -34,6 +45,3 @@ const CampaignStaffGlobalSearchResults = () => {
|
|||
</CampaignHomeSearch>
|
||||
)
|
||||
}
|
||||
|
||||
/** Result groups + input — requires an ancestor `HomeSearchProvider`. */
|
||||
export const CampaignStaffGlobalSearchBody = CampaignStaffGlobalSearchResults
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
import { CampaignGlobalSearchBody } from '@/components/campaign/dashboard/CampaignGlobalSearchMount'
|
||||
import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/CampaignHomeActionStrip'
|
||||
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
|
||||
import { useCampaignQuickActionsSnap } from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
|
||||
import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/Drawer'
|
||||
import {
|
||||
|
|
@ -24,6 +25,7 @@ export const CampaignQuickActionsDrawer = ({
|
|||
actions: readonly CampaignQuickAction[]
|
||||
}) => {
|
||||
const { snapPoint, setSnapPoint, isDock } = useCampaignQuickActionsSnap()
|
||||
const { uiFocused } = useHomeSearch()
|
||||
const showActions = actions.length > 0
|
||||
|
||||
const handleSnapPointChange = useCallback(
|
||||
|
|
@ -52,6 +54,13 @@ export const CampaignQuickActionsDrawer = ({
|
|||
[setSnapPoint],
|
||||
)
|
||||
|
||||
// Active search focus keeps the drawer docked (B105) — same gate as scroll.
|
||||
useEffect(() => {
|
||||
if (uiFocused && !isDock) {
|
||||
setSnapPoint(QUICK_ACTIONS_SNAP_DOCK)
|
||||
}
|
||||
}, [isDock, setSnapPoint, uiFocused])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open
|
||||
|
|
@ -68,17 +77,26 @@ 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={cn(
|
||||
'flex min-h-0 flex-col gap-4 overflow-hidden px-4 pt-2',
|
||||
isDock ? 'flex-1' : 'hidden',
|
||||
'flex min-h-0 flex-col gap-3 overflow-y-auto px-4 pb-[max(0.5rem,env(safe-area-inset-bottom,0px))]',
|
||||
isDock ? 'flex-1' : 'shrink-0',
|
||||
)}
|
||||
data-snap={isDock ? 'dock' : 'collapsed'}
|
||||
aria-hidden={!isDock}
|
||||
>
|
||||
{showActions ? (
|
||||
<div className="-mx-4 w-[calc(100%+2rem)]">
|
||||
<div hidden={!isDock} className="-mx-4 w-[calc(100%+2rem)]">
|
||||
<CampaignHomeActionStrip
|
||||
actions={actions.map((action) => ({
|
||||
id: action.id,
|
||||
|
|
@ -91,18 +109,8 @@ export const CampaignQuickActionsDrawer = ({
|
|||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<CampaignGlobalSearchBody />
|
||||
<CampaignGlobalSearchBody placeholder={isDock ? undefined : ''} showResults={isDock} />
|
||||
</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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,27 +11,37 @@ 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,
|
||||
quickActionsScrollDirection,
|
||||
} from '@/lib/campaignQuickActionSnap'
|
||||
import type { CampaignRole } from '@/lib/campaignRoles'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const CampaignContentScrollWithPeek = ({ children }: { children: ReactNode }) => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const lastScrollTopRef = useRef(0)
|
||||
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 syncSnapFromScroll = useCallback(() => {
|
||||
const scrollport = scrollRef.current
|
||||
if (!scrollport) return
|
||||
|
||||
if (scrollport.scrollTop > QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX) {
|
||||
const nextTop = scrollport.scrollTop
|
||||
const direction = quickActionsScrollDirection(lastScrollTopRef.current, nextTop)
|
||||
lastScrollTopRef.current = nextTop
|
||||
|
||||
// Keep the scroll baseline fresh while search is focused; only gate snap writes.
|
||||
if (uiFocused) return
|
||||
|
||||
if (direction === 'down') {
|
||||
setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED)
|
||||
return
|
||||
}
|
||||
if (direction === 'up') {
|
||||
setSnapPoint(QUICK_ACTIONS_SNAP_DOCK)
|
||||
}
|
||||
}, [setSnapPoint, uiFocused])
|
||||
|
||||
|
|
@ -39,11 +49,12 @@ const CampaignContentScrollWithPeek = ({ children }: { children: ReactNode }) =>
|
|||
const scrollport = scrollRef.current
|
||||
if (!scrollport) return
|
||||
|
||||
scrollport.addEventListener('scroll', collapseIfScrolled, { passive: true })
|
||||
lastScrollTopRef.current = scrollport.scrollTop
|
||||
scrollport.addEventListener('scroll', syncSnapFromScroll, { passive: true })
|
||||
return () => {
|
||||
scrollport.removeEventListener('scroll', collapseIfScrolled)
|
||||
scrollport.removeEventListener('scroll', syncSnapFromScroll)
|
||||
}
|
||||
}, [collapseIfScrolled])
|
||||
}, [syncSnapFromScroll])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
/** Collapsed peek — swipe handle only (replaces legacy bottom-nav `pb-24`). */
|
||||
export const QUICK_ACTIONS_SNAP_COLLAPSED = '3rem' as const
|
||||
/** Collapsed peek — handle + discreet search (no action strip). */
|
||||
export const QUICK_ACTIONS_SNAP_COLLAPSED = '6.5rem' as const
|
||||
|
||||
/** Dock — action strip + global search visible on load and after navigation. */
|
||||
export const QUICK_ACTIONS_SNAP_DOCK = '12rem' as const
|
||||
/** Dock — handle + action strip (2-line labels) + global search. */
|
||||
export const QUICK_ACTIONS_SNAP_DOCK = '15rem' as const
|
||||
|
||||
/** Scroll down past this threshold on `campaign-content-scroll` collapses the drawer. */
|
||||
/**
|
||||
* Scroll delta past this threshold on `campaign-content-scroll` collapses (↓)
|
||||
* or re-docks (↑) the drawer.
|
||||
*/
|
||||
export const QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX = 24
|
||||
|
||||
export const QUICK_ACTIONS_SNAP_POINTS = [
|
||||
|
|
@ -14,5 +17,19 @@ export const QUICK_ACTIONS_SNAP_POINTS = [
|
|||
|
||||
export type QuickActionsSnapPoint = (typeof QUICK_ACTIONS_SNAP_POINTS)[number]
|
||||
|
||||
export type QuickActionsScrollDirection = 'up' | 'down' | 'none'
|
||||
|
||||
export const quickActionsSnapIsDock = (snap: QuickActionsSnapPoint | null): boolean =>
|
||||
snap === QUICK_ACTIONS_SNAP_DOCK
|
||||
|
||||
/** Pure scroll-direction detector for the quick-actions peek (B105). */
|
||||
export const quickActionsScrollDirection = (
|
||||
previousScrollTop: number,
|
||||
nextScrollTop: number,
|
||||
thresholdPx: number = QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
|
||||
): QuickActionsScrollDirection => {
|
||||
const delta = nextScrollTop - previousScrollTop
|
||||
if (delta > thresholdPx) return 'down'
|
||||
if (delta < -thresholdPx) return 'up'
|
||||
return 'none'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -408,13 +408,13 @@ test.describe('Municípios — cards no celular (B42)', () => {
|
|||
})
|
||||
|
||||
/**
|
||||
* B100: bottom drawer loads docked with search visible, collapses on page scroll,
|
||||
* and reopens only from the handle — not when scrolling back to the top.
|
||||
* B105: bottom drawer loads docked with search visible, collapses on page scroll down
|
||||
* (discreet search peek remains), and re-docks on scroll up; handle sits above the search.
|
||||
*/
|
||||
test.describe('Municípios — bottom drawer mobile (B100)', () => {
|
||||
test.describe('Municípios — bottom drawer mobile (B105)', () => {
|
||||
test.use({ viewport: { width: 390, height: 844 } })
|
||||
|
||||
test('dock on load, collapse on scroll down, reopen on handle tap', async ({
|
||||
test('dock on load, collapse on scroll down with search peek, reopen on scroll up', async ({
|
||||
campaign,
|
||||
page,
|
||||
}) => {
|
||||
|
|
@ -430,6 +430,14 @@ test.describe('Municípios — bottom drawer mobile (B100)', () => {
|
|||
|
||||
const search = page.getByLabel('Buscar na campanha')
|
||||
await expect(search).toBeInViewport()
|
||||
await expect(search).toHaveAttribute('placeholder', 'Município, liderança, atividade…')
|
||||
|
||||
const handle = page.getByRole('button', { name: 'Ocultar ações rápidas' })
|
||||
const handleBox = await handle.boundingBox()
|
||||
const searchBox = await search.boundingBox()
|
||||
expect(handleBox).toBeTruthy()
|
||||
expect(searchBox).toBeTruthy()
|
||||
expect(handleBox!.y).toBeLessThan(searchBox!.y)
|
||||
|
||||
const scrollport = page.locator('[data-slot="campaign-content-scroll"]')
|
||||
await scrollport.evaluate((el) => {
|
||||
|
|
@ -438,15 +446,15 @@ test.describe('Municípios — bottom drawer mobile (B100)', () => {
|
|||
})
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Mostrar ações rápidas' })).toBeVisible()
|
||||
await expect(search).not.toBeInViewport()
|
||||
await expect(search).toBeInViewport()
|
||||
await expect(search).toHaveAttribute('placeholder', '')
|
||||
|
||||
await scrollport.evaluate((el) => {
|
||||
el.scrollTop = 0
|
||||
el.dispatchEvent(new Event('scroll'))
|
||||
})
|
||||
await expect(search).not.toBeInViewport()
|
||||
|
||||
await page.getByRole('button', { name: 'Mostrar ações rápidas' }).click()
|
||||
await expect(page.getByRole('button', { name: 'Ocultar ações rápidas' })).toBeVisible()
|
||||
await expect(search).toBeInViewport()
|
||||
await expect(search).toHaveAttribute('placeholder', 'Município, liderança, atividade…')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch'
|
||||
import { HomeSearchProvider } from '@/components/campaign/dashboard/HomeSearchContext'
|
||||
|
|
@ -14,6 +14,10 @@ const providerValue = {
|
|||
uiFocused: true,
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe('CampaignHomeSearch', () => {
|
||||
it('renders search input and results region', () => {
|
||||
render(
|
||||
|
|
@ -25,7 +29,25 @@ describe('CampaignHomeSearch', () => {
|
|||
)
|
||||
|
||||
expect(screen.getByLabelText('Buscar na campanha')).toBeTruthy()
|
||||
expect((screen.getByLabelText('Buscar na campanha') as HTMLInputElement).placeholder).toBe(
|
||||
'Município, liderança, atividade…',
|
||||
)
|
||||
expect(screen.getByRole('region', { name: 'Resultados da busca' })).toBeTruthy()
|
||||
expect(screen.getByText('Group stub')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('supports discreet peek without placeholder and hidden results', () => {
|
||||
render(
|
||||
<HomeSearchProvider value={providerValue}>
|
||||
<CampaignHomeSearch placeholder="" showResults={false}>
|
||||
<p>Group stub</p>
|
||||
</CampaignHomeSearch>
|
||||
</HomeSearchProvider>,
|
||||
)
|
||||
|
||||
expect((screen.getByLabelText('Buscar na campanha') as HTMLInputElement).placeholder).toBe('')
|
||||
const results = document.querySelector('[data-slot="home-search-results"]')
|
||||
expect(results).toBeTruthy()
|
||||
expect((results as HTMLElement).hidden).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
|
||||
QUICK_ACTIONS_SNAP_COLLAPSED,
|
||||
QUICK_ACTIONS_SNAP_DOCK,
|
||||
quickActionsScrollDirection,
|
||||
quickActionsSnapIsDock,
|
||||
} from '@/lib/campaignQuickActionSnap'
|
||||
|
||||
|
|
@ -99,8 +100,8 @@ const SnapReadout = () => {
|
|||
return <div data-testid="snap">{snapPoint ?? 'null'}</div>
|
||||
}
|
||||
|
||||
describe('CampaignQuickActionsDrawer (B100)', () => {
|
||||
it('loads in dock snap with global search visible', () => {
|
||||
describe('CampaignQuickActionsDrawer (B105)', () => {
|
||||
it('loads in dock snap with handle above search and placeholder visible', () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: idleSuggestPayload,
|
||||
|
|
@ -109,11 +110,35 @@ describe('CampaignQuickActionsDrawer (B100)', () => {
|
|||
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
|
||||
|
||||
const context = document.getElementById('quickActionContext')
|
||||
expect(context?.className.split(/\s+/)).not.toContain('hidden')
|
||||
expect(context?.getAttribute('data-snap')).toBe('dock')
|
||||
expect(screen.getByLabelText('Buscar na campanha')).toBeTruthy()
|
||||
expect(context?.hasAttribute('hidden')).toBe(false)
|
||||
|
||||
const handle = screen.getByRole('button', { name: 'Ocultar ações rápidas' })
|
||||
const search = screen.getByLabelText('Buscar na campanha')
|
||||
expect(handle.compareDocumentPosition(search) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(search.getAttribute('placeholder')).toBe('Município, liderança, atividade…')
|
||||
expect(screen.getByRole('region', { name: 'Resultados da busca' })).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Ocultar ações rápidas' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('collapsed peek keeps discreet search without placeholder; actions stay hidden', () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: idleSuggestPayload,
|
||||
})
|
||||
|
||||
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Ocultar ações rápidas' }))
|
||||
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe(
|
||||
'collapsed',
|
||||
)
|
||||
|
||||
const search = screen.getByLabelText('Buscar na campanha')
|
||||
expect(search.getAttribute('placeholder')).toBe('')
|
||||
const results = document.querySelector('[data-slot="home-search-results"]')
|
||||
expect(results).toBeTruthy()
|
||||
expect((results as HTMLElement).hidden).toBe(true)
|
||||
expect(screen.getByRole('button', { name: 'Mostrar ações rápidas' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('bleeds the action strip edge-to-edge inside the drawer gutter (B101)', () => {
|
||||
|
|
@ -216,6 +241,36 @@ describe('CampaignQuickActionsDrawer (B100)', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('expands to dock when the peek search is focused', () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: idleSuggestPayload,
|
||||
})
|
||||
|
||||
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Ocultar ações rápidas' }))
|
||||
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe(
|
||||
'collapsed',
|
||||
)
|
||||
|
||||
fireEvent.focus(screen.getByLabelText('Buscar na campanha'))
|
||||
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe('dock')
|
||||
})
|
||||
|
||||
it('stays docked while search remains focused even if the handle collapses', () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: idleSuggestPayload,
|
||||
})
|
||||
|
||||
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
|
||||
|
||||
fireEvent.focus(screen.getByLabelText('Buscar na campanha'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Ocultar ações rápidas' }))
|
||||
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe('dock')
|
||||
})
|
||||
|
||||
it('toggles between dock and collapsed from the handle', () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
|
|
@ -228,7 +283,6 @@ 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' }))
|
||||
|
|
@ -236,8 +290,8 @@ describe('CampaignQuickActionsDrawer (B100)', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('CampaignContentScroll quick-actions collapse (B100)', () => {
|
||||
it('collapses on content scroll down but not when scrolling back to top', () => {
|
||||
describe('CampaignContentScroll quick-actions direction (B105)', () => {
|
||||
it('collapses on scroll down and re-docks on scroll up', () => {
|
||||
renderQuickActionsChrome(
|
||||
<>
|
||||
<CampaignContentScroll quickActionsPeek>
|
||||
|
|
@ -256,7 +310,6 @@ describe('CampaignContentScroll quick-actions collapse (B100)', () => {
|
|||
value: QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX + 1,
|
||||
})
|
||||
fireEvent.scroll(scrollport!)
|
||||
|
||||
expect(screen.getByTestId('snap').textContent).toBe(QUICK_ACTIONS_SNAP_COLLAPSED)
|
||||
|
||||
Object.defineProperty(scrollport, 'scrollTop', {
|
||||
|
|
@ -264,8 +317,7 @@ describe('CampaignContentScroll quick-actions collapse (B100)', () => {
|
|||
value: 0,
|
||||
})
|
||||
fireEvent.scroll(scrollport!)
|
||||
|
||||
expect(screen.getByTestId('snap').textContent).toBe(QUICK_ACTIONS_SNAP_COLLAPSED)
|
||||
expect(screen.getByTestId('snap').textContent).toBe(QUICK_ACTIONS_SNAP_DOCK)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -275,4 +327,10 @@ describe('campaignQuickActionSnap', () => {
|
|||
expect(quickActionsSnapIsDock(QUICK_ACTIONS_SNAP_COLLAPSED)).toBe(false)
|
||||
expect(quickActionsSnapIsDock(null)).toBe(false)
|
||||
})
|
||||
|
||||
it('resolves scroll direction from delta threshold', () => {
|
||||
expect(quickActionsScrollDirection(0, 25)).toBe('down')
|
||||
expect(quickActionsScrollDirection(80, 50)).toBe('up')
|
||||
expect(quickActionsScrollDirection(40, 50)).toBe('none')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue