Improved handling of onClicks for menu items (#23749)

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

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

@@ -24,7 +24,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
} }
show={true} 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}
> >

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

@@ -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>
); );
} }