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 */}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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,138 +110,182 @@ export default function SectionOverlay({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dialogRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="v2-overlay-title"
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 28,
|
||||
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 */}
|
||||
<>
|
||||
{/* 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: 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",
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: PANEL_TOP,
|
||||
bottom: 0,
|
||||
zIndex: 29,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* overlay header */}
|
||||
<div
|
||||
ref={dialogRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="v2-overlay-title"
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
flex: "0 0 auto",
|
||||
position: "absolute",
|
||||
left: 28,
|
||||
right: 28,
|
||||
top: PANEL_TOP,
|
||||
bottom: 30,
|
||||
zIndex: 30,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "16px 24px",
|
||||
borderBottom: "1px solid #cdd9e6",
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: 11 }}>
|
||||
<span
|
||||
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"
|
||||
<style>{`
|
||||
.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 */}
|
||||
<div
|
||||
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",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
cursor: "pointer",
|
||||
border: `1px solid ${tokens.line}`,
|
||||
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",
|
||||
justifyContent: "space-between",
|
||||
padding: "16px 24px",
|
||||
borderBottom: "1px solid #cdd9e6",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 13 }}>←</span>
|
||||
<span
|
||||
style={{ fontSize: 10, letterSpacing: 1.5, color: tokens.body2 }}
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: 11 }}>
|
||||
<span
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
style={{ fontSize: 15, color: tokens.onAccent, lineHeight: 1 }}
|
||||
>
|
||||
←
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: 1.5,
|
||||
color: tokens.onAccent,
|
||||
}}
|
||||
>
|
||||
К ОЦЕНКЕ
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* overlay body */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
padding: "22px 24px 26px",
|
||||
}}
|
||||
>
|
||||
{active === 1 && <HistoryView data={history} />}
|
||||
{active === 2 && <SourcesView data={sources} />}
|
||||
{active === 3 && (
|
||||
<AnalyticsView data={analytics} onNavigate={onNavigate} />
|
||||
)}
|
||||
{active === 4 && <CacheView data={cache} />}
|
||||
{/* overlay body */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
padding: "22px 24px 26px",
|
||||
}}
|
||||
>
|
||||
{active === 1 && <HistoryView data={history} />}
|
||||
{active === 2 && <SourcesView data={sources} />}
|
||||
{active === 3 && (
|
||||
<AnalyticsView data={analytics} onNavigate={onNavigate} />
|
||||
)}
|
||||
{active === 4 && <CacheView data={cache} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue