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>
Этот коммит содержится в:
@@ -58,13 +58,26 @@ describe('Verify Accessibility keyboard usability across different regions in th
|
||||
// # Change the focus to search results
|
||||
cy.get('#searchContainer').within(() => {
|
||||
cy.get('button.sidebar--right__expand').focus().tab({shift: true}).tab();
|
||||
cy.focused().tab().tab().tab().tab();
|
||||
|
||||
// # focus the messages and files tabs
|
||||
cy.focused().tab().tab();
|
||||
});
|
||||
|
||||
// # verify the tabs buttons files or messages are accesible via left and right keys
|
||||
cy.get('body').type('{rightarrow}').wait(100);
|
||||
cy.get('#filesTab').should('have.class', 'a11y--active a11y--focused');
|
||||
|
||||
cy.get('body').type('{leftarrow}').wait(100);
|
||||
cy.get('#messagesTab').should('have.class', 'a11y--active a11y--focused');
|
||||
|
||||
// # move focus to search results items
|
||||
cy.focused().tab();
|
||||
|
||||
cy.get('body').type('{downarrow}{uparrow}');
|
||||
|
||||
// # Use down arrow keys and verify if results are highlighted sequentially
|
||||
for (let index = 0; index < count; index++) {
|
||||
cy.get('#search-items-container').children('.search-item__container').eq(index).then(($el) => {
|
||||
cy.get('.files-or-messages-panel').children('.search-item__container').eq(index).then(($el) => {
|
||||
// * Verify search result is highlighted
|
||||
cy.get($el).find('.post').should('have.class', 'a11y--active a11y--focused');
|
||||
cy.get('body').type('{downarrow}');
|
||||
@@ -73,7 +86,7 @@ describe('Verify Accessibility keyboard usability across different regions in th
|
||||
|
||||
// # Use up arrow keys and verify if results are highlighted sequentially
|
||||
for (let index = count; index > 0; index--) {
|
||||
cy.get('#search-items-container').children('.search-item__container').eq(index).then(($el) => {
|
||||
cy.get('.files-or-messages-panel').children('.search-item__container').eq(index).then(($el) => {
|
||||
// * Verify search result is highlighted
|
||||
cy.get($el).find('.post').should('have.class', 'a11y--active a11y--focused');
|
||||
cy.get('body').type('{uparrow}');
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('Messaging', () => {
|
||||
cy.get('#channelHeaderPinButton').click();
|
||||
|
||||
// * Verify the pinned posts (4 & 6) are added to the Pinned Posts list on the right hand side
|
||||
cy.get('#search-items-container').children().should('have.length', 2);
|
||||
cy.get('.files-or-messages-panel').children().should('have.length', 2);
|
||||
|
||||
// # Close out of the Pinned Post side bar
|
||||
cy.get('#searchResultsCloseButton').click();
|
||||
@@ -78,11 +78,11 @@ describe('Messaging', () => {
|
||||
cy.get('#channelHeaderPinButton').click();
|
||||
|
||||
// * Verify that there are now 3 pinned messages in the right-hand-side
|
||||
cy.get('#search-items-container').children().should('have.length', 3);
|
||||
cy.get('.files-or-messages-panel').children().should('have.length', 3);
|
||||
|
||||
// * Verify sorted by newest at top: 1st post is the newest, and 3rd post is the oldest
|
||||
cy.get('#search-items-container').children().eq(0).get(`#postMessageText_${lastPostId}`);
|
||||
cy.get('#search-items-container').children().eq(2).get(`#postMessageText_${postId}`).and('contain', pinnedPostText);
|
||||
cy.get('.files-or-messages-panel').children().eq(0).get(`#postMessageText_${lastPostId}`);
|
||||
cy.get('.files-or-messages-panel').children().eq(2).get(`#postMessageText_${postId}`).and('contain', pinnedPostText);
|
||||
|
||||
// # Scroll back up to the last pinned post.
|
||||
cy.get(`#post_${postId}`).scrollIntoView();
|
||||
@@ -93,10 +93,10 @@ describe('Messaging', () => {
|
||||
cy.get(`#unpin_post_${postId}`).click();
|
||||
|
||||
// * Right-hand-side only has 2 initially pinned posts
|
||||
cy.get('#search-items-container').children().should('have.length', 2);
|
||||
cy.get('.files-or-messages-panel').children().should('have.length', 2);
|
||||
|
||||
// * Right-hand-side does not have the last pinned post.
|
||||
cy.get('#search-items-container').children().should('not.contain', `#rhsPostMessageText_${postId}`);
|
||||
cy.get('.files-or-messages-panel').children().should('not.contain', `#rhsPostMessageText_${postId}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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