mirror of https://github.com/vector-im/riot-web
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