Skip to content

fix(a11y): convert click-only divs/spans to keyboard-accessible buttons#64

Merged
hoainho merged 1 commit into
hoainho:mainfrom
Kunall7890:fix/a11y-keyboard-accessible-buttons
Jun 7, 2026
Merged

fix(a11y): convert click-only divs/spans to keyboard-accessible buttons#64
hoainho merged 1 commit into
hoainho:mainfrom
Kunall7890:fix/a11y-keyboard-accessible-buttons

Conversation

@Kunall7890

Copy link
Copy Markdown
Contributor

Linked issue

Closes #29

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Accessibility improvement (a11y)

What changed

Converted 5 click-only toggle elements (containers that expand/collapse cards/logs) to fully keyboard-accessible role="button" elements with tabIndex={0}, aria-expanded and custom keyboard event handlers (onKeyDown handling both Enter and Space).

Locations handled:

  • IssueCard.tsx: .issue-header (changed nested visual expand button to a span element to avoid invalid HTML nested button trees)
  • AIAnalysisTab.tsx: .ai-item-card
  • TimelineTab.tsx: .snapshot-header and .snapshot-item-header (safeguarded keyboard events with target equality check to prevent bubbled events from child action buttons from triggering parent collapse/expand)
  • MemoryTab.tsx: .crash-header

Additionally:

  • Configured visual focus outline rings (:focus-visible styling) consistently for all [role="button"] controls in panel.css.
  • Updated test selectors in IssueCard.test.tsx to handle container-level role="button" names.

Testing notes

$ npm run build
(build succeeds with no errors)

$ npm run test:run
(modified test files pass successfully; remaining pre-existing failures on main are unrelated to these changes)

Checklist

  • npm run build succeeds
  • Keyboard tab, Enter, and Space interaction verified
  • PR title follows Conventional Commits
  • No visual regression

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request improves keyboard accessibility and ARIA compliance across several interactive components (such as IssueCard, AIAnalysisTab, MemoryTab, and TimelineTab) by adding role="button", tabIndex, aria-expanded, and keyboard event handlers, along with corresponding CSS focus-visible styles. The review feedback highlights two critical accessibility issues: applying role="button" to the entire .ai-item-card container in AIAnalysisTab.tsx is an anti-pattern due to its complex nested content, and applying it to .snapshot-header in TimelineTab.tsx creates an invalid nested interactive control violation because of an internal native button.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 32 to 46
<div
className="ai-item-card"
style={{ borderLeftColor: style.color, backgroundColor: style.bg }}
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={() => setExpanded(!expanded)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpanded(!expanded);
}
}}
>
<div className="ai-item-header">

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 role="button" to the entire .ai-item-card container makes all of its content—including the description, suggestions, and affected components inside .ai-item-body when expanded—part of the button's accessible content. This is an accessibility anti-pattern because buttons should not contain complex structured or interactive content.

Instead, only the .ai-item-header should act as the interactive toggle button, keeping the .ai-item-body semantically separate.

Suggested change
<div
className="ai-item-card"
style={{ borderLeftColor: style.color, backgroundColor: style.bg }}
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={() => setExpanded(!expanded)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpanded(!expanded);
}
}}
>
<div className="ai-item-header">
<div
className="ai-item-card"
style={{ borderLeftColor: style.color, backgroundColor: style.bg }}
>
<div
className="ai-item-header"
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={() => setExpanded(!expanded)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpanded(!expanded);
}
}}
>

Comment on lines +557 to 572
<div
className="snapshot-header"
role="button"
tabIndex={0}
aria-expanded={snapshotPanelOpen}
onClick={() => setSnapshotPanelOpen(!snapshotPanelOpen)}
onKeyDown={(e) => {
if (e.target !== e.currentTarget) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setSnapshotPanelOpen(!snapshotPanelOpen);
}
}}
>
<span className="snapshot-toggle">{snapshotPanelOpen ? '▼' : '▶'}</span>
<span className="snapshot-title">Snapshots ({snapshots.length})</span>

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 role="button" to the entire .snapshot-header container creates a nested interactive control violation because it contains a native <button className="snapshot-create-btn"> element. Nested interactive controls are invalid HTML and violate WCAG/ARIA standards, causing screen readers and keyboard navigation to behave unpredictably.

Instead of making the entire header a button and using target checks to prevent event bubbling, wrap only the toggle icon and title in a separate role="button" element, leaving the create button as a sibling.

        <div className="snapshot-header">
          <div
            className="snapshot-toggle-button"
            role="button"
            tabIndex={0}
            aria-expanded={snapshotPanelOpen}
            onClick={() => setSnapshotPanelOpen(!snapshotPanelOpen)}
            onKeyDown={(e) => {
              if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                setSnapshotPanelOpen(!snapshotPanelOpen);
              }
            }}
            style={{ display: 'flex', alignItems: 'center', gap: '8px', flex: 1 }}
          >
            <span className="snapshot-toggle">{snapshotPanelOpen ? '▼' : '▶'}</span>
            <span className="snapshot-title">Snapshots ({snapshots.length})</span>
          </div>

@hoainho hoainho added the pre-star-rule PR opened before the star-check rule landed (2026-06-01); exempt from Star Check CI label Jun 7, 2026
@hoainho

hoainho commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Hey @Kunall7890 — fourth PR in 24 hours, you're making it hard to keep up! 🎉

Same false-negative on the Star Check (read-replica lag, your star is there). I've applied the pre-star-rule label to bypass and re-run. No action needed on your side.

a11y review — this is the real deal ✅

5 click-only elements converted to role="button" + tabIndex={0} + aria-expanded ✅ Full keyboard accessibility (Tab → focus, Enter/Space → activate).
onKeyDown handles both Enter AND Space ✅ Matches the WAI-ARIA button spec — Space is the primary activator for role="button".
IssueCard.tsx fix ✅ Spotted the "nested visual button" issue and changed the inner element to a span — invalid HTML nested buttons are a real WCAG failure.
TimelineTab.tsx event-target guard e.target === e.currentTarget prevents child action buttons from bubbling to trigger parent collapse. That's a subtle bug-a-day-saved.
:focus-visible styling for [role="button"] ✅ Visible focus rings on keyboard nav only — mouse users don't see them.
Test selectors updated ✅ Regex matchers (name: /▶/) handle the role="button" with text content correctly.

Note on the launch-day timing

This is the 4th PR you've opened in the last ~20 hours. All four are tight, well-tested, well-documented, and close real issues (#30#58, #5#61, #31#62, #29#64). That's not normal — that's contributor-loop mastery.

I'm going to merge #62 (timeout constants) first to avoid file-overlap conflicts with this one (#62 and #64 both touch TimelineTab.tsx and MemoryTab.tsx). If #64 ends up with a conflict after #62 lands, I'll ask you to rebase. But I expect it to be clean since your changes are on different lines.

Why this matters for launch

  • a11y is the kind of contribution that wins the day — 12% of users have a disability, and keyboard-only navigation is a real barrier.
  • Hits on launch day 🚀 — v2.0.3 release notes will list "a11y: keyboard-accessible buttons" as a contributor credit.
  • You're 4-for-4 on follow-up suggestions from PR ux: replace alert() validation with inline error messages in ReduxTab #58's merge comment. If you have a 5th in mind, send it; otherwise, take a break — you've earned it.

Going to merge once CI re-runs (after #62 lands). Issue #29 will auto-close.

— Hoài

@hoainho hoainho merged commit 90c59f3 into hoainho:main Jun 7, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pre-star-rule PR opened before the star-check rule landed (2026-06-01); exempt from Star Check CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a11y: convert click-only divs/spans to keyboard-accessible buttons (5 locations)

2 participants