Skip to content

fix(Typeahead): omit aria-activedescendant when search results are empty (#4059)#4312

Open
HelloOjasMutreja wants to merge 1 commit into
facebook:mainfrom
HelloOjasMutreja:fix/typeahead-empty-results-active-descendant
Open

fix(Typeahead): omit aria-activedescendant when search results are empty (#4059)#4312
HelloOjasMutreja wants to merge 1 commit into
facebook:mainfrom
HelloOjasMutreja:fix/typeahead-empty-results-active-descendant

Conversation

@HelloOjasMutreja

Copy link
Copy Markdown
Contributor

Summary

Fixes #4059.

When keyboard navigation (ArrowDown, Home) was used in a Typeahead with no matching search results, highlightedIndex was set to 0 and aria-activedescendant emitted an ID pointing to <listbox-id>-option-0. Because 0 option nodes exist in an empty results listbox, this left assistive technology with a dangling reference to a non-existent DOM element.

Root cause

  1. In BaseTypeahead.tsx handleKeyDown, ArrowDown evaluated:
    setHighlightedIndex(prev => prev < results.length - 1 ? prev + 1 : 0)
    When results.length === 0, results.length - 1 is -1. -1 < -1 evaluated to false, causing highlightedIndex to evaluate to 0.
  2. Home unconditionally set setHighlightedIndex(0).
  3. aria-activedescendant checked popover.isOpen && highlightedIndex >= 0, which evaluated to true when highlightedIndex === 0 even though results.length === 0.

Fix

  • BaseTypeahead.tsx:
    • Guarded ArrowDown, ArrowUp, Home, and End in handleKeyDown to only update highlight when results.length > 0.
    • Added strict bounds checking to aria-activedescendant: popover.isOpen && highlightedIndex >= 0 && highlightedIndex < results.length. Omits aria-activedescendant when results are empty.
    • Added bounds checking to the scrollIntoView effect (highlightedIndex < results.length).
    • Updated performSearch & performBootstrap to initialize highlight with shown.length > 0 ? 0 : -1 (the actual sliced results array).

Testing

  • Added unit test in Typeahead.test.tsx verifying that aria-activedescendant is absent when search results are empty, and remains absent after pressing ArrowDown and Home.
  • 43/43 tests in Typeahead.test.tsx pass.
  • Full @astryxdesign/core package built cleanly (Babel + tsc + CSS + UMD).

…pty (facebook#4059)

When keyboard navigation (ArrowDown, Home) was used in a Typeahead with no
matching results, highlightedIndex was set to 0 and aria-activedescendant
emitted an ID pointing to option-0. Because 0 option nodes exist in an empty
results listbox, this left assistive technology with a dangling reference.

Guards keyboard navigation to only highlight items when results exist, and
validates highlightedIndex < results.length in aria-activedescendant and
the scrollIntoView effect.

Includes unit test coverage and a patch changeset.

Closes facebook#4059
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 25, 2026 5:55am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 25, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge labels Jul 25, 2026
@HelloOjasMutreja
HelloOjasMutreja marked this pull request as ready for review July 25, 2026 05:54
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

Modified Components

Typeahead
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 1107 -
Complexity N/A Very High (158) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.7KB 0B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | View full report

@HelloOjasMutreja

Copy link
Copy Markdown
Contributor Author

Technical Verification & Implementation Detail

  • Capped results (searchResults.slice(0, maxMenuItems)) are stored in local shown scope to prevent setHighlightedIndex(0) from pointing to an index that exists in raw search results but is omitted from the rendered DOM listbox.
  • aria-activedescendant explicitly checks highlightedIndex < results.length before emitting the item ID string, preventing screen readers from attempting to focus a non-existent option.
  • Added bounds checks to useEffect scroll container listener and keyboard navigation guards (ArrowDown, ArrowUp, Home, End) when results.length === 0.

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

Labels

CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Typeahead exposes a nonexistent active descendant for empty results

1 participant