Skip to content

fix(dashboard): a11y and UX fixes — focus trap, keyboard nav, confirmations, error states#379

Merged
duyet merged 9 commits into
mainfrom
claude/w12-dashboard-a11y-ux
Jul 17, 2026
Merged

fix(dashboard): a11y and UX fixes — focus trap, keyboard nav, confirmations, error states#379
duyet merged 9 commits into
mainfrom
claude/w12-dashboard-a11y-ux

Conversation

@duyet

@duyet duyet commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Seven accessibility and UX fixes from the audit backlog, each landed as its own commit.

Design decisions

Verification

  • bunx biome check packages/dashboard/src/ — 0 errors (35 pre-existing warnings, all in files this PR doesn't touch: brand.astro, badge.astro, logo.astro, etc.)
  • bunx astro check (packages/dashboard) — 0 errors across all runs
  • Not run: bun run build (per repo guidance, skipped in agents to avoid OOM — verify after merge)

Closes #295, #294, #299, #293, #296, #297, #298

Co-Authored-By: Duyet Le me@duyet.net
Co-Authored-By: duyetbot bot@duyet.net

duyet and others added 7 commits July 17, 2026 08:56
Adds WAI-ARIA modal focus management to the shared Dialog primitive:
focus moves into the panel on open, Tab/Shift+Tab cycles within it,
and focus is restored to the triggering element on close. Fixes the
gap for every Dialog consumer (delete-project confirmation, key
creation, retention settings) in one contained change.

Closes #295

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
Traces rows were a bare clickable <tr> with no tabIndex/onKeyDown/role,
so keyboard and screen-reader users could not select a trace at all.
Mirrors the same TableRow(clickable, tabIndex, onKeyDown) pattern
already used correctly in _projects-table.tsx. Also upgrades the empty
state to the standard icon + title + description shape used elsewhere
(ApiKeysTable, AnalyticsEmpty) instead of a bare string.

Closes #294

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
The shared api() helper threw a generic Error with no status code, so
every call site (project data, key/domain actions, etc.) could only
show an opaque "API error 401" toast when the Clerk session expired,
leaving the page stale with no path back to sign-in.

api() now throws an ApiError carrying the HTTP status and dispatches a
window-level SESSION_EXPIRED_EVENT on 401. AppShell's Gate listens for
that event once, shows a clear "session expired" toast, and signs the
user out — which flips Gate back to its existing sign-in branch.

Also labels the auth-gate spinner with aria-live + sr-only text so
screen-reader users hear that sign-in is being checked (part of the
same Gate component touched here).

Closes #297

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
The sidebar's project-switcher skeleton was a silent animate-pulse div
with no accessible indication it was a loading placeholder. Adds
aria-live="polite" + visually-hidden "Loading projects…" text,
mirroring the aria-live pattern already used correctly in
_messages-panel.tsx.

Closes #299

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
…rror state

useProjectData swallowed every load failure (404, expired session,
network blip) into the same transient toast, then rendered a single
hard-coded "Project not found." string with no way to retry. The hook
now exposes error: "not-found" | "failed" | null (via the ApiError
status from lib/api.ts) plus a retry() that re-runs the load, and
project-content renders the standard icon + title + description card
(matching AnalyticsEmpty/ApiKeysTable) — "Project not found" with a
link back to the dashboard for a genuine 404, or "Couldn't load this
project" with a Retry button for anything transient.

Closes #296

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
…elete domain

Revoking an API key and removing an org member fired their destructive
DELETE call immediately on click, with zero confirmation — one misclick
permanently revoked a live credential or removed a teammate. Deleting a
custom domain used a native browser confirm(), inconsistent with the
rest of the design system.

Adds one reusable ConfirmDialog (Cancel/Confirm on the existing Dialog
primitive) and wires it into all three call sites:
- ApiKeysTable (shared by both /dashboard/keys and the project Keys tab)
- MembersList's Remove button
- _DomainsList, replacing _use-domain-actions.ts's native confirm()

Project deletion keeps its heavier typed-slug confirmation
(_delete-confirmation.tsx) since it has a larger blast radius.

Closes #293

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
isValidDomain() was only checked in handleAddDomain after clicking Add,
surfacing a generic "Invalid domain format" toast that didn't say what
was wrong and disappeared after a few seconds — unlike ProjectSlugInput,
which validates live and shows an inline role="alert" message.

_AddDomainForm now runs isValidDomain() as the user types and passes
the result to Input's existing error prop (border + role="alert",
already built into the shared Input primitive), disabling submit while
invalid. The toast in useDomainActions remains as a fallback for
server-side failures (domain already claimed, etc.).

Closes #298

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @duyet, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@duyet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0d86025b-9c35-47b5-a25d-b2106d489fc9

📥 Commits

Reviewing files that changed from the base of the PR and between 1ea5907 and d9b203b.

📒 Files selected for processing (13)
  • packages/dashboard/src/components/app-shell.tsx
  • packages/dashboard/src/components/dashboard/domains/_components/add-domain-form.tsx
  • packages/dashboard/src/components/dashboard/domains/_components/domains-list.tsx
  • packages/dashboard/src/components/dashboard/domains/_use-domain-actions.ts
  • packages/dashboard/src/components/dashboard/organizations/members/_members-list.tsx
  • packages/dashboard/src/components/dashboard/project/_api-keys-table.tsx
  • packages/dashboard/src/components/dashboard/project/_project-error-state.tsx
  • packages/dashboard/src/components/dashboard/project/_use-project-data.ts
  • packages/dashboard/src/components/dashboard/project/project-content.tsx
  • packages/dashboard/src/components/dashboard/traces/_traces-table.tsx
  • packages/dashboard/src/components/ui/confirm-dialog.tsx
  • packages/dashboard/src/components/ui/dialog.tsx
  • packages/dashboard/src/lib/api.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/w12-dashboard-a11y-ux

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.

duyet added 2 commits July 17, 2026 13:20
# Conflicts:
#	packages/dashboard/src/components/dashboard/project/_use-project-data.ts
@duyet
duyet merged commit bc0b32a into main Jul 17, 2026
6 checks passed
@duyet
duyet deleted the claude/w12-dashboard-a11y-ux branch July 17, 2026 06:31
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.

a11y: Custom Dialog component has no focus trap, initial focus, or focus restore

1 participant