From 94ae7905ade0703783e4936a2b4c5ba11dedd443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20V=C3=A9lez?= Date: Tue, 14 Jan 2025 08:03:42 -0500 Subject: [PATCH] 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 --- .../accessibility_keyboard_usability_spec.js | 19 +++- .../message_pinning_unpinning_spec.js | 12 +-- .../resizable_sidebar/resizable_rhs/index.tsx | 1 + .../messages_or_files_selector.tsx | 92 ++++++++++++++++--- .../search_results/search_results.tsx | 11 ++- .../sidebar_right/sidebar_right.tsx | 2 +- .../src/sass/layout/_sidebar-right.scss | 7 ++ webapp/channels/src/utils/constants.tsx | 2 +- 8 files changed, 119 insertions(+), 27 deletions(-) diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_keyboard_usability_spec.js b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_keyboard_usability_spec.js index f5ced8aa2e..9877e57b38 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_keyboard_usability_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_keyboard_usability_spec.js @@ -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}'); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/message_pinning_unpinning_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/message_pinning_unpinning_spec.js index 0aab9f65a3..a0865275c8 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/message_pinning_unpinning_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/message_pinning_unpinning_spec.js @@ -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}`); }); }); }); diff --git a/webapp/channels/src/components/resizable_sidebar/resizable_rhs/index.tsx b/webapp/channels/src/components/resizable_sidebar/resizable_rhs/index.tsx index 32ff5051f0..94abac08d3 100644 --- a/webapp/channels/src/components/resizable_sidebar/resizable_rhs/index.tsx +++ b/webapp/channels/src/components/resizable_sidebar/resizable_rhs/index.tsx @@ -91,6 +91,7 @@ function ResizableRhs({ className={className} role={role} ref={containerRef} + aria-labelledby='rhsPanelTitle' > {children} 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(null); + const filesTabRef = useRef(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, + currentTab: DataSearchLiteral, + ) => { + if (Keyboard.isKeyPressed(e, KeyCodes.LEFT) || Keyboard.isKeyPressed(e, KeyCodes.RIGHT)) { + e.preventDefault(); + e.stopPropagation(); + let nextTab: SearchType; + let nextTabRef: React.RefObject; + + 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(A11yCustomEventTypes.FOCUS, { + detail: { + target: nextTabRef.current, + keyboardOnly: true, + }, + }), + ); + }, 0); + } + return; + } + + if (Keyboard.isKeyPressed(e, KeyCodes.ENTER)) { + props.onChange(currentTab); + } + }; + return (
-
+
- {props.isFileAttachmentsEnabled && + {props.isFileAttachmentsEnabled && ( - } + )}
{props.crossTeamSearchEnabled && (
@@ -93,11 +158,12 @@ export default function MessagesOrFilesSelector(props: Props): JSX.Element {
)} - {props.selected === 'files' && + {props.selected === DataSearchTypes.FILES_SEARCH_TYPE && ( } + /> + )}
); } diff --git a/webapp/channels/src/components/search_results/search_results.tsx b/webapp/channels/src/components/search_results/search_results.tsx index 52ff8002c8..f7cb67df03 100644 --- a/webapp/channels/src/components/search_results/search_results.tsx +++ b/webapp/channels/src/components/search_results/search_results.tsx @@ -363,9 +363,9 @@ const SearchResults: React.FC = (props: Props): JSX.Element => { className='SearchResults sidebar-right__body' > - +

{formattedTitle} - +

{props.channelDisplayName &&
{props.channelDisplayName}
}
{isMessagesSearch && @@ -426,7 +426,12 @@ const SearchResults: React.FC = (props: Props): JSX.Element => { regionTitle: formattedTitle, })} > - {contentItems} +
+ {contentItems} +
{loadingMorePostsComponent}
diff --git a/webapp/channels/src/components/sidebar_right/sidebar_right.tsx b/webapp/channels/src/components/sidebar_right/sidebar_right.tsx index 4767a8a99e..794beae433 100644 --- a/webapp/channels/src/components/sidebar_right/sidebar_right.tsx +++ b/webapp/channels/src/components/sidebar_right/sidebar_right.tsx @@ -337,7 +337,7 @@ export default class SidebarRight extends React.PureComponent {