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
|
// # Change the focus to search results
|
||||||
cy.get('#searchContainer').within(() => {
|
cy.get('#searchContainer').within(() => {
|
||||||
cy.get('button.sidebar--right__expand').focus().tab({shift: true}).tab();
|
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}');
|
cy.get('body').type('{downarrow}{uparrow}');
|
||||||
|
|
||||||
// # Use down arrow keys and verify if results are highlighted sequentially
|
// # Use down arrow keys and verify if results are highlighted sequentially
|
||||||
for (let index = 0; index < count; index++) {
|
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
|
// * Verify search result is highlighted
|
||||||
cy.get($el).find('.post').should('have.class', 'a11y--active a11y--focused');
|
cy.get($el).find('.post').should('have.class', 'a11y--active a11y--focused');
|
||||||
cy.get('body').type('{downarrow}');
|
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
|
// # Use up arrow keys and verify if results are highlighted sequentially
|
||||||
for (let index = count; index > 0; index--) {
|
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
|
// * Verify search result is highlighted
|
||||||
cy.get($el).find('.post').should('have.class', 'a11y--active a11y--focused');
|
cy.get($el).find('.post').should('have.class', 'a11y--active a11y--focused');
|
||||||
cy.get('body').type('{uparrow}');
|
cy.get('body').type('{uparrow}');
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe('Messaging', () => {
|
|||||||
cy.get('#channelHeaderPinButton').click();
|
cy.get('#channelHeaderPinButton').click();
|
||||||
|
|
||||||
// * Verify the pinned posts (4 & 6) are added to the Pinned Posts list on the right hand side
|
// * 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
|
// # Close out of the Pinned Post side bar
|
||||||
cy.get('#searchResultsCloseButton').click();
|
cy.get('#searchResultsCloseButton').click();
|
||||||
@@ -78,11 +78,11 @@ describe('Messaging', () => {
|
|||||||
cy.get('#channelHeaderPinButton').click();
|
cy.get('#channelHeaderPinButton').click();
|
||||||
|
|
||||||
// * Verify that there are now 3 pinned messages in the right-hand-side
|
// * 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
|
// * 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('.files-or-messages-panel').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(2).get(`#postMessageText_${postId}`).and('contain', pinnedPostText);
|
||||||
|
|
||||||
// # Scroll back up to the last pinned post.
|
// # Scroll back up to the last pinned post.
|
||||||
cy.get(`#post_${postId}`).scrollIntoView();
|
cy.get(`#post_${postId}`).scrollIntoView();
|
||||||
@@ -93,10 +93,10 @@ describe('Messaging', () => {
|
|||||||
cy.get(`#unpin_post_${postId}`).click();
|
cy.get(`#unpin_post_${postId}`).click();
|
||||||
|
|
||||||
// * Right-hand-side only has 2 initially pinned posts
|
// * 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.
|
// * 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}
|
className={className}
|
||||||
role={role}
|
role={role}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
aria-labelledby='rhsPanelTitle'
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ResizableDivider
|
<ResizableDivider
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React, {useRef} from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
@@ -11,7 +11,8 @@ import {getSearchTeam} from 'selectors/rhs';
|
|||||||
|
|
||||||
import type {SearchFilterType} from 'components/search/types';
|
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 * as Keyboard from 'utils/keyboard';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
@@ -35,10 +36,16 @@ type Props = {
|
|||||||
onTeamChange: (teamId: string) => void;
|
onTeamChange: (teamId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DataSearchLiteral = typeof DataSearchTypes[keyof typeof DataSearchTypes];
|
||||||
|
|
||||||
export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
||||||
const teams = useSelector((state: GlobalState) => getMyTeams(state));
|
const teams = useSelector((state: GlobalState) => getMyTeams(state));
|
||||||
const searchTeam = useSelector((state: GlobalState) => getSearchTeam(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 === ''}];
|
const options = [{value: '', label: 'All teams', selected: searchTeam === ''}];
|
||||||
for (const team of teams) {
|
for (const team of teams) {
|
||||||
options.push({value: team.id, label: team.display_name, selected: searchTeam === team.id});
|
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);
|
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 (
|
return (
|
||||||
<div className='MessagesOrFilesSelector'>
|
<div className='MessagesOrFilesSelector'>
|
||||||
<div className='buttons-container'>
|
<div
|
||||||
|
className='buttons-container'
|
||||||
|
role='tablist'
|
||||||
|
aria-label='Messages or Files'
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => props.onChange('messages')}
|
ref={messagesTabRef}
|
||||||
onKeyDown={(e: React.KeyboardEvent<HTMLSpanElement>) => Keyboard.isKeyPressed(e, KeyCodes.ENTER) && props.onChange('messages')}
|
role='tab'
|
||||||
className={props.selected === 'messages' ? 'active tab messages-tab' : 'tab messages-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
|
<FormattedMessage
|
||||||
id='search_bar.messages_tab'
|
id='search_bar.messages_tab'
|
||||||
@@ -62,11 +121,17 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
<span className='counter'>{props.messagesCounter}</span>
|
<span className='counter'>{props.messagesCounter}</span>
|
||||||
</button>
|
</button>
|
||||||
{props.isFileAttachmentsEnabled &&
|
{props.isFileAttachmentsEnabled && (
|
||||||
<button
|
<button
|
||||||
onClick={() => props.onChange('files')}
|
ref={filesTabRef}
|
||||||
onKeyDown={(e: React.KeyboardEvent<HTMLSpanElement>) => Keyboard.isKeyPressed(e, KeyCodes.ENTER) && props.onChange('files')}
|
role='tab'
|
||||||
className={props.selected === 'files' ? 'active tab files-tab' : 'tab files-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
|
<FormattedMessage
|
||||||
id='search_bar.files_tab'
|
id='search_bar.files_tab'
|
||||||
@@ -74,7 +139,7 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
<span className='counter'>{props.filesCounter}</span>
|
<span className='counter'>{props.filesCounter}</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{props.crossTeamSearchEnabled && (
|
{props.crossTeamSearchEnabled && (
|
||||||
<div className='team-selector-container'>
|
<div className='team-selector-container'>
|
||||||
@@ -93,11 +158,12 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{props.selected === 'files' &&
|
{props.selected === DataSearchTypes.FILES_SEARCH_TYPE && (
|
||||||
<FilesFilterMenu
|
<FilesFilterMenu
|
||||||
selectedFilter={props.selectedFilter}
|
selectedFilter={props.selectedFilter}
|
||||||
onFilter={props.onFilter}
|
onFilter={props.onFilter}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,9 +363,9 @@ const SearchResults: React.FC<Props> = (props: Props): JSX.Element => {
|
|||||||
className='SearchResults sidebar-right__body'
|
className='SearchResults sidebar-right__body'
|
||||||
>
|
>
|
||||||
<SearchResultsHeader>
|
<SearchResultsHeader>
|
||||||
<span>
|
<h2 id='rhsPanelTitle'>
|
||||||
{formattedTitle}
|
{formattedTitle}
|
||||||
</span>
|
</h2>
|
||||||
{props.channelDisplayName && <div className='sidebar--right__title__channel'>{props.channelDisplayName}</div>}
|
{props.channelDisplayName && <div className='sidebar--right__title__channel'>{props.channelDisplayName}</div>}
|
||||||
</SearchResultsHeader>
|
</SearchResultsHeader>
|
||||||
{isMessagesSearch &&
|
{isMessagesSearch &&
|
||||||
@@ -425,8 +425,13 @@ const SearchResults: React.FC<Props> = (props: Props): JSX.Element => {
|
|||||||
}, {
|
}, {
|
||||||
regionTitle: formattedTitle,
|
regionTitle: formattedTitle,
|
||||||
})}
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id={`${searchType}Panel`}
|
||||||
|
className='files-or-messages-panel'
|
||||||
>
|
>
|
||||||
{contentItems}
|
{contentItems}
|
||||||
|
</div>
|
||||||
{loadingMorePostsComponent}
|
{loadingMorePostsComponent}
|
||||||
</div>
|
</div>
|
||||||
</Scrollbars>
|
</Scrollbars>
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
|||||||
<ResizableRhs
|
<ResizableRhs
|
||||||
className={containerClassName}
|
className={containerClassName}
|
||||||
id='sidebar-right'
|
id='sidebar-right'
|
||||||
role='complementary'
|
role='region'
|
||||||
rightWidthHolderRef={this.sidebarRightWidthHolder}
|
rightWidthHolderRef={this.sidebarRightWidthHolder}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -128,6 +128,13 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
@include mixins.clearfix;
|
@include mixins.clearfix;
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar--right__title__channel {
|
.sidebar--right__title__channel {
|
||||||
|
|||||||
@@ -2211,7 +2211,7 @@ export enum ClaimErrors {
|
|||||||
export const DataSearchTypes = {
|
export const DataSearchTypes = {
|
||||||
FILES_SEARCH_TYPE: 'files',
|
FILES_SEARCH_TYPE: 'files',
|
||||||
MESSAGES_SEARCH_TYPE: 'messages',
|
MESSAGES_SEARCH_TYPE: 'messages',
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export const OverActiveUserLimits = {
|
export const OverActiveUserLimits = {
|
||||||
MIN: 0.05,
|
MIN: 0.05,
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user