diff --git a/tradein-mvp/frontend/src/app/v2/page.tsx b/tradein-mvp/frontend/src/app/v2/page.tsx
index 87136092..cd14c8f9 100644
--- a/tradein-mvp/frontend/src/app/v2/page.tsx
+++ b/tradein-mvp/frontend/src/app/v2/page.tsx
@@ -10,7 +10,7 @@
// a pending or errored street-deals / analytics call degrades its section to a
// fallback inside the mapper instead of blanking the page.
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
@@ -247,8 +247,10 @@ function PlaceholderPanel({
fontFamily: tokens.font.sans,
}}
>
-
(preflight not loaded → reset UA margin). */}
+
{title}
-
+
-
(preflight not loaded → reset UA margin). */}
+
СВОДКА ОБЪЕКТА
-
+
Появится после оценки квартиры.
@@ -382,6 +386,13 @@ export default function TradeInV2Page() {
const [nav, setNav] = useState(0);
const [drawerOpen, setDrawerOpen] = useState(false);
+
+ // #2264 C7: after a fresh estimate lands we move focus onto the result region
+ // (a labelled, tabIndex={-1} container in ResultPanel) so keyboard/SR users
+ // don't get dumped back on . `focusedEstimateRef` guards against
+ // re-focusing on unrelated re-renders / sub-hook updates.
+ const resultRegionRef = useRef(null);
+ const focusedEstimateRef = useRef(null);
// Locally-held just-computed estimate (so we render instantly after submit
// without a round-trip through the restore query).
const [freshResult, setFreshResult] = useState(
@@ -498,6 +509,23 @@ export default function TradeInV2Page() {
[estimate, analyticsData, streetDealsData],
);
+ // Move focus to the result region once a user-submitted estimate is rendered.
+ // Gated on `freshResult` (a submit, not a restore-by-id cold load, which must
+ // not steal focus) and on the result actually being on screen (!loading +
+ // resultPanelData). Fires once per distinct estimate id.
+ useEffect(() => {
+ if (
+ freshResult &&
+ !loading &&
+ hasEstimate &&
+ resultPanelData &&
+ focusedEstimateRef.current !== freshResult.estimate_id
+ ) {
+ focusedEstimateRef.current = freshResult.estimate_id;
+ resultRegionRef.current?.focus();
+ }
+ }, [freshResult, loading, hasEstimate, resultPanelData]);
+
// ── Overlay (section) mapped data. Computed unconditionally: the mappers
// accept nulls and degrade per-section, so an overlay opened before/without
// an estimate shows an honest empty shell — never the design fixtures as if
@@ -604,7 +632,13 @@ export default function TradeInV2Page() {
/>
);
} else if (estimate && !insufficient && resultPanelData) {
- middleContent = ;
+ middleContent = (
+
+ );
} else if (estimate && insufficient) {
middleContent = ;
} else {
@@ -655,6 +689,25 @@ export default function TradeInV2Page() {
>
+ {/* #2264 C5: the page's single visible "title" is the SVG logo in TopNav,
+ so give the document a real, visually-hidden
(page-has-heading-one
+ + a top of the heading order for the overlay/panel
s). */}
+
+ Мера — оценка квартиры
+
+
{/* Polite, visually-hidden status announcer for SR users. */}
-
-
+
-
+ = 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return few;
+ return many;
+}
+
+// Visually-hidden style for the polite suggestion-count live region.
+const srOnly: CSSProperties = {
+ position: "absolute",
+ width: 1,
+ height: 1,
+ padding: 0,
+ margin: -1,
+ overflow: "hidden",
+ clip: "rect(0 0 0 0)",
+ whiteSpace: "nowrap",
+ border: 0,
+};
+
interface DdProps {
open: boolean;
onToggle: () => void;
@@ -286,7 +308,7 @@ const styles = `
.pp-dd-trigger-disabled{border:1px dashed ${tokens.line}}
.pp-dd-opt{transition:background .15s}
.pp-dd-opt:hover{background:rgba(46,139,255,.1)}
-.pp-input:focus-visible{outline:none;border-color:${tokens.accent};box-shadow:0 0 0 3px rgba(46,139,255,.18)}
+.pp-input:focus-visible{outline:none;border-color:${tokens.accentDeep};box-shadow:0 0 0 2px ${tokens.accentDeep}}
.pp-eval-btn{background:linear-gradient(90deg,${tokens.accentDeep},${tokens.accent});box-shadow:0 2px 10px rgba(46,139,255,.25);transition:all .18s}
.pp-eval-btn:hover{background:${tokens.accentDeep};box-shadow:0 6px 22px rgba(46,139,255,.4)}
.pp-eval-btn:active{transform:translateY(1px)}
@@ -765,6 +787,14 @@ export default function ParamsPanel({
const addressPopupOpen = suggestOpen && addressQuery.trim().length >= 3;
const addressListboxOpen = addressPopupOpen && addressList.length > 0;
+ // #2264 C9: a polite announcement of the suggestion COUNT so SR users know the
+ // list changed under the combobox (role="listbox" is otherwise silent on
+ // update). The loading / empty states are already announced by the visible
+ // role="status" note below, so this region carries only the count.
+ const addressAnnounce = addressListboxOpen
+ ? `${addressList.length} ${pluralRu(addressList.length, "подсказка", "подсказки", "подсказок")}`
+ : "";
+
return (
+ {/* Polite announcement of the suggestion count (#2264 C9). */}
+
+ {addressAnnounce}
+
{
diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/ResultPanel.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/ResultPanel.tsx
index 47b04928..b881264f 100644
--- a/tradein-mvp/frontend/src/components/trade-in/v2/ResultPanel.tsx
+++ b/tradein-mvp/frontend/src/components/trade-in/v2/ResultPanel.tsx
@@ -1,6 +1,7 @@
"use client";
import { Fragment } from "react";
+import type { Ref } from "react";
import { tokens } from "./tokens";
import {
ranges,
@@ -47,6 +48,11 @@ const RESULT_FIXTURE: ResultPanelData = {
interface ResultPanelProps {
data?: ResultPanelData;
onNavigate: (i: number) => void;
+ // #2264 C7: after a successful estimate the page moves focus here (a labelled,
+ // programmatically-focusable region) so keyboard/SR users land on the result
+ // instead of the . tabIndex={-1} makes it focus()-able without adding a
+ // Tab stop.
+ regionRef?: Ref;
}
// Mini-histogram column tint: peak column = accent, its neighbours = mid, rest = lo.
@@ -85,10 +91,21 @@ function sourceName(name: string) {
export default function ResultPanel({
data = RESULT_FIXTURE,
onNavigate,
+ regionRef,
}: ResultPanelProps) {
return (