From bc805cb1787d4a409e23bc60271c837473245d1f Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 26 Apr 2026 20:46:12 +0200 Subject: [PATCH] feat(nav): add cursor-pointer and tooltip to notification bell Extract bellLabel as $derived to DRY up aria-label and title. Add cursor-pointer globally to button via @layer base so Tailwind preflight reset doesn't override the browser default. Closes #344 Co-Authored-By: Claude Sonnet 4.6 --- .../lib/components/NotificationBell.svelte | 13 ++++++--- .../NotificationBell.svelte.spec.ts | 28 +++++++++++++++++++ frontend/src/routes/layout.css | 5 ++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/NotificationBell.svelte b/frontend/src/lib/components/NotificationBell.svelte index 0e390357..a06d90e4 100644 --- a/frontend/src/lib/components/NotificationBell.svelte +++ b/frontend/src/lib/components/NotificationBell.svelte @@ -48,6 +48,12 @@ function handleKeydown(event: KeyboardEvent) { } } +const bellLabel = $derived( + stream.unreadCount > 0 + ? m.notification_bell_unread_label({ count: stream.unreadCount }) + : m.notification_bell_label() +); + function attachBellButton(node: HTMLButtonElement) { bellButtonEl = node; return () => { @@ -72,12 +78,11 @@ onDestroy(() => { {@attach attachBellButton} type="button" onclick={toggleDropdown} - aria-label={stream.unreadCount > 0 - ? m.notification_bell_unread_label({ count: stream.unreadCount }) - : m.notification_bell_label()} + aria-label={bellLabel} + title={bellLabel} aria-expanded={open} aria-haspopup="true" - class="relative rounded-sm p-2 text-white/65 transition-colors hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" + class="relative cursor-pointer rounded-sm p-2 text-white/65 transition-colors hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" > { + it('bell button has cursor-pointer class', async () => { + render(NotificationBell); + const btn = document.querySelector('button[aria-haspopup="true"]')!; + expect(btn.classList.contains('cursor-pointer')).toBe(true); + }); + + it('bell button title equals aria-label when unreadCount is 0', async () => { + mockNotificationList.value = []; + render(NotificationBell); + const btn = document.querySelector('button[aria-haspopup="true"]')!; + expect(btn.getAttribute('title')).toBe('Benachrichtigungen'); + expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title')); + }); + + it('bell button title equals aria-label when unreadCount is 3', async () => { + mockNotificationList.value = [ + makeNotification({ id: 'n1' }), + makeNotification({ id: 'n2' }), + makeNotification({ id: 'n3' }) + ]; + render(NotificationBell); + const btn = document.querySelector('button[aria-haspopup="true"]')!; + expect(btn.getAttribute('title')).toBe('3 ungelesene Benachrichtigungen'); + expect(btn.getAttribute('aria-label')).toBe(btn.getAttribute('title')); + }); +}); + describe('NotificationBell', () => { it('handleMarkRead navigates to URL including annotationId when notification has annotationId', async () => { mockNotificationList.value = [makeNotification({ annotationId: 'annot-1' })]; diff --git a/frontend/src/routes/layout.css b/frontend/src/routes/layout.css index ef368838..e0335a34 100644 --- a/frontend/src/routes/layout.css +++ b/frontend/src/routes/layout.css @@ -365,6 +365,11 @@ text-underline-offset: 4px; } + /* Tailwind preflight resets cursor on *, overriding the browser default for buttons */ + button { + cursor: pointer; + } + /* Fallback focus ring for any interactive element not styled with ring-focus-ring */ :focus-visible { outline: 2px solid var(--c-focus-ring);