From e08ee94546bc7fd03eff8e7f9ff002c0db7aa311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sat, 11 Apr 2026 23:27:56 +0200 Subject: [PATCH] =?UTF-8?q?fix(web):=20accessibility=20pass=20=E2=80=94=20?= =?UTF-8?q?add=20aria-labels,=20dialog=20roles,=20and=20pressed=20states?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../analytics/ComputationGraphClient.tsx | 125 +++++++++++------- .../components/resources/BulkEditModal.tsx | 85 +++++++++--- .../components/timeline/AllocationPopover.tsx | 1 + .../timeline/BatchAssignPopover.tsx | 1 + .../src/components/timeline/DemandPopover.tsx | 80 ++++++++--- .../timeline/KeyboardShortcutOverlay.tsx | 20 ++- .../timeline/NewAllocationPopover.tsx | 1 + .../src/components/timeline/ProjectPanel.tsx | 1 + .../components/timeline/TimelineToolbar.tsx | 4 + 9 files changed, 235 insertions(+), 83 deletions(-) diff --git a/apps/web/src/components/analytics/ComputationGraphClient.tsx b/apps/web/src/components/analytics/ComputationGraphClient.tsx index 8388fc3..6edd2a9 100644 --- a/apps/web/src/components/analytics/ComputationGraphClient.tsx +++ b/apps/web/src/components/analytics/ComputationGraphClient.tsx @@ -60,39 +60,46 @@ export default function ComputationGraphClient() { const [dimension, setDimension] = useState("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() {
+
{fieldDefs.length === 0 && ( -

No custom fields defined. Configure them in Admin → Blueprints.

+

+ No custom fields defined. Configure them in Admin → Blueprints. +

)} {fieldDefs.map((field) => ( -
+
@@ -100,9 +118,7 @@ export function DemandPopover({ Open Demand - - {demand.status} - + {demand.status} {/* Headcount */} @@ -137,11 +153,15 @@ export function DemandPopover({ {/* Hours */}
Hours / day
-
{demand.hoursPerDay}h
+
+ {demand.hoursPerDay}h +
Total hours
-
{totalHours}h ({days}d)
+
+ {totalHours}h ({days}d) +
{/* Budget */} @@ -166,7 +186,9 @@ export function DemandPopover({ {demand.percentage > 0 && (
Percentage
-
{demand.percentage}%
+
+ {demand.percentage}% +
)} @@ -175,8 +197,18 @@ export function DemandPopover({ {(loadingSuggestions || suggestions.length > 0) && (
- - + + Suggested Resources @@ -205,13 +237,19 @@ export function DemandPopover({
-
{s.name}
+
+ {s.name} +
- {Math.round(s.utilization)}% utilized · {s.availableHoursPerDay.toFixed(1)}h/d free + {Math.round(s.utilization)}% utilized · {s.availableHoursPerDay.toFixed(1)} + h/d free
)} @@ -147,6 +148,7 @@ export function TimelineToolbar({ onClick={onNavigateForward} className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800" title="Next 4 weeks" + aria-label="Next 4 weeks" > › @@ -160,6 +162,7 @@ export function TimelineToolbar({ onClick={onUndo} disabled={!canUndo} title="Undo (Ctrl+Z)" + aria-label="Undo" className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800" > ↩ @@ -169,6 +172,7 @@ export function TimelineToolbar({ onClick={onRedo} disabled={!canRedo} title="Redo (Ctrl+Shift+Z / Ctrl+Y)" + aria-label="Redo" className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800" > ↪