fix(Select): updated aria-live property for results announcement#1114
fix(Select): updated aria-live property for results announcement#1114ssherigar-c-eightfold wants to merge 4 commits intoEightfoldAI:mainfrom
Conversation
|
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. |
ychhabra-eightfold
left a comment
There was a problem hiding this comment.
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); | ||
|
|
There was a problem hiding this comment.
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 true → false, 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.
There was a problem hiding this comment.
Fixed, Memoized liveRegionMessage using useMemo and implemented Ref-based retention using lastLiveRegionMessageRef
src/components/Select/Select.tsx
Outdated
| (opt: SelectOption) => !opt.hideOption | ||
| ).length; | ||
| const liveRegionMessage = (() => { | ||
| if (!dropdownVisible) return ''; |
There was a problem hiding this comment.
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:
noResultsFoundTextshould not end with "for" when used standalone
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
updated with aria-label on the element with the listbox role
| <Menu | ||
| aria-label={ariaLabel || undefined} | ||
| id={selectMenuId?.current} | ||
| {...restMenuProps} |
There was a problem hiding this comment.
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:
- Assert the live region text changes to
"N results available."when the dropdown opens - Assert
"No results found for X"when filtering yields zero results - Assert the message clears when the dropdown closes
- Assert singular vs. plural forms (
1 result availablevs3 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} |
There was a problem hiding this comment.
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:
- The TextInput prop here
- 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.
There was a problem hiding this comment.
removed aria-haspopup="listbox" from this file
| resultAvailableText: 'result available.', | ||
| resultsAvailableText: 'results available.', | ||
| noResultsFoundText: 'No results found for', | ||
| }, |
There was a problem hiding this comment.
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).
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:
TEST COVERAGE:
TEST PLAN:
Dropdown Input Select