Skip to content

feat(notification-bell): controllable open state, independent unread count, and lazy loading - #412

Open
Nitjsefnie wants to merge 4 commits into
codeatlasdev:mainfrom
Nitjsefnie-OSC:feat/380-notification-bell-state
Open

Nitjsefnie wants to merge 4 commits into
codeatlasdev:mainfrom
Nitjsefnie-OSC:feat/380-notification-bell-state

Conversation

@Nitjsefnie

@Nitjsefnie Nitjsefnie commented Oct 23, 2026 •

Copy link
Copy Markdown

Closes #380

Regression test (written first)

  • The test reproduces the problem and fails on main:
FAIL  src/lib/components/NotificationBell/NotificationBellState.svelte.test.ts > NotificationBell state > parent: combination 0
Error: expect(element).X()

Expected element to have accessible name:
  Notifications (7 unread)
Received:
  Notifications (0 unread)

Test Files  1 failed | 1 passed (2)
     Tests  19 failed | 8 passed (27)

Nineteen new behavior tests failed on the unmodified component (assertion/DOM failures,
each naming a missing prop's effect: count override ignored, no bind:open, no loading
state), while the explicit legacy control and the seven original tests passed. After the
implementation landed, review found one more acceptance gap on the newly exposed
parent-close path, and its regression test failed before the fix:

FAIL  ... > parent close restores focus from a notification item
Error: expect(element).toHaveFocus()
Expected: the trigger button   Received: <body>
Tests  1 failed | 31 passed (32)
  • The same test passes with this change.
 ✓  src/lib/components/NotificationBell/NotificationBellState.svelte.test.ts (25 tests)
 ✓  src/lib/components/NotificationBell/NotificationBell.svelte.test.ts (8 tests)

 Test Files  2 passed (2)
      Tests  33 passed (33)

Root cause and fix

NotificationBell owned its open state privately and derived the unread count from the
loaded list, so a consumer could not lazy-load the list while keeping the count fresh, and
the page could not observe opens/closes. This adds three additive props, following the
existing Popover controllable-state idiom (open = $bindable(false), guarded
setOpen, user-only onOpenChange, close(restoreFocus)):

  • bind:open + onOpenChange — panel state is controllable and observable. A
    parent-driven close returns focus to the trigger when focus was inside the panel
    (a pre-effect before the closing DOM update; Escape and outside-click behavior are
    unchanged); closing via Escape still restores trigger focus; outside presses keep the
    outside target's focus, matching Popover.
  • count — overrides the list-derived unread count (?? fallback, so an explicit 0
    wins); the trigger name, badge and mark-all visibility share it. Count changes announce
    through the shared polite announcer (initial render stays silent).
  • loading (+ labels.loading, en + pt-BR messages) — while loading with an empty list
    the definitive empty state is suppressed; any loaded list is retained.

Additive only: notifications, onRead, onReadAll, onClear, classes and the existing
translated name format are unchanged. Note the issue's example names (items,
onSelect, onMarkAllRead) differ from the real API (notifications, onRead,
onReadAll); the real names were kept and no aliases added.

Tests: a new state matrix (NotificationBellState.svelte.test.ts + harness) covers
parent-set vs user-clicked open × explicit/derived count × loading × populated/empty list
(16 combinations) plus zero-count override, polite announcements, Escape/trigger/outside
dismissals, the lazy flow (open → callback → loading → items arrive), parent-close focus
restoration (focused item and focus-already-outside controls), and an omitted-props control
pinning current behavior. Fixture values (7 unread, 0–2 items) are inputs the component
cannot produce by accident.

The generated skill reference (API.md) is updated by the rebase regeneration step: it
gains only NotificationBell's new prop rows (open, onOpenChange, count, loading,
labels.loading), exactly what the land script's regeneration produces.

Checklist

  • bun run validate passes in full (the nine validate steps pass at this head; the run currently fails only on the newly added wall-time budget, red identically on main itself)
  • Changeset added (bun changeset) if the published package changes
  • Docs updated (site/, AGENTS.md) if behavior or API changed, checked at desktop and phone width (n/a for visual width check: docs rendering is exercised by the site build and the land gate's changed-screenshot run)

Footer

Generated by GPT-6.1 Sol (implementation, tests), GPT-6 Astra (review), GLM-5.3-Flash (orchestration)

@Nitjsefnie
Nitjsefnie marked this pull request as ready for review October 23, 2026 06:41
@Nitjsefnie

Copy link
Copy Markdown
Author

Heads-up: this branch lives in an organization-owned fork, and GitHub doesn't let maintainers push to those. If anything needs changing, just say so here and I'll make the change on this branch right away.

@mvilacad

Copy link
Copy Markdown
Contributor

Reviewed against main: the API (bind:open, onOpenChange, independent count, loading), the polite count announcement, focus restore on parent close, en + pt-BR strings, docs and changeset cover everything #380 asks for, and the 32 NotificationBell browser tests pass at this head (bun run test:browser -- src/lib/components/NotificationBell). Nothing to change from my side. The CI run is waiting for maintainer approval (action_required, first-time contributor), and the branch is 30 commits behind main: a maintainer lands it with sh scripts/issue-land.sh 380, which rebases and validates before merging.

@Nitjsefnie
Nitjsefnie marked this pull request as draft October 23, 2026 14:35
@Nitjsefnie
Nitjsefnie force-pushed the feat/380-notification-bell-state branch from a35a9d0 to 8a3a509 Compare October 23, 2026 14:37
@Nitjsefnie

Copy link
Copy Markdown
Author

Rebased onto current main (9a10232) exactly as issue-land.sh's rebase half does — the lockfile came out byte-identical to main's, the generated skill API.md regenerated with only NotificationBell's new props entering it, and a range-diff confirms both reviewed patches unchanged (independently re-reviewed at the new head 8a3a509, 32/32 browser tests). I set the PR back to draft for the force-push since a ready head can't take it from an org fork; your land script re-readies it. Fork CI on 8a3a509 is green where main is green (validate+build, all NotificationBell tests), and every red job fails identically on untouched main itself (probe run of 9a10232 — baseline drift from main's recent style changes plus the known AudioPlayer/e2e flakes), so issue-land.sh 380 can take it from here.

@Nitjsefnie
Nitjsefnie marked this pull request as ready for review October 23, 2026 15:21
@mvilacad

Copy link
Copy Markdown
Contributor

Thanks @Nitjsefnie! Landing this now: I'm rebasing your branch on main (30+ commits behind), regenerating the generated files and running bun run validate before the squash-merge. That means a force-push to your branch.

@mvilacad

Copy link
Copy Markdown
Contributor

Correction: I can't push to your fork (403), so I haven't changed your branch. Your 32 NotificationBell tests pass, and validate is green once rebased locally. Could you rebase feat/380-notification-bell-state on main and push it? I'll land it right after.

@Nitjsefnie

Copy link
Copy Markdown
Author

Will do: rebasing feat/380-notification-bell-state onto current main now, and I will comment here as soon as it is pushed.

@Nitjsefnie
Nitjsefnie marked this pull request as draft October 23, 2026 17:35
@Nitjsefnie
Nitjsefnie force-pushed the feat/380-notification-bell-state branch from 8a3a509 to 85cad30 Compare October 23, 2026 17:37
@Nitjsefnie
Nitjsefnie marked this pull request as ready for review October 23, 2026 18:00
@Nitjsefnie

Copy link
Copy Markdown
Author

Pushed: feat/380-notification-bell-state is now rebased onto main at 85cad30 (base 539c4be), with the generated API.md regenerated, bun.lock identical to main, and bun run validate green. The NotificationBell changes themselves are unchanged, and the PR is ready again.

@mvilacad

Copy link
Copy Markdown
Contributor

Hi @Nitjsefnie, we're preparing the 4.6.0 release. This PR is ~120 commits behind main and we can't push to your fork. If you rebase on main this week we'll land it in 4.6 (tests already passed on your head). Otherwise it goes into the next release, no problem.

@Nitjsefnie
Nitjsefnie marked this pull request as draft October 25, 2026 16:42
Nitjsefnie and others added 4 commits October 26, 2026 05:59
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
@Nitjsefnie
Nitjsefnie force-pushed the feat/380-notification-bell-state branch from 85cad30 to 849fa50 Compare October 26, 2026 05:03
@Nitjsefnie
Nitjsefnie marked this pull request as ready for review October 26, 2026 06:19
@Nitjsefnie

Copy link
Copy Markdown
Author

Pushed: rebased onto main at 3c9d1b — the new head is 849fa50, with the lockfile and generated files rebuilt the way issue-land.sh does. All 33 NotificationBell browser tests pass at that head; they now expect main's new trigger label (2 unread notifications, and plain Notifications at zero), and the component changes themselves are unchanged.

@Nitjsefnie
Nitjsefnie marked this pull request as draft October 26, 2026 06:33
@Nitjsefnie
Nitjsefnie marked this pull request as ready for review October 26, 2026 10:13

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NotificationBell: controllable open state and independent unread count (lazy list loading)

2 participants