B58: polish Início action strip and centralize coarse press state.
Short staff labels, hidden scrollbar, circle feedback on hover/press, and long-press pressing lives in useCampaignLongPress for reuse.
This commit is contained in:
parent
3581880950
commit
274cde65a7
11 changed files with 130 additions and 87 deletions
|
|
@ -7,6 +7,7 @@ alwaysApply: false
|
|||
|
||||
## Status
|
||||
|
||||
- **B58 entregue em código (2026-07-29):** polimento visual da strip de ações do Início (UX-1) — rótulos curtos no catálogo (`Ajustar votos`, `Registrar sinal`, `Ver esquecidos`, …); removido o `h2` "O que você quer fazer?" (`aria-label` da strip = "Ações rápidas"); scrollbar horizontal oculta com pan preservado; escala ~1,05 no círculo (`pointer-fine` hover / `pointer-coarse` `data-pressing`) + `active:opacity-90`; e2e B47 retarget para `[data-slot="home-actions"]`. Sem migration/action/URL. Plano: [`polimento-strip-acoes-inicio.md`](../../../docs/plans/polimento-strip-acoes-inicio.md). Próximo chassis: **B48** resultados busca.
|
||||
- **Fill-in Cenário junto aos filtros entregue em código (2026-07-29):** em `/campanha/municipios` (staff), o seletor **Cenário** (`VoteEstimateScenarioField`) saiu do header do `MunicipalityListOverview` e entrou na barra slim de `MunicipalityFilters` (desktop: antes de Salvar/Limpar; mobile: após filtros staff, antes de Ordenar). `MunicipalityEstimateScenarioProvider` passou a envolver filtros + resultados; cenário continua estado local (fora da URL e do **Limpar**). Hint `VOTE_ESTIMATE_SCENARIO_FILTERS_HINT`. Sem migration/action/Consent. Plano: [`cenario-junto-filtros-municipios.md`](../../../docs/plans/cenario-junto-filtros-municipios.md).
|
||||
- **B48 entregue em código (2026-07-29):** primeiro provider da busca global do Início — grupo **Municípios** (municípios scoped + TIs no mesmo bloco) via `POST /campanha/home-search`, `searchHomeMunicipalities`, `HomeSearchMunicipalityGroup` no slot B47; readout 2022 alinhado à lista (`MunicipalityVotePositionReadout`). Próximo: **B49** lideranças (mesmo payload); **B54** grid/cap. Plano: [`busca-global-resultados-municipios.md`](../../../docs/plans/busca-global-resultados-municipios.md).
|
||||
- **B47 entregue em código (2026-07-29):** busca global do Início (UX-1) — input staff-only com debounce 250 ms (`HOME_SEARCH_DEBOUNCE_MS`), sem botão Pesquisar; modo focado quando query debounced trim ≥2 chars esconde strip B45 e spacer thumb-zone (`CampaignHomeLayout` `focused`). Ilha `CampaignHomeStaffChrome` + `CampaignHomeSearch` + `HomeSearchContext` / `useHomeSearch` para grupos B48+; contrato em `src/lib/campaignHomeSearchContract.ts`. Leader continua sem omnibox (`CampaignHomeLayout` seco). Escape limpa. Sem migration. Plano: [`busca-global-inicio-input.md`](../../../docs/plans/busca-global-inicio-input.md). ~~Próximo: **B48** resultados Municípios.~~
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Polimento visual da strip de ações do Início
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-07-29
|
||||
Status: entregue (2026-07-29)
|
||||
Atualizado em: 2026-07-29 — as-built: rótulos curtos no catálogo; sem `h2` em `CampaignHomeActions`; scrollbar oculta na strip; escala no círculo (hover fine / press coarse) + `active:opacity-90`; e2e retarget de heading para `[data-slot="home-actions"]` / `Ações rápidas`.
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B — **B58**; chassis UX-1)
|
||||
Impeccable: B — encaixe na strip existente (`CampaignHomeActionButton` / `CampaignHomeActionStrip` / `campaignHomeActions.ts`)
|
||||
Appetite: ~0,5 dia eng (sem migration, sem action, sem URL)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Roadmap — Teqo
|
||||
|
||||
Atualizado em: 2026-07-29 — **B65** ancorar busca+ações no limite inferior mobile (B46 ✓ não ancorou); B48 ✓; B64; B59–B63; B47 ✓; B58; B49–B55.
|
||||
Atualizado em: 2026-07-29 — ~~**B58**~~ ✓ (polimento strip Início); **B65** ancorar busca+ações no limite inferior mobile (B46 ✓ não ancorou); B48 ✓; B64; B59–B63; B47 ✓; B49–B55.
|
||||
Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc).
|
||||
|
||||
## Âncoras do calendário eleitoral 2026 (Res. TSE 23.760/2026)
|
||||
|
|
@ -83,11 +83,11 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re
|
|||
|
||||
### Demais itens abertos
|
||||
|
||||
- **UX-1 fluxos ação-primeiro (prioridade pós-sessão 29/07)** — evidência: [snapshot](plans/sessao-observada-coordenador-2026-07-29-snapshot.md) · O9–O11 em CUSTOMER.md · **rascunho:** [fluxos-acao-primeiro-inicio.md](plans/fluxos-acao-primeiro-inicio.md). Soft deadline: **03/08**. **Chassis Início:** ~~**B43**~~ ✓ → ~~**B44**~~ ✓ → ~~**B45**~~ ✓ → ~~**B46**~~ ✓ → ~~**B47**~~ ✓ → **B65** (âncora inferior mobile — B46 não ancorou) → **B58** (polimento strip) → ~~**B48**~~ ✓ → **B49–B55** (demais resultados busca) · **B56** (resumo) · **B57** delta 7d = futuro. **Wizards (etapas):** **B59** chassis → **B60** busca município · **B61** ajuste votos · **B62** simplificar sinal → **B63** wizard sinal · **B64** wizard tendência; wiring A1/A2 depois.
|
||||
- **UX-1 fluxos ação-primeiro (prioridade pós-sessão 29/07)** — evidência: [snapshot](plans/sessao-observada-coordenador-2026-07-29-snapshot.md) · O9–O11 em CUSTOMER.md · **rascunho:** [fluxos-acao-primeiro-inicio.md](plans/fluxos-acao-primeiro-inicio.md). Soft deadline: **03/08**. **Chassis Início:** ~~**B43**~~ ✓ → ~~**B44**~~ ✓ → ~~**B45**~~ ✓ → ~~**B46**~~ ✓ → ~~**B47**~~ ✓ → ~~**B58**~~ ✓ → **B65** (âncora inferior mobile — B46 não ancorou) → ~~**B48**~~ ✓ → **B49–B55** (demais resultados busca) · **B56** (resumo) · **B57** delta 7d = futuro. **Wizards (etapas):** **B59** chassis → **B60** busca município · **B61** ajuste votos · **B62** simplificar sinal → **B63** wizard sinal · **B64** wizard tendência; wiring A1/A2 depois.
|
||||
- ~~**B43** Início em branco + Quadro~~ — **entregue 2026-07-29** (`/campanha` blank; staff dashboard em `/campanha/quadro`; liderança em `/campanha/contatos`; nav Início+Quadro / Início+Meus contatos; `requireCampaignPageActor` manda leader bloqueado → `/campanha/contatos`; sem migration) · [plano](plans/inicio-em-branco-quadro.md)
|
||||
- ~~**B44** Botão de ação do Início + lista horizontal~~ — **entregue 2026-07-29** (`CampaignHomeActionButton` + `CampaignHomeActionStrip`; Tooltip fine / long-press Drawer coarse; montado no Início por B45 ✓) · ~1d · Janela 1 · Impeccable C · [plano](plans/botao-acao-inicio-strip.md)
|
||||
- ~~**B45** Catálogo de ações por persona~~ — **entregue 2026-07-29** (`src/lib/campaignHomeActions.ts` + `CampaignHomeActions`; staff 6 ações com `uncovered-municipalities` → `?coverage=sem_assessor&sort=votos`; leader 2 com `my-contacts` → `/campanha/contatos`; demais inertes; ilha client por limite RSC↔ícones Lucide) · [plano](plans/catalogo-acoes-inicio-por-persona.md)
|
||||
- **B58** Polimento visual da strip de ações do Início — remover heading "O que você quer fazer?"; rótulos curtos (Ajustar votos, Registrar sinal, …; atalho → **"Ver esquecidos"** _(proposto — validar com produto)_); scale discreto no círculo (hover/long-press) + opacity no active; scrollbar horizontal oculta (pan preservado) · Depende de B45 ✓ · ~0,5d · Janela 1 · Paralelizável com ~~B46~~ ✓ · Impeccable B · [plano](plans/polimento-strip-acoes-inicio.md)
|
||||
- **B58** Polimento visual da strip de ações do Início — ~~remover heading "O que você quer fazer?"; rótulos curtos (Ajustar votos, Registrar sinal, …; atalho → **"Ver esquecidos"** _(proposto — validar com produto)_); scale discreto no círculo (hover/long-press) + opacity no active; scrollbar horizontal oculta (pan preservado)~~ — **entregue 2026-07-29** (catálogo + strip + botão; e2e atualizado) · Depende de B45 ✓ · ~0,5d · Janela 1 · Impeccable B · [plano](plans/polimento-strip-acoes-inicio.md)
|
||||
- **B59** Chassis do wizard (shell de passo + seta Voltar só mobile; desktop = browser) · Depende de B45 ✓ · ~0,75d · Janela 1 · Impeccable C · [plano](plans/chassis-wizard-campanha.md)
|
||||
- **B60** Etapa busca município nos wizards (UX da busca do Início, só municípios; select → avança) · Depende de B59 · soft B48 · ~1d · Janela 1 · Impeccable B · [plano](plans/busca-municipio-wizard.md)
|
||||
- **B61** Etapa ajuste de votos (média→pessimista→otimista; atalhos 2×/±50/±100; coerência com jump+warning) · Depende de B59 · soft B60 · ~1–1,25d · Janela 1 · Impeccable C · [plano](plans/ajuste-votos-wizard.md)
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ export type CampaignHomeActionButtonProps = {
|
|||
}
|
||||
|
||||
const actionControlClassName =
|
||||
'group flex min-h-11 w-[4.75rem] shrink-0 snap-start flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50'
|
||||
'group flex min-h-11 w-[4.75rem] 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'
|
||||
|
||||
const ActionVisual = ({ label, icon: Icon }: { label: string; icon: LucideIcon }) => (
|
||||
<>
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex size-14 items-center justify-center rounded-full bg-muted text-foreground transition-colors group-hover:bg-muted/80 group-focus-visible:bg-muted/80"
|
||||
>
|
||||
<span aria-hidden className={circleClassName}>
|
||||
<Icon className="size-6" strokeWidth={1.75} />
|
||||
</span>
|
||||
<span className="line-clamp-2 w-full text-sm leading-snug font-medium text-foreground">
|
||||
|
|
@ -62,34 +62,29 @@ export const CampaignHomeActionButton = ({
|
|||
if (description) setDrawerOpen(true)
|
||||
}, [description])
|
||||
|
||||
const fireClick = useCallback(
|
||||
(_event: MouseEvent) => {
|
||||
onClick?.()
|
||||
},
|
||||
[onClick],
|
||||
)
|
||||
|
||||
const useLongPress = Boolean(description) && isCoarsePointer && !disabled
|
||||
const longPressEnabled = Boolean(description) && isCoarsePointer && !disabled
|
||||
const isInert = !disabled && !href && !onClick
|
||||
const longPress = useCampaignLongPress({
|
||||
enabled: useLongPress,
|
||||
const { pressing, ...longPressHandlers } = useCampaignLongPress({
|
||||
enabled: longPressEnabled,
|
||||
onLongPress: openDescription,
|
||||
onClick: href ? undefined : onClick ? fireClick : undefined,
|
||||
onClick:
|
||||
href || !onClick
|
||||
? undefined
|
||||
: (_event: MouseEvent) => {
|
||||
onClick()
|
||||
},
|
||||
})
|
||||
|
||||
const pointerHandlers = useLongPress ? longPress : undefined
|
||||
const className = cn(actionControlClassName, isInert && 'cursor-default')
|
||||
const aria = {
|
||||
'aria-label': label,
|
||||
'aria-describedby': drawerOpen && useLongPress ? descriptionId : undefined,
|
||||
'aria-describedby': drawerOpen && longPressEnabled ? descriptionId : undefined,
|
||||
}
|
||||
|
||||
const pointerProps = pointerHandlers
|
||||
const longPressProps = longPressEnabled
|
||||
? {
|
||||
onPointerDown: pointerHandlers.onPointerDown,
|
||||
onPointerMove: pointerHandlers.onPointerMove,
|
||||
onPointerUp: pointerHandlers.onPointerUp,
|
||||
onPointerCancel: pointerHandlers.onPointerCancel,
|
||||
...longPressHandlers,
|
||||
'data-pressing': pressing ? true : undefined,
|
||||
}
|
||||
: undefined
|
||||
|
||||
|
|
@ -103,13 +98,7 @@ export const CampaignHomeActionButton = ({
|
|||
)
|
||||
} else if (href) {
|
||||
control = (
|
||||
<Link
|
||||
href={href}
|
||||
className={className}
|
||||
{...aria}
|
||||
{...pointerProps}
|
||||
onClick={pointerHandlers?.onClick}
|
||||
>
|
||||
<Link href={href} className={className} {...aria} {...longPressProps}>
|
||||
<ActionVisual label={label} icon={icon} />
|
||||
</Link>
|
||||
)
|
||||
|
|
@ -119,8 +108,16 @@ export const CampaignHomeActionButton = ({
|
|||
type="button"
|
||||
className={className}
|
||||
{...aria}
|
||||
{...pointerProps}
|
||||
onClick={pointerHandlers?.onClick ?? (onClick ? fireClick : undefined)}
|
||||
{...longPressProps}
|
||||
onClick={
|
||||
longPressEnabled
|
||||
? longPressHandlers.onClick
|
||||
: onClick
|
||||
? () => {
|
||||
onClick()
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ActionVisual label={label} icon={icon} />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
CampaignHomeActionButton,
|
||||
type CampaignHomeActionButtonProps,
|
||||
|
|
@ -10,12 +8,10 @@ import { TooltipProvider } from '@/components/ui/tooltip'
|
|||
import { cn } from '@/lib/utils'
|
||||
|
||||
export const CampaignHomeActionStrip = ({
|
||||
children,
|
||||
actions,
|
||||
ariaLabel = 'Ações rápidas',
|
||||
className,
|
||||
}: {
|
||||
children?: ReactNode
|
||||
actions?: readonly (CampaignHomeActionButtonProps & { id?: string })[]
|
||||
ariaLabel?: string
|
||||
className?: string
|
||||
|
|
@ -24,12 +20,11 @@ export const CampaignHomeActionStrip = ({
|
|||
<div
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
'min-w-0 overflow-x-auto overflow-y-hidden overscroll-x-contain [touch-action:pan-x] [scrollbar-width:thin]',
|
||||
'min-w-0 overflow-x-auto overflow-y-hidden overscroll-x-contain [touch-action:pan-x] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ul role="list" className="m-0 flex min-w-max list-none snap-x snap-proximity gap-6 p-0 pb-1">
|
||||
{children}
|
||||
{actions?.map(({ id, ...button }) => (
|
||||
<li key={id ?? button.href ?? button.label} className="m-0 list-none p-0">
|
||||
<CampaignHomeActionButton {...button} />
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/Campaig
|
|||
import { homeActionsForRole, toHomeActionButtonProps } from '@/lib/campaignHomeActions'
|
||||
import type { CampaignRole } from '@/lib/campaignRoles'
|
||||
|
||||
const homeActionsHeadingId = 'campaign-home-actions-heading'
|
||||
|
||||
export const CampaignHomeActions = ({
|
||||
role,
|
||||
uncoveredMunicipalitiesHref,
|
||||
|
|
@ -17,12 +15,5 @@ export const CampaignHomeActions = ({
|
|||
|
||||
if (actions.length === 0) return null
|
||||
|
||||
return (
|
||||
<section aria-labelledby={homeActionsHeadingId} className="w-full">
|
||||
<h2 id={homeActionsHeadingId} className="text-base font-semibold text-foreground">
|
||||
O que você quer fazer?
|
||||
</h2>
|
||||
<CampaignHomeActionStrip actions={actions} className="mt-4 w-full" />
|
||||
</section>
|
||||
)
|
||||
return <CampaignHomeActionStrip actions={actions} className="w-full" />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,13 +34,13 @@ export type CampaignHomeAction = {
|
|||
const staffHomeActionsCoordinator: readonly CampaignHomeAction[] = [
|
||||
{
|
||||
id: 'update-votes',
|
||||
label: 'Atualizar votos de um município',
|
||||
label: 'Ajustar votos',
|
||||
icon: BarChart3,
|
||||
description: 'Atualizar a projeção (média, pessimista, otimista) de um município',
|
||||
},
|
||||
{
|
||||
id: 'register-signal',
|
||||
label: 'Registrar o que aconteceu',
|
||||
label: 'Registrar sinal',
|
||||
icon: Megaphone,
|
||||
description: 'Anotar sinal urgente: invasão, perda de apoio, novo apoio, dificuldade',
|
||||
},
|
||||
|
|
@ -64,7 +64,7 @@ const staffHomeActionsCoordinator: readonly CampaignHomeAction[] = [
|
|||
},
|
||||
{
|
||||
id: 'uncovered-municipalities',
|
||||
label: 'Ver quem ainda não está coberto',
|
||||
label: 'Ver esquecidos',
|
||||
icon: UserRoundSearch,
|
||||
description: 'Abrir a lista de municípios sem assessor (atalho — não é wizard)',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, type MouseEvent, type PointerEvent } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type MouseEvent,
|
||||
type PointerEvent,
|
||||
} from 'react'
|
||||
|
||||
export const CAMPAIGN_LONG_PRESS_MS = 450
|
||||
const CAMPAIGN_LONG_PRESS_SLOP_PX = 10
|
||||
|
||||
type CampaignLongPressPointerHandlers = {
|
||||
export type CampaignLongPressPointerHandlers = {
|
||||
onPointerDown: (event: PointerEvent) => void
|
||||
onPointerMove: (event: PointerEvent) => void
|
||||
onPointerUp: (event: PointerEvent) => void
|
||||
|
|
@ -13,6 +21,11 @@ type CampaignLongPressPointerHandlers = {
|
|||
onClick: (event: MouseEvent) => void
|
||||
}
|
||||
|
||||
export type CampaignLongPressResult = CampaignLongPressPointerHandlers & {
|
||||
/** True while the primary pointer is down (only when `enabled`). */
|
||||
pressing: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Long-press for coarse-pointer description drawers (B44). Short tap still
|
||||
* reaches `onClick`; the synthetic click after a long-press is suppressed.
|
||||
|
|
@ -25,7 +38,8 @@ export const useCampaignLongPress = ({
|
|||
enabled: boolean
|
||||
onLongPress: () => void
|
||||
onClick?: (event: MouseEvent) => void
|
||||
}): CampaignLongPressPointerHandlers => {
|
||||
}): CampaignLongPressResult => {
|
||||
const [pressing, setPressing] = useState(false)
|
||||
const suppressClickRef = useRef(false)
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const originRef = useRef<{ x: number; y: number } | null>(null)
|
||||
|
|
@ -37,9 +51,16 @@ export const useCampaignLongPress = ({
|
|||
}
|
||||
}, [])
|
||||
|
||||
const releasePress = useCallback(() => {
|
||||
clearTimer()
|
||||
originRef.current = null
|
||||
setPressing(false)
|
||||
}, [clearTimer])
|
||||
|
||||
const onPointerDown = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
if (!enabled || event.button !== 0) return
|
||||
setPressing(true)
|
||||
originRef.current = { x: event.clientX, y: event.clientY }
|
||||
clearTimer()
|
||||
timerRef.current = setTimeout(() => {
|
||||
|
|
@ -65,11 +86,6 @@ export const useCampaignLongPress = ({
|
|||
[clearTimer],
|
||||
)
|
||||
|
||||
const endPointer = useCallback(() => {
|
||||
clearTimer()
|
||||
originRef.current = null
|
||||
}, [clearTimer])
|
||||
|
||||
useEffect(() => () => clearTimer(), [clearTimer])
|
||||
|
||||
const onClickHandler = useCallback(
|
||||
|
|
@ -85,11 +101,15 @@ export const useCampaignLongPress = ({
|
|||
[onClick],
|
||||
)
|
||||
|
||||
return {
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp: endPointer,
|
||||
onPointerCancel: endPointer,
|
||||
onClick: onClickHandler,
|
||||
}
|
||||
return useMemo(
|
||||
() => ({
|
||||
pressing: enabled && pressing,
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp: releasePress,
|
||||
onPointerCancel: releasePress,
|
||||
onClick: onClickHandler,
|
||||
}),
|
||||
[enabled, onClickHandler, onPointerDown, onPointerMove, pressing, releasePress],
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { expect, test } from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
const staffActionLabels = [
|
||||
'Atualizar votos de um município',
|
||||
'Registrar o que aconteceu',
|
||||
'Ajustar votos',
|
||||
'Registrar sinal',
|
||||
'Mudar tendência',
|
||||
'Atualizar liderança',
|
||||
'Registrar pedido',
|
||||
'Ver quem ainda não está coberto',
|
||||
'Ver esquecidos',
|
||||
] as const
|
||||
|
||||
test.describe('Início — busca global (B47)', () => {
|
||||
|
|
@ -37,14 +37,15 @@ test.describe('Início — busca global (B47)', () => {
|
|||
})
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
const heading = page.getByRole('heading', { name: 'O que você quer fazer?', exact: true })
|
||||
await expect(heading).toBeVisible()
|
||||
const actionsSlot = page.locator('[data-slot="home-actions"]')
|
||||
await expect(actionsSlot).toBeVisible()
|
||||
await expect(page.getByLabel('Ações rápidas')).toBeVisible()
|
||||
|
||||
await page.getByLabel('Buscar na campanha').fill('ca')
|
||||
await expect(heading).toBeHidden({ timeout: 5000 })
|
||||
await expect(actionsSlot).toBeHidden({ timeout: 5000 })
|
||||
|
||||
await page.getByLabel('Buscar na campanha').fill('')
|
||||
await expect(heading).toBeVisible({ timeout: 5000 })
|
||||
await expect(actionsSlot).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('staff search shows municipality hits and opens detail (B48)', async ({
|
||||
|
|
@ -80,9 +81,7 @@ test.describe('Início — catálogo de ações (B45)', () => {
|
|||
})
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'O que você quer fazer?', exact: true }),
|
||||
).toBeVisible()
|
||||
await expect(page.getByLabel('Ações rápidas')).toBeVisible()
|
||||
|
||||
for (const label of staffActionLabels) {
|
||||
const link = page.getByRole('link', { name: label, exact: true })
|
||||
|
|
@ -90,7 +89,7 @@ test.describe('Início — catálogo de ações (B45)', () => {
|
|||
await expect(link.or(button)).toBeVisible()
|
||||
}
|
||||
|
||||
await page.getByRole('link', { name: 'Ver quem ainda não está coberto', exact: true }).click()
|
||||
await page.getByRole('link', { name: 'Ver esquecidos', exact: true }).click()
|
||||
await page.waitForURL(/\/campanha\/municipios/)
|
||||
const url = new URL(page.url())
|
||||
expect(url.searchParams.get('coverage')).toBe('sem_assessor')
|
||||
|
|
@ -104,9 +103,7 @@ test.describe('Início — catálogo de ações (B45)', () => {
|
|||
})
|
||||
|
||||
await campaign.login(page, leader.email!, leader.password)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'O que você quer fazer?', exact: true }),
|
||||
).toBeVisible()
|
||||
await expect(page.getByLabel('Ações rápidas')).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Cadastrar apoiador', exact: true }),
|
||||
|
|
|
|||
|
|
@ -120,6 +120,33 @@ describe('CampaignHomeActionButton', () => {
|
|||
expect(onClick).not.toHaveBeenCalled()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('sets data-pressing on coarse pointer down before long-press fires', () => {
|
||||
resetCampaignCoarsePointerForTests()
|
||||
matchMediaMock.mockImplementation((query: string) => ({
|
||||
matches: query.includes('coarse'),
|
||||
media: query,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}))
|
||||
|
||||
renderActionButton({
|
||||
label: 'Ver cobertura',
|
||||
icon: BarChart3,
|
||||
description: 'Lista filtrada.',
|
||||
href: '/campanha/municipios',
|
||||
})
|
||||
|
||||
const target = screen.getByRole('link', { name: 'Ver cobertura' })
|
||||
expect(target.getAttribute('data-pressing')).toBeNull()
|
||||
fireEvent.pointerDown(target, { button: 0, clientX: 0, clientY: 0 })
|
||||
expect(target.getAttribute('data-pressing')).toBe('true')
|
||||
fireEvent.pointerUp(target)
|
||||
expect(target.getAttribute('data-pressing')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CampaignHomeActionStrip', () => {
|
||||
|
|
@ -135,6 +162,7 @@ describe('CampaignHomeActionStrip', () => {
|
|||
|
||||
const scroller = container.querySelector('[aria-label="Ações rápidas"]')
|
||||
expect(scroller?.className).toContain('overflow-x-auto')
|
||||
expect(scroller?.className).toContain('scrollbar-width:none')
|
||||
expect(scroller?.querySelector('ul[role="list"]')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ const LongPressHarness = ({
|
|||
onLongPress: () => void
|
||||
onClick?: () => void
|
||||
}) => {
|
||||
const handlers = useCampaignLongPress({
|
||||
const { pressing, ...handlers } = useCampaignLongPress({
|
||||
enabled: true,
|
||||
onLongPress,
|
||||
onClick,
|
||||
})
|
||||
return (
|
||||
<button type="button" {...handlers}>
|
||||
<button type="button" data-pressing={pressing ? true : undefined} {...handlers}>
|
||||
Alvo
|
||||
</button>
|
||||
)
|
||||
|
|
@ -88,6 +88,20 @@ describe('useCampaignLongPress', () => {
|
|||
expect(onLongPress).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sets pressing while the primary pointer is down', () => {
|
||||
const onLongPress = vi.fn()
|
||||
render(<LongPressHarness onLongPress={onLongPress} />)
|
||||
|
||||
const target = screen.getByRole('button', { name: 'Alvo' })
|
||||
expect(target.getAttribute('data-pressing')).toBeNull()
|
||||
|
||||
fireEvent.pointerDown(target, { button: 0, clientX: 0, clientY: 0 })
|
||||
expect(target.getAttribute('data-pressing')).toBe('true')
|
||||
|
||||
fireEvent.pointerUp(target)
|
||||
expect(target.getAttribute('data-pressing')).toBeNull()
|
||||
})
|
||||
|
||||
it('does not call onLongPress after unmount during the hold', () => {
|
||||
const onLongPress = vi.fn()
|
||||
const { unmount } = render(<LongPressHarness onLongPress={onLongPress} />)
|
||||
|
|
|
|||
Loading…
Reference in a new issue