fix(dashboard): a11y and UX fixes — focus trap, keyboard nav, confirmations, error states#379
Conversation
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>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Warning Review limit reached
Next review available in: 50 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (13)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
# Conflicts: # packages/dashboard/src/components/dashboard/project/_use-project-data.ts
Summary
Seven accessibility and UX fixes from the audit backlog, each landed as its own commit.
Dialognow traps Tab/Shift+Tab, moves focus in on open, and restores focus to the trigger on close (WAI-ARIA modal pattern). Fixed in the single shared primitive so every consumer (delete-project, key creation, retention settings, and the new confirm dialogs below) inherits it.tabIndex,onKeyDown,role="button"), mirroring the existing pattern in_projects-table.tsx. Also upgraded the empty state to the standard icon+title+description shape.aria-live="polite"+ visually-hidden text, mirroring_messages-panel.tsx's existing correct pattern.ConfirmDialogand wired it into API key revoke (ApiKeysTable, shared by both/dashboard/keysand the project Keys tab), org-member removal (MembersList), and custom-domain deletion (replacing the nativeconfirm()in_use-domain-actions.ts).useProjectDatanow surfaceserror: "not-found" | "failed" | null(via the newApiErrorstatus) plus aretry();project-content.tsxrenders a proper error card instead of a bare "Project not found." string, distinguishing a genuine 404 from a transient failure.api()now throwsApiErrorwith the HTTP status and dispatches aSESSION_EXPIRED_EVENTon 401.AppShell'sGatelistens once, shows a clear toast, and signs the user out (re-triggering the existing sign-in branch) instead of leaving a stale page with an opaque "API error 401"._AddDomainFormvalidates live via the existingisValidDomain()helper and surfaces it throughInput's built-inerrorprop (border +role="alert"), instead of only checking on submit.Design decisions
Dialograther than swapping to shadcn/Radix. This repo has nocomponents.jsonand no Radix dependency yet — introducing one for 2–3 Dialog consumers plus the newConfirmDialogfelt like a bigger lift than a correct, contained ~40-line focus-trap implementation in the one existing primitive. Flagging this as a reasonable follow-up if Dialog usage grows.ConfirmDialog(Cancel/Confirm) for key-revoke/member-removal/domain-delete — lower blast radius than project deletion, which keeps its existing typed-slug confirmation.Input's existingerrorprop rather than duplicatingProjectSlugInput's hand-rolled border/icon logic — it already implements the same visual contract.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 runsbun 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