fix(tradein): recharts a11y — aria-hidden чарты, h1, aria-label th (#835)

axe-core на /ui-preview/estimate, 3 дефекта:
1. svg-img-alt: recharts symbol'ы role=img без accessible-name (Exposure/
   Distribution/PriceTrend/PriceHistory) → aria-hidden на wrapper-div чарта
   (декоративный; данные в заголовке/легенде/тексте карточки).
2. page-has-heading-one: на /ui-preview нет h1 (HeroSummary=h2; real page.tsx
   h1 есть) → визуально-скрытый h1 «Оценка стоимости квартиры».
3. empty-table-header: пустой <th/> (actions-колонка) в Listings/Deals/
   StreetDeals → aria-label=«Действия».

Refs #835, #801
This commit is contained in:
bot-frontend 2026-05-30 23:45:15 +03:00
parent cd406225fc
commit b1b0dc4b0f
8 changed files with 30 additions and 7 deletions

View file

@ -119,6 +119,24 @@ function PreviewContent() {
{/* Те же layout-классы, что у реальной страницы (page.tsx): .page .result-col
(flex-column gap, --container ширина). Без них карточки рендерились узко/«мобильно». */}
<main className="page">
{/* #835: страница-результат рендерит HeroSummary как h2 добавляем
визуально-скрытый h1 (page-has-heading-one). На реальной page.tsx h1
есть в page-title; здесь (preview, аудит) обёртки нет. */}
<h1
style={{
position: "absolute",
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
whiteSpace: "nowrap",
border: 0,
}}
>
Оценка стоимости квартиры
</h1>
<section className="result-col">
{/* 1. SourcesProgress */}
<SourcesProgress estimate={FIXTURE_ESTIMATE} isPending={false} />

View file

@ -128,7 +128,7 @@ export function DealsCard({ estimate }: Props) {
<th className="num"> / м²</th>
<th className="num">Цена сделки</th>
<th className="num">Дата</th>
<th />
<th aria-label="Действия" />
</tr>
</thead>
<tbody>

View file

@ -94,7 +94,8 @@ export function DistributionCard({ estimate }: Props) {
</div>
</div>
<div style={{ width: "100%", height: 260, padding: "12px 8px 0" }}>
{/* #835: декоративный чарт — aria-hidden (данные в тексте/легенде карточки). */}
<div aria-hidden="true" style={{ width: "100%", height: 260, padding: "12px 8px 0" }}>
<ResponsiveContainer>
<BarChart data={bins} margin={{ top: 8, right: 16, left: 8, bottom: 8 }}>
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />

View file

@ -72,7 +72,9 @@ export function ExposureCard({ estimate }: Props) {
</div>
</div>
<div style={{ width: "100%", height: 280, padding: "12px 8px 0" }}>
{/* #835: чарт декоративный (данные в заголовке/легенде/тексте карточки);
aria-hidden убирает recharts symbol'ы role="img" без accessible-name. */}
<div aria-hidden="true" style={{ width: "100%", height: 280, padding: "12px 8px 0" }}>
<ResponsiveContainer>
<ScatterChart margin={{ top: 8, right: 20, left: 8, bottom: 16 }}>
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />

View file

@ -258,7 +258,7 @@ export function ListingsCard({ estimate, estimateId }: Props) {
<th className="num"> / м²</th>
<th className="num">Стоимость</th>
<th className="num">Дист.</th>
<th />
<th aria-label="Действия" />
</tr>
</thead>
<tbody>

View file

@ -52,7 +52,8 @@ export function PriceHistoryChart({ points }: Props) {
Медиана /м² по году · Avito + Яндекс (по {totalLots} лотам)
</small>
</header>
<div style={{ width: "100%", height: 240 }}>
{/* #835: декоративный чарт — aria-hidden (данные в тексте/легенде карточки). */}
<div aria-hidden="true" style={{ width: "100%", height: 240 }}>
<ResponsiveContainer>
<LineChart
data={pivoted}

View file

@ -71,7 +71,8 @@ export function PriceTrendCard({ estimate }: Props) {
</div>
</div>
<div style={{ width: "100%", height: 240, padding: "12px 8px 0" }}>
{/* #835: декоративный чарт — aria-hidden (данные в тексте/легенде карточки). */}
<div aria-hidden="true" style={{ width: "100%", height: 240, padding: "12px 8px 0" }}>
<ResponsiveContainer>
<LineChart data={data} margin={{ top: 8, right: 16, left: 8, bottom: 8 }}>
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />

View file

@ -165,7 +165,7 @@ export function StreetDealsCard({ estimate }: Props) {
<th className="num">/м²</th>
<th className="num">Дата</th>
{hasAnyListing && <th>Объявление до сделки</th>}
<th />
<th aria-label="Действия" />
</tr>
</thead>
<tbody>