Make the whole UserMenu a button to open the menu

This commit is contained in:
Travis Ralston 2020-06-25 19:54:17 -06:00
parent bcfdd4d984
commit 411271422c
3 changed files with 193 additions and 186 deletions

View file

@ -132,16 +132,6 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
.mx_LeftPanel2_roomListContainer { .mx_LeftPanel2_roomListContainer {
width: 68px; width: 68px;
.mx_LeftPanel2_userHeader {
.mx_LeftPanel2_headerRow {
justify-content: center;
}
.mx_LeftPanel2_userAvatarContainer {
margin-right: 0;
}
}
.mx_LeftPanel2_filterContainer { .mx_LeftPanel2_filterContainer {
// Organize the flexbox into a centered column layout // Organize the flexbox into a centered column layout
flex-direction: column; flex-direction: column;

View file

@ -15,39 +15,7 @@ limitations under the License.
*/ */
.mx_UserMenu { .mx_UserMenu {
// Create a row-based flexbox to ensure items stay aligned correctly.
display: flex;
align-items: center;
.mx_UserMenu_userAvatarContainer {
position: relative; // to make default avatars work
margin-right: 8px;
height: 32px; // to remove the unknown 4px gap the browser puts below it
.mx_UserMenu_userAvatar {
border-radius: 32px; // should match avatar size
}
}
.mx_UserMenu_userName {
font-weight: 600;
font-size: $font-15px;
line-height: $font-20px;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_UserMenu_headerButtons { .mx_UserMenu_headerButtons {
// No special styles: the rest of the layout happens to make it work.
}
}
.mx_UserMenuButton {
> span {
width: 16px; width: 16px;
height: 16px; height: 16px;
position: relative; position: relative;
@ -67,6 +35,50 @@ limitations under the License.
mask-image: url('$(res)/img/feather-customised/more-horizontal.svg'); mask-image: url('$(res)/img/feather-customised/more-horizontal.svg');
} }
} }
.mx_UserMenu_row {
// Create a row-based flexbox to ensure items stay aligned correctly.
display: flex;
align-items: center;
.mx_UserMenu_userAvatarContainer {
position: relative; // to make default avatars work
margin-right: 8px;
height: 32px; // to remove the unknown 4px gap the browser puts below it
.mx_UserMenu_userAvatar {
border-radius: 32px; // should match avatar size
}
}
.mx_UserMenu_userName {
font-weight: 600;
font-size: $font-15px;
line-height: $font-20px;
flex: 1;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_UserMenu_headerButtons {
// No special styles: the rest of the layout happens to make it work.
}
}
&.mx_UserMenu_minimized {
.mx_UserMenu_userHeader {
.mx_UserMenu_row {
justify-content: center;
}
.mx_UserMenu_userAvatarContainer {
margin-right: 0;
}
}
}
} }
.mx_UserMenu_contextMenu { .mx_UserMenu_contextMenu {

View file

@ -36,6 +36,7 @@ import {getHomePageUrl} from "../../utils/pages";
import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import BaseAvatar from '../views/avatars/BaseAvatar'; import BaseAvatar from '../views/avatars/BaseAvatar';
import classNames from "classnames";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -108,7 +109,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
this.setState({menuDisplayed: true}); this.setState({menuDisplayed: true});
}; };
private onCloseMenu = () => { private onCloseMenu = (ev: InputEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({menuDisplayed: false}); this.setState({menuDisplayed: false});
}; };
@ -160,147 +163,132 @@ export default class UserMenu extends React.Component<IProps, IState> {
defaultDispatcher.dispatch({action: 'view_home_page'}); defaultDispatcher.dispatch({action: 'view_home_page'});
}; };
private renderMenuButton(): React.ReactNode { private renderContextMenu = (): React.ReactNode => {
let contextMenu; if (!this.state.menuDisplayed) return null;
if (this.state.menuDisplayed) {
let hostingLink;
const signupLink = getHostingLink("user-context-menu");
if (signupLink) {
hostingLink = (
<div className="mx_UserMenu_contextMenu_header">
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub => (
<a
href={signupLink}
target="_blank"
rel="noreferrer noopener"
tabIndex={-1}
>{sub}</a>
),
},
)}
</div>
);
}
let homeButton = null; let hostingLink;
if (this.hasHomePage) { const signupLink = getHostingLink("user-context-menu");
homeButton = ( if (signupLink) {
<li> hostingLink = (
<AccessibleButton onClick={this.onHomeClick}> <div className="mx_UserMenu_contextMenu_header">
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconHome" /> {_t(
<span>{_t("Home")}</span> "<a>Upgrade</a> to your own domain", {},
</AccessibleButton> {
</li> a: sub => (
); <a
} href={signupLink}
target="_blank"
const elementRect = this.buttonRef.current.getBoundingClientRect(); rel="noreferrer noopener"
contextMenu = ( tabIndex={-1}
<ContextMenu >{sub}</a>
chevronFace="none" ),
left={elementRect.left} },
top={elementRect.top + elementRect.height} )}
onFinished={this.onCloseMenu} </div>
>
<div className="mx_IconizedContextMenu mx_UserMenu_contextMenu">
<div className="mx_UserMenu_contextMenu_header">
<div className="mx_UserMenu_contextMenu_name">
<span className="mx_UserMenu_contextMenu_displayName">
{OwnProfileStore.instance.displayName}
</span>
<span className="mx_UserMenu_contextMenu_userId">
{MatrixClientPeg.get().getUserId()}
</span>
</div>
<div
className="mx_UserMenu_contextMenu_themeButton"
onClick={this.onSwitchThemeClick}
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
>
<img
src={require("../../../res/img/feather-customised/sun.svg")}
alt={_t("Switch theme")}
width={16}
/>
</div>
</div>
{hostingLink}
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
<ul>
{homeButton}
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconBell" />
<span>{_t("Notification settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconLock" />
<span>{_t("Security & privacy")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSettings" />
<span>{_t("All settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onShowArchived}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconArchive" />
<span>{_t("Archived rooms")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onProvideFeedback}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconMessage" />
<span>{_t("Feedback")}</span>
</AccessibleButton>
</li>
</ul>
</div>
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li className="mx_UserMenu_contextMenu_redRow">
<AccessibleButton onClick={this.onSignOutClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSignOut" />
<span>{_t("Sign out")}</span>
</AccessibleButton>
</li>
</ul>
</div>
</div>
</ContextMenu>
); );
} }
let homeButton = null;
if (this.hasHomePage) {
homeButton = (
<li>
<AccessibleButton onClick={this.onHomeClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconHome" />
<span>{_t("Home")}</span>
</AccessibleButton>
</li>
);
}
const elementRect = this.buttonRef.current.getBoundingClientRect();
return ( return (
<React.Fragment> <ContextMenu
<ContextMenuButton chevronFace="none"
className="mx_UserMenuButton" left={elementRect.left}
onClick={this.onOpenMenuClick} top={elementRect.top + elementRect.height}
inputRef={this.buttonRef} onFinished={this.onCloseMenu}
label={_t("Account settings")} >
isExpanded={this.state.menuDisplayed} <div className="mx_IconizedContextMenu mx_UserMenu_contextMenu">
> <div className="mx_UserMenu_contextMenu_header">
<span>{/* masked image in CSS */}</span> <div className="mx_UserMenu_contextMenu_name">
</ContextMenuButton> <span className="mx_UserMenu_contextMenu_displayName">
{contextMenu} {OwnProfileStore.instance.displayName}
</React.Fragment> </span>
<span className="mx_UserMenu_contextMenu_userId">
{MatrixClientPeg.get().getUserId()}
</span>
</div>
<div
className="mx_UserMenu_contextMenu_themeButton"
onClick={this.onSwitchThemeClick}
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
>
<img
src={require("../../../res/img/feather-customised/sun.svg")}
alt={_t("Switch theme")}
width={16}
/>
</div>
</div>
{hostingLink}
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
<ul>
{homeButton}
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconBell" />
<span>{_t("Notification settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconLock" />
<span>{_t("Security & privacy")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onSettingsOpen(e, null)}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSettings" />
<span>{_t("All settings")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onShowArchived}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconArchive" />
<span>{_t("Archived rooms")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onProvideFeedback}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconMessage" />
<span>{_t("Feedback")}</span>
</AccessibleButton>
</li>
</ul>
</div>
<div className="mx_IconizedContextMenu_optionList">
<ul>
<li className="mx_UserMenu_contextMenu_redRow">
<AccessibleButton onClick={this.onSignOutClick}>
<span className="mx_IconizedContextMenu_icon mx_UserMenu_iconSignOut" />
<span>{_t("Sign out")}</span>
</AccessibleButton>
</li>
</ul>
</div>
</div>
</ContextMenu>
); );
} };
public render() { public render() {
console.log(this.state);
const avatarSize = 32; // should match border-radius of the avatar const avatarSize = 32; // should match border-radius of the avatar
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>; let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
let buttons = ( let buttons = (
<span className="mx_UserMenu_headerButtons"> <span className="mx_UserMenu_headerButtons">
{this.renderMenuButton()} {/* masked image in CSS */}
</span> </span>
); );
if (this.props.isMinimized) { if (this.props.isMinimized) {
@ -308,22 +296,39 @@ export default class UserMenu extends React.Component<IProps, IState> {
buttons = null; buttons = null;
} }
const classes = classNames({
'mx_UserMenu': true,
'mx_UserMenu_minimized': this.props.isMinimized,
});
return ( return (
<div className="mx_UserMenu"> <React.Fragment>
<span className="mx_UserMenu_userAvatarContainer"> <ContextMenuButton
<BaseAvatar className={classes}
idName={MatrixClientPeg.get().getUserId()} onClick={this.onOpenMenuClick}
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()} inputRef={this.buttonRef}
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)} label={_t("Account settings")}
width={avatarSize} isExpanded={this.state.menuDisplayed}
height={avatarSize} >
resizeMethod="crop" <div className="mx_UserMenu_row">
className="mx_UserMenu_userAvatar" <span className="mx_UserMenu_userAvatarContainer">
/> <BaseAvatar
</span> idName={MatrixClientPeg.get().getUserId()}
{name} name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
{buttons} url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
</div> width={avatarSize}
height={avatarSize}
resizeMethod="crop"
className="mx_UserMenu_userAvatar"
/>
</span>
{name}
{buttons}
</div>
{this.renderContextMenu()}
</ContextMenuButton>
</React.Fragment>
); );
} }
} }