Skip to content

a11y(tools): replace div+onClick with semantic button elements (#5223)#5350

Open
armorbreak001 wants to merge 1 commit intoasyncapi:masterfrom
armorbreak001:fix/tools-page-a11y
Open

a11y(tools): replace div+onClick with semantic button elements (#5223)#5350
armorbreak001 wants to merge 1 commit intoasyncapi:masterfrom
armorbreak001:fix/tools-page-a11y

Conversation

@armorbreak001
Copy link
Copy Markdown

@armorbreak001 armorbreak001 commented Apr 22, 2026

Problem

Fixes #5223

The /tools page Filter and Jump to Category controls use <div onClick> instead of <button>, causing:

  • No keyboard focus visibility
  • Inconsistent Enter/Space key activation
  • Screen readers not announcing them as interactive controls
  • Invalid HTML: filter popup wrapped in <button> containing other interactive elements

Changes

components/tools/ToolsDashboard.tsx

Element Before After
Filter trigger <div className=... onClick={...}> <button type=button ...>
Filter popup wrapper <button><Filters/></button> <div role=menu> (non-interactive)
Jump to Category <div className=... onClick={...}> <button type=button ...>
Text wrappers <div>Filter</div> <span>Filter</span>

components/tools/Filters.tsx

Element Before After
Undo Changes <div className=... onClick={undoChanges}> <button type=button ...>

Accessibility Impact

  • ✅ Controls now receive proper keyboard focus with visible outline
  • ✅ Enter/Space keys activate controls natively
  • ✅ Screen readers announce as buttons, not generic text
  • ✅ Valid HTML: no nested interactive elements
  • ✅ All existing styling and behavior preserved

Summary by CodeRabbit

  • Refactor
    • Improved semantic HTML structure and accessibility in filter and dashboard components by updating interactive elements for better standards compliance.

…api#5223)

Problem: Filter and Jump to Category controls on /tools page are
implemented as clickable div elements instead of semantic buttons.
This causes:
- Inconsistent keyboard focus/activation behavior
- Screen reader announcement issues
- Invalid markup (button wrapping interactive children)

Changes:
- ToolsDashboard.tsx:
  - Filter trigger: <div onClick> → <button type='button'>
  - Filter popup wrapper: <button> → <div role='menu'> (non-interactive)
  - Jump to Category: <div onClick> → <button type='button'>
  - Inner text wrappers: <div> → <span>
- Filters.tsx:
  - Undo Changes: <div onClick> → <button type='button'>

All changes preserve existing styling and behavior while adding
proper keyboard accessibility and ARIA semantics.
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 22, 2026

Deploy Preview for asyncapi-website ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 1b4e394
🔍 Latest deploy log https://app.netlify.com/projects/asyncapi-website/deploys/69e8751edad5c200088221ab
😎 Deploy Preview https://deploy-preview-5350--asyncapi-website.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@asyncapi-bot
Copy link
Copy Markdown
Contributor

asyncapi-bot commented Apr 22, 2026

We require all PRs to follow Conventional Commits specification.
More details 👇🏼

 Unknown release type "a11y" found in pull request title "a11y(tools): replace div+onClick with semantic button elements (#5223)". 

Available types:
 - feat: A new feature
 - fix: A bug fix
 - docs: Documentation only changes
 - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 - refactor: A code change that neither fixes a bug nor adds a feature
 - perf: A code change that improves performance
 - test: Adding missing tests or correcting existing tests
 - build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
 - chore: Other changes that don't modify src or test files
 - revert: Reverts a previous commit

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Welcome to AsyncAPI. Thanks a lot for creating your first pull request. Please check out our contributors guide useful for opening a pull request.
Keep in mind there are also other channels you can use to interact with AsyncAPI community. For more details check out this issue.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

The changes replace non-semantic clickable div elements with proper <button type="button"> elements across the tools page components. Additionally, the filter dropdown wrapper is converted from a <button> to a semantic <div> with role="menu" to prevent invalid nested interactive markup while preserving functionality.

Changes

Cohort / File(s) Summary
Semantic Button Replacements
components/tools/Filters.tsx, components/tools/ToolsDashboard.tsx
Replaced clickable div elements with semantic <button type="button"> controls for "Undo Changes," Filter toggle, and "Jump to Category" toggle, updating inner content wrappers from div to span where applicable.
Filter Dropdown Wrapper
components/tools/ToolsDashboard.tsx
Changed conditionally-rendered filter dropdown container from <button> to <div> with role="menu" to eliminate invalid nested interactive content while maintaining open/close control flow and positioning.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 Our tools were stubborn divs, not grand,
No keyboard love in this old land,
But buttons true now rule the day,
Accessible paths for all to play! 🎉
(Even keyboard-hoppers navigate with glee!)

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: replacing non-semantic div+onClick elements with semantic button elements for accessibility improvements.
Linked Issues check ✅ Passed All code changes directly address issue #5223 requirements: replacing clickable divs with semantic buttons, converting filter popup wrapper from button to div, and preserving styling and behavior.
Out of Scope Changes check ✅ Passed All changes in both files are directly scoped to accessibility improvements for the /tools page controls as specified in issue #5223; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (621272f) to head (1b4e394).

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #5350   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           22        22           
  Lines          830       830           
  Branches       159       159           
=========================================
  Hits           830       830           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@asyncapi-bot
Copy link
Copy Markdown
Contributor

⚡️ Lighthouse report for the changes in this PR:

Category Score
🔴 Performance 44
🟢 Accessibility 98
🟢 Best practices 92
🟢 SEO 100
🔴 PWA 33

Lighthouse ran on https://deploy-preview-5350--asyncapi-website.netlify.app/

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/tools/ToolsDashboard.tsx (1)

233-237: ⚠️ Potential issue | 🟠 Major

Use a semantic button for Clear Filters too.

This remains a clickable div, so keyboard users cannot focus or activate it when filters are active.

♿ Proposed fix
         {isFiltered && (
-          <div className='mt-4 flex cursor-pointer items-center text-gray-600 hover:text-black' onClick={clearFilters}>
+          <button
+            type='button'
+            className='mt-4 flex cursor-pointer items-center bg-transparent p-0 text-gray-600 hover:text-black'
+            onClick={clearFilters}
+          >
             <Cross />
             <span className='ml-3'>Clear Filters</span>
-          </div>
+          </button>
         )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/tools/ToolsDashboard.tsx` around lines 233 - 237, Replace the
clickable div used when isFiltered is true with a semantic <button> element
(keeping the same visual classes and the Cross component) so keyboard users can
focus and activate it; ensure the button has type="button", retains the onClick
handler clearFilters, and include an accessible name (either visible text "Clear
Filters" or aria-label) so screen readers can announce its purpose. Reference
the isFiltered conditional and clearFilters handler and update the element that
currently renders the Cross icon to be a button to provide native keyboard and
accessibility behavior.
components/tools/Filters.tsx (1)

147-163: ⚠️ Potential issue | 🟠 Major

Finish replacing the remaining clickable <div> controls in this panel.

Keyboard users can open the filter panel, but the pricing chips, dropdown headers, and Apply Filters wrapper still depend on div onClick. Convert them to semantic buttons and add aria-pressed/aria-expanded where stateful.

♿ Example direction
-            <div
+            <button
+              type='button'
               className={twMerge(
                 `bg-gray-200 px-4 py-2 flex gap-1 rounded-md hover:bg-secondary-100 border hover:border-secondary-500 cursor-pointer ${checkPaid === 'free' ? 'bg-secondary-100 border-secondary-500' : ''}`
               )}
+              aria-pressed={checkPaid === 'free'}
               onClick={() => (checkPaid === 'free' ? setCheckPaid('all') : setCheckPaid('free'))}
             >
-              <div className='text-sm'>Open Source</div>
+              <span className='text-sm'>Open Source</span>
               <img src='/img/illustrations/icons/FreeIcon.svg' alt='Free' />
-            </div>
+            </button>
-            <div
+            <button
+              type='button'
               className={twMerge(
                 `px-4 py-2 flex justify-between rounded-lg border border-gray-400 w-full bg-gray-200 text-gray-700 shadow text-sm cursor-pointer ${openedFiltersDropown === OpenedFiltersDropdownType.LANGUAGE ? 'rounded-b-none' : ''}`
               )}
+              aria-expanded={openedFiltersDropown === OpenedFiltersDropdownType.LANGUAGE}
               onClick={() => toggleDropdown(OpenedFiltersDropdownType.LANGUAGE)}
             >

Also applies to: 195-200, 237-242, 279-284, 310-312

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/tools/Filters.tsx` around lines 147 - 163, Replace the interactive
<div>s used for the pricing chips and other filter controls with semantic
<button type="button"> elements (e.g., the Open Source/Commercial chips that
read checkPaid and call setCheckPaid) while preserving existing className, click
handlers, and data-testid ('Applied-filters'); add aria-pressed to toggle
buttons that reflect checkPaid state and aria-expanded to any dropdown header
buttons that open/close panels, and ensure keyboard behavior is preserved
(focusable, no implicit submit). Also convert the Apply Filters wrapper to a
button with appropriate aria attributes and role if it opens a panel, keeping
visual styles and onClick handlers unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/tools/ToolsDashboard.tsx`:
- Around line 179-191: The container for the filter panel in ToolsDashboard
currently uses role='menu' which is incorrect because Filters renders form
controls, so remove role='menu' from the panel and instead connect the trigger
button and panel with accessible state: add aria-expanded={openFilter} to the
Filter button (the onClick that toggles openFilter) and give the panel a stable
id (e.g., filters-panel) then add aria-controls="filters-panel" on the button;
keep the panel as a plain div (no menu/menuitem roles) and ensure Filters
receives setOpenFilter so closing logic remains intact.
- Around line 197-207: The button toggle for the category dropdown currently
uses openCategory and setopenCategory but does not expose state to assistive
tech; update the button element rendered in ToolsDashboard (the toggle that
calls setopenCategory and uses openCategory) to include aria-expanded tied to
openCategory (aria-expanded={openCategory}) and add aria-controls pointing to
the popup container's id, and give the popup div a matching unique id and an
appropriate landmark/role (e.g., role="menu" or role="region") so screen readers
can associate the button with the popup; ensure the popup div that renders when
openCategory is true (the div with className 'absolute right-52 top-16 z-20')
receives that id.

---

Outside diff comments:
In `@components/tools/Filters.tsx`:
- Around line 147-163: Replace the interactive <div>s used for the pricing chips
and other filter controls with semantic <button type="button"> elements (e.g.,
the Open Source/Commercial chips that read checkPaid and call setCheckPaid)
while preserving existing className, click handlers, and data-testid
('Applied-filters'); add aria-pressed to toggle buttons that reflect checkPaid
state and aria-expanded to any dropdown header buttons that open/close panels,
and ensure keyboard behavior is preserved (focusable, no implicit submit). Also
convert the Apply Filters wrapper to a button with appropriate aria attributes
and role if it opens a panel, keeping visual styles and onClick handlers
unchanged.

In `@components/tools/ToolsDashboard.tsx`:
- Around line 233-237: Replace the clickable div used when isFiltered is true
with a semantic <button> element (keeping the same visual classes and the Cross
component) so keyboard users can focus and activate it; ensure the button has
type="button", retains the onClick handler clearFilters, and include an
accessible name (either visible text "Clear Filters" or aria-label) so screen
readers can announce its purpose. Reference the isFiltered conditional and
clearFilters handler and update the element that currently renders the Cross
icon to be a button to provide native keyboard and accessibility behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 71cdf94f-6ee8-4037-8f63-96ed70392ff9

📥 Commits

Reviewing files that changed from the base of the PR and between 621272f and 1b4e394.

📒 Files selected for processing (2)
  • components/tools/Filters.tsx
  • components/tools/ToolsDashboard.tsx

Comment on lines +179 to +191
<button
type='button'
className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
onClick={() => setOpenFilter(!openFilter)}
data-testid='ToolsDashboard-Filters-Click'
>
<FilterIcon />
<div>Filter</div>
</div>
<span>Filter</span>
</button>
{openFilter && (
<button className='absolute top-16 z-20 min-w-[20rem]'>
<div className='absolute top-16 z-20 min-w-[20rem]' role='menu'>
<Filters setOpenFilter={setOpenFilter} />
</button>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t expose the filter panel as an ARIA menu.

Line 189 adds role='menu', but Filters renders form-like controls rather than menuitem descendants. Use a plain container and wire it to the trigger with expanded/control state.

♿ Proposed fix
               <button
                 type='button'
+                aria-expanded={openFilter}
+                aria-controls='tools-filter-panel'
                 className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
                 onClick={() => setOpenFilter(!openFilter)}
                 data-testid='ToolsDashboard-Filters-Click'
               >
@@
               </button>
               {openFilter && (
-                <div className='absolute top-16 z-20 min-w-[20rem]' role='menu'>
+                <div id='tools-filter-panel' className='absolute top-16 z-20 min-w-[20rem]'>
                   <Filters setOpenFilter={setOpenFilter} />
                 </div>
               )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
type='button'
className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
onClick={() => setOpenFilter(!openFilter)}
data-testid='ToolsDashboard-Filters-Click'
>
<FilterIcon />
<div>Filter</div>
</div>
<span>Filter</span>
</button>
{openFilter && (
<button className='absolute top-16 z-20 min-w-[20rem]'>
<div className='absolute top-16 z-20 min-w-[20rem]' role='menu'>
<Filters setOpenFilter={setOpenFilter} />
</button>
</div>
<button
type='button'
aria-expanded={openFilter}
aria-controls='tools-filter-panel'
className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
onClick={() => setOpenFilter(!openFilter)}
data-testid='ToolsDashboard-Filters-Click'
>
<FilterIcon />
<span>Filter</span>
</button>
{openFilter && (
<div id='tools-filter-panel' className='absolute top-16 z-20 min-w-[20rem]'>
<Filters setOpenFilter={setOpenFilter} />
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/tools/ToolsDashboard.tsx` around lines 179 - 191, The container
for the filter panel in ToolsDashboard currently uses role='menu' which is
incorrect because Filters renders form controls, so remove role='menu' from the
panel and instead connect the trigger button and panel with accessible state:
add aria-expanded={openFilter} to the Filter button (the onClick that toggles
openFilter) and give the panel a stable id (e.g., filters-panel) then add
aria-controls="filters-panel" on the button; keep the panel as a plain div (no
menu/menuitem roles) and ensure Filters receives setOpenFilter so closing logic
remains intact.

Comment on lines +197 to 207
<button
type='button'
className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
onClick={() => setopenCategory(!openCategory)}
data-testid='ToolsDashboard-category'
>
<div>Jump to Category</div>
<span>Jump to Category</span>
<ArrowDown className={`my-auto ${openCategory ? 'rotate-180' : ''}`} />
</div>
</button>
{openCategory && (
<div className='absolute right-52 top-16 z-20'>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Expose the Jump to Category expanded state.

The trigger is now keyboard-operable, but screen readers still won’t know whether the dropdown is open. Add aria-expanded and connect it to the popup.

♿ Proposed fix
               <button
                 type='button'
+                aria-expanded={openCategory}
+                aria-controls='tools-category-dropdown'
                 className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
                 onClick={() => setopenCategory(!openCategory)}
                 data-testid='ToolsDashboard-category'
               >
@@
               </button>
               {openCategory && (
-                <div className='absolute right-52 top-16 z-20'>
+                <div id='tools-category-dropdown' className='absolute right-52 top-16 z-20'>
                   <CategoryDropdown setopenCategory={setopenCategory} />
                 </div>
               )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
type='button'
className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
onClick={() => setopenCategory(!openCategory)}
data-testid='ToolsDashboard-category'
>
<div>Jump to Category</div>
<span>Jump to Category</span>
<ArrowDown className={`my-auto ${openCategory ? 'rotate-180' : ''}`} />
</div>
</button>
{openCategory && (
<div className='absolute right-52 top-16 z-20'>
<button
type='button'
aria-expanded={openCategory}
aria-controls='tools-category-dropdown'
className='flex h-14 w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 px-4 py-1 text-sm text-gray-700 shadow hover:border-gray-600 hover:shadow-md'
onClick={() => setopenCategory(!openCategory)}
data-testid='ToolsDashboard-category'
>
<span>Jump to Category</span>
<ArrowDown className={`my-auto ${openCategory ? 'rotate-180' : ''}`} />
</button>
{openCategory && (
<div id='tools-category-dropdown' className='absolute right-52 top-16 z-20'>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/tools/ToolsDashboard.tsx` around lines 197 - 207, The button
toggle for the category dropdown currently uses openCategory and setopenCategory
but does not expose state to assistive tech; update the button element rendered
in ToolsDashboard (the toggle that calls setopenCategory and uses openCategory)
to include aria-expanded tied to openCategory (aria-expanded={openCategory}) and
add aria-controls pointing to the popup container's id, and give the popup div a
matching unique id and an appropriate landmark/role (e.g., role="menu" or
role="region") so screen readers can associate the button with the popup; ensure
the popup div that renders when openCategory is true (the div with className
'absolute right-52 top-16 z-20') receives that id.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To Be Triaged

Development

Successfully merging this pull request may close these issues.

[BUG] Tools page filter controls are not keyboard accessible and use invalid interactive markup

2 participants