Mm 61590 - trap focus in modals (#30622)

* MM-61590 - trap focus in modals

* Adjust focus trap for dynamic loading elements and multi modal support

* add tests and improve code in generic modal

* fix snapshot

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Pablo Vélez
2025-04-24 11:46:36 +02:00
коммит произвёл GitHub
родитель 131cf039bb
Коммит 89319cafb1
17 изменённых файлов: 740 добавлений и 392 удалений

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

@@ -3,7 +3,6 @@
exports[`components/BrowseChannels should match snapshot and state 1`] = `
<GenericModal
aria-modal={true}
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={false}
bodyPadding={false}
compassDesign={true}
@@ -32,18 +31,13 @@ exports[`components/BrowseChannels should match snapshot and state 1`] = `
</Memo(TeamPermissionGate)>
}
id="browseChannelsModal"
keyboardEscape={true}
modalHeaderText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Browse Channels"
id="more_channels.title"
/>
}
modalLocation="center"
onExited={[Function]}
show={true}
showCloseButton={true}
showHeader={true}
>
<injectIntl(SearchableChannelList)
canShowArchivedChannels={true}

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

@@ -2,13 +2,9 @@
exports[`components/drafts/draft_actions/delete_draft_modal should have called onConfirm 1`] = `
<GenericModal
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
autoFocusConfirmButton={true}
bodyPadding={true}
compassDesign={true}
confirmButtonText="Yes, delete"
enforceFocus={true}
handleCancel={[Function]}
handleConfirm={
[MockFunction] {
@@ -23,15 +19,9 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
],
}
}
id="genericModal"
isDeleteModal={true}
keyboardEscape={true}
modalHeaderText="Delete draft"
modalLocation="center"
onExited={[MockFunction]}
show={true}
showCloseButton={true}
showHeader={true}
>
<MemoizedFormattedMessage
defaultMessage="Are you sure you want to delete this draft to <strong>{displayName}</strong>?"
@@ -48,20 +38,13 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
exports[`components/drafts/draft_actions/delete_draft_modal should have called onExited 1`] = `
<GenericModal
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
autoFocusConfirmButton={true}
bodyPadding={true}
compassDesign={true}
confirmButtonText="Yes, delete"
enforceFocus={true}
handleCancel={[Function]}
handleConfirm={[MockFunction]}
id="genericModal"
isDeleteModal={true}
keyboardEscape={true}
modalHeaderText="Delete draft"
modalLocation="center"
onExited={
[MockFunction] {
"calls": Array [
@@ -75,9 +58,6 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
],
}
}
show={true}
showCloseButton={true}
showHeader={true}
>
<MemoizedFormattedMessage
defaultMessage="Are you sure you want to delete this draft to <strong>{displayName}</strong>?"

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

@@ -2,12 +2,8 @@
exports[`components/drafts/draft_actions/send_draft_modal should have called onConfirm 1`] = `
<GenericModal
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
compassDesign={true}
confirmButtonText="Yes, send now"
enforceFocus={true}
handleCancel={[Function]}
handleConfirm={
[MockFunction] {
@@ -22,14 +18,8 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onC
],
}
}
id="genericModal"
keyboardEscape={true}
modalHeaderText="Send message now"
modalLocation="center"
onExited={[MockFunction]}
show={true}
showCloseButton={true}
showHeader={true}
>
<MemoizedFormattedMessage
defaultMessage="Are you sure you want to send this message to <strong>{displayName}</strong>?"
@@ -46,18 +36,11 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onC
exports[`components/drafts/draft_actions/send_draft_modal should have called onExited 1`] = `
<GenericModal
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
compassDesign={true}
confirmButtonText="Yes, send now"
enforceFocus={true}
handleCancel={[Function]}
handleConfirm={[MockFunction]}
id="genericModal"
keyboardEscape={true}
modalHeaderText="Send message now"
modalLocation="center"
onExited={
[MockFunction] {
"calls": Array [
@@ -71,9 +54,6 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onE
],
}
}
show={true}
showCloseButton={true}
showHeader={true}
>
<MemoizedFormattedMessage
defaultMessage="Are you sure you want to send this message to <strong>{displayName}</strong>?"

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

@@ -499,21 +499,13 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
>
<GenericModal
ariaLabelledby="confirmModalLabel"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
className="ConfirmModal a11y__modal "
compassDesign={true}
enforceFocus={true}
id="confirmModal"
keyboardEscape={true}
modalHeaderText="Save Outgoing OAuth Connection"
modalLocation="center"
onExited={[Function]}
onHide={[Function]}
show={false}
showCloseButton={true}
showHeader={true}
>
<Modal
animation={true}
@@ -1103,21 +1095,13 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
>
<GenericModal
ariaLabelledby="confirmModalLabel"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
className="ConfirmModal a11y__modal "
compassDesign={true}
enforceFocus={true}
id="confirmModal"
keyboardEscape={true}
modalHeaderText="Save Outgoing OAuth Connection"
modalLocation="center"
onExited={[Function]}
onHide={[Function]}
show={false}
showCloseButton={true}
showHeader={true}
>
<Modal
animation={true}

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

@@ -488,21 +488,13 @@ exports[`components/integrations/AddOutgoingOAuthConnection should match snapsho
>
<GenericModal
ariaLabelledby="confirmModalLabel"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
className="ConfirmModal a11y__modal "
compassDesign={true}
enforceFocus={true}
id="confirmModal"
keyboardEscape={true}
modalHeaderText="Save Outgoing OAuth Connection"
modalLocation="center"
onExited={[Function]}
onHide={[Function]}
show={false}
showCloseButton={true}
showHeader={true}
>
<Modal
animation={true}

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

@@ -550,26 +550,18 @@ https://myothersite.com/api/v2"
>
<GenericModal
ariaLabelledby="confirmModalLabel"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
className="ConfirmModal a11y__modal integrations-backstage-modal"
compassDesign={true}
enforceFocus={true}
id="confirmModal"
keyboardEscape={true}
modalHeaderText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit Outgoing OAuth Connection"
id="update_outgoing_oauth_connection.confirm"
/>
}
modalLocation="center"
onExited={[Function]}
onHide={[Function]}
show={false}
showCloseButton={true}
showHeader={true}
>
<Modal
animation={true}
@@ -673,21 +665,13 @@ https://myothersite.com/api/v2"
>
<GenericModal
ariaLabelledby="confirmModalLabel"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
className="ConfirmModal a11y__modal "
compassDesign={true}
enforceFocus={true}
id="confirmModal"
keyboardEscape={true}
modalHeaderText="Save Outgoing OAuth Connection"
modalLocation="center"
onExited={[Function]}
onHide={[Function]}
show={false}
showCloseButton={true}
showHeader={true}
>
<Modal
animation={true}
@@ -1310,26 +1294,18 @@ https://myothersite.com/api/v2"
>
<GenericModal
ariaLabelledby="confirmModalLabel"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
className="ConfirmModal a11y__modal integrations-backstage-modal"
compassDesign={true}
enforceFocus={true}
id="confirmModal"
keyboardEscape={true}
modalHeaderText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit Outgoing OAuth Connection"
id="update_outgoing_oauth_connection.confirm"
/>
}
modalLocation="center"
onExited={[Function]}
onHide={[Function]}
show={false}
showCloseButton={true}
showHeader={true}
>
<Modal
animation={true}
@@ -1433,21 +1409,13 @@ https://myothersite.com/api/v2"
>
<GenericModal
ariaLabelledby="confirmModalLabel"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
className="ConfirmModal a11y__modal "
compassDesign={true}
enforceFocus={true}
id="confirmModal"
keyboardEscape={true}
modalHeaderText="Save Outgoing OAuth Connection"
modalLocation="center"
onExited={[Function]}
onHide={[Function]}
show={false}
showCloseButton={true}
showHeader={true}
>
<Modal
animation={true}
@@ -2069,26 +2037,18 @@ https://myothersite.com/api/v2"
>
<GenericModal
ariaLabelledby="confirmModalLabel"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
className="ConfirmModal a11y__modal integrations-backstage-modal"
compassDesign={true}
enforceFocus={true}
id="confirmModal"
keyboardEscape={true}
modalHeaderText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit Outgoing OAuth Connection"
id="update_outgoing_oauth_connection.confirm"
/>
}
modalLocation="center"
onExited={[Function]}
onHide={[Function]}
show={false}
showCloseButton={true}
showHeader={true}
>
<Modal
animation={true}
@@ -2192,21 +2152,13 @@ https://myothersite.com/api/v2"
>
<GenericModal
ariaLabelledby="confirmModalLabel"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={true}
className="ConfirmModal a11y__modal "
compassDesign={true}
enforceFocus={true}
id="confirmModal"
keyboardEscape={true}
modalHeaderText="Save Outgoing OAuth Connection"
modalLocation="center"
onExited={[Function]}
onHide={[Function]}
show={false}
showCloseButton={true}
showHeader={true}
>
<Modal
animation={true}

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

@@ -2,14 +2,11 @@
exports[`components/MoreDirectChannels should exclude deleted users if there is not direct channel between users 1`] = `
<GenericModal
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={false}
className="a11y__modal more-modal more-direct-channels more-direct-channels-generic-modal"
compassDesign={true}
enforceFocus={true}
delayFocusTrap={true}
id="moreDmModal"
keyboardEscape={true}
modalHeaderText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Direct Messages"
@@ -21,8 +18,6 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
onExited={[Function]}
onHide={[Function]}
show={true}
showCloseButton={true}
showHeader={true}
>
<div
role="application"
@@ -251,14 +246,11 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
exports[`components/MoreDirectChannels should match snapshot 1`] = `
<GenericModal
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={false}
className="a11y__modal more-modal more-direct-channels more-direct-channels-generic-modal"
compassDesign={true}
enforceFocus={true}
delayFocusTrap={true}
id="moreDmModal"
keyboardEscape={true}
modalHeaderText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Direct Messages"
@@ -270,8 +262,6 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
onExited={[Function]}
onHide={[Function]}
show={true}
showCloseButton={true}
showHeader={true}
>
<div
role="application"

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

@@ -296,6 +296,7 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
bodyPadding={false}
onEntered={this.loadModalData}
modalLocation={'top'}
delayFocusTrap={true}
>
<div role='application'>
{body}

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

@@ -225,7 +225,6 @@ export default class MultiSelectList<T extends Value> extends React.PureComponen
id='multiSelectList'
className='more-modal__options'
role='presentation'
aria-hidden={true}
>
{optionControls}
</div>

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

@@ -4,7 +4,6 @@ exports[`ProductNoticesModal Match snapshot for single notice 1`] = `
<GenericModal
autoCloseOnCancelButton={false}
autoCloseOnConfirmButton={true}
bodyPadding={true}
cancelButtonText={null}
className="productNotices"
compassDesign={true}
@@ -13,21 +12,14 @@ exports[`ProductNoticesModal Match snapshot for single notice 1`] = `
Download
</span>
}
enforceFocus={true}
handleConfirm={[Function]}
handleEnterKeyPress={[Function]}
id="genericModal"
keyboardEscape={true}
modalHeaderText={
<span>
title
</span>
}
modalLocation="center"
onExited={[Function]}
show={true}
showCloseButton={true}
showHeader={true}
>
<span
className="productNotices__helpText"
@@ -49,7 +41,6 @@ exports[`ProductNoticesModal Match snapshot for user notice 1`] = `
<GenericModal
autoCloseOnCancelButton={false}
autoCloseOnConfirmButton={true}
bodyPadding={true}
cancelButtonText={
<React.Fragment>
<Memo(PreviousIcon) />
@@ -67,22 +58,15 @@ exports[`ProductNoticesModal Match snapshot for user notice 1`] = `
id="generic.done"
/>
}
enforceFocus={true}
handleCancel={[Function]}
handleConfirm={[Function]}
handleEnterKeyPress={[Function]}
id="genericModal"
keyboardEscape={true}
modalHeaderText={
<span>
title
</span>
}
modalLocation="center"
onExited={[Function]}
show={true}
showCloseButton={true}
showHeader={true}
>
<span
className="productNotices__helpText"
@@ -130,7 +114,6 @@ exports[`ProductNoticesModal Should match snapshot for system admin notice 1`] =
<GenericModal
autoCloseOnCancelButton={false}
autoCloseOnConfirmButton={false}
bodyPadding={true}
cancelButtonText={null}
className="productNotices"
compassDesign={true}
@@ -143,21 +126,14 @@ exports[`ProductNoticesModal Should match snapshot for system admin notice 1`] =
<Memo(NextIcon) />
</React.Fragment>
}
enforceFocus={true}
handleConfirm={[Function]}
handleEnterKeyPress={[Function]}
id="genericModal"
keyboardEscape={true}
modalHeaderText={
<span>
for sysadmin
</span>
}
modalLocation="center"
onExited={[Function]}
show={true}
showCloseButton={true}
showHeader={true}
>
<span
className="productNotices__helpText"

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

@@ -3,14 +3,11 @@
exports[`components/QuickSwitchModal should match snapshot 1`] = `
<GenericModal
ariaLabel="Find Channels"
autoCloseOnCancelButton={true}
autoCloseOnConfirmButton={true}
bodyPadding={false}
className="a11y__modal channel-switcher"
compassDesign={true}
enforceFocus={false}
id="quickSwitchModal"
keyboardEscape={true}
modalHeaderText={
<div
className="channel-switcher__header"
@@ -25,7 +22,6 @@ exports[`components/QuickSwitchModal should match snapshot 1`] = `
</h2>
</div>
}
modalLocation="center"
modalSubheaderText={
<div
className="channel-switcher__hint"
@@ -45,8 +41,6 @@ exports[`components/QuickSwitchModal should match snapshot 1`] = `
onExited={[Function]}
onHide={[Function]}
show={true}
showCloseButton={true}
showHeader={true}
>
<div
className="channel-switcher__suggestion-box"

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

@@ -9,7 +9,7 @@ import QuickSwitchModal from 'components/quick_switch_modal/quick_switch_modal';
import ChannelNavigator from 'components/sidebar/channel_navigator/channel_navigator';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {act, renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
describe('components/QuickSwitchModal', () => {
@@ -149,7 +149,7 @@ describe('components/QuickSwitchModal', () => {
});
describe('accessibility', () => {
it('should restore focus to button', () => {
it('should restore focus to button', async () => {
const channelNavigatorProps = {
showUnreadsCategory: false,
isQuickSwitcherOpen: false,
@@ -168,8 +168,10 @@ describe('components/QuickSwitchModal', () => {
</IntlProvider>,
);
userEvent.click(screen.getByTestId('SidebarChannelNavigatorButton'));
userEvent.keyboard('{escape}');
await act(async () => {
userEvent.click(await screen.getByTestId('SidebarChannelNavigatorButton'));
userEvent.keyboard('{escape}');
});
expect(screen.getByTestId('SidebarChannelNavigatorButton')).toHaveFocus();
});
});

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

@@ -33,8 +33,10 @@ describe('components/TeamGroupsManageModal', () => {
const wrapper = renderWithContext(<TeamGroupsManageModal {...baseProps}/>);
expect(await wrapper.findByTestId('group-name')).toBeInTheDocument();
userEvent.click(wrapper.getByTestId('menu-button'));
userEvent.click(wrapper.getByTestId('remove-group-button'));
expect(wrapper.getByTestId('confirm-modal')).toBeInTheDocument();
expect(await wrapper.findByTestId('confirm-modal')).toBeInTheDocument();
});
test('should call loadItems on render', async () => {
@@ -44,10 +46,12 @@ describe('components/TeamGroupsManageModal', () => {
test('should hide confirm modal when cancel button is clicked', async () => {
const wrapper = renderWithContext(<TeamGroupsManageModal {...baseProps}/>);
await wrapper.findByTestId('group-name');
expect(await wrapper.findByTestId('group-name')).toBeInTheDocument();
userEvent.click(wrapper.getByTestId('menu-button'));
userEvent.click(wrapper.getByTestId('remove-group-button'));
expect(wrapper.getByTestId('confirm-modal')).toBeInTheDocument();
expect(await wrapper.findByTestId('confirm-modal')).toBeInTheDocument();
userEvent.click(wrapper.getByTestId('cancel-button'));
await waitForElementToBeRemoved(() => wrapper.queryByTestId('confirm-modal'));
expect(wrapper.queryByTestId('confirm-modal')).toBeNull();

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

@@ -368,6 +368,7 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
bodyPadding={false}
modalHeaderText={headerTitle}
modalLocation='top'
delayFocusTrap={true}
>
<div
ref={this.modalBodyRef}