-
-
Notifications
You must be signed in to change notification settings - Fork 9
a11y: implement WAI-ARIA Tabs pattern on tab navigation #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -150,6 +150,28 @@ export function Panel() { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }, [tabId, state.reduxDetected]); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // WAI-ARIA Tabs — arrow-key roving focus between tabs | ||||||||||||||||||||||||||||||||
| const handleTabKeyDown = useCallback((e: React.KeyboardEvent, currentIndex: number) => { | ||||||||||||||||||||||||||||||||
| const tabCount = TABS.length; | ||||||||||||||||||||||||||||||||
| let nextIndex: number | null = null; | ||||||||||||||||||||||||||||||||
| if (e.key === 'ArrowRight') { | ||||||||||||||||||||||||||||||||
| nextIndex = (currentIndex + 1) % tabCount; | ||||||||||||||||||||||||||||||||
| } else if (e.key === 'ArrowLeft') { | ||||||||||||||||||||||||||||||||
| nextIndex = (currentIndex - 1 + tabCount) % tabCount; | ||||||||||||||||||||||||||||||||
| } else if (e.key === 'Home') { | ||||||||||||||||||||||||||||||||
| nextIndex = 0; | ||||||||||||||||||||||||||||||||
| } else if (e.key === 'End') { | ||||||||||||||||||||||||||||||||
| nextIndex = tabCount - 1; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if (nextIndex !== null) { | ||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||
| const nextTab = TABS[nextIndex]; | ||||||||||||||||||||||||||||||||
| handleTabChange(nextTab.id); | ||||||||||||||||||||||||||||||||
| const nextBtn = document.getElementById(`tab-${nextTab.id}`); | ||||||||||||||||||||||||||||||||
| nextBtn?.focus(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }, [handleTabChange]); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||
| fetchState(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -427,25 +449,38 @@ export function Panel() { | |||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| </header> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| <nav className="tab-nav"> | ||||||||||||||||||||||||||||||||
| {TABS.map(tab => { | ||||||||||||||||||||||||||||||||
| <nav className="tab-nav" role="tablist" aria-label="React Debugger sections"> | ||||||||||||||||||||||||||||||||
| {TABS.map((tab, index) => { | ||||||||||||||||||||||||||||||||
| const badge = getBadge(tab.id); | ||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||
| key={tab.id} | ||||||||||||||||||||||||||||||||
| id={`tab-${tab.id}`} | ||||||||||||||||||||||||||||||||
| role="tab" | ||||||||||||||||||||||||||||||||
| aria-selected={activeTab === tab.id} | ||||||||||||||||||||||||||||||||
| aria-controls={`panel-${tab.id}`} | ||||||||||||||||||||||||||||||||
| tabIndex={activeTab === tab.id ? 0 : -1} | ||||||||||||||||||||||||||||||||
| className={`tab-button ${activeTab === tab.id ? 'active' : ''}`} | ||||||||||||||||||||||||||||||||
| onClick={() => handleTabChange(tab.id)} | ||||||||||||||||||||||||||||||||
| onKeyDown={(e) => handleTabKeyDown(e, index)} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <span className="tab-label">{tab.label}</span> | ||||||||||||||||||||||||||||||||
| {badge !== undefined && badge > 0 && ( | ||||||||||||||||||||||||||||||||
| <span className="tab-badge">{badge}</span> | ||||||||||||||||||||||||||||||||
| <span className="tab-badge" aria-label={`${badge} items`}>{badge}</span> | ||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||
| </nav> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| <main className="tab-content"> | ||||||||||||||||||||||||||||||||
| <main | ||||||||||||||||||||||||||||||||
| className="tab-content" | ||||||||||||||||||||||||||||||||
| role="tabpanel" | ||||||||||||||||||||||||||||||||
| id={`panel-${activeTab}`} | ||||||||||||||||||||||||||||||||
| aria-labelledby={`tab-${activeTab}`} | ||||||||||||||||||||||||||||||||
| aria-live="polite" | ||||||||||||||||||||||||||||||||
| tabIndex={0} | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
|
Comment on lines
+476
to
+483
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applying Because this is a debugger panel with real-time streaming data (such as timeline events, performance metrics, and memory updates), any background update will trigger continuous screen reader announcements. This will overwhelm screen reader users and make the tool extremely noisy and difficult to use.
Suggested change
|
||||||||||||||||||||||||||||||||
| {!isDebuggerEnabled && state.timelineEvents.length === 0 ? ( | ||||||||||||||||||||||||||||||||
| <div className="debugger-disabled-placeholder"> | ||||||||||||||||||||||||||||||||
| <div className="placeholder-icon"> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using global
document.getElementByIdinside a React component is a code smell and can lead to bugs if multiple instances of this panel are rendered (e.g., in testing environments or multiple DevTools windows). Scoping the query to the component's subtree is safer and more idiomatic.Since the event handler is attached directly to the tab buttons, you can leverage
e.currentTarget.parentElementto query only within the parent<nav>container.