Mm 62677 modal focus second part (#30099)
* 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 * MM-62677 - migrate modals, invite modal work * user settings modal * fix snapshots * finish user settings migration * migrate confirm modal to use generic modal * notification preferences migration * implement focus back to trigger to channel notifications modal * fix test snapshots * initial self code review * fix CI errors, translation and some types * add modal location param and adjust test * fix cypress test text * fix cypress test text * fix e2e test for invitation modal * fix e2e test selector * adjust modal height * temp * fix e2e tests * fix snapshot * fix e2e tests * fix snapshots * fix snapshots * fix snapshots * fix e2e tests * update snapshots * fix snapshots * fix linter * Implement PR feedback * fix e2e tests * adjust styling for channel notifications modal * more fixes to e2e tests --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -73,7 +73,7 @@ describe('Managing bots in Teams and Channels', () => {
|
||||
cy.postMessage(`/invite @${bot.username} `);
|
||||
|
||||
// * Verify system message in-channel
|
||||
cy.uiWaitUntilMessagePostedIncludes(`@${bot.username} is not a member of the team.`);
|
||||
cy.uiWaitUntilMessagePostedIncludes(`You can add @${bot.username} to this channel once they are members of`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('Channel Info RHS', () => {
|
||||
cy.uiGetRHS().findByText('Notification Preferences').should('be.visible').click();
|
||||
|
||||
// * Ensures the modal is there
|
||||
cy.get('.channel-notifications-settings-modal').should('be.visible');
|
||||
cy.get('.ChannelNotificationModal').should('be.visible');
|
||||
});
|
||||
it('should be able to view files and come back', () => {
|
||||
// # Go to test channel
|
||||
@@ -401,7 +401,7 @@ describe('Channel Info RHS', () => {
|
||||
cy.uiGetRHS().findByText('Notification Preferences').should('be.visible').click();
|
||||
|
||||
// * Ensures the modal is there
|
||||
cy.get('.channel-notifications-settings-modal').should('be.visible');
|
||||
cy.get('.ChannelNotificationModal').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -206,7 +206,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
|
||||
cy.get('#invitePeople').should('be.visible').click();
|
||||
|
||||
// * Verify accessibility support in Invite People Dialog
|
||||
cy.get('.InvitationModal').should('have.attr', 'aria-modal', 'true').and('have.attr', 'aria-labelledby', 'invitation_modal_title').and('have.attr', 'role', 'dialog');
|
||||
cy.findByTestId('invitationModal').should('have.attr', 'aria-modal', 'true').and('have.attr', 'aria-labelledby', 'invitation_modal_title').and('have.attr', 'role', 'dialog');
|
||||
cy.get('#invitation_modal_title').should('be.visible').and('contain.text', 'Invite people to');
|
||||
|
||||
// # Press tab
|
||||
|
||||
@@ -82,8 +82,8 @@ describe('Guest Account - Verify Guest Access UI', () => {
|
||||
|
||||
// * Verify the confirmation message displayed
|
||||
cy.get('#confirmModal').should('be.visible').within(() => {
|
||||
cy.get('#confirmModalLabel').should('be.visible').and('have.text', 'Save and Disable Guest Access?');
|
||||
cy.get('.modal-body').should('be.visible').and('have.text', 'Disabling guest access will revoke all current Guest Account sessions. Guests will no longer be able to login and new guests cannot be invited into Mattermost. Guest users will be marked as inactive in user lists. Enabling this feature will not reinstate previous guest accounts. Are you sure you wish to remove these users?');
|
||||
cy.get('#genericModalLabel').should('be.visible').and('have.text', 'Save and Disable Guest Access?');
|
||||
cy.get('.ConfirmModal__body').should('be.visible').and('have.text', 'Disabling guest access will revoke all current Guest Account sessions. Guests will no longer be able to login and new guests cannot be invited into Mattermost. Guest users will be marked as inactive in user lists. Enabling this feature will not reinstate previous guest accounts. Are you sure you wish to remove these users?');
|
||||
cy.get('#confirmModalButton').should('have.text', 'Save and Disable Guest Access');
|
||||
});
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ describe('Guest Account - Verify Manage Guest Users', () => {
|
||||
|
||||
// * Verify the confirmation message displayed
|
||||
cy.get('#confirmModal').should('be.visible').within(() => {
|
||||
cy.get('#confirmModalLabel').should('be.visible').and('have.text', `Deactivate ${guestUser.username}`);
|
||||
cy.get('.modal-body').should('be.visible').and('have.text', `This action deactivates ${guestUser.username}. They will be logged out and not have access to any teams or channels on this system.\nAre you sure you want to deactivate ${guestUser.username}?`);
|
||||
cy.get('#genericModalLabel').should('be.visible').and('have.text', `Deactivate ${guestUser.username}`);
|
||||
cy.get('.modal-body .ConfirmModal__body').should('be.visible').and('have.text', `This action deactivates ${guestUser.username}. They will be logged out and not have access to any teams or channels on this system.\nAre you sure you want to deactivate ${guestUser.username}?`);
|
||||
});
|
||||
|
||||
// * Verify the behavior when Cancel button in the confirmation message is clicked
|
||||
|
||||
@@ -106,8 +106,8 @@ describe('Guest Account - Verify Manage Guest Users', () => {
|
||||
|
||||
// * Verify the confirmation message displayed
|
||||
cy.get('#confirmModal').should('be.visible').within(() => {
|
||||
cy.get('#confirmModalLabel').should('be.visible').and('have.text', `Revoke Sessions for ${guestUser.username}`);
|
||||
cy.get('.modal-body').should('be.visible').and('have.text', `This action revokes all sessions for ${guestUser.username}. They will be logged out from all devices. Are you sure you want to revoke all sessions for ${guestUser.username}?`);
|
||||
cy.get('#genericModalLabel').should('be.visible').and('have.text', `Revoke Sessions for ${guestUser.username}`);
|
||||
cy.get('.modal-body .ConfirmModal__body').should('be.visible').and('have.text', `This action revokes all sessions for ${guestUser.username}. They will be logged out from all devices. Are you sure you want to revoke all sessions for ${guestUser.username}?`);
|
||||
});
|
||||
|
||||
// * Verify the behavior when Cancel button in the confirmation message is clicked
|
||||
|
||||
@@ -169,14 +169,14 @@ describe('MM-23102 - Channel Moderation - Channel Mentions', () => {
|
||||
|
||||
// * Type at all and enter that no confirmation dialogue shows up
|
||||
cy.postMessage('@all ');
|
||||
cy.get('#confirmModalLabel').should('not.exist');
|
||||
cy.get('#genericModalLabel').should('not.exist');
|
||||
|
||||
// * Type at channel and enter that no confirmation dialogue shows up
|
||||
cy.postMessage('@channel ');
|
||||
cy.get('#confirmModalLabel').should('not.exist');
|
||||
cy.get('#genericModalLabel').should('not.exist');
|
||||
|
||||
// * Type at here and enter that no confirmation dialogue shows up
|
||||
cy.postMessage('@here ');
|
||||
cy.get('#confirmModalLabel').should('not.exist');
|
||||
cy.get('#genericModalLabel').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,11 +103,11 @@ describe('User Management', () => {
|
||||
function verifyManageUserSettingModal(user, writeAccess) {
|
||||
if (writeAccess) {
|
||||
cy.get('.manageUserSettingsBtn').should('be.visible').should('have.text', 'Manage User Settings').click();
|
||||
cy.get('#confirmModalLabel').should('be.visible').should('have.text', `Manage ${user.nickname}'s Settings`);
|
||||
cy.get('#genericModalLabel').should('be.visible').should('have.text', `Manage ${user.nickname}'s Settings`);
|
||||
|
||||
cy.get('#cancelModalButton').should('be.visible').should('have.text', 'Cancel');
|
||||
cy.get('#confirmModalButton').should('be.visible').should('have.text', 'Manage User Settings').click();
|
||||
cy.get('h2#accountSettingsModalLabel').should('be.visible').should('have.text', `Manage ${user.nickname}'s Settings`);
|
||||
cy.get('span#accountSettingsModalLabel').should('be.visible').should('have.text', `Manage ${user.nickname}'s Settings`);
|
||||
cy.get('.adminModeBadge').should('be.visible').should('have.text', 'Admin Mode');
|
||||
} else {
|
||||
cy.get('.manageUserSettingsBtn').should('not.exist');
|
||||
|
||||
@@ -127,6 +127,6 @@ function joinPrivateChannel(channel) {
|
||||
function verifyPrivateChannelJoinPromptIsVisible(channel) {
|
||||
// * Verify modal is shown before joining the private channel
|
||||
cy.get('#confirmModal').should('be.visible');
|
||||
cy.get('#confirmModalLabel').should('be.visible').and('have.text', 'Join private channel');
|
||||
cy.get('#genericModalLabel').should('be.visible').and('have.text', 'Join private channel');
|
||||
cy.get('#confirmModalBody').should('be.visible').and('have.text', `You are about to join ${channel.name} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?`);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('Notifications', () => {
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Close the modal
|
||||
cy.get('#accountSettingsHeader').find('button').should('be.visible').click();
|
||||
cy.uiClose();
|
||||
} else {
|
||||
// * Ensure that 'Send email notifications' is set to 'Immediately'
|
||||
cy.get('#emailNotificationImmediately').should('be.visible').and('be.checked');
|
||||
|
||||
@@ -43,7 +43,6 @@ describe('Settings > Display > Channel Display Mode', () => {
|
||||
cy.get('#channel_display_modeTitle').should('contain', 'Channel Display');
|
||||
cy.get('#channel_display_modeDesc').should('contain', 'Full width');
|
||||
cy.get('#channel_display_modeEdit').should('contain', 'Edit');
|
||||
cy.get('#accountSettingsHeader > .close').should('be.visible');
|
||||
});
|
||||
|
||||
it('should render in max setting view', () => {
|
||||
@@ -59,7 +58,6 @@ describe('Settings > Display > Channel Display Mode', () => {
|
||||
cy.get('#channel_display_modeFormatB').should('be.visible');
|
||||
cy.get('#saveSetting').should('contain', 'Save');
|
||||
cy.get('#cancelSetting').should('contain', 'Cancel');
|
||||
cy.get('#accountSettingsHeader > .close').should('be.visible');
|
||||
});
|
||||
|
||||
it('MM-T296 change channel display mode setting to "Full width"', () => {
|
||||
|
||||
@@ -38,7 +38,10 @@ describe('Settings > Display > Theme > Custom Theme Colors', () => {
|
||||
cy.get('#centerChannelStylesAccordion').click({force: true}).wait(TIMEOUTS.ONE_HUNDRED_MILLIS);
|
||||
|
||||
// # Select custom code theme
|
||||
cy.get('#codeThemeSelect').scrollIntoView().should('be.visible').select(theme.name);
|
||||
cy.get('#codeThemeSelect').
|
||||
scrollIntoView({offset: {top: 20, left: 0}}).
|
||||
should('exist').
|
||||
select(theme.name, {force: true});
|
||||
|
||||
// * Verify that the setting changes in the background?
|
||||
verifyLastPostStyle(theme);
|
||||
|
||||
@@ -196,7 +196,7 @@ function setTimezoneDisplayTo(isAutomatic, value) {
|
||||
});
|
||||
|
||||
// # Close Settings modal
|
||||
cy.get('#accountSettingsHeader > .close').should('be.visible').click();
|
||||
cy.uiClose();
|
||||
}
|
||||
|
||||
function setTimezoneDisplayToAutomatic(value) {
|
||||
|
||||
@@ -16,7 +16,7 @@ function uiCreateSidebarCategory(categoryName: string = `category-${getRandomId(
|
||||
// # Click on the sidebar menu dropdown and select Create Category
|
||||
cy.uiBrowseOrCreateChannel('Create new category');
|
||||
|
||||
cy.findByRole('dialog', {name: 'Rename Category'}).should('be.visible').within(() => {
|
||||
cy.findByRole('dialog', {name: 'Create New Category'}).should('be.visible').within(() => {
|
||||
// # Fill in the category name and click 'Create'
|
||||
cy.findByRole('textbox').should('be.visible').typeWithForce(categoryName).
|
||||
invoke('val').should('equal', categoryName);
|
||||
@@ -59,7 +59,7 @@ function uiMoveChannelToCategory(channelName: string, categoryName: string, newC
|
||||
});
|
||||
|
||||
if (newCategory) {
|
||||
cy.findByRole('dialog', {name: 'Rename Category'}).should('be.visible').within(() => {
|
||||
cy.findByRole('dialog', {name: 'Create New Category'}).should('be.visible').within(() => {
|
||||
// # Fill in the category name and click 'Create'
|
||||
cy.findByRole('textbox').should('be.visible').typeWithForce(categoryName).
|
||||
invoke('val').should('equal', categoryName);
|
||||
|
||||
@@ -43,8 +43,6 @@ export function openInvitationsModal(timeout = 1): ActionFunc {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.INVITATION,
|
||||
dialogType: InvitationModal,
|
||||
dialogProps: {
|
||||
},
|
||||
}));
|
||||
}, timeout);
|
||||
return {data: true};
|
||||
|
||||
@@ -766,6 +766,7 @@ exports[`SystemUserDetail should show manage user settings button as activated 1
|
||||
</button>
|
||||
<button
|
||||
class="manageUserSettingsBtn btn btn-tertiary"
|
||||
id="manageUserSettingsBtn"
|
||||
>
|
||||
Manage User Settings
|
||||
</button>
|
||||
|
||||
@@ -302,6 +302,7 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
||||
dialogProps: {
|
||||
user: this.state.user,
|
||||
onConfirm: this.openUserSettingsModal,
|
||||
focusOriginElement: 'manageUserSettingsBtn',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -318,6 +319,7 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
||||
adminMode: true,
|
||||
isContentProductSettings: true,
|
||||
userID: this.state.user.id,
|
||||
focusOriginElement: 'manageUserSettingsBtn',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -448,6 +450,7 @@ export class SystemUserDetail extends PureComponent<Props, State> {
|
||||
<button
|
||||
className='manageUserSettingsBtn btn btn-tertiary'
|
||||
onClick={this.openConfirmEditUserSettingsModal}
|
||||
id='manageUserSettingsBtn'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.manageSettings'
|
||||
|
||||
@@ -8,15 +8,22 @@ import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import {getDisplayName} from 'utils/utils';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
onConfirm: () => void;
|
||||
onExited: () => void;
|
||||
focusOriginElement: string;
|
||||
}
|
||||
|
||||
export default function ConfirmManageUserSettingsModal(props: Props) {
|
||||
const handleExited = () => {
|
||||
props.onExited?.();
|
||||
focusElement(props.focusOriginElement, true, true);
|
||||
};
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='userSettings.adminMode.modal_header'
|
||||
@@ -49,7 +56,7 @@ export default function ConfirmManageUserSettingsModal(props: Props) {
|
||||
message={message}
|
||||
confirmButtonText={confirmButtonText}
|
||||
onConfirm={props.onConfirm}
|
||||
onExited={props.onExited}
|
||||
onExited={handleExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
|
||||
adminMode: true,
|
||||
isContentProductSettings: true,
|
||||
userID: user.id,
|
||||
focusOriginElement: menuButtonId,
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -144,6 +145,7 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
|
||||
dialogProps: {
|
||||
user,
|
||||
onConfirm: onConfirmManageUserSettingsClick,
|
||||
focusOriginElement: menuButtonId,
|
||||
},
|
||||
}));
|
||||
}, [user]);
|
||||
|
||||
@@ -135,7 +135,6 @@ function NotificationFromMembersModal(props: Props) {
|
||||
backdrop={true}
|
||||
show={show}
|
||||
onExited={handleOnClose}
|
||||
aria-modal='true'
|
||||
modalHeaderText={modalHeaderText}
|
||||
>
|
||||
<MembersContainer>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
exports[`components/BrowseChannels should match snapshot and state 1`] = `
|
||||
<GenericModal
|
||||
aria-labelledby="browseChannelsModalLabel"
|
||||
aria-modal={true}
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={false}
|
||||
@@ -40,8 +39,11 @@ exports[`components/BrowseChannels should match snapshot and state 1`] = `
|
||||
id="more_channels.title"
|
||||
/>
|
||||
}
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<injectIntl(SearchableChannelList)
|
||||
canShowArchivedChannels={true}
|
||||
|
||||
@@ -372,7 +372,6 @@ export default class BrowseChannels extends React.PureComponent<Props, State> {
|
||||
return (
|
||||
<GenericModal
|
||||
id='browseChannelsModal'
|
||||
aria-labelledby='browseChannelsModalLabel'
|
||||
onExited={this.handleExit}
|
||||
compassDesign={true}
|
||||
modalHeaderText={title}
|
||||
|
||||
@@ -147,6 +147,7 @@ const ChannelHeaderTitle = ({
|
||||
id='channelHeaderDropdownButton'
|
||||
>
|
||||
<button
|
||||
id='channelHeaderMenuButton'
|
||||
className={classNames('channel-header__trigger style--none', {active: titleMenuOpen})}
|
||||
aria-label={
|
||||
(isDirect && currentUser.id === dmUser?.id) ? personalChannelHeaderAriaLabel.toLowerCase() : othersChannelHeaderAriaLabel.toLowerCase()
|
||||
|
||||
@@ -152,6 +152,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
|
||||
"update_at": 0,
|
||||
"username": "some-user",
|
||||
},
|
||||
"focusOriginElement": "channelHeaderMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
@@ -280,7 +281,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "channel_header.menuAriaLabel",
|
||||
"focusOriginElement": "channelHeaderMenuButton",
|
||||
"isExistingChannel": true,
|
||||
}
|
||||
}
|
||||
@@ -985,6 +986,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
|
||||
"update_at": 0,
|
||||
"username": "some-user",
|
||||
},
|
||||
"focusOriginElement": "channelHeaderMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
@@ -1113,7 +1115,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "channel_header.menuAriaLabel",
|
||||
"focusOriginElement": "channelHeaderMenuButton",
|
||||
"isExistingChannel": true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ export default class ChannelHeaderDropdown extends React.PureComponent<Props> {
|
||||
dialogProps={{
|
||||
channel,
|
||||
currentUser: user,
|
||||
focusOriginElement: 'channelHeaderMenuButton',
|
||||
}}
|
||||
text={localizeMessage({id: 'navbar.preferences', defaultMessage: 'Notification Preferences'})}
|
||||
/>
|
||||
@@ -167,7 +168,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, focusOriginElement: 'channel_header.menuAriaLabel'}}
|
||||
dialogProps={{isExistingChannel: true, focusOriginElement: 'channelHeaderMenuButton'}}
|
||||
text={localizeMessage({id: 'navbar.addMembers', defaultMessage: 'Add Members'})}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
|
||||
@@ -131,7 +131,7 @@ const ChannelInfoRhs = ({
|
||||
const openNotificationSettings = () => actions.openModal({
|
||||
modalId: ModalIdentifiers.CHANNEL_NOTIFICATIONS,
|
||||
dialogType: ChannelNotificationsModal,
|
||||
dialogProps: {channel, currentUser},
|
||||
dialogProps: {channel, currentUser, focusOriginElement: 'channelInfoRHSNotificationSettings'},
|
||||
});
|
||||
|
||||
const gmUsers = channelMembers.filter((user) => {
|
||||
|
||||
@@ -68,10 +68,11 @@ interface MenuItemProps {
|
||||
opensSubpanel?: boolean;
|
||||
badge?: string | number | JSX.Element;
|
||||
onClick: () => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
function MenuItem(props: MenuItemProps) {
|
||||
const {icon, text, opensSubpanel, badge, onClick} = props;
|
||||
const {icon, text, opensSubpanel, badge, onClick, id} = props;
|
||||
const hasRightSide = (badge !== undefined) || opensSubpanel;
|
||||
|
||||
return (
|
||||
@@ -79,6 +80,7 @@ function MenuItem(props: MenuItemProps) {
|
||||
onClick={onClick}
|
||||
aria-label={text}
|
||||
type='button'
|
||||
id={id || ''}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
<MenuItemText>{text}</MenuItemText>
|
||||
@@ -148,6 +150,7 @@ export default function Menu(props: MenuProps) {
|
||||
>
|
||||
{showNotificationPreferences && (
|
||||
<MenuItem
|
||||
id='channelInfoRHSNotificationSettings'
|
||||
icon={<i className='icon icon-bell-outline'/>}
|
||||
text={formatMessage({
|
||||
id: 'channel_info_rhs.menu.notification_preferences',
|
||||
|
||||
@@ -460,10 +460,9 @@ export class ChannelInviteModal extends React.PureComponent<Props, State> {
|
||||
this.props.actions.closeModal(ModalIdentifiers.CHANNEL_INVITE);
|
||||
};
|
||||
|
||||
const InviteModalLink = (props: {inviteAsGuest?: boolean; children: React.ReactNode}) => {
|
||||
const InviteModalLink = (props: {inviteAsGuest?: boolean; children: React.ReactNode; id?: string}) => {
|
||||
return (
|
||||
<ToggleModalButton
|
||||
id='inviteGuest'
|
||||
className={`${props.inviteAsGuest ? 'invite-as-guest' : ''} btn btn-link`}
|
||||
modalId={ModalIdentifiers.INVITATION}
|
||||
dialogType={InvitationModal}
|
||||
@@ -471,8 +470,10 @@ export class ChannelInviteModal extends React.PureComponent<Props, State> {
|
||||
channelToInvite: this.props.channel,
|
||||
initialValue: this.state.term,
|
||||
inviteAsGuest: props.inviteAsGuest,
|
||||
focusOriginElement: 'customNoOptionsMessageLink',
|
||||
}}
|
||||
onClick={closeMembersInviteModal}
|
||||
id={props.id}
|
||||
>
|
||||
{props.children}
|
||||
</ToggleModalButton>
|
||||
@@ -480,13 +481,15 @@ export class ChannelInviteModal extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
const customNoOptionsMessage = (
|
||||
<div className='custom-no-options-message'>
|
||||
<div
|
||||
className='custom-no-options-message'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_invite.no_options_message'
|
||||
defaultMessage='No matches found - <InvitationModalLink>Invite them to the team</InvitationModalLink>'
|
||||
values={{
|
||||
InvitationModalLink: (chunks: string) => (
|
||||
<InviteModalLink>
|
||||
<InviteModalLink id='customNoOptionsMessageLink'>
|
||||
{chunks}
|
||||
</InviteModalLink>
|
||||
),
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,17 +1,61 @@
|
||||
.modal .channel-notifications-settings-modal {
|
||||
overflow: hidden;
|
||||
.modal .ChannelNotificationModal {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin-top: -24px;
|
||||
margin-top: 5vh !important;
|
||||
overflow-y: hidden;
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
max-height: calc(100vh - 240px);
|
||||
flex-direction: column;
|
||||
border: var(--border-default);
|
||||
border-radius: var(--radius-l);
|
||||
box-shadow: var(--elevation-6);
|
||||
overflow-y: hidden;
|
||||
|
||||
.GenericModal__wrapper {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
max-height: calc(100vh - 160px);
|
||||
flex-direction: column;
|
||||
border: var(--border-default);
|
||||
border-radius: var(--radius-l);
|
||||
box-shadow: var(--elevation-6);
|
||||
|
||||
.modal-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
min-height: 150px;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
min-height: 150px;
|
||||
flex-direction: column;
|
||||
padding: 28px 32px;
|
||||
gap: 24px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__ctr {
|
||||
@@ -19,35 +63,6 @@
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border: none;
|
||||
border-bottom: var(--border-light);
|
||||
background: none;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
min-height: 150px;
|
||||
flex-direction: column;
|
||||
padding: 28px 32px;
|
||||
gap: 24px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 24px 32px;
|
||||
border-top: var(--border-light);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__reset-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -97,4 +112,11 @@
|
||||
max-height: calc(100vh - 160px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 600px) {
|
||||
.modal-content, .GenericModal__wrapper {
|
||||
max-height: 85vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import type {OnChangeValue} from 'react-select';
|
||||
|
||||
import {BellOffOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
import type {Channel, ChannelMembership, ChannelNotifyProps} from '@mattermost/types/channels';
|
||||
import type {UserNotifyProps, UserProfile} from '@mattermost/types/users';
|
||||
|
||||
@@ -18,6 +18,7 @@ import ModalSection from 'components/widgets/modals/components/modal_section';
|
||||
import RadioSettingItem from 'components/widgets/modals/components/radio_setting_item';
|
||||
import type {Option} from 'components/widgets/modals/components/react_select_item';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import {NotificationLevels, DesktopSound, IgnoreChannelMentions} from 'utils/constants';
|
||||
import {convertDesktopSoundNotifyPropFromUserToDesktop, DesktopNotificationSounds, getValueOfNotificationSoundsSelect, stopTryNotificationRing, tryNotificationSound} from 'utils/notification_sounds';
|
||||
|
||||
@@ -44,6 +45,8 @@ export type Props = PropsFromRedux & {
|
||||
* Object with info about current user
|
||||
*/
|
||||
currentUser: UserProfile;
|
||||
|
||||
focusOriginElement?: string;
|
||||
};
|
||||
|
||||
export default function ChannelNotificationsModal(props: Props) {
|
||||
@@ -66,6 +69,13 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
setShow(false);
|
||||
}
|
||||
|
||||
function handleExited() {
|
||||
if (props.focusOriginElement) {
|
||||
focusElement(props.focusOriginElement, true);
|
||||
}
|
||||
props.onExited?.();
|
||||
}
|
||||
|
||||
const handleChange = useCallback((values: Record<string, string>) => {
|
||||
setSettings((prevSettings) => ({...prevSettings, ...values}));
|
||||
}, []);
|
||||
@@ -87,7 +97,7 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
|
||||
function handleResetToDefaultClicked(channelNotifyPropsDefaultedToUserNotifyProps: ChannelMembership['notify_props'], sectionName: SectionName) {
|
||||
if (sectionName === SectionName.Mobile) {
|
||||
const desktopAndMobileSettingsDifferent = areDesktopAndMobileSettingsDifferent(
|
||||
const desktopAndMobileSettingsDiff = areDesktopAndMobileSettingsDifferent(
|
||||
props.collapsedReplyThreads,
|
||||
settings.desktop,
|
||||
settings.desktop_threads,
|
||||
@@ -95,7 +105,7 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
channelNotifyPropsDefaultedToUserNotifyProps.push_threads,
|
||||
);
|
||||
|
||||
setDesktopAndMobileSettingDifferent(desktopAndMobileSettingsDifferent);
|
||||
setDesktopAndMobileSettingDifferent(desktopAndMobileSettingsDiff);
|
||||
}
|
||||
|
||||
setSettings({...settings, ...channelNotifyPropsDefaultedToUserNotifyProps});
|
||||
@@ -277,7 +287,7 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
|
||||
const desktopAndMobileNotificationSectionContent = settings.mark_unread === 'all' ? (
|
||||
<>
|
||||
<div className='channel-notifications-settings-modal__divider'/>
|
||||
<div className='ChannelNotificationModal__divider'/>
|
||||
<ModalSection
|
||||
title={formatMessage({
|
||||
id: 'channel_notifications.desktopNotificationsTitle',
|
||||
@@ -297,7 +307,7 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
})}
|
||||
content={desktopNotificationsSectionContent}
|
||||
/>
|
||||
<div className='channel-notifications-settings-modal__divider'/>
|
||||
<div className='ChannelNotificationModal__divider'/>
|
||||
<ModalSection
|
||||
title={formatMessage({
|
||||
id: 'channel_notifications.mobileNotificationsTitle',
|
||||
@@ -344,32 +354,70 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
/>
|
||||
);
|
||||
|
||||
const footerContent = (
|
||||
<footer className='ChannelNotificationModal__footer'>
|
||||
{serverError &&
|
||||
<span
|
||||
role='alert'
|
||||
className='ChannelNotificationModal__server-error'
|
||||
>
|
||||
{serverError}
|
||||
</span>
|
||||
}
|
||||
<button
|
||||
className='btn btn-tertiary btn-md'
|
||||
onClick={handleHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='generic_btn.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-primary btn-md'
|
||||
onClick={handleSave}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='generic_btn.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</footer>
|
||||
);
|
||||
|
||||
const headerText = (
|
||||
<ModalHeader
|
||||
id={'channelNotificationModalLabel'}
|
||||
title={formatMessage({
|
||||
id: 'channel_notifications.preferences',
|
||||
defaultMessage: 'Notification Preferences',
|
||||
})}
|
||||
subtitle={props.channel.display_name}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal channel-notifications-settings-modal'
|
||||
<GenericModal
|
||||
id='channelNotificationModal'
|
||||
className='a11y__modal ChannelNotificationModal modal--overflow'
|
||||
show={show}
|
||||
onHide={handleHide}
|
||||
onExited={props.onExited}
|
||||
role='none'
|
||||
aria-labelledby='channelNotificationModalLabel'
|
||||
style={{display: 'flex', placeItems: 'center'}}
|
||||
onExited={handleExited}
|
||||
ariaLabel='channelNotificationModalLabel'
|
||||
compassDesign={true}
|
||||
footerContent={footerContent}
|
||||
modalHeaderText={headerText}
|
||||
bodyPadding={false}
|
||||
footerDivider={true}
|
||||
>
|
||||
<ModalHeader
|
||||
id={'channelNotificationModalLabel'}
|
||||
title={formatMessage({
|
||||
id: 'channel_notifications.preferences',
|
||||
defaultMessage: 'Notification Preferences',
|
||||
})}
|
||||
subtitle={props.channel.display_name}
|
||||
handleClose={handleHide}
|
||||
/>
|
||||
<main className='channel-notifications-settings-modal__body'>
|
||||
<fieldset aria-labelledby='channel-notifications-settings-modal-legend'>
|
||||
|
||||
<main className='ChannelNotificationModal__body'>
|
||||
<fieldset aria-labelledby='ChannelNotificationModal-legend'>
|
||||
<ModalSection
|
||||
title={
|
||||
<legend
|
||||
style={{all: 'unset'}}
|
||||
id='channel-notifications-settings-modal-legend'
|
||||
id='ChannelNotificationModal-legend'
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'channel_notifications.muteAndIgnore',
|
||||
@@ -383,7 +431,7 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
{desktopAndMobileNotificationSectionContent}
|
||||
{props.collapsedReplyThreads &&
|
||||
<>
|
||||
<div className='channel-notifications-settings-modal__divider'/>
|
||||
<div className='ChannelNotificationModal__divider'/>
|
||||
<ModalSection
|
||||
title={formatMessage({
|
||||
id: 'channel_notifications.autoFollowThreadsTitle',
|
||||
@@ -398,53 +446,60 @@ export default function ChannelNotificationsModal(props: Props) {
|
||||
</>
|
||||
}
|
||||
</main>
|
||||
<footer className='channel-notifications-settings-modal__footer'>
|
||||
{serverError &&
|
||||
<span
|
||||
role='alert'
|
||||
className='channel-notifications-settings-modal__server-error'
|
||||
>
|
||||
{serverError}
|
||||
</span>
|
||||
}
|
||||
<button
|
||||
className='btn btn-tertiary btn-md'
|
||||
onClick={handleHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='generic_btn.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-primary btn-md'
|
||||
onClick={handleSave}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='generic_btn.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
);
|
||||
}
|
||||
|
||||
function getStateFromNotifyProps(currentUserNotifyProps: UserNotifyProps, channelMemberNotifyProps?: ChannelMembership['notify_props']): Required<Omit<ChannelMembership['notify_props'], 'email'>> {
|
||||
function getStateFromNotifyProps(
|
||||
currentUserNotifyProps: UserNotifyProps,
|
||||
channelMemberNotifyProps?: ChannelMembership['notify_props'],
|
||||
): Required<Omit<ChannelMembership['notify_props'], 'email'>> {
|
||||
return {
|
||||
mark_unread: channelMemberNotifyProps?.mark_unread ?? NotificationLevels.ALL,
|
||||
ignore_channel_mentions: getInitialValuesOfIgnoreChannelMentions(channelMemberNotifyProps?.mark_unread, channelMemberNotifyProps?.ignore_channel_mentions, currentUserNotifyProps?.channel),
|
||||
desktop: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['desktop']>(NotificationLevels.ALL, channelMemberNotifyProps?.desktop, currentUserNotifyProps?.desktop),
|
||||
desktop_threads: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['desktop_threads']>(NotificationLevels.ALL, channelMemberNotifyProps?.desktop_threads, currentUserNotifyProps?.desktop_threads),
|
||||
desktop_sound: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['desktop_sound']>(DesktopSound.ON, channelMemberNotifyProps?.desktop_sound, convertDesktopSoundNotifyPropFromUserToDesktop(currentUserNotifyProps?.desktop_sound)),
|
||||
desktop_notification_sound: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['desktop_notification_sound']>(DesktopNotificationSounds.BING, channelMemberNotifyProps?.desktop_notification_sound, currentUserNotifyProps?.desktop_notification_sound),
|
||||
push: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['push']>(NotificationLevels.ALL, channelMemberNotifyProps?.push, currentUserNotifyProps?.push),
|
||||
push_threads: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['push_threads']>(NotificationLevels.ALL, channelMemberNotifyProps?.push_threads, currentUserNotifyProps?.push_threads),
|
||||
ignore_channel_mentions: getInitialValuesOfIgnoreChannelMentions(
|
||||
channelMemberNotifyProps?.mark_unread,
|
||||
channelMemberNotifyProps?.ignore_channel_mentions,
|
||||
currentUserNotifyProps?.channel,
|
||||
),
|
||||
desktop: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['desktop']>(
|
||||
NotificationLevels.ALL,
|
||||
channelMemberNotifyProps?.desktop,
|
||||
currentUserNotifyProps?.desktop,
|
||||
),
|
||||
desktop_threads: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['desktop_threads']>(
|
||||
NotificationLevels.ALL,
|
||||
channelMemberNotifyProps?.desktop_threads,
|
||||
currentUserNotifyProps?.desktop_threads,
|
||||
),
|
||||
desktop_sound: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['desktop_sound']>(
|
||||
DesktopSound.ON,
|
||||
channelMemberNotifyProps?.desktop_sound,
|
||||
convertDesktopSoundNotifyPropFromUserToDesktop(currentUserNotifyProps?.desktop_sound),
|
||||
),
|
||||
desktop_notification_sound: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['desktop_notification_sound']>(
|
||||
DesktopNotificationSounds.BING,
|
||||
channelMemberNotifyProps?.desktop_notification_sound,
|
||||
currentUserNotifyProps?.desktop_notification_sound,
|
||||
),
|
||||
push: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['push']>(
|
||||
NotificationLevels.ALL,
|
||||
channelMemberNotifyProps?.push,
|
||||
currentUserNotifyProps?.push,
|
||||
),
|
||||
push_threads: getInitialValuesOfChannelNotifyProps<ChannelMembership['notify_props']['push_threads']>(
|
||||
NotificationLevels.ALL,
|
||||
channelMemberNotifyProps?.push_threads,
|
||||
currentUserNotifyProps?.push_threads,
|
||||
),
|
||||
channel_auto_follow_threads: channelMemberNotifyProps?.channel_auto_follow_threads ?? 'off',
|
||||
};
|
||||
}
|
||||
|
||||
export function getInitialValuesOfChannelNotifyProps<KeyInNotifyProps>(defaultValue: NonNullable<KeyInNotifyProps>, channelMemberNotifyProp: KeyInNotifyProps | undefined = undefined, userNotifyProp: KeyInNotifyProps | undefined = undefined) {
|
||||
export function getInitialValuesOfChannelNotifyProps<KeyInNotifyProps>(
|
||||
defaultValue: NonNullable<KeyInNotifyProps>,
|
||||
channelMemberNotifyProp: KeyInNotifyProps | undefined = undefined,
|
||||
userNotifyProp: KeyInNotifyProps | undefined = undefined,
|
||||
) {
|
||||
let value = defaultValue;
|
||||
|
||||
// Check if channel_member's notify_prop is defined for the selected notify_prop
|
||||
@@ -470,7 +525,10 @@ export function getInitialValuesOfIgnoreChannelMentions(
|
||||
userNotifyPropForChannel: UserNotifyProps['channel'],
|
||||
): NonNullable<ChannelMembership['notify_props']['ignore_channel_mentions']> {
|
||||
let ignoreChannelMentionsDefault: ChannelNotifyProps['ignore_channel_mentions'] = IgnoreChannelMentions.OFF;
|
||||
if (markUnread === NotificationLevels.MENTION || (userNotifyPropForChannel && userNotifyPropForChannel === 'false')) {
|
||||
if (
|
||||
markUnread === NotificationLevels.MENTION ||
|
||||
(userNotifyPropForChannel && userNotifyPropForChannel === 'false')
|
||||
) {
|
||||
ignoreChannelMentionsDefault = IgnoreChannelMentions.ON;
|
||||
}
|
||||
|
||||
@@ -533,8 +591,10 @@ export function createChannelNotifyPropsFromSelectedSettings(
|
||||
// Otherwise, we should use the CHANNEL's new desktop_notification_sound setting as is
|
||||
channelNotifyProps.desktop_notification_sound = savedChannelNotifyProps.desktop_notification_sound;
|
||||
}
|
||||
} else if (savedChannelNotifyProps.desktop_notification_sound === DesktopNotificationSounds.BING || savedChannelNotifyProps.desktop_notification_sound === DesktopNotificationSounds.DEFAULT) {
|
||||
// If USER's desktop_notification_sound setting is not defined and CHANNEL's new desktop_notification_sound setting is either BING or DEFAULT, then save it as default
|
||||
} else if (
|
||||
savedChannelNotifyProps.desktop_notification_sound === DesktopNotificationSounds.BING ||
|
||||
savedChannelNotifyProps.desktop_notification_sound === DesktopNotificationSounds.DEFAULT
|
||||
) {
|
||||
channelNotifyProps.desktop_notification_sound = DesktopNotificationSounds.DEFAULT;
|
||||
} else {
|
||||
// Otherwise, we should use the CHANNEL's new desktop_notification_sound setting as is
|
||||
@@ -600,6 +660,5 @@ export function areDesktopAndMobileSettingsDifferent(
|
||||
if (push === NotificationLevels.DEFAULT || push === desktop) {
|
||||
return isCollapsedThreadsEnabled && desktopThreads !== pushThreads;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`ResetToDefaultButton should render if section name is valid 1`] = `
|
||||
<div>
|
||||
<button
|
||||
class="channel-notifications-settings-modal__reset-btn"
|
||||
class="ChannelNotificationModal__reset-btn"
|
||||
data-testid="resetToDefaultButton-desktop"
|
||||
>
|
||||
<i
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function ResetToDefaultButton(props: Props) {
|
||||
|
||||
return (
|
||||
<button
|
||||
className='channel-notifications-settings-modal__reset-btn'
|
||||
className='ChannelNotificationModal__reset-btn'
|
||||
onClick={handleOnClick}
|
||||
data-testid={`resetToDefaultButton-${props.sectionName}`}
|
||||
>
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
|
||||
.modal-header .modal-title {
|
||||
font-family: Metropolis, sans-serif;
|
||||
font-weight: 600;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.modal-confirm .modal-footer .checkbox {
|
||||
margin-right: auto;
|
||||
.ConfirmModal {
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
padding: 32px 0;
|
||||
|
||||
.checkbox {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
|
||||
import './confirm_modal.scss';
|
||||
|
||||
type Props = {
|
||||
@@ -167,45 +168,40 @@ export default class ConfirmModal extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<GenericModal
|
||||
id={classNames('confirmModal', this.props.id)}
|
||||
className={'modal-confirm ' + this.props.modalClass}
|
||||
dialogClassName='a11y__modal'
|
||||
className={`ConfirmModal a11y__modal ${this.props.modalClass}`}
|
||||
show={this.props.show}
|
||||
onHide={this.handleCancel}
|
||||
onExited={this.props.onExited}
|
||||
role='none'
|
||||
aria-modal={true}
|
||||
aria-labelledby='confirmModalLabel'
|
||||
aria-describedby='confirmModalBody'
|
||||
data-testid={this.props.id}
|
||||
compassDesign={true}
|
||||
modalHeaderText={this.props.title}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title
|
||||
componentClass='h1'
|
||||
id='confirmModalLabel'
|
||||
<div
|
||||
data-testid={this.props.id}
|
||||
>
|
||||
<div
|
||||
className='ConfirmModal__body'
|
||||
id='confirmModalBody'
|
||||
>
|
||||
{this.props.title}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body id='confirmModalBody'>
|
||||
{this.props.message}
|
||||
{!this.props.checkboxInFooter && checkbox}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{this.props.checkboxInFooter && checkbox}
|
||||
{cancelButton}
|
||||
<button
|
||||
autoFocus={true}
|
||||
type='button'
|
||||
className={this.props.confirmButtonClass}
|
||||
onClick={this.handleConfirm}
|
||||
id='confirmModalButton'
|
||||
>
|
||||
{this.props.confirmButtonText}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
{this.props.message}
|
||||
{!this.props.checkboxInFooter && checkbox}
|
||||
</div>
|
||||
<div className='ConfirmModal__footer'>
|
||||
{this.props.checkboxInFooter && checkbox}
|
||||
{cancelButton}
|
||||
<button
|
||||
autoFocus={true}
|
||||
type='button'
|
||||
className={this.props.confirmButtonClass}
|
||||
onClick={this.handleConfirm}
|
||||
id='confirmModalButton'
|
||||
>
|
||||
{this.props.confirmButtonText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,11 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
|
||||
isDeleteModal={true}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Delete draft"
|
||||
modalLocation="center"
|
||||
onExited={[MockFunction]}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Are you sure you want to delete this draft to <strong>{displayName}</strong>?"
|
||||
@@ -58,6 +61,7 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
|
||||
isDeleteModal={true}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Delete draft"
|
||||
modalLocation="center"
|
||||
onExited={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
@@ -72,6 +76,8 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
|
||||
}
|
||||
}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Are you sure you want to delete this draft to <strong>{displayName}</strong>?"
|
||||
|
||||
@@ -25,8 +25,11 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onC
|
||||
id="genericModal"
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Send message now"
|
||||
modalLocation="center"
|
||||
onExited={[MockFunction]}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Are you sure you want to send this message to <strong>{displayName}</strong>?"
|
||||
@@ -54,6 +57,7 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onE
|
||||
id="genericModal"
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Send message now"
|
||||
modalLocation="center"
|
||||
onExited={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
@@ -68,6 +72,8 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onE
|
||||
}
|
||||
}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Are you sure you want to send this message to <strong>{displayName}</strong>?"
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('components/drafts/draft_actions/delete_draft_modal', () => {
|
||||
<DeleteDraftModal {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.find(GenericModal).first().props().onExited();
|
||||
wrapper.find(GenericModal).first().props().onExited?.();
|
||||
expect(baseProps.onExited).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('components/drafts/draft_actions/send_draft_modal', () => {
|
||||
<SendDraftModal {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.find(GenericModal).first().props().onExited();
|
||||
wrapper.find(GenericModal).first().props().onExited?.();
|
||||
expect(baseProps.onExited).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -140,7 +140,6 @@ export default class EditCategoryModal extends React.PureComponent<Props, State>
|
||||
return (
|
||||
<GenericModal
|
||||
id='editCategoryModal'
|
||||
ariaLabel={localizeMessage({id: 'rename_category_modal.renameCategory', defaultMessage: 'Rename Category'})}
|
||||
modalHeaderText={modalHeaderText}
|
||||
confirmButtonText={editButtonText}
|
||||
compassDesign={true}
|
||||
|
||||
@@ -35,7 +35,7 @@ const SettingsButton = (props: Props): JSX.Element | null => {
|
||||
size={'sm'}
|
||||
icon={'settings-outline'}
|
||||
onClick={(): void => {
|
||||
props.actions.openModal({modalId: ModalIdentifiers.USER_SETTINGS, dialogType: UserSettingsModal, dialogProps: {isContentProductSettings: true}});
|
||||
props.actions.openModal({modalId: ModalIdentifiers.USER_SETTINGS, dialogType: UserSettingsModal, dialogProps: {isContentProductSettings: true, focusOriginElement: 'settings_button'}});
|
||||
}}
|
||||
inverted={true}
|
||||
compact={true}
|
||||
|
||||
@@ -497,46 +497,34 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
|
||||
show={false}
|
||||
title="Save Outgoing OAuth Connection"
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm "
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
className="ConfirmModal a11y__modal "
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Save Outgoing OAuth Connection"
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
bsClass="modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign ConfirmModal a11y__modal "
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
@@ -550,15 +538,43 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</AbstractOutgoingOAuthConnection>
|
||||
@@ -1084,46 +1100,34 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
|
||||
show={false}
|
||||
title="Save Outgoing OAuth Connection"
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm "
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
className="ConfirmModal a11y__modal "
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Save Outgoing OAuth Connection"
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
bsClass="modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign ConfirmModal a11y__modal "
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
@@ -1137,15 +1141,43 @@ exports[`components/integrations/AbstractOutgoingOAuthConnection should match sn
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</AbstractOutgoingOAuthConnection>
|
||||
|
||||
@@ -486,46 +486,34 @@ exports[`components/integrations/AddOutgoingOAuthConnection should match snapsho
|
||||
show={false}
|
||||
title="Save Outgoing OAuth Connection"
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm "
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
className="ConfirmModal a11y__modal "
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Save Outgoing OAuth Connection"
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
bsClass="modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign ConfirmModal a11y__modal "
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
@@ -539,15 +527,43 @@ exports[`components/integrations/AddOutgoingOAuthConnection should match snapsho
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</AbstractOutgoingOAuthConnection>
|
||||
|
||||
@@ -548,45 +548,38 @@ https://myothersite.com/api/v2"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm integrations-backstage-modal"
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
className="ConfirmModal a11y__modal integrations-backstage-modal"
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
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="Edit Outgoing OAuth Connection"
|
||||
id="update_outgoing_oauth_connection.confirm"
|
||||
/>
|
||||
}
|
||||
modalLocation="center"
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
bsClass="modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign ConfirmModal a11y__modal integrations-backstage-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
@@ -600,15 +593,42 @@ https://myothersite.com/api/v2"
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</form>
|
||||
@@ -648,46 +668,34 @@ https://myothersite.com/api/v2"
|
||||
show={false}
|
||||
title="Save Outgoing OAuth Connection"
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm "
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
className="ConfirmModal a11y__modal "
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Save Outgoing OAuth Connection"
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
bsClass="modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign ConfirmModal a11y__modal "
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
@@ -701,15 +709,43 @@ https://myothersite.com/api/v2"
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</AbstractOutgoingOAuthConnection>
|
||||
@@ -1268,45 +1304,38 @@ https://myothersite.com/api/v2"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm integrations-backstage-modal"
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
className="ConfirmModal a11y__modal integrations-backstage-modal"
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
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="Edit Outgoing OAuth Connection"
|
||||
id="update_outgoing_oauth_connection.confirm"
|
||||
/>
|
||||
}
|
||||
modalLocation="center"
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
bsClass="modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign ConfirmModal a11y__modal integrations-backstage-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
@@ -1320,15 +1349,42 @@ https://myothersite.com/api/v2"
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1368,46 +1424,34 @@ https://myothersite.com/api/v2"
|
||||
show={false}
|
||||
title="Save Outgoing OAuth Connection"
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm "
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
className="ConfirmModal a11y__modal "
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Save Outgoing OAuth Connection"
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
bsClass="modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign ConfirmModal a11y__modal "
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
@@ -1421,15 +1465,43 @@ https://myothersite.com/api/v2"
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</AbstractOutgoingOAuthConnection>
|
||||
@@ -1987,45 +2059,38 @@ https://myothersite.com/api/v2"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm integrations-backstage-modal"
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
className="ConfirmModal a11y__modal integrations-backstage-modal"
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
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="Edit Outgoing OAuth Connection"
|
||||
id="update_outgoing_oauth_connection.confirm"
|
||||
/>
|
||||
}
|
||||
modalLocation="center"
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
bsClass="modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign ConfirmModal a11y__modal integrations-backstage-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
@@ -2039,15 +2104,42 @@ https://myothersite.com/api/v2"
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</form>
|
||||
@@ -2087,46 +2179,34 @@ https://myothersite.com/api/v2"
|
||||
show={false}
|
||||
title="Save Outgoing OAuth Connection"
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-describedby="confirmModalBody"
|
||||
aria-labelledby="confirmModalLabel"
|
||||
aria-modal={true}
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
className="modal-confirm "
|
||||
dialogClassName="a11y__modal"
|
||||
dialogComponentClass={[Function]}
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
className="ConfirmModal a11y__modal "
|
||||
compassDesign={true}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
keyboardEscape={true}
|
||||
modalHeaderText="Save Outgoing OAuth Connection"
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
bsClass="modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign ConfirmModal a11y__modal "
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="confirmModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
@@ -2140,15 +2220,43 @@ https://myothersite.com/api/v2"
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="none"
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<Modal
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
backdropClassName="modal-backdrop"
|
||||
backdropTransition={[Function]}
|
||||
containerClassName="modal-open"
|
||||
enforceFocus={true}
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onEntering={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
show={false}
|
||||
transition={[Function]}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</AbstractOutgoingOAuthConnection>
|
||||
|
||||
@@ -12,5 +12,13 @@
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
|
||||
}
|
||||
&__header {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ const defaultProps: Props = deepFreeze({
|
||||
townSquareDisplayName: '',
|
||||
onExited: jest.fn(),
|
||||
roleForTrackFlow: {started_by_role: General.SYSTEM_USER_ROLE},
|
||||
focusOriginElement: 'elementId',
|
||||
});
|
||||
|
||||
let props = defaultProps;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {defineMessages} from 'react-intl';
|
||||
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
@@ -16,6 +16,8 @@ import {isEmail} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
|
||||
import {InviteType} from './invite_as';
|
||||
import InviteView, {initializeInviteState} from './invite_view';
|
||||
import type {InviteState} from './invite_view';
|
||||
@@ -81,6 +83,7 @@ export type Props = {
|
||||
initialValue?: string;
|
||||
inviteAsGuest?: boolean;
|
||||
roleForTrackFlow: {started_by_role: string};
|
||||
focusOriginElement: string;
|
||||
}
|
||||
|
||||
export const View = {
|
||||
@@ -128,6 +131,11 @@ export default class InvitationModal extends React.PureComponent<Props, State> {
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
handleExit = () => {
|
||||
focusElement(this.props.focusOriginElement, true);
|
||||
this.props.onExited?.();
|
||||
};
|
||||
|
||||
toggleCustomMessage = () => {
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
@@ -416,21 +424,21 @@ export default class InvitationModal extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<GenericModal
|
||||
id='invitationModal'
|
||||
data-testid='invitationModal'
|
||||
dialogClassName='a11y__modal modal--overflow'
|
||||
className='InvitationModal'
|
||||
dataTestId='invitationModal'
|
||||
className='InvitationModal a11y__modal modal--overflow'
|
||||
show={this.state.show}
|
||||
onHide={this.handleHide}
|
||||
onExited={this.props.onExited}
|
||||
role='none'
|
||||
onExited={this.handleExit}
|
||||
backdrop={this.getBackdrop()}
|
||||
aria-modal='true'
|
||||
aria-labelledby='invitation_modal_title'
|
||||
ariaLabelledby='invitation_modal_title'
|
||||
compassDesign={true}
|
||||
showCloseButton={false}
|
||||
showHeader={false}
|
||||
>
|
||||
{view}
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function InviteAs(props: Props) {
|
||||
clickCallback={closeInviteModal}
|
||||
tooltipMessage={hasPriorTrial ? formatMessage({id: 'free.professional_feature.upgrade', defaultMessage: 'Upgrade'}) : undefined}
|
||||
|
||||
// the secondary back button first closes the restridted feature modal and then opens back the invitation modal
|
||||
// the secondary back button first closes the restricted feature modal and then opens back the invitation modal
|
||||
customSecondaryButtonInModal={hasPriorTrial ? undefined : {
|
||||
msg: formatMessage({id: 'free.professional_feature.back', defaultMessage: 'Back'}),
|
||||
action: () => {
|
||||
|
||||
@@ -11,9 +11,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: 0 !important;
|
||||
|
||||
// to override clearfix rule in clearfix.less
|
||||
&::before,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
@@ -86,7 +87,7 @@ export default function ResultView(props: Props) {
|
||||
/>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={props.footerClass}>
|
||||
<Modal.Footer className={classNames('InviteView__footer', props.footerClass)}>
|
||||
<button
|
||||
onClick={props.inviteMore}
|
||||
className='btn btn-tertiary ResultView__inviteMore'
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal', () => {
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
wrapper.find(GenericModal).props().onExited();
|
||||
wrapper.find(GenericModal).props().onExited?.();
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
@@ -116,7 +116,7 @@ describe('components/learn_more_trial_modal/learn_more_trial_modal', () => {
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
wrapper.find(GenericModal).props().onExited();
|
||||
wrapper.find(GenericModal).props().onExited?.();
|
||||
|
||||
expect(mockOnExited).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -278,6 +278,11 @@ exports[`components/Menu should match snapshot with id 1`] = `
|
||||
teamId="team-id"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
@@ -485,6 +490,11 @@ exports[`components/Menu should match snapshot with most of the thing disabled 1
|
||||
teamId="team-id"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
@@ -714,6 +724,7 @@ exports[`components/Menu should match snapshot with most of the thing disabled i
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton2",
|
||||
"isContentProductSettings": false,
|
||||
}
|
||||
}
|
||||
@@ -738,6 +749,7 @@ exports[`components/Menu should match snapshot with most of the thing disabled i
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton3",
|
||||
"isContentProductSettings": true,
|
||||
}
|
||||
}
|
||||
@@ -799,6 +811,11 @@ exports[`components/Menu should match snapshot with most of the thing disabled i
|
||||
teamId="team-id"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
@@ -1105,6 +1122,11 @@ exports[`components/Menu should match snapshot with most of the thing enabled 1`
|
||||
teamId="team-id"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
@@ -1334,6 +1356,7 @@ exports[`components/Menu should match snapshot with most of the thing enabled in
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton2",
|
||||
"isContentProductSettings": false,
|
||||
}
|
||||
}
|
||||
@@ -1358,6 +1381,7 @@ exports[`components/Menu should match snapshot with most of the thing enabled in
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton3",
|
||||
"isContentProductSettings": true,
|
||||
}
|
||||
}
|
||||
@@ -1419,6 +1443,11 @@ exports[`components/Menu should match snapshot with most of the thing enabled in
|
||||
teamId="team-id"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
@@ -1727,6 +1756,11 @@ exports[`components/Menu should match snapshot with plugins 1`] = `
|
||||
teamId="team-id"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
@@ -1973,6 +2007,7 @@ exports[`components/Menu should match snapshot with plugins in mobile 1`] = `
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton2",
|
||||
"isContentProductSettings": false,
|
||||
}
|
||||
}
|
||||
@@ -1997,6 +2032,7 @@ exports[`components/Menu should match snapshot with plugins in mobile 1`] = `
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton3",
|
||||
"isContentProductSettings": true,
|
||||
}
|
||||
}
|
||||
@@ -2058,6 +2094,11 @@ exports[`components/Menu should match snapshot with plugins in mobile 1`] = `
|
||||
teamId="team-id"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
|
||||
@@ -94,7 +94,7 @@ export class MainMenu extends React.PureComponent<Props> {
|
||||
handleKeyDown = (e: KeyboardEvent): void => {
|
||||
if (cmdOrCtrlPressed(e) && e.shiftKey && isKeyPressed(e, Constants.KeyCodes.A)) {
|
||||
e.preventDefault();
|
||||
this.props.actions.openModal({modalId: ModalIdentifiers.USER_SETTINGS, dialogType: UserSettingsModal, dialogProps: {isContentProductSettings: true}});
|
||||
this.props.actions.openModal({modalId: ModalIdentifiers.USER_SETTINGS, dialogType: UserSettingsModal, dialogProps: {isContentProductSettings: true, focusOriginElement: 'userAccountMenuButton'}});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -174,6 +174,7 @@ export class MainMenu extends React.PureComponent<Props> {
|
||||
})}
|
||||
icon={this.props.mobile && <i className='fa fa-user-plus'/>}
|
||||
onClick={() => trackEvent('ui', 'click_sidebar_team_dropdown_invite_people')}
|
||||
dialogProps={{focusOriginElement: 'sidebarDropdownMenuButton'}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -220,7 +221,7 @@ export class MainMenu extends React.PureComponent<Props> {
|
||||
id='profileSettings'
|
||||
modalId={ModalIdentifiers.USER_SETTINGS}
|
||||
dialogType={UserSettingsModal}
|
||||
dialogProps={{isContentProductSettings: false}}
|
||||
dialogProps={{isContentProductSettings: false, focusOriginElement: 'userAccountMenuButton2'}}
|
||||
text={formatMessage({id: 'navbar_dropdown.profileSettings', defaultMessage: 'Profile'})}
|
||||
icon={<i className='fa fa-user'/>}
|
||||
/>
|
||||
@@ -228,7 +229,7 @@ export class MainMenu extends React.PureComponent<Props> {
|
||||
id='accountSettings'
|
||||
modalId={ModalIdentifiers.USER_SETTINGS}
|
||||
dialogType={UserSettingsModal}
|
||||
dialogProps={{isContentProductSettings: true}}
|
||||
dialogProps={{isContentProductSettings: true, focusOriginElement: 'userAccountMenuButton3'}}
|
||||
text={formatMessage({id: 'navbar_dropdown.accountSettings', defaultMessage: 'Settings'})}
|
||||
icon={<i className='fa fa-cog'/>}
|
||||
/>
|
||||
|
||||
@@ -16,10 +16,13 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
|
||||
id="more_direct_channels.title"
|
||||
/>
|
||||
}
|
||||
modalLocation="top"
|
||||
onEntered={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<div
|
||||
role="application"
|
||||
@@ -262,10 +265,13 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
|
||||
id="more_direct_channels.title"
|
||||
/>
|
||||
}
|
||||
modalLocation="top"
|
||||
onEntered={[Function]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<div
|
||||
role="application"
|
||||
|
||||
@@ -21,7 +21,6 @@ import List from './list';
|
||||
import {USERS_PER_PAGE} from './list/list';
|
||||
import {isGroupChannel, optionValue} from './types';
|
||||
import type {OptionValue} from './types';
|
||||
import './more_direct_channels.scss';
|
||||
|
||||
export type Props = {
|
||||
currentUserId: string;
|
||||
@@ -296,6 +295,7 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
|
||||
compassDesign={true}
|
||||
bodyPadding={false}
|
||||
onEntered={this.loadModalData}
|
||||
modalLocation={'top'}
|
||||
>
|
||||
<div role='application'>
|
||||
{body}
|
||||
|
||||
@@ -4,10 +4,12 @@ exports[`components/marketplace/ doesn't show web marketplace banner in FeatureF
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-label="App Marketplace"
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal GenericModal GenericModal__compassDesign marketplace-modal streamlined-marketplace"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign marketplace-modal streamlined-marketplace"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="marketplace-modal"
|
||||
@@ -32,7 +34,7 @@ exports[`components/marketplace/ doesn't show web marketplace banner in FeatureF
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="GenericModal__wrapper-enter-key-press-catcher"
|
||||
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
@@ -94,10 +96,12 @@ exports[`components/marketplace/ hides search, shows web marketplace banner in F
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-label="App Marketplace"
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal GenericModal GenericModal__compassDesign marketplace-modal streamlined-marketplace with-web-marketplace-link"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign marketplace-modal streamlined-marketplace with-web-marketplace-link"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="marketplace-modal"
|
||||
@@ -122,7 +126,7 @@ exports[`components/marketplace/ hides search, shows web marketplace banner in F
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="GenericModal__wrapper-enter-key-press-catcher"
|
||||
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
@@ -185,10 +189,12 @@ exports[`components/marketplace/ should render default 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-label="App Marketplace"
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="marketplace-modal"
|
||||
@@ -213,7 +219,7 @@ exports[`components/marketplace/ should render default 1`] = `
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="GenericModal__wrapper-enter-key-press-catcher"
|
||||
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
@@ -327,10 +333,12 @@ exports[`components/marketplace/ should render with error banner 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-label="App Marketplace"
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="marketplace-modal"
|
||||
@@ -355,7 +363,7 @@ exports[`components/marketplace/ should render with error banner 1`] = `
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="GenericModal__wrapper-enter-key-press-catcher"
|
||||
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
@@ -486,10 +494,12 @@ exports[`components/marketplace/ should render with no plugins available 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-label="App Marketplace"
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="marketplace-modal"
|
||||
@@ -514,7 +524,7 @@ exports[`components/marketplace/ should render with no plugins available 1`] = `
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="GenericModal__wrapper-enter-key-press-catcher"
|
||||
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
@@ -636,10 +646,12 @@ exports[`components/marketplace/ should render with plugins available 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-label="App Marketplace"
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="marketplace-modal"
|
||||
@@ -664,7 +676,7 @@ exports[`components/marketplace/ should render with plugins available 1`] = `
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="GenericModal__wrapper-enter-key-press-catcher"
|
||||
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
@@ -804,10 +816,12 @@ exports[`components/marketplace/ should render with plugins installed 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-label="App Marketplace"
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogClassName="GenericModal__location--center a11y__modal GenericModal GenericModal__compassDesign marketplace-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="marketplace-modal"
|
||||
@@ -832,7 +846,7 @@ exports[`components/marketplace/ should render with plugins installed 1`] = `
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="GenericModal__wrapper-enter-key-press-catcher"
|
||||
className="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
@@ -72,6 +72,7 @@ const LessThanMaxFreeUsers = ({pluginButtons}: {pluginButtons: React.ReactNode})
|
||||
modalId={ModalIdentifiers.INVITATION}
|
||||
dialogType={InvitationModal}
|
||||
onClick={() => trackEvent('channel_intro_message', 'click_invite_button')}
|
||||
dialogProps={{focusOriginElement: 'browseOrAddChannelMenuButton'}}
|
||||
>
|
||||
<i
|
||||
className='icon-email-plus-outline'
|
||||
|
||||
@@ -753,11 +753,12 @@ function createFavoriteButton(isFavorite: boolean, toggleFavorite: () => void, c
|
||||
function createNotificationPreferencesButton(channel: Channel, currentUser: UserProfileType) {
|
||||
return (
|
||||
<ToggleModalButton
|
||||
id='channelIntroNotificationPreferencesButton'
|
||||
modalId={ModalIdentifiers.CHANNEL_NOTIFICATIONS}
|
||||
ariaLabel={Utils.localizeMessage({id: 'intro_messages.notificationPreferences', defaultMessage: 'Notification Preferences'})}
|
||||
className={'action-button'}
|
||||
dialogType={ChannelNotificationsModal}
|
||||
dialogProps={{channel, currentUser}}
|
||||
dialogProps={{channel, currentUser, focusOriginElement: 'channelIntroNotificationPreferencesButton'}}
|
||||
>
|
||||
<BellRingOutlineIcon size={24}/>
|
||||
<FormattedMessage
|
||||
|
||||
@@ -23,8 +23,11 @@ exports[`ProductNoticesModal Match snapshot for single notice 1`] = `
|
||||
title
|
||||
</span>
|
||||
}
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<span
|
||||
className="productNotices__helpText"
|
||||
@@ -75,8 +78,11 @@ exports[`ProductNoticesModal Match snapshot for user notice 1`] = `
|
||||
title
|
||||
</span>
|
||||
}
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<span
|
||||
className="productNotices__helpText"
|
||||
@@ -147,8 +153,11 @@ exports[`ProductNoticesModal Should match snapshot for system admin notice 1`] =
|
||||
for sysadmin
|
||||
</span>
|
||||
}
|
||||
modalLocation="center"
|
||||
onExited={[Function]}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<span
|
||||
className="productNotices__helpText"
|
||||
|
||||
@@ -25,6 +25,7 @@ exports[`components/QuickSwitchModal should match snapshot 1`] = `
|
||||
</h2>
|
||||
</div>
|
||||
}
|
||||
modalLocation="center"
|
||||
modalSubheaderText={
|
||||
<div
|
||||
className="channel-switcher__hint"
|
||||
@@ -44,6 +45,8 @@ exports[`components/QuickSwitchModal should match snapshot 1`] = `
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
show={true}
|
||||
showCloseButton={true}
|
||||
showHeader={true}
|
||||
>
|
||||
<div
|
||||
className="channel-switcher__suggestion-box"
|
||||
|
||||
@@ -32,6 +32,11 @@ exports[`components/sidebar/invite_members_button should match snapshot 1`] = `
|
||||
<ToggleModalButton
|
||||
ariaLabel="Invite Members"
|
||||
className="intro-links color--link cursor--pointer"
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "inviteMembersButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
|
||||
@@ -46,6 +46,7 @@ const InviteMembersButton = (props: Props): JSX.Element | null => {
|
||||
modalId={ModalIdentifiers.INVITATION}
|
||||
dialogType={InvitationModal}
|
||||
onClick={handleButtonClick}
|
||||
dialogProps={{focusOriginElement: 'inviteMembersButton'}}
|
||||
>
|
||||
<div
|
||||
className='SidebarChannelNavigator__inviteMembersLhsButton'
|
||||
|
||||
@@ -122,6 +122,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
||||
dialogType: UserSettingsModal,
|
||||
dialogProps: {
|
||||
isContentProductSettings: true,
|
||||
focusOriginElement: 'sidebar.tsx',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -158,6 +159,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
||||
this.props.actions.openModal({
|
||||
modalId: ModalIdentifiers.INVITATION,
|
||||
dialogType: InvitationModal,
|
||||
dialogProps: {focusOriginElement: 'browseOrAddChannelMenuButton'},
|
||||
});
|
||||
trackEvent('ui', 'ui_channels_dropdown_invite_people');
|
||||
};
|
||||
|
||||
@@ -61,6 +61,7 @@ const SidebarHeader = (props: Props) => {
|
||||
aria-haspopup='menu'
|
||||
aria-expanded={menuToggled}
|
||||
aria-controls='sidebarDropdownMenu'
|
||||
id='sidebarDropdownMenuButton'
|
||||
>
|
||||
<span className='title'>{currentTeam.display_name}</span>
|
||||
<i
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('components/three_days_left_trial_modal/three_days_left_trial_modal', (
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
wrapper.find(GenericModal).props().onExited();
|
||||
wrapper.find(GenericModal).props().onExited?.();
|
||||
|
||||
expect(mockOnExited).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('components/trial_benefits_modal/trial_benefits_modal', () => {
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
wrapper.find(GenericModal).props().onExited();
|
||||
wrapper.find(GenericModal).props().onExited?.();
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
@@ -147,7 +147,7 @@ describe('components/trial_benefits_modal/trial_benefits_modal', () => {
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
wrapper.find(GenericModal).props().onExited();
|
||||
wrapper.find(GenericModal).props().onExited?.();
|
||||
|
||||
expect(mockOnExited).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function UserAccountProfileMenuItem(props: Props) {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||
dialogType: UserSettingsModal,
|
||||
dialogProps: {isContentProductSettings: false},
|
||||
dialogProps: {isContentProductSettings: false, focusOriginElement: 'userAccountMenuButton'},
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
.UserSettingsModal {
|
||||
|
||||
.GenericModal__wrapper {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
max-height: 90vh;
|
||||
flex-direction: column;
|
||||
border: var(--border-default);
|
||||
border-radius: var(--radius-l);
|
||||
box-shadow: var(--elevation-6);
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
min-height: 150px;
|
||||
flex-direction: column;
|
||||
padding: 28px 32px;
|
||||
gap: 24px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
min-height: 150px;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__headerWrapper{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.adminModeBadge {
|
||||
display: flex;
|
||||
width: 86px;
|
||||
min-width: 86px;
|
||||
height: 22px;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: auto;
|
||||
margin-left: 24px;
|
||||
background: var(--error-text);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
gap: 6px;
|
||||
|
||||
span {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
max-height: unset;
|
||||
flex-direction: column;
|
||||
border-radius: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 900px) and (min-width: 768px) {
|
||||
.modal-content {
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 600px) {
|
||||
.modal-content, .GenericModal__wrapper {
|
||||
max-height: 85vh !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
import type {PreferencesType} from '@mattermost/types/preferences';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
@@ -18,6 +18,7 @@ import UserSettings from 'components/user_settings';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
import SmartLoader from 'components/widgets/smart_loader';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import Constants from 'utils/constants';
|
||||
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
|
||||
import {stopTryNotificationRing} from 'utils/notification_sounds';
|
||||
@@ -26,6 +27,8 @@ import {getDisplayName} from 'utils/utils';
|
||||
|
||||
import type {PluginConfiguration} from 'types/plugins/user_settings';
|
||||
|
||||
import './user_settings_modal.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
userID?: string;
|
||||
adminMode?: boolean;
|
||||
@@ -35,15 +38,16 @@ export type OwnProps = {
|
||||
}
|
||||
|
||||
export type Props = OwnProps & {
|
||||
onExited: () => void;
|
||||
intl: IntlShape;
|
||||
pluginSettings: {[pluginId: string]: PluginConfiguration};
|
||||
user?: UserProfile;
|
||||
onExited: () => void;
|
||||
focusOriginElement?: string;
|
||||
actions: {
|
||||
sendVerificationEmail: (email: string) => Promise<ActionResult>;
|
||||
getUserPreferences: (userID: string) => Promise<unknown>;
|
||||
getUser: (userID: string) => Promise<unknown>;
|
||||
};
|
||||
pluginSettings: {[pluginId: string]: PluginConfiguration};
|
||||
user?: UserProfile;
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -54,13 +58,13 @@ type State = {
|
||||
show: boolean;
|
||||
resendStatus: string;
|
||||
loading: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
private requireConfirm: boolean;
|
||||
private customConfirmAction: ((handleConfirm: () => void) => void) | null;
|
||||
private modalBodyRef: React.RefObject<Modal>;
|
||||
private afterConfirm: (() => void) | null;
|
||||
private modalBodyRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -107,7 +111,6 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
if (!this.props.userPreferences) {
|
||||
this.props.actions.getUserPreferences(this.props.userID);
|
||||
}
|
||||
|
||||
if (!this.props.user) {
|
||||
this.props.actions.getUser(this.props.userID);
|
||||
}
|
||||
@@ -124,8 +127,11 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (this.state.active_tab !== prevState.active_tab) {
|
||||
const el = ReactDOM.findDOMNode(this.modalBodyRef.current) as any;
|
||||
el.scrollTop = 0;
|
||||
// Scroll to top if user changes tabs
|
||||
if (this.modalBodyRef.current) {
|
||||
const el = this.modalBodyRef.current as HTMLDivElement;
|
||||
el.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +140,7 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Ctrl+Shift+A (or Cmd+Shift+A) to close
|
||||
if (cmdOrCtrlPressed(e) && e.shiftKey && isKeyPressed(e, Constants.KeyCodes.A)) {
|
||||
e.preventDefault();
|
||||
this.handleHide();
|
||||
@@ -149,10 +156,7 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
|
||||
// Cancel any ongoing notification sound, if any (from DesktopNotificationSettings)
|
||||
stopTryNotificationRing();
|
||||
|
||||
this.setState({
|
||||
show: false,
|
||||
});
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
// called after the dialog is fully hidden and faded out
|
||||
@@ -161,6 +165,9 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
active_tab: this.props.isContentProductSettings ? 'notifications' : 'profile',
|
||||
active_section: '',
|
||||
});
|
||||
if (this.props.focusOriginElement) {
|
||||
focusElement(this.props.focusOriginElement, true);
|
||||
}
|
||||
this.props.onExited();
|
||||
};
|
||||
|
||||
@@ -180,7 +187,6 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
showConfirmModal: false,
|
||||
enforceFocus: true,
|
||||
});
|
||||
|
||||
this.requireConfirm = false;
|
||||
this.customConfirmAction = null;
|
||||
|
||||
@@ -195,7 +201,6 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
showConfirmModal: false,
|
||||
enforceFocus: true,
|
||||
});
|
||||
|
||||
this.afterConfirm = null;
|
||||
};
|
||||
|
||||
@@ -248,62 +253,62 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
if (!skipConfirm && this.requireConfirm) {
|
||||
this.showConfirmModal(() => this.updateSection(section, true));
|
||||
} else {
|
||||
this.setState({
|
||||
active_section: section ?? '',
|
||||
});
|
||||
this.setState({active_section: section ?? ''});
|
||||
}
|
||||
};
|
||||
|
||||
getUserSettingsTabs = () => {
|
||||
const {formatMessage} = this.props.intl;
|
||||
return [
|
||||
{
|
||||
name: 'notifications',
|
||||
uiName: this.props.intl.formatMessage({id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'}),
|
||||
uiName: formatMessage({id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'}),
|
||||
icon: 'icon icon-bell-outline',
|
||||
iconTitle: this.props.intl.formatMessage({id: 'user.settings.notifications.icon', defaultMessage: 'Notification Settings Icon'}),
|
||||
iconTitle: formatMessage({id: 'user.settings.notifications.icon', defaultMessage: 'Notification Settings Icon'}),
|
||||
},
|
||||
{
|
||||
name: 'display',
|
||||
uiName: this.props.intl.formatMessage({id: 'user.settings.modal.display', defaultMessage: 'Display'}),
|
||||
uiName: formatMessage({id: 'user.settings.modal.display', defaultMessage: 'Display'}),
|
||||
icon: 'icon icon-eye-outline',
|
||||
iconTitle: this.props.intl.formatMessage({id: 'user.settings.display.icon', defaultMessage: 'Display Settings Icon'}),
|
||||
iconTitle: formatMessage({id: 'user.settings.display.icon', defaultMessage: 'Display Settings Icon'}),
|
||||
},
|
||||
{
|
||||
name: 'sidebar',
|
||||
uiName: this.props.intl.formatMessage({id: 'user.settings.modal.sidebar', defaultMessage: 'Sidebar'}),
|
||||
uiName: formatMessage({id: 'user.settings.modal.sidebar', defaultMessage: 'Sidebar'}),
|
||||
icon: 'icon icon-dock-left',
|
||||
iconTitle: this.props.intl.formatMessage({id: 'user.settings.sidebar.icon', defaultMessage: 'Sidebar Settings Icon'}),
|
||||
iconTitle: formatMessage({id: 'user.settings.sidebar.icon', defaultMessage: 'Sidebar Settings Icon'}),
|
||||
},
|
||||
{
|
||||
name: 'advanced',
|
||||
uiName: this.props.intl.formatMessage({id: 'user.settings.modal.advanced', defaultMessage: 'Advanced'}),
|
||||
uiName: formatMessage({id: 'user.settings.modal.advanced', defaultMessage: 'Advanced'}),
|
||||
icon: 'icon icon-tune',
|
||||
iconTitle: this.props.intl.formatMessage({id: 'user.settings.advance.icon', defaultMessage: 'Advanced Settings Icon'}),
|
||||
iconTitle: formatMessage({id: 'user.settings.advance.icon', defaultMessage: 'Advanced Settings Icon'}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
getProfileSettingsTab = () => {
|
||||
const {formatMessage} = this.props.intl;
|
||||
return [
|
||||
{
|
||||
name: 'profile',
|
||||
uiName: this.props.intl.formatMessage({id: 'user.settings.modal.profile', defaultMessage: 'Profile'}),
|
||||
uiName: formatMessage({id: 'user.settings.modal.profile', defaultMessage: 'Profile'}),
|
||||
icon: 'icon icon-settings-outline',
|
||||
iconTitle: this.props.intl.formatMessage({id: 'user.settings.profile.icon', defaultMessage: 'Profile Settings Icon'}),
|
||||
iconTitle: formatMessage({id: 'user.settings.profile.icon', defaultMessage: 'Profile Settings Icon'}),
|
||||
},
|
||||
{
|
||||
name: 'security',
|
||||
uiName: this.props.intl.formatMessage({id: 'user.settings.modal.security', defaultMessage: 'Security'}),
|
||||
uiName: formatMessage({id: 'user.settings.modal.security', defaultMessage: 'Security'}),
|
||||
icon: 'icon icon-lock-outline',
|
||||
iconTitle: this.props.intl.formatMessage({id: 'user.settings.security.icon', defaultMessage: 'Security Settings Icon'}),
|
||||
iconTitle: formatMessage({id: 'user.settings.security.icon', defaultMessage: 'Security Settings Icon'}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
getPluginsSettingsTab = () => {
|
||||
return Object.values(this.props.pluginSettings).map((v) => {
|
||||
const className = v.icon ? `icon ${v.icon}` : 'icon icon-power-plug-outline';
|
||||
const useURL = v.icon && (isValidUrl(v.icon) || v.icon.startsWith('/'));
|
||||
const className = v.icon ? `icon ${v.icon}` : 'icon icon-power-plug-outline';
|
||||
return {
|
||||
name: v.id,
|
||||
uiName: v.uiName,
|
||||
@@ -317,60 +322,58 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
let modalTitle: string;
|
||||
|
||||
if (this.props.adminMode && this.props.user) {
|
||||
modalTitle = formatMessage({
|
||||
id: 'userSettings.adminMode.modal_header',
|
||||
defaultMessage: "{userDisplayName}'s Settings",
|
||||
}, {
|
||||
userDisplayName: getDisplayName(this.props.user),
|
||||
});
|
||||
modalTitle = formatMessage(
|
||||
{id: 'userSettings.adminMode.modal_header', defaultMessage: "{userDisplayName}'s Settings"},
|
||||
{userDisplayName: getDisplayName(this.props.user)},
|
||||
);
|
||||
} else if (this.props.isContentProductSettings) {
|
||||
modalTitle = formatMessage({id: 'global_header.productSettings', defaultMessage: 'Settings'});
|
||||
} else {
|
||||
modalTitle = this.props.isContentProductSettings ? formatMessage({
|
||||
id: 'global_header.productSettings',
|
||||
defaultMessage: 'Settings',
|
||||
}) : formatMessage({
|
||||
id: 'user.settings.modal.title',
|
||||
defaultMessage: 'Profile',
|
||||
});
|
||||
modalTitle = formatMessage({id: 'user.settings.modal.title', defaultMessage: 'Profile'});
|
||||
}
|
||||
|
||||
const headerTitle = (
|
||||
<div
|
||||
className='UserSettingsModal__headerWrapper'
|
||||
id='accountSettingsHeader'
|
||||
>
|
||||
<span
|
||||
id='accountSettingsModalLabel'
|
||||
className='modal-header__title'
|
||||
>
|
||||
{modalTitle}
|
||||
</span>
|
||||
{this.props.adminMode && (
|
||||
<div className='adminModeBadge'>
|
||||
<FormattedMessage
|
||||
id='userSettings.adminMode.admin_mode_badge'
|
||||
defaultMessage='Admin Mode'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<GenericModal
|
||||
id='accountSettingsModal'
|
||||
dialogClassName='a11y__modal settings-modal'
|
||||
className='a11y__modal settings-modal UserSettingsModal'
|
||||
show={this.state.show}
|
||||
onHide={this.handleHide}
|
||||
onExited={this.handleHidden}
|
||||
ariaLabel={modalTitle}
|
||||
compassDesign={true}
|
||||
enforceFocus={this.state.enforceFocus}
|
||||
aria-label={modalTitle}
|
||||
role='none'
|
||||
bodyPadding={false}
|
||||
modalHeaderText={headerTitle}
|
||||
modalLocation='top'
|
||||
>
|
||||
<Modal.Header
|
||||
id='accountSettingsHeader'
|
||||
closeButton={true}
|
||||
<div
|
||||
ref={this.modalBodyRef}
|
||||
>
|
||||
<Modal.Title
|
||||
componentClass='h2'
|
||||
id='accountSettingsModalLabel'
|
||||
className='modal-header__title'
|
||||
>
|
||||
{modalTitle}
|
||||
</Modal.Title>
|
||||
|
||||
{
|
||||
this.props.adminMode &&
|
||||
<div className='adminModeBadge'>
|
||||
<FormattedMessage
|
||||
id='userSettings.adminMode.admin_mode_badge'
|
||||
defaultMessage='Admin Mode'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</Modal.Header>
|
||||
<Modal.Body ref={this.modalBodyRef}>
|
||||
{
|
||||
this.props.adminMode &&
|
||||
{/* Admin Mode: wait for user + preferences */}
|
||||
{this.props.adminMode &&
|
||||
<SmartLoader
|
||||
loading={this.props.adminMode && (!this.props.userPreferences || !this.props.user)}
|
||||
className='loadingIndicator'
|
||||
@@ -380,40 +383,41 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
</SmartLoader>
|
||||
}
|
||||
|
||||
{
|
||||
!this.state.loading && this.props.user &&
|
||||
<div className='settings-table'>
|
||||
<div className='settings-links'>
|
||||
<SettingsSidebar
|
||||
tabs={this.props.isContentProductSettings ? this.getUserSettingsTabs() : this.getProfileSettingsTab()}
|
||||
pluginTabs={this.props.isContentProductSettings ? this.getPluginsSettingsTab() : []}
|
||||
activeTab={this.state.active_tab}
|
||||
updateTab={this.updateTab}
|
||||
/>
|
||||
{/* Show content only when not loading */}
|
||||
{!this.state.loading && this.props.user &&
|
||||
<>
|
||||
<div className='settings-table'>
|
||||
<div className='settings-links'>
|
||||
<SettingsSidebar
|
||||
tabs={this.props.isContentProductSettings ? this.getUserSettingsTabs() : this.getProfileSettingsTab()}
|
||||
pluginTabs={this.props.isContentProductSettings ? this.getPluginsSettingsTab() : []}
|
||||
activeTab={this.state.active_tab}
|
||||
updateTab={this.updateTab}
|
||||
/>
|
||||
</div>
|
||||
<div className='settings-content minimize-settings'>
|
||||
<UserSettings
|
||||
activeTab={this.state.active_tab}
|
||||
activeSection={this.state.active_section}
|
||||
updateSection={this.updateSection}
|
||||
updateTab={this.updateTab}
|
||||
closeModal={this.closeModal}
|
||||
collapseModal={this.collapseModal}
|
||||
setRequireConfirm={(requireConfirm?: boolean, customConfirmAction?: () => () => void) => {
|
||||
this.requireConfirm = requireConfirm || false;
|
||||
this.customConfirmAction = customConfirmAction || null;
|
||||
}}
|
||||
pluginSettings={this.props.pluginSettings}
|
||||
user={this.props.user}
|
||||
adminMode={this.props.adminMode}
|
||||
userPreferences={this.props.userPreferences}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='settings-content minimize-settings'>
|
||||
<UserSettings
|
||||
activeTab={this.state.active_tab}
|
||||
activeSection={this.state.active_section}
|
||||
updateSection={this.updateSection}
|
||||
updateTab={this.updateTab}
|
||||
closeModal={this.closeModal}
|
||||
collapseModal={this.collapseModal}
|
||||
setRequireConfirm={
|
||||
(requireConfirm?: boolean, customConfirmAction?: () => () => void) => {
|
||||
this.requireConfirm = requireConfirm!;
|
||||
this.customConfirmAction = customConfirmAction!;
|
||||
}
|
||||
}
|
||||
pluginSettings={this.props.pluginSettings}
|
||||
user={this.props.user}
|
||||
adminMode={this.props.adminMode}
|
||||
userPreferences={this.props.userPreferences}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</Modal.Body>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
title={formatMessage({id: 'user.settings.modal.confirmTitle', defaultMessage: 'Discard Changes?'})}
|
||||
message={formatMessage({
|
||||
@@ -428,7 +432,7 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
|
||||
onConfirm={this.handleConfirm}
|
||||
onCancel={this.handleCancelConfirmation}
|
||||
/>
|
||||
</Modal>
|
||||
</GenericModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,15 @@ function ModalHeader({id, title, subtitle, handleClose}: Props) {
|
||||
</h1>
|
||||
<div className='mm-modal-header__vertical-divider'/>
|
||||
<p className='mm-modal-header__subtitle'>{subtitle}</p>
|
||||
<div
|
||||
className='mm-modal-header__ctr'
|
||||
onClick={handleClose}
|
||||
>
|
||||
{handleClose && <div className='mm-modal-header__ctr'>
|
||||
<button
|
||||
className='btn btn-icon'
|
||||
onClick={handleClose}
|
||||
aria-label={intl.formatMessage({id: 'modal.header_close', defaultMessage: 'Close'})}
|
||||
>
|
||||
<i className='icon icon-close'/>
|
||||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ exports[`plugins/MainMenuActions should match snapshot in mobile view with some
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton2",
|
||||
"isContentProductSettings": false,
|
||||
}
|
||||
}
|
||||
@@ -91,6 +92,7 @@ exports[`plugins/MainMenuActions should match snapshot in mobile view with some
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton3",
|
||||
"isContentProductSettings": true,
|
||||
}
|
||||
}
|
||||
@@ -152,6 +154,11 @@ exports[`plugins/MainMenuActions should match snapshot in mobile view with some
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
@@ -479,6 +486,11 @@ exports[`plugins/MainMenuActions should match snapshot in web view 1`] = `
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
|
||||
@@ -86,24 +86,6 @@
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
.adminModeBadge {
|
||||
display: flex;
|
||||
width: 86px;
|
||||
min-width: 86px;
|
||||
height: 22px;
|
||||
align-items: center;
|
||||
padding: 2px var(--spacing-xxxs, 6px);
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: auto;
|
||||
margin-left: 24px;
|
||||
background: var(--error-text);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
|
||||
@@ -22,6 +22,9 @@ export function focusElement(
|
||||
keyboardOnly = true,
|
||||
resetOriginElement = false,
|
||||
) {
|
||||
if (!elementOrId) {
|
||||
return;
|
||||
}
|
||||
let target: HTMLElement | null = null;
|
||||
|
||||
if (typeof elementOrId === 'string') {
|
||||
|
||||
@@ -18,6 +18,7 @@ exports[`GenericModal should match snapshot for base case 1`] = `
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="genericModalLabel"
|
||||
aria-modal="true"
|
||||
class="fade in modal"
|
||||
id="genericModal"
|
||||
role="dialog"
|
||||
@@ -25,14 +26,14 @@ exports[`GenericModal should match snapshot for base case 1`] = `
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="a11y__modal GenericModal modal-dialog"
|
||||
class="GenericModal__location--center a11y__modal GenericModal modal-dialog"
|
||||
>
|
||||
<div
|
||||
class="modal-content"
|
||||
role="document"
|
||||
>
|
||||
<div
|
||||
class="GenericModal__wrapper-enter-key-press-catcher"
|
||||
class="GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
.console__body .modal .GenericModal,
|
||||
.app__body .modal .GenericModal {
|
||||
&.modal-dialog {
|
||||
&__location--top {
|
||||
margin-top: 5vh;
|
||||
}
|
||||
|
||||
&__location--center {
|
||||
margin-top: calc(50vh - 240px);
|
||||
}
|
||||
|
||||
&__location--bottom {
|
||||
margin-top: calc(50vh + 240px);
|
||||
}
|
||||
|
||||
&.modal-overflow {
|
||||
.modal-body {
|
||||
overflow: visible;
|
||||
@@ -131,7 +139,7 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
margin: 0;
|
||||
margin: 0 !important;
|
||||
|
||||
.modal-header {
|
||||
box-shadow: var(--elevation-2);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {render, screen} from '@testing-library/react';
|
||||
import {render, screen, waitFor} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import {GenericModal} from './generic_modal';
|
||||
@@ -37,4 +37,108 @@ describe('GenericModal', () => {
|
||||
expect(screen.getByText('Confirm')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onExited when modal exits', async () => {
|
||||
const onExitedMock = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
onExited: onExitedMock,
|
||||
};
|
||||
render(
|
||||
wrapIntl(<GenericModal {...props}/>),
|
||||
);
|
||||
|
||||
// Find and click the close button to trigger modal exit
|
||||
const closeButton = screen.getByLabelText('Close');
|
||||
closeButton.click();
|
||||
|
||||
// Wait for onExited to be called
|
||||
await waitFor(() => {
|
||||
expect(onExitedMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('does not throw if onExited is undefined', async () => {
|
||||
// Create props without onExited
|
||||
const {onExited, ...propsWithoutOnExited} = baseProps; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
const props = {
|
||||
...propsWithoutOnExited,
|
||||
onHide: jest.fn(), // Ensure onHide is provided since it's mandatory
|
||||
};
|
||||
|
||||
// This should not throw
|
||||
render(
|
||||
wrapIntl(<GenericModal {...props}/>),
|
||||
);
|
||||
|
||||
// Find and click the close button to trigger modal exit
|
||||
const closeButton = screen.getByLabelText('Close');
|
||||
|
||||
// This should not throw
|
||||
expect(() => {
|
||||
closeButton.click();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('calls onEntered when modal enters', async () => {
|
||||
const onEnteredMock = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
onEntered: onEnteredMock,
|
||||
show: false, // Start with modal hidden
|
||||
};
|
||||
|
||||
const {rerender} = render(
|
||||
wrapIntl(<GenericModal {...props}/>),
|
||||
);
|
||||
|
||||
// Show the modal
|
||||
rerender(
|
||||
wrapIntl(<GenericModal {...props} show={true}/>),
|
||||
);
|
||||
|
||||
// Wait for onEntered to be called
|
||||
await waitFor(() => {
|
||||
expect(onEnteredMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('does not throw if onEntered is undefined', async () => {
|
||||
// Create props without onEntered
|
||||
const props = {
|
||||
...baseProps,
|
||||
show: false, // Start with modal hidden
|
||||
};
|
||||
|
||||
const {rerender} = render(
|
||||
wrapIntl(<GenericModal {...props}/>),
|
||||
);
|
||||
|
||||
// This should not throw
|
||||
expect(() => {
|
||||
rerender(
|
||||
wrapIntl(<GenericModal {...props} show={true}/>),
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('calls onHide when modal is closed', async () => {
|
||||
const onHideMock = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
onHide: onHideMock,
|
||||
};
|
||||
render(
|
||||
wrapIntl(<GenericModal {...props}/>),
|
||||
);
|
||||
|
||||
// Find and click the close button to trigger modal exit
|
||||
const closeButton = screen.getByLabelText('Close');
|
||||
closeButton.click();
|
||||
|
||||
// Wait for onHide to be called
|
||||
await waitFor(() => {
|
||||
expect(onHideMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,9 +8,11 @@ import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import './generic_modal.scss';
|
||||
|
||||
export type ModalLocation = 'top' | 'center' | 'bottom';
|
||||
|
||||
export type Props = {
|
||||
className?: string;
|
||||
onExited: () => void;
|
||||
onExited?: () => void;
|
||||
onEntered?: () => void;
|
||||
onHide?: () => void;
|
||||
modalHeaderText?: React.ReactNode;
|
||||
@@ -32,6 +34,7 @@ export type Props = {
|
||||
enforceFocus?: boolean;
|
||||
container?: React.ReactNode | React.ReactNodeArray;
|
||||
ariaLabel?: string;
|
||||
ariaLabelledby?: string;
|
||||
errorText?: string | React.ReactNode;
|
||||
compassDesign?: boolean;
|
||||
backdrop?: boolean | 'static';
|
||||
@@ -48,6 +51,22 @@ export type Props = {
|
||||
footerDivider?: boolean;
|
||||
appendedContent?: React.ReactNode;
|
||||
headerButton?: React.ReactNode;
|
||||
showCloseButton?: boolean;
|
||||
showHeader?: boolean;
|
||||
|
||||
/*
|
||||
* Controls the vertical location of the modal.
|
||||
* 'top' => margin-top: 5vh
|
||||
* 'center' => margin-top: calc(50vh - 240px)
|
||||
* 'bottom' => margin-top: calc(50vh + 240px) (example calculation)
|
||||
*/
|
||||
modalLocation?: ModalLocation;
|
||||
|
||||
/**
|
||||
* Optionally set a test ID for the container, so that the modal can be easily referenced
|
||||
* in tests (Cypress, Playwright, etc.)
|
||||
*/
|
||||
dataTestId?: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -63,6 +82,9 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
||||
enforceFocus: true,
|
||||
keyboardEscape: true,
|
||||
bodyPadding: true,
|
||||
showCloseButton: true,
|
||||
showHeader: true,
|
||||
modalLocation: 'center',
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -184,13 +206,33 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
||||
</div>
|
||||
);
|
||||
|
||||
const locationClassMapping: Record<Required<Props>['modalLocation'], string> = {
|
||||
top: 'GenericModal__location--top',
|
||||
center: 'GenericModal__location--center',
|
||||
bottom: 'GenericModal__location--bottom',
|
||||
};
|
||||
|
||||
const modalLocationClass = locationClassMapping[this.props.modalLocation ?? 'center'];
|
||||
|
||||
// Accessibility labeling strategy:
|
||||
// 1. We always set aria-labelledby to ensure the modal has a proper label
|
||||
// - First try to use the provided ariaLabeledBy prop
|
||||
// - Fall back to 'genericModalLabel' which references the modal title
|
||||
// 2. We also support aria-label as a secondary option
|
||||
// - This will only be used by screen readers if the element referenced by aria-labelledby doesn't exist
|
||||
// - This provides a fallback for accessibility in case the referenced element is missing
|
||||
// Note: When both aria-labelledby and aria-label are present, aria-labelledby takes precedence
|
||||
const ariaLabelledby = this.props.ariaLabelledby || 'genericModalLabel';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
id={this.props.id}
|
||||
role='none'
|
||||
aria-label={this.props.ariaLabel}
|
||||
aria-labelledby={this.props.ariaLabel ? undefined : 'genericModalLabel'}
|
||||
aria-labelledby={ariaLabelledby}
|
||||
aria-modal='true'
|
||||
dialogClassName={classNames(
|
||||
modalLocationClass,
|
||||
'a11y__modal GenericModal',
|
||||
{
|
||||
GenericModal__compassDesign: this.props.compassDesign,
|
||||
@@ -208,13 +250,14 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
||||
container={this.props.container}
|
||||
keyboard={this.props.keyboardEscape}
|
||||
onEntered={this.props.onEntered}
|
||||
data-testid={this.props.dataTestId}
|
||||
>
|
||||
<div
|
||||
onKeyDown={this.onEnterKeyDown}
|
||||
tabIndex={this.props.tabIndex || 0}
|
||||
className='GenericModal__wrapper-enter-key-press-catcher'
|
||||
className='GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
{this.props.showHeader && <Modal.Header closeButton={this.props.showCloseButton}>
|
||||
<div
|
||||
className='GenericModal__header__text_container'
|
||||
>
|
||||
@@ -236,7 +279,7 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Modal.Header>
|
||||
</Modal.Header>}
|
||||
<Modal.Body className={classNames({divider: this.props.bodyDivider, 'overflow-visible': this.props.bodyOverflowVisible})}>
|
||||
{this.props.compassDesign ? (
|
||||
this.props.errorText && (
|
||||
|
||||
Ссылка в новой задаче
Block a user