fix(web): accessibility pass — add aria-labels, dialog roles, and pressed states

- KeyboardShortcutOverlay: add role="dialog", aria-modal, aria-labelledby, close button aria-label
- Timeline popovers (5 files): add aria-label="Close" to symbol-only close buttons
- TimelineToolbar: add aria-label to navigation and undo/redo icon buttons
- ComputationGraphClient: add aria-pressed to 2D/3D and view mode toggle buttons
- BulkEditModal: fix type mismatch from jsonb field hardening

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 23:27:56 +02:00
parent 85c064ba32
commit e08ee94546
9 changed files with 235 additions and 83 deletions
@@ -60,39 +60,46 @@ export default function ComputationGraphClient() {
const [dimension, setDimension] = useState<Dimension>("2d");
const {
viewMode, setViewMode,
resourceId, setResourceId,
month, setMonth,
projectId, setProjectId,
resources, projects,
viewMode,
setViewMode,
resourceId,
setResourceId,
month,
setMonth,
projectId,
setProjectId,
resources,
projects,
isLoading,
activeDomains,
graphData,
rawData,
highlightedNodes, setHighlightedNodes,
domainFilter, toggleDomain,
highlightedNodes,
setHighlightedNodes,
domainFilter,
toggleDomain,
} = state;
const resourceMeta = viewMode === "resource"
? (rawData?.meta as ResourceGraphMeta | undefined)
: undefined;
const resourceMeta =
viewMode === "resource" ? (rawData?.meta as ResourceGraphMeta | undefined) : undefined;
const resourceFactors = resourceMeta?.factors;
const weeklyAvailabilityEntries: Array<[string, number | undefined]> = resourceFactors?.weeklyAvailability
? [
["Mo", resourceFactors.weeklyAvailability.monday],
["Di", resourceFactors.weeklyAvailability.tuesday],
["Mi", resourceFactors.weeklyAvailability.wednesday],
["Do", resourceFactors.weeklyAvailability.thursday],
["Fr", resourceFactors.weeklyAvailability.friday],
["Sa", resourceFactors.weeklyAvailability.saturday],
["So", resourceFactors.weeklyAvailability.sunday],
]
: [];
const weeklyAvailabilityEntries: Array<[string, number | undefined]> =
resourceFactors?.weeklyAvailability
? [
["Mo", resourceFactors.weeklyAvailability.monday],
["Di", resourceFactors.weeklyAvailability.tuesday],
["Mi", resourceFactors.weeklyAvailability.wednesday],
["Do", resourceFactors.weeklyAvailability.thursday],
["Fr", resourceFactors.weeklyAvailability.friday],
["Sa", resourceFactors.weeklyAvailability.saturday],
["So", resourceFactors.weeklyAvailability.sunday],
]
: [];
const weeklyAvailability = resourceFactors?.weeklyAvailability
? weeklyAvailabilityEntries
.filter((entry): entry is [string, number] => typeof entry[1] === "number" && entry[1] > 0)
.map(([label, hours]) => `${label} ${formatNumber(hours, 1)}h`)
.join(" · ")
.filter((entry): entry is [string, number] => typeof entry[1] === "number" && entry[1] > 0)
.map(([label, hours]) => `${label} ${formatNumber(hours, 1)}h`)
.join(" · ")
: "—";
const topHolidays = resourceMeta?.resolvedHolidays?.slice(0, 6) ?? [];
@@ -104,6 +111,7 @@ export default function ComputationGraphClient() {
<div className="flex rounded-lg border border-zinc-300 dark:border-zinc-600">
<button
onClick={() => setDimension("2d")}
aria-pressed={dimension === "2d"}
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
dimension === "2d"
? "bg-zinc-800 text-white dark:bg-zinc-200 dark:text-zinc-900"
@@ -114,6 +122,7 @@ export default function ComputationGraphClient() {
</button>
<button
onClick={() => setDimension("3d")}
aria-pressed={dimension === "3d"}
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
dimension === "3d"
? "bg-zinc-800 text-white dark:bg-zinc-200 dark:text-zinc-900"
@@ -128,6 +137,7 @@ export default function ComputationGraphClient() {
<div className="flex rounded-lg border border-zinc-300 dark:border-zinc-600">
<button
onClick={() => setViewMode("resource")}
aria-pressed={viewMode === "resource"}
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
viewMode === "resource"
? "bg-blue-600 text-white"
@@ -138,6 +148,7 @@ export default function ComputationGraphClient() {
</button>
<button
onClick={() => setViewMode("project")}
aria-pressed={viewMode === "project"}
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
viewMode === "project"
? "bg-blue-600 text-white"
@@ -177,11 +188,14 @@ export default function ComputationGraphClient() {
className="rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200"
>
<option value="">Select Project...</option>
{(Array.isArray(projects) ? projects : []).map((p: { id: string; name: string; shortCode?: string | null }) => (
<option key={p.id} value={p.id}>
{p.shortCode ? `${p.shortCode}` : ""}{p.name}
</option>
))}
{(Array.isArray(projects) ? projects : []).map(
(p: { id: string; name: string; shortCode?: string | null }) => (
<option key={p.id} value={p.id}>
{p.shortCode ? `${p.shortCode}` : ""}
{p.name}
</option>
),
)}
</select>
)}
@@ -246,15 +260,22 @@ export default function ComputationGraphClient() {
<aside className="w-[24rem] overflow-y-auto border-l border-zinc-200 bg-white/90 p-4 dark:border-zinc-700 dark:bg-zinc-950/90">
<div className="space-y-4">
<section className="rounded-xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900">
<div className="text-xs font-semibold uppercase tracking-wide text-zinc-500">Bezugsgroessen</div>
<div className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
Bezugsgroessen
</div>
<div className="mt-2 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{resourceMeta.resourceName ?? "Resource"}
</div>
<div className="text-sm text-zinc-500">{resourceMeta.resourceEid ?? "—"} · {resourceMeta.month ?? month}</div>
<div className="text-sm text-zinc-500">
{resourceMeta.resourceEid ?? "—"} · {resourceMeta.month ?? month}
</div>
<div className="mt-3 grid grid-cols-1 gap-2 text-sm text-zinc-700 dark:text-zinc-300">
<div className="rounded-lg bg-white px-3 py-2 dark:bg-zinc-950">
<div className="text-xs uppercase text-zinc-500">Land</div>
<div>{resourceMeta.countryName ?? resourceMeta.countryCode ?? "—"}{resourceMeta.countryCode ? ` (${resourceMeta.countryCode})` : ""}</div>
<div>
{resourceMeta.countryName ?? resourceMeta.countryCode ?? "—"}
{resourceMeta.countryCode ? ` (${resourceMeta.countryCode})` : ""}
</div>
</div>
<div className="rounded-lg bg-white px-3 py-2 dark:bg-zinc-950">
<div className="text-xs uppercase text-zinc-500">Bundesland / Region</div>
@@ -273,23 +294,30 @@ export default function ComputationGraphClient() {
<section className="rounded-xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900">
<div className="flex items-center justify-between">
<div className="text-xs font-semibold uppercase tracking-wide text-zinc-500">Feiertagsbasis</div>
<div className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
Feiertagsbasis
</div>
<div className="text-xs text-zinc-500">
{resourceFactors?.publicHolidayCount ?? 0} Feiertage, {resourceFactors?.publicHolidayWorkdayCount ?? 0} wirksam
{resourceFactors?.publicHolidayCount ?? 0} Feiertage,{" "}
{resourceFactors?.publicHolidayWorkdayCount ?? 0} wirksam
</div>
</div>
<div className="mt-3 space-y-2">
{topHolidays.length > 0 ? topHolidays.map((holiday) => (
<div
key={`${holiday.date}-${holiday.name}`}
className="rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm dark:border-zinc-800 dark:bg-zinc-950"
>
<div className="font-medium text-zinc-900 dark:text-zinc-100">{holiday.name}</div>
<div className="text-xs text-zinc-500">
{holiday.date} · {holiday.scope} · {holiday.calendarName ?? "Kalender"}
{topHolidays.length > 0 ? (
topHolidays.map((holiday) => (
<div
key={`${holiday.date}-${holiday.name}`}
className="rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm dark:border-zinc-800 dark:bg-zinc-950"
>
<div className="font-medium text-zinc-900 dark:text-zinc-100">
{holiday.name}
</div>
<div className="text-xs text-zinc-500">
{holiday.date} · {holiday.scope} · {holiday.calendarName ?? "Kalender"}
</div>
</div>
</div>
)) : (
))
) : (
<div className="rounded-lg border border-dashed border-zinc-200 px-3 py-2 text-sm text-zinc-500 dark:border-zinc-800">
Keine aufgeloesten Feiertage im gewaehlten Monat.
</div>
@@ -298,12 +326,17 @@ export default function ComputationGraphClient() {
</section>
<section className="rounded-xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900">
<div className="text-xs font-semibold uppercase tracking-wide text-zinc-500">Herleitung</div>
<div className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
Herleitung
</div>
<div className="mt-3 space-y-2">
<div className="rounded-lg bg-white px-3 py-2 text-sm dark:bg-zinc-950">
<div className="text-xs uppercase text-zinc-500">SAH Formel</div>
<div className="font-medium text-zinc-900 dark:text-zinc-100">
{formatNumber(resourceFactors?.baseAvailableHours)}h - {formatNumber(resourceFactors?.publicHolidayHoursDeduction)}h - {formatNumber(resourceFactors?.absenceHoursDeduction)}h = {formatNumber(resourceFactors?.effectiveAvailableHours)}h
{formatNumber(resourceFactors?.baseAvailableHours)}h -{" "}
{formatNumber(resourceFactors?.publicHolidayHoursDeduction)}h -{" "}
{formatNumber(resourceFactors?.absenceHoursDeduction)}h ={" "}
{formatNumber(resourceFactors?.effectiveAvailableHours)}h
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-sm">