Skip to content

fix(Select): updated aria-live property for results announcement#1114

Open
ssherigar-c-eightfold wants to merge 4 commits intoEightfoldAI:mainfrom
ssherigar-c-eightfold:allyant/select-a11y
Open

fix(Select): updated aria-live property for results announcement#1114
ssherigar-c-eightfold wants to merge 4 commits intoEightfoldAI:mainfrom
ssherigar-c-eightfold:allyant/select-a11y

Conversation

@ssherigar-c-eightfold
Copy link
Copy Markdown
Contributor

@ssherigar-c-eightfold ssherigar-c-eightfold commented Apr 2, 2026

SUMMARY:

Select
Added aria-live="polite" region for result announcements
Added aria-haspopup="listbox" to trigger
Passed aria-label to Menu

GITHUB ISSUE (Open Source Contributors)

NA

JIRA TASK (Eightfold Employees Only):

https://eightfoldai.atlassian.net/browse/ENG-167744
https://eightfoldai.atlassian.net/browse/ENG-167639

CHANGE TYPE:

  • Bugfix Pull Request
  • Feature Pull Request

TEST COVERAGE:

  • Tests for this change already exist
  • I have added unittests for this change

TEST PLAN:

Dropdown Input Select

  • Live region announces results correctly
  • Announces result count (“4 results available”)
  • when No result ("No results found for test")
  • Trigger has correct ARIA and retains focus
  • Roles and labels applied correctly

@codesandbox-ci
Copy link
Copy Markdown

codesandbox-ci bot commented Apr 2, 2026

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Copy link
Copy Markdown
Collaborator

@ychhabra-eightfold ychhabra-eightfold left a comment

Choose a reason for hiding this comment

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

Regression Risk Analysis

This PR adds an aria-live="polite" region to the Select component for announcing result counts to screen readers, adds aria-haspopup="listbox" to the trigger input, passes aria-label to the Menu, and introduces localized strings across 35+ locales.

Overall assessment: This is a well-structured a11y enhancement with low regression risk. The live region pattern is correctly implemented. A few items need attention — see inline comments.

!filterable && !multiple
);
const [dropdownVisible, setDropdownVisibility] = useState<boolean>(false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Regression Risk: Live region recomputed on every render — may cause excessive screen reader announcements

visibleOptionsCount and liveRegionMessage are derived inline (not memoized), meaning they recompute on every render. Since aria-live="polite" announces whenever its text content changes, any re-render that produces the same message string is fine (React won't touch the DOM). But if the message is set to '' and then back to the same count across renders (e.g., from an unrelated state change causing a re-render while the dropdown is closed then re-opened), it could trigger duplicate announcements.

Consider wrapping this in useMemo keyed on [dropdownVisible, visibleOptionsCount, searchQuery, selectLocale] to make the derivation intent explicit, and to prevent edge cases where React batching causes intermediate empty states.

Also: when dropdownVisible goes from truefalse, the message becomes ''. Some screen readers interpret clearing a live region as a new (empty) announcement. Consider keeping the last message until the next opening, or using a ref-based debounce.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed, Memoized liveRegionMessage using useMemo and implemented Ref-based retention using lastLiveRegionMessageRef

(opt: SelectOption) => !opt.hideOption
).length;
const liveRegionMessage = (() => {
if (!dropdownVisible) return '';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor: noResultsFoundText without search query reads awkwardly

When searchQuery is falsy but there are zero visible options, this returns just noResultsFoundText (e.g., "No results found for") — note the trailing preposition with no query. This can happen if options are empty from the start (not because of a filter).

Consider either:

  • Adding a separate locale string for the no-query case (e.g., "No results available")
  • Or trimming the trailing preposition: noResultsFoundText should not end with "for" when used standalone

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is fixed, When searchQuery is falsy and there are zero visible options, the message is ' '

return searchQuery ? `${noResultsText} ${searchQuery}` : noResultsText;
})();

const toggleOption = (option: SelectOption): void => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Regression Risk: aria-label on Menu goes to the <List> wrapper, not the <ul>

aria-label={ariaLabel || undefined} is passed to <Menu>, which spreads ...rest onto <List>. However, List renders a wrapping structure and the aria-label ends up on the outer List container, not on the <ul role="listbox"> element itself.

Screen readers use aria-label on the element with the listbox role to announce the list's purpose. If it lands on a wrapper <div> instead, it won't be associated with the listbox. Verify via the accessibility tree that the label is announced when the listbox receives focus/activedescendant.

Consider using the new ariaLabelledBy prop added to List in PR #1113, or adding a direct aria-label passthrough to the <ul>/<ol> element in List.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated with aria-label on the element with the listbox role

<Menu
aria-label={ariaLabel || undefined}
id={selectMenuId?.current}
{...restMenuProps}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Missing: No unit tests for the live region behavior

The test plan mentions verifying live region announcements, but only snapshot tests were updated (adding the <div aria-live="polite"> to the DOM). There are no behavioral tests that:

  1. Assert the live region text changes to "N results available." when the dropdown opens
  2. Assert "No results found for X" when filtering yields zero results
  3. Assert the message clears when the dropdown closes
  4. Assert singular vs. plural forms (1 result available vs 3 results available)

These are the exact behaviors most likely to regress and should have dedicated RTL assertions (e.g., expect(screen.getByRole('status')).toHaveTextContent('3 results available.')).

{liveRegionMessage}
</div>
{/* When Dropdown is hidden, place Pills outside the reference element */}
{!dropdownVisible && showPills() ? getPills() : null}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Note: Potential conflict with PR #1113

PR #1113 changes how aria-haspopup is handled in Dropdown.tsx — it removes the default 'true' and adds special logic to set 'listbox' for combobox elements. This PR adds aria-haspopup="listbox" directly on the <TextInput>.

If both PRs merge, the Select input will get aria-haspopup="listbox" from both:

  1. The TextInput prop here
  2. The Dropdown's ariaRef logic (since Select's ariaRef has role="combobox")

While setting the same value twice is harmless, it indicates the two PRs weren't coordinated. Whichever merges second should verify no duplication or conflict.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed aria-haspopup="listbox" from this file

resultAvailableText: 'result available.',
resultsAvailableText: 'results available.',
noResultsFoundText: 'No results found for',
},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor: Singular/plural distinction won't work for all languages

The current approach uses two strings (resultAvailableText / resultsAvailableText) and picks between them based on count !== 1. This works for English and many European languages, but fails for languages with more complex plural rules:

  • Arabic has 6 plural forms (0, 1, 2, few, many, other)
  • Polish/Czech distinguish 1, 2-4, and 5+
  • Chinese/Japanese/Korean have no plural distinction

The Arabic locale file has only two strings but Arabic grammar requires different forms for 2, 3-10, and 11+. This is a known i18n limitation — not necessarily blocking, but worth documenting as a future improvement (e.g., using ICU MessageFormat or a pluralization function).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants