Skip to content

fix(ui): notification a11y/state + rail-pager gesture follow-ups (23 verified bugs) - #11316

Merged
lalalune merged 5 commits into
developfrom
shaw/notif-ui-followups
Jul 2, 2026
Merged

fix(ui): notification a11y/state + rail-pager gesture follow-ups (23 verified bugs)#11316
lalalune merged 5 commits into
developfrom
shaw/notif-ui-followups

Conversation

@lalalune

@lalalune lalalune commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up bug-fix sweep on the launcher / notification / rail surfaces after the per-surface NotificationCenter + single-owner 50%-swipe landed (#11174). An adversarial fable review (6 finders → per-finding verify) surfaced 25 confirmed in-scope defects; 23 are fixed here across 4 commits, each with tests. Two are deliberately deferred (below).

Notifications — a11y + state correctness

  • Focus management on the controlled sheet/panel dialogs: aria-modal, focus moves in on open, Tab is trapped, focus returns to the opener on close; the invisible backdrops are no longer Tab stops.
  • Bell popover now controlled, so a deep-link row closes it after navigating (was left open over the new view).
  • Filter chips were fake ARIA tabs (role=tablist/tab) with none of the tabs keyboard model → now role=group + aria-pressed toggle buttons, matching the sort toggle.
  • Stale category filter no longer resurrects: the drain→"all" fallback is committed to state, so a later notification of the drained category can't snap the open shell back.
  • Empty state waits for hydration — shows "Loading…" until the inbox settles instead of flashing "You're all caught up" then popping rows in.
  • Overlay z-indexes moved onto the floating-layers scale (Z_NOTIFICATION_BACKDROP/OVERLAY, above the pointer-events-none tutorial spotlight, below the system-banner band) — were raw z-[9500]/[9501] tying Z_TUTORIAL.
  • WS payload is now validated (id+title required, category/priority coerced to defaults, timestamps/strings type-checked) instead of a double-cast that trusted every field but id.
  • Filter chip bar gets shrink-0 so it isn't vertically crushed when the list overflows.
  • Sheet grabber is now a real dismiss button (it implied a drag-to-dismiss that didn't exist).

Rail / pager — gesture + a11y (incl. regressions from the 50% change)

  • Release velocity (trailing 100ms window, not whole-gesture average): "drag slowly to ~40%, then flick" now commits; direction-guarded so a fling-back can't commit the wrong page. Fixed the exact "swipe feels stuck" the 50% threshold introduced.
  • Edge-swipe-home commits at the 64px MIN threshold (it's EDGE_RESISTANCE-damped), not the 50%-of-width floor that demanded a half-screen pull.
  • Committed-swipe click suppression moved into the hook (handlers.onClickCapture), so inner launcher paging + the edge-swipe-home path no longer tap-launch the element under the release (was rail-only after the single-owner refactor).
  • Mouse-button guards: ignore non-primary buttons; abandon a stale drag whose button was released off-surface (was panning the rail on a later hover).
  • Settle-teleport: touching down mid-settle catches the rail where it visually sits instead of jumping to the animation's end.
  • Reduced-motion: honored in writeOffset (an inline transition can't be overridden by the CSS class); dead motion-reduce:transition-none classes removed.
  • Offscreen rail half is now inert, not just aria-hidden, so Tab can't reach controls behind the visible page.
  • Viewport resize during a drag no longer leaves the rail mis-offset (settle re-measures width).

Launcher

  • Curation alias/route mismatch: an alias that wins the tile now drops its alias path, so launch navigates to /apps/<canonicalId> — matching the id that recents/telemetry record (was navigating to the alias route).
  • Standalone page clamp now uses the rendered pages count, not layout.pages (wrong source in grouped mode).
  • Doc corrections: LAUNCHER_PAGE_SIZE (responsive 4→5 col), Launcher header (fixed white-on-wallpaper treatment; production renders read-only curated pages).

Tests / evidence

  • +18 unit tests (release-velocity flick + direction guard, edge-swipe-home threshold, mouse-button/stale-drag guards, click suppression, reduced-motion, dialog focus flow, hydration-gated empty state, aria-pressed chips, WS validation, alias-path drop). 142 launcher/pager/notification/store/home unit tests green; ui typecheck + biome clean.
  • home-screen e2e PASSES end-to-end (0 page errors) with regenerated checked-in captures: output-home/02-mobile-notification-sheet.png (centered, on-screen), output-home/09-desktop-notification-panel.png (right-anchored, on-screen). Rebased onto latest develop.

Deferred (honest, out of scope for a bug-fix PR)

  1. Launcher edit-mode/reorder/persistence is unreachable from the production mount (LauncherSurface always passes pageGroups → read-only curated pages). The verifier flagged ~200 lines as dead, but that machinery is still exercised (and passing) by the standalone launcher-e2e + stories — it's a disconnected feature, and deleting it removes user-facing drag-to-reorder (a product decision, Interaction QA is shallow/larpy — de-larp touch, mouse, XR & TUI testing with real-input e2e, recordings, per-step validation & fuzz #10722). Surfacing rather than deleting unilaterally.
  2. Two notification shells can stack (HomeScreen's controlled shell + the App-level headless owner are independent). The clean single-owner fix requires the e2e fixture to mount a headless owner too; low severity (needs opening via both the tray event AND the home pull simultaneously). Deferred to avoid e2e blast radius in a fix sweep.

🤖 Generated with Claude Code

Shaw and others added 5 commits July 2, 2026 04:18
…d bugs)

Adversarial fable review of the merged per-surface NotificationCenter (#11174)
surfaced 6 confirmed in-scope defects; all fixed here:

1. Focus management: the sheet/panel are hand-rolled role=dialog shells portaled
   to body but did nothing for keyboard/AT — no aria-modal, no focus-in, no trap,
   no focus return. Added aria-modal, save/restore activeElement across open, a
   Tab-wrap handler, and tabIndex=-1 on the invisible backdrops so the first Tab
   lands on a real control.
2. Bell popover stayed open after a deep-link row navigated (uncontrolled Radix
   Popover). Made it controlled (bellOpen) so a row click closes it after nav.
3. Filter chips claimed role=tablist/tab+aria-selected without any tabs keyboard
   semantics. Switched to role=group + aria-pressed toggle buttons, matching the
   adjacent sort toggle.
4. Stale category filter could silently resurrect: the drain-to-all fallback was
   computed-only, so a later notification of the drained category snapped the
   open shell back to the old filter. Now committed to state so it is sticky.
5. Empty state showed the definitive "all caught up" before the inbox hydrated
   (conflating not-loaded with empty). Gate on the store hydrated flag; show
   "Loading" until it settles.
6. Overlay z-indexes were raw z-[9500]/[9501] off the floating-layers scale,
   tying/colliding with Z_TUTORIAL. Added Z_NOTIFICATION_BACKDROP/OVERLAY
   (9550/9560, above the pointer-events:none tutorial spotlight, below the
   system-banner band) and mirrored them with the standard comment.

+3 unit tests (a11y dialog focus flow, hydration-gated empty state, aria-pressed
filter toggles) + a __setHydratedForTests store seam. 13 notification + 19
home/store tests green; ui typecheck + biome clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial fable review of the merged single-owner 50%-threshold pager surfaced
several gesture defects — two are regressions from the 50% change itself:

1. Release velocity, not gesture average (regression): the flick escape-hatch used
   dx/total-elapsed, so the common "drag slowly to ~40%, then flick" read as slow
   and sprang back. Now a trailing 100ms sample window derives RELEASE velocity;
   falls back to the average for tap-flicks/single-sample. Direction-guarded so a
   drag-forward-then-fling-back release can't commit the wrong page. The same
   release velocity now feeds the momentum-settle duration.
2. Edge-swipe-home needed a half-screen drag (regression): the right-drag-home at
   page 0 is damped by EDGE_RESISTANCE, so the 50% raw-distance floor demanded a
   half-screen pull that read as "you can't do this". It now commits at the 64px
   MIN threshold; inter-page swipes keep the 50% floor.
3. Committed-swipe click suppression only covered rail commits (regression from the
   single-owner refactor): inner launcher paging and the edge-swipe-home path could
   still tap-launch the element under the release. Moved suppression into the hook
   (armed on every commit) and exposed handlers.onClickCapture; Launcher +
   HomeLauncherSurface use it, replacing HomeLauncherSurface's local ref.
4. Mouse-button guards: pointerdown ignores non-primary (right/middle/pen-barrel)
   buttons; a mouse/pen move with buttons=0 after a button-down abandons the stale
   drag (released off-surface before capture → no pointerup → hover was panning the
   rail). Gated on hadButtons so synthetic/touch events aren't spuriously abandoned.
5. Settle-teleport: touching down during a momentum settle grabbed the rail at the
   animation's END position; now it reads the live transform and catches the rail
   under the finger.

+9 pager unit tests (release-velocity flick + direction guard, edge-swipe-home
reduced threshold on/off, non-primary-button ignore, stale-hover abandon,
click-suppression) — 17 pager + 112 launcher/home tests green; ui typecheck +
biome clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unch route (5 verified bugs)

Continued fable-review cleanup across the launcher/notification/rail surfaces:

1. Reduced-motion was silently defeated: the pager writes an inline
   `style.transition`, which always overrides the `motion-reduce:transition-none`
   CSS class. Gate it in writeOffset (one seam for settle/snap-back/abandon/edge-
   buttons/mount) via a fresh prefers-reduced-motion read, and drop the now-dead
   motion-reduce classes from both rails.
2. Offscreen home↔launcher rail half was aria-hidden but still keyboard-focusable
   — added `inert` to each half (matches the Launcher's inert page pattern) so Tab
   can't reach controls hidden behind the visible page.
3. Notification category-filter chip bar (a horizontal scroll container) got
   vertically crushed when the list overflowed the flex-column shell — added
   `shrink-0` so only the intended list `ul` is the flex scroller.
4. Launcher curation dedup rewrote a winning alias to the canonical id but kept
   the alias `path`, and handleLaunch prefers `path` — so an alias win navigated
   to the alias route while recents/telemetry recorded the canonical id. Drop the
   path when (and only when) the id is rewritten, so launch falls back to
   /apps/<canonicalId> and all three agree.
5. Viewport resize DURING a drag left the rail permanently mis-offset after
   snap-back — finish()/abandonDrag() now re-measure width for the settle
   geometry (the commit threshold still uses the gesture's original width).

+4 unit tests (reduced-motion transition drop, alias-path drop + non-alias path
kept). 128 launcher/pager/notification/home tests green; ui typecheck + biome
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cs (5 verified bugs)

Final batch of the fable-review cleanup on the launcher/notification surfaces:

1. WS notification boundary was a double-cast that trusted every field but `id`.
   Replace with validateWsNotification: require id + title (drop otherwise),
   coerce category/priority to their canonical union defaults, createdAt→now,
   and pass optional string/timestamp fields through only when well-typed.
2. Standalone page clamp read `layout.pages.length` while the RENDERED pages come
   from `curatedPages` in grouped mode — wrong count source. Clamp against
   `pages` (the actually-rendered list) and move the effect below its memo.
3. Notification sheet grabber implied a drag-to-dismiss gesture that didn't exist;
   made it a real dismiss button (tap the pill closes the sheet) — no whole-sheet
   pull gesture, which would fight the list's own scroll.
4. LAUNCHER_PAGE_SIZE doc claimed a fixed 4-col grid though the launcher is
   grid-cols-4 → sm:grid-cols-5; corrected the comment (value unchanged).
5. Launcher header doc claimed "fully token-themed" though the springboard
   intentionally uses a fixed white-on-wallpaper treatment; corrected the doc and
   clarified that production (LauncherSurface) renders read-only curated pages.

+3 tests (WS drop-on-missing + enum coercion) + a __getStateForTests store seam.
123 launcher/notification/store/home tests green; ui typecheck + biome clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…esture follow-ups)

Home-screen e2e passes end-to-end on the rebased tree: mobile sheet + desktop
panel render on-screen, read-only launcher, swipe-back returns home, 0 page errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c9035450-83c3-4df0-8a98-817e88906d1e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch shaw/notif-ui-followups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@lalalune

lalalune commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

QA (subagent-verified): 10+ of the 23 claims spot-checked in code and all real (velocity window+direction guard, edge-swipe MIN_DISTANCE commit, hook-owned click suppression with the duplicate deleted, WS payload validation replacing the double-cast, focus-trap/restore, aria group/pressed). No #11174 regression — 0.5 threshold and claimPagerPointer arbitration untouched, and the boot-free home-screen e2e passes end-to-end with real CDP touch. 116 unit tests green, pager suite doubled. Nit on record: z-index classes mirror rather than consume the new constants (Tailwind constraint). Merging.

@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant