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