From d8efbc34fab09feb5bb275569e4d230eb1b94ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20Rosenl=C3=B6cher?= Date: Mon, 19 Oct 2026 14:02:31 +0200 Subject: [PATCH] =?UTF-8?q?fix(a11y):=20Name=20der=20Glocke=20enth=C3=A4lt?= =?UTF-8?q?=20die=20sichtbare=20Zahl=20(Lighthouse=20label-content-name-mi?= =?UTF-8?q?smatch)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/components/notification-bell.spec.tsx | 12 +++++++----- .../src/components/notification-bell.tsx | 19 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/ApiCore-Frontend/src/components/notification-bell.spec.tsx b/ApiCore-Frontend/src/components/notification-bell.spec.tsx index 3756bac..0b00367 100644 --- a/ApiCore-Frontend/src/components/notification-bell.spec.tsx +++ b/ApiCore-Frontend/src/components/notification-bell.spec.tsx @@ -60,7 +60,7 @@ describe("NotificationBell", () => { notification({ id: "n-2", message: "Ohne Ziel", relatedType: "Colony", relatedId: "c-9" }), ]); renderBell(); - await userEvent.click(await screen.findByRole("button", { name: "Benachrichtigungen" })); + await userEvent.click(await screen.findByRole("button", { name: /^Benachrichtigungen/ })); const linked = await screen.findByText("Wartezeit-Konflikt bei B3"); expect(linked.closest("a")).toHaveAttribute("href", "/harvests/h-1"); @@ -76,7 +76,7 @@ describe("NotificationBell", () => { notification({ id: "n-2", message: "Schema", link: "https://evil.example" }), ]); renderBell(); - await userEvent.click(await screen.findByRole("button", { name: "Benachrichtigungen" })); + await userEvent.click(await screen.findByRole("button", { name: /^Benachrichtigungen/ })); expect((await screen.findByText("Wartezeit-Konflikt bei B3")).closest("a")).toBeNull(); expect(screen.getByText("Schema").closest("a")).toBeNull(); }); @@ -84,7 +84,7 @@ describe("NotificationBell", () => { it("hides „Alle gelesen“ when nothing is unread and names the category in words", async () => { vi.mocked(fetchNotifications).mockResolvedValue([notification({ read: true })]); renderBell(); - await userEvent.click(await screen.findByRole("button", { name: "Benachrichtigungen" })); + await userEvent.click(await screen.findByRole("button", { name: /^Benachrichtigungen/ })); expect(await screen.findByText("Wartezeit")).toBeInTheDocument(); expect(screen.queryByRole("menuitem", { name: /Alle gelesen/ })).not.toBeInTheDocument(); }); @@ -120,10 +120,12 @@ describe("NotificationBell", () => { expect(vi.mocked(toast.error).mock.calls[0][1]).toEqual({ id: "notification-read-error" }); }); - it("the badge is announced with the unread count, the digit is hidden from readers", async () => { + it("the button name carries the visible badge text, the digit itself is hidden from readers", async () => { vi.mocked(fetchNotifications).mockResolvedValue([notification({}), notification({ id: "n-2" })]); renderBell(); - expect(await screen.findByText("2 ungelesene Benachrichtigungen")).toHaveClass("sr-only"); + expect( + await screen.findByRole("button", { name: "Benachrichtigungen, 2 ungelesen" }), + ).toBeInTheDocument(); expect(screen.getByText("2", { selector: "[aria-hidden='true']" })).toBeInTheDocument(); }); diff --git a/ApiCore-Frontend/src/components/notification-bell.tsx b/ApiCore-Frontend/src/components/notification-bell.tsx index 47fd2db..78535df 100644 --- a/ApiCore-Frontend/src/components/notification-bell.tsx +++ b/ApiCore-Frontend/src/components/notification-bell.tsx @@ -109,19 +109,22 @@ export function NotificationBell() { variant="ghost" size="icon" className="relative" - aria-label="Benachrichtigungen" + // The name contains the visible badge text (WCAG 2.5.3). + aria-label={ + unreadCount > 0 + ? `Benachrichtigungen, ${unreadCount > 9 ? "9+" : unreadCount} ungelesen` + : "Benachrichtigungen" + } /> } > {unreadCount > 0 ? ( - - - - {unreadCount} ungelesene Benachrichtigungen - + ) : null}