Improved handling of onClicks for menu items (#23749)

mattermost.atlassian.net/browse/MM-52994
mattermost.atlassian.net/browse/MM-53007
mattermost.atlassian.net/browse/MM-51728
mattermost.atlassian.net/browse/MM-52758
mattermost.atlassian.net/browse/MM-53227
Этот коммит содержится в:
M-ZubairAhmed
2023-06-22 22:43:41 +05:30
коммит произвёл GitHub
родитель 644381b35e
Коммит ba4dc1a91c
25 изменённых файлов: 1089 добавлений и 1049 удалений

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

@@ -24,7 +24,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
}
show={true}
/>
<Memo(ChannelMoveToSubMenu)
<Memo(ChannelMoveToSubMenuOld)
channel={
Object {
"create_at": 0,
@@ -816,7 +816,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
}
show={true}
/>
<Memo(ChannelMoveToSubMenu)
<Memo(ChannelMoveToSubMenuOld)
channel={
Object {
"create_at": 0,

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

@@ -41,7 +41,7 @@ type Props = {
inHeaderDropdown?: boolean;
};
const ChannelMoveToSubMenu = (props: Props) => {
const ChannelMoveToSubMenuOld = (props: Props) => {
const {formatMessage} = useIntl();
const dispatch = useDispatch<DispatchFunc>();
@@ -168,4 +168,4 @@ const ChannelMoveToSubMenu = (props: Props) => {
);
};
export default memo(ChannelMoveToSubMenu);
export default memo(ChannelMoveToSubMenuOld);

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

@@ -59,7 +59,6 @@ export default class DeleteCategoryModal extends React.PureComponent<Props, Stat
/>
)}
confirmButtonClassName={'delete'}
enforceFocus={false}
>
<span className='delete-category__helpText'>
<FormattedMarkdownMessage

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

@@ -9,7 +9,7 @@ exports[`components/delete_post_modal should match snapshot for delete_post_moda
bsClass="modal"
dialogClassName="a11y__modal"
dialogComponentClass={[Function]}
enforceFocus={false}
enforceFocus={true}
id="deletePostModal"
keyboard={true}
manager={
@@ -114,7 +114,7 @@ exports[`components/delete_post_modal should match snapshot for delete_post_moda
bsClass="modal"
dialogClassName="a11y__modal"
dialogComponentClass={[Function]}
enforceFocus={false}
enforceFocus={true}
id="deletePostModal"
keyboard={true}
manager={
@@ -228,7 +228,7 @@ exports[`components/delete_post_modal should match snapshot for post with 1 comm
bsClass="modal"
dialogClassName="a11y__modal"
dialogComponentClass={[Function]}
enforceFocus={false}
enforceFocus={true}
id="deletePostModal"
keyboard={true}
manager={

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

@@ -133,7 +133,6 @@ export default class DeletePostModal extends React.PureComponent<Props, State> {
onEntered={this.handleEntered}
onHide={this.onHide}
onExited={this.props.onExited}
enforceFocus={false}
id='deletePostModal'
role='dialog'
aria-labelledby='deletePostModalLabel'

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

@@ -112,7 +112,6 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
menu={
Object {
"aria-label": "Post extra options",
"closeMenuManually": false,
"id": "CENTER_dropdown_post_id_1",
"onKeyDown": [Function],
"onToggle": [Function],
@@ -236,7 +235,7 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
/>
}
/>
<PostReminderSubmenu
<Memo(PostReminderSubmenu)
isMilitaryTime={false}
post={
Object {

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

@@ -12,7 +12,6 @@ import {GlobalState} from 'types/store';
import {DeepPartial} from '@mattermost/types/utilities';
import {PostType} from '@mattermost/types/posts';
import * as dotUtils from './utils';
jest.mock('./utils');
import DotMenu, {DotMenuClass} from './dot_menu';
@@ -286,39 +285,5 @@ describe('components/dot_menu/DotMenu', () => {
expect(menuItem).toBeVisible();
expect(menuItem).toHaveTextContent(text);
});
test.each([
[false, {isFollowingThread: true}],
[true, {isFollowingThread: false}],
])('should call setThreadFollow with following as %s', async (following, caseProps) => {
const spySetThreadFollow = jest.fn();
const spy = jest.spyOn(dotUtils, 'trackDotMenuEvent');
const props = {
...baseProps,
...caseProps,
location: Locations.RHS_ROOT,
actions: {
...baseProps.actions,
setThreadFollow: spySetThreadFollow,
},
};
renderWithIntlAndStore(
<DotMenu {...props}/>,
initialState,
);
const button = screen.getByTestId(`PostDotMenu-Button-${baseProps.post.id}`);
fireEvent.click(button);
const menuItem = screen.getByTestId(`follow_post_thread_${baseProps.post.id}`);
expect(menuItem).toBeVisible();
fireEvent.mouseDown(menuItem);
expect(spy).toHaveBeenCalled();
expect(spySetThreadFollow).toHaveBeenCalledWith(
'user_id_1',
'team_id_1',
'post_id_1',
following,
);
});
});
});

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

@@ -5,6 +5,9 @@ import React from 'react';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import classNames from 'classnames';
import {UserThread} from '@mattermost/types/threads';
import {Post} from '@mattermost/types/posts';
import {
ArrowRightBoldOutlineIcon,
BookmarkIcon,
@@ -25,24 +28,21 @@ import {
import Permissions from 'mattermost-redux/constants/permissions';
import {ModalData} from 'types/actions';
import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants';
import DeletePostModal from 'components/delete_post_modal';
import DelayedAction from 'utils/delayed_action';
import * as Keyboard from 'utils/keyboard';
import * as PostUtils from 'utils/post_utils';
import * as Menu from 'components/menu';
import * as Utils from 'utils/utils';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import {ModalData} from 'types/actions';
import {UserThread} from '@mattermost/types/threads';
import {Post} from '@mattermost/types/posts';
import ForwardPostModal from '../forward_post_modal';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import DeletePostModal from 'components/delete_post_modal';
import ForwardPostModal from 'components/forward_post_modal';
import * as Menu from 'components/menu';
import {ChangeEvent, trackDotMenuEvent} from './utils';
import PostReminderSubMenu from './post_reminder_submenu';
import './dot_menu.scss';
import {PostReminderSubmenu} from './post_reminder_submenu';
type ShortcutKeyProps = {
shortcutKey: string;
@@ -115,12 +115,6 @@ type Props = {
* Function to set the thread as followed/unfollowed
*/
setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void;
/**
* Function to set a global storage item on the store
*/
setGlobalItem: (name: string, value: any) => void;
}; // TechDebt: Made non-mandatory while converting to typescript
canEdit: boolean;
@@ -134,7 +128,6 @@ type Props = {
}
type State = {
closeMenuManually: boolean;
canEdit: boolean;
canDelete: boolean;
}
@@ -146,7 +139,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
location: Locations.CENTER,
};
private editDisableAction: DelayedAction;
private buttonRef: React.RefObject<HTMLButtonElement>;
private canPostBeForwarded: boolean;
constructor(props: Props) {
@@ -155,13 +147,10 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
this.editDisableAction = new DelayedAction(this.handleEditDisable);
this.state = {
closeMenuManually: false,
canEdit: props.canEdit && !props.isReadOnly,
canDelete: props.canDelete && !props.isReadOnly,
};
this.buttonRef = React.createRef<HTMLButtonElement>();
this.canPostBeForwarded = false;
}
@@ -173,7 +162,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
return state;
}
disableCanEditPostByTime(): void {
disableCanEditPostByTime() {
const {post, isLicensed} = this.props;
const {canEdit} = this.state;
@@ -190,32 +179,29 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
}
}
componentDidMount(): void {
componentDidMount() {
this.disableCanEditPostByTime();
}
componentWillUnmount(): void {
componentWillUnmount() {
this.editDisableAction.cancel();
}
handleEditDisable = (): void => {
handleEditDisable = () => {
this.setState({canEdit: false});
};
handleFlagMenuItemActivated = (e: ChangeEvent): void => {
handleFlagMenuItemActivated = (e: ChangeEvent) => {
if (this.props.isFlagged) {
trackDotMenuEvent(e, TELEMETRY_LABELS.UNSAVE);
this.props.actions.unflagPost(this.props.post.id);
trackDotMenuEvent(e, TELEMETRY_LABELS.UNSAVE);
} else {
trackDotMenuEvent(e, TELEMETRY_LABELS.SAVE);
this.props.actions.flagPost(this.props.post.id);
trackDotMenuEvent(e, TELEMETRY_LABELS.SAVE);
}
};
// listen to clicks/taps on add reaction menu item and pass to parent handler
handleAddReactionMenuItemActivated = (e: ChangeEvent): void => {
e.preventDefault();
handleAddReactionMenuItemActivated = () => {
// to be safe, make sure the handler function has been defined
if (this.props.handleAddReactionClick) {
this.props.handleAddReactionClick();
@@ -223,35 +209,31 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
};
copyLink = (e: ChangeEvent) => {
trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_LINK);
Utils.copyToClipboard(`${this.props.teamUrl}/pl/${this.props.post.id}`);
trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_LINK);
};
copyText = (e: ChangeEvent) => {
trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_TEXT);
Utils.copyToClipboard(this.props.post.message);
trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_TEXT);
};
handlePinMenuItemActivated = (e: ChangeEvent): void => {
if (this.props.post.is_pinned) {
trackDotMenuEvent(e, TELEMETRY_LABELS.UNPIN);
this.props.actions.unpinPost(this.props.post.id);
trackDotMenuEvent(e, TELEMETRY_LABELS.UNPIN);
} else {
trackDotMenuEvent(e, TELEMETRY_LABELS.PIN);
this.props.actions.pinPost(this.props.post.id);
trackDotMenuEvent(e, TELEMETRY_LABELS.PIN);
}
};
handleMarkPostAsUnread = (e: ChangeEvent): void => {
e.preventDefault();
trackDotMenuEvent(e, TELEMETRY_LABELS.UNREAD);
this.props.actions.markPostAsUnread(this.props.post, this.props.location);
trackDotMenuEvent(e, TELEMETRY_LABELS.UNREAD);
};
handleDeleteMenuItemActivated = (e: ChangeEvent): void => {
e.preventDefault();
trackDotMenuEvent(e, TELEMETRY_LABELS.DELETE);
const deletePostModalData = {
modalId: ModalIdentifiers.DELETE_POST,
dialogType: DeletePostModal,
@@ -262,6 +244,8 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
};
this.props.actions.openModal(deletePostModalData);
trackDotMenuEvent(e, TELEMETRY_LABELS.DELETE);
};
handleForwardMenuItemActivated = (e: ChangeEvent): void => {
@@ -271,8 +255,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
return;
}
e.preventDefault();
trackDotMenuEvent(e, TELEMETRY_LABELS.FORWARD);
const forwardPostModalData = {
modalId: ModalIdentifiers.FORWARD_POST_MODAL,
@@ -286,7 +268,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
};
handleEditMenuItemActivated = (e: ChangeEvent): void => {
trackDotMenuEvent(e, TELEMETRY_LABELS.EDIT);
this.props.handleDropdownOpened?.(false);
this.props.actions.setEditingPost(
this.props.post.id,
@@ -294,6 +275,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
this.props.post.root_id ? Utils.localizeMessage('rhs_comment.comment', 'Comment') : Utils.localizeMessage('create_post.post', 'Post'),
this.props.location === Locations.RHS_ROOT || this.props.location === Locations.RHS_COMMENT || this.props.location === Locations.SEARCH,
);
trackDotMenuEvent(e, TELEMETRY_LABELS.EDIT);
};
handleSetThreadFollow = (e: ChangeEvent) => {
@@ -325,88 +307,79 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
this.props.handleCommentClick?.(e);
};
isKeyboardEvent = (e: React.KeyboardEvent): any => {
return (e).getModifierState !== undefined;
};
handleMenuKeydown = (event: React.KeyboardEvent<HTMLDivElement>, forceCloseMenu?: (() => void)) => {
event.preventDefault();
onShortcutKeyDown = (e: React.KeyboardEvent): void => {
e.preventDefault();
if (!this.isKeyboardEvent(e)) {
if (!forceCloseMenu) {
return;
}
const isShiftKeyPressed = e.shiftKey;
const isShiftKeyPressed = event.shiftKey;
switch (true) {
case Keyboard.isKeyPressed(e, Constants.KeyCodes.R):
this.handleCommentClick(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.R):
forceCloseMenu();
this.handleCommentClick(event);
break;
// edit post
case Keyboard.isKeyPressed(e, Constants.KeyCodes.E):
this.handleEditMenuItemActivated(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.E):
forceCloseMenu();
this.handleEditMenuItemActivated(event);
break;
// follow thread
case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && !isShiftKeyPressed:
this.handleSetThreadFollow(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.F) && !isShiftKeyPressed:
forceCloseMenu();
this.handleSetThreadFollow(event);
break;
// forward post
case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && isShiftKeyPressed:
this.handleForwardMenuItemActivated(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.F) && isShiftKeyPressed:
forceCloseMenu();
this.handleForwardMenuItemActivated(event);
break;
// copy link
case Keyboard.isKeyPressed(e, Constants.KeyCodes.K):
this.copyLink(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.K):
forceCloseMenu();
this.copyLink(event);
break;
// copy text
case Keyboard.isKeyPressed(e, Constants.KeyCodes.C):
this.copyText(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.C):
forceCloseMenu();
this.copyText(event);
break;
// delete post
case Keyboard.isKeyPressed(e, Constants.KeyCodes.DELETE):
this.handleDeleteMenuItemActivated(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.DELETE):
forceCloseMenu();
this.handleDeleteMenuItemActivated(event);
break;
// pin / unpin
case Keyboard.isKeyPressed(e, Constants.KeyCodes.P):
this.handlePinMenuItemActivated(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.P):
forceCloseMenu();
this.handlePinMenuItemActivated(event);
break;
// save / unsave
case Keyboard.isKeyPressed(e, Constants.KeyCodes.S):
this.handleFlagMenuItemActivated(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.S):
forceCloseMenu();
this.handleFlagMenuItemActivated(event);
break;
// mark as unread
case Keyboard.isKeyPressed(e, Constants.KeyCodes.U):
this.handleMarkPostAsUnread(e);
this.handleDropdownOpened(false);
case Keyboard.isKeyPressed(event, Constants.KeyCodes.U):
forceCloseMenu();
this.handleMarkPostAsUnread(event);
break;
}
};
handleDropdownOpened = (open: boolean) => {
this.props.handleDropdownOpened?.(open);
this.setState({closeMenuManually: true});
};
handleMenuToggle = (open: boolean) => {
this.props.handleDropdownOpened?.(open);
this.setState({closeMenuManually: false});
};
render(): JSX.Element {
@@ -414,11 +387,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
const isFollowingThread = this.props.isFollowingThread ?? this.props.isMentionedInRootPost;
const isMobile = this.props.isMobileView;
const isSystemMessage = PostUtils.isSystemMessage(this.props.post);
const deleteShortcutText = (
<span>
{'delete'}
</span>
);
this.canPostBeForwarded = !(isSystemMessage);
@@ -482,6 +450,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
defaultMessage='Pin'
/>
);
const unPinPost = (
<FormattedMessage
id='post_info.unpin'
@@ -503,10 +472,9 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
menu={{
id: `${this.props.location}_dropdown_${this.props.post.id}`,
'aria-label': formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'}),
onKeyDown: this.onShortcutKeyDown,
onKeyDown: this.handleMenuKeydown,
width: '264px',
onToggle: this.handleMenuToggle,
closeMenuManually: this.state.closeMenuManually,
}}
menuButtonTooltip={{
id: `PostDotMenu-ButtonTooltip-${this.props.post.id}`,
@@ -563,20 +531,23 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
{Boolean(
!isSystemMessage &&
this.props.isCollapsedThreadsEnabled &&
(
this.props.location === Locations.CENTER ||
(this.props.location === Locations.CENTER ||
this.props.location === Locations.RHS_ROOT ||
this.props.location === Locations.RHS_COMMENT
),
) &&
<Menu.Item
id={`follow_post_thread_${this.props.post.id}`}
data-testid={`follow_post_thread_${this.props.post.id}`}
trailingElements={<ShortcutKey shortcutKey='F'/>}
labels={followPostLabel()}
leadingElement={isFollowingThread ? <MessageMinusOutlineIcon size={18}/> : <MessageCheckOutlineIcon size={18}/>}
onClick={this.handleSetThreadFollow}
/>
this.props.location === Locations.RHS_COMMENT)) &&
<Menu.Item
id={`follow_post_thread_${this.props.post.id}`}
data-testid={`follow_post_thread_${this.props.post.id}`}
trailingElements={<ShortcutKey shortcutKey='F'/>}
labels={followPostLabel()}
leadingElement={
isFollowingThread ? (
<MessageMinusOutlineIcon size={18}/>
) : (
<MessageCheckOutlineIcon size={18}/>
)
}
onClick={this.handleSetThreadFollow}
/>
}
{Boolean(!isSystemMessage && !this.props.channelIsArchived && this.props.location !== Locations.SEARCH) &&
<Menu.Item
@@ -594,7 +565,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
/>
}
{!isSystemMessage &&
<PostReminderSubmenu
<PostReminderSubMenu
userId={this.props.userId}
post={this.props.post}
isMilitaryTime={this.props.isMilitaryTime}
@@ -670,7 +641,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
id={`delete_post_${this.props.post.id}`}
data-testid={`delete_post_${this.props.post.id}`}
leadingElement={<TrashCanOutlineIcon size={18}/>}
trailingElements={deleteShortcutText}
trailingElements={<span>{'delete'}</span>}
labels={
<FormattedMessage
id='post_info.del'

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

@@ -43,7 +43,6 @@ import {allAtMentions} from 'utils/text_formatting';
import {matchUserMentionTriggersWithMessageMentions} from 'utils/post_utils';
import {Post} from '@mattermost/types/posts';
import {setGlobalItem} from '../../actions/storage';
import DotMenu from './dot_menu';
@@ -141,7 +140,6 @@ type Actions = {
openModal: <P>(modalData: ModalData<P>) => void;
markPostAsUnread: (post: Post) => void;
setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void;
setGlobalItem: (name: string, value: any) => void;
}
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
@@ -155,7 +153,6 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
openModal,
markPostAsUnread,
setThreadFollow,
setGlobalItem,
}, dispatch),
};
}

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {memo} from 'react';
import {useDispatch} from 'react-redux';
import {FormattedMessage, FormattedDate, FormattedTime, useIntl} from 'react-intl';
@@ -14,7 +14,6 @@ import {ModalIdentifiers} from 'utils/constants';
import {toUTCUnix} from 'utils/datetime';
import PostReminderCustomTimePicker from 'components/post_reminder_custom_time_picker_modal';
import {addPostReminder} from 'mattermost-redux/actions/posts';
import {t} from 'utils/i18n';
import {Post} from '@mattermost/types/posts';
@@ -25,93 +24,122 @@ type Props = {
timezone?: string;
}
const postReminderTimes = [
{id: 'thirty_minutes', label: t('post_info.post_reminder.sub_menu.thirty_minutes'), labelDefault: '30 mins'},
{id: 'one_hour', label: t('post_info.post_reminder.sub_menu.one_hour'), labelDefault: '1 hour'},
{id: 'two_hours', label: t('post_info.post_reminder.sub_menu.two_hours'), labelDefault: '2 hours'},
{id: 'tomorrow', label: t('post_info.post_reminder.sub_menu.tomorrow'), labelDefault: 'Tomorrow'},
{id: 'custom', label: t('post_info.post_reminder.sub_menu.custom'), labelDefault: 'Custom'},
];
const PostReminders = {
THIRTY_MINUTES: 'thirty_minutes',
ONE_HOUR: 'one_hour',
TWO_HOURS: 'two_hours',
TOMORROW: 'tomorrow',
CUSTOM: 'custom',
} as const;
export function PostReminderSubmenu(props: Props) {
function PostReminderSubmenu(props: Props) {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const setPostReminder = (id: string): void => {
const currentDate = getCurrentMomentForTimezone(props.timezone);
let endTime = currentDate;
switch (id) {
case 'thirty_minutes':
// add 30 minutes in current time
endTime = currentDate.add(30, 'minutes');
break;
case 'one_hour':
// add 1 hour in current time
endTime = currentDate.add(1, 'hour');
break;
case 'two_hours':
// add 2 hours in current time
endTime = currentDate.add(2, 'hours');
break;
case 'tomorrow':
// add one day in current date
endTime = currentDate.add(1, 'day');
break;
function handlePostReminderMenuClick(id: string) {
if (id === PostReminders.CUSTOM) {
const postReminderCustomTimePicker = {
modalId: ModalIdentifiers.POST_REMINDER_CUSTOM_TIME_PICKER,
dialogType: PostReminderCustomTimePicker,
dialogProps: {
postId: props.post.id,
},
};
dispatch(openModal(postReminderCustomTimePicker));
} else {
const currentDate = getCurrentMomentForTimezone(props.timezone);
let endTime = currentDate;
if (id === PostReminders.THIRTY_MINUTES) {
// add 30 minutes in current time
endTime = currentDate.add(30, 'minutes');
} else if (id === PostReminders.ONE_HOUR) {
// add 1 hour in current time
endTime = currentDate.add(1, 'hour');
} else if (id === PostReminders.TWO_HOURS) {
// add 2 hours in current time
endTime = currentDate.add(2, 'hours');
} else if (id === PostReminders.TOMORROW) {
// add one day in current date
endTime = currentDate.add(1, 'day');
}
dispatch(addPostReminder(props.userId, props.post.id, toUTCUnix(endTime.toDate())));
}
}
const postReminderSubMenuItems = Object.values(PostReminders).map((postReminder) => {
let labels = null;
if (postReminder === PostReminders.THIRTY_MINUTES) {
labels = (
<FormattedMessage
id='post_info.post_reminder.sub_menu.thirty_minutes'
defaultMessage='30 mins'
/>
);
} else if (postReminder === PostReminders.ONE_HOUR) {
labels = (
<FormattedMessage
id='post_info.post_reminder.sub_menu.one_hour'
defaultMessage='1 hour'
/>
);
} else if (postReminder === PostReminders.TWO_HOURS) {
labels = (
<FormattedMessage
id='post_info.post_reminder.sub_menu.two_hours'
defaultMessage='2 hours'
/>
);
} else if (postReminder === PostReminders.TOMORROW) {
labels = (
<FormattedMessage
id='post_info.post_reminder.sub_menu.tomorrow'
defaultMessage='Tomorrow'
/>
);
} else {
labels = (
<FormattedMessage
id='post_info.post_reminder.sub_menu.custom'
defaultMessage='Custom'
/>
);
}
dispatch(addPostReminder(props.userId, props.post.id, toUTCUnix(endTime.toDate())));
};
let trailingElements = null;
if (postReminder === PostReminders.TOMORROW) {
const tomorrow = getCurrentMomentForTimezone(props.timezone).add(1, 'day').toDate();
const setCustomPostReminder = (): void => {
const postReminderCustomTimePicker = {
modalId: ModalIdentifiers.POST_REMINDER_CUSTOM_TIME_PICKER,
dialogType: PostReminderCustomTimePicker,
dialogProps: {
postId: props.post.id,
},
};
dispatch(openModal(postReminderCustomTimePicker));
};
const postReminderSubMenuItems =
postReminderTimes.map(({id, label, labelDefault}) => {
const labels = (
<FormattedMessage
id={label}
defaultMessage={labelDefault}
/>
trailingElements = (
<span className={`postReminder-${postReminder}_timestamp`}>
<FormattedDate
value={tomorrow}
weekday='short'
timeZone={props.timezone}
/>
{', '}
<FormattedTime
value={tomorrow}
timeStyle='short'
hour12={!props.isMilitaryTime}
timeZone={props.timezone}
/>
</span>
);
}
let trailing: React.ReactNode;
if (id === 'tomorrow') {
const tomorrow = getCurrentMomentForTimezone(props.timezone).add(1, 'day').toDate();
trailing = (
<span className={`postReminder-${id}_timestamp`}>
<FormattedDate
value={tomorrow}
weekday='short'
timeZone={props.timezone}
/>
{', '}
<FormattedTime
value={tomorrow}
timeStyle='short'
hour12={!props.isMilitaryTime}
timeZone={props.timezone}
/>
</span>
);
}
return (
<Menu.Item
key={`remind_post_options_${id}`}
id={`remind_post_options_${id}`}
labels={labels}
trailingElements={trailing}
onClick={id === 'custom' ? () => setCustomPostReminder() : () => setPostReminder(id)}
/>
);
});
return (
<Menu.Item
id={`remind_post_options_${postReminder}`}
key={`remind_post_options_${postReminder}`}
labels={labels}
trailingElements={trailingElements}
onClick={() => handlePostReminderMenuClick(postReminder)}
/>
);
});
return (
<Menu.SubMenu
@@ -136,3 +164,5 @@ export function PostReminderSubmenu(props: Props) {
</Menu.SubMenu>
);
}
export default memo(PostReminderSubmenu);

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

@@ -132,7 +132,6 @@ export default class EditCategoryModal extends React.PureComponent<Props, State>
handleConfirm={this.handleConfirm}
handleCancel={this.handleCancel}
isConfirmDisabled={this.isConfirmDisabled()}
enforceFocus={false}
>
<QuickInput
inputComponent={MaxLengthInput}

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

@@ -8,7 +8,8 @@ import React, {
useEffect,
KeyboardEvent,
SyntheticEvent,
KeyboardEventHandler,
useMemo,
useCallback,
} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import MuiMenuList from '@mui/material/MenuList';
@@ -28,10 +29,11 @@ import OverlayTrigger from 'components/overlay_trigger';
import {GenericModal} from '@mattermost/components';
import {MuiMenuStyled} from './menu_styled';
import {MenuContext} from './menu_context';
const OVERLAY_TIME_DELAY = 500;
const MENU_OPEN_ANIMATION_DURATION = 150;
const MENU_CLOSE_ANIMATION_DURATION = 100;
export const MENU_CLOSE_ANIMATION_DURATION = 100;
type MenuButtonProps = {
id: string;
@@ -56,8 +58,7 @@ type MenuProps = {
* @warning Make the styling of your components such a way that they dont need this handler
*/
onToggle?: (isOpen: boolean) => void;
closeMenuManually?: boolean;
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>, forceCloseMenu?: () => void) => void;
width?: string;
}
@@ -89,30 +90,30 @@ export function Menu(props: Props) {
const [disableAutoFocusItem, setDisableAutoFocusItem] = useState(false);
const isMenuOpen = Boolean(anchorElement);
// Callback funtion handler called when menu is closed by escapeKeyDown, backdropClick or tabKeyDown
function handleMenuClose(event: MouseEvent<HTMLDivElement>) {
event.preventDefault();
setAnchorElement(null);
setDisableAutoFocusItem(false);
}
// Handle function injected into menu items to close the menu
const closeMenu = useCallback(() => {
setAnchorElement(null);
setDisableAutoFocusItem(false);
}, []);
function handleMenuModalClose(modalId: MenuProps['id']) {
dispatch(closeModal(modalId));
setAnchorElement(null);
}
function handleMenuClick() {
setAnchorElement(null);
// Stop sythetic events from bubbling up to the parent
// @see https://github.com/mui/material-ui/issues/32064
function handleMenuClick(e: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
e.stopPropagation();
}
useEffect(() => {
if (props.menu.closeMenuManually) {
setAnchorElement(null);
if (isMobileView) {
handleMenuModalClose(props.menu.id);
}
}
}, [props.menu.closeMenuManually]);
function handleMenuKeyDown(event: KeyboardEvent<HTMLDivElement>) {
if (isKeyPressed(event, Constants.KeyCodes.ENTER) || isKeyPressed(event, Constants.KeyCodes.SPACE)) {
const target = event.target as HTMLElement;
@@ -125,7 +126,13 @@ export function Menu(props: Props) {
setAnchorElement(null);
}
}
props.menu.onKeyDown?.(event);
if (props.menu.onKeyDown) {
// We need to pass the closeMenu function to the onKeyDown handler so that the menu can be closed manually
// This is helpful for cases when menu needs to be closed after certain keybindings are pressed in components which uses menu
// This however is not the case for mouse events as they are handled/closed by menu item click handlers
props.menu.onKeyDown(event, closeMenu);
}
}
function handleMenuButtonClick(event: SyntheticEvent<HTMLButtonElement>) {
@@ -152,13 +159,13 @@ export function Menu(props: Props) {
}
}
// Function to prevent focus-visible from being set on clicking menu items with the mouse
function handleMenuButtonMouseDown() {
// This is needed to prevent focus-visible being set on clicking menuitems with mouse
setDisableAutoFocusItem(true);
}
// We construct the menu button so we can set onClick correctly here to support both web and mobile view
function renderMenuButton() {
// We construct the menu button so we can set onClick correctly here to support both web and mobile view
const triggerElement = (
<button
id={props.menuButton.id}
@@ -204,6 +211,13 @@ export function Menu(props: Props) {
}
}, [isMenuOpen]);
const providerValue = useMemo(() => {
return {
close: closeMenu,
isOpen: Boolean(anchorElement),
};
}, [anchorElement, closeMenu]);
if (isMobileView) {
// In mobile view, the menu is rendered as a modal
return renderMenuButton();
@@ -219,6 +233,7 @@ export function Menu(props: Props) {
onClick={handleMenuClick}
onKeyDown={handleMenuKeyDown}
className={A11yClassNames.POPUP}
width={props.menu.width}
disableAutoFocusItem={disableAutoFocusItem} // This is not anti-pattern, see handleMenuButtonMouseDown
MenuListProps={{
id: props.menu.id,
@@ -232,9 +247,10 @@ export function Menu(props: Props) {
exit: MENU_CLOSE_ANIMATION_DURATION,
},
}}
width={props.menu.width}
>
{props.children}
<MenuContext.Provider value={providerValue}>
{props.children}
</MenuContext.Provider>
</MuiMenuStyled>
</CompassDesignProvider>
);
@@ -246,13 +262,13 @@ interface MenuModalProps {
menuAriaLabel: MenuProps['aria-label'];
onModalClose: (modalId: MenuProps['id']) => void;
children: Props['children'];
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
onKeyDown?: MenuProps['onKeyDown'];
}
function MenuModal(props: MenuModalProps) {
const theme = useSelector(getTheme);
function handleModalExited() {
function closeMenuModal() {
props.onModalClose(props.menuId);
}
@@ -262,15 +278,16 @@ function MenuModal(props: MenuModalProps) {
if (currentElement.contains(event.target as Node) && !currentElement.ariaHasPopup) {
// We check for property ariaHasPopup because we don't want to close the menu
// if the user clicks on a submenu item or menu item which open modal. And let submenu component handle the click.
handleModalExited();
closeMenuModal();
break;
}
}
}
}
function handleKeydown(event?: React.KeyboardEvent<HTMLDivElement>) {
if (event && props.onKeyDown) {
props.onKeyDown(event);
props.onKeyDown(event, closeMenuModal);
}
}
@@ -281,7 +298,7 @@ function MenuModal(props: MenuModalProps) {
className='menuModal'
backdrop={true}
ariaLabel={props.menuAriaLabel}
onExited={handleModalExited}
onExited={closeMenuModal}
enforceFocus={false}
handleKeydown={handleKeydown}
>

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

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {createContext} from 'react';
interface MenuSubmenuContextType {
close?: () => void;
isOpen: boolean;
}
export const MenuContext = createContext<MenuSubmenuContextType>({
isOpen: false,
});
MenuContext.displayName = 'MenuContext';
export const SubMenuContext = createContext<MenuSubmenuContextType>({
isOpen: false,
});
SubMenuContext.displayName = 'SubMenuContext';

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

@@ -1,14 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactElement, ReactNode, Children, KeyboardEvent, MouseEvent} from 'react';
import React, {
ReactElement,
ReactNode,
Children,
KeyboardEvent,
MouseEvent,
useContext,
useRef,
useEffect,
} from 'react';
import {styled} from '@mui/material/styles';
import {useSelector} from 'react-redux';
import MuiMenuItem from '@mui/material/MenuItem';
import type {MenuItemProps as MuiMenuItemProps} from '@mui/material/MenuItem';
import {cloneDeep} from 'lodash';
import Constants from 'utils/constants';
import {getIsMobileView} from 'selectors/views/browser';
import Constants, {EventTypes} from 'utils/constants';
import {isKeyPressed} from 'utils/keyboard';
import {MENU_CLOSE_ANIMATION_DURATION} from './menu';
import {MenuContext, SubMenuContext} from './menu_context';
const DELAY_CLICK_EVENT_EXECUTION_MODIFIER = 1.2;
export interface Props extends MuiMenuItemProps {
/**
@@ -63,7 +81,7 @@ export interface Props extends MuiMenuItemProps {
*/
isDestructive?: boolean;
onClick: (event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) => void;
onClick?: (event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) => void;
/**
* ONLY to support submenus. Avoid passing children to this component. Support for children is only added to support submenus.
@@ -73,7 +91,7 @@ export interface Props extends MuiMenuItemProps {
/**
* To be used as a child of Menu component.
* Checkout Compass's Menu Item(compass.mattermost.com) for terminology, styling and usage guidelines.
* Checkout Compass's Menu Item(compass.mattermost.com) for terminology, styling and usage guidelines.
*
* @example
* <Menu.Container>
@@ -92,15 +110,68 @@ export function MenuItem(props: Props) {
...restProps
} = props;
// When both primary and secondary labels are passed, we need to apply minor changes to the styling. Check below in styled component for more details.
const hasSecondaryLabel = labels && labels.props && labels.props.children && Children.count(labels.props.children) === 2;
const menuContext = useContext(MenuContext);
const subMenuContext = useContext(SubMenuContext);
const isMobileView = useSelector(getIsMobileView);
const onClickEventRef = useRef<MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>>();
function handleClick(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
if (isCorrectKeyPressedOnMenuItem(event)) {
onClick(event);
// close submenu first if it is open
if (subMenuContext.close) {
subMenuContext.close();
}
// And then close the menu
if (menuContext.close) {
menuContext.close();
}
if (onClick) {
if (isMobileView) {
// If the menu is in mobile view, we execute the click event immediately.
onClick(event);
} else {
// We set the ref of event here, see the `useEffect` hook below for more details.
onClickEventRef.current = cloneDeep(event);
}
}
}
}
// This `useEffect` hook is responsible for executing a click event (`onClick`).
// 1. If MenuItem was part of submenu then both menu and submenu should be closed before executing the click event.
// 2. If MenuItem was part of only Menu then only should be closed before executing the click event.
// After the conditions are met the delay is introduced to allow the menu to animate out properly before executing the click event.
// This delay also improves percieved UX as it gives the user a chance to see the menu close before the click event is executed. (eg in case of opening a modal)
useEffect(() => {
let shouldExecuteClick = false;
if (subMenuContext.close) {
// This means that the menu item is a submenu item and both menu and submenu are closed.
shouldExecuteClick = subMenuContext.isOpen === false && menuContext.isOpen === false && Boolean(onClickEventRef.current);
} else {
shouldExecuteClick = menuContext.isOpen === false && Boolean(onClickEventRef.current);
}
if (shouldExecuteClick) {
const delayExecutionTimeout = MENU_CLOSE_ANIMATION_DURATION * DELAY_CLICK_EVENT_EXECUTION_MODIFIER;
setTimeout(() => {
if (onClick && onClickEventRef.current) {
onClick(onClickEventRef.current);
}
onClickEventRef.current = undefined;
}, delayExecutionTimeout);
}
}, [menuContext.isOpen, subMenuContext.isOpen, subMenuContext.close, onClick]);
// When both primary and secondary labels are passed, we need to apply minor changes to the styling. Check below in styled component for more details.
const hasSecondaryLabel = labels && labels.props && labels.props.children && Children.count(labels.props.children) === 2;
return (
<MenuItemStyled
disableRipple={true}
@@ -239,14 +310,14 @@ const MenuItemStyled = styled(MuiMenuItem, {
* @returns true if the menu item was pressed by mouse's "Primary" key or keyboard's "Space" or "Enter" key
**/
function isCorrectKeyPressedOnMenuItem(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
if (event.type === 'keydown') {
if (event.type === EventTypes.KEY_DOWN) {
const keyboardEvent = event as KeyboardEvent<HTMLLIElement>;
if (isKeyPressed(keyboardEvent, Constants.KeyCodes.ENTER) || isKeyPressed(keyboardEvent, Constants.KeyCodes.SPACE)) {
return true;
}
return false;
} else if (event.type === 'mousedown') {
} else if (event.type === EventTypes.MOUSE_DOWN) {
const mouseEvent = event as MouseEvent<HTMLLIElement>;
if (mouseEvent.button === 0) {
return true;

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

@@ -10,6 +10,7 @@ import {Divider} from '@mui/material';
* <Menu.Container>
* <Menu.Item>
* <Menu.Separator />
* </Menu.Container>
*/
export function MenuItemSeparator() {
return (

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

@@ -1,7 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactNode, useState, MouseEvent, KeyboardEvent, useEffect, useMemo} from 'react';
import React, {
ReactNode,
useState,
MouseEvent,
KeyboardEvent,
useEffect,
useMemo,
useCallback,
} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import MuiMenuList from '@mui/material/MenuList';
import {PopoverOrigin} from '@mui/material/Popover';
@@ -20,7 +28,8 @@ import CompassDesignProvider from 'components/compass_design_provider';
import {GenericModal} from '@mattermost/components';
import {MuiMenuStyled} from './menu_styled';
import {MenuItem as ParentMenuItem, Props as MenuItemProps} from './menu_item';
import {MenuItem, Props as MenuItemProps} from './menu_item';
import {SubMenuContext} from './menu_context';
import './sub_menu.scss';
@@ -39,41 +48,66 @@ interface Props {
children: ReactNode;
}
export function SubMenu({id, leadingElement, labels, trailingElements, isDestructive, menuId, menuAriaLabel, forceOpenOnLeft, children, ...rest}: Props) {
export function SubMenu(props: Props) {
const {
id,
leadingElement,
labels,
trailingElements,
isDestructive,
menuId,
menuAriaLabel,
forceOpenOnLeft,
children,
...rest
} = props;
const [anchorElement, setAnchorElement] = useState<null | HTMLElement>(null);
const isSubMenuOpen = Boolean(anchorElement);
const isMobileView = useSelector(getIsMobileView);
const anyModalOpen = useSelector(isAnyModalOpen);
const dispatch = useDispatch();
function handleSubMenuOpen(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
if (isMobileView) {
dispatch(openModal<SubMenuModalProps>({
modalId: menuId,
dialogType: SubMenuModal,
dialogProps: {
menuId,
menuAriaLabel,
children,
},
}));
} else {
setAnchorElement(event.currentTarget);
useEffect(() => {
if (anyModalOpen && !isMobileView) {
setAnchorElement(null);
}
}, [anyModalOpen, isMobileView]);
const originOfAnchorAndTransform = useMemo(() => {
return getOriginOfAnchorAndTransform(forceOpenOnLeft, anchorElement);
}, [anchorElement, forceOpenOnLeft]);
// Handler function injected in the menu items to close the submenu
const closeSubMenu = useCallback(() => {
setAnchorElement(null);
}, []);
const providerValue = useMemo(() => {
return {
close: closeSubMenu,
isOpen: Boolean(anchorElement),
};
}, [anchorElement, closeSubMenu]);
const hasSubmenuItems = Boolean(children);
if (!hasSubmenuItems) {
return null;
}
function handleSubMenuClose(event: MouseEvent<HTMLLIElement>) {
function handleMouseEnter(event: MouseEvent<HTMLLIElement>) {
event.preventDefault();
setAnchorElement(event.currentTarget);
}
function handleMouseLeave(event: MouseEvent<HTMLLIElement>) {
event.preventDefault();
setAnchorElement(null);
}
// This handleKeyDown is on the menu item which opens the submenu
function handleSubMenuParentItemKeyDown(event: KeyboardEvent<HTMLLIElement>) {
function handleKeyDown(event: KeyboardEvent<HTMLLIElement>) {
if (
isKeyPressed(event, Constants.KeyCodes.ENTER) ||
isKeyPressed(event, Constants.KeyCodes.SPACE) ||
@@ -94,17 +128,17 @@ export function SubMenu({id, leadingElement, labels, trailingElements, isDestruc
}
}
useEffect(() => {
if (anyModalOpen && !isMobileView) {
setAnchorElement(null);
}
}, [anyModalOpen, isMobileView]);
const originOfAnchorAndTransform = useMemo(() => getOriginOfAnchorAndTransform(forceOpenOnLeft, anchorElement), [anchorElement]);
const hasSubmenuItems = Boolean(children);
if (!hasSubmenuItems) {
return null;
// This is used in MobileView to open the submenu in a modal
function handleOnClick() {
dispatch(openModal<SubMenuModalProps>({
modalId: menuId,
dialogType: SubMenuModal,
dialogProps: {
menuId,
menuAriaLabel,
children,
},
}));
}
const passedInTriggerButtonProps = {
@@ -117,20 +151,20 @@ export function SubMenu({id, leadingElement, labels, trailingElements, isDestruc
labels,
trailingElements,
isDestructive,
onClick: handleSubMenuOpen,
onClick: isMobileView ? handleOnClick : undefined, // OnClicks on parent menuItem of subMenu is only needed in mobile view
};
if (isMobileView) {
return (<ParentMenuItem {...passedInTriggerButtonProps}/>);
return (<MenuItem {...passedInTriggerButtonProps}/>);
}
return (
<ParentMenuItem
<MenuItem
{...rest} // pass through other props which might be coming in from the material-ui
{...passedInTriggerButtonProps}
onMouseEnter={handleSubMenuOpen}
onMouseLeave={handleSubMenuClose}
onKeyDown={handleSubMenuParentItemKeyDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onKeyDown={handleKeyDown}
>
<MuiMenuStyled
anchorEl={anchorElement}
@@ -138,8 +172,11 @@ export function SubMenu({id, leadingElement, labels, trailingElements, isDestruc
asSubMenu={true}
anchorOrigin={originOfAnchorAndTransform.anchorOrigin}
transformOrigin={originOfAnchorAndTransform.transformOrigin}
sx={{pointerEvents: 'none'}} // disables the menu background wrapper for accessing submenu
sx={{pointerEvents: 'none'}}
>
{/* This component is needed here to re enable pointer events for the submenu items which we had to disable above as */}
{/* pointer turns to default as soon as it leaves the parent menu */}
{/* Notice we dont use the below component in menu.tsx */}
<MuiMenuList
id={menuId}
component='ul'
@@ -152,10 +189,12 @@ export function SubMenu({id, leadingElement, labels, trailingElements, isDestruc
paddingBottom: 0,
}}
>
{children}
<SubMenuContext.Provider value={providerValue}>
{children}
</SubMenuContext.Provider>
</MuiMenuList>
</MuiMenuStyled>
</ParentMenuItem>
</MenuItem>
);
}

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

@@ -9,7 +9,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
bsClass="modal"
dialogClassName="a11y__modal more-modal more-direct-channels"
dialogComponentClass={[Function]}
enforceFocus={false}
enforceFocus={true}
id="moreDmModal"
keyboard={true}
manager={
@@ -294,7 +294,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
bsClass="modal"
dialogClassName="a11y__modal more-modal more-direct-channels"
dialogComponentClass={[Function]}
enforceFocus={false}
enforceFocus={true}
id="moreDmModal"
keyboard={true}
manager={

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

@@ -291,7 +291,6 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
role='dialog'
aria-labelledby='moreDmModalLabel'
id='moreDmModal'
enforceFocus={false}
>
<Modal.Header closeButton={true}>
<Modal.Title

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -2,21 +2,18 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {Moment} from 'moment-timezone';
import {FormattedMessage, useIntl} from 'react-intl';
import {GenericModal} from '@mattermost/components';
import {isKeyPressed} from 'utils/keyboard';
import {localizeMessage} from 'utils/utils';
import DateTimeInput, {getRoundedTime} from 'components/custom_status/date_time_input';
import {isKeyPressed} from 'utils/keyboard';
import {toUTCUnix} from 'utils/datetime';
import {getCurrentMomentForTimezone} from 'utils/timezone';
import Constants from 'utils/constants';
import type {PropsFromRedux} from './index';
import './post_reminder_custom_time_picker_modal.scss';
type Props = PropsFromRedux & {
@@ -27,55 +24,55 @@ type Props = PropsFromRedux & {
};
};
const modalHeaderText = (
<FormattedMessage
id='post_reminder.custom_time_picker_modal.header'
defaultMessage='Set a reminder'
/>
);
const confirmButtonText = (
<FormattedMessage
id='post_reminder.custom_time_picker_modal.submit_button'
defaultMessage='Set reminder'
/>
);
function PostReminderCustomTimePicker({userId, timezone, onExited, postId, actions}: Props) {
const currentTime = getCurrentMomentForTimezone(timezone);
const initialReminderTime: Moment = getRoundedTime(currentTime);
const [customReminderTime, setCustomReminderTime] = useState<Moment>(initialReminderTime);
const initialReminderTime = getRoundedTime(currentTime);
const [customReminderTime, setCustomReminderTime] = useState(initialReminderTime);
const handleConfirm = useCallback(() => {
actions.addPostReminder(userId, postId, toUTCUnix(customReminderTime.toDate()));
}, [customReminderTime]);
const [isDatePickerOpen, setIsDatePickerOpen] = useState<boolean>(false);
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
const handleKeyDown = useCallback((event: KeyboardEvent) => {
if (isKeyPressed(event, Constants.KeyCodes.ESCAPE) && !isDatePickerOpen) {
onExited();
}
}, [isDatePickerOpen, onExited]);
const {formatMessage} = useIntl();
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (isKeyPressed(event, Constants.KeyCodes.ESCAPE) && !isDatePickerOpen) {
onExited();
}
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
}, [isDatePickerOpen]);
return (
<GenericModal
ariaLabel={localizeMessage('post_reminder_custom_time_picker_modal.defaultMsg', 'Set a reminder')}
id='PostReminderCustomTimePickerModal'
ariaLabel={formatMessage({id: 'post_reminder_custom_time_picker_modal.defaultMsg', defaultMessage: 'Set a reminder'})}
onExited={onExited}
modalHeaderText={modalHeaderText}
confirmButtonText={confirmButtonText}
modalHeaderText={(
<FormattedMessage
id='post_reminder.custom_time_picker_modal.header'
defaultMessage='Set a reminder'
/>
)}
confirmButtonText={(
<FormattedMessage
id='post_reminder.custom_time_picker_modal.submit_button'
defaultMessage='Set reminder'
/>
)}
handleConfirm={handleConfirm}
handleEnterKeyPress={handleConfirm}
id='PostReminderCustomTimePickerModal'
className={'post-reminder-modal'}
compassDesign={true}
enforceFocus={true}
keyboardEscape={false}
>
<DateTimeInput

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

@@ -143,6 +143,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
this.props.actions.openModal({
modalId: ModalIdentifiers.EDIT_CATEGORY,
dialogType: EditCategoryModal,
dialogProps: {},
});
trackEvent('ui', 'ui_sidebar_menu_createCategory');
};

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, MouseEvent, useState, KeyboardEvent} from 'react';
import React, {memo, useState} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import classNames from 'classnames';
@@ -44,8 +44,7 @@ const SidebarCategoryMenu = (props: Props) => {
let muteUnmuteCategoryMenuItem: JSX.Element | null = null;
if (props.category.type !== CategoryTypes.DIRECT_MESSAGES) {
function toggleCategoryMute(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
function toggleCategoryMute() {
props.setCategoryMuted(props.category.id, !props.category.muted);
}
@@ -127,9 +126,7 @@ const SidebarCategoryMenu = (props: Props) => {
);
}
function handleSortChannels(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>, sorting: CategorySorting) {
event.preventDefault();
function handleSortChannels(sorting: CategorySorting) {
props.setCategorySorting(props.category.id, sorting);
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
}
@@ -186,7 +183,7 @@ const SidebarCategoryMenu = (props: Props) => {
defaultMessage='Alphabetically'
/>
)}
onClick={(event) => handleSortChannels(event, CategorySorting.Alphabetical)}
onClick={() => handleSortChannels(CategorySorting.Alphabetical)}
/>
<Menu.Item
id={`sortByMostRecent-${props.category.id}`}
@@ -196,7 +193,7 @@ const SidebarCategoryMenu = (props: Props) => {
defaultMessage='Recent Activity'
/>
)}
onClick={(event) => handleSortChannels(event, CategorySorting.Recency)}
onClick={() => handleSortChannels(CategorySorting.Recency)}
/>
<Menu.Item
id={`sortManual-${props.category.id}`}
@@ -206,7 +203,7 @@ const SidebarCategoryMenu = (props: Props) => {
defaultMessage='Manually'
/>
)}
onClick={(event) => handleSortChannels(event, CategorySorting.Manual)}
onClick={() => handleSortChannels(CategorySorting.Manual)}
/>
</Menu.SubMenu>
);

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

@@ -37,9 +37,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const {formatMessage} = useIntl();
function handleSortDirectMessages(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>, sorting: CategorySorting) {
event.preventDefault();
function handleSortDirectMessages(sorting: CategorySorting) {
props.setCategorySorting(props.category.id, sorting);
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
}
@@ -87,7 +85,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
defaultMessage='Alphabetically'
/>
)}
onClick={(event) => handleSortDirectMessages(event, CategorySorting.Alphabetical)}
onClick={() => handleSortDirectMessages(CategorySorting.Alphabetical)}
/>
<Menu.Item
id={`sortByMostRecent-${props.category.id}`}
@@ -97,14 +95,13 @@ const SidebarCategorySortingMenu = (props: Props) => {
defaultMessage='Recent Activity'
/>
)}
onClick={(event) => handleSortDirectMessages(event, CategorySorting.Recency)}
onClick={() => handleSortDirectMessages(CategorySorting.Recency)}
/>
</Menu.SubMenu>
);
function handlelimitVisibleDMsGMs(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>, number: number) {
event.preventDefault();
function handlelimitVisibleDMsGMs(number: number) {
props.savePreferences(props.currentUserId, [{
user_id: props.currentUserId,
category: Constants.Preferences.CATEGORY_SIDEBAR_SETTINGS,
@@ -149,7 +146,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
defaultMessage='All direct messages'
/>
)}
onClick={(event) => handlelimitVisibleDMsGMs(event, Constants.HIGHEST_DM_SHOW_COUNT)}
onClick={() => handlelimitVisibleDMsGMs(Constants.HIGHEST_DM_SHOW_COUNT)}
/>
<Menu.Separator/>
{Constants.DM_AND_GM_SHOW_COUNTS.map((dmGmShowCount) => (
@@ -157,7 +154,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
id={`showDmCount-${props.category.id}-${dmGmShowCount}`}
key={`showDmCount-${props.category.id}-${dmGmShowCount}`}
labels={<span>{dmGmShowCount}</span>}
onClick={(event) => handlelimitVisibleDMsGMs(event, dmGmShowCount)}
onClick={() => handlelimitVisibleDMsGMs(dmGmShowCount)}
/>
))}
</Menu.SubMenu>

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

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useRef, MouseEvent, KeyboardEvent, memo} from 'react';
import React, {useRef, memo} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {
@@ -36,9 +36,7 @@ const SidebarChannelMenu = (props: Props) => {
let markAsReadUnreadMenuItem: JSX.Element | null = null;
if (props.isUnread) {
function handleMarkAsRead(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
function handleMarkAsRead() {
props.markChannelAsRead(props.channel.id);
trackEvent('ui', 'ui_sidebar_channel_menu_markAsRead');
}
@@ -58,9 +56,7 @@ const SidebarChannelMenu = (props: Props) => {
);
} else {
function handleMarkAsUnread(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
function handleMarkAsUnread() {
props.markMostRecentPostInChannelAsUnread(props.channel.id);
trackEvent('ui', 'ui_sidebar_channel_menu_markAsUnread');
}
@@ -82,9 +78,7 @@ const SidebarChannelMenu = (props: Props) => {
let favoriteUnfavoriteMenuItem: JSX.Element | null = null;
if (props.isFavorite) {
function handleUnfavoriteChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
function handleUnfavoriteChannel() {
props.unfavoriteChannel(props.channel.id);
trackEvent('ui', 'ui_sidebar_channel_menu_unfavorite');
}
@@ -103,9 +97,7 @@ const SidebarChannelMenu = (props: Props) => {
/>
);
} else {
function handleFavoriteChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
function handleFavoriteChannel() {
props.favoriteChannel(props.channel.id);
trackEvent('ui', 'ui_sidebar_channel_menu_favorite');
}
@@ -143,9 +135,7 @@ const SidebarChannelMenu = (props: Props) => {
);
}
function handleUnmuteChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
function handleUnmuteChannel() {
props.unmuteChannel(props.currentUserId, props.channel.id);
}
@@ -173,9 +163,7 @@ const SidebarChannelMenu = (props: Props) => {
);
}
function handleMuteChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
function handleMuteChannel() {
props.muteChannel(props.currentUserId, props.channel.id);
}
@@ -191,9 +179,7 @@ const SidebarChannelMenu = (props: Props) => {
let copyLinkMenuItem: JSX.Element | null = null;
if (props.channel.type === Constants.OPEN_CHANNEL || props.channel.type === Constants.PRIVATE_CHANNEL) {
function handleCopyLink(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
function handleCopyLink() {
copyToClipboard(props.channelLink);
}
@@ -256,9 +242,7 @@ const SidebarChannelMenu = (props: Props) => {
);
}
function handleLeaveChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
event.preventDefault();
function handleLeaveChannel() {
if (isLeaving.current || !props.channelLeaveHandler) {
return;
}

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

@@ -6,7 +6,6 @@ import classNames from 'classnames';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {FocusTrap} from '../focus_trap';
import './generic_modal.scss';
export type Props = {
@@ -27,11 +26,6 @@ export type Props = {
id: string;
autoCloseOnCancelButton?: boolean;
autoCloseOnConfirmButton?: boolean;
/**
* If false, bootrap's Modal will not enforce focus on the modal and will
* transfer the mechanism to the FocusTrap component instead.
*/
enforceFocus?: boolean;
container?: React.ReactNode | React.ReactNodeArray;
ariaLabel?: string;
@@ -111,12 +105,6 @@ export class GenericModal extends React.PureComponent<Props, State> {
this.props.handleKeydown?.(event);
}
private handleShow = () => {
if (this.props.enforceFocus === false) {
this.setState({isFocalTrapActive: true});
}
}
render() {
let confirmButton;
if (this.props.handleConfirm) {
@@ -178,8 +166,6 @@ export class GenericModal extends React.PureComponent<Props, State> {
</div>
);
const isFocusTrapActive = this.props.enforceFocus === false ? this.state.isFocalTrapActive : false;
return (
<Modal
id={this.props.id}
@@ -188,7 +174,6 @@ export class GenericModal extends React.PureComponent<Props, State> {
aria-labelledby={this.props.ariaLabel ? undefined : 'genericModalLabel'}
dialogClassName={classNames('a11y__modal GenericModal', {GenericModal__compassDesign: this.props.compassDesign}, this.props.className)}
show={this.state.show}
onShow={this.handleShow}
restoreFocus={true}
enforceFocus={this.props.enforceFocus}
onHide={this.onHide}
@@ -198,49 +183,47 @@ export class GenericModal extends React.PureComponent<Props, State> {
container={this.props.container}
keyboard={this.props.keyboardEscape}
>
<FocusTrap active={isFocusTrapActive}>
<div
onKeyDown={this.onEnterKeyDown}
tabIndex={this.props.tabIndex || 0}
className='GenericModal__wrapper-enter-key-press-catcher'
>
<Modal.Header closeButton={true}>
{this.props.compassDesign && (
<>
{headerText}
{this.props.headerInput}
</>
)}
</Modal.Header>
<Modal.Body>
{this.props.compassDesign ? (
this.props.errorText && (
<div className='genericModalError'>
<i className='icon icon-alert-outline'/>
<span>{this.props.errorText}</span>
</div>
)
) : (
headerText
)}
<div className={classNames('GenericModal__body', {padding: this.props.bodyPadding})}>
{this.props.children}
</div>
</Modal.Body>
{(cancelButton || confirmButton || this.props.footerContent) && (
<Modal.Footer className={classNames({divider: this.props.footerDivider})}>
{(cancelButton || confirmButton) ? (
<>
{cancelButton}
{confirmButton}
</>
) : (
this.props.footerContent
)}
</Modal.Footer>
<div
onKeyDown={this.onEnterKeyDown}
tabIndex={this.props.tabIndex || 0}
className='GenericModal__wrapper-enter-key-press-catcher'
>
<Modal.Header closeButton={true}>
{this.props.compassDesign && (
<>
{headerText}
{this.props.headerInput}
</>
)}
</div>
</FocusTrap>
</Modal.Header>
<Modal.Body>
{this.props.compassDesign ? (
this.props.errorText && (
<div className='genericModalError'>
<i className='icon icon-alert-outline'/>
<span>{this.props.errorText}</span>
</div>
)
) : (
headerText
)}
<div className={classNames('GenericModal__body', {padding: this.props.bodyPadding})}>
{this.props.children}
</div>
</Modal.Body>
{(cancelButton || confirmButton || this.props.footerContent) && (
<Modal.Footer className={classNames({divider: this.props.footerDivider})}>
{(cancelButton || confirmButton) ? (
<>
{cancelButton}
{confirmButton}
</>
) : (
this.props.footerContent
)}
</Modal.Footer>
)}
</div>
</Modal>
);
}