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 */} {/* CONTENT WRAP */}
<div <div
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{ style={{
position: "absolute", position: "absolute",
inset: 14, inset: 14,
@ -710,7 +709,17 @@ export default function TradeInV2Page() {
flexDirection: "column", 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 <TopNav
active={nav} active={nav}
onNavigate={setNav} onNavigate={setNav}
@ -719,7 +728,10 @@ export default function TradeInV2Page() {
onLogout={logout} onLogout={logout}
/> />
</nav> </nav>
<main style={{ display: "contents" }}> <main
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<HeroBar <HeroBar
data={{ report, object: objectInfo }} data={{ report, object: objectInfo }}
estimateId={mounted ? currentEstimateId : null} estimateId={mounted ? currentEstimateId : null}
@ -749,7 +761,10 @@ export default function TradeInV2Page() {
</div> </div>
</main> </main>
<footer style={{ display: "contents" }}> <footer
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<Footer data={report} hasEstimate={hasEstimate} /> <Footer data={report} hasEstimate={hasEstimate} />
</footer> </footer>
</div> </div>

View file

@ -30,6 +30,11 @@ interface SectionOverlayProps {
cache?: CacheData; 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({ export default function SectionOverlay({
active, active,
onClose, onClose,
@ -105,6 +110,28 @@ export default function SectionOverlay({
}, []); }, []);
return ( 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 <div
ref={dialogRef} ref={dialogRef}
role="dialog" role="dialog"
@ -115,7 +142,7 @@ export default function SectionOverlay({
position: "absolute", position: "absolute",
left: 28, left: 28,
right: 28, right: 28,
top: 80, top: PANEL_TOP,
bottom: 30, bottom: 30,
zIndex: 30, zIndex: 30,
display: "flex", display: "flex",
@ -131,8 +158,19 @@ export default function SectionOverlay({
}} }}
> >
<style>{` <style>{`
.tiv2-so-back { color: ${tokens.ink}; transition: all .15s; } .tiv2-so-back {
.tiv2-so-back:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; } 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> `}</style>
{/* HUD corner brackets */} {/* HUD corner brackets */}
@ -198,23 +236,33 @@ export default function SectionOverlay({
type="button" type="button"
onClick={onClose} onClick={onClose}
className="tiv2-so-back" className="tiv2-so-back"
aria-label="Вернуться к оценке"
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: 8, gap: 8,
cursor: "pointer", cursor: "pointer",
border: `1px solid ${tokens.line}`, border: `1px solid ${tokens.accentDeep}`,
borderRadius: 6, borderRadius: 6,
padding: "6px 12px", padding: "8px 15px",
background: tokens.surface.w60, // background + shadow live in .tiv2-so-back so :hover can override
// reset UA button defaults while keeping the original box look; // them (an inline background would beat the hover rule on
// base colour lives in .tiv2-so-back so :hover can win // specificity). Solid accentDeep keeps white text ≥4.5:1 (AA).
fontFamily: "inherit", fontFamily: "inherit",
}} }}
> >
<span style={{ fontSize: 13 }}></span>
<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> </span>
@ -238,5 +286,6 @@ export default function SectionOverlay({
{active === 4 && <CacheView data={cache} />} {active === 4 && <CacheView data={cache} />}
</div> </div>
</div> </div>
</>
); );
} }