Add a context menu to rooms in the new room list

The tag options are not implemented out of concern for diff size. 

This splits the context menu classes out to a new "iconized" style which is common across a number of context menus, including the UserMenu.

Some of the badge/sublist styles had to change to better accommodate the menu icon lining up.

This also contains the framework required for https://github.com/vector-im/riot-web/issues/13961
This commit is contained in:
Travis Ralston 2020-06-09 23:09:15 -06:00
parent 7549d7d98a
commit 0755b4c148
10 changed files with 362 additions and 95 deletions

View file

@ -180,7 +180,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
top={elementRect.top + elementRect.height}
onFinished={this.onCloseMenu}
>
<div className="mx_UserMenuButton_contextMenu">
<div className="mx_IconizedContextMenu mx_UserMenuButton_contextMenu">
<div className="mx_UserMenuButton_contextMenu_header">
<div className="mx_UserMenuButton_contextMenu_name">
<span className="mx_UserMenuButton_contextMenu_displayName">
@ -203,7 +203,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
</div>
</div>
{hostingLink}
<div className="mx_UserMenuButton_contextMenu_optionList">
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
<ul>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
@ -237,7 +237,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
</li>
</ul>
</div>
<div className="mx_UserMenuButton_contextMenu_optionList">
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li>
<AccessibleButton onClick={this.onSignOutClick}>

View file

@ -27,6 +27,9 @@ import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import NotificationBadge, { INotificationState, NotificationColor, RoomNotificationState } from "./NotificationBadge";
import { _t } from "../../../languageHandler";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
/*******************************************************************
* CAUTION *
@ -49,10 +52,12 @@ interface IState {
hover: boolean;
notificationState: INotificationState;
selected: boolean;
generalMenuDisplayed: boolean;
}
export default class RoomTile2 extends React.Component<IProps, IState> {
private roomTile = createRef();
private roomTileRef: React.RefObject<HTMLDivElement> = createRef();
private generalMenuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
// TODO: Custom status
// TODO: Lock icon
@ -72,6 +77,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
hover: false,
notificationState: new RoomNotificationState(this.props.room),
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
generalMenuDisplayed: false,
};
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
@ -105,6 +111,124 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
this.setState({selected: isActive});
};
private onGeneralMenuOpenClick = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({generalMenuDisplayed: true});
};
private onCloseGeneralMenu = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({generalMenuDisplayed: false});
};
private onTagRoom = (ev: React.MouseEvent, tagId: TagID) => {
ev.preventDefault();
ev.stopPropagation();
if (tagId === DefaultTagID.DM) {
// TODO: DM Flagging
} else {
// TODO: XOR favourites and low priority
}
};
private onLeaveRoomClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'leave_room',
room_id: this.props.room.roomId,
});
this.setState({generalMenuDisplayed: false}); // hide the menu
};
private onOpenRoomSettings = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'open_room_settings',
room_id: this.props.room.roomId,
});
this.setState({generalMenuDisplayed: false}); // hide the menu
};
private renderGeneralMenu(): React.ReactElement {
let contextMenu = null;
if (this.state.generalMenuDisplayed) {
// The context menu appears within the list, so use the room tile as a reference point
const elementRect = this.roomTileRef.current.getBoundingClientRect();
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height + 8}
onFinished={this.onCloseGeneralMenu}
>
<div
className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"
style={{width: elementRect.width}}
>
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
<span>{_t("Favourite")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconArrowDown" />
<span>{_t("Low Priority")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.DM)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconUser" />
<span>{_t("Direct Chat")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onOpenRoomSettings}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
<span>{_t("Settings")}</span>
</AccessibleButton>
</li>
</ul>
</div>
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li className="mx_RoomTile2_contextMenu_redRow">
<AccessibleButton onClick={this.onLeaveRoomClick}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span>{_t("Leave Room")}</span>
</AccessibleButton>
</li>
</ul>
</div>
</div>
</ContextMenu>
);
}
return (
<React.Fragment>
<ContextMenuButton
className="mx_RoomTile2_menuButton"
onClick={this.onGeneralMenuOpenClick}
inputRef={this.generalMenuButtonRef}
label={_t("Room options")}
isExpanded={this.state.generalMenuDisplayed}
/>
{contextMenu}
</React.Fragment>
)
}
public render(): React.ReactElement {
// TODO: Collapsed state
// TODO: Invites
@ -114,6 +238,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,
});
const badge = <NotificationBadge notification={this.state.notificationState} allowNoCount={true} />;
@ -141,7 +266,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
const avatarSize = 32;
return (
<React.Fragment>
<RovingTabIndexWrapper inputRef={this.roomTile}>
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
@ -165,6 +290,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<div className="mx_RoomTile2_badgeContainer">
{badge}
</div>
{this.renderGeneralMenu()}
</AccessibleButton>
}
</RovingTabIndexWrapper>