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
Этот коммит содержится в:
Pablo Vélez
2025-03-12 23:10:07 +01:00
коммит произвёл GitHub
родитель cc92ee79c9
Коммит 2102391672
7 изменённых файлов: 73 добавлений и 18 удалений

17
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",

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

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

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

@@ -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');
});
});

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

@@ -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';

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

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

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

@@ -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<Props, State> {
// 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<Props, State> {
} else {
setTimeout(() => {
if (this.previousActiveElement) {
focusElement(this.previousActiveElement, false);
focusElement(this.previousActiveElement, true);
this.previousActiveElement = null;
}
}, 0);

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

@@ -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:
* - <a href="...">
* - <button>, <input>, <select>, <textarea> (unless disabled)
* - Elements with a non-negative tabindex.
*/
export function getFirstFocusableChild(container: HTMLElement): HTMLElement | null {
if (!container) {
return null;
}
// Common selectors for focusable elements:
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
];
// Use querySelector to find the first match
const focusable = container.querySelector(focusableSelectors.join(', ')) as HTMLElement | null;
return focusable || null;
}