diff --git a/src/components/campaign/dashboard/HomeSearchHitRowsSkeleton.tsx b/src/components/campaign/dashboard/HomeSearchHitRowsSkeleton.tsx new file mode 100644 index 00000000..64623647 --- /dev/null +++ b/src/components/campaign/dashboard/HomeSearchHitRowsSkeleton.tsx @@ -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 }) => ( + +) diff --git a/src/components/campaign/shared/CampaignWizardShell.tsx b/src/components/campaign/shared/CampaignWizardShell.tsx index 053e6abb..ab895c24 100644 --- a/src/components/campaign/shared/CampaignWizardShell.tsx +++ b/src/components/campaign/shared/CampaignWizardShell.tsx @@ -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(null) const titleId = useId() @@ -56,24 +57,28 @@ export const CampaignWizardShell = ({ useSetCampaignWizardChrome(chrome) useEffect(() => { + if (!hasStepTitle) return titleRef.current?.focus() - }, [stepTitle]) + }, [hasStepTitle, stepTitle]) return (
{trailingAction ?
{trailingAction}
: null}
@@ -82,14 +87,16 @@ export const CampaignWizardShell = ({ {municipalityLabel}

) : null} -

- {stepTitle} -

+ {hasStepTitle ? ( +

+ {stepTitle} +

+ ) : null} {children}
diff --git a/src/components/campaign/shared/WizardMunicipalitySearchStep.tsx b/src/components/campaign/shared/WizardMunicipalitySearchStep.tsx index 85fd3c77..34a98a1c 100644 --- a/src/components/campaign/shared/WizardMunicipalitySearchStep.tsx +++ b/src/components/campaign/shared/WizardMunicipalitySearchStep.tsx @@ -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 ( -
- setRaw(event.target.value)} - autoComplete="off" - enterKeyHint="search" - aria-busy={resultsBusy || undefined} - maxLength={HOME_SEARCH_QUERY_MAX_LENGTH} - /> +
+
+ setRaw(event.target.value)} + autoComplete="off" + enterKeyHint="search" + aria-busy={resultsBusy || undefined} + maxLength={HOME_SEARCH_QUERY_MAX_LENGTH} + /> +
@@ -202,6 +208,7 @@ export const WizardMunicipalitySearchStep = ({ {results.message}

) : null} + {showSkeleton ? : null} {results.status === 'success' && displayRows.length > 0 ? (
    {displayRows.map(({ hit, continuityReason }) => ( diff --git a/tests/e2e/campaignHomeActions.e2e.spec.ts b/tests/e2e/campaignHomeActions.e2e.spec.ts index 881bc132..655b1a41 100644 --- a/tests/e2e/campaignHomeActions.e2e.spec.ts +++ b/tests/e2e/campaignHomeActions.e2e.spec.ts @@ -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() diff --git a/tests/e2e/campaignWizardChrome.e2e.spec.ts b/tests/e2e/campaignWizardChrome.e2e.spec.ts index c7256755..dd91c740 100644 --- a/tests/e2e/campaignWizardChrome.e2e.spec.ts +++ b/tests/e2e/campaignWizardChrome.e2e.spec.ts @@ -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() }) }) diff --git a/tests/unit/campaignWizardShell.unit.spec.tsx b/tests/unit/campaignWizardShell.unit.spec.tsx index 61b7dadb..9332a2db 100644 --- a/tests/unit/campaignWizardShell.unit.spec.tsx +++ b/tests/unit/campaignWizardShell.unit.spec.tsx @@ -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) => @@ -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(