mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:53:05 +00:00
Merge pull request #145 from fsolla/cursor/pool-i142-0f38ef32-58e4
B107 — Wizard município — sem título + busca sticky + skeleton
This commit is contained in:
commit
97ae71b0fe
6 changed files with 109 additions and 34 deletions
|
|
@ -0,0 +1,25 @@
|
|||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { HOME_SEARCH_HIT_ROW_WRAPPER_CLASS } from '@/lib/homeSearchUi'
|
||||
|
||||
const HOME_SEARCH_HIT_ROW_SKELETON_PRIORITY_CLASS = 'h-4 w-4 shrink-0 rounded-sm'
|
||||
const HOME_SEARCH_HIT_ROW_SKELETON_PRIMARY_CLASS = 'h-4 w-40 max-w-full'
|
||||
const HOME_SEARCH_HIT_ROW_SKELETON_SECONDARY_CLASS = 'h-3 w-28 max-w-full'
|
||||
|
||||
export const HomeSearchHitRowsSkeleton = ({ count = 3 }: { count?: number }) => (
|
||||
<ul className="m-0 list-none p-0" aria-hidden="true">
|
||||
{Array.from({ length: count }, (_, index) => (
|
||||
<li key={index}>
|
||||
<div className={HOME_SEARCH_HIT_ROW_WRAPPER_CLASS}>
|
||||
<div className="flex min-h-11 items-center gap-3 py-2.5">
|
||||
<Skeleton className={HOME_SEARCH_HIT_ROW_SKELETON_PRIORITY_CLASS} />
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Skeleton className={HOME_SEARCH_HIT_ROW_SKELETON_PRIMARY_CLASS} />
|
||||
<Skeleton className={HOME_SEARCH_HIT_ROW_SKELETON_SECONDARY_CLASS} />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-8 shrink-0 tabular-nums" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
|
|
@ -9,12 +9,12 @@ import {
|
|||
type CampaignWizardChromeSkip,
|
||||
} from '@/components/campaign/shell/CampaignWizardChromeContext'
|
||||
import { CAMPAIGN_HOME } from '@/lib/campaignPaths'
|
||||
import { WIZARD_STEP_PENDING_MESSAGE } from '@/lib/campaignWizardCopy'
|
||||
import { WIZARD_STEP_PENDING_MESSAGE, wizardFlowChromeAriaLabel } from '@/lib/campaignWizardCopy'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export type CampaignWizardShellProps = {
|
||||
flowTitle: string
|
||||
stepTitle: string
|
||||
stepTitle?: string | null
|
||||
isEntryStep: boolean
|
||||
previousHref: string
|
||||
dismissHref?: string
|
||||
|
|
@ -37,6 +37,7 @@ export const CampaignWizardShell = ({
|
|||
contentAlign = 'start',
|
||||
children,
|
||||
}: CampaignWizardShellProps) => {
|
||||
const hasStepTitle = stepTitle != null && stepTitle !== ''
|
||||
const titleRef = useRef<HTMLHeadingElement>(null)
|
||||
const titleId = useId()
|
||||
|
||||
|
|
@ -56,24 +57,28 @@ export const CampaignWizardShell = ({
|
|||
useSetCampaignWizardChrome(chrome)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasStepTitle) return
|
||||
titleRef.current?.focus()
|
||||
}, [stepTitle])
|
||||
}, [hasStepTitle, stepTitle])
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full w-full flex-col">
|
||||
{trailingAction ? <div className="hidden justify-end md:flex">{trailingAction}</div> : null}
|
||||
|
||||
<main
|
||||
aria-labelledby={titleId}
|
||||
aria-labelledby={hasStepTitle ? titleId : undefined}
|
||||
aria-label={hasStepTitle ? undefined : wizardFlowChromeAriaLabel(flowTitle)}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col pt-3 pb-6 md:justify-start md:py-6',
|
||||
'flex flex-1 flex-col pb-6 md:justify-start md:py-6',
|
||||
hasStepTitle ? 'pt-3' : 'pt-2',
|
||||
contentAlign === 'end' ? 'justify-end md:justify-start' : 'justify-start',
|
||||
)}
|
||||
>
|
||||
<CampaignListResults
|
||||
pendingMessage={WIZARD_STEP_PENDING_MESSAGE}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col gap-6',
|
||||
'flex flex-1 flex-col',
|
||||
hasStepTitle ? 'gap-6' : 'gap-0',
|
||||
contentAlign === 'end' ? 'justify-end md:justify-start' : 'justify-start',
|
||||
)}
|
||||
>
|
||||
|
|
@ -82,14 +87,16 @@ export const CampaignWizardShell = ({
|
|||
{municipalityLabel}
|
||||
</p>
|
||||
) : null}
|
||||
<h1
|
||||
ref={titleRef}
|
||||
id={titleId}
|
||||
tabIndex={-1}
|
||||
className="text-xl font-semibold tracking-tight outline-none md:text-2xl"
|
||||
>
|
||||
{stepTitle}
|
||||
</h1>
|
||||
{hasStepTitle ? (
|
||||
<h1
|
||||
ref={titleRef}
|
||||
id={titleId}
|
||||
tabIndex={-1}
|
||||
className="text-xl font-semibold tracking-tight outline-none md:text-2xl"
|
||||
>
|
||||
{stepTitle}
|
||||
</h1>
|
||||
) : null}
|
||||
{children}
|
||||
</CampaignListResults>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { HomeSearchHitRow } from '@/components/campaign/dashboard/HomeSearchHitRow'
|
||||
import { HomeSearchHitRowsSkeleton } from '@/components/campaign/dashboard/HomeSearchHitRowsSkeleton'
|
||||
import { HomeSearchMunicipalityVoteTrailing } from '@/components/campaign/dashboard/HomeSearchMunicipalityVoteTrailing'
|
||||
import { useHomeSearchQuery } from '@/components/campaign/dashboard/useHomeSearchQuery'
|
||||
import { CampaignSearchInput } from '@/components/campaign/shared/CampaignSearchInput'
|
||||
|
|
@ -170,30 +171,35 @@ export const WizardMunicipalitySearchStep = ({
|
|||
results.mode === 'search' &&
|
||||
results.municipalities.length === 0
|
||||
|
||||
const showSkeleton =
|
||||
resultsBusy && displayRows.length === 0 && results.status !== 'error' && !showEmpty
|
||||
|
||||
return (
|
||||
<CampaignWizardShell
|
||||
flowTitle={wizardFlowTitleForSlug(actionSlug)}
|
||||
stepTitle={WIZARD_MUNICIPALITY_STEP_TITLE}
|
||||
stepTitle={null}
|
||||
isEntryStep
|
||||
previousHref={previousHref}
|
||||
dismissHref={previousHref}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<CampaignSearchInput
|
||||
id={WIZARD_MUNICIPALITY_SEARCH_INPUT_ID}
|
||||
label={WIZARD_MUNICIPALITY_SEARCH_LABEL}
|
||||
placeholder={WIZARD_MUNICIPALITY_SEARCH_PLACEHOLDER}
|
||||
value={query.raw}
|
||||
onChange={(event) => setRaw(event.target.value)}
|
||||
autoComplete="off"
|
||||
enterKeyHint="search"
|
||||
aria-busy={resultsBusy || undefined}
|
||||
maxLength={HOME_SEARCH_QUERY_MAX_LENGTH}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="sticky top-0 z-10 -mx-4 bg-background px-4 pb-2 md:-mx-6 md:px-6">
|
||||
<CampaignSearchInput
|
||||
id={WIZARD_MUNICIPALITY_SEARCH_INPUT_ID}
|
||||
label={WIZARD_MUNICIPALITY_SEARCH_LABEL}
|
||||
placeholder={WIZARD_MUNICIPALITY_SEARCH_PLACEHOLDER}
|
||||
value={query.raw}
|
||||
onChange={(event) => setRaw(event.target.value)}
|
||||
autoComplete="off"
|
||||
enterKeyHint="search"
|
||||
aria-busy={resultsBusy || undefined}
|
||||
maxLength={HOME_SEARCH_QUERY_MAX_LENGTH}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="region"
|
||||
aria-live="polite"
|
||||
aria-label="Resultados da busca"
|
||||
aria-label={WIZARD_MUNICIPALITY_STEP_TITLE}
|
||||
aria-busy={resultsBusy || undefined}
|
||||
className="min-w-0"
|
||||
>
|
||||
|
|
@ -202,6 +208,7 @@ export const WizardMunicipalitySearchStep = ({
|
|||
{results.message}
|
||||
</p>
|
||||
) : null}
|
||||
{showSkeleton ? <HomeSearchHitRowsSkeleton /> : null}
|
||||
{results.status === 'success' && displayRows.length > 0 ? (
|
||||
<ul className={HOME_SEARCH_GROUP_LIST_CLASS}>
|
||||
{displayRows.map(({ hit, continuityReason }) => (
|
||||
|
|
|
|||
|
|
@ -122,7 +122,8 @@ test.describe('Início — catálogo de ações (B45)', () => {
|
|||
|
||||
await page.getByRole('link', { name: 'Ajustar votos', exact: true }).click()
|
||||
await page.waitForURL(/\/campanha\/acoes\/atualizar-votos/)
|
||||
await expect(page.getByRole('heading', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toBeVisible()
|
||||
await expect(page.getByLabel('Buscar município')).toBeVisible()
|
||||
await expect(page.getByRole('region', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toBeVisible()
|
||||
|
||||
await page.goto('/campanha')
|
||||
await page.getByRole('link', { name: 'Ver esquecidos', exact: true }).click()
|
||||
|
|
@ -167,12 +168,13 @@ test.describe('Wizard — busca município (B60)', () => {
|
|||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/atualizar-votos')
|
||||
await expect(page.getByRole('heading', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toBeVisible()
|
||||
await expect(page.getByLabel('Buscar município')).toBeVisible()
|
||||
await expect(page.getByRole('region', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toBeVisible()
|
||||
|
||||
const search = page.getByLabel('Buscar município')
|
||||
await search.fill(municipality.name)
|
||||
|
||||
const results = page.getByRole('region', { name: 'Resultados da busca' })
|
||||
const results = page.getByRole('region', { name: WIZARD_MUNICIPALITY_STEP_TITLE })
|
||||
const hit = results.locator(`a[href*="municipio=${municipality.slug}"]`)
|
||||
await expect(hit).toBeVisible({ timeout: 15000 })
|
||||
await hit.click()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ test.describe('Wizard — header mobile (B75)', () => {
|
|||
const topBar = page.locator('[data-slot="campaign-mobile-top-bar"][data-mode="wizard"]')
|
||||
await expect(topBar).toBeVisible()
|
||||
await expect(topBar.getByText('Ajustar votos', { exact: true })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toHaveCount(0)
|
||||
await expect(page.getByLabel('Buscar município')).toBeVisible()
|
||||
await expect(page.getByRole('region', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: 'Sair da ação' })).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: /Voltar/ })).toHaveCount(0)
|
||||
|
||||
|
|
@ -55,6 +57,7 @@ test.describe('Wizard — header mobile (B75)', () => {
|
|||
|
||||
await page.getByRole('link', { name: /Voltar/ }).click()
|
||||
await page.waitForURL(/\/campanha\/acoes\/atualizar-votos\/?$/)
|
||||
await expect(page.getByRole('heading', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toHaveCount(0)
|
||||
await expect(page.getByLabel('Buscar município')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizard
|
|||
import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar'
|
||||
import { CampaignWizardChromeProvider } from '@/components/campaign/shell/CampaignWizardChromeContext'
|
||||
import { SidebarProvider } from '@/components/ui/Sidebar'
|
||||
import { WIZARD_MUNICIPALITY_STEP_TITLE } from '@/lib/campaignWizardCopy'
|
||||
import { WIZARD_MUNICIPALITY_STEP_TITLE, wizardFlowChromeAriaLabel } from '@/lib/campaignWizardCopy'
|
||||
import { stubMatchMedia } from '../helpers/matchMedia'
|
||||
|
||||
const renderWizardShell = (props: React.ComponentProps<typeof CampaignWizardShell>) =>
|
||||
|
|
@ -86,6 +86,37 @@ describe('CampaignWizardShell', () => {
|
|||
expect(main.getAttribute('aria-labelledby')).toBe(heading.id)
|
||||
})
|
||||
|
||||
it('omits step heading when stepTitle is null and labels main with flow title', () => {
|
||||
const { container } = renderWizardShell({
|
||||
flowTitle: 'Ajustar votos',
|
||||
stepTitle: null,
|
||||
isEntryStep: true,
|
||||
previousHref: '/campanha',
|
||||
children: 'Corpo',
|
||||
})
|
||||
|
||||
expect(screen.queryByRole('heading', { level: 1 })).toBeNull()
|
||||
const main = within(container).getByRole('main')
|
||||
expect(main.getAttribute('aria-labelledby')).toBeNull()
|
||||
expect(main.getAttribute('aria-label')).toBe(wizardFlowChromeAriaLabel('Ajustar votos'))
|
||||
})
|
||||
|
||||
it('uses tighter top padding on mobile when step title is omitted', () => {
|
||||
const { container } = renderWizardShell({
|
||||
flowTitle: 'Ajustar votos',
|
||||
stepTitle: null,
|
||||
isEntryStep: true,
|
||||
previousHref: '/campanha',
|
||||
children: null,
|
||||
})
|
||||
|
||||
const main = container.querySelector('main')
|
||||
expect(main?.className).toMatch(/pt-2/)
|
||||
expect(main?.className).not.toMatch(/pt-3/)
|
||||
expect(main?.className).toMatch(/pb-6/)
|
||||
expect(main?.className).toMatch(/md:py-6/)
|
||||
})
|
||||
|
||||
it('moves focus to the step title when stepTitle changes', () => {
|
||||
const { rerender } = render(
|
||||
<SidebarProvider>
|
||||
|
|
|
|||
Loading…
Reference in a new issue