[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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e367872c0b
Коммит
df1b278f62
@@ -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);
|
||||
|
||||
Ссылка в новой задаче
Block a user