MM-61576 - add a11y to messages and files results tabs (#29740)
* MM-61576 - add a11y to messages and files results tabs * add coverage for left-right arrow focus tab and fix broken test * add the proper role and label to RHS - MM-61615 * fix linter * fix e2e tests --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -91,6 +91,7 @@ function ResizableRhs({
|
||||
className={className}
|
||||
role={role}
|
||||
ref={containerRef}
|
||||
aria-labelledby='rhsPanelTitle'
|
||||
>
|
||||
{children}
|
||||
<ResizableDivider
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useRef} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
@@ -11,7 +11,8 @@ import {getSearchTeam} from 'selectors/rhs';
|
||||
|
||||
import type {SearchFilterType} from 'components/search/types';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import Constants, {A11yCustomEventTypes, DataSearchTypes} from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
@@ -35,10 +36,16 @@ type Props = {
|
||||
onTeamChange: (teamId: string) => void;
|
||||
};
|
||||
|
||||
type DataSearchLiteral = typeof DataSearchTypes[keyof typeof DataSearchTypes];
|
||||
|
||||
export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||
const teams = useSelector((state: GlobalState) => getMyTeams(state));
|
||||
const searchTeam = useSelector((state: GlobalState) => getSearchTeam(state));
|
||||
|
||||
// REFS to the tabs so there is ability to pass the custom A11y focus event
|
||||
const messagesTabRef = useRef<HTMLButtonElement>(null);
|
||||
const filesTabRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const options = [{value: '', label: 'All teams', selected: searchTeam === ''}];
|
||||
for (const team of teams) {
|
||||
options.push({value: team.id, label: team.display_name, selected: searchTeam === team.id});
|
||||
@@ -48,13 +55,65 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||
props.onTeamChange(e.target.value);
|
||||
};
|
||||
|
||||
// Enhanced arrow key handling to focus the new select tab and also send the a11y custom event
|
||||
const handleTabKeyDown = (
|
||||
e: React.KeyboardEvent<HTMLButtonElement>,
|
||||
currentTab: DataSearchLiteral,
|
||||
) => {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.LEFT) || Keyboard.isKeyPressed(e, KeyCodes.RIGHT)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let nextTab: SearchType;
|
||||
let nextTabRef: React.RefObject<HTMLButtonElement>;
|
||||
|
||||
if (currentTab === DataSearchTypes.MESSAGES_SEARCH_TYPE && props.isFileAttachmentsEnabled) {
|
||||
nextTab = DataSearchTypes.FILES_SEARCH_TYPE;
|
||||
nextTabRef = filesTabRef;
|
||||
} else {
|
||||
nextTab = DataSearchTypes.MESSAGES_SEARCH_TYPE;
|
||||
nextTabRef = messagesTabRef;
|
||||
}
|
||||
|
||||
props.onChange(nextTab);
|
||||
|
||||
// Dispatch the custom a11y focus event to focus the selected tab
|
||||
if (nextTabRef.current) {
|
||||
setTimeout(() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<A11yFocusEventDetail>(A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: nextTabRef.current,
|
||||
keyboardOnly: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.ENTER)) {
|
||||
props.onChange(currentTab);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='MessagesOrFilesSelector'>
|
||||
<div className='buttons-container'>
|
||||
<div
|
||||
className='buttons-container'
|
||||
role='tablist'
|
||||
aria-label='Messages or Files'
|
||||
>
|
||||
<button
|
||||
onClick={() => props.onChange('messages')}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLSpanElement>) => Keyboard.isKeyPressed(e, KeyCodes.ENTER) && props.onChange('messages')}
|
||||
className={props.selected === 'messages' ? 'active tab messages-tab' : 'tab messages-tab'}
|
||||
ref={messagesTabRef}
|
||||
role='tab'
|
||||
aria-selected={props.selected === DataSearchTypes.MESSAGES_SEARCH_TYPE ? 'true' : 'false'}
|
||||
tabIndex={props.selected === DataSearchTypes.MESSAGES_SEARCH_TYPE ? 0 : -1}
|
||||
aria-controls='messagesPanel'
|
||||
id='messagesTab'
|
||||
onClick={() => props.onChange(DataSearchTypes.MESSAGES_SEARCH_TYPE)}
|
||||
onKeyDown={(e) => handleTabKeyDown(e, DataSearchTypes.MESSAGES_SEARCH_TYPE)}
|
||||
className={props.selected === DataSearchTypes.MESSAGES_SEARCH_TYPE ? 'active tab messages-tab' : 'tab messages-tab'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='search_bar.messages_tab'
|
||||
@@ -62,11 +121,17 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||
/>
|
||||
<span className='counter'>{props.messagesCounter}</span>
|
||||
</button>
|
||||
{props.isFileAttachmentsEnabled &&
|
||||
{props.isFileAttachmentsEnabled && (
|
||||
<button
|
||||
onClick={() => props.onChange('files')}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLSpanElement>) => Keyboard.isKeyPressed(e, KeyCodes.ENTER) && props.onChange('files')}
|
||||
className={props.selected === 'files' ? 'active tab files-tab' : 'tab files-tab'}
|
||||
ref={filesTabRef}
|
||||
role='tab'
|
||||
aria-selected={props.selected === DataSearchTypes.FILES_SEARCH_TYPE ? 'true' : 'false'}
|
||||
tabIndex={props.selected === DataSearchTypes.FILES_SEARCH_TYPE ? 0 : -1}
|
||||
aria-controls='filesPanel'
|
||||
id='filesTab'
|
||||
onClick={() => props.onChange(DataSearchTypes.FILES_SEARCH_TYPE)}
|
||||
onKeyDown={(e) => handleTabKeyDown(e, DataSearchTypes.FILES_SEARCH_TYPE)}
|
||||
className={props.selected === DataSearchTypes.FILES_SEARCH_TYPE ? 'active tab files-tab' : 'tab files-tab'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='search_bar.files_tab'
|
||||
@@ -74,7 +139,7 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||
/>
|
||||
<span className='counter'>{props.filesCounter}</span>
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{props.crossTeamSearchEnabled && (
|
||||
<div className='team-selector-container'>
|
||||
@@ -93,11 +158,12 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{props.selected === 'files' &&
|
||||
{props.selected === DataSearchTypes.FILES_SEARCH_TYPE && (
|
||||
<FilesFilterMenu
|
||||
selectedFilter={props.selectedFilter}
|
||||
onFilter={props.onFilter}
|
||||
/>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -363,9 +363,9 @@ const SearchResults: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
className='SearchResults sidebar-right__body'
|
||||
>
|
||||
<SearchResultsHeader>
|
||||
<span>
|
||||
<h2 id='rhsPanelTitle'>
|
||||
{formattedTitle}
|
||||
</span>
|
||||
</h2>
|
||||
{props.channelDisplayName && <div className='sidebar--right__title__channel'>{props.channelDisplayName}</div>}
|
||||
</SearchResultsHeader>
|
||||
{isMessagesSearch &&
|
||||
@@ -426,7 +426,12 @@ const SearchResults: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
regionTitle: formattedTitle,
|
||||
})}
|
||||
>
|
||||
{contentItems}
|
||||
<div
|
||||
id={`${searchType}Panel`}
|
||||
className='files-or-messages-panel'
|
||||
>
|
||||
{contentItems}
|
||||
</div>
|
||||
{loadingMorePostsComponent}
|
||||
</div>
|
||||
</Scrollbars>
|
||||
|
||||
@@ -337,7 +337,7 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
||||
<ResizableRhs
|
||||
className={containerClassName}
|
||||
id='sidebar-right'
|
||||
role='complementary'
|
||||
role='region'
|
||||
rightWidthHolderRef={this.sidebarRightWidthHolder}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -128,6 +128,13 @@
|
||||
line-height: 1;
|
||||
|
||||
@include mixins.clearfix;
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar--right__title__channel {
|
||||
|
||||
@@ -2211,7 +2211,7 @@ export enum ClaimErrors {
|
||||
export const DataSearchTypes = {
|
||||
FILES_SEARCH_TYPE: 'files',
|
||||
MESSAGES_SEARCH_TYPE: 'messages',
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const OverActiveUserLimits = {
|
||||
MIN: 0.05,
|
||||
|
||||
Ссылка в новой задаче
Block a user