fix(tradein/v2): keep top nav active over overlays, stronger «К оценке», backdrop-close
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped

Three UX fixes on the /trade-in/v2 overlays (feedback):

1. TopNav stays interactive while a section overlay is open. The a11y
   audit #2081 put inert on the whole content wrap (which contains
   TopNav), so opening an overlay disabled the top tabs. Move inert off
   the wrap onto <main>/<footer> only; the nav is inert just for the
   full-screen LocationDrawer. Users can now switch sections directly
   from an open overlay. role=dialog/aria-modal/Esc/focus-trap kept.

2. «← К ОЦЕНКЕ» made a clear filled primary button (solid accentDeep bg,
   white text ≥4.9:1 AA, shadow, hover lift) instead of the weak ghost
   pill. Uses existing accent tokens only.

3. Backdrop click closes the overlay. Add a transparent sibling layer
   below the panel (z below it, starting under the nav so tabs stay
   clickable); clicks in the margin close it. Clicks inside the panel /
   on its buttons never reach it (sibling, not ancestor) so they don't
   close. Esc + focus restore preserved.
This commit is contained in:
bot-backend 2026-07-02 21:28:47 +03:00
parent 3b9dfdb44e
commit 35a0e29122
2 changed files with 182 additions and 118 deletions

View file

@ -701,7 +701,6 @@ export default function TradeInV2Page() {
{/* CONTENT WRAP */}
<div
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{
position: "absolute",
inset: 14,
@ -710,7 +709,17 @@ export default function TradeInV2Page() {
flexDirection: "column",
}}
>
<nav aria-label="Разделы" style={{ display: "contents" }}>
{/* TopNav stays interactive while a section overlay is open so the
user can switch sections directly from the overlay. Only the
LocationDrawer (a full modal over the whole HUD) inerts it. The
dashboard body (<main>/<footer>) is still inerted behind ANY
overlay/drawer preserving the focus + click guard from a11y
audit #2081 for everything except the top tabs. */}
<nav
aria-label="Разделы"
inert={drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<TopNav
active={nav}
onNavigate={setNav}
@ -719,7 +728,10 @@ export default function TradeInV2Page() {
onLogout={logout}
/>
</nav>
<main style={{ display: "contents" }}>
<main
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<HeroBar
data={{ report, object: objectInfo }}
estimateId={mounted ? currentEstimateId : null}
@ -749,7 +761,10 @@ export default function TradeInV2Page() {
</div>
</main>
<footer style={{ display: "contents" }}>
<footer
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<Footer data={report} hasEstimate={hasEstimate} />
</footer>
</div>

View file

@ -30,6 +30,11 @@ interface SectionOverlayProps {
cache?: CacheData;
}
// Panel geometry (relative to the artboard). Kept as a constant so the click
// backdrop can align its top edge to the panel — the backdrop starts BELOW the
// TopNav so the top tabs stay clickable while an overlay is open.
const PANEL_TOP = 80;
export default function SectionOverlay({
active,
onClose,
@ -105,6 +110,28 @@ export default function SectionOverlay({
}, []);
return (
<>
{/* Click-outside backdrop. A transparent layer covering the artboard BELOW
the TopNav (top: PANEL_TOP) clicking the margin area around the panel
closes the overlay (in addition to Esc). It is a SIBLING of the dialog
(not an ancestor) and sits one z-index below it, so clicks inside the
panel / on its buttons never reach this handler (they don't bubble to a
sibling) and therefore never close the overlay. Starting below the nav
keeps the top tabs clickable. aria-hidden + no tabIndex invisible to
AT and keyboard; Esc remains the keyboard close path. */}
<div
aria-hidden="true"
onClick={onClose}
style={{
position: "absolute",
left: 0,
right: 0,
top: PANEL_TOP,
bottom: 0,
zIndex: 29,
}}
/>
<div
ref={dialogRef}
role="dialog"
@ -115,7 +142,7 @@ export default function SectionOverlay({
position: "absolute",
left: 28,
right: 28,
top: 80,
top: PANEL_TOP,
bottom: 30,
zIndex: 30,
display: "flex",
@ -131,8 +158,19 @@ export default function SectionOverlay({
}}
>
<style>{`
.tiv2-so-back { color: ${tokens.ink}; transition: all .15s; }
.tiv2-so-back:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
.tiv2-so-back {
transition: all .15s;
background: ${tokens.accentDeep};
box-shadow: 0 3px 10px rgba(46,139,255,.28);
}
.tiv2-so-back:hover {
box-shadow: 0 6px 16px rgba(46,139,255,.45);
transform: translateY(-1px);
}
.tiv2-so-back:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(46,139,255,.3);
}
`}</style>
{/* HUD corner brackets */}
@ -198,23 +236,33 @@ export default function SectionOverlay({
type="button"
onClick={onClose}
className="tiv2-so-back"
aria-label="Вернуться к оценке"
style={{
display: "flex",
alignItems: "center",
gap: 8,
cursor: "pointer",
border: `1px solid ${tokens.line}`,
border: `1px solid ${tokens.accentDeep}`,
borderRadius: 6,
padding: "6px 12px",
background: tokens.surface.w60,
// reset UA button defaults while keeping the original box look;
// base colour lives in .tiv2-so-back so :hover can win
padding: "8px 15px",
// background + shadow live in .tiv2-so-back so :hover can override
// them (an inline background would beat the hover rule on
// specificity). Solid accentDeep keeps white text ≥4.5:1 (AA).
fontFamily: "inherit",
}}
>
<span style={{ fontSize: 13 }}></span>
<span
style={{ fontSize: 10, letterSpacing: 1.5, color: tokens.body2 }}
style={{ fontSize: 15, color: tokens.onAccent, lineHeight: 1 }}
>
</span>
<span
style={{
fontSize: 11,
fontWeight: 600,
letterSpacing: 1.5,
color: tokens.onAccent,
}}
>
К ОЦЕНКЕ
</span>
@ -238,5 +286,6 @@ export default function SectionOverlay({
{active === 4 && <CacheView data={cache} />}
</div>
</div>
</>
);
}