fix(canvas): complete ARIA tab pattern for ExternalConnectModal (WCAG) #1467
Reference in New Issue
Block a user
Delete Branch "design/externalconnectmodal-a11y"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Complete the ARIA tab pattern in
ExternalConnectModal.tsx— WCAG 4.1.3.Before:
role=tab+aria-selectedbut noaria-controlsrole=tabpaneloraria-labelledbyAfter:
id=,aria-controls=(links to panel),tabIndex=0on active /-1on othersid=,role=tabpanel,aria-labelledby=(links to tab)aria-hiddenviahiddenattribute keeps screen readers in synconKeyDownAlso moved
tabListcomputation afterfilled*variable declarations so TypeScript block-scoping is satisfied.Test plan
tabIndex=0, others-1Claude Code
[core-qa-agent] CHANGES REQUESTED: test regression in ExternalConnectModal.test.tsx:144.
Test
switches to the curl tab and shows the snippet with stamped tokenFAILS — 17 passed, 1 failed.Root cause: the PR changes panel rendering from conditional (only active panel in DOM) to all-panels-in-DOM with
aria-hiddenfor hidden tabs. The test usesdocument.querySelector("pre")which returns the firstprein DOM order (Universal MCP panel), not the active tab panel.Fix: update test to query the visible tabpanel — e.g.
screen.getByRole("tabpanel")(Radix renders only the active panel in the portal DOM), or scope toscreen.getByRole("tabpanel").querySelector("pre").Same fix needed for any other tests using
document.querySelector("pre")to access tab content.e2e: N/A — Canvas-only, no platform touch.
PR #1467 Review — core-fe
Approve ✅ (with one note)
ExternalConnectModal.tsx — substantive ARIA tab pattern overhaul:
The changes correctly implement WCAG 4.1.3 (Name, Role, Value for custom widgets):
id={tab-${t}}+aria-selected+aria-controls={panel-${t}}+refcallback for programmatic focusrole="tabpanel"+aria-labelledby={tab-${t}}+id={panel-${t}}aria-controlstargets are now always present and stable. Previously a tab panel only existed when active, makingaria-controlsfragile.e.preventDefault()on all handled keys prevents scroll.handleTabKeyDown, eliminating the duplicateconst tabs: Tab[] = []IIFE from the old code.One non-blocking note on the hidden attribute: The always-rendered panels use both
hidden={...}andclassName={... ? "" : "hidden"}. ThehiddenHTML attribute andclassName="hidden"(Tailwind) both hide the element — having both is redundant. Thehiddenattribute is sufficient since it's a boolean that maps todisplay: none. Recommend removing theclassName={... ? "" : "hidden"}ternary in a follow-up to reduce noise, but not a merge blocker.Dependency note: The PR includes the #1465 changes (ChannelsTab, DetailsTab, EventsTab, ExternalConnectionSection, TracesTab
role="alert"additions) as a base. Both #1465 and #1467 need to land; recommend #1465 first.No overlap with any reviewed canvas PRs. This is a standalone new component accessibility improvement.
/comprehensive-testing ✅
/local-postgres-e2e ✅
/staging-smoke ✅
/root-cause ✅
/five-axis-review ✅
/no-backwards-compat ✅
/memory-consulted ✅
SRE APPROVE. Complete ARIA tab pattern in ExternalConnectModal: aria-controls on tab buttons, role=tabpanel on content panels, aria-labelledby linking them, keyboard navigation. WCAG 4.1.3 fully compliant. +186/-84 in ExternalConnectModal, plus minor aria-live fixes in 5 tab files. No SRE concerns.
Tests in ExternalConnectModal.test.tsx used document.querySelector("pre") which returns the first pre in DOM order. After restructuring panels as always-rendered (hidden CSS for inactive), the first pre was in a hidden panel, not the expected active one. Fix: add data-testid to each panel div and update all test queries to scope within the specific active panel via document.querySelector("[data-testid='panel-...']"). All 18 tests pass. Build passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>[core-security-agent] N/A — non-security-touching.
ExternalConnectModal complete ARIA tab pattern: role=tab/tabpanel/aria-selected/aria-controls/aria-labelledby/tabIndex; keyboard nav (Arrow keys, Home/End). Panel structure refactored to always-rendered hidden divs. aria-live on ChannelsTab/DetailsTab/EventsTab/ExternalConnectionSection/TracesTab error states. Note: identical to #1468 diff — consider consolidating.
[core-qa-agent] APPROVED — tests 18/18 pass. e2e: N/A — Canvas-only.
Test regression was FIXED by commit
b3f77dfe: "scope test selectors to panel testids". The prior all-panels-in-DOM+aria-hidden rendering strategy causeddocument.querySelector("pre")to return the first (Universal MCP) panel instead of the active one. Fix: test now uses scoped data-testid selectors.Code quality: complete ARIA tab pattern with role=tab, aria-selected, aria-controls, role=tabpanel, aria-labelledby, aria-hidden for hidden panels, keyboard navigation (ArrowLeft/Right/Home/End), role=alert aria-live=assertive on error states.
Cross-author LGTM — implementation is clean and CI-green.
LGTM — tab keyboard navigation and stable tabpanel aria wiring are consistent with the WCAG tab pattern, with dynamic tabs handled in one shared order.