teqo/src/components/campaign/shared/CampaignSearchForm.tsx
Francisco Ignácio Fontoura Solla abe37cbc5c B33+: shared list filter navigation hook
Extract the debounce/dedup/transition scaffold that StateDeputyFilters,
TerritoryFilters and MunicipalityFilters each reimplemented into
useCampaignListFilterNavigation, and unify the two behaviors that had
silently diverged: touching a filter mid-typing used to discard the
pending search on dobradinhas and territorios, and the debounce timer
captured a stale state, so a sort clicked within its window was reverted
a second later. Also folds the six copies of the pending-transition
fallback into useCampaignListTransition and drops a redundant aria-live
that made the municipality summary the third live region on its page.
2026-07-27 09:05:08 -03:00

63 lines
2 KiB
TypeScript

'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useCampaignListTransition } from '@/components/campaign/shared/CampaignListPending'
import { Button } from '@/components/ui/button'
/**
* Name-search form for simple list pages (lideranças, organizações,
* dobradinhas): keeps the input value locally (optimistic) and navigates in a
* transition so the results region dims instead of a dead full-page wait.
*/
export const CampaignSearchForm = ({
ariaLabel,
placeholder,
initialQuery,
basePath,
}: {
ariaLabel: string
placeholder: string
initialQuery: string
/** List route; the query lands as `?q=` (serializable — no function props across the RSC boundary). */
basePath: string
}) => {
const router = useRouter()
const { isPending, startTransition } = useCampaignListTransition()
const [query, setQuery] = useState(initialQuery)
const hrefForQuery = (value: string) =>
value ? `${basePath}?q=${encodeURIComponent(value)}` : basePath
return (
<form
role="search"
className="flex gap-2 transition-opacity data-[pending=true]:opacity-70"
data-pending={isPending || undefined}
aria-busy={isPending}
onSubmit={(event) => {
event.preventDefault()
startTransition(() => {
router.replace(hrefForQuery(query), { scroll: false })
})
}}
>
<p className="sr-only" aria-live="polite">
{isPending ? 'Atualizando resultados…' : ''}
</p>
<input
type="search"
name="q"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={placeholder}
aria-label={ariaLabel}
className="min-h-11 w-full max-w-md rounded-[6px] border border-input bg-background px-3 text-sm outline-none focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/30"
/>
<Button type="submit" variant="secondary" className="min-h-11" disabled={isPending}>
Buscar
</Button>
</form>
)
}