[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 <build@mattermost.com>
Этот коммит содержится в:
Devin Binnie
2025-06-17 17:47:30 -04:00
коммит произвёл GitHub
родитель e367872c0b
Коммит df1b278f62
35 изменённых файлов: 204 добавлений и 147 удалений

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

@@ -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(() => {

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

@@ -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');

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

@@ -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');
});
});

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

@@ -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');

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

@@ -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);

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

@@ -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');

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

@@ -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');

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

@@ -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');

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

@@ -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');

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

@@ -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');

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

@@ -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();

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

@@ -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

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

@@ -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(() => {

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

@@ -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();

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

@@ -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);

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

@@ -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) => {

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

@@ -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(() => {

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

@@ -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

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

@@ -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();

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

@@ -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', () => {

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

@@ -22,7 +22,7 @@ export {
export {
components,
GlobalHeader,
SearchPopover,
SearchBox,
ChannelsCenterView,
ChannelsSidebarLeft,
ChannelsSidebarRight,

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

@@ -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;

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

@@ -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,

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

@@ -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'));

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

@@ -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'

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

@@ -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');

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

@@ -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<boolean>(false);
const [currentChannel, setCurrentChannel] = useState('');
const searchBoxRef = useRef<HTMLDivElement | null>(null);
const searchButtonRef = useRef<HTMLDivElement | null>(null);
const {refs, floatingStyles, context: floatingContext} = useFloating<HTMLDivElement>({
open: focused,
onOpenChange: setFocused,
whileElementsMounted: autoUpdate,
placement: 'bottom',
middleware: [offset({mainAxis: -28})],
});
const searchButtonRef = refs.reference as React.RefObject<HTMLDivElement>;
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 (
<NewSearchContainer
tabIndex={0}
onKeyDown={openSearchBoxOnKeyPress}
onClick={openSearchBox}
id='searchFormContainer'
role='button'
className='a11y__region'
ref={searchButtonRef}
>
<i className='icon icon-magnify'/>
{(searchType === 'messages' || searchType === 'files') && (
<SearchTypeBadge data-testid='searchTypeBadge'>
{searchType === 'messages' && (
<FormattedMessage
id='search_bar.search_types.messages'
defaultMessage='MESSAGES'
<>
<NewSearchContainer
tabIndex={0}
id='searchFormContainer'
role='button'
className='a11y__region'
ref={refs.setReference}
{...getReferenceProps({
onKeyDown: openSearchBoxOnKeyPress,
onClick: openSearchBox,
})}
>
<i className='icon icon-magnify'/>
{(searchType === 'messages' || searchType === 'files') && (
<SearchTypeBadge data-testid='searchTypeBadge'>
{searchType === 'messages' && (
<FormattedMessage
id='search_bar.search_types.messages'
defaultMessage='MESSAGES'
/>
)}
{searchType === 'files' && (
<FormattedMessage
id='search_bar.search_types.files'
defaultMessage='FILES'
/>
)}
<i
className='icon icon-close icon-12'
onClick={clearSearchType}
/>
)}
{searchType === 'files' && (
<FormattedMessage
id='search_bar.search_types.files'
defaultMessage='FILES'
/>
)}
<i
className='icon icon-close icon-12'
onClick={clearSearchType}
/>
</SearchTypeBadge>
)}
{searchTerms && <NewSearchTerms tabIndex={0}>{searchTerms}</NewSearchTerms>}
{searchTerms && (
<CloseIcon
data-testid='input-clear'
role='button'
onClick={onClose}
>
<span
className='input-clear-x'
aria-hidden='true'
</SearchTypeBadge>
)}
{searchTerms && <NewSearchTerms tabIndex={0}>{searchTerms}</NewSearchTerms>}
{searchTerms && (
<CloseIcon
data-testid='input-clear'
role='button'
onClick={onClose}
>
<i className='icon icon-close-circle'/>
</span>
</CloseIcon>
)}
{!searchTerms && (
<FormattedMessage
id='search_bar.search'
defaultMessage='Search'
/>
)}
{focused && (
<PopoverStyled
id='searchPopover'
placement='bottom'
>
<SearchBox
ref={searchBoxRef}
onClose={closeSearchBox}
onSearch={runSearch}
initialSearchTerms={currentChannel ? `in:${currentChannel} ` : searchTerms}
initialSearchType={searchType}
initialSearchTeam={searchTeam}
crossTeamSearchEnabled={crossTeamSearchEnabled}
myTeams={myTeams}
<span
className='input-clear-x'
aria-hidden='true'
>
<i className='icon icon-close-circle'/>
</span>
</CloseIcon>
)}
{!searchTerms && (
<FormattedMessage
id='search_bar.search'
defaultMessage='Search'
/>
</PopoverStyled>
)}
</NewSearchContainer>
{focused && (
<FloatingPortal id={RootHtmlPortalId}>
<FloatingFocusManager context={floatingContext}>
<SearchBoxContainer
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
aria-label={intl.formatMessage({
id: 'search_bar.search_box',
defaultMessage: 'Search box',
})}
>
<SearchBox
ref={searchBoxRef}
onClose={closeSearchBox}
onSearch={runSearch}
initialSearchTerms={currentChannel ? `in:${currentChannel} ` : searchTerms}
initialSearchType={searchType}
initialSearchTeam={searchTeam}
crossTeamSearchEnabled={crossTeamSearchEnabled}
myTeams={myTeams}
/>
</SearchBoxContainer>
</FloatingFocusManager>
</FloatingPortal>
)}
</NewSearchContainer>
</>
);
};

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

@@ -268,12 +268,6 @@ const SearchBox = forwardRef(
<SearchBoxContainer
ref={ref}
id='searchBox'
aria-label={intl.formatMessage({
id: 'search_bar.search',
defaultMessage: 'Search',
})}
aria-describedby='searchHints'
role='searchbox'
>
<CloseIcon
data-testid='searchBoxClose'

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

@@ -116,6 +116,7 @@ const SearchInput = forwardRef<HTMLInputElement, Props>(({searchTerms, searchTyp
autoFocus={true}
onKeyDown={onKeyDown}
tabIndex={0}
role='searchbox'
/>
{searchTerms.length > 0 && (
<ClearButton

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

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux';
import styled from 'styled-components';
@@ -93,7 +94,15 @@ const SearchSuggestions = ({searchType, searchTeam, searchTerms, suggestionsHead
className='sr-only'
key={providerResults.terms[selectedOption]}
>
{generateLabel(providerResults.items[selectedOption])}
<FormattedMessage
id='search_box_suggestions.suggestions_readout'
defaultMessage='{label} ({idx} of {total} results available)'
values={{
label: generateLabel(providerResults.items[selectedOption]),
idx: selectedOption + 1,
total: providerResults.items.length,
}}
/>
</div>
)}
{providerResults.items.map((item, idx) => {

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

@@ -55,10 +55,13 @@ const SearchTypeSelector = ({searchType, setSearchType}: Props) => {
const searchPluginButtons = useSelector(getSearchButtons);
return (
<SearchTypeSelectorContainer>
<SearchTypeSelectorContainer
role='radiogroup'
>
<SearchTypeItem
selected={searchType === 'messages'}
onClick={setMessagesSearchType}
role='radio'
>
<FormattedMessage
id='search_bar.usage.search_type_messages'
@@ -68,6 +71,7 @@ const SearchTypeSelector = ({searchType, setSearchType}: Props) => {
<SearchTypeItem
selected={searchType === 'files'}
onClick={setFilesSearchType}
role='radio'
>
<FormattedMessage
id='search_bar.usage.search_type_files'
@@ -81,6 +85,7 @@ const SearchTypeSelector = ({searchType, setSearchType}: Props) => {
key={pluginId}
selected={searchType === pluginId}
onClick={() => setSearchType(pluginId)}
role='radio'
>
<ErrorBoundary>
<Component/>

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

@@ -81,6 +81,7 @@ export type Props = {
id?: string;
onInput?: (e?: React.FormEvent<HTMLInputElement>) => void;
tabIndex?: number;
role?: string;
}
// A component that can be used to make controlled inputs that function properly in certain

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

@@ -466,16 +466,18 @@ const Search = ({
const renderSearchBar = (): JSX.Element => (
<>
<div className='sidebar-collapse__container'>
<div
<button
id={isSideBarRight ? 'sbrSidebarCollapse' : 'sidebarCollapse'}
className='sidebar-collapse'
onClick={handleClose}
aria-label={intl.formatMessage({id: 'channel_header.back', defaultMessage: 'Back to channel'})}
>
<span
className='fa fa-2x fa-angle-left'
title={intl.formatMessage({id: 'generic_icons.back', defaultMessage: 'Back Icon'})}
aria-hidden='true'
/>
</div>
</button>
</div>
<SearchBar
updateHighlightedSearchHint={updateHighlightedSearchHint}

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

@@ -3478,6 +3478,7 @@
"channel_bookmarks.editBookmarkLabel": "Bookmark menu",
"channel_bookmarks.open": "Open",
"channel_groups": "{channel} Groups",
"channel_header.back": "Back to channel",
"channel_header.channel_settings": "Channel Settings",
"channel_header.channelFiles": "Channel files",
"channel_header.channelHasGuests": "Channel has guests",
@@ -5191,6 +5192,7 @@
"search_bar.files_tab": "Files",
"search_bar.messages_tab": "Messages",
"search_bar.search": "Search",
"search_bar.search_box": "Search box",
"search_bar.search_files": "Search files",
"search_bar.search_messages": "Search messages",
"search_bar.search_types.files": "FILES",
@@ -5204,6 +5206,7 @@
"search_bar.usage.title_files": "File search options",
"search_bar.usage.title_messages": "Message search options",
"search_bar.users": "Users",
"search_box_suggestions.suggestions_readout": "{label} ({idx} of {total} results available)",
"search_file_extension_suggestion.aria_label": "{fileType} (.{extension})",
"search_files_list_option.after": "Files after a date",
"search_files_list_option.before": "Files before a date",

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

@@ -277,6 +277,8 @@
height: 48px;
align-items: center;
justify-content: center;
border: none;
background: transparent;
cursor: pointer;
text-align: center;
transform: translateX(0);