From df1b278f6210277f4c2510f232f4c4714cad34f0 Mon Sep 17 00:00:00 2001 From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:47:30 -0400 Subject: [PATCH] [MM-63007][MM-63004][MM-63020][MM-63009][MM-63008] More accessibility fixes around Search (#31409) * [MM-63008] Make collapse button on search bar an actual button * [MM-63004][MM-63020] Convert search box to floating-ui, fix some of the roles and labels that were incorrect * [MM-63009] Add radiogroup and radio roles to the search box types * [MM-63007] Ensure search box reads out number of results with suggestion items * Fix playwright tests * PR feedback * Remove floating ui overlay * Remove unnecessary .first() by being more specific about the search box --------- Co-authored-by: Mattermost Build --- .../accessibility_nav_diff_regions_spec.js | 2 +- .../archive_channel_reaction_spec.ts | 2 +- .../archive_channel_search_spec.ts | 8 +- .../archived_channel/archived_channel_spec.ts | 4 +- .../archived_leave_channel_spec.ts | 4 +- ...pen_rhs_coming_from_system_console_spec.ts | 2 +- .../accessibility_input_fields_spec.ts | 6 +- .../disabled_file_upload_spec.js | 2 +- .../ctrl_cmd_l_set_message_focus_spec.js | 2 +- .../center_channel_rhs_overlap_spec.js | 2 +- .../messaging/channel_and_posts_links_spec.js | 4 +- .../messaging/permalink_message_edit_spec.js | 2 +- .../messaging/post_pre_header_spec.js | 2 +- .../channels/search/clear_input_spec.js | 4 +- .../search/post_search_display_spec.js | 4 +- .../channels/search/results_post_spec.js | 4 +- .../search/search_group_message_spec.js | 2 +- .../channels/search_filter/input_spec.js | 8 +- .../channels/search_filter/negative_spec.js | 2 +- .../cypress/tests/support/ui/global_header.js | 2 +- e2e-tests/playwright/lib/src/index.ts | 2 +- .../{search_popover.ts => search_box.ts} | 3 +- .../playwright/lib/src/ui/components/index.ts | 6 +- .../playwright/lib/src/ui/pages/channels.ts | 4 +- .../search/search_box_clear_button.spec.ts | 6 +- .../search/search_box_suggestions.spec.ts | 20 +- .../src/components/new_search/new_search.tsx | 205 +++++++++++------- .../src/components/new_search/search_box.tsx | 6 - .../new_search/search_box_input.tsx | 1 + .../new_search/search_box_suggestions.tsx | 11 +- .../new_search/search_box_type_selector.tsx | 7 +- .../components/quick_input/quick_input.tsx | 1 + .../channels/src/components/search/search.tsx | 6 +- webapp/channels/src/i18n/en.json | 3 + .../src/sass/layout/_sidebar-right.scss | 2 + 35 files changed, 204 insertions(+), 147 deletions(-) rename e2e-tests/playwright/lib/src/ui/components/channels/{search_popover.ts => search_box.ts} (94%) diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_nav_diff_regions_spec.js b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_nav_diff_regions_spec.js index 44ada4537f..55de4a1500 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_nav_diff_regions_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_nav_diff_regions_spec.js @@ -126,7 +126,7 @@ describe('Verify Quick Navigation support across different regions in the app', it('MM-T1460_8 Verify Navigation Support in Search Results', () => { // # Search for some text cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().should('be.visible').first().type('hello {enter}'); + cy.uiGetSearchBox().should('be.visible').type('hello {enter}'); // # Change the focus to search results cy.get('#searchContainer').within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_reaction_spec.ts b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_reaction_spec.ts index 3f44a8537b..60b8342331 100644 --- a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_reaction_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_reaction_spec.ts @@ -65,7 +65,7 @@ describe('Archived channels', () => { // # Search for "Test archive reaction" cy.get('body').type('{esc}'); cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().should('be.visible').first().type(messageText).type('{enter}'); + cy.uiGetSearchBox().should('be.visible').type(messageText).type('{enter}'); // # Click on post dot menu so we can check for reaction icon cy.clickPostDotMenu(postId, 'SEARCH'); diff --git a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_search_spec.ts b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_search_spec.ts index bb2db9add5..03ed52718e 100644 --- a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_search_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_search_spec.ts @@ -61,7 +61,7 @@ describe('archive tests while preventing viewing archived channels', () => { // # Search for the post from step 1') cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type(`${messageText}{enter}`); + cy.uiGetSearchBox().clear().type(`${messageText}{enter}`); // * Post is returned by search, since it's not archived anymore cy.get('#searchContainer').should('be.visible'); @@ -96,7 +96,7 @@ describe('archive tests while preventing viewing archived channels', () => { // # Archive dialogue message reads "This will archive the channel from the team and make its contents inaccessible for all users" (Mobile dialogue makes no mention of the data will be accessible) cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type(`${testArchivedMessage}{enter}`); + cy.uiGetSearchBox().clear().type(`${testArchivedMessage}{enter}`); // * Post is not returned by search cy.get('#searchContainer').should('be.visible'); @@ -112,7 +112,7 @@ describe('archive tests while preventing viewing archived channels', () => { // # Search for the string of text from step 1 cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type(`${messageText}{enter}`); + cy.uiGetSearchBox().clear().type(`${messageText}{enter}`); // * Post is returned by search, since it's not archived anymore cy.get('#searchContainer').should('be.visible'); @@ -131,7 +131,7 @@ describe('archive tests while preventing viewing archived channels', () => { // # Archive a channel and make a mental note of the channel name // # Type "in:" and note the list of channels that appear cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type(`in:${testChannel.name}`); + cy.uiGetSearchBox().clear().type(`in:${testChannel.name}`); cy.findByTestId(testChannel.name).should('not.exist'); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/archived_channel/archived_channel_spec.ts b/e2e-tests/cypress/tests/integration/channels/archived_channel/archived_channel_spec.ts index 0fa22bbe17..a43afbc637 100644 --- a/e2e-tests/cypress/tests/integration/channels/archived_channel/archived_channel_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/archived_channel/archived_channel_spec.ts @@ -42,7 +42,7 @@ describe('Leave an archived channel', () => { // # Search for a post in an archived channel cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type(`${testArchivedMessage}{enter}`); + cy.uiGetSearchBox().clear().type(`${testArchivedMessage}{enter}`); // # Open the archived channel by selecting Jump from search results and then selecting the link to move to the most recent posts in the channel cy.uiGetSearchContainer().should('be.visible'); @@ -113,7 +113,7 @@ describe('Leave an archived channel', () => { createArchivedChannel({prefix: 'archived-search-for'}, messageList).then(({name}) => { // # Locate the post in a search cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type(`${messageList[1]}{enter}`); + cy.uiGetSearchBox().clear().type(`${messageList[1]}{enter}`); // # Click jump to open an archive post in permalink view cy.uiGetSearchContainer().should('be.visible'); diff --git a/e2e-tests/cypress/tests/integration/channels/archived_channel/archived_leave_channel_spec.ts b/e2e-tests/cypress/tests/integration/channels/archived_channel/archived_leave_channel_spec.ts index 02177d40cf..6b862c4a60 100644 --- a/e2e-tests/cypress/tests/integration/channels/archived_channel/archived_leave_channel_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/archived_channel/archived_leave_channel_spec.ts @@ -97,7 +97,7 @@ describe('Leave an archived channel', () => { // # Search for content from an archived channel cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type(`${messageD}{enter}`); + cy.uiGetSearchBox().clear().type(`${messageD}{enter}`); // # Open the channel from search results cy.get('#searchContainer').should('be.visible'); @@ -109,7 +109,7 @@ describe('Leave an archived channel', () => { // # Search for content from a different archived channel cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type(`${messageC}{enter}`); + cy.uiGetSearchBox().clear().type(`${messageC}{enter}`); // # Open the channel from search result by clicking Jump cy.get('#searchContainer').should('be.visible').findByText('Jump').click().wait(TIMEOUTS.ONE_SEC); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/open_rhs_coming_from_system_console_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/open_rhs_coming_from_system_console_spec.ts index 190d8536ab..38a81bd5b7 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/open_rhs_coming_from_system_console_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/open_rhs_coming_from_system_console_spec.ts @@ -134,7 +134,7 @@ describe('Channel RHS', () => { // # Enter the search terms and hit enter to start the search cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type(MESSAGES.TINY).type('{enter}'); + cy.uiGetSearchBox().clear().type(MESSAGES.TINY).type('{enter}'); // * Verify that the search results is opened in RHS verifyRHSisOpenAndHasTitle('Search Results'); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_input_fields_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_input_fields_spec.ts index 26c502fbdd..008d044a7c 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_input_fields_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_input_fields_spec.ts @@ -89,7 +89,7 @@ describe('Verify Accessibility Support in different input fields', () => { cy.uiGetSearchBox().type('from:').wait(TIMEOUTS.ONE_SEC); // # Trigger the user autocomplete again - cy.uiGetSearchBox().first().clear().type('from:').wait(TIMEOUTS.ONE_SEC).type('{downarrow}{downarrow}'); + cy.uiGetSearchBox().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.ONE_SEC); + cy.uiGetSearchBox().clear().type('in:').wait(TIMEOUTS.ONE_SEC); // # Trigger the channel autocomplete again - cy.uiGetSearchBox().first().clear().type('in:').wait(TIMEOUTS.ONE_SEC).type('{downarrow}{downarrow}'); + cy.uiGetSearchBox().clear().type('in:').wait(TIMEOUTS.ONE_SEC).type('{downarrow}{downarrow}'); // * Verify Accessibility Support in search autocomplete verifySearchAutocomplete(2, 'channel'); diff --git a/e2e-tests/cypress/tests/integration/channels/files_and_attachments/disabled_file_upload_spec.js b/e2e-tests/cypress/tests/integration/channels/files_and_attachments/disabled_file_upload_spec.js index 54a54e9441..2e69de5370 100644 --- a/e2e-tests/cypress/tests/integration/channels/files_and_attachments/disabled_file_upload_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/files_and_attachments/disabled_file_upload_spec.js @@ -72,7 +72,7 @@ describe('Upload Files - Settings', () => { cy.get('#searchHints').find('.search-hint__search-type-selector button > .icon-file-text-outline').should('not.exist'); // # Search for posts - cy.uiGetSearchBox().first().type('sample').type('{enter}'); + cy.uiGetSearchBox().type('sample').type('{enter}'); // * Verify search results do not have File button cy.get('.files-tab').should('not.exist'); diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_l_set_message_focus_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_l_set_message_focus_spec.js index 60e285475c..6e9dfbf5ee 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_l_set_message_focus_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_l_set_message_focus_spec.js @@ -43,7 +43,7 @@ describe('Keyboard Shortcuts', () => { it('MM-T1250 CTRL/CMD+SHIFT+L - Set focus to center channel message box (with SEARCH RHS open)', () => { // # Search cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().type('test{enter}'); + cy.uiGetSearchBox().type('test{enter}'); // * Wait for the RHS to open and the search results to appear cy.contains('.sidebar--right__header', 'Search Results').should('be.visible'); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/center_channel_rhs_overlap_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/center_channel_rhs_overlap_spec.js index 0ad7cbc882..20900d3cb5 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/center_channel_rhs_overlap_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/center_channel_rhs_overlap_spec.js @@ -720,7 +720,7 @@ describe('Messaging', () => { cy.getLastPostId().then((postId) => { // # Search for the posted message cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().should('be.visible').first().type(messageX).type('{enter}').wait(TIMEOUTS.HALF_SEC); + cy.uiGetSearchBox().should('be.visible').type(messageX).type('{enter}').wait(TIMEOUTS.HALF_SEC); // # Click on post dot menu so we can edit cy.clickPostDotMenu(postId, 'SEARCH'); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/channel_and_posts_links_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/channel_and_posts_links_spec.js index bfb9410588..1739a0f2ce 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/channel_and_posts_links_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/channel_and_posts_links_spec.js @@ -55,7 +55,7 @@ describe('Message permalink', () => { // # Search for a message in the current channel cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type('in:town-square').wait(TIMEOUTS.HALF_SEC).type('{enter}{enter}'); + cy.uiGetSearchBox().clear().type('in:town-square').wait(TIMEOUTS.HALF_SEC).type('{enter}{enter}'); // # Jump to first permalink view (most recent message) cy.get('.search-item__jump').first().click(); @@ -73,7 +73,7 @@ describe('Message permalink', () => { // # Search for a message in the current channel cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().type('in:town-square').wait(TIMEOUTS.HALF_SEC).type('{enter}{enter}'); + cy.uiGetSearchBox().clear().type('in:town-square').wait(TIMEOUTS.HALF_SEC).type('{enter}{enter}'); // # Jump to first permalink view (most recent message) cy.get('.search-item__jump').first().click(); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/permalink_message_edit_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/permalink_message_edit_spec.js index 04d75a2446..001ba8ba0d 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/permalink_message_edit_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/permalink_message_edit_spec.js @@ -71,7 +71,7 @@ describe('Permalink message edit', () => { // # Find searchWord and verify edited post cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().should('be.visible').first().type(searchWord).type('{enter}'); + cy.uiGetSearchBox().should('be.visible').type(searchWord).type('{enter}'); cy.get('.search-item__jump').first().click(); // # Check if url include the permalink diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/post_pre_header_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/post_pre_header_spec.js index ff4362c84d..1e7543121d 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/post_pre_header_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/post_pre_header_spec.js @@ -187,7 +187,7 @@ describe('Post PreHeader', () => { // # Search for the channel. cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().type('test both pinned and saved {enter}'); + cy.uiGetSearchBox().type('test both pinned and saved {enter}'); // * Check that the post pre-header has both pinned and saved links in RHS search results cy.get('#searchContainer').should('be.visible').within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/search/clear_input_spec.js b/e2e-tests/cypress/tests/integration/channels/search/clear_input_spec.js index a9e17d2118..938558568a 100644 --- a/e2e-tests/cypress/tests/integration/channels/search/clear_input_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/search/clear_input_spec.js @@ -26,7 +26,7 @@ describe('Search', () => { cy.uiGetSearchContainer().click(); // # Write something on the input - cy.uiGetSearchBox().first().clear().wait(TIMEOUTS.HALF_SEC).type('abc').wait(TIMEOUTS.HALF_SEC).type('{enter}'); + cy.uiGetSearchBox().clear().wait(TIMEOUTS.HALF_SEC).type('abc').wait(TIMEOUTS.HALF_SEC).type('{enter}'); cy.uiGetSearchContainer().click(); @@ -55,7 +55,7 @@ describe('Search', () => { // * Verify search input field exists and not search button, as inputs contains placeholder not buttons/icons // and then type in a search text cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().first().clear().wait(TIMEOUTS.HALF_SEC).type(searchText + '{enter}').wait(TIMEOUTS.HALF_SEC); + cy.uiGetSearchBox().clear().wait(TIMEOUTS.HALF_SEC).type(searchText + '{enter}').wait(TIMEOUTS.HALF_SEC); // # Now click on the saved post button from the header cy.uiGetSavedPostButton().click(); diff --git a/e2e-tests/cypress/tests/integration/channels/search/post_search_display_spec.js b/e2e-tests/cypress/tests/integration/channels/search/post_search_display_spec.js index f9e56b8857..ee423efe07 100644 --- a/e2e-tests/cypress/tests/integration/channels/search/post_search_display_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/search/post_search_display_spec.js @@ -48,7 +48,7 @@ describe('Search', () => { // # Click on "x" displayed on searchbox cy.uiGetSearchBox().parent().siblings('.input-clear-x').wait(TIMEOUTS.ONE_SEC).click({force: true}); cy.uiGetSearchBox().parents('[class*="SearchInputContainer"]').siblings('#searchHints').should('be.visible'); - cy.uiGetSearchBox().first().focus().type('{esc}'); + cy.uiGetSearchBox().focus().type('{esc}'); // # RHS should be visible with search results cy.get('#search-items-container').should('be.visible'); @@ -134,7 +134,7 @@ describe('Search', () => { assertSearchHint(); // # Clear search box - cy.uiGetSearchBox().get('.input-clear-x').first().click({force: true}).wait(TIMEOUTS.HALF_SEC); + cy.uiGetSearchBox().get('.input-clear-x').click({force: true}).wait(TIMEOUTS.HALF_SEC); // # Search for search term in:town-square{enter} cy.uiGetSearchBox().type('in:town-square').wait(TIMEOUTS.HALF_SEC); diff --git a/e2e-tests/cypress/tests/integration/channels/search/results_post_spec.js b/e2e-tests/cypress/tests/integration/channels/search/results_post_spec.js index 1c576e09e4..02681558f7 100644 --- a/e2e-tests/cypress/tests/integration/channels/search/results_post_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/search/results_post_spec.js @@ -37,7 +37,7 @@ describe('Search', () => { // # Search for "apple" cy.uiGetSearchContainer().click(); - cy.uiGetSearchBox().should('be.visible').first().type(apple).type('{enter}'); + cy.uiGetSearchBox().should('be.visible').type(apple).type('{enter}'); // # Get last postId cy.getLastPostId().as('lastPostId'); @@ -50,7 +50,7 @@ describe('Search', () => { cy.uiGetSearchContainer().click(); // * Type banana on search box but don't hit search - cy.uiGetSearchBox().first().clear({force: true}).type(banana, {force: true}); + cy.uiGetSearchBox().clear({force: true}).type(banana, {force: true}); // * Search result should not change and remain as one result with highlight still on apple cy.get('@lastPostId').then((postId) => { diff --git a/e2e-tests/cypress/tests/integration/channels/search/search_group_message_spec.js b/e2e-tests/cypress/tests/integration/channels/search/search_group_message_spec.js index 32d7bd47eb..be5bce2777 100644 --- a/e2e-tests/cypress/tests/integration/channels/search/search_group_message_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/search/search_group_message_spec.js @@ -68,7 +68,7 @@ describe('Search', () => { //# Search for the message cy.uiGetSearchContainer().should('be.visible').click(); - cy.uiGetSearchBox().first().clear().type(`${message}{enter}`); + cy.uiGetSearchBox().clear().type(`${message}{enter}`); // * Should return exactly one result from the group channel and matches the message cy.findAllByTestId('search-item-container').should('be.visible').and('have.length', 1).within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/search_filter/input_spec.js b/e2e-tests/cypress/tests/integration/channels/search_filter/input_spec.js index 059c2a9bb7..ad6f2a701e 100644 --- a/e2e-tests/cypress/tests/integration/channels/search_filter/input_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/search_filter/input_spec.js @@ -54,7 +54,7 @@ describe('Search Date Filter', () => { // # Type before: in search field cy.uiGetSearchContainer().should('be.visible').click(); - cy.uiGetSearchBox().first().clear().type('before:'); + cy.uiGetSearchBox().clear().type('before:'); // * Day picker should be visible cy.get('.rdp'). @@ -74,7 +74,7 @@ describe('Search Date Filter', () => { cy.uiGetSearchBox().parent('.input-wrapper').siblings('.input-clear-x').click({force: true}); // * The "x" to clear the search query has disappeared - cy.uiGetSearchBox().first().should('have.value', ''); + cy.uiGetSearchBox().should('have.value', ''); cy.uiGetSearchContainer().should('be.visible').click(); }); @@ -84,7 +84,7 @@ describe('Search Date Filter', () => { // # Type before: in search field cy.uiGetSearchContainer().should('be.visible').click(); - cy.uiGetSearchBox().first().clear().type('before:'); + cy.uiGetSearchBox().clear().type('before:'); // * Date picker should be visible cy.get('.rdp'). @@ -135,7 +135,7 @@ describe('Search Date Filter', () => { should('have.value', ''); // # Enter query to search box and then click "x" to the right of the search term - cy.uiGetSearchBox().first().clear().wait(TIMEOUTS.HALF_SEC).type(queryString); + cy.uiGetSearchBox().clear().wait(TIMEOUTS.HALF_SEC).type(queryString); cy.uiGetSearchBox().parent('.input-wrapper').siblings('.input-clear-x').click({force: true}); // * The "x" to clear the search query has disappeared diff --git a/e2e-tests/cypress/tests/integration/channels/search_filter/negative_spec.js b/e2e-tests/cypress/tests/integration/channels/search_filter/negative_spec.js index 65ae42f1af..5ff2c71ca8 100644 --- a/e2e-tests/cypress/tests/integration/channels/search_filter/negative_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/search_filter/negative_spec.js @@ -66,7 +66,7 @@ describe('Negative search filters will omit results', () => { function search(query) { cy.reload(); cy.uiGetSearchContainer().should('be.visible').click(); - cy.uiGetSearchBox().first().clear().wait(TIMEOUTS.HALF_SEC).type(query).wait(TIMEOUTS.HALF_SEC).type('{enter}'); + cy.uiGetSearchBox().clear().wait(TIMEOUTS.HALF_SEC).type(query).wait(TIMEOUTS.HALF_SEC).type('{enter}'); cy.get('#loadingSpinner').should('not.exist'); cy.uiGetRHSSearchContainer(); diff --git a/e2e-tests/cypress/tests/support/ui/global_header.js b/e2e-tests/cypress/tests/support/ui/global_header.js index 4800e2841e..911e4207a3 100644 --- a/e2e-tests/cypress/tests/support/ui/global_header.js +++ b/e2e-tests/cypress/tests/support/ui/global_header.js @@ -104,7 +104,7 @@ Cypress.Commands.add('uiGetSearchContainer', () => { }); Cypress.Commands.add('uiGetSearchBox', () => { - return cy.get('.search-bar').should('be.visible'); + return cy.findByRole('searchbox').should('be.visible'); }); Cypress.Commands.add('uiGetRecentMentionButton', () => { diff --git a/e2e-tests/playwright/lib/src/index.ts b/e2e-tests/playwright/lib/src/index.ts index 9216a5de97..5df4baf223 100644 --- a/e2e-tests/playwright/lib/src/index.ts +++ b/e2e-tests/playwright/lib/src/index.ts @@ -22,7 +22,7 @@ export { export { components, GlobalHeader, - SearchPopover, + SearchBox, ChannelsCenterView, ChannelsSidebarLeft, ChannelsSidebarRight, diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/search_popover.ts b/e2e-tests/playwright/lib/src/ui/components/channels/search_box.ts similarity index 94% rename from e2e-tests/playwright/lib/src/ui/components/channels/search_popover.ts rename to e2e-tests/playwright/lib/src/ui/components/channels/search_box.ts index 57afd477b9..36be3fd664 100644 --- a/e2e-tests/playwright/lib/src/ui/components/channels/search_popover.ts +++ b/e2e-tests/playwright/lib/src/ui/components/channels/search_box.ts @@ -3,7 +3,7 @@ import {Locator, expect} from '@playwright/test'; -export default class SearchPopover { +export default class SearchBox { readonly container: Locator; readonly messagesButton; @@ -30,6 +30,7 @@ export default class SearchPopover { async clearIfPossible() { if (await this.clearButton.isVisible()) { await this.clearButton.click(); + await expect(this.searchInput).toHaveValue(''); return true; } return false; diff --git a/e2e-tests/playwright/lib/src/ui/components/index.ts b/e2e-tests/playwright/lib/src/ui/components/index.ts index f4311e7ea7..85943ffba5 100644 --- a/e2e-tests/playwright/lib/src/ui/components/index.ts +++ b/e2e-tests/playwright/lib/src/ui/components/index.ts @@ -13,7 +13,7 @@ import FindChannelsModal from './channels/find_channels_modal'; import SettingsModal from './channels/settings/settings_modal'; import Footer from './footer'; import GlobalHeader from './global_header'; -import SearchPopover from './channels/search_popover'; +import SearchBox from './channels/search_box'; import MainHeader from './main_header'; import PostDotMenu from './channels/post_dot_menu'; import PostReminderMenu from './channels/post_reminder_menu'; @@ -47,7 +47,7 @@ import DraftPost from './channels/draft_post'; const components = { GlobalHeader, - SearchPopover, + SearchBox, ChannelsCenterView, ChannelsSidebarLeft, ChannelsSidebarRight, @@ -94,7 +94,7 @@ const components = { export { components, GlobalHeader, - SearchPopover, + SearchBox, ChannelsCenterView, ChannelsSidebarLeft, ChannelsSidebarRight, diff --git a/e2e-tests/playwright/lib/src/ui/pages/channels.ts b/e2e-tests/playwright/lib/src/ui/pages/channels.ts index baa6e46510..9a282d526a 100644 --- a/e2e-tests/playwright/lib/src/ui/pages/channels.ts +++ b/e2e-tests/playwright/lib/src/ui/pages/channels.ts @@ -14,7 +14,7 @@ export default class ChannelsPage { readonly globalHeader; readonly userAccountMenuButton; - readonly searchPopover; + readonly searchBox; readonly centerView; readonly scheduledDraftModal; readonly sidebarLeft; @@ -40,7 +40,7 @@ export default class ChannelsPage { // The main areas of the app this.globalHeader = new components.GlobalHeader(this, page.locator('#global-header')); - this.searchPopover = new components.SearchPopover(page.locator('#searchPopover')); + this.searchBox = new components.SearchBox(page.locator('#searchBox')); this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view'), page); this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer')); this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right')); diff --git a/e2e-tests/playwright/specs/functional/channels/search/search_box_clear_button.spec.ts b/e2e-tests/playwright/specs/functional/channels/search/search_box_clear_button.spec.ts index 013b97d2ae..463258689c 100644 --- a/e2e-tests/playwright/specs/functional/channels/search/search_box_clear_button.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/search/search_box_clear_button.spec.ts @@ -19,14 +19,14 @@ test('MM-64155 search box clear button should not leave type badge after closing // # Type something in the search box const searchText = 'abcdef'; - const {searchInput} = channelsPage.searchPopover; + const {searchInput} = channelsPage.searchBox; await searchInput.pressSequentially(searchText); // * Verify text was entered await expect(searchInput).toHaveValue(searchText); // # Click the clear button - await channelsPage.searchPopover.clearIfPossible(); + await channelsPage.searchBox.clearIfPossible(); // * Verify the input is cleared await expect(searchInput).toHaveValue(''); @@ -35,7 +35,7 @@ test('MM-64155 search box clear button should not leave type badge after closing await channelsPage.page.click('body', {position: {x: 0, y: 0}}); // * Verify the search box is closed - await expect(channelsPage.searchPopover.container).not.toBeVisible(); + await expect(channelsPage.searchBox.container).not.toBeVisible(); // * Verify there is no search type badge/chip in the search bar // The search type badge is rendered when searchType is either 'messages' or 'files' diff --git a/e2e-tests/playwright/specs/functional/channels/search/search_box_suggestions.spec.ts b/e2e-tests/playwright/specs/functional/channels/search/search_box_suggestions.spec.ts index 190b5c183f..a3f4aaf85b 100644 --- a/e2e-tests/playwright/specs/functional/channels/search/search_box_suggestions.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/search/search_box_suggestions.spec.ts @@ -22,12 +22,12 @@ test('Search box suggestion must be case insensitive', async ({pw}) => { // Should work as expected when using lowercase // # Type in lowercase "off" to search for the "Off-Topic" channel - const {searchInput} = channelsPage.searchPopover; + const {searchInput} = channelsPage.searchBox; await searchInput.pressSequentially(`In:${searchWord}`); // * The suggestion should be visible - await expect(channelsPage.searchPopover.selectedSuggestion).toBeVisible(); - await expect(channelsPage.searchPopover.selectedSuggestion).toHaveText(channelName); + await expect(channelsPage.searchBox.selectedSuggestion).toBeVisible(); + await expect(channelsPage.searchBox.selectedSuggestion).toHaveText(channelName); // # Press Enter to select the suggestion and another Enter to search await searchInput.press('Enter'); @@ -41,14 +41,14 @@ test('Search box suggestion must be case insensitive', async ({pw}) => { await channelsPage.globalHeader.openSearch(); // # Clear its content - await channelsPage.searchPopover.clearIfPossible(); + await channelsPage.searchBox.clearIfPossible(); // # Type in uppercase "OFF" to search for the "Off-Topic" channel - await searchInput.pressSequentially(`In:${searchWord.toUpperCase()}`); + await searchInput.type(`In:${searchWord.toUpperCase()}`); // * The suggestion should be visible - await expect(channelsPage.searchPopover.selectedSuggestion).toBeVisible(); - await expect(channelsPage.searchPopover.selectedSuggestion).toHaveText(channelName); + await expect(channelsPage.searchBox.selectedSuggestion).toBeVisible(); + await expect(channelsPage.searchBox.selectedSuggestion).toHaveText(channelName); // # Press Enter to select the suggestion and another Enter to search await searchInput.press('Enter'); @@ -73,12 +73,12 @@ test('remove extra whitespace when selecting a user', async ({pw}) => { await channelsPage.globalHeader.openSearch(); // # Type "from:" followed by multiple spaces - const {searchInput} = channelsPage.searchPopover; + const {searchInput} = channelsPage.searchBox; await searchInput.pressSequentially(`from: ${admin.username}`); // * The suggestion should be visible - await expect(channelsPage.searchPopover.selectedSuggestion).toBeVisible(); - await expect(channelsPage.searchPopover.selectedSuggestion).toHaveText(`@` + admin.username); + await expect(channelsPage.searchBox.selectedSuggestion).toBeVisible(); + await expect(channelsPage.searchBox.selectedSuggestion).toHaveText(`@` + admin.username); // # Press enter to validate the selection await searchInput.press('Enter'); diff --git a/webapp/channels/src/components/new_search/new_search.tsx b/webapp/channels/src/components/new_search/new_search.tsx index 89b6329a38..4ba4a13f2b 100644 --- a/webapp/channels/src/components/new_search/new_search.tsx +++ b/webapp/channels/src/components/new_search/new_search.tsx @@ -1,8 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import { + useFloating, + autoUpdate, + useClick, + useDismiss, + useInteractions, + useRole, + FloatingFocusManager, + FloatingPortal, + offset, +} from '@floating-ui/react'; import React, {useEffect, useState, useRef, useCallback} from 'react'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, useIntl} from 'react-intl'; import {useSelector, useDispatch} from 'react-redux'; import styled from 'styled-components'; @@ -16,30 +27,15 @@ import {updateSearchTerms, showSearchResults, updateSearchType, updateSearchTeam import {getSearchButtons} from 'selectors/plugins'; import {getSearchTeam, getSearchTerms, getSearchType} from 'selectors/rhs'; -import Popover from 'components/widgets/popover'; - import a11yController from 'utils/a11y_controller_instance'; import {focusElement} from 'utils/a11y_utils'; -import Constants from 'utils/constants'; +import {RootHtmlPortalId, Constants} from 'utils/constants'; import * as Keyboard from 'utils/keyboard'; import {isServerVersionGreaterThanOrEqualTo} from 'utils/server_version'; import {isDesktopApp, getDesktopVersion, isMacApp} from 'utils/user_agent'; import SearchBox from './search_box'; -const PopoverStyled = styled(Popover)` - min-width: 600px; - left: -90px; - top: -12px; - border-radius: 12px; - max-height: 90vh; - overflow-y: auto; - - .popover-content { - padding: 0px; - } -`; - const SearchTypeBadge = styled.div` display: flex; align-items: center; @@ -106,7 +102,18 @@ const NewSearchTerms = styled.span` white-space: nowrap; `; +const SearchBoxContainer = styled.div` + min-width: 600px; + border-radius: 12px; + max-height: 90vh; + overflow-y: auto; + background: var(--center-channel-bg); + box-shadow: 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.16), 0 4px 6px rgba(0, 0, 0, 0.12); + z-index: 1050; +`; + const NewSearch = (): JSX.Element => { + const intl = useIntl(); const currentChannelName = useSelector(getCurrentChannelNameForSearchShortcut); const searchTerms = useSelector(getSearchTerms) || ''; const searchType = useSelector(getSearchType) || ''; @@ -120,7 +127,25 @@ const NewSearch = (): JSX.Element => { const [focused, setFocused] = useState(false); const [currentChannel, setCurrentChannel] = useState(''); const searchBoxRef = useRef(null); - const searchButtonRef = useRef(null); + + const {refs, floatingStyles, context: floatingContext} = useFloating({ + open: focused, + onOpenChange: setFocused, + whileElementsMounted: autoUpdate, + placement: 'bottom', + middleware: [offset({mainAxis: -28})], + }); + const searchButtonRef = refs.reference as React.RefObject; + + const clickInteractions = useClick(floatingContext); + const dismissInteraction = useDismiss(floatingContext); + const role = useRole(floatingContext); + + const {getReferenceProps, getFloatingProps} = useInteractions([ + clickInteractions, + dismissInteraction, + role, + ]); useEffect(() => { const isDesktop = isDesktopApp() && isServerVersionGreaterThanOrEqualTo(getDesktopVersion(), '4.7.0'); @@ -255,75 +280,89 @@ const NewSearch = (): JSX.Element => { const clearSearchType = useCallback(() => dispatch(updateSearchType('')), []); return ( - - - {(searchType === 'messages' || searchType === 'files') && ( - - {searchType === 'messages' && ( - + + + {(searchType === 'messages' || searchType === 'files') && ( + + {searchType === 'messages' && ( + + )} + {searchType === 'files' && ( + + )} + - )} - {searchType === 'files' && ( - - )} - - - )} - {searchTerms && {searchTerms}} - {searchTerms && ( - - - - )} - {!searchTerms && ( - - )} - {focused && ( - - + )} + + + {focused && ( + + + + + + + )} - + ); }; diff --git a/webapp/channels/src/components/new_search/search_box.tsx b/webapp/channels/src/components/new_search/search_box.tsx index 77d98cc0fe..78dde9b09a 100644 --- a/webapp/channels/src/components/new_search/search_box.tsx +++ b/webapp/channels/src/components/new_search/search_box.tsx @@ -268,12 +268,6 @@ const SearchBox = forwardRef( (({searchTerms, searchTyp autoFocus={true} onKeyDown={onKeyDown} tabIndex={0} + role='searchbox' /> {searchTerms.length > 0 && ( - {generateLabel(providerResults.items[selectedOption])} + )} {providerResults.items.map((item, idx) => { diff --git a/webapp/channels/src/components/new_search/search_box_type_selector.tsx b/webapp/channels/src/components/new_search/search_box_type_selector.tsx index 218c81ca64..83796e23cb 100644 --- a/webapp/channels/src/components/new_search/search_box_type_selector.tsx +++ b/webapp/channels/src/components/new_search/search_box_type_selector.tsx @@ -55,10 +55,13 @@ const SearchTypeSelector = ({searchType, setSearchType}: Props) => { const searchPluginButtons = useSelector(getSearchButtons); return ( - + { { key={pluginId} selected={searchType === pluginId} onClick={() => setSearchType(pluginId)} + role='radio' > diff --git a/webapp/channels/src/components/quick_input/quick_input.tsx b/webapp/channels/src/components/quick_input/quick_input.tsx index 09255766b6..bf1cb71cfe 100644 --- a/webapp/channels/src/components/quick_input/quick_input.tsx +++ b/webapp/channels/src/components/quick_input/quick_input.tsx @@ -81,6 +81,7 @@ export type Props = { id?: string; onInput?: (e?: React.FormEvent) => void; tabIndex?: number; + role?: string; } // A component that can be used to make controlled inputs that function properly in certain diff --git a/webapp/channels/src/components/search/search.tsx b/webapp/channels/src/components/search/search.tsx index 09e1a7908c..701e94cd33 100644 --- a/webapp/channels/src/components/search/search.tsx +++ b/webapp/channels/src/components/search/search.tsx @@ -466,16 +466,18 @@ const Search = ({ const renderSearchBar = (): JSX.Element => ( <>
-
+