From 132c27fb34ea39b274d7d64798da48eb0f6bd292 Mon Sep 17 00:00:00 2001 From: Saurabh Sharma Date: Fri, 17 Jan 2025 01:05:13 +0530 Subject: [PATCH] [MM-55285]: Screen reader speaking wrong item in list in Find Channels modal (#29552) * [MA-11]: Fix Screen reader speaking wrong item in list in Find Channels modal * [MA-11]: Update types across files * [MA-11]: Minor refactoring * [MA-11]: Fix e2e test * [MA-11]: Fix E2E tests * [MA-11]: Update role and id * [MA-11]: Fix playwright tests --------- Co-authored-by: Mattermost Build --- .../archive_channel_operations_spec.ts | 4 +-- .../users_in_channel_switcher_spec.ts | 2 +- .../channels/autocomplete/helpers.ts | 4 +-- .../channels/channel/channel_switcher_spec.ts | 12 ++++----- .../elasticsearch_autocomplete/helpers.ts | 2 +- .../users_in_channel_switcher_spec.ts | 2 +- .../guest_identification_ui_spec.ts | 2 +- .../enterprise/ldap/ldap_group_sync_spec.ts | 4 +-- .../interactive_dialog/scrollable_spec.js | 4 +-- .../ctrl_cmd_k_user_from_other_team_spec.js | 4 +-- .../keyboard_shortcuts_1_spec.js | 4 +-- .../ctrl_cmd_k_open_dm_with_mouse_spec.js | 2 +- .../channels/messaging/focus_move_spec.js | 2 +- .../message_draft_then_switch_channel_spec.js | 6 ++--- .../messaging/private_channel_open_spec.js | 2 +- .../search_autocomplete/renaming_spec.js | 2 +- .../channel_switcher_not_cloud_spec.js | 4 +-- .../settings/sidebar/channel_switcher_spec.js | 4 +-- .../channels/find_channels_modal.ts | 2 +- .../__snapshots__/search_bar.test.tsx.snap | 20 ++++++++++++++ .../at_mention_suggestion.test.tsx.snap | 8 +++--- .../at_mention_suggestion.tsx | 2 +- .../suggestion/channel_mention_provider.tsx | 2 +- .../command_provider/command_provider.tsx | 2 +- .../suggestion/emoticon_provider.tsx | 2 +- .../suggestion/generic_channel_provider.tsx | 2 +- .../suggestion/generic_user_provider.tsx | 2 +- .../suggestion/menu_action_provider.tsx | 2 +- .../search_channel_suggestion.tsx | 2 +- ...arch_channel_with_permissions_provider.tsx | 2 +- .../suggestion/search_suggestion_list.tsx | 2 +- .../suggestion/search_user_provider.tsx | 2 +- .../src/components/suggestion/suggestion.tsx | 8 +++--- .../suggestion_box/suggestion_box.jsx | 6 +++++ .../suggestion_box/suggestion_box.test.tsx | 26 +++++++++---------- .../components/suggestion/suggestion_list.tsx | 13 +++++----- .../suggestion/switch_channel_provider.tsx | 24 ++++++++++++++--- .../src/sass/components/_suggestion-list.scss | 1 + 38 files changed, 120 insertions(+), 76 deletions(-) diff --git a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_operations_spec.ts b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_operations_spec.ts index 8b794e590b1..9ac0d7b53eb 100644 --- a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_operations_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_operations_spec.ts @@ -57,7 +57,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(`#switchChannel_${testChannel.name}`).should('be.visible'); + cy.get('#suggestionList').find(`#quickSwitchInput_${testChannel.id}`).should('be.visible'); // # Reload the app (refresh the web page) cy.reload().then(() => { @@ -68,7 +68,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(`#switchChannel_${testChannel.name}`).should('be.visible'); + cy.get('#suggestionList').find(`#quickSwitchInput_${testChannel.id}`).should('be.visible'); }); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/autocomplete/database/users_in_channel_switcher_spec.ts b/e2e-tests/cypress/tests/integration/channels/autocomplete/database/users_in_channel_switcher_spec.ts index 13a81fb86a8..35a75b9e8ba 100644 --- a/e2e-tests/cypress/tests/integration/channels/autocomplete/database/users_in_channel_switcher_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/autocomplete/database/users_in_channel_switcher_spec.ts @@ -35,7 +35,7 @@ describe('Autocomplete with Database - Users', () => { // # Open quick channel switcher cy.typeCmdOrCtrl().type('k'); - cy.findByRole('textbox', {name: 'quick switch input'}).should('be.visible'); + cy.findByRole('combobox', {name: 'quick switch input'}).should('be.visible'); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/autocomplete/helpers.ts b/e2e-tests/cypress/tests/integration/channels/autocomplete/helpers.ts index e24aa24eb63..1119f108a5d 100644 --- a/e2e-tests/cypress/tests/integration/channels/autocomplete/helpers.ts +++ b/e2e-tests/cypress/tests/integration/channels/autocomplete/helpers.ts @@ -119,7 +119,7 @@ function getPostTextboxInput() { } function getQuickChannelSwitcherInput() { - cy.findByRole('textbox', {name: 'quick switch input'}). + cy.findByRole('combobox', {name: 'quick switch input'}). should('be.visible'). as('input'). clear(); @@ -158,7 +158,7 @@ function searchForChannel(name: string) { cy.typeCmdOrCtrl().type('k').wait(TIMEOUTS.ONE_SEC); // # Clear out and type in the name - cy.findByRole('textbox', {name: 'quick switch input'}). + cy.findByRole('combobox', {name: 'quick switch input'}). should('be.visible'). as('input'). clear(). diff --git a/e2e-tests/cypress/tests/integration/channels/channel/channel_switcher_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/channel_switcher_spec.ts index 9cfde8ea932..4a89359feef 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/channel_switcher_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/channel_switcher_spec.ts @@ -42,7 +42,7 @@ describe('Channel Switcher', () => { // # Start typing channel name in the "Switch Channels" modal message box // # Use up/down arrow keys to highlight second channel // # Press ENTER - cy.findByRole('textbox', {name: 'quick switch input'}). + cy.findByRole('combobox', {name: 'quick switch input'}). type(`${channelDisplayNamePrefix} `). type('{downarrow}{downarrow}{enter}'); @@ -60,7 +60,7 @@ describe('Channel Switcher', () => { cy.typeCmdOrCtrl().type('K', {release: true}); // # Start typing channel name in the "Switch Channels" modal message box - cy.findByRole('textbox', {name: 'quick switch input'}).type(`${channelDisplayNamePrefix} `); + cy.findByRole('combobox', {name: 'quick switch input'}).type(`${channelDisplayNamePrefix} `); cy.get(`[data-testid^=${channelNamePrefix}-c] > span`).click(); @@ -78,7 +78,7 @@ describe('Channel Switcher', () => { cy.typeCmdOrCtrl().type('K', {release: true}); // # Type invalid channel name in the "Switch Channels" modal message box - cy.findByRole('textbox', {name: 'quick switch input'}).type('there-is-no-spoon'); + cy.findByRole('combobox', {name: 'quick switch input'}).type('there-is-no-spoon'); // * Expect 'nothing found' message cy.get('.no-results__title > span').should('be.visible'); @@ -91,10 +91,10 @@ describe('Channel Switcher', () => { cy.typeCmdOrCtrl().type('K', {release: true}); // # Press ESC - cy.findByRole('textbox', {name: 'quick switch input'}).type('{esc}'); + cy.findByRole('combobox', {name: 'quick switch input'}).type('{esc}'); // * Expect the dialog to be closed - cy.findByRole('textbox', {name: 'quick switch input'}).should('not.exist'); + cy.findByRole('combobox', {name: 'quick switch input'}).should('not.exist'); // * Expect staying in the same channel cy.url().should('contain', 'off-topic'); @@ -106,7 +106,7 @@ describe('Channel Switcher', () => { cy.get('.modal').click({force: true}); // * Expect the dialog to be closed - cy.findByRole('textbox', {name: 'quick switch input'}).should('not.exist'); + cy.findByRole('combobox', {name: 'quick switch input'}).should('not.exist'); // * Expect staying in the same channel cy.url().should('contain', 'off-topic'); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/helpers.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/helpers.ts index 66a2487b54c..3f32bf78327 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/helpers.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/helpers.ts @@ -31,7 +31,7 @@ export function searchForChannel(name: string) { cy.typeCmdOrCtrl().type('k').wait(TIMEOUTS.ONE_SEC); // # Clear out and type in the name - cy.findByRole('textbox', {name: 'quick switch input'}). + cy.findByRole('combobox', {name: 'quick switch input'}). should('be.visible'). as('input'). clear(). diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/users_in_channel_switcher_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/users_in_channel_switcher_spec.ts index c1b604bed73..162c500e18b 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/users_in_channel_switcher_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/users_in_channel_switcher_spec.ts @@ -38,7 +38,7 @@ describe('Autocomplete with Elasticsearch - Users', () => { // # Open quick channel switcher cy.typeCmdOrCtrl().type('k'); - cy.findByRole('textbox', {name: 'quick switch input'}).should('be.visible'); + cy.findByRole('combobox', {name: 'quick switch input'}).should('be.visible'); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts index f42dd524979..ed27395f449 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts @@ -133,7 +133,7 @@ describe('Verify Guest User Identification in different screens', () => { cy.uiOpenFindChannels(); // # Type the guest user name on Channel switcher input - cy.findByRole('textbox', {name: 'quick switch input'}).type(guestUser.username).wait(TIMEOUTS.HALF_SEC); + cy.findByRole('combobox', {name: 'quick switch input'}).type(guestUser.username).wait(TIMEOUTS.HALF_SEC); // * Verify if Guest badge is displayed for the guest user in the Switch Channel Dialog cy.get('#suggestionList').should('be.visible'); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.ts index 6b2ceabdeed..725eb48427f 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.ts @@ -408,7 +408,7 @@ context('ldap', () => { cy.wait(TIMEOUTS.THREE_SEC); // # Type channel display name on Channel switcher input - cy.findByRole('textbox', {name: 'quick switch input'}).type(publicChannel.display_name); + cy.findByRole('combobox', {name: 'quick switch input'}).type(publicChannel.display_name); cy.wait(TIMEOUTS.HALF_SEC); // * Should open up suggestion list for channels @@ -433,7 +433,7 @@ context('ldap', () => { cy.wait(TIMEOUTS.THREE_SEC); // # Type channel display name on Channel switcher input - cy.findByRole('textbox', {name: 'quick switch input'}).type(publicChannel.display_name); + cy.findByRole('combobox', {name: 'quick switch input'}).type(publicChannel.display_name); cy.wait(TIMEOUTS.HALF_SEC); // * Should open up suggestion list for channels diff --git a/e2e-tests/cypress/tests/integration/channels/interactive_dialog/scrollable_spec.js b/e2e-tests/cypress/tests/integration/channels/interactive_dialog/scrollable_spec.js index 7fa398a51f3..3e14622a438 100644 --- a/e2e-tests/cypress/tests/integration/channels/interactive_dialog/scrollable_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/interactive_dialog/scrollable_spec.js @@ -86,7 +86,7 @@ describe('Interactive Dialog', () => { cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible'); cy.wrap($elForm).find('.form-control').type('{uparrow}', {force: true}); cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10), {force: true}); - cy.wrap($elForm).find('.suggestion-list__item').first().should('not.be.visible'); + cy.wrap($elForm).find('.suggestion-list__item').should('not.exist'); cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10), {force: true}); cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible'); } else if (index === 1) { @@ -94,7 +94,7 @@ describe('Interactive Dialog', () => { cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible'); cy.wrap($elForm).find('.form-control').type('{uparrow}', {force: true}); cy.wrap($elForm).find('.form-control').type('{downarrow}'.repeat(10), {force: true}); - cy.wrap($elForm).find('.suggestion-list__item').first().should('not.be.visible'); + cy.wrap($elForm).find('.suggestion-list__item').should('not.exist'); cy.wrap($elForm).find('.form-control').type('{uparrow}'.repeat(10), {force: true}); cy.wrap($elForm).find('.suggestion-list__item').first().should('be.visible'); } diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_k_user_from_other_team_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_k_user_from_other_team_spec.js index 9d54f0dc94a..691c4b1dec8 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_k_user_from_other_team_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_k_user_from_other_team_spec.js @@ -56,7 +56,7 @@ describe('Keyboard Shortcuts', () => { cy.typeCmdOrCtrl().type('K', {release: true}); // # Start typing the name of other user - cy.findByRole('textbox', {name: 'quick switch input'}).type(this.otherUser.username); + cy.findByRole('combobox', {name: 'quick switch input'}).type(this.otherUser.username); // # Select other user from the list cy.findByTestId(this.otherUser.username).should('not.exist'); @@ -104,7 +104,7 @@ function verifyUserIsFoundAndDMOpensOnClick(user) { cy.typeCmdOrCtrl().type('K', {release: true}); // # Start typing the name of other user - cy.findByRole('textbox', {name: 'quick switch input'}).type(user.username); + cy.findByRole('combobox', {name: 'quick switch input'}).type(user.username); // # Select other user from the list cy.findByTestId(user.username).should('be.visible'); diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/keyboard_shortcuts_1_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/keyboard_shortcuts_1_spec.js index d096ff30e5e..92ace315fd7 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/keyboard_shortcuts_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/keyboard_shortcuts_1_spec.js @@ -50,7 +50,7 @@ describe('Keyboard Shortcuts', () => { cy.apiAddUserToTeam(testTeam.id, tempUser.id); // # In the "Switch Channels" modal type the first chars of the test channel name - cy.findByRole('textbox', {name: 'quick switch input'}).should('be.focused').type(testChannel.name.substring(0, 3)).wait(TIMEOUTS.HALF_SEC); + cy.findByRole('combobox', {name: 'quick switch input'}).should('be.focused').type(testChannel.name.substring(0, 3)).wait(TIMEOUTS.HALF_SEC); // # Verify that the list of users and channels suggestions is present cy.get('#suggestionList').should('be.visible').within(() => { @@ -360,7 +360,7 @@ describe('Keyboard Shortcuts', () => { cy.uiGetPostTextBox().cmdOrCtrlShortcut('K').then(() => { // * Channel switcher hint should be visible and focused on cy.get('#quickSwitchHint').should('be.visible'); - cy.findByRole('textbox', {name: 'quick switch input'}).should('be.focused'); + cy.findByRole('combobox', {name: 'quick switch input'}).should('be.focused'); }); // # Type CTRL/CMD+K to close 'Switch Channels' modal diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/ctrl_cmd_k_open_dm_with_mouse_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/ctrl_cmd_k_open_dm_with_mouse_spec.js index 44deb6326ba..cdf666703ac 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/ctrl_cmd_k_open_dm_with_mouse_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/ctrl_cmd_k_open_dm_with_mouse_spec.js @@ -41,7 +41,7 @@ describe('Messaging', () => { cy.uiGetPostTextBox().cmdOrCtrlShortcut('K'); // # In the "Switch Channels" modal type the first 6 characters of the username - cy.findByRole('textbox', {name: 'quick switch input'}).should('be.focused').type(secondUser.username.substring(0, 6)).wait(TIMEOUTS.HALF_SEC); + cy.findByRole('combobox', {name: 'quick switch input'}).should('be.focused').type(secondUser.username.substring(0, 6)).wait(TIMEOUTS.HALF_SEC); // # Verify that the list of users and channels suggestions is present cy.get('#suggestionList').should('be.visible').within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/focus_move_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/focus_move_spec.js index 77851c6d155..bf70a82cc91 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/focus_move_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/focus_move_spec.js @@ -68,7 +68,7 @@ describe('Messaging', () => { cy.get('#quickSwitchHint').should('be.visible'); // # Type channel name and select it - cy.findByRole('textbox', {name: 'quick switch input'}).type(testChannelName).wait(TIMEOUTS.HALF_SEC).type('{enter}'); + cy.findByRole('combobox', {name: 'quick switch input'}).type(testChannelName).wait(TIMEOUTS.HALF_SEC).type('{enter}'); // * Verify that it redirected into selected channel cy.get('#channelHeaderTitle').should('be.visible').should('contain', testChannelName); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/message_draft_then_switch_channel_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/message_draft_then_switch_channel_spec.js index f44baa8aed2..d07103b034c 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/message_draft_then_switch_channel_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/message_draft_then_switch_channel_spec.js @@ -25,7 +25,7 @@ describe('Message Draft and Switch Channels', () => { }); it('MM-T131 Message Draft Pencil Icon - CTRL/CMD+K & "Jump to"', () => { - const {name, display_name: displayName} = testChannel; + const {name, display_name: displayName, id} = testChannel; const message = 'message draft test'; // * Validate if the draft icon is not visible at LHS before making a draft @@ -55,10 +55,10 @@ describe('Message Draft and Switch Channels', () => { // * Suggestion list is visible cy.get('#suggestionList').should('be.visible').within(() => { // * A pencil icon before the channel name in the filtered list is visible - cy.get(`#switchChannel_${name}`).find('.icon-pencil-outline').should('be.visible'); + cy.get(`#switchChannel_${id}`).find('.icon-pencil-outline').should('be.visible'); // # Click to switch back to the test channel - cy.get(`#switchChannel_${name}`).click({force: true}); + cy.get(`#switchChannel_${id}`).click({force: true}); }); // * Draft is saved in the text input box of the test channel diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/private_channel_open_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/private_channel_open_spec.js index 8cd2299094b..b62bcab25aa 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/private_channel_open_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/private_channel_open_spec.js @@ -32,7 +32,7 @@ describe('Messaging - Opening a private channel using keyboard shortcuts', () => // # Type the first letter of a private channel in the "Switch Channels" modal message box // # Use up/down arrow keys to highlight a private channel // # Press ENTER - cy.findByRole('textbox', {name: 'quick switch input'}).type('Pr').type('{downarrow}').type('{enter}'); + cy.findByRole('combobox', {name: 'quick switch input'}).type('Pr').type('{downarrow}').type('{enter}'); // * Private channel opens cy.get('#channelHeaderTitle').should('be.visible').should('contain', 'Private').wait(TIMEOUTS.HALF_SEC); diff --git a/e2e-tests/cypress/tests/integration/channels/search_autocomplete/renaming_spec.js b/e2e-tests/cypress/tests/integration/channels/search_autocomplete/renaming_spec.js index 0d41e95e2a0..1e2fb81ff40 100644 --- a/e2e-tests/cypress/tests/integration/channels/search_autocomplete/renaming_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/search_autocomplete/renaming_spec.js @@ -144,7 +144,7 @@ function searchAndVerifyChannel(channel) { cy.typeCmdOrCtrl().type('k'); // # Search for channel's display name - cy.findByRole('textbox', {name: 'quick switch input'}). + cy.findByRole('combobox', {name: 'quick switch input'}). should('be.visible'). as('input'). clear(). diff --git a/e2e-tests/cypress/tests/integration/channels/settings/sidebar/channel_switcher_not_cloud_spec.js b/e2e-tests/cypress/tests/integration/channels/settings/sidebar/channel_switcher_not_cloud_spec.js index 2e2b2c374c8..6c195027b8c 100644 --- a/e2e-tests/cypress/tests/integration/channels/settings/sidebar/channel_switcher_not_cloud_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/settings/sidebar/channel_switcher_not_cloud_spec.js @@ -36,7 +36,7 @@ describe('Settings > Sidebar > Channel Switcher', () => { cy.get('#quickSwitchHint').should('be.visible').should('contain', 'Type to find a channel. Use UP/DOWN to browse, ENTER to select, ESC to dismiss.'); // # Type CTRL/CMD+shift+L - cy.findByRole('textbox', {name: 'quick switch input'}).cmdOrCtrlShortcut('{shift}L'); + cy.findByRole('combobox', {name: 'quick switch input'}).cmdOrCtrlShortcut('{shift}L'); // * Suggestion list should not be visible cy.get('#suggestionList').should('not.exist'); @@ -56,7 +56,7 @@ describe('Settings > Sidebar > Channel Switcher', () => { cy.get('#quickSwitchHint').should('be.visible').should('contain', 'Type to find a channel. Use UP/DOWN to browse, ENTER to select, ESC to dismiss.'); // # Type CTRL/CMD+shift+m - cy.findByRole('textbox', {name: 'quick switch input'}).cmdOrCtrlShortcut('{shift}M'); + cy.findByRole('combobox', {name: 'quick switch input'}).cmdOrCtrlShortcut('{shift}M'); // * Suggestion list should not be visible cy.get('#suggestionList').should('not.exist'); diff --git a/e2e-tests/cypress/tests/integration/channels/settings/sidebar/channel_switcher_spec.js b/e2e-tests/cypress/tests/integration/channels/settings/sidebar/channel_switcher_spec.js index d0c990afd07..6f7d7e29f31 100644 --- a/e2e-tests/cypress/tests/integration/channels/settings/sidebar/channel_switcher_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/settings/sidebar/channel_switcher_spec.js @@ -50,14 +50,14 @@ function verifyChannelSwitch(team, channel) { cy.get('#quickSwitchHint').should('be.visible').should('contain', 'Type to find a channel. Use UP/DOWN to browse, ENTER to select, ESC to dismiss.'); // # Type channel display name on Channel switcher input - cy.findByRole('textbox', {name: 'quick switch input'}).type(channel.display_name); + cy.findByRole('combobox', {name: 'quick switch input'}).type(channel.display_name); cy.wait(TIMEOUTS.HALF_SEC); // * Suggestion list should be visible cy.get('#suggestionList').should('be.visible'); // # Press enter - cy.findByRole('textbox', {name: 'quick switch input'}).type('{enter}'); + cy.findByRole('combobox', {name: 'quick switch input'}).type('{enter}'); // * Verify that it redirected into "channel-switcher" as selected channel cy.url().should('include', `/${team.name}/channels/${channel.name}`); diff --git a/e2e-tests/playwright/support/ui/components/channels/find_channels_modal.ts b/e2e-tests/playwright/support/ui/components/channels/find_channels_modal.ts index cab7d6d810a..2ff87cd9004 100644 --- a/e2e-tests/playwright/support/ui/components/channels/find_channels_modal.ts +++ b/e2e-tests/playwright/support/ui/components/channels/find_channels_modal.ts @@ -11,7 +11,7 @@ export default class FindChannelsModal { constructor(container: Locator) { this.container = container; - this.input = container.getByRole('textbox', {name: 'quick switch input'}); + this.input = container.getByRole('combobox', {name: 'quick switch input'}); this.searchList = container.locator('.suggestion-list__item'); } diff --git a/webapp/channels/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap b/webapp/channels/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap index 56335de0572..6715a2b0194 100644 --- a/webapp/channels/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap +++ b/webapp/channels/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap @@ -32,13 +32,17 @@ exports[`components/search_bar/SearchBar should match snapshot with search 1`] = class="input-wrapper" > -
-
+ `; @@ -129,7 +129,7 @@ exports[`at mention suggestion Should not display nick name of the signed in use onMouseMove={[MockFunction]} term="@user" > -
-
+ `; diff --git a/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_suggestion.tsx b/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_suggestion.tsx index ffde62fe14b..a14b763bffb 100644 --- a/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_suggestion.tsx +++ b/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_suggestion.tsx @@ -34,7 +34,7 @@ interface Group extends Item { member_count: number; } -const AtMentionSuggestion = React.forwardRef>((props, ref) => { +const AtMentionSuggestion = React.forwardRef>((props, ref) => { const {item} = props; const intl = useIntl(); diff --git a/webapp/channels/src/components/suggestion/channel_mention_provider.tsx b/webapp/channels/src/components/suggestion/channel_mention_provider.tsx index 53358e631b4..fab2a000b60 100644 --- a/webapp/channels/src/components/suggestion/channel_mention_provider.tsx +++ b/webapp/channels/src/components/suggestion/channel_mention_provider.tsx @@ -27,7 +27,7 @@ type WrappedChannel = { loading?: boolean; } -export const ChannelMentionSuggestion = React.forwardRef>((props, ref) => { +export const ChannelMentionSuggestion = React.forwardRef>((props, ref) => { const {item} = props; const channelIsArchived = item.channel && item.channel.delete_at && item.channel.delete_at !== 0; diff --git a/webapp/channels/src/components/suggestion/command_provider/command_provider.tsx b/webapp/channels/src/components/suggestion/command_provider/command_provider.tsx index 017fff0ecb7..d7ea33f99a8 100644 --- a/webapp/channels/src/components/suggestion/command_provider/command_provider.tsx +++ b/webapp/channels/src/components/suggestion/command_provider/command_provider.tsx @@ -30,7 +30,7 @@ const EXECUTE_CURRENT_COMMAND_ITEM_ID = Constants.Integrations.EXECUTE_CURRENT_C const OPEN_COMMAND_IN_MODAL_ITEM_ID = Constants.Integrations.OPEN_COMMAND_IN_MODAL_ITEM_ID; const COMMAND_SUGGESTION_ERROR = Constants.Integrations.COMMAND_SUGGESTION_ERROR; -const CommandSuggestion = React.forwardRef>((props, ref) => { +const CommandSuggestion = React.forwardRef>((props, ref) => { const {item} = props; let symbolSpan = {'/'}; diff --git a/webapp/channels/src/components/suggestion/emoticon_provider.tsx b/webapp/channels/src/components/suggestion/emoticon_provider.tsx index cfe94b2c1db..bdb4a87ee1a 100644 --- a/webapp/channels/src/components/suggestion/emoticon_provider.tsx +++ b/webapp/channels/src/components/suggestion/emoticon_provider.tsx @@ -31,7 +31,7 @@ type EmojiItem = { const suggestionTypeEmoji = 'emoji'; -const EmoticonSuggestion = React.forwardRef>((props, ref) => { +const EmoticonSuggestion = React.forwardRef>((props, ref) => { const text = props.term; const emoji = props.item.emoji; diff --git a/webapp/channels/src/components/suggestion/generic_channel_provider.tsx b/webapp/channels/src/components/suggestion/generic_channel_provider.tsx index 7e0ce1b6725..831f4f7fc75 100644 --- a/webapp/channels/src/components/suggestion/generic_channel_provider.tsx +++ b/webapp/channels/src/components/suggestion/generic_channel_provider.tsx @@ -15,7 +15,7 @@ import type {SuggestionProps} from './suggestion'; type ChannelSearchFunc = (term: string, success: (channels: Channel[]) => void, error?: (err: ServerError) => void) => (ActionResult | Promise); -const GenericChannelSuggestion = React.forwardRef>((props, ref) => { +const GenericChannelSuggestion = React.forwardRef>((props, ref) => { const {item} = props; const channelName = item.display_name; diff --git a/webapp/channels/src/components/suggestion/generic_user_provider.tsx b/webapp/channels/src/components/suggestion/generic_user_provider.tsx index 4bbae1759ab..f311c3e54d2 100644 --- a/webapp/channels/src/components/suggestion/generic_user_provider.tsx +++ b/webapp/channels/src/components/suggestion/generic_user_provider.tsx @@ -18,7 +18,7 @@ import type {ResultsCallback} from './provider'; import {SuggestionContainer} from './suggestion'; import type {SuggestionProps} from './suggestion'; -const GenericUserSuggestion = React.forwardRef>((props, ref) => { +const GenericUserSuggestion = React.forwardRef>((props, ref) => { const {item} = props; const username = item.username; diff --git a/webapp/channels/src/components/suggestion/menu_action_provider.tsx b/webapp/channels/src/components/suggestion/menu_action_provider.tsx index 5d8cc33f47e..b0a5e7d3461 100644 --- a/webapp/channels/src/components/suggestion/menu_action_provider.tsx +++ b/webapp/channels/src/components/suggestion/menu_action_provider.tsx @@ -13,7 +13,7 @@ interface MenuAction { value: string; } -const MenuActionSuggestion = React.forwardRef>((props, ref) => { +const MenuActionSuggestion = React.forwardRef>((props, ref) => { const {item} = props; return ( diff --git a/webapp/channels/src/components/suggestion/search_channel_suggestion/search_channel_suggestion.tsx b/webapp/channels/src/components/suggestion/search_channel_suggestion/search_channel_suggestion.tsx index eb7c6c9dfee..bdcac04414a 100644 --- a/webapp/channels/src/components/suggestion/search_channel_suggestion/search_channel_suggestion.tsx +++ b/webapp/channels/src/components/suggestion/search_channel_suggestion/search_channel_suggestion.tsx @@ -76,7 +76,7 @@ type Props = SuggestionProps & { teammateIsBot: boolean; } -const SearchChannelSuggestion = React.forwardRef((props, ref) => { +const SearchChannelSuggestion = React.forwardRef((props, ref) => { const {item, teammateIsBot, currentUserId} = props; const nameObject = itemToName(item, currentUserId); diff --git a/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.tsx b/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.tsx index 9f79f57a598..e1c3768329b 100644 --- a/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.tsx +++ b/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.tsx @@ -36,7 +36,7 @@ interface WrappedChannel { type ChannelSearchFunction = (teamId: string, channelPrefix: string) => Promise -const SearchChannelWithPermissionsSuggestion = React.forwardRef>((props, ref) => { +const SearchChannelWithPermissionsSuggestion = React.forwardRef>((props, ref) => { const {item} = props; const channel = item.channel; const channelIsArchived = channel.delete_at && channel.delete_at !== 0; diff --git a/webapp/channels/src/components/suggestion/search_suggestion_list.tsx b/webapp/channels/src/components/suggestion/search_suggestion_list.tsx index 032fc9134c6..c5ffce689a6 100644 --- a/webapp/channels/src/components/suggestion/search_suggestion_list.tsx +++ b/webapp/channels/src/components/suggestion/search_suggestion_list.tsx @@ -58,7 +58,7 @@ export default class SearchSuggestionList extends SuggestionList { } getContent = () => { - return this.itemsContainerRef?.current?.parentNode as HTMLDivElement | null; + return this.itemsContainerRef?.current?.parentNode as HTMLUListElement | null; }; renderChannelDivider(type: string) { diff --git a/webapp/channels/src/components/suggestion/search_user_provider.tsx b/webapp/channels/src/components/suggestion/search_user_provider.tsx index a354f47eb49..ff734de4cc9 100644 --- a/webapp/channels/src/components/suggestion/search_user_provider.tsx +++ b/webapp/channels/src/components/suggestion/search_user_provider.tsx @@ -17,7 +17,7 @@ import type {ResultsCallback} from './provider'; import {SuggestionContainer} from './suggestion'; import type {SuggestionProps} from './suggestion'; -export const SearchUserSuggestion = React.forwardRef>((props, ref) => { +export const SearchUserSuggestion = React.forwardRef>((props, ref) => { const {item} = props; const username = item.username; diff --git a/webapp/channels/src/components/suggestion/suggestion.tsx b/webapp/channels/src/components/suggestion/suggestion.tsx index 5ed17d74c3e..31ccda92690 100644 --- a/webapp/channels/src/components/suggestion/suggestion.tsx +++ b/webapp/channels/src/components/suggestion/suggestion.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import React, {useCallback} from 'react'; -export interface SuggestionProps extends Omit, 'onClick' | 'onMouseMove'> { +export interface SuggestionProps extends Omit, 'onClick' | 'onMouseMove'> { // eslint-disable-next-line react/no-unused-prop-types item: Item; @@ -17,7 +17,7 @@ export interface SuggestionProps extends Omit void; } -const SuggestionContainer = React.forwardRef>((props, ref) => { +const SuggestionContainer = React.forwardRef>((props, ref) => { const { children, term, @@ -47,7 +47,7 @@ const SuggestionContainer = React.forwardRef {children} - + ); }); diff --git a/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.jsx b/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.jsx index ff8f58ea95e..b034b219516 100644 --- a/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.jsx +++ b/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.jsx @@ -820,6 +820,12 @@ export default class SuggestionBox extends React.PureComponent { ref={this.inputRef} autoComplete='off' {...props} + aria-controls='suggestionList' + role='combobox' + {...(this.state.selection && {'aria-activedescendant': `${props.id}_${this.state.selection}`} + )} + aria-autocomplete='list' + aria-expanded={this.state.focused || this.props.forceSuggestionsWhenBlur} onInput={this.handleChange} onCompositionStart={this.handleCompositionStart} onCompositionUpdate={this.handleCompositionUpdate} diff --git a/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.test.tsx b/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.test.tsx index 94f281d511f..d519787ebc3 100644 --- a/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.test.tsx +++ b/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.test.tsx @@ -92,7 +92,7 @@ describe('SuggestionBox', () => { ); // Start with no suggestions rendered - expect(screen.queryByRole('list')).not.toBeInTheDocument(); + expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); // Typing some text should cause a suggestion to be shown userEvent.click(screen.getByPlaceholderText('test input')); @@ -103,9 +103,9 @@ describe('SuggestionBox', () => { expect(providerSpy).toHaveBeenCalledTimes(1); }); - expect(screen.queryByRole('list')).toBeVisible(); + expect(screen.queryByRole('listbox')).toBeVisible(); - expect(screen.queryByRole('list')).toBeVisible(); + expect(screen.queryByRole('listbox')).toBeVisible(); expect(screen.getByText('Suggestion: testtest')).toBeVisible(); // Typing more text should cause the suggestion to be updaetd @@ -115,13 +115,13 @@ describe('SuggestionBox', () => { expect(providerSpy).toHaveBeenCalledTimes(2); }); - expect(screen.queryByRole('list')).toBeVisible(); + expect(screen.queryByRole('listbox')).toBeVisible(); expect(screen.getByText('Suggestion: testwordstestwords')).toBeVisible(); // Clearing the textbox hides all suggestions await userEvent.clear(screen.getByPlaceholderText('test input')); - expect(screen.queryByRole('list')).not.toBeInTheDocument(); + expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); }); test('should hide suggestions on pressing escape', async () => { @@ -135,20 +135,20 @@ describe('SuggestionBox', () => { ); // Start with no suggestions rendered - expect(screen.queryByRole('list')).not.toBeInTheDocument(); + expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); // Typing some text should cause a suggestion to be shown userEvent.click(screen.getByPlaceholderText('test input')); await userEvent.keyboard('test'); await waitFor(() => { - expect(screen.getByRole('list')).toBeVisible(); + expect(screen.getByRole('listbox')).toBeVisible(); }); // Pressing escape hides all suggestions await userEvent.keyboard('{escape}'); - expect(screen.queryByRole('list')).not.toBeInTheDocument(); + expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); }); test('should autocomplete suggestions by pressing enter', async () => { @@ -166,7 +166,7 @@ describe('SuggestionBox', () => { await userEvent.keyboard('test'); await waitFor(() => { - expect(screen.queryByRole('list')).toBeVisible(); + expect(screen.queryByRole('listbox')).toBeVisible(); expect(screen.getByText('Suggestion: testtest')).toBeVisible(); }); @@ -177,7 +177,7 @@ describe('SuggestionBox', () => { expect(screen.getByPlaceholderText('test input')).toHaveValue('testtest '); }); - expect(screen.queryByRole('list')).not.toBeInTheDocument(); + expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); }); test('MM-57320 completing text with enter and calling resultCallback twice should not erase text following caret', async () => { @@ -203,14 +203,14 @@ describe('SuggestionBox', () => { onSuggestionsReceived.mockClear(); expect(screen.getByPlaceholderText('test input')).toHaveValue('This is important'); - expect(screen.getByRole('list')).toBeVisible(); + expect(screen.getByRole('listbox')).toBeVisible(); expect(screen.getByText('Suggestion: This is importantThis is important')).toBeVisible(); // Move the caret back to the start of the textbox and then use escape to clear the suggestions because // we don't support moving the caret with the autocomplete open yet await userEvent.keyboard('{home}{escape}'); - expect(screen.queryByRole('list')).not.toBeInTheDocument(); + expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); // Type a space and then start typing something again to show results onSuggestionsReceived.mockClear(); @@ -221,7 +221,7 @@ describe('SuggestionBox', () => { expect(onSuggestionsReceived).toHaveBeenCalledTimes(2); }); - expect(screen.getByRole('list')).toBeVisible(); + expect(screen.getByRole('listbox')).toBeVisible(); expect(screen.getByText('Suggestion: @us@us')).toBeVisible(); onSuggestionsReceived.mockClear(); diff --git a/webapp/channels/src/components/suggestion/suggestion_list.tsx b/webapp/channels/src/components/suggestion/suggestion_list.tsx index b4b6cd4e7f3..c586495999c 100644 --- a/webapp/channels/src/components/suggestion/suggestion_list.tsx +++ b/webapp/channels/src/components/suggestion/suggestion_list.tsx @@ -44,7 +44,7 @@ export default class SuggestionList extends React.PureComponent { renderDividers: [], renderNoResults: false, }; - contentRef: React.RefObject; + contentRef: React.RefObject; wrapperRef: React.RefObject; itemRefs: Map; currentLabel: string | null; @@ -209,6 +209,7 @@ export default class SuggestionList extends React.PureComponent {
@@ -219,7 +220,7 @@ export default class SuggestionList extends React.PureComponent { renderNoResults() { return ( -
{ b: (chunks: string) => {chunks}, }} /> -
+ ); } @@ -296,10 +297,10 @@ export default class SuggestionList extends React.PureComponent { ref={this.wrapperRef} className={mainClass} > -
{ onMouseDown={this.props.preventClose} > {items} -
+
); } diff --git a/webapp/channels/src/components/suggestion/switch_channel_provider.tsx b/webapp/channels/src/components/suggestion/switch_channel_provider.tsx index 28dd5670ee7..28475d49460 100644 --- a/webapp/channels/src/components/suggestion/switch_channel_provider.tsx +++ b/webapp/channels/src/components/suggestion/switch_channel_provider.tsx @@ -121,9 +121,10 @@ type Props = SuggestionProps & WrappedComponentProps & { isPartOfOnlyOneTeam: boolean; status?: string; team?: Team; + id: string; } -const SwitchChannelSuggestion = React.forwardRef((props, ref) => { +const SwitchChannelSuggestion = React.forwardRef((props, ref) => { const {item, status, collapsedThreads, team, isPartOfOnlyOneTeam} = props; const channel = item.channel; const channelIsArchived = channel.delete_at && channel.delete_at !== 0; @@ -255,18 +256,31 @@ const SwitchChannelSuggestion = React.forwardRef((props, } const showSlug = (isPartOfOnlyOneTeam || channel.type === Constants.DM_CHANNEL) && channel.type !== Constants.THREADS; + const getId = () => { + if (channel.type === Constants.DM_CHANNEL) { + if (prefix) { + return `quickSwitchInput_${(channel as FakeDirectChannel).userId}`; + } + } + return `quickSwitchInput_${channel.id}`; + }; + return ( {icon}
- {name} + {name} {showSlug && description && {description}} {customStatus} @@ -316,6 +330,8 @@ function mapStateToPropsForSwitchChannelSuggestion(state: GlobalState, ownProps: collapsedThreads, team, isPartOfOnlyOneTeam, + + // id: 'quickSwitchInput', }; } diff --git a/webapp/channels/src/sass/components/_suggestion-list.scss b/webapp/channels/src/sass/components/_suggestion-list.scss index 46836e3de46..e640fa43c20 100644 --- a/webapp/channels/src/sass/components/_suggestion-list.scss +++ b/webapp/channels/src/sass/components/_suggestion-list.scss @@ -62,6 +62,7 @@ max-width: 100%; max-height: 292px; padding-bottom: 12px; + padding-left: 0; border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); border-radius: 4px; background-color: functions.v(center-channel-bg);