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>
Этот коммит содержится в:
Pablo Vélez
2025-01-14 08:03:42 -05:00
коммит произвёл GitHub
родитель 25a6501e10
Коммит 94ae7905ad
8 изменённых файлов: 119 добавлений и 27 удалений

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

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