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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
644381b35e
Коммит
ba4dc1a91c
@@ -24,7 +24,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
|
|||||||
}
|
}
|
||||||
show={true}
|
show={true}
|
||||||
/>
|
/>
|
||||||
<Memo(ChannelMoveToSubMenu)
|
<Memo(ChannelMoveToSubMenuOld)
|
||||||
channel={
|
channel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -816,7 +816,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
|
|||||||
}
|
}
|
||||||
show={true}
|
show={true}
|
||||||
/>
|
/>
|
||||||
<Memo(ChannelMoveToSubMenu)
|
<Memo(ChannelMoveToSubMenuOld)
|
||||||
channel={
|
channel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type Props = {
|
|||||||
inHeaderDropdown?: boolean;
|
inHeaderDropdown?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChannelMoveToSubMenu = (props: Props) => {
|
const ChannelMoveToSubMenuOld = (props: Props) => {
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
const dispatch = useDispatch<DispatchFunc>();
|
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'}
|
confirmButtonClassName={'delete'}
|
||||||
enforceFocus={false}
|
|
||||||
>
|
>
|
||||||
<span className='delete-category__helpText'>
|
<span className='delete-category__helpText'>
|
||||||
<FormattedMarkdownMessage
|
<FormattedMarkdownMessage
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ exports[`components/delete_post_modal should match snapshot for delete_post_moda
|
|||||||
bsClass="modal"
|
bsClass="modal"
|
||||||
dialogClassName="a11y__modal"
|
dialogClassName="a11y__modal"
|
||||||
dialogComponentClass={[Function]}
|
dialogComponentClass={[Function]}
|
||||||
enforceFocus={false}
|
enforceFocus={true}
|
||||||
id="deletePostModal"
|
id="deletePostModal"
|
||||||
keyboard={true}
|
keyboard={true}
|
||||||
manager={
|
manager={
|
||||||
@@ -114,7 +114,7 @@ exports[`components/delete_post_modal should match snapshot for delete_post_moda
|
|||||||
bsClass="modal"
|
bsClass="modal"
|
||||||
dialogClassName="a11y__modal"
|
dialogClassName="a11y__modal"
|
||||||
dialogComponentClass={[Function]}
|
dialogComponentClass={[Function]}
|
||||||
enforceFocus={false}
|
enforceFocus={true}
|
||||||
id="deletePostModal"
|
id="deletePostModal"
|
||||||
keyboard={true}
|
keyboard={true}
|
||||||
manager={
|
manager={
|
||||||
@@ -228,7 +228,7 @@ exports[`components/delete_post_modal should match snapshot for post with 1 comm
|
|||||||
bsClass="modal"
|
bsClass="modal"
|
||||||
dialogClassName="a11y__modal"
|
dialogClassName="a11y__modal"
|
||||||
dialogComponentClass={[Function]}
|
dialogComponentClass={[Function]}
|
||||||
enforceFocus={false}
|
enforceFocus={true}
|
||||||
id="deletePostModal"
|
id="deletePostModal"
|
||||||
keyboard={true}
|
keyboard={true}
|
||||||
manager={
|
manager={
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ export default class DeletePostModal extends React.PureComponent<Props, State> {
|
|||||||
onEntered={this.handleEntered}
|
onEntered={this.handleEntered}
|
||||||
onHide={this.onHide}
|
onHide={this.onHide}
|
||||||
onExited={this.props.onExited}
|
onExited={this.props.onExited}
|
||||||
enforceFocus={false}
|
|
||||||
id='deletePostModal'
|
id='deletePostModal'
|
||||||
role='dialog'
|
role='dialog'
|
||||||
aria-labelledby='deletePostModalLabel'
|
aria-labelledby='deletePostModalLabel'
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
|
|||||||
menu={
|
menu={
|
||||||
Object {
|
Object {
|
||||||
"aria-label": "Post extra options",
|
"aria-label": "Post extra options",
|
||||||
"closeMenuManually": false,
|
|
||||||
"id": "CENTER_dropdown_post_id_1",
|
"id": "CENTER_dropdown_post_id_1",
|
||||||
"onKeyDown": [Function],
|
"onKeyDown": [Function],
|
||||||
"onToggle": [Function],
|
"onToggle": [Function],
|
||||||
@@ -236,7 +235,7 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<PostReminderSubmenu
|
<Memo(PostReminderSubmenu)
|
||||||
isMilitaryTime={false}
|
isMilitaryTime={false}
|
||||||
post={
|
post={
|
||||||
Object {
|
Object {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {GlobalState} from 'types/store';
|
|||||||
import {DeepPartial} from '@mattermost/types/utilities';
|
import {DeepPartial} from '@mattermost/types/utilities';
|
||||||
import {PostType} from '@mattermost/types/posts';
|
import {PostType} from '@mattermost/types/posts';
|
||||||
|
|
||||||
import * as dotUtils from './utils';
|
|
||||||
jest.mock('./utils');
|
jest.mock('./utils');
|
||||||
|
|
||||||
import DotMenu, {DotMenuClass} from './dot_menu';
|
import DotMenu, {DotMenuClass} from './dot_menu';
|
||||||
@@ -286,39 +285,5 @@ describe('components/dot_menu/DotMenu', () => {
|
|||||||
expect(menuItem).toBeVisible();
|
expect(menuItem).toBeVisible();
|
||||||
expect(menuItem).toHaveTextContent(text);
|
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 {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import {UserThread} from '@mattermost/types/threads';
|
||||||
|
import {Post} from '@mattermost/types/posts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ArrowRightBoldOutlineIcon,
|
ArrowRightBoldOutlineIcon,
|
||||||
BookmarkIcon,
|
BookmarkIcon,
|
||||||
@@ -25,24 +28,21 @@ import {
|
|||||||
|
|
||||||
import Permissions from 'mattermost-redux/constants/permissions';
|
import Permissions from 'mattermost-redux/constants/permissions';
|
||||||
|
|
||||||
|
import {ModalData} from 'types/actions';
|
||||||
import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants';
|
import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants';
|
||||||
import DeletePostModal from 'components/delete_post_modal';
|
|
||||||
import DelayedAction from 'utils/delayed_action';
|
import DelayedAction from 'utils/delayed_action';
|
||||||
import * as Keyboard from 'utils/keyboard';
|
import * as Keyboard from 'utils/keyboard';
|
||||||
import * as PostUtils from 'utils/post_utils';
|
import * as PostUtils from 'utils/post_utils';
|
||||||
import * as Menu from 'components/menu';
|
|
||||||
import * as Utils from 'utils/utils';
|
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 ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||||
import {Post} from '@mattermost/types/posts';
|
import DeletePostModal from 'components/delete_post_modal';
|
||||||
import ForwardPostModal from '../forward_post_modal';
|
import ForwardPostModal from 'components/forward_post_modal';
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
|
||||||
import {ChangeEvent, trackDotMenuEvent} from './utils';
|
import {ChangeEvent, trackDotMenuEvent} from './utils';
|
||||||
|
import PostReminderSubMenu from './post_reminder_submenu';
|
||||||
import './dot_menu.scss';
|
import './dot_menu.scss';
|
||||||
import {PostReminderSubmenu} from './post_reminder_submenu';
|
|
||||||
|
|
||||||
type ShortcutKeyProps = {
|
type ShortcutKeyProps = {
|
||||||
shortcutKey: string;
|
shortcutKey: string;
|
||||||
@@ -115,12 +115,6 @@ type Props = {
|
|||||||
* Function to set the thread as followed/unfollowed
|
* Function to set the thread as followed/unfollowed
|
||||||
*/
|
*/
|
||||||
setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void;
|
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
|
}; // TechDebt: Made non-mandatory while converting to typescript
|
||||||
|
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
@@ -134,7 +128,6 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
closeMenuManually: boolean;
|
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
canDelete: boolean;
|
canDelete: boolean;
|
||||||
}
|
}
|
||||||
@@ -146,7 +139,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
location: Locations.CENTER,
|
location: Locations.CENTER,
|
||||||
};
|
};
|
||||||
private editDisableAction: DelayedAction;
|
private editDisableAction: DelayedAction;
|
||||||
private buttonRef: React.RefObject<HTMLButtonElement>;
|
|
||||||
private canPostBeForwarded: boolean;
|
private canPostBeForwarded: boolean;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
@@ -155,13 +147,10 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
this.editDisableAction = new DelayedAction(this.handleEditDisable);
|
this.editDisableAction = new DelayedAction(this.handleEditDisable);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
closeMenuManually: false,
|
|
||||||
canEdit: props.canEdit && !props.isReadOnly,
|
canEdit: props.canEdit && !props.isReadOnly,
|
||||||
canDelete: props.canDelete && !props.isReadOnly,
|
canDelete: props.canDelete && !props.isReadOnly,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.buttonRef = React.createRef<HTMLButtonElement>();
|
|
||||||
|
|
||||||
this.canPostBeForwarded = false;
|
this.canPostBeForwarded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +162,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
disableCanEditPostByTime(): void {
|
disableCanEditPostByTime() {
|
||||||
const {post, isLicensed} = this.props;
|
const {post, isLicensed} = this.props;
|
||||||
const {canEdit} = this.state;
|
const {canEdit} = this.state;
|
||||||
|
|
||||||
@@ -190,32 +179,29 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount() {
|
||||||
this.disableCanEditPostByTime();
|
this.disableCanEditPostByTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount() {
|
||||||
this.editDisableAction.cancel();
|
this.editDisableAction.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditDisable = (): void => {
|
handleEditDisable = () => {
|
||||||
this.setState({canEdit: false});
|
this.setState({canEdit: false});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFlagMenuItemActivated = (e: ChangeEvent): void => {
|
handleFlagMenuItemActivated = (e: ChangeEvent) => {
|
||||||
if (this.props.isFlagged) {
|
if (this.props.isFlagged) {
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.UNSAVE);
|
|
||||||
this.props.actions.unflagPost(this.props.post.id);
|
this.props.actions.unflagPost(this.props.post.id);
|
||||||
|
trackDotMenuEvent(e, TELEMETRY_LABELS.UNSAVE);
|
||||||
} else {
|
} else {
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.SAVE);
|
|
||||||
this.props.actions.flagPost(this.props.post.id);
|
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 = () => {
|
||||||
handleAddReactionMenuItemActivated = (e: ChangeEvent): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// to be safe, make sure the handler function has been defined
|
// to be safe, make sure the handler function has been defined
|
||||||
if (this.props.handleAddReactionClick) {
|
if (this.props.handleAddReactionClick) {
|
||||||
this.props.handleAddReactionClick();
|
this.props.handleAddReactionClick();
|
||||||
@@ -223,35 +209,31 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
copyLink = (e: ChangeEvent) => {
|
copyLink = (e: ChangeEvent) => {
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_LINK);
|
|
||||||
Utils.copyToClipboard(`${this.props.teamUrl}/pl/${this.props.post.id}`);
|
Utils.copyToClipboard(`${this.props.teamUrl}/pl/${this.props.post.id}`);
|
||||||
|
trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_LINK);
|
||||||
};
|
};
|
||||||
|
|
||||||
copyText = (e: ChangeEvent) => {
|
copyText = (e: ChangeEvent) => {
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_TEXT);
|
|
||||||
Utils.copyToClipboard(this.props.post.message);
|
Utils.copyToClipboard(this.props.post.message);
|
||||||
|
trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_TEXT);
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePinMenuItemActivated = (e: ChangeEvent): void => {
|
handlePinMenuItemActivated = (e: ChangeEvent): void => {
|
||||||
if (this.props.post.is_pinned) {
|
if (this.props.post.is_pinned) {
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.UNPIN);
|
|
||||||
this.props.actions.unpinPost(this.props.post.id);
|
this.props.actions.unpinPost(this.props.post.id);
|
||||||
|
trackDotMenuEvent(e, TELEMETRY_LABELS.UNPIN);
|
||||||
} else {
|
} else {
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.PIN);
|
|
||||||
this.props.actions.pinPost(this.props.post.id);
|
this.props.actions.pinPost(this.props.post.id);
|
||||||
|
trackDotMenuEvent(e, TELEMETRY_LABELS.PIN);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMarkPostAsUnread = (e: ChangeEvent): void => {
|
handleMarkPostAsUnread = (e: ChangeEvent): void => {
|
||||||
e.preventDefault();
|
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.UNREAD);
|
|
||||||
this.props.actions.markPostAsUnread(this.props.post, this.props.location);
|
this.props.actions.markPostAsUnread(this.props.post, this.props.location);
|
||||||
|
trackDotMenuEvent(e, TELEMETRY_LABELS.UNREAD);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDeleteMenuItemActivated = (e: ChangeEvent): void => {
|
handleDeleteMenuItemActivated = (e: ChangeEvent): void => {
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.DELETE);
|
|
||||||
const deletePostModalData = {
|
const deletePostModalData = {
|
||||||
modalId: ModalIdentifiers.DELETE_POST,
|
modalId: ModalIdentifiers.DELETE_POST,
|
||||||
dialogType: DeletePostModal,
|
dialogType: DeletePostModal,
|
||||||
@@ -262,6 +244,8 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.props.actions.openModal(deletePostModalData);
|
this.props.actions.openModal(deletePostModalData);
|
||||||
|
|
||||||
|
trackDotMenuEvent(e, TELEMETRY_LABELS.DELETE);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleForwardMenuItemActivated = (e: ChangeEvent): void => {
|
handleForwardMenuItemActivated = (e: ChangeEvent): void => {
|
||||||
@@ -271,8 +255,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.FORWARD);
|
trackDotMenuEvent(e, TELEMETRY_LABELS.FORWARD);
|
||||||
const forwardPostModalData = {
|
const forwardPostModalData = {
|
||||||
modalId: ModalIdentifiers.FORWARD_POST_MODAL,
|
modalId: ModalIdentifiers.FORWARD_POST_MODAL,
|
||||||
@@ -286,7 +268,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleEditMenuItemActivated = (e: ChangeEvent): void => {
|
handleEditMenuItemActivated = (e: ChangeEvent): void => {
|
||||||
trackDotMenuEvent(e, TELEMETRY_LABELS.EDIT);
|
|
||||||
this.props.handleDropdownOpened?.(false);
|
this.props.handleDropdownOpened?.(false);
|
||||||
this.props.actions.setEditingPost(
|
this.props.actions.setEditingPost(
|
||||||
this.props.post.id,
|
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.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,
|
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) => {
|
handleSetThreadFollow = (e: ChangeEvent) => {
|
||||||
@@ -325,88 +307,79 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
this.props.handleCommentClick?.(e);
|
this.props.handleCommentClick?.(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
isKeyboardEvent = (e: React.KeyboardEvent): any => {
|
handleMenuKeydown = (event: React.KeyboardEvent<HTMLDivElement>, forceCloseMenu?: (() => void)) => {
|
||||||
return (e).getModifierState !== undefined;
|
event.preventDefault();
|
||||||
};
|
|
||||||
|
|
||||||
onShortcutKeyDown = (e: React.KeyboardEvent): void => {
|
if (!forceCloseMenu) {
|
||||||
e.preventDefault();
|
|
||||||
if (!this.isKeyboardEvent(e)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isShiftKeyPressed = e.shiftKey;
|
const isShiftKeyPressed = event.shiftKey;
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.R):
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.R):
|
||||||
this.handleCommentClick(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.handleCommentClick(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// edit post
|
// edit post
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.E):
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.E):
|
||||||
this.handleEditMenuItemActivated(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.handleEditMenuItemActivated(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// follow thread
|
// follow thread
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && !isShiftKeyPressed:
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.F) && !isShiftKeyPressed:
|
||||||
this.handleSetThreadFollow(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.handleSetThreadFollow(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// forward post
|
// forward post
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && isShiftKeyPressed:
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.F) && isShiftKeyPressed:
|
||||||
this.handleForwardMenuItemActivated(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.handleForwardMenuItemActivated(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// copy link
|
// copy link
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.K):
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.K):
|
||||||
this.copyLink(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.copyLink(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// copy text
|
// copy text
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.C):
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.C):
|
||||||
this.copyText(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.copyText(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// delete post
|
// delete post
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.DELETE):
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.DELETE):
|
||||||
this.handleDeleteMenuItemActivated(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.handleDeleteMenuItemActivated(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// pin / unpin
|
// pin / unpin
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.P):
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.P):
|
||||||
this.handlePinMenuItemActivated(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.handlePinMenuItemActivated(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// save / unsave
|
// save / unsave
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.S):
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.S):
|
||||||
this.handleFlagMenuItemActivated(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.handleFlagMenuItemActivated(event);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// mark as unread
|
// mark as unread
|
||||||
case Keyboard.isKeyPressed(e, Constants.KeyCodes.U):
|
case Keyboard.isKeyPressed(event, Constants.KeyCodes.U):
|
||||||
this.handleMarkPostAsUnread(e);
|
forceCloseMenu();
|
||||||
this.handleDropdownOpened(false);
|
this.handleMarkPostAsUnread(event);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDropdownOpened = (open: boolean) => {
|
|
||||||
this.props.handleDropdownOpened?.(open);
|
|
||||||
this.setState({closeMenuManually: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMenuToggle = (open: boolean) => {
|
handleMenuToggle = (open: boolean) => {
|
||||||
this.props.handleDropdownOpened?.(open);
|
this.props.handleDropdownOpened?.(open);
|
||||||
this.setState({closeMenuManually: false});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
@@ -414,11 +387,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
const isFollowingThread = this.props.isFollowingThread ?? this.props.isMentionedInRootPost;
|
const isFollowingThread = this.props.isFollowingThread ?? this.props.isMentionedInRootPost;
|
||||||
const isMobile = this.props.isMobileView;
|
const isMobile = this.props.isMobileView;
|
||||||
const isSystemMessage = PostUtils.isSystemMessage(this.props.post);
|
const isSystemMessage = PostUtils.isSystemMessage(this.props.post);
|
||||||
const deleteShortcutText = (
|
|
||||||
<span>
|
|
||||||
{'delete'}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
this.canPostBeForwarded = !(isSystemMessage);
|
this.canPostBeForwarded = !(isSystemMessage);
|
||||||
|
|
||||||
@@ -482,6 +450,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
defaultMessage='Pin'
|
defaultMessage='Pin'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const unPinPost = (
|
const unPinPost = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='post_info.unpin'
|
id='post_info.unpin'
|
||||||
@@ -503,10 +472,9 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
menu={{
|
menu={{
|
||||||
id: `${this.props.location}_dropdown_${this.props.post.id}`,
|
id: `${this.props.location}_dropdown_${this.props.post.id}`,
|
||||||
'aria-label': formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'}),
|
'aria-label': formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'}),
|
||||||
onKeyDown: this.onShortcutKeyDown,
|
onKeyDown: this.handleMenuKeydown,
|
||||||
width: '264px',
|
width: '264px',
|
||||||
onToggle: this.handleMenuToggle,
|
onToggle: this.handleMenuToggle,
|
||||||
closeMenuManually: this.state.closeMenuManually,
|
|
||||||
}}
|
}}
|
||||||
menuButtonTooltip={{
|
menuButtonTooltip={{
|
||||||
id: `PostDotMenu-ButtonTooltip-${this.props.post.id}`,
|
id: `PostDotMenu-ButtonTooltip-${this.props.post.id}`,
|
||||||
@@ -563,20 +531,23 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
{Boolean(
|
{Boolean(
|
||||||
!isSystemMessage &&
|
!isSystemMessage &&
|
||||||
this.props.isCollapsedThreadsEnabled &&
|
this.props.isCollapsedThreadsEnabled &&
|
||||||
(
|
(this.props.location === Locations.CENTER ||
|
||||||
this.props.location === Locations.CENTER ||
|
|
||||||
this.props.location === Locations.RHS_ROOT ||
|
this.props.location === Locations.RHS_ROOT ||
|
||||||
this.props.location === Locations.RHS_COMMENT
|
this.props.location === Locations.RHS_COMMENT)) &&
|
||||||
),
|
<Menu.Item
|
||||||
) &&
|
id={`follow_post_thread_${this.props.post.id}`}
|
||||||
<Menu.Item
|
data-testid={`follow_post_thread_${this.props.post.id}`}
|
||||||
id={`follow_post_thread_${this.props.post.id}`}
|
trailingElements={<ShortcutKey shortcutKey='F'/>}
|
||||||
data-testid={`follow_post_thread_${this.props.post.id}`}
|
labels={followPostLabel()}
|
||||||
trailingElements={<ShortcutKey shortcutKey='F'/>}
|
leadingElement={
|
||||||
labels={followPostLabel()}
|
isFollowingThread ? (
|
||||||
leadingElement={isFollowingThread ? <MessageMinusOutlineIcon size={18}/> : <MessageCheckOutlineIcon size={18}/>}
|
<MessageMinusOutlineIcon size={18}/>
|
||||||
onClick={this.handleSetThreadFollow}
|
) : (
|
||||||
/>
|
<MessageCheckOutlineIcon size={18}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={this.handleSetThreadFollow}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
{Boolean(!isSystemMessage && !this.props.channelIsArchived && this.props.location !== Locations.SEARCH) &&
|
{Boolean(!isSystemMessage && !this.props.channelIsArchived && this.props.location !== Locations.SEARCH) &&
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -594,7 +565,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{!isSystemMessage &&
|
{!isSystemMessage &&
|
||||||
<PostReminderSubmenu
|
<PostReminderSubMenu
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
post={this.props.post}
|
post={this.props.post}
|
||||||
isMilitaryTime={this.props.isMilitaryTime}
|
isMilitaryTime={this.props.isMilitaryTime}
|
||||||
@@ -670,7 +641,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
|||||||
id={`delete_post_${this.props.post.id}`}
|
id={`delete_post_${this.props.post.id}`}
|
||||||
data-testid={`delete_post_${this.props.post.id}`}
|
data-testid={`delete_post_${this.props.post.id}`}
|
||||||
leadingElement={<TrashCanOutlineIcon size={18}/>}
|
leadingElement={<TrashCanOutlineIcon size={18}/>}
|
||||||
trailingElements={deleteShortcutText}
|
trailingElements={<span>{'delete'}</span>}
|
||||||
labels={
|
labels={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='post_info.del'
|
id='post_info.del'
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import {allAtMentions} from 'utils/text_formatting';
|
|||||||
import {matchUserMentionTriggersWithMessageMentions} from 'utils/post_utils';
|
import {matchUserMentionTriggersWithMessageMentions} from 'utils/post_utils';
|
||||||
|
|
||||||
import {Post} from '@mattermost/types/posts';
|
import {Post} from '@mattermost/types/posts';
|
||||||
import {setGlobalItem} from '../../actions/storage';
|
|
||||||
|
|
||||||
import DotMenu from './dot_menu';
|
import DotMenu from './dot_menu';
|
||||||
|
|
||||||
@@ -141,7 +140,6 @@ type Actions = {
|
|||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
markPostAsUnread: (post: Post) => void;
|
markPostAsUnread: (post: Post) => void;
|
||||||
setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void;
|
setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void;
|
||||||
setGlobalItem: (name: string, value: any) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||||
@@ -155,7 +153,6 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
|||||||
openModal,
|
openModal,
|
||||||
markPostAsUnread,
|
markPostAsUnread,
|
||||||
setThreadFollow,
|
setThreadFollow,
|
||||||
setGlobalItem,
|
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React, {memo} from 'react';
|
||||||
import {useDispatch} from 'react-redux';
|
import {useDispatch} from 'react-redux';
|
||||||
|
|
||||||
import {FormattedMessage, FormattedDate, FormattedTime, useIntl} from 'react-intl';
|
import {FormattedMessage, FormattedDate, FormattedTime, useIntl} from 'react-intl';
|
||||||
@@ -14,7 +14,6 @@ import {ModalIdentifiers} from 'utils/constants';
|
|||||||
import {toUTCUnix} from 'utils/datetime';
|
import {toUTCUnix} from 'utils/datetime';
|
||||||
import PostReminderCustomTimePicker from 'components/post_reminder_custom_time_picker_modal';
|
import PostReminderCustomTimePicker from 'components/post_reminder_custom_time_picker_modal';
|
||||||
import {addPostReminder} from 'mattermost-redux/actions/posts';
|
import {addPostReminder} from 'mattermost-redux/actions/posts';
|
||||||
import {t} from 'utils/i18n';
|
|
||||||
|
|
||||||
import {Post} from '@mattermost/types/posts';
|
import {Post} from '@mattermost/types/posts';
|
||||||
|
|
||||||
@@ -25,93 +24,122 @@ type Props = {
|
|||||||
timezone?: string;
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const postReminderTimes = [
|
const PostReminders = {
|
||||||
{id: 'thirty_minutes', label: t('post_info.post_reminder.sub_menu.thirty_minutes'), labelDefault: '30 mins'},
|
THIRTY_MINUTES: 'thirty_minutes',
|
||||||
{id: 'one_hour', label: t('post_info.post_reminder.sub_menu.one_hour'), labelDefault: '1 hour'},
|
ONE_HOUR: 'one_hour',
|
||||||
{id: 'two_hours', label: t('post_info.post_reminder.sub_menu.two_hours'), labelDefault: '2 hours'},
|
TWO_HOURS: 'two_hours',
|
||||||
{id: 'tomorrow', label: t('post_info.post_reminder.sub_menu.tomorrow'), labelDefault: 'Tomorrow'},
|
TOMORROW: 'tomorrow',
|
||||||
{id: 'custom', label: t('post_info.post_reminder.sub_menu.custom'), labelDefault: 'Custom'},
|
CUSTOM: 'custom',
|
||||||
];
|
} as const;
|
||||||
|
|
||||||
export function PostReminderSubmenu(props: Props) {
|
function PostReminderSubmenu(props: Props) {
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const setPostReminder = (id: string): void => {
|
function handlePostReminderMenuClick(id: string) {
|
||||||
const currentDate = getCurrentMomentForTimezone(props.timezone);
|
if (id === PostReminders.CUSTOM) {
|
||||||
let endTime = currentDate;
|
const postReminderCustomTimePicker = {
|
||||||
switch (id) {
|
modalId: ModalIdentifiers.POST_REMINDER_CUSTOM_TIME_PICKER,
|
||||||
case 'thirty_minutes':
|
dialogType: PostReminderCustomTimePicker,
|
||||||
// add 30 minutes in current time
|
dialogProps: {
|
||||||
endTime = currentDate.add(30, 'minutes');
|
postId: props.post.id,
|
||||||
break;
|
},
|
||||||
case 'one_hour':
|
};
|
||||||
// add 1 hour in current time
|
|
||||||
endTime = currentDate.add(1, 'hour');
|
dispatch(openModal(postReminderCustomTimePicker));
|
||||||
break;
|
} else {
|
||||||
case 'two_hours':
|
const currentDate = getCurrentMomentForTimezone(props.timezone);
|
||||||
// add 2 hours in current time
|
|
||||||
endTime = currentDate.add(2, 'hours');
|
let endTime = currentDate;
|
||||||
break;
|
if (id === PostReminders.THIRTY_MINUTES) {
|
||||||
case 'tomorrow':
|
// add 30 minutes in current time
|
||||||
// add one day in current date
|
endTime = currentDate.add(30, 'minutes');
|
||||||
endTime = currentDate.add(1, 'day');
|
} else if (id === PostReminders.ONE_HOUR) {
|
||||||
break;
|
// 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 => {
|
trailingElements = (
|
||||||
const postReminderCustomTimePicker = {
|
<span className={`postReminder-${postReminder}_timestamp`}>
|
||||||
modalId: ModalIdentifiers.POST_REMINDER_CUSTOM_TIME_PICKER,
|
<FormattedDate
|
||||||
dialogType: PostReminderCustomTimePicker,
|
value={tomorrow}
|
||||||
dialogProps: {
|
weekday='short'
|
||||||
postId: props.post.id,
|
timeZone={props.timezone}
|
||||||
},
|
/>
|
||||||
};
|
{', '}
|
||||||
dispatch(openModal(postReminderCustomTimePicker));
|
<FormattedTime
|
||||||
};
|
value={tomorrow}
|
||||||
|
timeStyle='short'
|
||||||
const postReminderSubMenuItems =
|
hour12={!props.isMilitaryTime}
|
||||||
postReminderTimes.map(({id, label, labelDefault}) => {
|
timeZone={props.timezone}
|
||||||
const labels = (
|
/>
|
||||||
<FormattedMessage
|
</span>
|
||||||
id={label}
|
|
||||||
defaultMessage={labelDefault}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let trailing: React.ReactNode;
|
return (
|
||||||
if (id === 'tomorrow') {
|
<Menu.Item
|
||||||
const tomorrow = getCurrentMomentForTimezone(props.timezone).add(1, 'day').toDate();
|
id={`remind_post_options_${postReminder}`}
|
||||||
trailing = (
|
key={`remind_post_options_${postReminder}`}
|
||||||
<span className={`postReminder-${id}_timestamp`}>
|
labels={labels}
|
||||||
<FormattedDate
|
trailingElements={trailingElements}
|
||||||
value={tomorrow}
|
onClick={() => handlePostReminderMenuClick(postReminder)}
|
||||||
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 (
|
return (
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
@@ -136,3 +164,5 @@ export function PostReminderSubmenu(props: Props) {
|
|||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(PostReminderSubmenu);
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ export default class EditCategoryModal extends React.PureComponent<Props, State>
|
|||||||
handleConfirm={this.handleConfirm}
|
handleConfirm={this.handleConfirm}
|
||||||
handleCancel={this.handleCancel}
|
handleCancel={this.handleCancel}
|
||||||
isConfirmDisabled={this.isConfirmDisabled()}
|
isConfirmDisabled={this.isConfirmDisabled()}
|
||||||
enforceFocus={false}
|
|
||||||
>
|
>
|
||||||
<QuickInput
|
<QuickInput
|
||||||
inputComponent={MaxLengthInput}
|
inputComponent={MaxLengthInput}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
SyntheticEvent,
|
SyntheticEvent,
|
||||||
KeyboardEventHandler,
|
useMemo,
|
||||||
|
useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
import MuiMenuList from '@mui/material/MenuList';
|
import MuiMenuList from '@mui/material/MenuList';
|
||||||
@@ -28,10 +29,11 @@ import OverlayTrigger from 'components/overlay_trigger';
|
|||||||
import {GenericModal} from '@mattermost/components';
|
import {GenericModal} from '@mattermost/components';
|
||||||
|
|
||||||
import {MuiMenuStyled} from './menu_styled';
|
import {MuiMenuStyled} from './menu_styled';
|
||||||
|
import {MenuContext} from './menu_context';
|
||||||
|
|
||||||
const OVERLAY_TIME_DELAY = 500;
|
const OVERLAY_TIME_DELAY = 500;
|
||||||
const MENU_OPEN_ANIMATION_DURATION = 150;
|
const MENU_OPEN_ANIMATION_DURATION = 150;
|
||||||
const MENU_CLOSE_ANIMATION_DURATION = 100;
|
export const MENU_CLOSE_ANIMATION_DURATION = 100;
|
||||||
|
|
||||||
type MenuButtonProps = {
|
type MenuButtonProps = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -56,8 +58,7 @@ type MenuProps = {
|
|||||||
* @warning Make the styling of your components such a way that they dont need this handler
|
* @warning Make the styling of your components such a way that they dont need this handler
|
||||||
*/
|
*/
|
||||||
onToggle?: (isOpen: boolean) => void;
|
onToggle?: (isOpen: boolean) => void;
|
||||||
closeMenuManually?: boolean;
|
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>, forceCloseMenu?: () => void) => void;
|
||||||
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
|
|
||||||
width?: string;
|
width?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,30 +90,30 @@ export function Menu(props: Props) {
|
|||||||
const [disableAutoFocusItem, setDisableAutoFocusItem] = useState(false);
|
const [disableAutoFocusItem, setDisableAutoFocusItem] = useState(false);
|
||||||
const isMenuOpen = Boolean(anchorElement);
|
const isMenuOpen = Boolean(anchorElement);
|
||||||
|
|
||||||
|
// Callback funtion handler called when menu is closed by escapeKeyDown, backdropClick or tabKeyDown
|
||||||
function handleMenuClose(event: MouseEvent<HTMLDivElement>) {
|
function handleMenuClose(event: MouseEvent<HTMLDivElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setAnchorElement(null);
|
setAnchorElement(null);
|
||||||
setDisableAutoFocusItem(false);
|
setDisableAutoFocusItem(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle function injected into menu items to close the menu
|
||||||
|
const closeMenu = useCallback(() => {
|
||||||
|
setAnchorElement(null);
|
||||||
|
setDisableAutoFocusItem(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
function handleMenuModalClose(modalId: MenuProps['id']) {
|
function handleMenuModalClose(modalId: MenuProps['id']) {
|
||||||
dispatch(closeModal(modalId));
|
dispatch(closeModal(modalId));
|
||||||
setAnchorElement(null);
|
setAnchorElement(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMenuClick() {
|
// Stop sythetic events from bubbling up to the parent
|
||||||
setAnchorElement(null);
|
// @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>) {
|
function handleMenuKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
||||||
if (isKeyPressed(event, Constants.KeyCodes.ENTER) || isKeyPressed(event, Constants.KeyCodes.SPACE)) {
|
if (isKeyPressed(event, Constants.KeyCodes.ENTER) || isKeyPressed(event, Constants.KeyCodes.SPACE)) {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
@@ -125,7 +126,13 @@ export function Menu(props: Props) {
|
|||||||
setAnchorElement(null);
|
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>) {
|
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() {
|
function handleMenuButtonMouseDown() {
|
||||||
// This is needed to prevent focus-visible being set on clicking menuitems with mouse
|
|
||||||
setDisableAutoFocusItem(true);
|
setDisableAutoFocusItem(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We construct the menu button so we can set onClick correctly here to support both web and mobile view
|
||||||
function renderMenuButton() {
|
function renderMenuButton() {
|
||||||
// We construct the menu button so we can set onClick correctly here to support both web and mobile view
|
|
||||||
const triggerElement = (
|
const triggerElement = (
|
||||||
<button
|
<button
|
||||||
id={props.menuButton.id}
|
id={props.menuButton.id}
|
||||||
@@ -204,6 +211,13 @@ export function Menu(props: Props) {
|
|||||||
}
|
}
|
||||||
}, [isMenuOpen]);
|
}, [isMenuOpen]);
|
||||||
|
|
||||||
|
const providerValue = useMemo(() => {
|
||||||
|
return {
|
||||||
|
close: closeMenu,
|
||||||
|
isOpen: Boolean(anchorElement),
|
||||||
|
};
|
||||||
|
}, [anchorElement, closeMenu]);
|
||||||
|
|
||||||
if (isMobileView) {
|
if (isMobileView) {
|
||||||
// In mobile view, the menu is rendered as a modal
|
// In mobile view, the menu is rendered as a modal
|
||||||
return renderMenuButton();
|
return renderMenuButton();
|
||||||
@@ -219,6 +233,7 @@ export function Menu(props: Props) {
|
|||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
onKeyDown={handleMenuKeyDown}
|
onKeyDown={handleMenuKeyDown}
|
||||||
className={A11yClassNames.POPUP}
|
className={A11yClassNames.POPUP}
|
||||||
|
width={props.menu.width}
|
||||||
disableAutoFocusItem={disableAutoFocusItem} // This is not anti-pattern, see handleMenuButtonMouseDown
|
disableAutoFocusItem={disableAutoFocusItem} // This is not anti-pattern, see handleMenuButtonMouseDown
|
||||||
MenuListProps={{
|
MenuListProps={{
|
||||||
id: props.menu.id,
|
id: props.menu.id,
|
||||||
@@ -232,9 +247,10 @@ export function Menu(props: Props) {
|
|||||||
exit: MENU_CLOSE_ANIMATION_DURATION,
|
exit: MENU_CLOSE_ANIMATION_DURATION,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
width={props.menu.width}
|
|
||||||
>
|
>
|
||||||
{props.children}
|
<MenuContext.Provider value={providerValue}>
|
||||||
|
{props.children}
|
||||||
|
</MenuContext.Provider>
|
||||||
</MuiMenuStyled>
|
</MuiMenuStyled>
|
||||||
</CompassDesignProvider>
|
</CompassDesignProvider>
|
||||||
);
|
);
|
||||||
@@ -246,13 +262,13 @@ interface MenuModalProps {
|
|||||||
menuAriaLabel: MenuProps['aria-label'];
|
menuAriaLabel: MenuProps['aria-label'];
|
||||||
onModalClose: (modalId: MenuProps['id']) => void;
|
onModalClose: (modalId: MenuProps['id']) => void;
|
||||||
children: Props['children'];
|
children: Props['children'];
|
||||||
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
|
onKeyDown?: MenuProps['onKeyDown'];
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuModal(props: MenuModalProps) {
|
function MenuModal(props: MenuModalProps) {
|
||||||
const theme = useSelector(getTheme);
|
const theme = useSelector(getTheme);
|
||||||
|
|
||||||
function handleModalExited() {
|
function closeMenuModal() {
|
||||||
props.onModalClose(props.menuId);
|
props.onModalClose(props.menuId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,15 +278,16 @@ function MenuModal(props: MenuModalProps) {
|
|||||||
if (currentElement.contains(event.target as Node) && !currentElement.ariaHasPopup) {
|
if (currentElement.contains(event.target as Node) && !currentElement.ariaHasPopup) {
|
||||||
// We check for property ariaHasPopup because we don't want to close the menu
|
// 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.
|
// if the user clicks on a submenu item or menu item which open modal. And let submenu component handle the click.
|
||||||
handleModalExited();
|
closeMenuModal();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(event?: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeydown(event?: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (event && props.onKeyDown) {
|
if (event && props.onKeyDown) {
|
||||||
props.onKeyDown(event);
|
props.onKeyDown(event, closeMenuModal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +298,7 @@ function MenuModal(props: MenuModalProps) {
|
|||||||
className='menuModal'
|
className='menuModal'
|
||||||
backdrop={true}
|
backdrop={true}
|
||||||
ariaLabel={props.menuAriaLabel}
|
ariaLabel={props.menuAriaLabel}
|
||||||
onExited={handleModalExited}
|
onExited={closeMenuModal}
|
||||||
enforceFocus={false}
|
enforceFocus={false}
|
||||||
handleKeydown={handleKeydown}
|
handleKeydown={handleKeydown}
|
||||||
>
|
>
|
||||||
|
|||||||
20
webapp/channels/src/components/menu/menu_context.ts
Обычный файл
20
webapp/channels/src/components/menu/menu_context.ts
Обычный файл
@@ -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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 {styled} from '@mui/material/styles';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
import MuiMenuItem from '@mui/material/MenuItem';
|
import MuiMenuItem from '@mui/material/MenuItem';
|
||||||
import type {MenuItemProps as MuiMenuItemProps} 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 {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 {
|
export interface Props extends MuiMenuItemProps {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +81,7 @@ export interface Props extends MuiMenuItemProps {
|
|||||||
*/
|
*/
|
||||||
isDestructive?: boolean;
|
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.
|
* 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.
|
* 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
|
* @example
|
||||||
* <Menu.Container>
|
* <Menu.Container>
|
||||||
@@ -92,15 +110,68 @@ export function MenuItem(props: Props) {
|
|||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = 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 menuContext = useContext(MenuContext);
|
||||||
const hasSecondaryLabel = labels && labels.props && labels.props.children && Children.count(labels.props.children) === 2;
|
const subMenuContext = useContext(SubMenuContext);
|
||||||
|
|
||||||
|
const isMobileView = useSelector(getIsMobileView);
|
||||||
|
|
||||||
|
const onClickEventRef = useRef<MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>>();
|
||||||
|
|
||||||
function handleClick(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleClick(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
||||||
if (isCorrectKeyPressedOnMenuItem(event)) {
|
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 (
|
return (
|
||||||
<MenuItemStyled
|
<MenuItemStyled
|
||||||
disableRipple={true}
|
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
|
* @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>) {
|
function isCorrectKeyPressedOnMenuItem(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
||||||
if (event.type === 'keydown') {
|
if (event.type === EventTypes.KEY_DOWN) {
|
||||||
const keyboardEvent = event as KeyboardEvent<HTMLLIElement>;
|
const keyboardEvent = event as KeyboardEvent<HTMLLIElement>;
|
||||||
if (isKeyPressed(keyboardEvent, Constants.KeyCodes.ENTER) || isKeyPressed(keyboardEvent, Constants.KeyCodes.SPACE)) {
|
if (isKeyPressed(keyboardEvent, Constants.KeyCodes.ENTER) || isKeyPressed(keyboardEvent, Constants.KeyCodes.SPACE)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} else if (event.type === 'mousedown') {
|
} else if (event.type === EventTypes.MOUSE_DOWN) {
|
||||||
const mouseEvent = event as MouseEvent<HTMLLIElement>;
|
const mouseEvent = event as MouseEvent<HTMLLIElement>;
|
||||||
if (mouseEvent.button === 0) {
|
if (mouseEvent.button === 0) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {Divider} from '@mui/material';
|
|||||||
* <Menu.Container>
|
* <Menu.Container>
|
||||||
* <Menu.Item>
|
* <Menu.Item>
|
||||||
* <Menu.Separator />
|
* <Menu.Separator />
|
||||||
|
* </Menu.Container>
|
||||||
*/
|
*/
|
||||||
export function MenuItemSeparator() {
|
export function MenuItemSeparator() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import 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 {useDispatch, useSelector} from 'react-redux';
|
||||||
import MuiMenuList from '@mui/material/MenuList';
|
import MuiMenuList from '@mui/material/MenuList';
|
||||||
import {PopoverOrigin} from '@mui/material/Popover';
|
import {PopoverOrigin} from '@mui/material/Popover';
|
||||||
@@ -20,7 +28,8 @@ import CompassDesignProvider from 'components/compass_design_provider';
|
|||||||
import {GenericModal} from '@mattermost/components';
|
import {GenericModal} from '@mattermost/components';
|
||||||
|
|
||||||
import {MuiMenuStyled} from './menu_styled';
|
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';
|
import './sub_menu.scss';
|
||||||
|
|
||||||
@@ -39,41 +48,66 @@ interface Props {
|
|||||||
children: ReactNode;
|
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 [anchorElement, setAnchorElement] = useState<null | HTMLElement>(null);
|
||||||
const isSubMenuOpen = Boolean(anchorElement);
|
const isSubMenuOpen = Boolean(anchorElement);
|
||||||
|
|
||||||
const isMobileView = useSelector(getIsMobileView);
|
const isMobileView = useSelector(getIsMobileView);
|
||||||
|
|
||||||
const anyModalOpen = useSelector(isAnyModalOpen);
|
const anyModalOpen = useSelector(isAnyModalOpen);
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
function handleSubMenuOpen(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
useEffect(() => {
|
||||||
event.preventDefault();
|
if (anyModalOpen && !isMobileView) {
|
||||||
|
setAnchorElement(null);
|
||||||
if (isMobileView) {
|
|
||||||
dispatch(openModal<SubMenuModalProps>({
|
|
||||||
modalId: menuId,
|
|
||||||
dialogType: SubMenuModal,
|
|
||||||
dialogProps: {
|
|
||||||
menuId,
|
|
||||||
menuAriaLabel,
|
|
||||||
children,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setAnchorElement(event.currentTarget);
|
|
||||||
}
|
}
|
||||||
|
}, [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();
|
event.preventDefault();
|
||||||
setAnchorElement(null);
|
setAnchorElement(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This handleKeyDown is on the menu item which opens the submenu
|
function handleKeyDown(event: KeyboardEvent<HTMLLIElement>) {
|
||||||
function handleSubMenuParentItemKeyDown(event: KeyboardEvent<HTMLLIElement>) {
|
|
||||||
if (
|
if (
|
||||||
isKeyPressed(event, Constants.KeyCodes.ENTER) ||
|
isKeyPressed(event, Constants.KeyCodes.ENTER) ||
|
||||||
isKeyPressed(event, Constants.KeyCodes.SPACE) ||
|
isKeyPressed(event, Constants.KeyCodes.SPACE) ||
|
||||||
@@ -94,17 +128,17 @@ export function SubMenu({id, leadingElement, labels, trailingElements, isDestruc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
// This is used in MobileView to open the submenu in a modal
|
||||||
if (anyModalOpen && !isMobileView) {
|
function handleOnClick() {
|
||||||
setAnchorElement(null);
|
dispatch(openModal<SubMenuModalProps>({
|
||||||
}
|
modalId: menuId,
|
||||||
}, [anyModalOpen, isMobileView]);
|
dialogType: SubMenuModal,
|
||||||
|
dialogProps: {
|
||||||
const originOfAnchorAndTransform = useMemo(() => getOriginOfAnchorAndTransform(forceOpenOnLeft, anchorElement), [anchorElement]);
|
menuId,
|
||||||
|
menuAriaLabel,
|
||||||
const hasSubmenuItems = Boolean(children);
|
children,
|
||||||
if (!hasSubmenuItems) {
|
},
|
||||||
return null;
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const passedInTriggerButtonProps = {
|
const passedInTriggerButtonProps = {
|
||||||
@@ -117,20 +151,20 @@ export function SubMenu({id, leadingElement, labels, trailingElements, isDestruc
|
|||||||
labels,
|
labels,
|
||||||
trailingElements,
|
trailingElements,
|
||||||
isDestructive,
|
isDestructive,
|
||||||
onClick: handleSubMenuOpen,
|
onClick: isMobileView ? handleOnClick : undefined, // OnClicks on parent menuItem of subMenu is only needed in mobile view
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isMobileView) {
|
if (isMobileView) {
|
||||||
return (<ParentMenuItem {...passedInTriggerButtonProps}/>);
|
return (<MenuItem {...passedInTriggerButtonProps}/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParentMenuItem
|
<MenuItem
|
||||||
{...rest} // pass through other props which might be coming in from the material-ui
|
{...rest} // pass through other props which might be coming in from the material-ui
|
||||||
{...passedInTriggerButtonProps}
|
{...passedInTriggerButtonProps}
|
||||||
onMouseEnter={handleSubMenuOpen}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleSubMenuClose}
|
onMouseLeave={handleMouseLeave}
|
||||||
onKeyDown={handleSubMenuParentItemKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<MuiMenuStyled
|
<MuiMenuStyled
|
||||||
anchorEl={anchorElement}
|
anchorEl={anchorElement}
|
||||||
@@ -138,8 +172,11 @@ export function SubMenu({id, leadingElement, labels, trailingElements, isDestruc
|
|||||||
asSubMenu={true}
|
asSubMenu={true}
|
||||||
anchorOrigin={originOfAnchorAndTransform.anchorOrigin}
|
anchorOrigin={originOfAnchorAndTransform.anchorOrigin}
|
||||||
transformOrigin={originOfAnchorAndTransform.transformOrigin}
|
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
|
<MuiMenuList
|
||||||
id={menuId}
|
id={menuId}
|
||||||
component='ul'
|
component='ul'
|
||||||
@@ -152,10 +189,12 @@ export function SubMenu({id, leadingElement, labels, trailingElements, isDestruc
|
|||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
<SubMenuContext.Provider value={providerValue}>
|
||||||
|
{children}
|
||||||
|
</SubMenuContext.Provider>
|
||||||
</MuiMenuList>
|
</MuiMenuList>
|
||||||
</MuiMenuStyled>
|
</MuiMenuStyled>
|
||||||
</ParentMenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
|
|||||||
bsClass="modal"
|
bsClass="modal"
|
||||||
dialogClassName="a11y__modal more-modal more-direct-channels"
|
dialogClassName="a11y__modal more-modal more-direct-channels"
|
||||||
dialogComponentClass={[Function]}
|
dialogComponentClass={[Function]}
|
||||||
enforceFocus={false}
|
enforceFocus={true}
|
||||||
id="moreDmModal"
|
id="moreDmModal"
|
||||||
keyboard={true}
|
keyboard={true}
|
||||||
manager={
|
manager={
|
||||||
@@ -294,7 +294,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
|
|||||||
bsClass="modal"
|
bsClass="modal"
|
||||||
dialogClassName="a11y__modal more-modal more-direct-channels"
|
dialogClassName="a11y__modal more-modal more-direct-channels"
|
||||||
dialogComponentClass={[Function]}
|
dialogComponentClass={[Function]}
|
||||||
enforceFocus={false}
|
enforceFocus={true}
|
||||||
id="moreDmModal"
|
id="moreDmModal"
|
||||||
keyboard={true}
|
keyboard={true}
|
||||||
manager={
|
manager={
|
||||||
|
|||||||
@@ -291,7 +291,6 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
|
|||||||
role='dialog'
|
role='dialog'
|
||||||
aria-labelledby='moreDmModalLabel'
|
aria-labelledby='moreDmModalLabel'
|
||||||
id='moreDmModal'
|
id='moreDmModal'
|
||||||
enforceFocus={false}
|
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header closeButton={true}>
|
||||||
<Modal.Title
|
<Modal.Title
|
||||||
|
|||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -2,21 +2,18 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback, useEffect, useState} from 'react';
|
import React, {useCallback, useEffect, useState} from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import {Moment} from 'moment-timezone';
|
|
||||||
|
|
||||||
import {GenericModal} from '@mattermost/components';
|
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 DateTimeInput, {getRoundedTime} from 'components/custom_status/date_time_input';
|
||||||
|
|
||||||
|
import {isKeyPressed} from 'utils/keyboard';
|
||||||
import {toUTCUnix} from 'utils/datetime';
|
import {toUTCUnix} from 'utils/datetime';
|
||||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
|
|
||||||
import type {PropsFromRedux} from './index';
|
import type {PropsFromRedux} from './index';
|
||||||
|
|
||||||
import './post_reminder_custom_time_picker_modal.scss';
|
import './post_reminder_custom_time_picker_modal.scss';
|
||||||
|
|
||||||
type Props = PropsFromRedux & {
|
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) {
|
function PostReminderCustomTimePicker({userId, timezone, onExited, postId, actions}: Props) {
|
||||||
const currentTime = getCurrentMomentForTimezone(timezone);
|
const currentTime = getCurrentMomentForTimezone(timezone);
|
||||||
const initialReminderTime: Moment = getRoundedTime(currentTime);
|
const initialReminderTime = getRoundedTime(currentTime);
|
||||||
const [customReminderTime, setCustomReminderTime] = useState<Moment>(initialReminderTime);
|
|
||||||
|
const [customReminderTime, setCustomReminderTime] = useState(initialReminderTime);
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
actions.addPostReminder(userId, postId, toUTCUnix(customReminderTime.toDate()));
|
actions.addPostReminder(userId, postId, toUTCUnix(customReminderTime.toDate()));
|
||||||
}, [customReminderTime]);
|
}, [customReminderTime]);
|
||||||
|
|
||||||
const [isDatePickerOpen, setIsDatePickerOpen] = useState<boolean>(false);
|
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
const {formatMessage} = useIntl();
|
||||||
if (isKeyPressed(event, Constants.KeyCodes.ESCAPE) && !isDatePickerOpen) {
|
|
||||||
onExited();
|
|
||||||
}
|
|
||||||
}, [isDatePickerOpen, onExited]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (isKeyPressed(event, Constants.KeyCodes.ESCAPE) && !isDatePickerOpen) {
|
||||||
|
onExited();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [handleKeyDown]);
|
}, [isDatePickerOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericModal
|
<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}
|
onExited={onExited}
|
||||||
modalHeaderText={modalHeaderText}
|
modalHeaderText={(
|
||||||
confirmButtonText={confirmButtonText}
|
<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}
|
handleConfirm={handleConfirm}
|
||||||
handleEnterKeyPress={handleConfirm}
|
handleEnterKeyPress={handleConfirm}
|
||||||
id='PostReminderCustomTimePickerModal'
|
|
||||||
className={'post-reminder-modal'}
|
className={'post-reminder-modal'}
|
||||||
compassDesign={true}
|
compassDesign={true}
|
||||||
enforceFocus={true}
|
|
||||||
keyboardEscape={false}
|
keyboardEscape={false}
|
||||||
>
|
>
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
|||||||
this.props.actions.openModal({
|
this.props.actions.openModal({
|
||||||
modalId: ModalIdentifiers.EDIT_CATEGORY,
|
modalId: ModalIdentifiers.EDIT_CATEGORY,
|
||||||
dialogType: EditCategoryModal,
|
dialogType: EditCategoryModal,
|
||||||
|
dialogProps: {},
|
||||||
});
|
});
|
||||||
trackEvent('ui', 'ui_sidebar_menu_createCategory');
|
trackEvent('ui', 'ui_sidebar_menu_createCategory');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {memo, MouseEvent, useState, KeyboardEvent} from 'react';
|
import React, {memo, useState} from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
@@ -44,8 +44,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
|
|
||||||
let muteUnmuteCategoryMenuItem: JSX.Element | null = null;
|
let muteUnmuteCategoryMenuItem: JSX.Element | null = null;
|
||||||
if (props.category.type !== CategoryTypes.DIRECT_MESSAGES) {
|
if (props.category.type !== CategoryTypes.DIRECT_MESSAGES) {
|
||||||
function toggleCategoryMute(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function toggleCategoryMute() {
|
||||||
event.preventDefault();
|
|
||||||
props.setCategoryMuted(props.category.id, !props.category.muted);
|
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) {
|
function handleSortChannels(sorting: CategorySorting) {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
props.setCategorySorting(props.category.id, sorting);
|
props.setCategorySorting(props.category.id, sorting);
|
||||||
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
|
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
|
||||||
}
|
}
|
||||||
@@ -186,7 +183,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
defaultMessage='Alphabetically'
|
defaultMessage='Alphabetically'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onClick={(event) => handleSortChannels(event, CategorySorting.Alphabetical)}
|
onClick={() => handleSortChannels(CategorySorting.Alphabetical)}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`sortByMostRecent-${props.category.id}`}
|
id={`sortByMostRecent-${props.category.id}`}
|
||||||
@@ -196,7 +193,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
defaultMessage='Recent Activity'
|
defaultMessage='Recent Activity'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onClick={(event) => handleSortChannels(event, CategorySorting.Recency)}
|
onClick={() => handleSortChannels(CategorySorting.Recency)}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`sortManual-${props.category.id}`}
|
id={`sortManual-${props.category.id}`}
|
||||||
@@ -206,7 +203,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
defaultMessage='Manually'
|
defaultMessage='Manually'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onClick={(event) => handleSortChannels(event, CategorySorting.Manual)}
|
onClick={() => handleSortChannels(CategorySorting.Manual)}
|
||||||
/>
|
/>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
function handleSortDirectMessages(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>, sorting: CategorySorting) {
|
function handleSortDirectMessages(sorting: CategorySorting) {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
props.setCategorySorting(props.category.id, sorting);
|
props.setCategorySorting(props.category.id, sorting);
|
||||||
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
|
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
|
||||||
}
|
}
|
||||||
@@ -87,7 +85,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
defaultMessage='Alphabetically'
|
defaultMessage='Alphabetically'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onClick={(event) => handleSortDirectMessages(event, CategorySorting.Alphabetical)}
|
onClick={() => handleSortDirectMessages(CategorySorting.Alphabetical)}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`sortByMostRecent-${props.category.id}`}
|
id={`sortByMostRecent-${props.category.id}`}
|
||||||
@@ -97,14 +95,13 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
defaultMessage='Recent Activity'
|
defaultMessage='Recent Activity'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onClick={(event) => handleSortDirectMessages(event, CategorySorting.Recency)}
|
onClick={() => handleSortDirectMessages(CategorySorting.Recency)}
|
||||||
/>
|
/>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function handlelimitVisibleDMsGMs(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>, number: number) {
|
function handlelimitVisibleDMsGMs(number: number) {
|
||||||
event.preventDefault();
|
|
||||||
props.savePreferences(props.currentUserId, [{
|
props.savePreferences(props.currentUserId, [{
|
||||||
user_id: props.currentUserId,
|
user_id: props.currentUserId,
|
||||||
category: Constants.Preferences.CATEGORY_SIDEBAR_SETTINGS,
|
category: Constants.Preferences.CATEGORY_SIDEBAR_SETTINGS,
|
||||||
@@ -149,7 +146,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
defaultMessage='All direct messages'
|
defaultMessage='All direct messages'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onClick={(event) => handlelimitVisibleDMsGMs(event, Constants.HIGHEST_DM_SHOW_COUNT)}
|
onClick={() => handlelimitVisibleDMsGMs(Constants.HIGHEST_DM_SHOW_COUNT)}
|
||||||
/>
|
/>
|
||||||
<Menu.Separator/>
|
<Menu.Separator/>
|
||||||
{Constants.DM_AND_GM_SHOW_COUNTS.map((dmGmShowCount) => (
|
{Constants.DM_AND_GM_SHOW_COUNTS.map((dmGmShowCount) => (
|
||||||
@@ -157,7 +154,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
id={`showDmCount-${props.category.id}-${dmGmShowCount}`}
|
id={`showDmCount-${props.category.id}-${dmGmShowCount}`}
|
||||||
key={`showDmCount-${props.category.id}-${dmGmShowCount}`}
|
key={`showDmCount-${props.category.id}-${dmGmShowCount}`}
|
||||||
labels={<span>{dmGmShowCount}</span>}
|
labels={<span>{dmGmShowCount}</span>}
|
||||||
onClick={(event) => handlelimitVisibleDMsGMs(event, dmGmShowCount)}
|
onClick={() => handlelimitVisibleDMsGMs(dmGmShowCount)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useRef, MouseEvent, KeyboardEvent, memo} from 'react';
|
import React, {useRef, memo} from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -36,9 +36,7 @@ const SidebarChannelMenu = (props: Props) => {
|
|||||||
|
|
||||||
let markAsReadUnreadMenuItem: JSX.Element | null = null;
|
let markAsReadUnreadMenuItem: JSX.Element | null = null;
|
||||||
if (props.isUnread) {
|
if (props.isUnread) {
|
||||||
function handleMarkAsRead(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleMarkAsRead() {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
props.markChannelAsRead(props.channel.id);
|
props.markChannelAsRead(props.channel.id);
|
||||||
trackEvent('ui', 'ui_sidebar_channel_menu_markAsRead');
|
trackEvent('ui', 'ui_sidebar_channel_menu_markAsRead');
|
||||||
}
|
}
|
||||||
@@ -58,9 +56,7 @@ const SidebarChannelMenu = (props: Props) => {
|
|||||||
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
function handleMarkAsUnread(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleMarkAsUnread() {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
props.markMostRecentPostInChannelAsUnread(props.channel.id);
|
props.markMostRecentPostInChannelAsUnread(props.channel.id);
|
||||||
trackEvent('ui', 'ui_sidebar_channel_menu_markAsUnread');
|
trackEvent('ui', 'ui_sidebar_channel_menu_markAsUnread');
|
||||||
}
|
}
|
||||||
@@ -82,9 +78,7 @@ const SidebarChannelMenu = (props: Props) => {
|
|||||||
|
|
||||||
let favoriteUnfavoriteMenuItem: JSX.Element | null = null;
|
let favoriteUnfavoriteMenuItem: JSX.Element | null = null;
|
||||||
if (props.isFavorite) {
|
if (props.isFavorite) {
|
||||||
function handleUnfavoriteChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleUnfavoriteChannel() {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
props.unfavoriteChannel(props.channel.id);
|
props.unfavoriteChannel(props.channel.id);
|
||||||
trackEvent('ui', 'ui_sidebar_channel_menu_unfavorite');
|
trackEvent('ui', 'ui_sidebar_channel_menu_unfavorite');
|
||||||
}
|
}
|
||||||
@@ -103,9 +97,7 @@ const SidebarChannelMenu = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
function handleFavoriteChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleFavoriteChannel() {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
props.favoriteChannel(props.channel.id);
|
props.favoriteChannel(props.channel.id);
|
||||||
trackEvent('ui', 'ui_sidebar_channel_menu_favorite');
|
trackEvent('ui', 'ui_sidebar_channel_menu_favorite');
|
||||||
}
|
}
|
||||||
@@ -143,9 +135,7 @@ const SidebarChannelMenu = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUnmuteChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleUnmuteChannel() {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
props.unmuteChannel(props.currentUserId, props.channel.id);
|
props.unmuteChannel(props.currentUserId, props.channel.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +163,7 @@ const SidebarChannelMenu = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMuteChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleMuteChannel() {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
props.muteChannel(props.currentUserId, props.channel.id);
|
props.muteChannel(props.currentUserId, props.channel.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,9 +179,7 @@ const SidebarChannelMenu = (props: Props) => {
|
|||||||
|
|
||||||
let copyLinkMenuItem: JSX.Element | null = null;
|
let copyLinkMenuItem: JSX.Element | null = null;
|
||||||
if (props.channel.type === Constants.OPEN_CHANNEL || props.channel.type === Constants.PRIVATE_CHANNEL) {
|
if (props.channel.type === Constants.OPEN_CHANNEL || props.channel.type === Constants.PRIVATE_CHANNEL) {
|
||||||
function handleCopyLink(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleCopyLink() {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
copyToClipboard(props.channelLink);
|
copyToClipboard(props.channelLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,9 +242,7 @@ const SidebarChannelMenu = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLeaveChannel(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleLeaveChannel() {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (isLeaving.current || !props.channelLeaveHandler) {
|
if (isLeaving.current || !props.channelLeaveHandler) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import classNames from 'classnames';
|
|||||||
import {Modal} from 'react-bootstrap';
|
import {Modal} from 'react-bootstrap';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import {FocusTrap} from '../focus_trap';
|
|
||||||
import './generic_modal.scss';
|
import './generic_modal.scss';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@@ -27,11 +26,6 @@ export type Props = {
|
|||||||
id: string;
|
id: string;
|
||||||
autoCloseOnCancelButton?: boolean;
|
autoCloseOnCancelButton?: boolean;
|
||||||
autoCloseOnConfirmButton?: 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;
|
enforceFocus?: boolean;
|
||||||
container?: React.ReactNode | React.ReactNodeArray;
|
container?: React.ReactNode | React.ReactNodeArray;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
@@ -111,12 +105,6 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||||||
this.props.handleKeydown?.(event);
|
this.props.handleKeydown?.(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleShow = () => {
|
|
||||||
if (this.props.enforceFocus === false) {
|
|
||||||
this.setState({isFocalTrapActive: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let confirmButton;
|
let confirmButton;
|
||||||
if (this.props.handleConfirm) {
|
if (this.props.handleConfirm) {
|
||||||
@@ -178,8 +166,6 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFocusTrapActive = this.props.enforceFocus === false ? this.state.isFocalTrapActive : false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
id={this.props.id}
|
id={this.props.id}
|
||||||
@@ -188,7 +174,6 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||||||
aria-labelledby={this.props.ariaLabel ? undefined : 'genericModalLabel'}
|
aria-labelledby={this.props.ariaLabel ? undefined : 'genericModalLabel'}
|
||||||
dialogClassName={classNames('a11y__modal GenericModal', {GenericModal__compassDesign: this.props.compassDesign}, this.props.className)}
|
dialogClassName={classNames('a11y__modal GenericModal', {GenericModal__compassDesign: this.props.compassDesign}, this.props.className)}
|
||||||
show={this.state.show}
|
show={this.state.show}
|
||||||
onShow={this.handleShow}
|
|
||||||
restoreFocus={true}
|
restoreFocus={true}
|
||||||
enforceFocus={this.props.enforceFocus}
|
enforceFocus={this.props.enforceFocus}
|
||||||
onHide={this.onHide}
|
onHide={this.onHide}
|
||||||
@@ -198,49 +183,47 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||||||
container={this.props.container}
|
container={this.props.container}
|
||||||
keyboard={this.props.keyboardEscape}
|
keyboard={this.props.keyboardEscape}
|
||||||
>
|
>
|
||||||
<FocusTrap active={isFocusTrapActive}>
|
<div
|
||||||
<div
|
onKeyDown={this.onEnterKeyDown}
|
||||||
onKeyDown={this.onEnterKeyDown}
|
tabIndex={this.props.tabIndex || 0}
|
||||||
tabIndex={this.props.tabIndex || 0}
|
className='GenericModal__wrapper-enter-key-press-catcher'
|
||||||
className='GenericModal__wrapper-enter-key-press-catcher'
|
>
|
||||||
>
|
<Modal.Header closeButton={true}>
|
||||||
<Modal.Header closeButton={true}>
|
{this.props.compassDesign && (
|
||||||
{this.props.compassDesign && (
|
<>
|
||||||
<>
|
{headerText}
|
||||||
{headerText}
|
{this.props.headerInput}
|
||||||
{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>
|
</Modal.Header>
|
||||||
</FocusTrap>
|
<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>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user