(modalData: ModalData) => 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) {
@@ -155,7 +153,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
openModal,
markPostAsUnread,
setThreadFollow,
- setGlobalItem,
}, dispatch),
};
}
diff --git a/webapp/channels/src/components/dot_menu/post_reminder_submenu.tsx b/webapp/channels/src/components/dot_menu/post_reminder_submenu.tsx
index 85ddefc955..bed40550e4 100644
--- a/webapp/channels/src/components/dot_menu/post_reminder_submenu.tsx
+++ b/webapp/channels/src/components/dot_menu/post_reminder_submenu.tsx
@@ -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 = (
+
+ );
+ } else if (postReminder === PostReminders.ONE_HOUR) {
+ labels = (
+
+ );
+ } else if (postReminder === PostReminders.TWO_HOURS) {
+ labels = (
+
+ );
+ } else if (postReminder === PostReminders.TOMORROW) {
+ labels = (
+
+ );
+ } else {
+ labels = (
+
+ );
}
- 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 = (
-
+ trailingElements = (
+
+
+ {', '}
+
+
);
+ }
- let trailing: React.ReactNode;
- if (id === 'tomorrow') {
- const tomorrow = getCurrentMomentForTimezone(props.timezone).add(1, 'day').toDate();
- trailing = (
-
-
- {', '}
-
-
- );
- }
- return (
- setCustomPostReminder() : () => setPostReminder(id)}
- />
- );
- });
+ return (
+ handlePostReminderMenuClick(postReminder)}
+ />
+ );
+ });
return (
);
}
+
+export default memo(PostReminderSubmenu);
diff --git a/webapp/channels/src/components/edit_category_modal/edit_category_modal.tsx b/webapp/channels/src/components/edit_category_modal/edit_category_modal.tsx
index 29b02d2937..8d94257839 100644
--- a/webapp/channels/src/components/edit_category_modal/edit_category_modal.tsx
+++ b/webapp/channels/src/components/edit_category_modal/edit_category_modal.tsx
@@ -132,7 +132,6 @@ export default class EditCategoryModal extends React.PureComponent
handleConfirm={this.handleConfirm}
handleCancel={this.handleCancel}
isConfirmDisabled={this.isConfirmDisabled()}
- enforceFocus={false}
>
void;
- closeMenuManually?: boolean;
- onKeyDown?: KeyboardEventHandler;
+ onKeyDown?: (event: KeyboardEvent, 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) {
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 | KeyboardEvent) {
+ e.stopPropagation();
}
- useEffect(() => {
- if (props.menu.closeMenuManually) {
- setAnchorElement(null);
- if (isMobileView) {
- handleMenuModalClose(props.menu.id);
- }
- }
- }, [props.menu.closeMenuManually]);
-
function handleMenuKeyDown(event: KeyboardEvent) {
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) {
@@ -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 = (
);
diff --git a/webapp/channels/src/components/sidebar/sidebar_category/sidebar_category_sorting_menu/sidebar_category_sorting_menu.tsx b/webapp/channels/src/components/sidebar/sidebar_category/sidebar_category_sorting_menu/sidebar_category_sorting_menu.tsx
index 879984b032..aa3937dc2b 100644
--- a/webapp/channels/src/components/sidebar/sidebar_category/sidebar_category_sorting_menu/sidebar_category_sorting_menu.tsx
+++ b/webapp/channels/src/components/sidebar/sidebar_category/sidebar_category_sorting_menu/sidebar_category_sorting_menu.tsx
@@ -37,9 +37,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const {formatMessage} = useIntl();
- function handleSortDirectMessages(event: MouseEvent | KeyboardEvent, 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)}
/>
{
defaultMessage='Recent Activity'
/>
)}
- onClick={(event) => handleSortDirectMessages(event, CategorySorting.Recency)}
+ onClick={() => handleSortDirectMessages(CategorySorting.Recency)}
/>
);
- function handlelimitVisibleDMsGMs(event: MouseEvent | KeyboardEvent, 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)}
/>
{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={{dmGmShowCount}}
- onClick={(event) => handlelimitVisibleDMsGMs(event, dmGmShowCount)}
+ onClick={() => handlelimitVisibleDMsGMs(dmGmShowCount)}
/>
))}
diff --git a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/sidebar_channel_menu.tsx b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/sidebar_channel_menu.tsx
index 7d0dd24b2e..573a51e3a3 100644
--- a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/sidebar_channel_menu.tsx
+++ b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/sidebar_channel_menu.tsx
@@ -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 | KeyboardEvent) {
- 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 | KeyboardEvent) {
- 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 | KeyboardEvent) {
- 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 | KeyboardEvent) {
- 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 | KeyboardEvent) {
- event.preventDefault();
-
+ function handleUnmuteChannel() {
props.unmuteChannel(props.currentUserId, props.channel.id);
}
@@ -173,9 +163,7 @@ const SidebarChannelMenu = (props: Props) => {
);
}
- function handleMuteChannel(event: MouseEvent | KeyboardEvent) {
- 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 | KeyboardEvent) {
- event.preventDefault();
-
+ function handleCopyLink() {
copyToClipboard(props.channelLink);
}
@@ -256,9 +242,7 @@ const SidebarChannelMenu = (props: Props) => {
);
}
- function handleLeaveChannel(event: MouseEvent | KeyboardEvent) {
- event.preventDefault();
-
+ function handleLeaveChannel() {
if (isLeaving.current || !props.channelLeaveHandler) {
return;
}
diff --git a/webapp/platform/components/src/generic_modal/generic_modal.tsx b/webapp/platform/components/src/generic_modal/generic_modal.tsx
index 7e65fbfdc2..807c16e15d 100644
--- a/webapp/platform/components/src/generic_modal/generic_modal.tsx
+++ b/webapp/platform/components/src/generic_modal/generic_modal.tsx
@@ -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 {
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 {
);
- const isFocusTrapActive = this.props.enforceFocus === false ? this.state.isFocalTrapActive : false;
-
return (
{
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 {
container={this.props.container}
keyboard={this.props.keyboardEscape}
>
-
-
-
- {this.props.compassDesign && (
- <>
- {headerText}
- {this.props.headerInput}
- >
- )}
-
-
- {this.props.compassDesign ? (
- this.props.errorText && (
-
-
- {this.props.errorText}
-
- )
- ) : (
- headerText
- )}
-
- {this.props.children}
-
-
- {(cancelButton || confirmButton || this.props.footerContent) && (
-
- {(cancelButton || confirmButton) ? (
- <>
- {cancelButton}
- {confirmButton}
- >
- ) : (
- this.props.footerContent
- )}
-
+
+
+ {this.props.compassDesign && (
+ <>
+ {headerText}
+ {this.props.headerInput}
+ >
)}
-
-
+
+
+ {this.props.compassDesign ? (
+ this.props.errorText && (
+
+
+ {this.props.errorText}
+
+ )
+ ) : (
+ headerText
+ )}
+
+ {this.props.children}
+
+
+ {(cancelButton || confirmButton || this.props.footerContent) && (
+
+ {(cancelButton || confirmButton) ? (
+ <>
+ {cancelButton}
+ {confirmButton}
+ >
+ ) : (
+ this.props.footerContent
+ )}
+
+ )}
+
);
}