fix(a11y): close cross-app interaction gaps
CI and Deploy / test (pull_request) Successful in 4m54s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 17:49:42 +02:00
parent deed948183
commit a7c25499bb
23 changed files with 224 additions and 61 deletions
+1 -1
View File
@@ -352,7 +352,7 @@ export default function CvBuilderEditor() {
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "minmax(360px, 460px) 1fr" }, gap: 2, alignItems: "start" }}>
<Paper sx={{ p: 2, borderRadius: 4, position: { md: "sticky" }, top: 12, minWidth: 0, maxHeight: { md: "calc(100vh - 24px)" }, overflowY: { md: "auto" } }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<Tooltip title="Back to CVs"><IconButton size="small" onClick={() => navigate("/career/builder")}><ArrowBackIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Back to CVs"><IconButton size="small" aria-label="Back to CVs" onClick={() => navigate("/career/builder")}><ArrowBackIcon fontSize="small" /></IconButton></Tooltip>
<TextField variant="standard" fullWidth value={name} onChange={(e) => renameVariant(e.target.value)}
error={!name.trim()} helperText={!name.trim() ? "Enter a name before saving." : undefined}
slotProps={{ input: { style: { fontWeight: 800, fontSize: "1.05rem" } }, htmlInput: { "aria-label": "CV name" } }} />
+14 -1
View File
@@ -99,7 +99,20 @@ export default function CvBuilderPage() {
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr", md: "1fr 1fr 1fr" }, gap: 2 }}>
{variants.map((v) => (
<Paper key={v.id} sx={{ p: 2, borderRadius: 4, cursor: "pointer", "&:hover": { boxShadow: 4 } }} onClick={() => navigate(`/career/builder/${v.id}`)}>
<Paper
key={v.id}
role="link"
tabIndex={0}
aria-label={`Open ${v.name}`}
sx={{ p: 2, borderRadius: 4, cursor: "pointer", "&:hover": { boxShadow: 4 }, "&:focus-visible": { outline: "3px solid", outlineColor: "primary.main", outlineOffset: 2 } }}
onClick={() => navigate(`/career/builder/${v.id}`)}
onKeyDown={(event) => {
if (event.target === event.currentTarget && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
navigate(`/career/builder/${v.id}`);
}
}}
>
<Stack direction="row" alignItems="flex-start" justifyContent="space-between">
<Typography sx={{ fontWeight: 800 }}>{v.name}</Typography>
<IconButton size="small" aria-label={`Actions for ${v.name}`} onClick={(e) => { e.stopPropagation(); setMenu({ anchor: e.currentTarget, id: v.id }); }}>
+29 -8
View File
@@ -3,6 +3,9 @@ import { useParams } from "react-router-dom";
import { api } from "../api";
const A4_WIDTH_PX = (210 / 25.4) * 96;
const A4_HEIGHT_PX = (297 / 25.4) * 96;
// Anonymous read-only public CV at /cv/:slug. Renders the server-produced HTML in a sandboxed
// iframe. noindex is enforced server-side (X-Robots-Tag) and reinforced with a meta tag here.
export default function PublicCvPage() {
@@ -10,6 +13,8 @@ export default function PublicCvPage() {
const pdfUrl = `${api.defaults.baseURL}/public-cv/${slug}/pdf`;
const [html, setHtml] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [frameScale, setFrameScale] = useState(1);
const [frameHeight, setFrameHeight] = useState(A4_HEIGHT_PX);
useEffect(() => {
const meta = document.createElement("meta");
@@ -34,6 +39,13 @@ export default function PublicCvPage() {
};
}, [slug]);
useEffect(() => {
const updateScale = () => setFrameScale(Math.min(1, Math.max(0.1, (window.innerWidth - 32) / A4_WIDTH_PX)));
updateScale();
window.addEventListener("resize", updateScale);
return () => window.removeEventListener("resize", updateScale);
}, []);
if (error) {
return (
<div style={{ display: "flex", height: "100vh", alignItems: "center", justifyContent: "center", fontFamily: "system-ui", color: "#374151" }}>
@@ -43,23 +55,32 @@ export default function PublicCvPage() {
}
if (html === null) {
return (
<div style={{ display: "flex", height: "100vh", alignItems: "center", justifyContent: "center", fontFamily: "system-ui", color: "#9ca3af" }}>
<div role="status" style={{ display: "flex", height: "100vh", alignItems: "center", justifyContent: "center", fontFamily: "system-ui", color: "#4b5563" }}>
Loading
</div>
);
}
return (
<div style={{ minHeight: "100vh", background: "#e9edf2", padding: "24px 0", display: "flex", flexDirection: "column", alignItems: "center", gap: 16 }}>
<div style={{ minHeight: "100vh", background: "#e9edf2", padding: "24px 16px", display: "flex", flexDirection: "column", alignItems: "center", gap: 16, overflowX: "hidden" }}>
<a href={pdfUrl} download style={{ padding: "10px 16px", borderRadius: 8, background: "#1d4ed8", color: "#fff", font: "600 14px system-ui", textDecoration: "none" }}>
Download PDF
</a>
<iframe
title="Public CV"
srcDoc={html}
sandbox="allow-same-origin"
style={{ width: "210mm", height: "297mm", border: "none", background: "#fff", boxShadow: "0 8px 30px rgba(0,0,0,0.18)" }}
/>
<div
data-testid="public-cv-frame-container"
style={{ width: A4_WIDTH_PX * frameScale, height: frameHeight * frameScale, maxWidth: "100%", overflow: "hidden", boxShadow: "0 8px 30px rgba(0,0,0,0.18)" }}
>
<iframe
title="Public CV"
srcDoc={html}
sandbox="allow-same-origin"
onLoad={(event) => {
const documentHeight = event.currentTarget.contentDocument?.documentElement.scrollHeight ?? A4_HEIGHT_PX;
setFrameHeight(Math.max(A4_HEIGHT_PX, documentHeight));
}}
style={{ width: A4_WIDTH_PX, height: frameHeight, transform: `scale(${frameScale})`, transformOrigin: "top left", border: "none", background: "#fff", display: "block" }}
/>
</div>
</div>
);
}