mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
Mm 62677 - modal focus management - find channels modal (#29957)
* MM-62312 - modal focus management; revamp quick switch channel modal! * get quick switch test working * configure the generic modal to accept refs to focus within and onhide to the origin element * apply pr feedback, get modal element get autofocus, use id instead of ref * update more direct channels modal to use generic modal * fix unit tests and snapshots * fix unit tests * fix modal margin top to fit in smaller screens * fix e2e test * remove unnecesary onexited extra call * fix e2e tests * set correct label * fix snapshots * create helper function for sending custom focus event * migrate quick switch modal to use new approach to focus * migrate more direct channels modal to new approach * fix snapshots * fix types * fix modal closing behavior * fix snapshots * fix cypress tests * remove only --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
fd356b62b4
commit
9e47f2ef0c
35 changed files with 450 additions and 495 deletions
|
|
@ -64,7 +64,7 @@ describe('Archive channel header spec', () => {
|
|||
cy.get('#channelArchiveChannel').should('be.visible');
|
||||
|
||||
// * Add members menu option should be visible;
|
||||
cy.get('#channelAddMembers').should('be.visible');
|
||||
cy.get('#channelInviteMembers').should('be.visible');
|
||||
|
||||
// * Notification preferences option should be visible;
|
||||
cy.get('#channelNotificationPreferences').should('be.visible');
|
||||
|
|
@ -91,7 +91,7 @@ describe('Archive channel header spec', () => {
|
|||
cy.get('#channelArchiveChannel').should('not.exist');
|
||||
|
||||
// * Add members menu option should not be visible;
|
||||
cy.get('#channelAddMembers').should('not.exist');
|
||||
cy.get('#channelInviteMembers').should('not.exist');
|
||||
|
||||
// * Notification preferences option should not be visible;
|
||||
cy.get('#channelNotificationPreferences').should('not.exist');
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ describe('Managing bots in Teams and Channels', () => {
|
|||
await client.addToTeam(team.id, bot.user_id);
|
||||
|
||||
// # Add bot to channel in team
|
||||
cy.uiAddUsersToCurrentChannel([bot.username]);
|
||||
cy.uiInviteUsersToCurrentChannel([bot.username]);
|
||||
|
||||
// * Verify system message in-channel
|
||||
cy.uiWaitUntilMessagePostedIncludes(`@${bot.username} added to the channel by you.`);
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ describe('Leave and Archive channel actions display as destructive', () => {
|
|||
// * Mute Channel menu option should be visible
|
||||
cy.get('#channelToggleMuteChannel').should('be.visible');
|
||||
|
||||
// * Add Members menu option should be visible
|
||||
cy.get('#channelAddMembers').should('be.visible');
|
||||
// * Invite Members menu option should be visible
|
||||
cy.get('#channelInviteMembers').should('be.visible');
|
||||
|
||||
// * Manage Members menu option should be visible
|
||||
cy.get('#channelManageMembers').should('be.visible');
|
||||
|
|
|
|||
|
|
@ -142,7 +142,9 @@ describe('Verify Guest User Identification in different screens', () => {
|
|||
});
|
||||
|
||||
// # Close Dialog
|
||||
cy.get('#quickSwitchModalLabel > .close').click();
|
||||
cy.get('#quickSwitchModal').within(() => {
|
||||
cy.get('button.close[aria-label="Close"]').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T1377 Verify Guest Badge in DM Search dialog', () => {
|
||||
|
|
|
|||
|
|
@ -168,8 +168,8 @@ describe('Team Permissions', () => {
|
|||
// * Verify dropdown opens
|
||||
cy.get('#channelHeaderDropdownMenu .Menu__content.dropdown-menu').should('be.visible');
|
||||
|
||||
// # Click on `Add Members`
|
||||
cy.get('#channelAddMembers').should('be.visible').click().wait(TIMEOUTS.HALF_SEC);
|
||||
// # Click on `Invite Members`
|
||||
cy.get('#channelInviteMembers').should('be.visible').click().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Search and select otherUser
|
||||
cy.get('#selectItems input').typeWithForce(otherUser.username).wait(TIMEOUTS.HALF_SEC);
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ function verifyFocusInAddChannelMemberModal() {
|
|||
cy.get('#channelLeaveChannel').should('be.visible');
|
||||
|
||||
// # Click 'Add Members'
|
||||
cy.get('#channelAddMembers').click();
|
||||
cy.get('#channelInviteMembers').click();
|
||||
|
||||
// * Assert that modal appears
|
||||
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||
|
|
|
|||
|
|
@ -40,6 +40,15 @@ declare namespace Cypress {
|
|||
*/
|
||||
uiAddUsersToCurrentChannel(usernameList: string[]);
|
||||
|
||||
/**
|
||||
* Invite users to the current channel.
|
||||
* @param {string[]} usernameList - list of userids to be invited to the channel
|
||||
*
|
||||
* @example
|
||||
* cy.uiInviteUsersToCurrentChannel(['user1', 'user2']);
|
||||
*/
|
||||
uiInviteUsersToCurrentChannel(usernameList: string[]);
|
||||
|
||||
/**
|
||||
* Archive the current channel.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -54,6 +54,19 @@ Cypress.Commands.add('uiAddUsersToCurrentChannel', (usernameList) => {
|
|||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiInviteUsersToCurrentChannel', (usernameList) => {
|
||||
if (usernameList.length) {
|
||||
cy.get('#channelHeaderDropdownIcon').click();
|
||||
cy.get('#channelInviteMembers').click();
|
||||
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||
usernameList.forEach((username) => {
|
||||
cy.get('#selectItems input').typeWithForce(`@${username}{enter}`);
|
||||
});
|
||||
cy.get('#saveItems').click();
|
||||
cy.get('#addUsersToChannelModal').should('not.exist');
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiArchiveChannel', () => {
|
||||
cy.get('#channelHeaderDropdownIcon').click();
|
||||
cy.get('#channelArchiveChannel').click();
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
|
|||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="channelAddMembers"
|
||||
id="channelInviteMembers"
|
||||
modalId="channel_invite"
|
||||
show={true}
|
||||
text="Add Members"
|
||||
|
|
@ -280,6 +280,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
|
|||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "channel_header.menuAriaLabel",
|
||||
"isExistingChannel": true,
|
||||
}
|
||||
}
|
||||
|
|
@ -1104,7 +1105,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
|
|||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="channelAddMembers"
|
||||
id="channelInviteMembers"
|
||||
modalId="channel_invite"
|
||||
show={true}
|
||||
text="Add Members"
|
||||
|
|
@ -1112,6 +1113,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
|
|||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "channel_header.menuAriaLabel",
|
||||
"isExistingChannel": true,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export default class ChannelHeaderDropdown extends React.PureComponent<Props> {
|
|||
permissions={[channelMembersPermission]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelAddMembers'
|
||||
id='channelInviteMembers'
|
||||
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault && !isGroupConstrained}
|
||||
modalId={ModalIdentifiers.CHANNEL_INVITE}
|
||||
dialogType={ChannelInviteModal}
|
||||
|
|
@ -167,7 +167,7 @@ export default class ChannelHeaderDropdown extends React.PureComponent<Props> {
|
|||
show={channel.type === Constants.GM_CHANNEL && !isArchived && !isGroupConstrained}
|
||||
modalId={ModalIdentifiers.CREATE_DM_CHANNEL}
|
||||
dialogType={MoreDirectChannels}
|
||||
dialogProps={{isExistingChannel: true}}
|
||||
dialogProps={{isExistingChannel: true, focusOriginElement: 'channel_header.menuAriaLabel'}}
|
||||
text={localizeMessage({id: 'navbar.addMembers', defaultMessage: 'Add Members'})}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ const ChannelInfoRhs = ({
|
|||
return actions.openModal({
|
||||
modalId: ModalIdentifiers.CREATE_DM_CHANNEL,
|
||||
dialogType: MoreDirectChannels,
|
||||
dialogProps: {isExistingChannel: true},
|
||||
dialogProps: {isExistingChannel: true, focusOriginElement: 'channelInfoRHSAddPeopleButton'},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ export default function TopButtons({
|
|||
onClick={actions.toggleFavorite}
|
||||
className={isFavorite ? 'active' : ''}
|
||||
aria-label={favoriteText}
|
||||
id='channelInfoRHSAddFavoriteButton'
|
||||
>
|
||||
<div>
|
||||
<i className={'icon ' + favoriteIcon}/>
|
||||
|
|
@ -154,6 +155,7 @@ export default function TopButtons({
|
|||
onClick={actions.toggleMute}
|
||||
className={isMuted ? 'active' : ''}
|
||||
aria-label={mutedText}
|
||||
id='channelInfoRHSMuteChannelButton'
|
||||
>
|
||||
<div>
|
||||
<i className={'icon ' + mutedIcon}/>
|
||||
|
|
@ -173,6 +175,7 @@ export default function TopButtons({
|
|||
<Button
|
||||
onClick={actions.addPeople}
|
||||
className={isInvitingPeople ? 'active' : ''}
|
||||
id='channelInfoRHSAddPeopleButton'
|
||||
>
|
||||
<div>
|
||||
<i className='icon icon-account-plus-outline'/>
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ export default function ChannelMembersRHS({
|
|||
return actions.openModal({
|
||||
modalId: ModalIdentifiers.CREATE_DM_CHANNEL,
|
||||
dialogType: MoreDirectChannels,
|
||||
dialogProps: {isExistingChannel: true},
|
||||
dialogProps: {isExistingChannel: true, focusOriginElement: 'channelInfoRHSAddPeopleButton'},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/MoreDirectChannels should exclude deleted users if there is not direct channel between users 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="moreDmModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal more-modal more-direct-channels"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={false}
|
||||
className="a11y__modal more-modal more-direct-channels more-direct-channels-generic-modal"
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="moreDmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Direct Messages"
|
||||
id="more_direct_channels.title"
|
||||
/>
|
||||
}
|
||||
onEntered={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="moreDmModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Direct Messages"
|
||||
id="more_direct_channels.title"
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
<div
|
||||
role="application"
|
||||
>
|
||||
<Connect(Component)
|
||||
|
|
@ -271,77 +242,32 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
|
|||
}
|
||||
values={Array []}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
bsClass="modal-footer"
|
||||
className="modal-footer--invisible"
|
||||
componentClass="div"
|
||||
>
|
||||
<button
|
||||
className="btn btn-tertiary"
|
||||
id="closeModalButton"
|
||||
type="button"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Close"
|
||||
id="general_button.close"
|
||||
/>
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</div>
|
||||
</GenericModal>
|
||||
`;
|
||||
|
||||
exports[`components/MoreDirectChannels should match snapshot 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="moreDmModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal more-modal more-direct-channels"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={false}
|
||||
className="a11y__modal more-modal more-direct-channels more-direct-channels-generic-modal"
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="moreDmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Direct Messages"
|
||||
id="more_direct_channels.title"
|
||||
/>
|
||||
}
|
||||
onEntered={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
id="moreDmModalLabel"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Direct Messages"
|
||||
id="more_direct_channels.title"
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
<div
|
||||
role="application"
|
||||
>
|
||||
<Connect(Component)
|
||||
|
|
@ -569,22 +495,6 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
bsClass="modal-footer"
|
||||
className="modal-footer--invisible"
|
||||
componentClass="div"
|
||||
>
|
||||
<button
|
||||
className="btn btn-tertiary"
|
||||
id="closeModalButton"
|
||||
type="button"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Close"
|
||||
id="general_button.close"
|
||||
/>
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</div>
|
||||
</GenericModal>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
|
||||
.more-direct-channels-generic-modal {
|
||||
margin-top: 5vh !important;
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ const mockedUser = TestHelper.getUserMock();
|
|||
|
||||
describe('components/MoreDirectChannels', () => {
|
||||
const baseProps: ComponentProps<typeof MoreDirectChannels> = {
|
||||
focusOriginElement: 'anyId',
|
||||
currentUserId: 'current_user_id',
|
||||
currentTeamId: 'team_id',
|
||||
currentTeamName: 'team_name',
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
|
|
@ -13,17 +13,15 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
|
|||
|
||||
import type MultiSelect from 'components/multiselect/multiselect';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import List from './list';
|
||||
import {USERS_PER_PAGE} from './list/list';
|
||||
import {
|
||||
isGroupChannel,
|
||||
optionValue,
|
||||
} from './types';
|
||||
import type {
|
||||
OptionValue} from './types';
|
||||
import {isGroupChannel, optionValue} from './types';
|
||||
import type {OptionValue} from './types';
|
||||
import './more_direct_channels.scss';
|
||||
|
||||
export type Props = {
|
||||
currentUserId: string;
|
||||
|
|
@ -50,8 +48,8 @@ export type Props = {
|
|||
onModalDismissed?: () => void;
|
||||
onExited?: () => void;
|
||||
actions: {
|
||||
getProfiles: (page?: number | undefined, perPage?: number | undefined, options?: any) => Promise<ActionResult>;
|
||||
getProfilesInTeam: (teamId: string, page: number, perPage?: number | undefined, sort?: string | undefined, options?: any) => Promise<ActionResult>;
|
||||
getProfiles: (page?: number, perPage?: number, options?: any) => Promise<ActionResult>;
|
||||
getProfilesInTeam: (teamId: string, page: number, perPage?: number, sort?: string, options?: any) => Promise<ActionResult>;
|
||||
loadProfilesMissingStatus: (users: UserProfile[]) => void;
|
||||
getTotalUsersStats: () => void;
|
||||
loadStatusesForProfilesList: (users: UserProfile[]) => void;
|
||||
|
|
@ -62,6 +60,7 @@ export type Props = {
|
|||
searchGroupChannels: (term: string) => Promise<ActionResult<Channel[]>>;
|
||||
setModalSearchTerm: (term: string) => void;
|
||||
};
|
||||
focusOriginElement: string;
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
|
@ -77,6 +76,7 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
|
|||
exitToChannel?: string;
|
||||
multiselect: React.RefObject<MultiSelect<OptionValue>>;
|
||||
selectedItemRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
|
|
@ -85,15 +85,12 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
|
|||
this.selectedItemRef = React.createRef();
|
||||
|
||||
const values: OptionValue[] = [];
|
||||
|
||||
if (props.currentChannelMembers) {
|
||||
for (let i = 0; i < props.currentChannelMembers.length; i++) {
|
||||
const user = Object.assign({}, props.currentChannelMembers[i]);
|
||||
|
||||
if (user.id === props.currentUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
values.push(optionValue(user));
|
||||
}
|
||||
}
|
||||
|
|
@ -144,9 +141,7 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.users.length !== this.props.users.length
|
||||
) {
|
||||
if (prevProps.users.length !== this.props.users.length) {
|
||||
this.props.actions.loadProfilesMissingStatus(this.props.users);
|
||||
}
|
||||
}
|
||||
|
|
@ -155,28 +150,31 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
|
|||
this.updateFromProps(prevProps);
|
||||
}
|
||||
|
||||
setUsersLoadingState = (loadingState: boolean) => {
|
||||
this.setState({loadingUsers: loadingState});
|
||||
};
|
||||
|
||||
handleHide = () => {
|
||||
this.props.actions.setModalSearchTerm('');
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
setUsersLoadingState = (loadingState: boolean) => {
|
||||
this.setState({
|
||||
loadingUsers: loadingState,
|
||||
});
|
||||
};
|
||||
|
||||
handleExit = () => {
|
||||
this.props.onExited?.();
|
||||
this.props.onModalDismissed?.();
|
||||
|
||||
if (this.exitToChannel) {
|
||||
getHistory().push(this.exitToChannel);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
focusElement(this.props.focusOriginElement, true);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
this.props.onModalDismissed?.();
|
||||
this.props.onExited?.();
|
||||
};
|
||||
|
||||
handleSubmit = (values = this.state.values) => {
|
||||
const {actions} = this.props;
|
||||
|
||||
if (this.state.saving) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -209,26 +207,22 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
|
|||
if (isGroupChannel(value)) {
|
||||
this.addUsers(value.profiles);
|
||||
} else {
|
||||
const values = Object.assign([], this.state.values);
|
||||
|
||||
if (values.indexOf(value) === -1) {
|
||||
const values = [...this.state.values];
|
||||
if (!values.includes(value)) {
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
this.setState({values});
|
||||
}
|
||||
};
|
||||
|
||||
addUsers = (users: UserProfile[]) => {
|
||||
const values: OptionValue[] = Object.assign([], this.state.values);
|
||||
const values = [...this.state.values];
|
||||
const existingUserIds = values.map((user) => user.id);
|
||||
for (const user of users) {
|
||||
if (existingUserIds.indexOf(user.id) !== -1) {
|
||||
continue;
|
||||
if (!existingUserIds.includes(user.id)) {
|
||||
values.push(optionValue(user));
|
||||
}
|
||||
values.push(optionValue(user));
|
||||
}
|
||||
|
||||
this.setState({values});
|
||||
};
|
||||
|
||||
|
|
@ -284,46 +278,29 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
|
|||
/>
|
||||
);
|
||||
|
||||
const modalHeaderText = (
|
||||
<FormattedMessage
|
||||
id='more_direct_channels.title'
|
||||
defaultMessage='Direct Messages'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal more-modal more-direct-channels'
|
||||
show={this.state.show}
|
||||
onHide={this.handleHide}
|
||||
onExited={this.handleExit}
|
||||
onEntered={this.loadModalData}
|
||||
role='none'
|
||||
aria-labelledby='moreDmModalLabel'
|
||||
<GenericModal
|
||||
id='moreDmModal'
|
||||
className='a11y__modal more-modal more-direct-channels more-direct-channels-generic-modal'
|
||||
show={this.state.show}
|
||||
modalHeaderText={modalHeaderText}
|
||||
onExited={this.handleExit}
|
||||
onHide={this.handleExit}
|
||||
compassDesign={true}
|
||||
bodyPadding={false}
|
||||
onEntered={this.loadModalData}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title
|
||||
componentClass='h1'
|
||||
id='moreDmModalLabel'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='more_direct_channels.title'
|
||||
defaultMessage='Direct Messages'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body
|
||||
role='application'
|
||||
>
|
||||
<div role='application'>
|
||||
{body}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className='modal-footer--invisible'>
|
||||
<button
|
||||
id='closeModalButton'
|
||||
type='button'
|
||||
className='btn btn-tertiary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='general_button.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
</GenericModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import {getSearchTeam, getSearchTerms, getSearchType} from 'selectors/rhs';
|
|||
import Popover from 'components/widgets/popover';
|
||||
|
||||
import a11yController from 'utils/a11y_controller_instance';
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import Constants, {A11yCustomEventTypes} from 'utils/constants';
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import Constants from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {isServerVersionGreaterThanOrEqualTo} from 'utils/server_version';
|
||||
import {isDesktopApp, getDesktopVersion, isMacApp} from 'utils/user_agent';
|
||||
|
|
@ -185,18 +185,9 @@ const NewSearch = (): JSX.Element => {
|
|||
const closeSearchBox = useCallback(() => {
|
||||
setFocused(false);
|
||||
setCurrentChannel('');
|
||||
if (searchButtonRef.current) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<A11yFocusEventDetail>(A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: searchButtonRef.current,
|
||||
keyboardOnly: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
a11yController.resetOriginElement();
|
||||
}
|
||||
}, []);
|
||||
|
||||
focusElement(searchButtonRef, true, true);
|
||||
}, [searchButtonRef, setFocused, setCurrentChannel]);
|
||||
|
||||
const openSearchBox = useCallback(() => {
|
||||
setFocused(true);
|
||||
|
|
|
|||
|
|
@ -1,123 +1,98 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/QuickSwitchModal should match snapshot 1`] = `
|
||||
<Modal
|
||||
animation={false}
|
||||
aria-describedby="quickSwitchHeaderWithHint"
|
||||
aria-labelledby="quickSwitchHeader"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal channel-switcher"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
ariaLabel="Find Channels"
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={false}
|
||||
className="a11y__modal channel-switcher"
|
||||
compassDesign={true}
|
||||
enforceFocus={false}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={false}
|
||||
role="none"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
className="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
id="quickSwitchModalLabel"
|
||||
>
|
||||
id="quickSwitchModal"
|
||||
keyboardEscape={true}
|
||||
modalHeaderText={
|
||||
<div
|
||||
className="channel-switcher__header"
|
||||
id="quickSwitchHeaderWithHint"
|
||||
>
|
||||
<h1
|
||||
<h2
|
||||
id="quickSwitchHeader"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Find Channels"
|
||||
id="quick_switch_modal.switchChannels"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="channel-switcher__hint"
|
||||
id="quickSwitchHint"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Type to find a channel. Use <b>UP/DOWN</b> to browse, <b>ENTER</b> to select, <b>ESC</b> to dismiss."
|
||||
id="quickSwitchModal.help_no_team"
|
||||
values={
|
||||
Object {
|
||||
"b": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
}
|
||||
modalSubheaderText={
|
||||
<div
|
||||
className="channel-switcher__suggestion-box"
|
||||
className="channel-switcher__hint"
|
||||
id="quickSwitchHint"
|
||||
>
|
||||
<i
|
||||
className="icon icon-magnify icon-16"
|
||||
/>
|
||||
<Connect(SuggestionBox)
|
||||
aria-label="quick switch input"
|
||||
className="form-control focused"
|
||||
completeOnTab={false}
|
||||
delayInputUpdate={true}
|
||||
forceSuggestionsWhenBlur={true}
|
||||
id="quickSwitchInput"
|
||||
listComponent={[Function]}
|
||||
listPosition="bottom"
|
||||
maxLength="64"
|
||||
onChange={[Function]}
|
||||
onItemSelected={[Function]}
|
||||
onSuggestionsReceived={[Function]}
|
||||
openWhenEmpty={true}
|
||||
providers={
|
||||
Array [
|
||||
SwitchChannelProvider {
|
||||
"disableDispatches": false,
|
||||
"forceDispatch": false,
|
||||
"latestComplete": true,
|
||||
"latestPrefix": "",
|
||||
"requestStarted": false,
|
||||
"store": Object {
|
||||
"@@observable": [Function],
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
},
|
||||
]
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Type to find a channel. Use <b>UP/DOWN</b> to browse, <b>ENTER</b> to select, <b>ESC</b> to dismiss."
|
||||
id="quickSwitchModal.help_no_team"
|
||||
values={
|
||||
Object {
|
||||
"b": [Function],
|
||||
}
|
||||
}
|
||||
renderDividers={
|
||||
Array [
|
||||
"mention.unread",
|
||||
"mention.recent.channels",
|
||||
]
|
||||
}
|
||||
shouldSearchCompleteText={true}
|
||||
spellCheck="false"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="channel-switcher__suggestion-box"
|
||||
>
|
||||
<i
|
||||
className="icon icon-magnify icon-16"
|
||||
/>
|
||||
<Connect(SuggestionBox)
|
||||
aria-label="quick switch input"
|
||||
className="form-control focused"
|
||||
completeOnTab={false}
|
||||
delayInputUpdate={true}
|
||||
forceSuggestionsWhenBlur={true}
|
||||
id="quickSwitchInput"
|
||||
listComponent={[Function]}
|
||||
listPosition="bottom"
|
||||
maxLength="64"
|
||||
onChange={[Function]}
|
||||
onItemSelected={[Function]}
|
||||
onSuggestionsReceived={[Function]}
|
||||
openWhenEmpty={true}
|
||||
providers={
|
||||
Array [
|
||||
SwitchChannelProvider {
|
||||
"disableDispatches": false,
|
||||
"forceDispatch": false,
|
||||
"latestComplete": true,
|
||||
"latestPrefix": "",
|
||||
"requestStarted": false,
|
||||
"store": Object {
|
||||
"@@observable": [Function],
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
renderDividers={
|
||||
Array [
|
||||
"mention.unread",
|
||||
"mention.recent.channels",
|
||||
]
|
||||
}
|
||||
shouldSearchCompleteText={true}
|
||||
spellCheck="false"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</GenericModal>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
|
||||
import type {QuickSwitchModal as QuickSwitchModalClass} from 'components/quick_switch_modal/quick_switch_modal';
|
||||
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 Constants from 'utils/constants';
|
||||
|
||||
describe('components/QuickSwitchModal', () => {
|
||||
const baseProps = {
|
||||
focusOriginElement: 'anyId',
|
||||
onExited: jest.fn(),
|
||||
showTeamSwitcher: false,
|
||||
isMobileView: false,
|
||||
|
|
@ -28,36 +31,32 @@ describe('components/QuickSwitchModal', () => {
|
|||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<QuickSwitchModal {...baseProps}/>,
|
||||
);
|
||||
|
||||
const wrapper = shallowWithIntl(<QuickSwitchModal {...baseProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('handleSubmit', () => {
|
||||
it('should do nothing if nothing selected', () => {
|
||||
const props = {...baseProps};
|
||||
const wrapper = shallowWithIntl(<QuickSwitchModal {...props}/>);
|
||||
const instance = wrapper.instance() as QuickSwitchModalClass;
|
||||
|
||||
const wrapper = shallow<QuickSwitchModal>(
|
||||
<QuickSwitchModal {...props}/>,
|
||||
);
|
||||
|
||||
wrapper.instance().handleSubmit();
|
||||
expect(baseProps.onExited).not.toBeCalled();
|
||||
instance.handleSubmit();
|
||||
expect(props.onExited).not.toBeCalled();
|
||||
expect(props.actions.switchToChannel).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should fail to switch to a channel', (done) => {
|
||||
const wrapper = shallow<QuickSwitchModal>(
|
||||
<QuickSwitchModal {...baseProps}/>,
|
||||
);
|
||||
const props = {...baseProps};
|
||||
const wrapper = shallowWithIntl(<QuickSwitchModal {...props}/>);
|
||||
const instance = wrapper.instance() as QuickSwitchModalClass;
|
||||
|
||||
const channel = {id: 'channel_id', userId: 'user_id', type: Constants.DM_CHANNEL};
|
||||
wrapper.instance().handleSubmit({channel});
|
||||
expect(baseProps.actions.switchToChannel).toBeCalledWith(channel);
|
||||
instance.handleSubmit({channel});
|
||||
expect(props.actions.switchToChannel).toBeCalledWith(channel);
|
||||
|
||||
process.nextTick(() => {
|
||||
expect(baseProps.onExited).not.toBeCalled();
|
||||
expect(props.onExited).not.toBeCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
@ -74,15 +73,15 @@ describe('components/QuickSwitchModal', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const wrapper = shallow<QuickSwitchModal>(
|
||||
<QuickSwitchModal {...props}/>,
|
||||
);
|
||||
const wrapper = shallowWithIntl(<QuickSwitchModal {...props}/>);
|
||||
const instance = wrapper.instance() as QuickSwitchModalClass;
|
||||
|
||||
const channel = {id: 'channel_id', userId: 'user_id', type: Constants.DM_CHANNEL};
|
||||
wrapper.instance().handleSubmit({channel});
|
||||
instance.handleSubmit({channel});
|
||||
expect(props.actions.switchToChannel).toBeCalledWith(channel);
|
||||
|
||||
process.nextTick(() => {
|
||||
expect(baseProps.onExited).toBeCalled();
|
||||
expect(props.onExited).toBeCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
@ -99,17 +98,18 @@ describe('components/QuickSwitchModal', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const wrapper = shallow<QuickSwitchModal>(
|
||||
<QuickSwitchModal {...props}/>,
|
||||
);
|
||||
const wrapper = shallowWithIntl(<QuickSwitchModal {...props}/>);
|
||||
const instance = wrapper.instance() as QuickSwitchModalClass;
|
||||
|
||||
const channel = {id: 'channel_id', name: 'test', type: Constants.OPEN_CHANNEL};
|
||||
const selected = {
|
||||
type: Constants.MENTION_MORE_CHANNELS,
|
||||
channel,
|
||||
};
|
||||
wrapper.instance().handleSubmit(selected);
|
||||
|
||||
instance.handleSubmit(selected);
|
||||
expect(props.actions.joinChannelById).toBeCalledWith(channel.id);
|
||||
|
||||
process.nextTick(() => {
|
||||
expect(props.actions.switchToChannel).toBeCalledWith(channel);
|
||||
done();
|
||||
|
|
@ -128,20 +128,21 @@ describe('components/QuickSwitchModal', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const wrapper = shallow<QuickSwitchModal>(
|
||||
<QuickSwitchModal {...props}/>,
|
||||
);
|
||||
const wrapper = shallowWithIntl(<QuickSwitchModal {...props}/>);
|
||||
const instance = wrapper.instance() as QuickSwitchModalClass;
|
||||
|
||||
const channel = {id: 'channel_id', name: 'test', type: Constants.DM_CHANNEL};
|
||||
const selected = {
|
||||
type: Constants.MENTION_MORE_CHANNELS,
|
||||
channel,
|
||||
};
|
||||
wrapper.instance().handleSubmit(selected);
|
||||
|
||||
instance.handleSubmit(selected);
|
||||
expect(props.actions.joinChannelById).not.toHaveBeenCalled();
|
||||
expect(props.actions.switchToChannel).toBeCalledWith(channel);
|
||||
|
||||
process.nextTick(() => {
|
||||
expect(baseProps.onExited).toBeCalled();
|
||||
expect(props.onExited).toBeCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
@ -159,10 +160,12 @@ describe('components/QuickSwitchModal', () => {
|
|||
};
|
||||
|
||||
renderWithContext(
|
||||
<>
|
||||
<ChannelNavigator {...channelNavigatorProps}/>
|
||||
<QuickSwitchModal {...baseProps}/>
|
||||
</>,
|
||||
<IntlProvider locale='en'>
|
||||
<>
|
||||
<ChannelNavigator {...channelNavigatorProps}/>
|
||||
<QuickSwitchModal {...baseProps}/>
|
||||
</>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('SidebarChannelNavigatorButton'));
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||
import type {WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
|
@ -16,6 +17,7 @@ import type SuggestionBoxComponent from 'components/suggestion/suggestion_box/su
|
|||
import SuggestionList from 'components/suggestion/suggestion_list';
|
||||
import SwitchChannelProvider from 'components/suggestion/switch_channel_provider';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import Constants, {RHSStates} from 'utils/constants';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
|
|
@ -30,13 +32,9 @@ type ProviderSuggestions = {
|
|||
terms: string[];
|
||||
items: any[];
|
||||
component: React.ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The function called to immediately hide the modal
|
||||
*/
|
||||
export type Props = WrappedComponentProps & {
|
||||
onExited: () => void;
|
||||
|
||||
isMobileView: boolean;
|
||||
|
|
@ -48,25 +46,25 @@ export type Props = {
|
|||
switchToChannel: (channel: Channel) => Promise<ActionResult>;
|
||||
closeRightHandSide: () => void;
|
||||
};
|
||||
}
|
||||
focusOriginElement: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
text: string;
|
||||
mode: string|null;
|
||||
mode: string | null;
|
||||
hasSuggestions: boolean;
|
||||
shouldShowLoadingSpinner: boolean;
|
||||
pretext: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default class QuickSwitchModal extends React.PureComponent<Props, State> {
|
||||
export class QuickSwitchModal extends React.PureComponent<Props, State> {
|
||||
private channelProviders: SwitchChannelProvider[];
|
||||
private switchBox: SuggestionBoxComponent|null;
|
||||
private switchBox: SuggestionBoxComponent | null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.channelProviders = [new SwitchChannelProvider()];
|
||||
|
||||
this.switchBox = null;
|
||||
|
||||
this.state = {
|
||||
|
|
@ -82,7 +80,6 @@ export default class QuickSwitchModal extends React.PureComponent<Props, State>
|
|||
if (this.switchBox === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textbox = this.switchBox.getTextbox();
|
||||
if (document.activeElement !== textbox) {
|
||||
textbox.focus();
|
||||
|
|
@ -116,12 +113,7 @@ export default class QuickSwitchModal extends React.PureComponent<Props, State>
|
|||
|
||||
private hideOnCancel = () => {
|
||||
this.props.onExited?.();
|
||||
setTimeout(() => {
|
||||
const modalButton = document.querySelector('.SidebarChannelNavigator_jumpToButton') as HTMLElement;
|
||||
if (modalButton) {
|
||||
modalButton.focus();
|
||||
}
|
||||
});
|
||||
focusElement(this.props.focusOriginElement, true);
|
||||
};
|
||||
|
||||
private onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
|
|
@ -168,15 +160,15 @@ export default class QuickSwitchModal extends React.PureComponent<Props, State>
|
|||
const providers: SwitchChannelProvider[] = this.channelProviders;
|
||||
|
||||
const header = (
|
||||
<h1 id='quickSwitchHeader'>
|
||||
<h2 id='quickSwitchHeader'>
|
||||
<FormattedMessage
|
||||
id='quick_switch_modal.switchChannels'
|
||||
defaultMessage='Find Channels'
|
||||
/>
|
||||
</h1>
|
||||
</h2>
|
||||
);
|
||||
|
||||
let help;
|
||||
let help: React.ReactNode;
|
||||
if (this.props.isMobileView) {
|
||||
help = (
|
||||
<FormattedMessage
|
||||
|
|
@ -196,71 +188,75 @@ export default class QuickSwitchModal extends React.PureComponent<Props, State>
|
|||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal channel-switcher'
|
||||
show={true}
|
||||
onHide={this.hideOnCancel}
|
||||
enforceFocus={false}
|
||||
restoreFocus={false}
|
||||
role='none'
|
||||
aria-labelledby='quickSwitchHeader'
|
||||
aria-describedby='quickSwitchHeaderWithHint'
|
||||
animation={false}
|
||||
const modalHeaderText = (
|
||||
<div className='channel-switcher__header'>
|
||||
{header}
|
||||
</div>
|
||||
);
|
||||
|
||||
const modalSubheaderText = (
|
||||
<div
|
||||
className='channel-switcher__hint'
|
||||
id='quickSwitchHint'
|
||||
>
|
||||
<Modal.Header
|
||||
className='modal-header'
|
||||
id='quickSwitchModalLabel'
|
||||
closeButton={true}
|
||||
>
|
||||
<div
|
||||
className='channel-switcher__header'
|
||||
id='quickSwitchHeaderWithHint'
|
||||
>
|
||||
{header}
|
||||
<div
|
||||
className='channel-switcher__hint'
|
||||
id='quickSwitchHint'
|
||||
>
|
||||
{help}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className='channel-switcher__suggestion-box'>
|
||||
<i className='icon icon-magnify icon-16'/>
|
||||
<SuggestionBox
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
ref={this.setSwitchBoxRef}
|
||||
id='quickSwitchInput'
|
||||
aria-label={Utils.localizeMessage({id: 'quick_switch_modal.input', defaultMessage: 'quick switch input'})}
|
||||
className='form-control focused'
|
||||
onChange={this.onChange}
|
||||
value={this.state.text}
|
||||
onItemSelected={this.handleSubmit}
|
||||
listComponent={SuggestionList}
|
||||
listPosition='bottom'
|
||||
maxLength='64'
|
||||
providers={providers}
|
||||
completeOnTab={false}
|
||||
spellCheck='false'
|
||||
delayInputUpdate={true}
|
||||
openWhenEmpty={true}
|
||||
onSuggestionsReceived={this.handleSuggestionsReceived}
|
||||
forceSuggestionsWhenBlur={true}
|
||||
renderDividers={[Constants.MENTION_UNREAD, Constants.MENTION_RECENT_CHANNELS]}
|
||||
shouldSearchCompleteText={true}
|
||||
/>
|
||||
{!this.state.shouldShowLoadingSpinner && !this.state.hasSuggestions && this.state.text &&
|
||||
{help}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
className='a11y__modal channel-switcher'
|
||||
id='quickSwitchModal'
|
||||
show={true}
|
||||
bodyPadding={false}
|
||||
enforceFocus={false}
|
||||
onExited={this.hideOnCancel}
|
||||
onHide={this.hideOnCancel}
|
||||
ariaLabel={this.props.intl.formatMessage({id: 'quick_switch_modal.switchChannels', defaultMessage: 'Find Channels'})}
|
||||
modalHeaderText={modalHeaderText}
|
||||
modalSubheaderText={modalSubheaderText}
|
||||
compassDesign={true}
|
||||
>
|
||||
<div className='channel-switcher__suggestion-box'>
|
||||
<i className='icon icon-magnify icon-16'/>
|
||||
<SuggestionBox
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
ref={this.setSwitchBoxRef}
|
||||
id='quickSwitchInput'
|
||||
aria-label={this.props.intl.formatMessage({id: 'quick_switch_modal.input', defaultMessage: 'quick switch input'})}
|
||||
className='form-control focused'
|
||||
onChange={this.onChange}
|
||||
value={this.state.text}
|
||||
onItemSelected={this.handleSubmit}
|
||||
listComponent={SuggestionList}
|
||||
listPosition='bottom'
|
||||
maxLength='64'
|
||||
providers={providers}
|
||||
completeOnTab={false}
|
||||
spellCheck='false'
|
||||
delayInputUpdate={true}
|
||||
openWhenEmpty={true}
|
||||
onSuggestionsReceived={this.handleSuggestionsReceived}
|
||||
forceSuggestionsWhenBlur={true}
|
||||
renderDividers={[Constants.MENTION_UNREAD, Constants.MENTION_RECENT_CHANNELS]}
|
||||
shouldSearchCompleteText={true}
|
||||
/>
|
||||
{
|
||||
!this.state.shouldShowLoadingSpinner &&
|
||||
!this.state.hasSuggestions &&
|
||||
this.state.text &&
|
||||
(
|
||||
<NoResultsIndicator
|
||||
variant={NoResultsVariant.Search}
|
||||
titleValues={{channelName: `${this.state.pretext}`}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default injectIntl(QuickSwitchModal);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ exports[`components/sidebar should match snapshot 1`] = `
|
|||
id="lhsNavigator"
|
||||
role="application"
|
||||
>
|
||||
<Connect(ChannelNavigator) />
|
||||
<Connect(injectIntl(ChannelNavigator)) />
|
||||
</div>
|
||||
<div
|
||||
className="sidebar--left__icons"
|
||||
|
|
@ -68,7 +68,7 @@ exports[`components/sidebar should match snapshot when direct channels modal is
|
|||
id="lhsNavigator"
|
||||
role="application"
|
||||
>
|
||||
<Connect(ChannelNavigator) />
|
||||
<Connect(injectIntl(ChannelNavigator)) />
|
||||
</div>
|
||||
<div
|
||||
className="sidebar--left__icons"
|
||||
|
|
@ -84,6 +84,7 @@ exports[`components/sidebar should match snapshot when direct channels modal is
|
|||
/>
|
||||
<Connect(DataPrefetch) />
|
||||
<MoreDirectChannels
|
||||
focusOriginElement="newDirectMessageButton"
|
||||
isExistingChannel={false}
|
||||
onModalDismissed={[Function]}
|
||||
/>
|
||||
|
|
@ -114,7 +115,7 @@ exports[`components/sidebar should match snapshot when more channels modal is op
|
|||
id="lhsNavigator"
|
||||
role="application"
|
||||
>
|
||||
<Connect(ChannelNavigator) />
|
||||
<Connect(injectIntl(ChannelNavigator)) />
|
||||
</div>
|
||||
<div
|
||||
className="sidebar--left__icons"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ describe('Components/ChannelNavigator', () => {
|
|||
props = {
|
||||
showUnreadsCategory: true,
|
||||
isQuickSwitcherOpen: false,
|
||||
intl: {} as any,
|
||||
actions: {
|
||||
openModal: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||
import type {WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ import type {ModalData} from 'types/actions';
|
|||
|
||||
import ChannelFilter from '../channel_filter';
|
||||
|
||||
export type Props = {
|
||||
export type Props = WrappedComponentProps & {
|
||||
showUnreadsCategory: boolean;
|
||||
isQuickSwitcherOpen: boolean;
|
||||
actions: {
|
||||
|
|
@ -26,7 +27,7 @@ export type Props = {
|
|||
};
|
||||
};
|
||||
|
||||
export default class ChannelNavigator extends React.PureComponent<Props> {
|
||||
class ChannelNavigator extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.handleShortcut);
|
||||
document.addEventListener('keydown', this.handleQuickSwitchKeyPress);
|
||||
|
|
@ -45,6 +46,7 @@ export default class ChannelNavigator extends React.PureComponent<Props> {
|
|||
this.props.actions.openModal({
|
||||
modalId: ModalIdentifiers.QUICK_SWITCH,
|
||||
dialogType: QuickSwitchModal,
|
||||
dialogProps: {focusOriginElement: 'SidebarChannelNavigatorButton'},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -81,6 +83,7 @@ export default class ChannelNavigator extends React.PureComponent<Props> {
|
|||
openModal({
|
||||
modalId: ModalIdentifiers.QUICK_SWITCH,
|
||||
dialogType: QuickSwitchModal,
|
||||
dialogProps: {focusOriginElement: 'SidebarChannelNavigatorButton'},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -92,9 +95,10 @@ export default class ChannelNavigator extends React.PureComponent<Props> {
|
|||
<button
|
||||
className={'SidebarChannelNavigator_jumpToButton'}
|
||||
onClick={this.openQuickSwitcher}
|
||||
aria-label={Utils.localizeMessage({id: 'sidebar_left.channel_navigator.channelSwitcherLabel', defaultMessage: 'Channel Switcher'})}
|
||||
aria-label={this.props.intl.formatMessage({id: 'sidebar_left.channel_navigator.channelSwitcherLabel', defaultMessage: 'Channel Switcher'})}
|
||||
aria-haspopup='dialog'
|
||||
data-testid='SidebarChannelNavigatorButton'
|
||||
id='SidebarChannelNavigatorButton'
|
||||
>
|
||||
<i className='icon icon-magnify'/>
|
||||
<FormattedMessage
|
||||
|
|
@ -109,3 +113,5 @@ export default class ChannelNavigator extends React.PureComponent<Props> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(ChannelNavigator);
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
|||
<MoreDirectChannels
|
||||
onModalDismissed={this.hideMoreDirectChannelsModal}
|
||||
isExistingChannel={false}
|
||||
focusOriginElement='newDirectMessageButton'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ exports[`components/sidebar/sidebar_category should match snapshot 2`] = `
|
|||
>
|
||||
<ul
|
||||
className="NavGroupContent"
|
||||
role="list"
|
||||
>
|
||||
<Connect(SidebarChannel)
|
||||
channelId="channel_id"
|
||||
|
|
@ -129,7 +128,6 @@ exports[`components/sidebar/sidebar_category should match snapshot when collapse
|
|||
>
|
||||
<ul
|
||||
className="NavGroupContent"
|
||||
role="list"
|
||||
>
|
||||
<Connect(SidebarChannel)
|
||||
channelId="channel_id"
|
||||
|
|
@ -208,7 +206,6 @@ exports[`components/sidebar/sidebar_category should match snapshot when isNewCat
|
|||
>
|
||||
<ul
|
||||
className="NavGroupContent"
|
||||
role="list"
|
||||
>
|
||||
<PublicDraggable
|
||||
draggableId="NEW_CHANNEL_SPACER__category1"
|
||||
|
|
@ -323,6 +320,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when sorting
|
|||
<button
|
||||
aria-label="Create new direct message"
|
||||
className="SidebarChannelGroupHeader_addButton"
|
||||
id="newDirectMessageButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
|
|
@ -336,7 +334,6 @@ exports[`components/sidebar/sidebar_category should match snapshot when sorting
|
|||
>
|
||||
<ul
|
||||
className="NavGroupContent"
|
||||
role="list"
|
||||
>
|
||||
<Connect(SidebarChannel)
|
||||
channelId="channel_id"
|
||||
|
|
@ -434,6 +431,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when the cate
|
|||
<button
|
||||
aria-label="Create new direct message"
|
||||
className="SidebarChannelGroupHeader_addButton"
|
||||
id="newDirectMessageButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
|
|
@ -447,7 +445,6 @@ exports[`components/sidebar/sidebar_category should match snapshot when the cate
|
|||
>
|
||||
<ul
|
||||
className="NavGroupContent"
|
||||
role="list"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -189,7 +189,6 @@ export default class SidebarCategory extends React.PureComponent<Props, State> {
|
|||
draggable='false'
|
||||
className={'SidebarChannel noFloat newChannelSpacer'}
|
||||
{...provided.draggableProps}
|
||||
role='listitem'
|
||||
tabIndex={-1}
|
||||
/>
|
||||
);
|
||||
|
|
@ -254,7 +253,7 @@ export default class SidebarCategory extends React.PureComponent<Props, State> {
|
|||
|
||||
let categoryMenu: JSX.Element;
|
||||
let newLabel: JSX.Element;
|
||||
let directMessagesModalButton: JSX.Element;
|
||||
const directMessagesModalButton: JSX.Element | null = null;
|
||||
let isCollapsible = true;
|
||||
if (isNewCategory) {
|
||||
newLabel = (
|
||||
|
|
@ -289,6 +288,7 @@ export default class SidebarCategory extends React.PureComponent<Props, State> {
|
|||
}
|
||||
>
|
||||
<button
|
||||
id='newDirectMessageButton'
|
||||
className='SidebarChannelGroupHeader_addButton'
|
||||
onClick={this.handleOpenDirectMessagesModal}
|
||||
aria-label={addHelpLabel}
|
||||
|
|
@ -380,7 +380,6 @@ export default class SidebarCategory extends React.PureComponent<Props, State> {
|
|||
className={classNames('SidebarChannelGroup_content')}
|
||||
>
|
||||
<ul
|
||||
role='list'
|
||||
className='NavGroupContent'
|
||||
>
|
||||
{this.renderNewDropBox(droppableSnapshot.isDraggingOver)}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import Search from 'components/search/index';
|
|||
|
||||
import RhsPlugin from 'plugins/rhs_plugin';
|
||||
import a11yController from 'utils/a11y_controller_instance';
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import Constants, {A11yCustomEventTypes} from 'utils/constants';
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import Constants from 'utils/constants';
|
||||
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
|
||||
import {isMac} from 'utils/user_agent';
|
||||
|
||||
|
|
@ -159,34 +159,21 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
|||
|
||||
if (this.props.isOpen && (contentChanged || (!wasOpen && isOpen))) {
|
||||
this.previousActiveElement = document.activeElement as HTMLElement;
|
||||
|
||||
// Focus the sidebar after a tick
|
||||
setTimeout(() => {
|
||||
if (this.sidebarRight.current) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<A11yFocusEventDetail>(A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: this.sidebarRight.current,
|
||||
keyboardOnly: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
focusElement(this.sidebarRight, false);
|
||||
}
|
||||
}, 0);
|
||||
} else if (!this.props.isOpen && wasOpen) {
|
||||
// RHS just was closed, restore focus to the previous element had it
|
||||
// this will have to change for upcoming work specially for search and probalby plugins
|
||||
if (a11yController.originElement) {
|
||||
a11yController.restoreOriginFocus();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (this.previousActiveElement) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<A11yFocusEventDetail>(A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: this.previousActiveElement,
|
||||
keyboardOnly: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
focusElement(this.previousActiveElement, false);
|
||||
this.previousActiveElement = null;
|
||||
}
|
||||
}, 0);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
@use 'utils/mixins';
|
||||
|
||||
// since the modal is kind of tall, this makes it look more aligned to the top and the content better distributed
|
||||
.GenericModal.modal-dialog {
|
||||
.three-days-left-generic-modal {
|
||||
margin-top: calc(40vh - 240px) !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ function ThreeDaysLeftTrialModal(props: Props): JSX.Element | null {
|
|||
|
||||
return (
|
||||
<GenericModal
|
||||
className='ThreeDaysLeftTrialModal'
|
||||
className='ThreeDaysLeftTrialModal three-days-left-generic-modal'
|
||||
id='threeDaysLeftTrialModal'
|
||||
onExited={handleOnClose}
|
||||
modalHeaderText={headerText}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@
|
|||
height: 362px;
|
||||
padding: 0;
|
||||
|
||||
.input-wrapper {
|
||||
position: inherit !important;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.icon-magnify {
|
||||
position: absolute;
|
||||
top: 1.1rem;
|
||||
|
|
@ -51,7 +56,7 @@
|
|||
height: 40px;
|
||||
padding: 0 34px;
|
||||
border-radius: 4px;
|
||||
margin: 0 32px;
|
||||
margin: 2px 32px;
|
||||
|
||||
&:focus {
|
||||
padding: 0 33px;
|
||||
|
|
@ -173,12 +178,13 @@ body:not(.app__body) {
|
|||
}
|
||||
|
||||
.channel-switcher__hint {
|
||||
color: rgb(var(--center-channel-color-rgb));
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.channel-switcher__header,
|
||||
.channel-invite__header {
|
||||
h1 {
|
||||
h2 {
|
||||
margin: 0 0 0.8rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
|
|
|
|||
|
|
@ -395,14 +395,13 @@ export default class A11yController {
|
|||
restoreOriginFocus() {
|
||||
if (this.originElement && this.isElementValid(this.originElement)) {
|
||||
// Dispatch a focus event to manually focus this element
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: this.originElement,
|
||||
keyboardOnly: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const customEvent = new CustomEvent(A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target: this.originElement,
|
||||
keyboardOnly: false,
|
||||
},
|
||||
});
|
||||
this.handleA11yFocus(customEvent);
|
||||
setTimeout(() => {
|
||||
this.originElement = null;
|
||||
}, 0);
|
||||
|
|
@ -410,7 +409,7 @@ export default class A11yController {
|
|||
}
|
||||
|
||||
/**
|
||||
* Resets the a11y navigation controller, active region/section/element, clears focus and resets user interraction states
|
||||
* Resets the a11y navigation controller, active region/section/element, clears focus and resets user interaction states
|
||||
*/
|
||||
cancelNavigation() {
|
||||
this.clearActiveRegion();
|
||||
|
|
|
|||
59
webapp/channels/src/utils/a11y_utils.ts
Normal file
59
webapp/channels/src/utils/a11y_utils.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import a11y from './a11y_controller_instance';
|
||||
import type {A11yFocusEventDetail} from './constants';
|
||||
import {A11yCustomEventTypes} from './constants';
|
||||
|
||||
/**
|
||||
* Dispatches an accessibility-focused custom event on the given DOM element,
|
||||
* ref, or string ID.
|
||||
*
|
||||
* - If a string is provided, it uses `document.getElementById(...)`.
|
||||
* - If a React ref is provided, it uses `ref.current`.
|
||||
* - If an HTMLElement is provided, it uses that element directly.
|
||||
*
|
||||
* @param elementOrId - The DOM element, a ref to it, or a string ID.
|
||||
* @param keyboardOnly - Whether this focus event is triggered by keyboard interaction. Defaults to `true`.
|
||||
* @param resetOriginElement - Whether the original element stored data in the a11y controller should be reseted.
|
||||
*/
|
||||
export function focusElement(
|
||||
elementOrId: HTMLElement | React.RefObject<HTMLElement> | string,
|
||||
keyboardOnly = true,
|
||||
resetOriginElement = false,
|
||||
) {
|
||||
let target: HTMLElement | null = null;
|
||||
|
||||
if (typeof elementOrId === 'string') {
|
||||
// It's an ID string
|
||||
target = document.getElementById(elementOrId);
|
||||
} else if (
|
||||
// It's a React ref object
|
||||
typeof elementOrId === 'object' &&
|
||||
'current' in elementOrId &&
|
||||
elementOrId.current instanceof HTMLElement
|
||||
) {
|
||||
target = elementOrId.current;
|
||||
} else if (elementOrId instanceof HTMLElement) {
|
||||
// Direct HTMLElement
|
||||
target = elementOrId;
|
||||
}
|
||||
|
||||
// Dispatch focus event if a valid DOM element is found.
|
||||
if (target) {
|
||||
setTimeout(() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<A11yFocusEventDetail>(A11yCustomEventTypes.FOCUS, {
|
||||
detail: {
|
||||
target,
|
||||
keyboardOnly,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
if (resetOriginElement) {
|
||||
a11y.resetOriginElement();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
p#genericModalSubheading {
|
||||
div#genericModalSubheading {
|
||||
font-size: 12px;
|
||||
margin-block: 10px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import './generic_modal.scss';
|
|||
export type Props = {
|
||||
className?: string;
|
||||
onExited: () => void;
|
||||
onEntered?: () => void;
|
||||
onHide?: () => void;
|
||||
modalHeaderText?: React.ReactNode;
|
||||
modalSubheaderText?: React.ReactNode;
|
||||
show?: boolean;
|
||||
|
|
@ -52,7 +54,6 @@ type State = {
|
|||
show: boolean;
|
||||
isFocalTrapActive: boolean;
|
||||
}
|
||||
|
||||
export class GenericModal extends React.PureComponent<Props, State> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
show: true,
|
||||
|
|
@ -73,8 +74,15 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.show !== this.props.show) {
|
||||
this.setState({show: Boolean(this.props.show)});
|
||||
}
|
||||
}
|
||||
|
||||
onHide = () => {
|
||||
this.setState({show: false});
|
||||
this.props.onHide?.();
|
||||
};
|
||||
|
||||
handleCancel = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
|
|
@ -102,7 +110,7 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||
if (event.nativeEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
if (this.props.autoCloseOnConfirmButton) {
|
||||
if (this.props.handleConfirm && this.props.autoCloseOnConfirmButton) {
|
||||
this.onHide();
|
||||
}
|
||||
if (this.props.handleEnterKeyPress) {
|
||||
|
|
@ -199,6 +207,7 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||
backdropClassName={this.props.backdropClassName}
|
||||
container={this.props.container}
|
||||
keyboard={this.props.keyboardEscape}
|
||||
onEntered={this.props.onEntered}
|
||||
>
|
||||
<div
|
||||
onKeyDown={this.onEnterKeyDown}
|
||||
|
|
@ -206,23 +215,24 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||
className='GenericModal__wrapper-enter-key-press-catcher'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<div className='GenericModal__header__text_container'>
|
||||
<div
|
||||
className='GenericModal__header__text_container'
|
||||
>
|
||||
{this.props.compassDesign && (
|
||||
<>
|
||||
{headerText}
|
||||
{this.props.headerInput}
|
||||
</>
|
||||
)}
|
||||
|
||||
{
|
||||
this.props.modalSubheaderText &&
|
||||
<div className='modal-subheading-container'>
|
||||
<p
|
||||
<div
|
||||
id='genericModalSubheading'
|
||||
className='modal-subheading'
|
||||
>
|
||||
{this.props.modalSubheaderText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue