From 2102391672d7c09c4e3c97731d291c0de179710e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20V=C3=A9lez?= Date: Wed, 12 Mar 2025 23:10:07 +0100 Subject: [PATCH] MM-61566 - focus first focusable element (#30294) * MM-61566 - focus first focusable element * adjust e2e tests to new focused element * adjust real-events library to use with cypress --- e2e-tests/cypress/package-lock.json | 17 +++++++++++ e2e-tests/cypress/package.json | 1 + .../accessibility_sidebar_spec.ts | 26 ++++++++--------- e2e-tests/cypress/tests/support/index.js | 1 + e2e-tests/cypress/tsconfig.json | 2 +- .../sidebar_right/sidebar_right.tsx | 15 ++++++++-- webapp/channels/src/utils/a11y_utils.ts | 29 +++++++++++++++++++ 7 files changed, 73 insertions(+), 18 deletions(-) diff --git a/e2e-tests/cypress/package-lock.json b/e2e-tests/cypress/package-lock.json index 3edc22da5b..60d02e95e4 100644 --- a/e2e-tests/cypress/package-lock.json +++ b/e2e-tests/cypress/package-lock.json @@ -42,6 +42,7 @@ "cypress-file-upload": "5.0.8", "cypress-multi-reporters": "1.6.4", "cypress-plugin-tab": "1.0.5", + "cypress-real-events": "1.14.0", "cypress-wait-until": "3.0.1", "dayjs": "1.11.10", "deepmerge": "4.3.1", @@ -4611,6 +4612,15 @@ "ally.js": "^1.4.1" } }, + "node_modules/cypress-real-events": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/cypress-real-events/-/cypress-real-events-1.14.0.tgz", + "integrity": "sha512-XmI8y3OZLh6cjRroPalzzS++iv+pGCaD9G9kfIbtspgv7GVsDt30dkZvSXfgZb4rAN+3pOkMVB7e0j4oXydW7Q==", + "dev": true, + "peerDependencies": { + "cypress": "^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x || ^14.x" + } + }, "node_modules/cypress-wait-until": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-3.0.1.tgz", @@ -14931,6 +14941,13 @@ "ally.js": "^1.4.1" } }, + "cypress-real-events": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/cypress-real-events/-/cypress-real-events-1.14.0.tgz", + "integrity": "sha512-XmI8y3OZLh6cjRroPalzzS++iv+pGCaD9G9kfIbtspgv7GVsDt30dkZvSXfgZb4rAN+3pOkMVB7e0j4oXydW7Q==", + "dev": true, + "requires": {} + }, "cypress-wait-until": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-3.0.1.tgz", diff --git a/e2e-tests/cypress/package.json b/e2e-tests/cypress/package.json index 1c7f8c07a8..08407017ca 100644 --- a/e2e-tests/cypress/package.json +++ b/e2e-tests/cypress/package.json @@ -37,6 +37,7 @@ "cypress-file-upload": "5.0.8", "cypress-multi-reporters": "1.6.4", "cypress-plugin-tab": "1.0.5", + "cypress-real-events": "1.14.0", "cypress-wait-until": "3.0.1", "dayjs": "1.11.10", "deepmerge": "4.3.1", diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_sidebar_spec.ts b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_sidebar_spec.ts index ed3c33edd3..4737388ad2 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_sidebar_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_sidebar_spec.ts @@ -192,51 +192,49 @@ describe('Accessibility tests for RHS getting focus after buttons actions', () = }); it('Focus should be on RHS when opening Recent Mentions', () => { - // # Click the Recent Mentions button - cy.findByRole('button', {name: /Recent mentions/i}).click(); + // # Enter key press on the Recent Mentions button + cy.findByRole('button', {name: /Recent mentions/i}).focus().realPress('Enter'); // * Verify RHS is open cy.get('#sidebar-right').should('be.visible'); // * Check that the RHS container is focused - cy.get('.sidebar-right-container').should('be.focused'); + cy.findByLabelText('Expand Sidebar Icon').should('be.focused'); }); it('Focus should be on RHS when opening Saved Messages', () => { - // # Click the Saved Messages button - cy.findByRole('button', {name: /Saved messages/i}).click(); + // # Enter key press on the Saved Messages button + cy.findByRole('button', {name: /Saved messages/i}).focus().realPress('Enter'); // * Verify RHS is open cy.get('#sidebar-right').should('be.visible'); // * Check that the RHS container is focused - cy.get('.sidebar-right-container').should('be.focused'); + cy.findByLabelText('Expand Sidebar Icon').should('be.focused'); }); it('Focus should be on RHS when opening Members', () => { cy.get('#channelHeaderInfo').should('exist'); - // # Click the Members button - cy.get('#member_rhs'). - should('be.visible'). - click(); + // # Enter key press on the Members button + cy.get('#member_rhs').should('be.visible').focus().realPress('Enter'); // * Verify RHS is open cy.get('#sidebar-right').should('be.visible'); // * Check that the RHS container is focused - cy.get('.sidebar-right-container').should('be.focused'); + cy.get('.sidebar-right-container').find('#rhsCloseButton').should('be.focused'); }); it('Focus should be on RHS when opening Channel files', () => { - // # Click the Channel files button - cy.findByRole('button', {name: /Channel files/i}).click(); + // # Enter key press on the Channel files button + cy.findByRole('button', {name: /Channel files/i}).focus().realPress('Enter'); // * Verify RHS is open cy.get('#sidebar-right').should('be.visible'); // * Check that the RHS container is focused - cy.get('.sidebar-right-container').should('be.focused'); + cy.findByLabelText('Expand Sidebar Icon').should('be.focused'); }); }); diff --git a/e2e-tests/cypress/tests/support/index.js b/e2e-tests/cypress/tests/support/index.js index 832b442ca3..b93186f9d3 100644 --- a/e2e-tests/cypress/tests/support/index.js +++ b/e2e-tests/cypress/tests/support/index.js @@ -14,6 +14,7 @@ import '@testing-library/cypress/add-commands'; import 'cypress-file-upload'; import 'cypress-wait-until'; import 'cypress-plugin-tab'; +import 'cypress-real-events'; import addContext from 'mochawesome/addContext'; import './api'; diff --git a/e2e-tests/cypress/tsconfig.json b/e2e-tests/cypress/tsconfig.json index 77cff92343..9de88dcc48 100644 --- a/e2e-tests/cypress/tsconfig.json +++ b/e2e-tests/cypress/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "esnext", "lib": ["esnext", "dom"], - "types": ["cypress", "cypress-wait-until", "@testing-library/cypress", "node"], + "types": ["cypress", "cypress-wait-until", "cypress-real-events", "@testing-library/cypress", "node"], "esModuleInterop": true, "resolveJsonModule": true, "moduleResolution": "node", diff --git a/webapp/channels/src/components/sidebar_right/sidebar_right.tsx b/webapp/channels/src/components/sidebar_right/sidebar_right.tsx index 77a03aa618..141be221cd 100644 --- a/webapp/channels/src/components/sidebar_right/sidebar_right.tsx +++ b/webapp/channels/src/components/sidebar_right/sidebar_right.tsx @@ -23,7 +23,7 @@ import Search from 'components/search/index'; import RhsPlugin from 'plugins/rhs_plugin'; import a11yController from 'utils/a11y_controller_instance'; -import {focusElement} from 'utils/a11y_utils'; +import {focusElement, getFirstFocusableChild} from 'utils/a11y_utils'; import Constants from 'utils/constants'; import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard'; import {isMac} from 'utils/user_agent'; @@ -163,7 +163,16 @@ export default class SidebarRight extends React.PureComponent { // Focus the sidebar after a tick setTimeout(() => { if (this.sidebarRight.current) { - focusElement(this.sidebarRight, false); + const rhsContainer = this.sidebarRight.current.querySelector('#rhsContainer') as HTMLElement; + const searchContainer = this.sidebarRight.current.querySelector('#searchContainer') as HTMLElement; + if (rhsContainer || searchContainer) { + const firstFocusable = getFirstFocusableChild(rhsContainer || searchContainer); + focusElement(firstFocusable || rhsContainer, true); + } else { + // Fallback: if rhsContainer isn't found, use sidebarRight.current directly. + const firstFocusable = getFirstFocusableChild(this.sidebarRight.current); + focusElement(firstFocusable || this.sidebarRight.current, true); + } } }, 0); } else if (!this.props.isOpen && wasOpen) { @@ -173,7 +182,7 @@ export default class SidebarRight extends React.PureComponent { } else { setTimeout(() => { if (this.previousActiveElement) { - focusElement(this.previousActiveElement, false); + focusElement(this.previousActiveElement, true); this.previousActiveElement = null; } }, 0); diff --git a/webapp/channels/src/utils/a11y_utils.ts b/webapp/channels/src/utils/a11y_utils.ts index bb1e29229f..fd4c3ca305 100644 --- a/webapp/channels/src/utils/a11y_utils.ts +++ b/webapp/channels/src/utils/a11y_utils.ts @@ -60,3 +60,32 @@ export function focusElement( }, 0); } } + +/** + * Returns the first focusable child within a given container element, + * or null if none is found. + * + * Focusable elements generally include: + * - + * -