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>
Этот коммит содержится в:
Pablo Vélez
2025-03-10 23:20:59 +01:00
коммит произвёл GitHub
родитель 1c6a130787
Коммит 661f7f6a83
78 изменённых файлов: 3698 добавлений и 2826 удалений

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

@@ -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 && (