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
Этот коммит содержится в:
17
e2e-tests/cypress/package-lock.json
сгенерированный
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;
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user