diff --git a/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap b/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap index 537af08b17..5b0a8d66cb 100644 --- a/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap +++ b/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap @@ -24,7 +24,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i } show={true} /> - - { +const ChannelMoveToSubMenuOld = (props: Props) => { const {formatMessage} = useIntl(); const dispatch = useDispatch(); @@ -168,4 +168,4 @@ const ChannelMoveToSubMenu = (props: Props) => { ); }; -export default memo(ChannelMoveToSubMenu); +export default memo(ChannelMoveToSubMenuOld); diff --git a/webapp/channels/src/components/delete_category_modal/delete_category_modal.tsx b/webapp/channels/src/components/delete_category_modal/delete_category_modal.tsx index 347acc6fe1..dd8b4a1399 100644 --- a/webapp/channels/src/components/delete_category_modal/delete_category_modal.tsx +++ b/webapp/channels/src/components/delete_category_modal/delete_category_modal.tsx @@ -59,7 +59,6 @@ export default class DeleteCategoryModal extends React.PureComponent )} confirmButtonClassName={'delete'} - enforceFocus={false} > { onEntered={this.handleEntered} onHide={this.onHide} onExited={this.props.onExited} - enforceFocus={false} id='deletePostModal' role='dialog' aria-labelledby='deletePostModalLabel' diff --git a/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap b/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap index 93cfa8e998..0df7a1dd84 100644 --- a/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap +++ b/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap @@ -112,7 +112,6 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = ` menu={ Object { "aria-label": "Post extra options", - "closeMenuManually": false, "id": "CENTER_dropdown_post_id_1", "onKeyDown": [Function], "onToggle": [Function], @@ -236,7 +235,7 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = ` /> } /> - { expect(menuItem).toBeVisible(); expect(menuItem).toHaveTextContent(text); }); - - test.each([ - [false, {isFollowingThread: true}], - [true, {isFollowingThread: false}], - ])('should call setThreadFollow with following as %s', async (following, caseProps) => { - const spySetThreadFollow = jest.fn(); - const spy = jest.spyOn(dotUtils, 'trackDotMenuEvent'); - - const props = { - ...baseProps, - ...caseProps, - location: Locations.RHS_ROOT, - actions: { - ...baseProps.actions, - setThreadFollow: spySetThreadFollow, - }, - }; - renderWithIntlAndStore( - , - 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, - ); - }); }); }); diff --git a/webapp/channels/src/components/dot_menu/dot_menu.tsx b/webapp/channels/src/components/dot_menu/dot_menu.tsx index 255d05a466..c75eb427a7 100644 --- a/webapp/channels/src/components/dot_menu/dot_menu.tsx +++ b/webapp/channels/src/components/dot_menu/dot_menu.tsx @@ -5,6 +5,9 @@ import React from 'react'; import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; import classNames from 'classnames'; +import {UserThread} from '@mattermost/types/threads'; +import {Post} from '@mattermost/types/posts'; + import { ArrowRightBoldOutlineIcon, BookmarkIcon, @@ -25,24 +28,21 @@ import { import Permissions from 'mattermost-redux/constants/permissions'; +import {ModalData} from 'types/actions'; import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants'; -import DeletePostModal from 'components/delete_post_modal'; import DelayedAction from 'utils/delayed_action'; import * as Keyboard from 'utils/keyboard'; import * as PostUtils from 'utils/post_utils'; -import * as Menu from 'components/menu'; import * as Utils from 'utils/utils'; -import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; -import {ModalData} from 'types/actions'; -import {UserThread} from '@mattermost/types/threads'; -import {Post} from '@mattermost/types/posts'; -import ForwardPostModal from '../forward_post_modal'; +import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; +import DeletePostModal from 'components/delete_post_modal'; +import ForwardPostModal from 'components/forward_post_modal'; +import * as Menu from 'components/menu'; import {ChangeEvent, trackDotMenuEvent} from './utils'; - +import PostReminderSubMenu from './post_reminder_submenu'; import './dot_menu.scss'; -import {PostReminderSubmenu} from './post_reminder_submenu'; type ShortcutKeyProps = { shortcutKey: string; @@ -115,12 +115,6 @@ type Props = { * Function to set the thread as followed/unfollowed */ setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void; - - /** - * Function to set a global storage item on the store - */ - setGlobalItem: (name: string, value: any) => void; - }; // TechDebt: Made non-mandatory while converting to typescript canEdit: boolean; @@ -134,7 +128,6 @@ type Props = { } type State = { - closeMenuManually: boolean; canEdit: boolean; canDelete: boolean; } @@ -146,7 +139,6 @@ export class DotMenuClass extends React.PureComponent { location: Locations.CENTER, }; private editDisableAction: DelayedAction; - private buttonRef: React.RefObject; private canPostBeForwarded: boolean; constructor(props: Props) { @@ -155,13 +147,10 @@ export class DotMenuClass extends React.PureComponent { this.editDisableAction = new DelayedAction(this.handleEditDisable); this.state = { - closeMenuManually: false, canEdit: props.canEdit && !props.isReadOnly, canDelete: props.canDelete && !props.isReadOnly, }; - this.buttonRef = React.createRef(); - this.canPostBeForwarded = false; } @@ -173,7 +162,7 @@ export class DotMenuClass extends React.PureComponent { return state; } - disableCanEditPostByTime(): void { + disableCanEditPostByTime() { const {post, isLicensed} = this.props; const {canEdit} = this.state; @@ -190,32 +179,29 @@ export class DotMenuClass extends React.PureComponent { } } - componentDidMount(): void { + componentDidMount() { this.disableCanEditPostByTime(); } - componentWillUnmount(): void { + componentWillUnmount() { this.editDisableAction.cancel(); } - handleEditDisable = (): void => { + handleEditDisable = () => { this.setState({canEdit: false}); }; - handleFlagMenuItemActivated = (e: ChangeEvent): void => { + handleFlagMenuItemActivated = (e: ChangeEvent) => { if (this.props.isFlagged) { - trackDotMenuEvent(e, TELEMETRY_LABELS.UNSAVE); this.props.actions.unflagPost(this.props.post.id); + trackDotMenuEvent(e, TELEMETRY_LABELS.UNSAVE); } else { - trackDotMenuEvent(e, TELEMETRY_LABELS.SAVE); this.props.actions.flagPost(this.props.post.id); + trackDotMenuEvent(e, TELEMETRY_LABELS.SAVE); } }; - // listen to clicks/taps on add reaction menu item and pass to parent handler - handleAddReactionMenuItemActivated = (e: ChangeEvent): void => { - e.preventDefault(); - + handleAddReactionMenuItemActivated = () => { // to be safe, make sure the handler function has been defined if (this.props.handleAddReactionClick) { this.props.handleAddReactionClick(); @@ -223,35 +209,31 @@ export class DotMenuClass extends React.PureComponent { }; copyLink = (e: ChangeEvent) => { - trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_LINK); Utils.copyToClipboard(`${this.props.teamUrl}/pl/${this.props.post.id}`); + trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_LINK); }; copyText = (e: ChangeEvent) => { - trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_TEXT); Utils.copyToClipboard(this.props.post.message); + trackDotMenuEvent(e, TELEMETRY_LABELS.COPY_TEXT); }; handlePinMenuItemActivated = (e: ChangeEvent): void => { if (this.props.post.is_pinned) { - trackDotMenuEvent(e, TELEMETRY_LABELS.UNPIN); this.props.actions.unpinPost(this.props.post.id); + trackDotMenuEvent(e, TELEMETRY_LABELS.UNPIN); } else { - trackDotMenuEvent(e, TELEMETRY_LABELS.PIN); this.props.actions.pinPost(this.props.post.id); + trackDotMenuEvent(e, TELEMETRY_LABELS.PIN); } }; handleMarkPostAsUnread = (e: ChangeEvent): void => { - e.preventDefault(); - trackDotMenuEvent(e, TELEMETRY_LABELS.UNREAD); this.props.actions.markPostAsUnread(this.props.post, this.props.location); + trackDotMenuEvent(e, TELEMETRY_LABELS.UNREAD); }; handleDeleteMenuItemActivated = (e: ChangeEvent): void => { - e.preventDefault(); - - trackDotMenuEvent(e, TELEMETRY_LABELS.DELETE); const deletePostModalData = { modalId: ModalIdentifiers.DELETE_POST, dialogType: DeletePostModal, @@ -262,6 +244,8 @@ export class DotMenuClass extends React.PureComponent { }; this.props.actions.openModal(deletePostModalData); + + trackDotMenuEvent(e, TELEMETRY_LABELS.DELETE); }; handleForwardMenuItemActivated = (e: ChangeEvent): void => { @@ -271,8 +255,6 @@ export class DotMenuClass extends React.PureComponent { return; } - e.preventDefault(); - trackDotMenuEvent(e, TELEMETRY_LABELS.FORWARD); const forwardPostModalData = { modalId: ModalIdentifiers.FORWARD_POST_MODAL, @@ -286,7 +268,6 @@ export class DotMenuClass extends React.PureComponent { }; handleEditMenuItemActivated = (e: ChangeEvent): void => { - trackDotMenuEvent(e, TELEMETRY_LABELS.EDIT); this.props.handleDropdownOpened?.(false); this.props.actions.setEditingPost( this.props.post.id, @@ -294,6 +275,7 @@ export class DotMenuClass extends React.PureComponent { this.props.post.root_id ? Utils.localizeMessage('rhs_comment.comment', 'Comment') : Utils.localizeMessage('create_post.post', 'Post'), this.props.location === Locations.RHS_ROOT || this.props.location === Locations.RHS_COMMENT || this.props.location === Locations.SEARCH, ); + trackDotMenuEvent(e, TELEMETRY_LABELS.EDIT); }; handleSetThreadFollow = (e: ChangeEvent) => { @@ -325,88 +307,79 @@ export class DotMenuClass extends React.PureComponent { this.props.handleCommentClick?.(e); }; - isKeyboardEvent = (e: React.KeyboardEvent): any => { - return (e).getModifierState !== undefined; - }; + handleMenuKeydown = (event: React.KeyboardEvent, forceCloseMenu?: (() => void)) => { + event.preventDefault(); - onShortcutKeyDown = (e: React.KeyboardEvent): void => { - e.preventDefault(); - if (!this.isKeyboardEvent(e)) { + if (!forceCloseMenu) { return; } - const isShiftKeyPressed = e.shiftKey; + const isShiftKeyPressed = event.shiftKey; switch (true) { - case Keyboard.isKeyPressed(e, Constants.KeyCodes.R): - this.handleCommentClick(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.R): + forceCloseMenu(); + this.handleCommentClick(event); break; // edit post - case Keyboard.isKeyPressed(e, Constants.KeyCodes.E): - this.handleEditMenuItemActivated(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.E): + forceCloseMenu(); + this.handleEditMenuItemActivated(event); break; // follow thread - case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && !isShiftKeyPressed: - this.handleSetThreadFollow(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.F) && !isShiftKeyPressed: + forceCloseMenu(); + this.handleSetThreadFollow(event); break; // forward post - case Keyboard.isKeyPressed(e, Constants.KeyCodes.F) && isShiftKeyPressed: - this.handleForwardMenuItemActivated(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.F) && isShiftKeyPressed: + forceCloseMenu(); + this.handleForwardMenuItemActivated(event); break; // copy link - case Keyboard.isKeyPressed(e, Constants.KeyCodes.K): - this.copyLink(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.K): + forceCloseMenu(); + this.copyLink(event); break; // copy text - case Keyboard.isKeyPressed(e, Constants.KeyCodes.C): - this.copyText(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.C): + forceCloseMenu(); + this.copyText(event); break; // delete post - case Keyboard.isKeyPressed(e, Constants.KeyCodes.DELETE): - this.handleDeleteMenuItemActivated(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.DELETE): + forceCloseMenu(); + this.handleDeleteMenuItemActivated(event); break; // pin / unpin - case Keyboard.isKeyPressed(e, Constants.KeyCodes.P): - this.handlePinMenuItemActivated(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.P): + forceCloseMenu(); + this.handlePinMenuItemActivated(event); break; // save / unsave - case Keyboard.isKeyPressed(e, Constants.KeyCodes.S): - this.handleFlagMenuItemActivated(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.S): + forceCloseMenu(); + this.handleFlagMenuItemActivated(event); break; // mark as unread - case Keyboard.isKeyPressed(e, Constants.KeyCodes.U): - this.handleMarkPostAsUnread(e); - this.handleDropdownOpened(false); + case Keyboard.isKeyPressed(event, Constants.KeyCodes.U): + forceCloseMenu(); + this.handleMarkPostAsUnread(event); break; } }; - handleDropdownOpened = (open: boolean) => { - this.props.handleDropdownOpened?.(open); - this.setState({closeMenuManually: true}); - }; - handleMenuToggle = (open: boolean) => { this.props.handleDropdownOpened?.(open); - this.setState({closeMenuManually: false}); }; render(): JSX.Element { @@ -414,11 +387,6 @@ export class DotMenuClass extends React.PureComponent { const isFollowingThread = this.props.isFollowingThread ?? this.props.isMentionedInRootPost; const isMobile = this.props.isMobileView; const isSystemMessage = PostUtils.isSystemMessage(this.props.post); - const deleteShortcutText = ( - - {'delete'} - - ); this.canPostBeForwarded = !(isSystemMessage); @@ -482,6 +450,7 @@ export class DotMenuClass extends React.PureComponent { defaultMessage='Pin' /> ); + const unPinPost = ( { menu={{ id: `${this.props.location}_dropdown_${this.props.post.id}`, 'aria-label': formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'}), - onKeyDown: this.onShortcutKeyDown, + onKeyDown: this.handleMenuKeydown, width: '264px', onToggle: this.handleMenuToggle, - closeMenuManually: this.state.closeMenuManually, }} menuButtonTooltip={{ id: `PostDotMenu-ButtonTooltip-${this.props.post.id}`, @@ -563,20 +531,23 @@ export class DotMenuClass extends React.PureComponent { {Boolean( !isSystemMessage && this.props.isCollapsedThreadsEnabled && - ( - this.props.location === Locations.CENTER || + (this.props.location === Locations.CENTER || this.props.location === Locations.RHS_ROOT || - this.props.location === Locations.RHS_COMMENT - ), - ) && - } - labels={followPostLabel()} - leadingElement={isFollowingThread ? : } - onClick={this.handleSetThreadFollow} - /> + this.props.location === Locations.RHS_COMMENT)) && + } + labels={followPostLabel()} + leadingElement={ + isFollowingThread ? ( + + ) : ( + + ) + } + onClick={this.handleSetThreadFollow} + /> } {Boolean(!isSystemMessage && !this.props.channelIsArchived && this.props.location !== Locations.SEARCH) && { /> } {!isSystemMessage && - { id={`delete_post_${this.props.post.id}`} data-testid={`delete_post_${this.props.post.id}`} leadingElement={} - trailingElements={deleteShortcutText} + trailingElements={{'delete'}} labels={ (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 = (