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