mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
Merge pull request #153 from fsolla/cursor/pool-i148-3c37da6e-2281
B109 — Bottom drawer: dock polish, fullscreen search, exclude current entity
This commit is contained in:
commit
ab910b499b
13 changed files with 354 additions and 39 deletions
|
|
@ -28,7 +28,7 @@ export type CampaignHomeActionButtonProps = {
|
|||
}
|
||||
|
||||
export const actionControlClassName =
|
||||
'group box-content flex min-h-11 w-[4.75rem] shrink-0 snap-start flex-col items-center gap-2 rounded-md px-2 text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50'
|
||||
'group box-content flex min-h-11 w-[5.5rem] shrink-0 snap-start flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50'
|
||||
|
||||
const circleClassName =
|
||||
'flex size-14 items-center justify-center rounded-full bg-muted text-foreground transition-[transform,colors] duration-150 ease-out motion-reduce:transition-none pointer-fine:group-hover:scale-[1.05] pointer-coarse:group-data-[pressing=true]:scale-[1.05] motion-reduce:scale-100 group-hover:bg-muted/80 group-focus-visible:bg-muted/80'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
'use client'
|
||||
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
|
||||
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
|
||||
|
||||
const HomeSearchExcludeContext = createContext<CampaignQuickActionContext | null>(null)
|
||||
|
||||
export const HomeSearchExcludeProvider = ({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: CampaignQuickActionContext
|
||||
children: ReactNode
|
||||
}) => (
|
||||
<HomeSearchExcludeContext.Provider value={value}>{children}</HomeSearchExcludeContext.Provider>
|
||||
)
|
||||
|
||||
export const useHomeSearchExcludeContext = (): CampaignQuickActionContext | null =>
|
||||
useContext(HomeSearchExcludeContext)
|
||||
|
|
@ -11,9 +11,11 @@ import {
|
|||
} from 'react'
|
||||
|
||||
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
|
||||
import { useHomeSearchExcludeContext } from '@/components/campaign/dashboard/HomeSearchExcludeContext'
|
||||
import type { HomeSearchSuccessResponse } from '@/lib/campaignHomeSearchHits'
|
||||
import { HOME_SEARCH_GENERIC_ERROR_MESSAGE } from '@/lib/campaignHomeSearchMessages'
|
||||
import { postCampaignJson } from '@/lib/campaignJsonRequest'
|
||||
import { filterHomeSearchResponseForContext } from '@/lib/homeSearchExcludeCurrentEntity'
|
||||
|
||||
const HOME_SEARCH_ROUTE = '/campanha/home-search'
|
||||
|
||||
|
|
@ -70,6 +72,7 @@ type HomeSearchErrorResponse = { status: 'error'; message: string }
|
|||
|
||||
export const useHomeSearchResultsState = (): HomeSearchResultsContextValue => {
|
||||
const { query, isDebouncing, uiFocused } = useHomeSearch()
|
||||
const excludeContext = useHomeSearchExcludeContext()
|
||||
const initialSuggest = useContext(InitialHomeSearchSuggestContext)
|
||||
const [results, setResults] = useState<HomeSearchResultsState>({ status: 'idle' })
|
||||
const requestSeq = useRef(0)
|
||||
|
|
@ -136,12 +139,20 @@ export const useHomeSearchResultsState = (): HomeSearchResultsContextValue => {
|
|||
|
||||
const isFetching = isDebouncing || results.status === 'loading'
|
||||
|
||||
const filteredResults = useMemo((): HomeSearchResultsState => {
|
||||
if (results.status !== 'success' || !excludeContext) return results
|
||||
return {
|
||||
status: 'success',
|
||||
data: filterHomeSearchResponseForContext(results.data, excludeContext),
|
||||
}
|
||||
}, [excludeContext, results])
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
results,
|
||||
results: filteredResults,
|
||||
isFetching,
|
||||
resultKind: results.status === 'success' ? results.data.resultKind : 'idle',
|
||||
resultKind: filteredResults.status === 'success' ? filteredResults.data.resultKind : 'idle',
|
||||
}),
|
||||
[results, isFetching],
|
||||
[filteredResults, isFetching],
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,59 @@
|
|||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
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 { HomeSearchExcludeProvider } from '@/components/campaign/dashboard/HomeSearchExcludeContext'
|
||||
import { useCampaignQuickActionContext } from '@/components/campaign/shell/CampaignQuickActionContext'
|
||||
import { useCampaignQuickActionsSnap } from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
|
||||
import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/Drawer'
|
||||
import {
|
||||
QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
|
||||
QUICK_ACTIONS_SNAP_COLLAPSED,
|
||||
QUICK_ACTIONS_SNAP_DOCK,
|
||||
QUICK_ACTIONS_SNAP_FULL,
|
||||
QUICK_ACTIONS_SNAP_POINTS,
|
||||
quickActionsSnapIsDock,
|
||||
quickActionsSnapIsFull,
|
||||
type QuickActionsSnapPoint,
|
||||
} from '@/lib/campaignQuickActionSnap'
|
||||
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
|
||||
import { resolveHomeSearchExcludeContext } from '@/lib/homeSearchExcludeCurrentEntity'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const SNAP_POINTS = [...QUICK_ACTIONS_SNAP_POINTS]
|
||||
|
||||
const restoreSnapAfterSearch = (): QuickActionsSnapPoint => {
|
||||
const scrollport = document.querySelector('[data-slot="campaign-content-scroll"]')
|
||||
const scrollTop = scrollport instanceof HTMLElement ? scrollport.scrollTop : 0
|
||||
return scrollTop > QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX
|
||||
? QUICK_ACTIONS_SNAP_COLLAPSED
|
||||
: QUICK_ACTIONS_SNAP_DOCK
|
||||
}
|
||||
|
||||
export const CampaignQuickActionsDrawer = ({
|
||||
actions,
|
||||
}: {
|
||||
actions: readonly CampaignQuickAction[]
|
||||
}) => {
|
||||
const { snapPoint, setSnapPoint, isDock } = useCampaignQuickActionsSnap()
|
||||
const pathname = usePathname()
|
||||
const { context } = useCampaignQuickActionContext()
|
||||
const excludeContext = resolveHomeSearchExcludeContext(pathname, context)
|
||||
const { snapPoint, setSnapPoint, isDock, isFull } = useCampaignQuickActionsSnap()
|
||||
const { uiFocused } = useHomeSearch()
|
||||
const showActions = actions.length > 0
|
||||
const showActions = actions.length > 0 && isDock && !isFull
|
||||
const showSearchResults = isDock || isFull
|
||||
|
||||
const handleSnapPointChange = useCallback(
|
||||
(next: QuickActionsSnapPoint | string | number | null) => {
|
||||
if (next === QUICK_ACTIONS_SNAP_COLLAPSED || next === QUICK_ACTIONS_SNAP_DOCK) {
|
||||
if (
|
||||
next === QUICK_ACTIONS_SNAP_COLLAPSED ||
|
||||
next === QUICK_ACTIONS_SNAP_DOCK ||
|
||||
next === QUICK_ACTIONS_SNAP_FULL
|
||||
) {
|
||||
setSnapPoint(next)
|
||||
return
|
||||
}
|
||||
|
|
@ -40,9 +63,11 @@ export const CampaignQuickActionsDrawer = ({
|
|||
)
|
||||
|
||||
const toggleSnap = useCallback(() => {
|
||||
setSnapPoint((current) =>
|
||||
quickActionsSnapIsDock(current) ? QUICK_ACTIONS_SNAP_COLLAPSED : QUICK_ACTIONS_SNAP_DOCK,
|
||||
)
|
||||
setSnapPoint((current) => {
|
||||
if (quickActionsSnapIsFull(current)) return restoreSnapAfterSearch()
|
||||
if (quickActionsSnapIsDock(current)) return QUICK_ACTIONS_SNAP_COLLAPSED
|
||||
return QUICK_ACTIONS_SNAP_DOCK
|
||||
})
|
||||
}, [setSnapPoint])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
|
|
@ -54,49 +79,62 @@ 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)
|
||||
if (!uiFocused) return
|
||||
if (!quickActionsSnapIsFull(snapPoint)) {
|
||||
setSnapPoint(QUICK_ACTIONS_SNAP_FULL)
|
||||
}
|
||||
}, [isDock, setSnapPoint, uiFocused])
|
||||
}, [snapPoint, setSnapPoint, uiFocused])
|
||||
|
||||
useEffect(() => {
|
||||
if (uiFocused) return
|
||||
setSnapPoint(restoreSnapAfterSearch())
|
||||
}, [setSnapPoint, uiFocused])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open
|
||||
modal={false}
|
||||
swipeDirection="down"
|
||||
snapPoints={SNAP_POINTS}
|
||||
snapPoint={snapPoint}
|
||||
snapPoints={uiFocused ? [QUICK_ACTIONS_SNAP_FULL] : SNAP_POINTS}
|
||||
snapPoint={uiFocused ? QUICK_ACTIONS_SNAP_FULL : snapPoint}
|
||||
onSnapPointChange={handleSnapPointChange}
|
||||
onOpenChange={handleOpenChange}
|
||||
disablePointerDismissal
|
||||
>
|
||||
<DrawerContent
|
||||
id="CampaignQuickActionsDrawer"
|
||||
className="border-t border-border bg-background text-foreground shadow-[0_-4px_24px_-8px_rgb(0_0_0/0.12)] print:hidden"
|
||||
className={cn(
|
||||
'border-t border-border bg-background text-foreground shadow-[0_-4px_24px_-8px_rgb(0_0_0/0.12)] print:hidden',
|
||||
isFull && 'z-[60] h-dvh max-h-dvh [--drawer-height:100dvh]',
|
||||
)}
|
||||
>
|
||||
<DrawerTitle className="sr-only">Ações rápidas</DrawerTitle>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSnap}
|
||||
aria-expanded={isDock}
|
||||
aria-expanded={isDock || isFull}
|
||||
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'}
|
||||
className="flex w-full shrink-0 cursor-grab flex-col items-center border-0 bg-transparent px-4 pt-2 pb-0.5 active:cursor-grabbing"
|
||||
aria-label={isDock || isFull ? '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-3 overflow-y-auto px-4 pb-[max(0.5rem,env(safe-area-inset-bottom,0px))]',
|
||||
isDock ? 'flex-1' : 'shrink-0',
|
||||
'flex min-h-0 flex-col overflow-y-auto px-4',
|
||||
isFull
|
||||
? 'flex-1 gap-2 pb-[max(0.25rem,env(safe-area-inset-bottom,0px))]'
|
||||
: cn(
|
||||
'gap-2 pb-[max(0.25rem,env(safe-area-inset-bottom,0px))]',
|
||||
isDock ? 'flex-1' : 'shrink-0',
|
||||
),
|
||||
)}
|
||||
data-snap={isDock ? 'dock' : 'collapsed'}
|
||||
data-snap={isFull ? 'full' : isDock ? 'dock' : 'collapsed'}
|
||||
>
|
||||
{showActions ? (
|
||||
<div hidden={!isDock} className="-mx-4 w-[calc(100%+2rem)]">
|
||||
<div className="-mx-4 w-[calc(100%+2rem)]">
|
||||
<CampaignHomeActionStrip
|
||||
actions={actions.map((action) => ({
|
||||
id: action.id,
|
||||
|
|
@ -109,7 +147,12 @@ export const CampaignQuickActionsDrawer = ({
|
|||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<CampaignGlobalSearchBody placeholder={isDock ? undefined : ''} showResults={isDock} />
|
||||
<HomeSearchExcludeProvider value={excludeContext}>
|
||||
<CampaignGlobalSearchBody
|
||||
placeholder={isDock || isFull ? undefined : ''}
|
||||
showResults={showSearchResults}
|
||||
/>
|
||||
</HomeSearchExcludeProvider>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,13 @@ import { cn } from '@/lib/utils'
|
|||
const CampaignContentScrollWithPeek = ({ children }: { children: ReactNode }) => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const lastScrollTopRef = useRef(0)
|
||||
const { isDock, setSnapPoint } = useCampaignQuickActionsSnap()
|
||||
const { isDock, isFull, setSnapPoint } = useCampaignQuickActionsSnap()
|
||||
const { uiFocused } = useHomeSearch()
|
||||
const peekHeight = isDock ? QUICK_ACTIONS_SNAP_DOCK : QUICK_ACTIONS_SNAP_COLLAPSED
|
||||
const peekHeight = isFull
|
||||
? '0px'
|
||||
: isDock
|
||||
? QUICK_ACTIONS_SNAP_DOCK
|
||||
: QUICK_ACTIONS_SNAP_COLLAPSED
|
||||
|
||||
const syncSnapFromScroll = useCallback(() => {
|
||||
const scrollport = scrollRef.current
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import {
|
||||
QUICK_ACTIONS_SNAP_DOCK,
|
||||
quickActionsSnapIsDock,
|
||||
quickActionsSnapIsFull,
|
||||
type QuickActionsSnapPoint,
|
||||
} from '@/lib/campaignQuickActionSnap'
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ type CampaignQuickActionsSnapContextValue = {
|
|||
snapPoint: QuickActionsSnapPoint | null
|
||||
setSnapPoint: Dispatch<SetStateAction<QuickActionsSnapPoint | null>>
|
||||
isDock: boolean
|
||||
isFull: boolean
|
||||
}
|
||||
|
||||
const CampaignQuickActionsSnapContext = createContext<CampaignQuickActionsSnapContextValue | null>(
|
||||
|
|
@ -41,6 +43,7 @@ export const CampaignQuickActionsSnapProvider = ({ children }: { children: React
|
|||
snapPoint,
|
||||
setSnapPoint,
|
||||
isDock: quickActionsSnapIsDock(snapPoint),
|
||||
isFull: quickActionsSnapIsFull(snapPoint),
|
||||
}),
|
||||
[snapPoint],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export type CampaignQuickActionContext = {
|
|||
organizationSlug?: string
|
||||
activitySlug?: string
|
||||
demandSlug?: string
|
||||
advisorId?: number
|
||||
}
|
||||
|
||||
export const emptyCampaignQuickActionContext = (): CampaignQuickActionContext => ({})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ export const QUICK_ACTIONS_SNAP_COLLAPSED = '6.5rem' as const
|
|||
/** Dock — handle + action strip (2-line labels) + global search. */
|
||||
export const QUICK_ACTIONS_SNAP_DOCK = '15rem' as const
|
||||
|
||||
/** Full viewport — search focus/active; covers mobile top bar (B109). */
|
||||
export const QUICK_ACTIONS_SNAP_FULL = 1 as const
|
||||
|
||||
/**
|
||||
* Scroll delta past this threshold on `campaign-content-scroll` collapses (↓)
|
||||
* or re-docks (↑) the drawer.
|
||||
|
|
@ -13,12 +16,16 @@ export const QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX = 24
|
|||
export const QUICK_ACTIONS_SNAP_POINTS = [
|
||||
QUICK_ACTIONS_SNAP_COLLAPSED,
|
||||
QUICK_ACTIONS_SNAP_DOCK,
|
||||
QUICK_ACTIONS_SNAP_FULL,
|
||||
] as const
|
||||
|
||||
export type QuickActionsSnapPoint = (typeof QUICK_ACTIONS_SNAP_POINTS)[number]
|
||||
|
||||
export type QuickActionsScrollDirection = 'up' | 'down' | 'none'
|
||||
|
||||
export const quickActionsSnapIsFull = (snap: QuickActionsSnapPoint | null): boolean =>
|
||||
snap === QUICK_ACTIONS_SNAP_FULL
|
||||
|
||||
export const quickActionsSnapIsDock = (snap: QuickActionsSnapPoint | null): boolean =>
|
||||
snap === QUICK_ACTIONS_SNAP_DOCK
|
||||
|
||||
|
|
|
|||
83
src/lib/homeSearchExcludeCurrentEntity.ts
Normal file
83
src/lib/homeSearchExcludeCurrentEntity.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { parseAdvisorDetailId } from '@/lib/campaignAdvisorQuickActions'
|
||||
import type { HomeSearchSuccessResponse } from '@/lib/campaignHomeSearchHits'
|
||||
import { parseMunicipalityDetailSlug } from '@/lib/campaignMunicipalityQuickActions'
|
||||
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
|
||||
import { isDemandDetailPath } from '@/lib/campaignQuickActionDemands'
|
||||
import {
|
||||
parseActivityQuickActionSurface,
|
||||
parseOrganizationQuickActionSurface,
|
||||
} from '@/lib/campaignQuickActionPaths'
|
||||
|
||||
const leadershipDetailIdFromPath = (pathname: string): number | undefined => {
|
||||
const match = pathname.match(/^\/campanha\/liderancas\/(\d+)(?:\/|$)/)
|
||||
if (!match?.[1]) return undefined
|
||||
const id = Number(match[1])
|
||||
return Number.isInteger(id) && id > 0 ? id : undefined
|
||||
}
|
||||
|
||||
const demandSlugFromPath = (pathname: string): string | undefined => {
|
||||
if (!isDemandDetailPath(pathname)) return undefined
|
||||
const rest = pathname.slice('/campanha/demandas/'.length)
|
||||
return rest || undefined
|
||||
}
|
||||
|
||||
/** Merged pathname + page context for client-side hit exclusion (B109). */
|
||||
export const resolveHomeSearchExcludeContext = (
|
||||
pathname: string,
|
||||
context: CampaignQuickActionContext,
|
||||
): CampaignQuickActionContext => {
|
||||
const organizationSurface = parseOrganizationQuickActionSurface(pathname)
|
||||
const activitySurface = parseActivityQuickActionSurface(pathname)
|
||||
|
||||
return {
|
||||
municipalitySlug: context.municipalitySlug ?? parseMunicipalityDetailSlug(pathname),
|
||||
municipalityId: context.municipalityId,
|
||||
leadershipId: context.leadershipId ?? leadershipDetailIdFromPath(pathname),
|
||||
organizationSlug:
|
||||
context.organizationSlug ??
|
||||
(organizationSurface?.kind === 'detail' ? organizationSurface.organizationSlug : undefined),
|
||||
activitySlug:
|
||||
context.activitySlug ??
|
||||
(activitySurface?.kind === 'detail' ? activitySurface.activitySlug : undefined),
|
||||
demandSlug: context.demandSlug ?? demandSlugFromPath(pathname),
|
||||
advisorId: context.advisorId ?? parseAdvisorDetailId(pathname),
|
||||
}
|
||||
}
|
||||
|
||||
const hasExcludeTarget = (context: CampaignQuickActionContext): boolean =>
|
||||
Boolean(
|
||||
context.municipalitySlug ||
|
||||
context.leadershipId !== undefined ||
|
||||
context.organizationSlug ||
|
||||
context.activitySlug ||
|
||||
context.demandSlug ||
|
||||
context.advisorId !== undefined,
|
||||
)
|
||||
|
||||
export const filterHomeSearchResponseForContext = (
|
||||
data: HomeSearchSuccessResponse,
|
||||
context: CampaignQuickActionContext,
|
||||
): HomeSearchSuccessResponse => {
|
||||
if (!hasExcludeTarget(context)) return data
|
||||
|
||||
return {
|
||||
...data,
|
||||
municipalities: context.municipalitySlug
|
||||
? data.municipalities.filter((hit) => hit.slug !== context.municipalitySlug)
|
||||
: data.municipalities,
|
||||
leaderships:
|
||||
context.leadershipId !== undefined
|
||||
? data.leaderships.filter((hit) => hit.id !== context.leadershipId)
|
||||
: data.leaderships,
|
||||
advisors:
|
||||
context.advisorId !== undefined
|
||||
? data.advisors.filter((hit) => hit.id !== context.advisorId)
|
||||
: data.advisors,
|
||||
activities: context.activitySlug
|
||||
? data.activities.filter((hit) => hit.slug !== context.activitySlug)
|
||||
: data.activities,
|
||||
demands: context.demandSlug
|
||||
? data.demands.filter((hit) => hit.slug !== context.demandSlug)
|
||||
: data.demands,
|
||||
}
|
||||
}
|
||||
|
|
@ -458,3 +458,44 @@ test.describe('Municípios — bottom drawer mobile (B105)', () => {
|
|||
await expect(search).toHaveAttribute('placeholder', 'Município, liderança, atividade…')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* B109: dock labels readable, search focus goes fullscreen (strip hidden), current
|
||||
* entity excluded from suggest/results.
|
||||
*/
|
||||
test.describe('Municípios — bottom drawer polish (B109)', () => {
|
||||
test.use({ viewport: { width: 390, height: 844 } })
|
||||
|
||||
test('dock labels readable, search fullscreen hides strip, excludes current município', async ({
|
||||
campaign,
|
||||
page,
|
||||
}) => {
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenador B109'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto(`${campaign.baseURL}/campanha/municipios/${municipality.slug}`)
|
||||
await expect(page.getByRole('heading', { name: municipality.name })).toBeVisible()
|
||||
|
||||
const changeTrend = page.getByRole('link', { name: 'Mudar tendência' })
|
||||
await expect(changeTrend).toBeVisible()
|
||||
await expect(changeTrend).toContainText('Mudar tendência')
|
||||
|
||||
const search = page.getByLabel('Buscar na campanha')
|
||||
await search.focus()
|
||||
await expect(page.getByRole('link', { name: 'Mudar tendência' })).toBeHidden()
|
||||
|
||||
const handle = page.getByRole('button', { name: 'Ocultar ações rápidas' })
|
||||
const handleBox = await handle.boundingBox()
|
||||
expect(handleBox).toBeTruthy()
|
||||
expect(handleBox!.y).toBeLessThan(40)
|
||||
|
||||
await expect(page.getByRole('region', { name: 'Sugestões' })).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('region', { name: 'Sugestões' }).getByText(municipality.name, { exact: true }),
|
||||
).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -91,14 +91,13 @@ describe('CampaignHomeActionButton', () => {
|
|||
expect(screen.getByLabelText('Em breve').getAttribute('aria-disabled')).toBe('true')
|
||||
})
|
||||
|
||||
it('uses horizontal padding for hit area without shrinking the visual column (B101)', () => {
|
||||
it('uses a wider control column for two-line labels (B109)', () => {
|
||||
renderActionButton({ label: 'Registrar', icon: BarChart3 })
|
||||
const control = screen.getByRole('button', { name: 'Registrar' })
|
||||
expect(control.className).toContain('px-2')
|
||||
expect(control.className).toContain('box-content')
|
||||
expect(control.className).toContain('w-[4.75rem]')
|
||||
expect(actionControlClassName).toContain('px-2')
|
||||
expect(control.className).toContain('w-[5.5rem]')
|
||||
expect(actionControlClassName).toContain('box-content')
|
||||
expect(actionControlClassName).toContain('w-[5.5rem]')
|
||||
})
|
||||
|
||||
it('opens the description drawer on long-press when the pointer is coarse', () => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { ReactNode } from 'react'
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { CampaignGlobalSearchProvider } from '@/components/campaign/dashboard/CampaignGlobalSearchMount'
|
||||
import { CampaignQuickActionContextProvider } from '@/components/campaign/shell/CampaignQuickActionContext'
|
||||
import { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer'
|
||||
import { CampaignContentScroll } from '@/components/campaign/shell/CampaignQuickActionsHost'
|
||||
import {
|
||||
|
|
@ -17,8 +18,10 @@ import {
|
|||
QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
|
||||
QUICK_ACTIONS_SNAP_COLLAPSED,
|
||||
QUICK_ACTIONS_SNAP_DOCK,
|
||||
QUICK_ACTIONS_SNAP_FULL,
|
||||
quickActionsScrollDirection,
|
||||
quickActionsSnapIsDock,
|
||||
quickActionsSnapIsFull,
|
||||
} from '@/lib/campaignQuickActionSnap'
|
||||
|
||||
vi.mock('@/lib/campaignJsonRequest', () => ({
|
||||
|
|
@ -89,9 +92,11 @@ afterEach(() => {
|
|||
const renderQuickActionsChrome = (ui: ReactNode) =>
|
||||
render(
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<CampaignQuickActionsSnapProvider>
|
||||
<CampaignGlobalSearchProvider>{ui}</CampaignGlobalSearchProvider>
|
||||
</CampaignQuickActionsSnapProvider>
|
||||
<CampaignQuickActionContextProvider>
|
||||
<CampaignQuickActionsSnapProvider>
|
||||
<CampaignGlobalSearchProvider>{ui}</CampaignGlobalSearchProvider>
|
||||
</CampaignQuickActionsSnapProvider>
|
||||
</CampaignQuickActionContextProvider>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
|
|
@ -241,7 +246,7 @@ describe('CampaignQuickActionsDrawer (B105)', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('expands to dock when the peek search is focused', () => {
|
||||
it('expands to full snap when the peek search is focused (B109)', () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: idleSuggestPayload,
|
||||
|
|
@ -255,10 +260,10 @@ describe('CampaignQuickActionsDrawer (B105)', () => {
|
|||
)
|
||||
|
||||
fireEvent.focus(screen.getByLabelText('Buscar na campanha'))
|
||||
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe('dock')
|
||||
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe('full')
|
||||
})
|
||||
|
||||
it('stays docked while search remains focused even if the handle collapses', () => {
|
||||
it('stays full while search remains focused even if the handle collapses (B109)', () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: idleSuggestPayload,
|
||||
|
|
@ -268,7 +273,7 @@ describe('CampaignQuickActionsDrawer (B105)', () => {
|
|||
|
||||
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')
|
||||
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe('full')
|
||||
})
|
||||
|
||||
it('toggles between dock and collapsed from the handle', () => {
|
||||
|
|
@ -288,6 +293,35 @@ describe('CampaignQuickActionsDrawer (B105)', () => {
|
|||
fireEvent.click(screen.getByRole('button', { name: 'Mostrar ações rápidas' }))
|
||||
expect(document.getElementById('quickActionContext')?.getAttribute('data-snap')).toBe('dock')
|
||||
})
|
||||
|
||||
it('hides the current municipality from drawer suggest hits (B109)', async () => {
|
||||
vi.mocked(postCampaignJson).mockResolvedValue({
|
||||
ok: true,
|
||||
payload: {
|
||||
...suggestWithPriorityPayload,
|
||||
municipalities: [
|
||||
...suggestWithPriorityPayload.municipalities,
|
||||
{
|
||||
kind: 'municipality' as const,
|
||||
slug: 'foo',
|
||||
name: 'Município Atual',
|
||||
region: 'Teste',
|
||||
priority: null,
|
||||
votePosition2022: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
|
||||
|
||||
fireEvent.focus(screen.getByLabelText('Buscar na campanha'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Cairu')).toBeTruthy()
|
||||
expect(screen.queryByText('Município Atual')).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('CampaignContentScroll quick-actions direction (B105)', () => {
|
||||
|
|
@ -322,10 +356,13 @@ describe('CampaignContentScroll quick-actions direction (B105)', () => {
|
|||
})
|
||||
|
||||
describe('campaignQuickActionSnap', () => {
|
||||
it('detects dock snap', () => {
|
||||
it('detects dock and full snaps', () => {
|
||||
expect(quickActionsSnapIsDock(QUICK_ACTIONS_SNAP_DOCK)).toBe(true)
|
||||
expect(quickActionsSnapIsDock(QUICK_ACTIONS_SNAP_COLLAPSED)).toBe(false)
|
||||
expect(quickActionsSnapIsDock(QUICK_ACTIONS_SNAP_FULL)).toBe(false)
|
||||
expect(quickActionsSnapIsDock(null)).toBe(false)
|
||||
expect(quickActionsSnapIsFull(QUICK_ACTIONS_SNAP_FULL)).toBe(true)
|
||||
expect(quickActionsSnapIsFull(QUICK_ACTIONS_SNAP_DOCK)).toBe(false)
|
||||
})
|
||||
|
||||
it('resolves scroll direction from delta threshold', () => {
|
||||
|
|
|
|||
66
tests/unit/homeSearchExcludeCurrentEntity.unit.spec.ts
Normal file
66
tests/unit/homeSearchExcludeCurrentEntity.unit.spec.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { HomeSearchSuccessResponse } from '@/lib/campaignHomeSearchHits'
|
||||
import {
|
||||
filterHomeSearchResponseForContext,
|
||||
resolveHomeSearchExcludeContext,
|
||||
} from '@/lib/homeSearchExcludeCurrentEntity'
|
||||
|
||||
const sampleResponse = (): HomeSearchSuccessResponse => ({
|
||||
status: 'success',
|
||||
resultKind: 'suggest',
|
||||
municipalities: [
|
||||
{
|
||||
kind: 'municipality',
|
||||
slug: 'cairu',
|
||||
name: 'Cairu',
|
||||
region: 'Baixo Sul',
|
||||
priority: null,
|
||||
votePosition2022: null,
|
||||
},
|
||||
{
|
||||
kind: 'municipality',
|
||||
slug: 'feira-de-santana',
|
||||
name: 'Feira de Santana',
|
||||
region: 'Centro-Norte',
|
||||
priority: null,
|
||||
votePosition2022: null,
|
||||
},
|
||||
],
|
||||
territories: [],
|
||||
advisors: [{ id: 9, name: 'Assessor', phone: null, municipalityCount: 2 }],
|
||||
leaderships: [
|
||||
{ kind: 'leadership', id: 42, name: 'Líder', phone: null, municipalitiesSummary: '' },
|
||||
],
|
||||
stateDeputies: [],
|
||||
activities: [{ id: 1, slug: 'comicio', title: 'Comício', secondary: '' }],
|
||||
demands: [{ id: 2, slug: 'pedido', title: 'Pedido', secondary: '' }],
|
||||
})
|
||||
|
||||
describe('homeSearchExcludeCurrentEntity (B109)', () => {
|
||||
it('merges municipality slug from pathname when context is empty', () => {
|
||||
expect(resolveHomeSearchExcludeContext('/campanha/municipios/cairu', {}).municipalitySlug).toBe(
|
||||
'cairu',
|
||||
)
|
||||
})
|
||||
|
||||
it('filters the current municipality from suggest and search hits', () => {
|
||||
const filtered = filterHomeSearchResponseForContext(sampleResponse(), {
|
||||
municipalitySlug: 'cairu',
|
||||
})
|
||||
expect(filtered.municipalities.map((hit) => hit.slug)).toEqual(['feira-de-santana'])
|
||||
})
|
||||
|
||||
it('filters leadership, advisor, activity and demand entities', () => {
|
||||
const filtered = filterHomeSearchResponseForContext(sampleResponse(), {
|
||||
leadershipId: 42,
|
||||
advisorId: 9,
|
||||
activitySlug: 'comicio',
|
||||
demandSlug: 'pedido',
|
||||
})
|
||||
expect(filtered.leaderships).toHaveLength(0)
|
||||
expect(filtered.advisors).toHaveLength(0)
|
||||
expect(filtered.activities).toHaveLength(0)
|
||||
expect(filtered.demands).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue