Support right click context menu interactions on Room List 2
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>pull/21833/head
							parent
							
								
									89bd572371
								
							
						
					
					
						commit
						5c2b291510
					
				|  | @ -42,8 +42,10 @@ interface IProps { | |||
|     isMinimized: boolean; | ||||
| } | ||||
| 
 | ||||
| type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">; | ||||
| 
 | ||||
| interface IState { | ||||
|     menuDisplayed: boolean; | ||||
|     contextMenuPosition: PartialDOMRect; | ||||
|     isDarkTheme: boolean; | ||||
| } | ||||
| 
 | ||||
|  | @ -56,7 +58,7 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|             menuDisplayed: false, | ||||
|             contextMenuPosition: null, | ||||
|             isDarkTheme: this.isUserOnDarkTheme(), | ||||
|         }; | ||||
| 
 | ||||
|  | @ -106,13 +108,27 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|     private onOpenMenuClick = (ev: InputEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({menuDisplayed: true}); | ||||
|         const target = ev.target as HTMLButtonElement; | ||||
|         this.setState({contextMenuPosition: target.getBoundingClientRect()}); | ||||
|     }; | ||||
| 
 | ||||
|     private onContextMenu = (ev: React.MouseEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({ | ||||
|             contextMenuPosition: { | ||||
|                 left: ev.clientX, | ||||
|                 top: ev.clientY, | ||||
|                 width: 20, | ||||
|                 height: 0, | ||||
|             }, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     private onCloseMenu = (ev: InputEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({menuDisplayed: false}); | ||||
|         this.setState({contextMenuPosition: null}); | ||||
|     }; | ||||
| 
 | ||||
|     private onSwitchThemeClick = () => { | ||||
|  | @ -129,7 +145,7 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
| 
 | ||||
|         const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId}; | ||||
|         defaultDispatcher.dispatch(payload); | ||||
|         this.setState({menuDisplayed: false}); // also close the menu
 | ||||
|         this.setState({contextMenuPosition: null}); // also close the menu
 | ||||
|     }; | ||||
| 
 | ||||
|     private onShowArchived = (ev: ButtonEvent) => { | ||||
|  | @ -145,7 +161,7 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|         ev.stopPropagation(); | ||||
| 
 | ||||
|         Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); | ||||
|         this.setState({menuDisplayed: false}); // also close the menu
 | ||||
|         this.setState({contextMenuPosition: null}); // also close the menu
 | ||||
|     }; | ||||
| 
 | ||||
|     private onSignOutClick = (ev: ButtonEvent) => { | ||||
|  | @ -153,7 +169,7 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|         ev.stopPropagation(); | ||||
| 
 | ||||
|         Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); | ||||
|         this.setState({menuDisplayed: false}); // also close the menu
 | ||||
|         this.setState({contextMenuPosition: null}); // also close the menu
 | ||||
|     }; | ||||
| 
 | ||||
|     private onHomeClick = (ev: ButtonEvent) => { | ||||
|  | @ -164,7 +180,7 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|     }; | ||||
| 
 | ||||
|     private renderContextMenu = (): React.ReactNode => { | ||||
|         if (!this.state.menuDisplayed) return null; | ||||
|         if (!this.state.contextMenuPosition) return null; | ||||
| 
 | ||||
|         let hostingLink; | ||||
|         const signupLink = getHostingLink("user-context-menu"); | ||||
|  | @ -198,13 +214,12 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         const elementRect = this.buttonRef.current.getBoundingClientRect(); | ||||
|         return ( | ||||
|             <ContextMenu | ||||
|                 chevronFace="none" | ||||
|                 // -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
 | ||||
|                 left={elementRect.width + elementRect.left - 20} | ||||
|                 top={elementRect.top + elementRect.height} | ||||
|                 left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20} | ||||
|                 top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height} | ||||
|                 onFinished={this.onCloseMenu} | ||||
|             > | ||||
|                 <div className="mx_IconizedContextMenu mx_UserMenu_contextMenu"> | ||||
|  | @ -290,7 +305,8 @@ export default class UserMenu extends React.Component<IProps, IState> { | |||
|                     onClick={this.onOpenMenuClick} | ||||
|                     inputRef={this.buttonRef} | ||||
|                     label={_t("Account settings")} | ||||
|                     isExpanded={this.state.menuDisplayed} | ||||
|                     isExpanded={!!this.state.contextMenuPosition} | ||||
|                     onContextMenu={this.onContextMenu} | ||||
|                 > | ||||
|                     <div className="mx_UserMenu_row"> | ||||
|                         <span className="mx_UserMenu_userAvatarContainer"> | ||||
|  |  | |||
|  | @ -65,22 +65,21 @@ interface IProps { | |||
|     // TODO: Account for https://github.com/vector-im/riot-web/issues/14179
 | ||||
| } | ||||
| 
 | ||||
| type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">; | ||||
| 
 | ||||
| interface IState { | ||||
|     notificationState: ListNotificationState; | ||||
|     menuDisplayed: boolean; | ||||
|     contextMenuPosition: PartialDOMRect; | ||||
|     isResizing: boolean; | ||||
| } | ||||
| 
 | ||||
| export default class RoomSublist2 extends React.Component<IProps, IState> { | ||||
|     private headerButton = createRef(); | ||||
|     private menuButtonRef: React.RefObject<HTMLButtonElement> = createRef(); | ||||
| 
 | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|             notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId), | ||||
|             menuDisplayed: false, | ||||
|             contextMenuPosition: null, | ||||
|             isResizing: false, | ||||
|         }; | ||||
|         this.state.notificationState.setRooms(this.props.rooms); | ||||
|  | @ -132,11 +131,24 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
|     private onOpenMenuClick = (ev: InputEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({menuDisplayed: true}); | ||||
|         const target = ev.target as HTMLButtonElement; | ||||
|         this.setState({contextMenuPosition: target.getBoundingClientRect()}); | ||||
|     }; | ||||
| 
 | ||||
|     private onContextMenu = (ev: React.MouseEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({ | ||||
|             contextMenuPosition: { | ||||
|                 left: ev.clientX, | ||||
|                 top: ev.clientY, | ||||
|                 height: 0, | ||||
|             }, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     private onCloseMenu = () => { | ||||
|         this.setState({menuDisplayed: false}); | ||||
|         this.setState({contextMenuPosition: null}); | ||||
|     }; | ||||
| 
 | ||||
|     private onUnreadFirstChanged = async () => { | ||||
|  | @ -202,15 +214,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
|         } | ||||
| 
 | ||||
|         let contextMenu = null; | ||||
|         if (this.state.menuDisplayed) { | ||||
|             const elementRect = this.menuButtonRef.current.getBoundingClientRect(); | ||||
|         if (this.state.contextMenuPosition) { | ||||
|             const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; | ||||
|             const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; | ||||
|             contextMenu = ( | ||||
|                 <ContextMenu | ||||
|                     chevronFace="none" | ||||
|                     left={elementRect.left} | ||||
|                     top={elementRect.top + elementRect.height} | ||||
|                     left={this.state.contextMenuPosition.left} | ||||
|                     top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height} | ||||
|                     onFinished={this.onCloseMenu} | ||||
|                 > | ||||
|                     <div className="mx_RoomSublist2_contextMenu"> | ||||
|  | @ -261,9 +272,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
|                 <ContextMenuButton | ||||
|                     className="mx_RoomSublist2_menuButton" | ||||
|                     onClick={this.onOpenMenuClick} | ||||
|                     inputRef={this.menuButtonRef} | ||||
|                     label={_t("List options")} | ||||
|                     isExpanded={this.state.menuDisplayed} | ||||
|                     isExpanded={!!this.state.contextMenuPosition} | ||||
|                 /> | ||||
|                 {contextMenu} | ||||
|             </React.Fragment> | ||||
|  | @ -272,7 +282,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
| 
 | ||||
|     private renderHeader(): React.ReactElement { | ||||
|         return ( | ||||
|             <RovingTabIndexWrapper inputRef={this.headerButton}> | ||||
|             <RovingTabIndexWrapper> | ||||
|                 {({onFocus, isActive, ref}) => { | ||||
|                     // TODO: Use onFocus: https://github.com/vector-im/riot-web/issues/14180
 | ||||
|                     const tabIndex = isActive ? 0 : -1; | ||||
|  | @ -317,12 +327,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
|                         <div className={classes}> | ||||
|                             <div className='mx_RoomSublist2_stickable'> | ||||
|                                 <AccessibleButton | ||||
|                                     onFocus={onFocus} | ||||
|                                     inputRef={ref} | ||||
|                                     tabIndex={tabIndex} | ||||
|                                     className={"mx_RoomSublist2_headerText"} | ||||
|                                     role="treeitem" | ||||
|                                     aria-level={1} | ||||
|                                     onClick={this.onHeaderClick} | ||||
|                                     onContextMenu={this.onContextMenu} | ||||
|                                 > | ||||
|                                     <span className={collapseClasses} /> | ||||
|                                     <span>{this.props.label}</span> | ||||
|  | @ -347,7 +359,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> { | |||
| 
 | ||||
|         const classes = classNames({ | ||||
|             'mx_RoomSublist2': true, | ||||
|             'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed, | ||||
|             'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition, | ||||
|             'mx_RoomSublist2_minimized': this.props.isMinimized, | ||||
|         }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -60,15 +60,17 @@ interface IProps { | |||
|     // TODO: Incoming call boxes: https://github.com/vector-im/riot-web/issues/14177
 | ||||
| } | ||||
| 
 | ||||
| type PartialDOMRect = Pick<DOMRect, "left" | "bottom">; | ||||
| 
 | ||||
| interface IState { | ||||
|     hover: boolean; | ||||
|     notificationState: INotificationState; | ||||
|     selected: boolean; | ||||
|     notificationsMenuDisplayed: boolean; | ||||
|     generalMenuDisplayed: boolean; | ||||
|     notificationsMenuPosition: PartialDOMRect; | ||||
|     generalMenuPosition: PartialDOMRect; | ||||
| } | ||||
| 
 | ||||
| const contextMenuBelow = (elementRect) => { | ||||
| const contextMenuBelow = (elementRect: PartialDOMRect) => { | ||||
|     // align the context menu's icons with the icon which opened the context menu
 | ||||
|     const left = elementRect.left + window.pageXOffset - 9; | ||||
|     let top = elementRect.bottom + window.pageYOffset + 17; | ||||
|  | @ -103,9 +105,6 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam | |||
| }; | ||||
| 
 | ||||
| export default class RoomTile2 extends React.Component<IProps, IState> { | ||||
|     private notificationsMenuButtonRef: React.RefObject<HTMLButtonElement> = createRef(); | ||||
|     private generalMenuButtonRef: React.RefObject<HTMLButtonElement> = createRef(); | ||||
| 
 | ||||
|     // TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
 | ||||
| 
 | ||||
|     constructor(props: IProps) { | ||||
|  | @ -115,8 +114,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|             hover: false, | ||||
|             notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag), | ||||
|             selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, | ||||
|             notificationsMenuDisplayed: false, | ||||
|             generalMenuDisplayed: false, | ||||
|             notificationsMenuPosition: null, | ||||
|             generalMenuPosition: null, | ||||
|         }; | ||||
| 
 | ||||
|         ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); | ||||
|  | @ -137,6 +136,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|     }; | ||||
| 
 | ||||
|     private onTileClick = (ev: React.KeyboardEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         dis.dispatch({ | ||||
|             action: 'view_room', | ||||
|             // TODO: Support show_room_tile in new room list: https://github.com/vector-im/riot-web/issues/14233
 | ||||
|  | @ -153,25 +154,34 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|     private onNotificationsMenuOpenClick = (ev: InputEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({notificationsMenuDisplayed: true}); | ||||
|         const target = ev.target as HTMLButtonElement; | ||||
|         this.setState({notificationsMenuPosition: target.getBoundingClientRect()}); | ||||
|     }; | ||||
| 
 | ||||
|     private onCloseNotificationsMenu = (ev: InputEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({notificationsMenuDisplayed: false}); | ||||
|     private onCloseNotificationsMenu = () => { | ||||
|         this.setState({notificationsMenuPosition: null}); | ||||
|     }; | ||||
| 
 | ||||
|     private onGeneralMenuOpenClick = (ev: InputEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({generalMenuDisplayed: true}); | ||||
|         const target = ev.target as HTMLButtonElement; | ||||
|         this.setState({generalMenuPosition: target.getBoundingClientRect()}); | ||||
|     }; | ||||
| 
 | ||||
|     private onCloseGeneralMenu = (ev: InputEvent) => { | ||||
|     private onContextMenu = (ev: React.MouseEvent) => { | ||||
|         ev.preventDefault(); | ||||
|         ev.stopPropagation(); | ||||
|         this.setState({generalMenuDisplayed: false}); | ||||
|         this.setState({ | ||||
|             generalMenuPosition: { | ||||
|                 left: ev.clientX, | ||||
|                 bottom: ev.clientY, | ||||
|             }, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     private onCloseGeneralMenu = () => { | ||||
|         this.setState({generalMenuPosition: null}); | ||||
|     }; | ||||
| 
 | ||||
|     private onTagRoom = (ev: ButtonEvent, tagId: TagID) => { | ||||
|  | @ -190,7 +200,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|             action: 'leave_room', | ||||
|             room_id: this.props.room.roomId, | ||||
|         }); | ||||
|         this.setState({generalMenuDisplayed: false}); // hide the menu
 | ||||
|         this.setState({generalMenuPosition: null}); // hide the menu
 | ||||
|     }; | ||||
| 
 | ||||
|     private onOpenRoomSettings = (ev: ButtonEvent) => { | ||||
|  | @ -201,7 +211,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|             action: 'open_room_settings', | ||||
|             room_id: this.props.room.roomId, | ||||
|         }); | ||||
|         this.setState({generalMenuDisplayed: false}); // hide the menu
 | ||||
|         this.setState({generalMenuPosition: null}); // hide the menu
 | ||||
|     }; | ||||
| 
 | ||||
|     private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) { | ||||
|  | @ -218,10 +228,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|             console.error(error); | ||||
|         } | ||||
| 
 | ||||
|         // Close the context menu
 | ||||
|         this.setState({ | ||||
|             notificationsMenuDisplayed: false, | ||||
|         }); | ||||
|         this.setState({notificationsMenuPosition: null}); // Close the context menu
 | ||||
|     } | ||||
| 
 | ||||
|     private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES); | ||||
|  | @ -238,10 +245,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|         const state = getRoomNotifsState(this.props.room.roomId); | ||||
| 
 | ||||
|         let contextMenu = null; | ||||
|         if (this.state.notificationsMenuDisplayed) { | ||||
|             const elementRect = this.notificationsMenuButtonRef.current.getBoundingClientRect(); | ||||
|         if (this.state.notificationsMenuPosition) { | ||||
|             contextMenu = ( | ||||
|                 <ContextMenu {...contextMenuBelow(elementRect)} onFinished={this.onCloseNotificationsMenu}> | ||||
|                 <ContextMenu {...contextMenuBelow(this.state.notificationsMenuPosition)} onFinished={this.onCloseNotificationsMenu}> | ||||
|                     <div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"> | ||||
|                         <div className="mx_IconizedContextMenu_optionList"> | ||||
|                             <NotifOption | ||||
|  | @ -289,9 +295,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|                 <ContextMenuButton | ||||
|                     className={classes} | ||||
|                     onClick={this.onNotificationsMenuOpenClick} | ||||
|                     inputRef={this.notificationsMenuButtonRef} | ||||
|                     label={_t("Notification options")} | ||||
|                     isExpanded={this.state.notificationsMenuDisplayed} | ||||
|                     isExpanded={!!this.state.notificationsMenuPosition} | ||||
|                 /> | ||||
|                 {contextMenu} | ||||
|             </React.Fragment> | ||||
|  | @ -307,10 +312,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|         } | ||||
| 
 | ||||
|         let contextMenu = null; | ||||
|         if (this.state.generalMenuDisplayed) { | ||||
|             const elementRect = this.generalMenuButtonRef.current.getBoundingClientRect(); | ||||
|         if (this.state.generalMenuPosition) { | ||||
|             contextMenu = ( | ||||
|                 <ContextMenu {...contextMenuBelow(elementRect)} onFinished={this.onCloseGeneralMenu}> | ||||
|                 <ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}> | ||||
|                     <div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"> | ||||
|                         <div className="mx_IconizedContextMenu_optionList"> | ||||
|                             <AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}> | ||||
|  | @ -338,9 +342,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|                 <ContextMenuButton | ||||
|                     className="mx_RoomTile2_menuButton" | ||||
|                     onClick={this.onGeneralMenuOpenClick} | ||||
|                     inputRef={this.generalMenuButtonRef} | ||||
|                     label={_t("Room options")} | ||||
|                     isExpanded={this.state.generalMenuDisplayed} | ||||
|                     isExpanded={!!this.state.generalMenuPosition} | ||||
|                 /> | ||||
|                 {contextMenu} | ||||
|             </React.Fragment> | ||||
|  | @ -354,7 +357,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|         const classes = classNames({ | ||||
|             'mx_RoomTile2': true, | ||||
|             'mx_RoomTile2_selected': this.state.selected, | ||||
|             'mx_RoomTile2_hasMenuOpen': this.state.generalMenuDisplayed || this.state.notificationsMenuDisplayed, | ||||
|             'mx_RoomTile2_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition), | ||||
|             'mx_RoomTile2_minimized': this.props.isMinimized, | ||||
|         }); | ||||
| 
 | ||||
|  | @ -416,6 +419,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> { | |||
|                             onMouseLeave={this.onTileMouseLeave} | ||||
|                             onClick={this.onTileClick} | ||||
|                             role="treeitem" | ||||
|                             onContextMenu={this.onContextMenu} | ||||
|                         > | ||||
|                             <div className="mx_RoomTile2_avatarContainer"> | ||||
|                                 <RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize} /> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski