This repository was archived by the owner on May 26, 2026. It is now read-only.
feat(kora): KR-FE-A11Y-AUDIT-AND-MULTI-TENANT-POLISH — a11y audit + recent tenants + wizard skip - #211
Merged
rafe-walker merged 1 commit intoMay 24, 2026
Conversation
…ecent tenants + wizard skip
Deliverable A — accessibility audit (top 4 findings fixed):
A.1 skip-to-main link in App shell (sr-only until focused;
first tab-stop; targets new id="kora-main" on
PageHeaderProvider's <main>; tabIndex=-1 so the anchor
actually moves keyboard focus)
A.2 TenantPicker trigger aria-label expanded to announce the
listbox interaction model ("Active tenant: X. Press Enter
to choose a different tenant.")
A.3 <TenantChangeAnnouncer> sr-only aria-live="polite"
role="status" region mounted in App shell; announces
"Active tenant changed to <X>" on switch. First-render +
single-tenant suppressed to avoid noise.
A.4 OAuthProvidersCard external-link gets aria-label on the
anchor; nested icon-only Button marked aria-hidden +
tabIndex=-1 so it doesn't double-announce or steal focus
A.5 / forced-colors styles deferred — listed in PR body
"deferred findings" section. Modal infra + sidebar
group-header buttons already accessible per audit.
Deliverable B — TenantPicker recent tenants:
* localStorage kora_recent_tenants capped at 5 (drift-pinned)
* useActiveTenant exposes recentTenants[]; filtered to
entries still present in availableTenants so a since-
deleted tenant won't leak into the picker
* TenantPicker renders Recent (up to 3) + All sections when
availableTenants.length >= 3; flat list otherwise
* pushRecentTenant() pure helper exported for test
accessibility; PickerSectionHeader renders role="presentation"
so it doesn't break the listbox option-index pattern
* Currently-active tenant filtered from Recent (no dupe)
* Cross-tab + in-tab sync via kora:recent-tenants-changed event
Deliverable C — wizard skip with confirmation + per-step link:
* Header skip button + new per-step "Skip wizard, configure
manually" link (subtle text-button, bottom of step card)
both route through requestSkip → ConfirmDialog reuse
* confirmSkip writes marker via api.completeWizard then
clears the resume blob and navigates to Dashboard
* Modal stays open on error so operator sees the cause; can
cancel back into the wizard or retry
* .env partial download stays reachable via StepPromotionIntro
body (operator can download before triggering skip)
Drift-guard extension (tests/test_tenants_endpoint.py):
* RECENT_TENANTS_STORAGE_KEY + RECENT_TENANTS_CAP pins
* pushRecentTenant export pin
* TENANT_ANNOUNCER_LIVE_REGION_ROLE = "status" +
TENANT_ANNOUNCER_ARIA_LIVE = "polite" pins
* App.tsx mounts <TenantChangeAnnouncer /> + skip-to-main
anchor targets #kora-main; PageHeaderProvider <main> has
matching id
* WizardPage routes skip through ConfirmDialog +
requestSkip/confirmSkip naming pins
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rafe-walker
deleted the
feat/kora-KR-FE-A11Y-AUDIT-AND-MULTI-TENANT-POLISH-MEGABUCKET
branch
May 24, 2026 19:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three deliverables. CC#1 NousResearch#447 (which would unlock the per-tenant audit-filter verification, the #1 recommendation from #210) is still in flight, so this bucket picks up the #2 recommendation (ad-hoc a11y audit) and bundles two small multi-tenant ergonomics items.
A — Ad-hoc a11y audit. Top 4 findings fixed (skip-to-main, tenant-change live region, picker trigger aria-label, OAuth external-link aria-label). Remaining findings documented in the deferred section below.
B — TenantPicker recent-tenants list. Recent section (up to 3) above All section when ≥3 tenants observed; capped 5-entry localStorage; corruption + since-deleted-tenant safe.
C — Wizard skip with confirmation + per-step link. Header skip button now routes through a confirmation modal; new per-step "Skip wizard, configure manually" subtle link inside each step card.
A11y audit findings + fixes
<a href=\"#kora-main\">in App shell;<main>getsid+tabIndex={-1}so the anchor moves keyboard focusaria-livefor tenant selection changes<TenantChangeAnnouncer />sr-onlyaria-live=\"polite\"role=\"status\"mounted in App shell; suppresses first-render + single-tenant noiseOAuthProvidersCardexternal-link icon button has no aria-labelaria-labelon the anchor; nested<Button>markedaria-hidden+tabIndex={-1}so the link is the single accessible nameforced-colorsstyles anywhere inweb/src/@media (forced-colors: active) { color: CanvasText; border: 1px solid CanvasText; }on tone-only badgesDeleteConfirmDialogdoesn't enforce description propdescriptionprop or runtime check in a follow-onAudit method: ad-hoc, source-level grep + traversal — no axe-core / Lighthouse CI tooling added per §A.1 (CC#2 design call: ad-hoc this bucket, follow-on bucket adds CI tooling if findings warrant). Modals (
ConfirmDialog,OAuthLoginModal,ModelPickerDialog) correctly trap focus, restore focus on close, havearia-modal+aria-labelledby. Sidebar nav groups use<button>(keyboard accessible). Toast already has a live region. Picker keyboard nav from #210 verified intact.What changed
web/src/components/TenantChangeAnnouncer.tsx(new)Single sr-only live region in the App shell. Exports
TENANT_ANNOUNCER_LIVE_REGION_ROLE = \"status\"+TENANT_ANNOUNCER_ARIA_LIVE = \"polite\"for drift-pinning. First-render suppression vialastRef === nullguard avoids announcing the boot state. Skipped entirely on single-tenant deployments.web/src/App.tsxSkip-to-main link (
sr-only focus:not-sr-onlyTailwind utility pattern) +<TenantChangeAnnouncer />instance mounted before the existing layout chrome.web/src/contexts/PageHeaderProvider.tsx<main>gainsid=\"kora-main\"+tabIndex={-1}(programmatic focus for the anchor jump).web/src/components/TenantPicker.tsxaria-labelon trigger now announces the interaction modelrecentTenantsconsumed fromuseActiveTenant<PickerSectionHeader role=\"presentation\">rendered between Recent + All sections (presentation role keeps it out of the listbox option-index pattern){kind, tenantId}so duplicates between Recent + All are unique focus targets (keyboard nav stays correct); active-tenant matching prefers the All-section instance for initial highlightopt.tenantIdweb/src/hooks/useActiveTenant.tsRECENT_TENANTS_STORAGE_KEY = \"kora_recent_tenants\"+RECENT_TENANTS_CAP = 5readRecent/writeRecentwith corruption-safe parse (array-only, string-only, deduped, capped)pushRecentTenant(current, picked)pure helper for dedupe + capkora:recent-tenants-changedeventsetActiveTenantnow also pushes onto the recent list (in addition to localStorage write + opt-in URL write from feat(kora): KR-FE-TENANT-PICKER-KEYBOARD-NAV-AND-URL-TOGGLE-AND-TAB-TITLE — picker keyboard nav + URL-toggle + tab-title prefix #210)recentTenantsexposed on the hook result — filtered to entries still inavailableTenants(since-deleted tenant won't leak into the picker; aggregate sentinel always allowed)web/src/components/OAuthProvidersCard.tsxExternal-link icon button:
aria-labelmoved to anchor (Open <name> documentation in a new tab); nested<Button>becomesaria-hidden tabIndex={-1}.web/src/pages/WizardPage.tsxConfirmDialog(focus + escape + portal already correct — no new modal infra per §C.2 STOP-ASK avoidance)requestSkipopens modal;confirmSkipdoes the actualapi.completeWizard+clearPersisted+ navigaterequestSkiptests/test_tenants_endpoint.pyNew
test_fe_a11y_and_recent_tenants_pinscross-stack pin: recent-tenants storage key + cap + helper export, announcer role + politeness + sr-only class + App-shell mount, skip-to-main anchor target + matchingidon<main>, wizard skip routes throughConfirmDialogwithrequestSkip/confirmSkipnaming.Build
tsc -b && vite build✓ clean (pre-existing chunk-size warning; not introduced by this bucket)python3 -m py_compile tests/test_tenants_endpoint.py✓.venv); CI runs on PRTest plan
<main>kora_recent_tenantslocalStorage value (e.g.,\"not-an-array\") → picker still renders, Recent stays emptyapi.completeWizardsimulated failure → modal stays open with error in description; cancel returns to wizardpytest tests/test_tenants_endpoint.py— 8 tests pass (3 endpoint + 5 drift-guard incl. new one)Recommendation for next CC#2 dispatch
Per-tenant audit BE filter verification (once CC#1 NousResearch#447 lands). Still the #1 outstanding item from #208/#210. Check
gh pr view 447 --json state,mergedbefore dispatch.Lighter alternatives if NousResearch#447 is still in flight:
@media (forced-colors: active)rules to badge/tone classes + tenant-picker option states so high-contrast (Windows) mode doesn't lose color-only cues. Run cockpit in Chrome devtoolsforced-colors: activeemulation; fix what fails. ~2-3 hours.@axe-core/reactdev-only audit + add a GitHub Actions step to fail PRs that introduce new violations. ~2 hours (most time is wiring CI; the axe-core integration itself is small).🤖 Generated with Claude Code