test(a11y): gate primary workspaces with axe

This commit is contained in:
cesnimda
2026-08-31 12:09:32 +02:00
parent ba7748193b
commit eca3479059
9 changed files with 121 additions and 55 deletions
@@ -192,7 +192,7 @@ export default function GoogleAuthCard({ onSignedIn, presentation = "account" }:
}
return clientId
? <Box aria-label={actionLabel} aria-busy={working} sx={{ display: "flex", justifyContent: "center", minHeight: 40 }}><div ref={hostRef} /></Box>
? <Box role="group" aria-label={actionLabel} aria-busy={working} sx={{ display: "flex", justifyContent: "center", minHeight: 40 }}><div ref={hostRef} /></Box>
: <Button fullWidth variant="outlined" disabled>{actionLabel}</Button>;
}
+12 -12
View File
@@ -474,15 +474,15 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1 }}>
<FormControl fullWidth size="small">
<InputLabel>{t("jobTableStatus")}</InputLabel>
<Select value={statusFilter} label={t("jobTableStatus")} onChange={(e) => changeStatus(e.target.value)}>
<InputLabel id="job-table-status-label">{t("jobTableStatus")}</InputLabel>
<Select labelId="job-table-status-label" value={statusFilter} label={t("jobTableStatus")} onChange={(e) => changeStatus(e.target.value)}>
{[t("jobTableAll"), t("statusApplied"), t("statusWaiting"), t("statusInterview"), t("statusOffer"), t("statusRejected"), t("statusGhosted")].map((s) => <MenuItem key={s} value={s === t("jobTableAll") ? "All" : s === t("statusApplied") ? "Applied" : s === t("statusWaiting") ? "Waiting" : s === t("statusInterview") ? "Interview" : s === t("statusOffer") ? "Offer" : s === t("statusRejected") ? "Rejected" : "Ghosted"}>{s}</MenuItem>)}
</Select>
</FormControl>
<FormControl fullWidth size="small">
<InputLabel>{t("jobTableCompany")}</InputLabel>
<Select value={companyFilterId} label={t("jobTableCompany")} onChange={(e) => changeCompany(e.target.value as number | "All")}>
<InputLabel id="job-table-company-label">{t("jobTableCompany")}</InputLabel>
<Select labelId="job-table-company-label" value={companyFilterId} label={t("jobTableCompany")} onChange={(e) => changeCompany(e.target.value as number | "All")}>
<MenuItem value="All">{t("jobTableAll")}</MenuItem>
{selectedCompanyIsLoading ? <MenuItem value={companyFilterId}>Company {companyFilterId}</MenuItem> : null}
{companies.map((c) => <MenuItem key={c.id} value={c.id}>{c.name}</MenuItem>)}
@@ -500,8 +500,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
{mode === "jobs" ? (
<FormControl fullWidth size="small">
<InputLabel>{t("jobTableReadiness")}</InputLabel>
<Select value={readinessFilter} label={t("jobTableReadiness")} onChange={(e) => changeReadiness(e.target.value as ReadinessFilter)}>
<InputLabel id="job-table-readiness-label">{t("jobTableReadiness")}</InputLabel>
<Select labelId="job-table-readiness-label" value={readinessFilter} label={t("jobTableReadiness")} onChange={(e) => changeReadiness(e.target.value as ReadinessFilter)}>
<MenuItem value="all">{t("jobTableAllReadiness")}</MenuItem>
<MenuItem value="needs-work">{t("jobTableNeedsWork")}</MenuItem>
<MenuItem value="interview">{t("jobTableInterviewStage")}</MenuItem>
@@ -552,15 +552,15 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
/>
<FormControl sx={{ width: { xs: "100%", sm: 180 } }} size="small">
<InputLabel>{t("jobTableStatus")}</InputLabel>
<Select value={statusFilter} label={t("jobTableStatus")} onChange={(e) => changeStatus(e.target.value)}>
<InputLabel id="job-table-status-label">{t("jobTableStatus")}</InputLabel>
<Select labelId="job-table-status-label" value={statusFilter} label={t("jobTableStatus")} onChange={(e) => changeStatus(e.target.value)}>
{[t("jobTableAll"), t("statusApplied"), t("statusWaiting"), t("statusInterview"), t("statusOffer"), t("statusRejected"), t("statusGhosted")].map((s) => <MenuItem key={s} value={s === t("jobTableAll") ? "All" : s === t("statusApplied") ? "Applied" : s === t("statusWaiting") ? "Waiting" : s === t("statusInterview") ? "Interview" : s === t("statusOffer") ? "Offer" : s === t("statusRejected") ? "Rejected" : "Ghosted"}>{s}</MenuItem>)}
</Select>
</FormControl>
<FormControl sx={{ width: { xs: "100%", sm: 220 } }} size="small">
<InputLabel>{t("jobTableCompany")}</InputLabel>
<Select value={companyFilterId} label={t("jobTableCompany")} onChange={(e) => changeCompany(e.target.value as number | "All")}>
<InputLabel id="job-table-company-label">{t("jobTableCompany")}</InputLabel>
<Select labelId="job-table-company-label" value={companyFilterId} label={t("jobTableCompany")} onChange={(e) => changeCompany(e.target.value as number | "All")}>
<MenuItem value="All">{t("jobTableAll")}</MenuItem>
{selectedCompanyIsLoading ? <MenuItem value={companyFilterId}>Company {companyFilterId}</MenuItem> : null}
{companies.map((c) => <MenuItem key={c.id} value={c.id}>{c.name}</MenuItem>)}
@@ -578,8 +578,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
{mode === "jobs" ? <FormControlLabel control={<Checkbox checked={needsFollowUpOnly} onChange={(e) => changeNeedsFollowUp(e.target.checked)} />} label={t("jobTableNeedsFollowUp")} sx={{ mr: 0 }} /> : null}
{mode === "jobs" ? (
<FormControl size="small" sx={{ width: { xs: "100%", sm: 180 } }}>
<InputLabel>{t("jobTableReadiness")}</InputLabel>
<Select value={readinessFilter} label={t("jobTableReadiness")} onChange={(e) => changeReadiness(e.target.value as ReadinessFilter)}>
<InputLabel id="job-table-readiness-label">{t("jobTableReadiness")}</InputLabel>
<Select labelId="job-table-readiness-label" value={readinessFilter} label={t("jobTableReadiness")} onChange={(e) => changeReadiness(e.target.value as ReadinessFilter)}>
<MenuItem value="all">{t("jobTableAllReadiness")}</MenuItem>
<MenuItem value="needs-work">{t("jobTableNeedsWork")}</MenuItem>
<MenuItem value="interview">{t("jobTableInterviewStage")}</MenuItem>
+46 -36
View File
@@ -13,6 +13,7 @@ import {
Drawer,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
@@ -168,38 +169,38 @@ export default function AppShell({
{rows.map((item) => {
const selected = item.to === activeTo;
return (
<ListItemButton
key={item.to}
selected={selected}
onClick={() => onNavigate(item.to)}
title={desktopNavCollapsed ? item.label : undefined}
sx={{
borderRadius: 2,
mb: 0.5,
minHeight: 44,
px: desktopNavCollapsed ? 1 : 1.5,
justifyContent: desktopNavCollapsed ? "center" : "flex-start",
border: "1px solid transparent",
color: SIDEBAR_TEXT_MUTED,
"&:hover": { backgroundColor: "rgba(255,255,255,0.06)", color: SIDEBAR_TEXT },
"&.Mui-selected": {
backgroundColor: SIDEBAR_SELECTED_BG,
color: SIDEBAR_SELECTED_TEXT,
},
"&.Mui-selected:hover": {
backgroundColor: SIDEBAR_SELECTED_BG,
},
}}
>
<ListItemIcon sx={{ minWidth: desktopNavCollapsed ? 0 : 36, justifyContent: "center", color: selected ? SIDEBAR_SELECTED_ICON : SIDEBAR_TEXT_MUTED }}>
{item.badgeCount && item.badgeCount > 0 ? (
<Badge color="error" badgeContent={item.badgeCount > 99 ? "99+" : item.badgeCount}>
{item.icon}
</Badge>
) : item.icon}
</ListItemIcon>
{!desktopNavCollapsed ? <ListItemText primary={item.label} primaryTypographyProps={{ fontWeight: 600 }} /> : null}
</ListItemButton>
<ListItem key={item.to} disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
selected={selected}
onClick={() => onNavigate(item.to)}
title={desktopNavCollapsed ? item.label : undefined}
sx={{
borderRadius: 2,
minHeight: 44,
px: desktopNavCollapsed ? 1 : 1.5,
justifyContent: desktopNavCollapsed ? "center" : "flex-start",
border: "1px solid transparent",
color: SIDEBAR_TEXT_MUTED,
"&:hover": { backgroundColor: "rgba(255,255,255,0.06)", color: SIDEBAR_TEXT },
"&.Mui-selected": {
backgroundColor: SIDEBAR_SELECTED_BG,
color: SIDEBAR_SELECTED_TEXT,
},
"&.Mui-selected:hover": {
backgroundColor: SIDEBAR_SELECTED_BG,
},
}}
>
<ListItemIcon sx={{ minWidth: desktopNavCollapsed ? 0 : 36, justifyContent: "center", color: selected ? SIDEBAR_SELECTED_ICON : SIDEBAR_TEXT_MUTED }}>
{item.badgeCount && item.badgeCount > 0 ? (
<Badge color="error" badgeContent={item.badgeCount > 99 ? "99+" : item.badgeCount}>
{item.icon}
</Badge>
) : item.icon}
</ListItemIcon>
{!desktopNavCollapsed ? <ListItemText primary={item.label} primaryTypographyProps={{ fontWeight: 600 }} /> : null}
</ListItemButton>
</ListItem>
);
})}
</List>
@@ -319,7 +320,7 @@ export default function AppShell({
onClick={(e) => setUserMenuAnchor(e.currentTarget)}
sx={{ borderRadius: 2.5, border: "1px solid", borderColor: "divider", width: 42, height: 42, flex: "0 0 auto" }}
>
<Avatar src={user.avatarImageDataUrl || undefined} sx={{ width: 28, height: 28, fontWeight: 900 }}>{initials}</Avatar>
<Avatar src={user.avatarImageDataUrl || undefined} sx={{ width: 28, height: 28, fontWeight: 900, bgcolor: "primary.main", color: "primary.contrastText" }}>{initials}</Avatar>
</IconButton>
) : <Box sx={{ width: 42, height: 42 }} />}
</Box>
@@ -427,7 +428,7 @@ export default function AppShell({
onClick={(e) => setUserMenuAnchor(e.currentTarget)}
sx={{ borderRadius: 2, border: "1px solid", borderColor: "divider" }}
>
<Avatar src={user.avatarImageDataUrl || undefined} sx={{ width: 30, height: 30, fontWeight: 900 }}>{initials}</Avatar>
<Avatar src={user.avatarImageDataUrl || undefined} sx={{ width: 30, height: 30, fontWeight: 900, bgcolor: "primary.main", color: "primary.contrastText" }}>{initials}</Avatar>
</IconButton>
<Box sx={{ display: { xs: "none", sm: "block" }, minWidth: 0 }}>
<Typography sx={{ fontWeight: 600, lineHeight: 1.2 }} noWrap>
@@ -556,10 +557,19 @@ export default function AppShell({
);
}
export function StackLanguageToggle({ language, setLanguage }: { language: "en" | "nb"; setLanguage: (language: "en" | "nb") => void }) {
export function StackLanguageToggle({ language, setLanguage, onDark = false }: { language: "en" | "nb"; setLanguage: (language: "en" | "nb") => void; onDark?: boolean }) {
const { t } = useI18n();
return (
<ButtonGroup size="small" variant="outlined" aria-label={t("applicationLanguage")} sx={{ flex: "0 0 auto", "& .MuiButton-root": { minWidth: 36, px: 0.6, fontWeight: 800 } }}>
<ButtonGroup
size="small"
variant="outlined"
aria-label={t("applicationLanguage")}
sx={{
flex: "0 0 auto",
"& .MuiButton-root": { minWidth: 36, px: 0.6, fontWeight: 800 },
...(onDark ? { "& .MuiButton-outlined": { color: "#fff", borderColor: "rgba(255,255,255,0.55)" } } : {}),
}}
>
<Button aria-label="English" aria-pressed={language === "en"} variant={language === "en" ? "contained" : "outlined"} onClick={() => setLanguage("en")}>EN</Button>
<Button aria-label="Norsk" aria-pressed={language === "nb"} variant={language === "nb" ? "contained" : "outlined"} onClick={() => setLanguage("nb")}>NO</Button>
</ButtonGroup>
+1 -1
View File
@@ -4,7 +4,7 @@ type PaletteLike = Record<string, any>;
// Single global brand accent -- matches the dark sidebar/landing page indigo used throughout
// the app. Not user-configurable; see jobbjakt-nextjs-migration memory / UI rework notes.
const ACCENT = "#6366F1";
const ACCENT = "#5B5BD6";
function buildPrimary(main: string) {
return {
+1 -1
View File
@@ -99,7 +99,7 @@ export default function LandingPage() {
<Typography sx={{ color: "#fff", fontWeight: 800, fontSize: 20 }}>Jobbjakt</Typography>
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<StackLanguageToggle language={language} setLanguage={setLanguage} />
<StackLanguageToggle language={language} setLanguage={setLanguage} onDark />
<GradientButton onClick={goToLogin} sx={{ color: "#0b1020", fontWeight: 700 }}>
{t("landingSignIn")}
</GradientButton>