fix(tradein/v2): keep top nav active over overlays, stronger «К оценке», backdrop-close
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:
parent
3b9dfdb44e
commit
35a0e29122
2 changed files with 182 additions and 118 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,138 +110,182 @@ export default function SectionOverlay({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
ref={dialogRef}
|
{/* Click-outside backdrop. A transparent layer covering the artboard BELOW
|
||||||
role="dialog"
|
the TopNav (top: PANEL_TOP) — clicking the margin area around the panel
|
||||||
aria-modal="true"
|
closes the overlay (in addition to Esc). It is a SIBLING of the dialog
|
||||||
aria-labelledby="v2-overlay-title"
|
(not an ancestor) and sits one z-index below it, so clicks inside the
|
||||||
tabIndex={-1}
|
panel / on its buttons never reach this handler (they don't bubble to a
|
||||||
style={{
|
sibling) and therefore never close the overlay. Starting below the nav
|
||||||
position: "absolute",
|
keeps the top tabs clickable. aria-hidden + no tabIndex → invisible to
|
||||||
left: 28,
|
AT and keyboard; Esc remains the keyboard close path. */}
|
||||||
right: 28,
|
|
||||||
top: 80,
|
|
||||||
bottom: 30,
|
|
||||||
zIndex: 30,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
background:
|
|
||||||
"linear-gradient(180deg,rgba(244,248,252,.985),rgba(235,242,250,.985))",
|
|
||||||
backdropFilter: "blur(12px)",
|
|
||||||
border: `1px solid ${tokens.line3}`,
|
|
||||||
borderRadius: 8,
|
|
||||||
boxShadow: "0 22px 60px rgba(40,80,130,.18)",
|
|
||||||
overflow: "hidden",
|
|
||||||
fontFamily: tokens.font.sans,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<style>{`
|
|
||||||
.tiv2-so-back { color: ${tokens.ink}; transition: all .15s; }
|
|
||||||
.tiv2-so-back:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
{/* HUD corner brackets */}
|
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
onClick={onClose}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 12,
|
left: 0,
|
||||||
top: 12,
|
right: 0,
|
||||||
width: 15,
|
top: PANEL_TOP,
|
||||||
height: 15,
|
bottom: 0,
|
||||||
borderLeft: `1.5px solid ${tokens.accent}`,
|
zIndex: 29,
|
||||||
borderTop: `1.5px solid ${tokens.accent}`,
|
|
||||||
pointerEvents: "none",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
right: 12,
|
|
||||||
bottom: 12,
|
|
||||||
width: 15,
|
|
||||||
height: 15,
|
|
||||||
borderRight: `1.5px solid ${tokens.accent}`,
|
|
||||||
borderBottom: `1.5px solid ${tokens.accent}`,
|
|
||||||
pointerEvents: "none",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* overlay header */}
|
|
||||||
<div
|
<div
|
||||||
|
ref={dialogRef}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="v2-overlay-title"
|
||||||
|
tabIndex={-1}
|
||||||
style={{
|
style={{
|
||||||
flex: "0 0 auto",
|
position: "absolute",
|
||||||
|
left: 28,
|
||||||
|
right: 28,
|
||||||
|
top: PANEL_TOP,
|
||||||
|
bottom: 30,
|
||||||
|
zIndex: 30,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
flexDirection: "column",
|
||||||
justifyContent: "space-between",
|
background:
|
||||||
padding: "16px 24px",
|
"linear-gradient(180deg,rgba(244,248,252,.985),rgba(235,242,250,.985))",
|
||||||
borderBottom: "1px solid #cdd9e6",
|
backdropFilter: "blur(12px)",
|
||||||
|
border: `1px solid ${tokens.line3}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: "0 22px 60px rgba(40,80,130,.18)",
|
||||||
|
overflow: "hidden",
|
||||||
|
fontFamily: tokens.font.sans,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "baseline", gap: 11 }}>
|
<style>{`
|
||||||
<span
|
.tiv2-so-back {
|
||||||
style={{
|
transition: all .15s;
|
||||||
fontFamily: tokens.font.mono,
|
background: ${tokens.accentDeep};
|
||||||
fontSize: 15,
|
box-shadow: 0 3px 10px rgba(46,139,255,.28);
|
||||||
color: tokens.accent,
|
}
|
||||||
}}
|
.tiv2-so-back:hover {
|
||||||
>
|
box-shadow: 0 6px 16px rgba(46,139,255,.45);
|
||||||
{overlayNums[active]}
|
transform: translateY(-1px);
|
||||||
</span>
|
}
|
||||||
<span
|
.tiv2-so-back:active {
|
||||||
id="v2-overlay-title"
|
transform: translateY(0);
|
||||||
style={{
|
box-shadow: 0 2px 6px rgba(46,139,255,.3);
|
||||||
fontSize: 14,
|
}
|
||||||
fontWeight: 600,
|
`}</style>
|
||||||
letterSpacing: 2.5,
|
|
||||||
color: tokens.ink2,
|
{/* HUD corner brackets */}
|
||||||
}}
|
<div
|
||||||
>
|
|
||||||
{overlayTitles[active]}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="tiv2-so-back"
|
|
||||||
style={{
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 12,
|
||||||
|
top: 12,
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
borderLeft: `1.5px solid ${tokens.accent}`,
|
||||||
|
borderTop: `1.5px solid ${tokens.accent}`,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: 12,
|
||||||
|
bottom: 12,
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
borderRight: `1.5px solid ${tokens.accent}`,
|
||||||
|
borderBottom: `1.5px solid ${tokens.accent}`,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* overlay header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: "0 0 auto",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 8,
|
justifyContent: "space-between",
|
||||||
cursor: "pointer",
|
padding: "16px 24px",
|
||||||
border: `1px solid ${tokens.line}`,
|
borderBottom: "1px solid #cdd9e6",
|
||||||
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
|
|
||||||
fontFamily: "inherit",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 13 }}>←</span>
|
<div style={{ display: "flex", alignItems: "baseline", gap: 11 }}>
|
||||||
<span
|
<span
|
||||||
style={{ fontSize: 10, letterSpacing: 1.5, color: tokens.body2 }}
|
style={{
|
||||||
|
fontFamily: tokens.font.mono,
|
||||||
|
fontSize: 15,
|
||||||
|
color: tokens.accent,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{overlayNums[active]}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
id="v2-overlay-title"
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: 2.5,
|
||||||
|
color: tokens.ink2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{overlayTitles[active]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="tiv2-so-back"
|
||||||
|
aria-label="Вернуться к оценке"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
cursor: "pointer",
|
||||||
|
border: `1px solid ${tokens.accentDeep}`,
|
||||||
|
borderRadius: 6,
|
||||||
|
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
|
||||||
</span>
|
style={{ fontSize: 15, color: tokens.onAccent, lineHeight: 1 }}
|
||||||
</button>
|
>
|
||||||
</div>
|
←
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: 1.5,
|
||||||
|
color: tokens.onAccent,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
К ОЦЕНКЕ
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* overlay body */}
|
{/* overlay body */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
padding: "22px 24px 26px",
|
padding: "22px 24px 26px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{active === 1 && <HistoryView data={history} />}
|
{active === 1 && <HistoryView data={history} />}
|
||||||
{active === 2 && <SourcesView data={sources} />}
|
{active === 2 && <SourcesView data={sources} />}
|
||||||
{active === 3 && (
|
{active === 3 && (
|
||||||
<AnalyticsView data={analytics} onNavigate={onNavigate} />
|
<AnalyticsView data={analytics} onNavigate={onNavigate} />
|
||||||
)}
|
)}
|
||||||
{active === 4 && <CacheView data={cache} />}
|
{active === 4 && <CacheView data={cache} />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue