MM-63050 Improve screen reader support for SuggestionList (#31228)

* Remove aria-live from SuggestionBox

aria-live isn't the idiomatic way to make the autocomplete accessible to
screen readers. Instead, we should've used aria-activedescendant which
was done for the at-mention autocomplete in a previous ticket, but that
didn't apply to other types of autocompletes. That lead to the
at-mention autocomplete being too noisy (as there were two different
things telling the user what the results were), and it meant that other
types of autocompletes didn't function.

This needs a couple direct followups:
1. The E2E tests need to be updated since they test for aria-live.
2. The Suggestion items for other types of autocompletes need IDs for
   the aria-activedescendant to work.

* Consistently set IDs for all SuggestionList items

Instead of leaving it up to the individual Suggestion components,
this'll ensure that a11y support works for all of them going forward as
long as they properly forward other props to the underlying li element.

I would've preferred if each instance of SuggestionList had unique IDs,
but they currently all use the ID suggestionList, and I didn't want to
update that ID across 55 different Cypress tests. There's should only be
a single SuggestionList visible at a time, so the current situation is
fine enough.

* Remove textboxId from AtMentionProvider

* Change suggestion list to scroll using IDs instead of findDOMNode and refs

* Add an aria-label to SuggestionList

* Make all Suggestion components use the option role

* Add number of results to SuggestionList readout

* Change SuggestionBox to only set aria-expanded with results

* Add tests for ARIA of SuggestionBox

* Address feedback
Этот коммит содержится в:
Harrison Healey
2025-06-10 12:56:25 -04:00
коммит произвёл GitHub
родитель 09a2037b61
Коммит d01d6501f7
18 изменённых файлов: 173 добавлений и 299 удалений

Просмотреть файл

@@ -56,7 +56,7 @@ describe('Leave an archived channel', () => {
// * The archived channel appears in channel switcher search results
cy.get('#suggestionList').should('be.visible');
cy.get('#suggestionList').find(`#quickSwitchInput_${testChannel.id}`).should('be.visible');
cy.get('#suggestionList').find(`#suggestionList_item_${testChannel.id}`).should('be.visible');
// # Reload the app (refresh the web page)
cy.reload().then(() => {
@@ -67,7 +67,7 @@ describe('Leave an archived channel', () => {
cy.get('#quickSwitchInput').type(testChannel.display_name).then(() => {
// * The archived channel appears in channel switcher search results
cy.get('#suggestionList').should('be.visible');
cy.get('#suggestionList').find(`#quickSwitchInput_${testChannel.id}`).should('be.visible');
cy.get('#suggestionList').find(`#suggestionList_item_${testChannel.id}`).should('be.visible');
});
});
});

Просмотреть файл

@@ -86,10 +86,10 @@ describe('Verify Accessibility Support in different input fields', () => {
cy.get('#searchHints').should('be.visible');
// # Ensure User list is cached once in UI
cy.uiGetSearchBox().type('from:').wait(TIMEOUTS.FIVE_SEC);
cy.uiGetSearchBox().type('from:').wait(TIMEOUTS.ONE_SEC);
// # Trigger the user autocomplete again
cy.uiGetSearchBox().first().clear().type('from:').wait(TIMEOUTS.FIVE_SEC).type('{downarrow}{downarrow}');
cy.uiGetSearchBox().first().clear().type('from:').wait(TIMEOUTS.ONE_SEC).type('{downarrow}{downarrow}');
// * Verify Accessibility Support in search autocomplete
verifySearchAutocomplete(2);
@@ -103,10 +103,10 @@ describe('Verify Accessibility Support in different input fields', () => {
verifySearchAutocomplete(3);
// # Type the in: filter and ensure channel list is cached once
cy.uiGetSearchBox().first().clear().type('in:').wait(TIMEOUTS.FIVE_SEC);
cy.uiGetSearchBox().first().clear().type('in:').wait(TIMEOUTS.ONE_SEC);
// # Trigger the channel autocomplete again
cy.uiGetSearchBox().first().clear().type('in:').wait(TIMEOUTS.FIVE_SEC).type('{downarrow}{downarrow}');
cy.uiGetSearchBox().first().clear().type('in:').wait(TIMEOUTS.ONE_SEC).type('{downarrow}{downarrow}');
// * Verify Accessibility Support in search autocomplete
verifySearchAutocomplete(2, 'channel');
@@ -125,7 +125,7 @@ describe('Verify Accessibility Support in different input fields', () => {
cy.uiGetPostTextBox().should('have.attr', 'placeholder', `Write to ${testChannel.display_name}`).clear().focus();
// # Ensure User list is cached once in UI
cy.uiGetPostTextBox().type('@').wait(TIMEOUTS.FIVE_SEC);
cy.uiGetPostTextBox().type('@').wait(TIMEOUTS.ONE_SEC);
// # Select the first user in the list
cy.get('#suggestionList').find('.suggestion-list__item').eq(0).within((el) => {
@@ -135,7 +135,7 @@ describe('Verify Accessibility Support in different input fields', () => {
});
// # Trigger the user autocomplete again
cy.uiGetPostTextBox().clear().type('@').wait(TIMEOUTS.FIVE_SEC).type('{uparrow}{uparrow}{downarrow}');
cy.uiGetPostTextBox().clear().type('@').wait(TIMEOUTS.ONE_SEC).type('{uparrow}{uparrow}{downarrow}');
// * Verify Accessibility Support in message autocomplete
verifyMessageAutocomplete(1);
@@ -147,19 +147,19 @@ describe('Verify Accessibility Support in different input fields', () => {
verifyMessageAutocomplete(0);
// # Trigger the channel autocomplete filter and ensure channel list is cached once
cy.uiGetPostTextBox().clear().type('~').wait(TIMEOUTS.FIVE_SEC);
cy.uiGetPostTextBox().clear().type('~').wait(TIMEOUTS.ONE_SEC);
// # Trigger the channel autocomplete again
cy.uiGetPostTextBox().clear().type('~').wait(TIMEOUTS.FIVE_SEC).type('{downarrow}{downarrow}');
// * Verify Accessibility Support in message autocomplete
verifyMessageAutocomplete(2, 'channel');
verifyMessageAutocomplete(2);
// # Press Up arrow and verify if focus changes
cy.focused().type('{downarrow}{uparrow}{uparrow}');
// * Verify Accessibility Support in message autocomplete
verifyMessageAutocomplete(1, 'channel');
verifyMessageAutocomplete(1);
});
});
});
@@ -307,20 +307,11 @@ function verifySearchAutocomplete(index, type = 'user') {
});
}
function verifyMessageAutocomplete(index, type = 'user') {
cy.get('#suggestionList').find('.suggestion-list__item').eq(index).should('be.visible').and('have.class', 'suggestion--selected').within((el) => {
if (type === 'user') {
cy.get('.suggestion-list__ellipsis').invoke('text').then((fullText) => {
cy.get('.suggestion-list__main').invoke('text').then((username) => {
const usernameFullNameNickName = getUserMentionAriaLabel(`${username} ${fullText.split(username)[1]}`);
cy.wrap(el).parents('.textarea-wrapper').find('.sr-only').should('have.attr', 'aria-live', 'polite').and('have.text', usernameFullNameNickName);
});
});
} else if (type === 'channel') {
cy.wrap(el).invoke('text').then((text) => {
const channel = text.split('~')[0].toLowerCase().trim();
cy.wrap(el).parents('.textarea-wrapper').find('.sr-only').should('have.attr', 'aria-live', 'polite').and('have.text', channel);
});
}
function verifyMessageAutocomplete(index) {
cy.get('#suggestionList').find('.suggestion-list__item').eq(index).should('be.visible').and('have.class', 'suggestion--selected');
cy.get('#suggestionList').find('.suggestion-list__item').eq(index).invoke('attr', 'id').then((selectedId) => {
cy.wrap(selectedId).should('not.equal', '');
cy.uiGetPostTextBox().should('have.attr', 'aria-activedescendant', selectedId);
});
}