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