Skip to content

fix(a11y): add keyboard navigation and ARIA labels to filter controls - #2053

Open
waterWang wants to merge 1 commit into
S3DFX-CYBER:mainfrom
waterWang:fix/filter-controls-keyboard-accessibility-1969
Open

fix(a11y): add keyboard navigation and ARIA labels to filter controls#2053
waterWang wants to merge 1 commit into
S3DFX-CYBER:mainfrom
waterWang:fix/filter-controls-keyboard-accessibility-1969

Conversation

@waterWang

@waterWang waterWang commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Fixes accessibility issues with the organization filter controls:

  • Mobile filter and language dropdowns lacked proper ARIA attributes
  • Dropdowns could not be closed with the Escape key
  • Focus management was missing when dropdowns opened/closed
  • Filter chips and pills lacked visible focus indicators
  • Screen readers could not identify expanded/collapsed state of dropdowns

Changes

HTML (index.html)

  • Mobile filter button: Added aria-expanded and aria-controls attributes
  • Mobile filter panel: Added role="menu" and aria-hidden for screen reader support
  • Mobile filter options: Added role="menuitem" to each chip button
  • Mobile language button: Added aria-expanded and aria-controls attributes
  • Mobile language panel: Added role="menu" and aria-hidden
  • Added focus-visible:outline-* Tailwind classes to all mobile filter buttons

JavaScript (inline)

  • toggleMobileFilterDropdown(): Now manages aria-expanded and aria-hidden for both filter and language panels
  • toggleMobileLangDropdown(): Same ARIA management for language panel
  • Click-outside handler: Updated to sync ARIA states when closing dropdowns
  • Escape key handler: New! Closes open dropdown and returns focus to trigger button
  • Focus management: Auto-focuses first item when dropdown opens

CSS (inline stylesheet)

  • Added .filter-chip:focus-visible, .pill:focus-visible styles with orange outline
  • Added .mobile-filter-option:focus-visible style for mobile dropdown items
  • Added .clear-filters-btn:focus-visible and #loadMoreBtn:focus-visible styles

Testing

  • ✅ All 25 existing tests pass
  • ✅ Tested keyboard navigation: Tab through filter controls
  • ✅ Escape key closes dropdowns and returns focus to trigger button
  • ✅ ARIA attributes toggle correctly as dropdowns open/close

Closes #1969

Review in cubic

- Add aria-expanded and aria-controls to mobile filter/language dropdown buttons
- Add role=menu and aria-hidden to mobile filter/language panels
- Add role=menuitem to mobile filter/language options
- Implement Escape key to close mobile dropdowns with focus return
- Add focus-visible styles for filter chips, pills, and mobile options
- Add focus management: auto-focus first item when dropdown opens
- Remove remaining blank line

Closes S3DFX-CYBER#1969
@waterWang
waterWang requested a review from S3DFX-CYBER as a code owner August 3, 2026 02:19
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@waterWang is attempting to deploy a commit to the s3dfx-cyber's projects Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps greptile-apps 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.

Your trial has ended. Reactivate Greptile to resume code reviews.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Validation Issues

Hi @waterWang, your PR requires fixes before review.

Warnings

  • ⚠️ Missing contribution program declaration (GSSOC or NSOC).
  • ⚠️ Missing PR template section: related issue
  • ⚠️ Missing PR template section: type of change
  • ⚠️ Missing PR template section: checklist

Please push fixes after updating the PR.

@github-actions github-actions Bot added the size/m label Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

👋 Thanks for opening a PR, @waterWang!

Your PR has entered the 🚦 PR Review Pipeline.

Standard PR detected — your PR will follow the standard repository review pipeline.


🔄 Review Flow

Stage Reviewer Purpose
Stage 1 🤖 Automation Validation · Duplicate Detection · AI/Slop Checks · Formatting · PR Analysis
Stage 2 👥 Repository Reviewer Code Review · Scope Validation · Quality Check
Stage 3 🔑 Project Admin / Maintainer Final Approval & Merge Decision

The automated PR analysis system will verify issue linkage, PR relevance, and contribution quality.

A pipeline status comment may appear automatically as your PR progresses.


✅ Contributor Checklist

  • Sign commits using git commit -s
  • Link a valid issue (Closes #123)
  • Keep changes focused and relevant
  • Do not include unrelated modifications
  • Ensure workflows/build/tests are passing
  • Read the appropriate contributor guide:

⚠️ Important Notes

  • Low-quality, spammy, or AI-generated PRs may be closed
  • PRs without linked issues may fail automated checks
  • Large unrelated PRs are likely to be rejected
  • Review times may vary depending on mentor/reviewer availability

Happy contributing 🚀

This message is posted automatically and only once.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

💬 Faster Reviews & Assignments

Hi @waterWang, for faster coordination and smoother communication, consider joining our Discord community:

👉 https://discord.gg/jYrbHKtn8t

Useful Channels

  • #issue-links-for-assignment → Share issue links for assignment help
  • #pr-links-for-review → Share PR links for mentor/maintainer review

Please avoid spamming channels or repeatedly pinging mentors/maintainers.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ DCO Sign-off Missing

Hi @waterWang 👋

Some commits in this PR are missing a valid Signed-off-by line.

Invalid Commits

  • 8eabc43

Fix Single Commit

git commit --amend --signoff
git push --force-with-lease

Fix Multiple Commits

git rebase --signoff HEAD~N
git push --force-with-lease

This comment updates automatically after fixes are pushed.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Accessibility Improvements
    • Added visible focus indicators for filter controls and the load-more button.
    • Improved keyboard navigation for mobile filter and language menus.
    • Added clearer expanded and collapsed states for dropdowns.
    • Dropdowns now close appropriately when clicking outside or pressing Escape, and restore focus to their trigger.

Walkthrough

The change adds visible focus indicators and accessibility semantics to filter controls. Mobile filter and language dropdowns now synchronize ARIA state, manage focus, close on outside clicks or Escape, and restore focus to their triggers.

Changes

Filter accessibility

Layer / File(s) Summary
Focus indicators and dropdown semantics
index.html
Filter controls, dropdown triggers, menus, options, language pills, and load-more controls receive focus styling and accessibility attributes.
Dropdown state and focus flow
index.html
Mobile dropdowns synchronize ARIA state, close the opposite menu, focus the first option, handle outside clicks and Escape, and restore trigger focus.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FilterTrigger as Mobile filter trigger
  participant LanguageTrigger as Language dropdown trigger
  participant DropdownMenu as Mobile dropdown menu
  participant Document as Document events
  FilterTrigger->>DropdownMenu: Open filter menu
  DropdownMenu->>LanguageTrigger: Close language menu
  DropdownMenu->>DropdownMenu: Set ARIA state
  DropdownMenu->>DropdownMenu: Focus first option
  Document->>DropdownMenu: Handle outside click or Escape
  DropdownMenu->>FilterTrigger: Restore trigger focus
Loading

Possibly related issues

  • S3DFX-CYBER/GSoC-Org-Finder-#2012 — Targets keyboard focus and accessibility for the same filter controls.

Possibly related PRs

Suggested labels: bug

Suggested reviewers: s3dfx-cyber, cubic-dev-ai

Poem

I’m a rabbit who tabs through the garden with care,
Bright focus rings now sparkle there.
Menus announce when they open wide,
Escape sends them hopping aside.
Back to the trigger, safe and spry—
Accessibility reaches the sky! 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses dropdown keyboard behavior, focus indicators, and ARIA states, but it does not demonstrate fixes for the issue's search box, checkboxes, or filter-purpose labels. Add and verify accessible labels and keyboard operation for the search box, checkboxes, and all filter controls covered by issue #1969.
Out of Scope Changes check ⚠️ Warning The focus styling added for the load-more button is outside the linked issue's filter-control scope. Remove the load-more focus styling or link a separate issue that includes load-more accessibility improvements.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the accessibility changes to keyboard navigation and ARIA support for filter controls.
Description check ✅ Passed The description explains the changes, linked issue, and testing, but it omits the template's program classification, change type, and checklist sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🚦 PR Review Pipeline

Standard PR

Stage Status
Stage 1 — Automated Checks ❌ Failed — fixes required
Stage 2 — Mentor/Reviewer 🔒 Blocked until Stage 1 passes
Stage 3 — Maintainer 🔒 Blocked until Stage 2 passes
  • DCO verification pending
  • DCO sign-off missing

Last updated: Mon, 03 Aug 2026 02:19:29 GMT

@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🤖 TENET Agent Review

📋 Summary

This pull request significantly enhances the accessibility of the mobile filter and language dropdown controls by implementing proper ARIA attributes (aria-expanded, aria-controls, role="menu", aria-hidden), enabling keyboard navigation including Escape key support to close dropdowns and return focus, and adding visible focus indicators for all interactive elements. These changes greatly improve the user experience for screen reader users and those navigating with a keyboard.

🔐 Security Findings

No security issues found. The changes are purely frontend accessibility enhancements and do not interact with security-sensitive logic, data, or LLM components of the TENET AI system.

🧹 Code Quality

  • index.html (inline CSS/JS) - The continued use of inline <style> and <script> blocks within index.html for application-specific CSS and JavaScript can hinder maintainability, caching, and separation of concerns. Consider externalizing these into dedicated .css and .js files.
  • index.html (JS functions toggleMobileFilterDropdown, toggleMobileLangDropdown) - There is significant duplication in the logic for managing ARIA attributes, closing the other panel, and setting focus. This could be refactored into a single, more generic function to reduce redundancy and improve maintainability.
  • index.html (JS functions toggleMobileFilterDropdown, toggleMobileLangDropdown) - The use of setTimeout(() => firstItem.focus(), 50); for focusing the first item upon opening a dropdown is a common workaround but can be brittle. While it might be necessary due to rendering timing, it's generally preferable to ensure elements are focusable immediately after becoming visible without an arbitrary delay.

✅ What's Done Well

  • Comprehensive Accessibility: The PR addresses multiple critical accessibility aspects, including ARIA roles/states, keyboard navigation, and visible focus indicators, making the UI much more inclusive.
  • Correct ARIA Implementation: The use of aria-expanded, aria-controls, role="menu", and role="menuitem" is correctly applied, providing essential context for assistive technologies.
  • Improved Keyboard Interaction: The addition of Escape key functionality to close dropdowns and return focus to the trigger button is a significant improvement for keyboard users.

📝 Overall Verdict

APPROVE - Excellent work on improving accessibility; minor code quality points do not impede functionality or security.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
index.html (1)

2753-2772: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce the outside-click handler complexity.

SonarCloud reports cognitive complexity 16, above the configured limit of 15. Extract the repeated panel, trigger, ARIA, and chevron reset logic into one closeMobileDropdown helper. Reuse that helper from the toggle, outside-click, and Escape paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` around lines 2753 - 2772, Reduce the click-handler complexity by
introducing a shared closeMobileDropdown helper that resets the specified panel,
trigger aria-expanded state, and chevron transform. Replace the duplicated
closing logic in the outside-click handler and reuse the helper from the mobile
dropdown toggle and Escape-key paths, preserving existing behavior and element
identifiers.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@index.html`:
- Around line 2719-2721: Update the dropdown-opening focus logic around the
isOpen checks to avoid deferring focus with an uncancellable setTimeout: focus
the first matching item synchronously after opening, or track the timer and
cancel it from every close path, including the corresponding logic near the
other affected block. Ensure delayed callbacks cannot focus a hidden panel or
override focus restoration after Escape or another dropdown opens.
- Around line 1189-1193: Update the clear-filters focus rule to target the
control by its existing clearFiltersBtn identifier instead of the unused
clear-filters-btn class, preserving the current focus styling.
- Around line 1502-1546: Replace the menu semantics on mobileFilterPanel and
mobileLangPanel with a labeled role="group", and remove the menu/menuitem roles
from their options. Add synchronized aria-pressed states to every mobile filter
and language button, updating them in toggleMobileChip and toggleMobileLangPill
whenever selections change.

---

Nitpick comments:
In `@index.html`:
- Around line 2753-2772: Reduce the click-handler complexity by introducing a
shared closeMobileDropdown helper that resets the specified panel, trigger
aria-expanded state, and chevron transform. Replace the duplicated closing logic
in the outside-click handler and reuse the helper from the mobile dropdown
toggle and Escape-key paths, preserving existing behavior and element
identifiers.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a3bc69f-16ee-47fd-907b-187f9e58ae3a

📥 Commits

Reviewing files that changed from the base of the PR and between 2174967 and 8eabc43.

📒 Files selected for processing (1)
  • index.html
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: cubic · AI code reviewer
⚠️ CI failures not shown inline (1)

Commit Status: Vercel: Vercel

Conclusion: failure

Authorization required to deploy.
🧰 Additional context used
🪛 ast-grep (0.45.0)
index.html

[error] 2720-2720: React's useState should not be directly called
Context: setTimeout(() => firstItem.focus(), 50)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)


[error] 2747-2747: React's useState should not be directly called
Context: setTimeout(() => firstItem.focus(), 50)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🪛 GitHub Check: SonarCloud Code Analysis
index.html

[failure] 2753-2753: Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=S3DFX-CYBER_GSoC-Org-Finder-&issues=AZ_Fa1Nrc71o87kaaLai&open=AZ_Fa1Nrc71o87kaaLai&pullRequest=2053

Comment thread index.html
Comment on lines +1189 to +1193
.clear-filters-btn:focus-visible {
outline: 2px solid var(--orange, #f46b0e);
outline-offset: 2px;
border-radius: 12px;
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match the clear-filters focus selector to the control.

Line 1595 uses id="clearFiltersBtn". It does not use the .clear-filters-btn class. The added custom focus style does not apply to this button.

Proposed fix
-.clear-filters-btn:focus-visible {
+#clearFiltersBtn:focus-visible {
📝 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
.clear-filters-btn:focus-visible {
outline: 2px solid var(--orange, #f46b0e);
outline-offset: 2px;
border-radius: 12px;
}
`#clearFiltersBtn`:focus-visible {
outline: 2px solid var(--orange, `#f46b0e`);
outline-offset: 2px;
border-radius: 12px;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` around lines 1189 - 1193, Update the clear-filters focus rule to
target the control by its existing clearFiltersBtn identifier instead of the
unused clear-filters-btn class, preserving the current focus styling.

Comment thread index.html
Comment on lines +1502 to +1546
<div id="mobileFilterPanel" role="menu" aria-hidden="true" class="hidden absolute left-0 top-full mt-1 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-zinc-100 dark:border-zinc-700 z-30 p-2 flex flex-col gap-1" style="min-width:180px">
<button id="mob-chip-veteran" data-mobile-chip="chip-veteran" onclick="toggleMobileChip(this)" role="menuitem" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
<span class="material-symbols-outlined text-sm">military_tech</span> Veterans Only
</button>
<button id="mob-chip-newcomer" data-mobile-chip="chip-newcomer" onclick="toggleMobileChip(this)" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left">
<button id="mob-chip-newcomer" data-mobile-chip="chip-newcomer" onclick="toggleMobileChip(this)" role="menuitem" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
<span class="material-symbols-outlined text-sm">fiber_new</span> Newcomers
</button>
<button id="mob-chip-hot" data-mobile-chip="chip-hot" onclick="toggleMobileChip(this)" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left">
<button id="mob-chip-hot" data-mobile-chip="chip-hot" onclick="toggleMobileChip(this)" role="menuitem" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
<span class="material-symbols-outlined text-sm">local_fire_department</span> High Competition
</button>
<button id="mob-chip-chill" data-mobile-chip="chip-chill" onclick="toggleMobileChip(this)" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left">
<button id="mob-chip-chill" data-mobile-chip="chip-chill" onclick="toggleMobileChip(this)" role="menuitem" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
<span class="material-symbols-outlined text-sm">sentiment_satisfied</span> Low Competition
</button>
<button id="mob-chip-active" data-mobile-chip="chip-active" onclick="toggleMobileChip(this)" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left">
<button id="mob-chip-active" data-mobile-chip="chip-active" onclick="toggleMobileChip(this)" role="menuitem" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
<span class="material-symbols-outlined text-sm">bolt</span> Actively Maintained
</button>
<button id="mob-chip-bookmarked" data-mobile-chip="chip-bookmarked" onclick="toggleMobileChip(this)" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left">
<button id="mob-chip-bookmarked" data-mobile-chip="chip-bookmarked" onclick="toggleMobileChip(this)" role="menuitem" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
<span class="material-symbols-outlined text-sm icon-fill" style="color:#f59e0b">star</span> Bookmarked
</button>
</div>
</div>

<!-- Mobile: Languages dropdown (replaces language pills) -->
<div class="relative flex-1" id="mobileLangDropdownWrap">
<button id="mobileLangBtn" onclick="toggleMobileLangDropdown()" class="w-full flex items-center justify-between px-3 py-2.5 bg-surface-container-highest rounded-xl text-xs font-bold text-on-surface border border-transparent hover:border-primary transition-colors">
<button id="mobileLangBtn" onclick="toggleMobileLangDropdown()" aria-expanded="false" aria-controls="mobileLangPanel" class="w-full flex items-center justify-between px-3 py-2.5 bg-surface-container-highest rounded-xl text-xs font-bold text-on-surface border border-transparent hover:border-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
<span class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-sm text-primary">code</span>
<span id="mobileLangLabel">Languages</span>
</span>
<span class="material-symbols-outlined text-sm transition-transform" id="mobileLangChevron">expand_more</span>
</button>
<div id="mobileLangPanel" class="hidden absolute right-0 top-full mt-1 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-zinc-100 dark:border-zinc-700 z-30 p-2" style="min-width:240px">
<div id="mobileLangPanel" role="menu" aria-hidden="true" class="hidden absolute right-0 top-full mt-1 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-zinc-100 dark:border-zinc-700 z-30 p-2" style="min-width:240px">
<div class="flex flex-wrap gap-1.5 p-1">
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="Python" onclick="toggleMobileLangPill(this)">Python</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="JavaScript" onclick="toggleMobileLangPill(this)">JavaScript</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="TypeScript" onclick="toggleMobileLangPill(this)">TypeScript</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="C/C++" onclick="toggleMobileLangPill(this)">C/C++</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="Java" onclick="toggleMobileLangPill(this)">Java</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="Rust" onclick="toggleMobileLangPill(this)">Rust</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="Go" onclick="toggleMobileLangPill(this)">Go</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="Ruby" onclick="toggleMobileLangPill(this)">Ruby</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="Haskell" onclick="toggleMobileLangPill(this)">Haskell</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="Scala" onclick="toggleMobileLangPill(this)">Scala</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="ML/AI" onclick="toggleMobileLangPill(this)">ML/AI</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors" data-lang="Robotics" onclick="toggleMobileLangPill(this)">Robotics</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="Python" onclick="toggleMobileLangPill(this)">Python</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="JavaScript" onclick="toggleMobileLangPill(this)">JavaScript</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="TypeScript" onclick="toggleMobileLangPill(this)">TypeScript</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="C/C++" onclick="toggleMobileLangPill(this)">C/C++</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="Java" onclick="toggleMobileLangPill(this)">Java</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="Rust" onclick="toggleMobileLangPill(this)">Rust</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="Go" onclick="toggleMobileLangPill(this)">Go</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="Ruby" onclick="toggleMobileLangPill(this)">Ruby</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="Haskell" onclick="toggleMobileLangPill(this)">Haskell</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="Scala" onclick="toggleMobileLangPill(this)">Scala</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="ML/AI" onclick="toggleMobileLangPill(this)">ML/AI</button>
<button class="mobile-lang-pill px-3 py-1.5 rounded-full bg-surface-container-high border border-zinc-200 text-xs font-medium hover:border-primary hover:text-primary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary" data-lang="Robotics" onclick="toggleMobileLangPill(this)">Robotics</button>

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'toggleMobileChip|toggleMobileLangPill|aria-pressed|aria-checked|ArrowDown|ArrowUp|Home|End|mobileFilterPanel|mobileLangPanel' index.html

Repository: S3DFX-CYBER/GSoC-Org-Finder-

Length of output: 24803


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '2799,2852p' index.html
sed -n '2620,2692p' index.html

Repository: S3DFX-CYBER/GSoC-Org-Finder-

Length of output: 4942


Use toggle semantics for the mobile filter controls.

mobileFilterPanel and mobileLangPanel use role="menu" with role="menuitem" options, but these controls work as multi-select toggles. Use a labeled role="group" and add aria-pressed on each filter/language button updated by toggleMobileChip and toggleMobileLangPill; or replace the entire menu model with menuitemcheckbox with synchronized aria-checked.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` around lines 1502 - 1546, Replace the menu semantics on
mobileFilterPanel and mobileLangPanel with a labeled role="group", and remove
the menu/menuitem roles from their options. Add synchronized aria-pressed states
to every mobile filter and language button, updating them in toggleMobileChip
and toggleMobileLangPill whenever selections change.

Comment thread index.html
Comment on lines +2719 to +2721
if (!isOpen) {
const firstItem = panel.querySelector('button, [tabindex]:not([tabindex="-1"])');
if (firstItem) setTimeout(() => firstItem.focus(), 50);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not defer focus without cancellation.

The setTimeout callback can run after Escape or after the other dropdown opens. It can then move focus into a hidden panel and override the focus restoration behavior.

Focus the first option synchronously after opening, or store and cancel the pending timer from every close path.

Proposed fix
-      if (firstItem) setTimeout(() => firstItem.focus(), 50);
+      if (firstItem) firstItem.focus();

Also applies to: 2746-2748

🧰 Tools
🪛 ast-grep (0.45.0)

[error] 2720-2720: React's useState should not be directly called
Context: setTimeout(() => firstItem.focus(), 50)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` around lines 2719 - 2721, Update the dropdown-opening focus logic
around the isOpen checks to avoid deferring focus with an uncancellable
setTimeout: focus the first matching item synchronously after opening, or track
the timer and cancel it from every close path, including the corresponding logic
near the other affected block. Ensure delayed callbacks cannot focus a hidden
panel or override focus restoration after Escape or another dropdown opens.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

4 issues found across 1 file

Confidence score: 3/5

  • In index.html, the deferred focus call (setTimeout(() => firstItem.focus(), 50)) can still fire after the panel is closed or another dropdown opens, causing focus to jump to hidden/stale UI and confusing keyboard users; store and clear the timeout on close/Escape/switch paths to prevent late focus moves.
  • In index.html, applying role="menu"/role="menuitem" without full menu keyboard behavior (single tab stop + Arrow navigation) creates an accessibility mismatch that can mislead assistive tech and break expected interaction patterns; either implement the ARIA menu pattern fully or use semantics that match simple filter controls.
  • In index.html, the Clear Filters control lacks a visible focus style because the selector does not match id="clearFiltersBtn", which leaves keyboard focus hard to track; update the selector to target the existing ID (or add the expected class).
  • In index.html, the two toggle handlers now duplicate roughly the same open/close, ARIA, chevron, sibling-close, and focus logic, increasing drift risk when future fixes are made; extract shared behavior into one helper so bug fixes land consistently in both paths.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="index.html">

<violation number="1" location="index.html:1189">
P3: Clear Filters still has no visible focus indicator because this selector does not match its `id="clearFiltersBtn"`. Target the existing ID (or add the class) so keyboard focus receives the intended style.</violation>

<violation number="2" location="index.html:1502">
P2: The new `role="menu"` / `role="menuitem"` markup doesn't match a real ARIA menu: per the WAI-ARIA menu pattern, a menu requires menuitem children, only one tab stop, and Arrow-key navigation — but these filter items are still plain buttons reached via Tab, with no arrow-key handling or `aria-haspopup` on the trigger. The result is that screen readers announce "menu/menuitem" semantics that don't match how the control actually behaves, which undercuts the accessibility goal of this PR. Additionally, the language panel has `role="menu"` while its child language pills were left as plain `<button>`s (no menuitem role), so that menu has no valid menuitem children at all. Recommend removing the menu/menuitem roles and relying on the native `<button>` names combined with the `aria-expanded`/`aria-controls` you already added (which is the simpler, correct pattern for a group of toggle options); or, if a true menu is wanted, implement the full pattern (aria-haspopup, roving tabindex, Arrow-key navigation, and menuitem roles on every item including the language pills).</violation>

<violation number="3" location="index.html:2721">
P3: The two toggle functions are now near-identical (~25 duplicated lines each): same isOpen computation, hidden-class toggle, aria-expanded/aria-hidden updates, chevron rotation, sibling-panel close, and focus-first-item block. This duplication makes the ARIA state logic drift-prone — the only differences are the element IDs. Consider extracting a single helper, e.g. `toggleDropdown(panelId, btnId, chevronId)` plus `closeDropdown(...)`, and have both triggers (and the click-outside/Escape handlers) call it, so the aria-expanded/aria-hidden/chevron/focus sync lives in one place.</violation>

<violation number="4" location="index.html:2721">
P2: The deferred `setTimeout(() => firstItem.focus(), 50)` call used to auto-focus the first item is never cancelled. If Escape is pressed or the panel is closed (or the other dropdown is opened) within that 50ms window, this pending timeout can still fire and move focus into a now-hidden panel, breaking the focus-restoration behavior added by the Escape handler. Focus synchronously, or store and clear the timer id in every close path.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread index.html
</button>
<div id="mobileFilterPanel" class="hidden absolute left-0 top-full mt-1 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-zinc-100 dark:border-zinc-700 z-30 p-2 flex flex-col gap-1" style="min-width:180px">
<button id="mob-chip-veteran" data-mobile-chip="chip-veteran" onclick="toggleMobileChip(this)" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left">
<div id="mobileFilterPanel" role="menu" aria-hidden="true" class="hidden absolute left-0 top-full mt-1 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-zinc-100 dark:border-zinc-700 z-30 p-2 flex flex-col gap-1" style="min-width:180px">

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.

P2: The new role="menu" / role="menuitem" markup doesn't match a real ARIA menu: per the WAI-ARIA menu pattern, a menu requires menuitem children, only one tab stop, and Arrow-key navigation — but these filter items are still plain buttons reached via Tab, with no arrow-key handling or aria-haspopup on the trigger. The result is that screen readers announce "menu/menuitem" semantics that don't match how the control actually behaves, which undercuts the accessibility goal of this PR. Additionally, the language panel has role="menu" while its child language pills were left as plain <button>s (no menuitem role), so that menu has no valid menuitem children at all. Recommend removing the menu/menuitem roles and relying on the native <button> names combined with the aria-expanded/aria-controls you already added (which is the simpler, correct pattern for a group of toggle options); or, if a true menu is wanted, implement the full pattern (aria-haspopup, roving tabindex, Arrow-key navigation, and menuitem roles on every item including the language pills).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At index.html, line 1502:

<comment>The new `role="menu"` / `role="menuitem"` markup doesn't match a real ARIA menu: per the WAI-ARIA menu pattern, a menu requires menuitem children, only one tab stop, and Arrow-key navigation — but these filter items are still plain buttons reached via Tab, with no arrow-key handling or `aria-haspopup` on the trigger. The result is that screen readers announce "menu/menuitem" semantics that don't match how the control actually behaves, which undercuts the accessibility goal of this PR. Additionally, the language panel has `role="menu"` while its child language pills were left as plain `<button>`s (no menuitem role), so that menu has no valid menuitem children at all. Recommend removing the menu/menuitem roles and relying on the native `<button>` names combined with the `aria-expanded`/`aria-controls` you already added (which is the simpler, correct pattern for a group of toggle options); or, if a true menu is wanted, implement the full pattern (aria-haspopup, roving tabindex, Arrow-key navigation, and menuitem roles on every item including the language pills).</comment>

<file context>
@@ -1469,58 +1492,58 @@ <h2 class="text-4xl md:text-5xl font-extrabold font-headline tracking-tighter mt
       </button>
-      <div id="mobileFilterPanel" class="hidden absolute left-0 top-full mt-1 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-zinc-100 dark:border-zinc-700 z-30 p-2 flex flex-col gap-1" style="min-width:180px">
-        <button id="mob-chip-veteran" data-mobile-chip="chip-veteran" onclick="toggleMobileChip(this)" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left">
+      <div id="mobileFilterPanel" role="menu" aria-hidden="true" class="hidden absolute left-0 top-full mt-1 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-zinc-100 dark:border-zinc-700 z-30 p-2 flex flex-col gap-1" style="min-width:180px">
+        <button id="mob-chip-veteran" data-mobile-chip="chip-veteran" onclick="toggleMobileChip(this)" role="menuitem" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
           <span class="material-symbols-outlined text-sm">military_tech</span> Veterans Only
</file context>

Comment thread index.html
// Focus first item when opening
if (!isOpen) {
const firstItem = panel.querySelector('button, [tabindex]:not([tabindex="-1"])');
if (firstItem) setTimeout(() => firstItem.focus(), 50);

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.

P2: The deferred setTimeout(() => firstItem.focus(), 50) call used to auto-focus the first item is never cancelled. If Escape is pressed or the panel is closed (or the other dropdown is opened) within that 50ms window, this pending timeout can still fire and move focus into a now-hidden panel, breaking the focus-restoration behavior added by the Escape handler. Focus synchronously, or store and clear the timer id in every close path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At index.html, line 2721:

<comment>The deferred `setTimeout(() => firstItem.focus(), 50)` call used to auto-focus the first item is never cancelled. If Escape is pressed or the panel is closed (or the other dropdown is opened) within that 50ms window, this pending timeout can still fire and move focus into a now-hidden panel, breaking the focus-restoration behavior added by the Escape handler. Focus synchronously, or store and clear the timer id in every close path.</comment>

<file context>
@@ -2674,43 +2697,102 @@ <h3 style="font-size:10px;font-weight:700;color:var(--muted);letter-spacing:.1em
+    // Focus first item when opening
+    if (!isOpen) {
+      const firstItem = panel.querySelector('button, [tabindex]:not([tabindex="-1"])');
+      if (firstItem) setTimeout(() => firstItem.focus(), 50);
+    }
   };
</file context>

Comment thread index.html
outline-offset: 1px;
border-radius: 8px;
}
.clear-filters-btn:focus-visible {

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.

P3: Clear Filters still has no visible focus indicator because this selector does not match its id="clearFiltersBtn". Target the existing ID (or add the class) so keyboard focus receives the intended style.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At index.html, line 1189:

<comment>Clear Filters still has no visible focus indicator because this selector does not match its `id="clearFiltersBtn"`. Target the existing ID (or add the class) so keyboard focus receives the intended style.</comment>

<file context>
@@ -1174,6 +1174,29 @@
+  outline-offset: 1px;
+  border-radius: 8px;
+}
+.clear-filters-btn:focus-visible {
+  outline: 2px solid var(--orange, #f46b0e);
+  outline-offset: 2px;
</file context>
Suggested change
.clear-filters-btn:focus-visible {
#clearFiltersBtn:focus-visible {

Comment thread index.html
// Focus first item when opening
if (!isOpen) {
const firstItem = panel.querySelector('button, [tabindex]:not([tabindex="-1"])');
if (firstItem) setTimeout(() => firstItem.focus(), 50);

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.

P3: The two toggle functions are now near-identical (~25 duplicated lines each): same isOpen computation, hidden-class toggle, aria-expanded/aria-hidden updates, chevron rotation, sibling-panel close, and focus-first-item block. This duplication makes the ARIA state logic drift-prone — the only differences are the element IDs. Consider extracting a single helper, e.g. toggleDropdown(panelId, btnId, chevronId) plus closeDropdown(...), and have both triggers (and the click-outside/Escape handlers) call it, so the aria-expanded/aria-hidden/chevron/focus sync lives in one place.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At index.html, line 2721:

<comment>The two toggle functions are now near-identical (~25 duplicated lines each): same isOpen computation, hidden-class toggle, aria-expanded/aria-hidden updates, chevron rotation, sibling-panel close, and focus-first-item block. This duplication makes the ARIA state logic drift-prone — the only differences are the element IDs. Consider extracting a single helper, e.g. `toggleDropdown(panelId, btnId, chevronId)` plus `closeDropdown(...)`, and have both triggers (and the click-outside/Escape handlers) call it, so the aria-expanded/aria-hidden/chevron/focus sync lives in one place.</comment>

<file context>
@@ -2674,43 +2697,102 @@ <h3 style="font-size:10px;font-weight:700;color:var(--muted);letter-spacing:.1em
+    // Focus first item when opening
+    if (!isOpen) {
+      const firstItem = panel.querySelector('button, [tabindex]:not([tabindex="-1"])');
+      if (firstItem) setTimeout(() => firstItem.focus(), 50);
+    }
   };
</file context>

@S3DFX-CYBER S3DFX-CYBER left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This adds keyboard navigation and ARIA attributes to the mobile filter and language dropdowns — Escape-to-close, focus management, aria-expanded/aria-controls on the trigger buttons, and focus-visible outlines on the chips and pills. The direction is solid and the intent is right, but there are a few things that need fixing before this can merge.

First, there's no linked issue in the PR body. The repo requires a Closes #N (or fix/resolve variant) — add one so the PR satisfies the hard rules and CI doesn't flag it.

The bigger problem is the ARIA semantics. The panels get role="menu" and the filter options get role="menuitem", but the actual interaction doesn't implement the WAI-ARIA menu pattern — there's no roving tabindex, no arrow-key navigation, and the trigger has no aria-haspopup="menu". Worse, the language panel gets role="menu" but its child language pills are left as plain <button> with no role="menuitem", so that menu has no valid menuitem children at all. Screen readers will announce "menu" semantics that don't match how the control actually behaves, which undercuts the accessibility goal. The simpler and correct approach for a group of toggle filters is to drop the menu/menuitem roles entirely and rely on the native <button> names plus the aria-expanded/aria-controls you already added (which is correct). If you want a true menu, you'd need to implement the full pattern — but that's overkill here.

The focus management has a race condition. Both toggle functions call setTimeout(() => firstItem.focus(), 50) to focus the first item after opening, but that timeout is never tracked or cancelled. If the user presses Escape or clicks outside within that 50ms window — or opens the other dropdown — the pending timeout can still fire and move focus into a now-hidden panel, breaking the focus restoration the Escape handler tries to do. Focus the first item synchronously after toggling hidden, or store the timer ID and clear it in every close path (Escape, click-outside, sibling-open).

Finally, the .clear-filters-btn:focus-visible rule at the top of the diff targets a class that doesn't exist on the element — the Clear Filters button uses id="clearFiltersBtn", not a .clear-filters-btn class. So that focus indicator will never apply. Target the existing ID instead.

One thing to verify: the toggleMobileFilterDropdown and toggleMobileLangDropdown functions live in the index.html inline script. If the same functions exist in src/js/app.js (the test mirror), they need to be updated there too — the repo's architecture requires the two stay in sync. If they don't exist in app.js, then this is fine as-is.

Comment thread index.html
</button>
<div id="mobileFilterPanel" class="hidden absolute left-0 top-full mt-1 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-zinc-100 dark:border-zinc-700 z-30 p-2 flex flex-col gap-1" style="min-width:180px">
<button id="mob-chip-veteran" data-mobile-chip="chip-veteran" onclick="toggleMobileChip(this)" class="mobile-filter-option flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium hover:bg-surface-container-low transition-colors text-left">
<div id="mobileFilterPanel" role="menu" aria-hidden="true" class="hidden absolute left-0 top-full mt-1 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-zinc-100 dark:border-zinc-700 z-30 p-2 flex flex-col gap-1" style="min-width:180px">

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

role="menu" / role="menuitem" without the full ARIA menu pattern (roving tabindex, arrow-key nav, aria-haspopup on trigger) creates a semantics/behavior mismatch. The language panel below has role="menu" but its child pills have no role="menuitem". Recommend dropping these roles and relying on the native button semantics + aria-expanded/aria-controls you already added.

Comment thread index.html
outline-offset: 1px;
border-radius: 8px;
}
.clear-filters-btn:focus-visible {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This selector targets .clear-filters-btn but the element uses id="clearFiltersBtn" — this focus style will never apply. Use #clearFiltersBtn:focus-visible instead.

Comment thread index.html
// Focus first item when opening
if (!isOpen) {
const firstItem = panel.querySelector('button, [tabindex]:not([tabindex="-1"])');
if (firstItem) setTimeout(() => firstItem.focus(), 50);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This setTimeout is never cancelled. If Escape or click-outside fires within 50ms, the callback still runs and focuses an element inside a hidden panel. Store the timer ID and clear it in every close path, or focus synchronously after toggling hidden.

@github-actions

Copy link
Copy Markdown
Contributor

⏰ Stale PR Warning

This PR has been inactive for 7+ days with unresolved issues.

If there is no activity, it will be automatically closed in 7 days.

Please push updates or comment if you need more time.

@github-actions github-actions Bot added the stale label Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ACCESSIBILITY] Filter controls lack keyboard navigation and ARIA labels

2 participants