Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions src/panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment on lines +170 to +171

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using global document.getElementById inside 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.parentElement to query only within the parent <nav> container.

Suggested change
const nextBtn = document.getElementById(`tab-${nextTab.id}`);
nextBtn?.focus();
const nextBtn = (e.currentTarget as HTMLElement).parentElement?.querySelector<HTMLButtonElement>(`#tab-${nextTab.id}`);
nextBtn?.focus();

}
}, [handleTabChange]);

useEffect(() => {
fetchState();

Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Applying aria-live="polite" to the entire <main> tab panel is an accessibility anti-pattern.

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.

aria-live should only be used on small, specific regions (like a status message or toast notification) rather than large content containers. Please remove aria-live="polite" from the main tab panel.

Suggested change
<main
className="tab-content"
role="tabpanel"
id={`panel-${activeTab}`}
aria-labelledby={`tab-${activeTab}`}
aria-live="polite"
tabIndex={0}
>
<main
className="tab-content"
role="tabpanel"
id={`panel-${activeTab}`}
aria-labelledby={`tab-${activeTab}`}
tabIndex={0}
>

{!isDebuggerEnabled && state.timelineEvents.length === 0 ? (
<div className="debugger-disabled-placeholder">
<div className="placeholder-icon">
Expand Down
Loading