mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 03:36:07 +03:00
Merge pull request #4742 from matrix-org/travis/room-list/hover-state
Add hover states and basic context menu to new room list
This commit is contained in:
commit
64a8767c5a
7 changed files with 238 additions and 23 deletions
|
@ -41,6 +41,36 @@ limitations under the License.
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// Both of these buttons are hidden by default until the list is hovered
|
||||
.mx_RoomSublist2_auxButton,
|
||||
.mx_RoomSublist2_menuButton {
|
||||
width: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_auxButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/plus.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_menuButton::before {
|
||||
mask-image: url('$(res)/img/feather-customised/more-horizontal.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_headerText {
|
||||
text-transform: uppercase;
|
||||
opacity: 0.5;
|
||||
|
@ -132,10 +162,82 @@ limitations under the License.
|
|||
}
|
||||
|
||||
// The aforementioned selector for the hover state.
|
||||
&:hover .react-resizable-handle {
|
||||
opacity: 0.2;
|
||||
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
||||
.react-resizable-handle {
|
||||
opacity: 0.2;
|
||||
|
||||
// Update the render() function for RoomSublist2 if this changes
|
||||
border: 2px solid $primary-fg-color;
|
||||
// Update the render() function for RoomSublist2 if this changes
|
||||
border: 2px solid $primary-fg-color;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_headerContainer {
|
||||
// If the header doesn't have an aux button we still need to hide the badge for
|
||||
// the menu button.
|
||||
.mx_RoomSublist2_badgeContainer {
|
||||
// Completely hide the badge
|
||||
width: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||
// The menu button will be the rightmost button, so make it correctly aligned.
|
||||
.mx_RoomSublist2_menuButton {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// Both of these buttons have circled backgrounds and are visible at this point,
|
||||
// so make them so.
|
||||
.mx_RoomSublist2_auxButton,
|
||||
.mx_RoomSublist2_menuButton {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 32px;
|
||||
margin-left: 16px;
|
||||
background-color: #fff; // TODO: Variable and theme
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We have a hover style on the room list with no specific list hovered, so account for that
|
||||
.mx_RoomList2:hover .mx_RoomSublist2,
|
||||
.mx_RoomSublist2_hasMenuOpen {
|
||||
.mx_RoomSublist2_headerContainer_withAux {
|
||||
.mx_RoomSublist2_badgeContainer {
|
||||
// Completely hide the badge
|
||||
width: 0;
|
||||
margin: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_auxButton {
|
||||
// Show the aux button, but not the list button
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_contextMenu {
|
||||
padding: 20px 16px;
|
||||
width: 250px;
|
||||
|
||||
hr {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-right: 16px; // additional 16px
|
||||
border: 1px solid $roomsublist2-divider-color;
|
||||
}
|
||||
|
||||
.mx_RoomSublist2_contextMenu_title {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ limitations under the License.
|
|||
|
||||
.mx_RoomTile2_name {
|
||||
font-size: $font-14px;
|
||||
line-height: $font-19px;
|
||||
line-height: $font-18px;
|
||||
}
|
||||
|
||||
.mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents {
|
||||
|
@ -63,6 +63,10 @@ limitations under the License.
|
|||
line-height: $font-18px;
|
||||
color: $roomtile2-preview-color;
|
||||
}
|
||||
|
||||
.mx_RoomTile2_nameWithPreview {
|
||||
margin-top: -4px; // shift the name up a bit more
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomTile2_badgeContainer {
|
||||
|
|
|
@ -178,6 +178,7 @@ $roomtile2-preview-color: #9e9e9e;
|
|||
$roomtile2-badge-color: #61708b;
|
||||
$roomtile2-selected-bg-color: #FFF;
|
||||
$theme-button-bg-color: #e3e8f0;
|
||||
$roomsublist2-divider-color: #e9eaeb;
|
||||
|
||||
$roomtile-name-color: #61708b;
|
||||
$roomtile-badge-fg-color: $accent-fg-color;
|
||||
|
|
|
@ -200,7 +200,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
addRoomLabel={aesthetics.addRoomLabel}
|
||||
isInvite={aesthetics.isInvite}
|
||||
layout={this.state.layouts.get(orderedTagId)}
|
||||
showMessagePreviews={orderedTagId === DefaultTagID.DM}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import RoomTile2 from "./RoomTile2";
|
|||
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||
import NotificationBadge, { ListNotificationState } from "./NotificationBadge";
|
||||
import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
|
@ -41,7 +43,6 @@ interface IProps {
|
|||
rooms?: Room[];
|
||||
startAsHidden: boolean;
|
||||
label: string;
|
||||
showMessagePreviews: boolean;
|
||||
onAddRoom?: () => void;
|
||||
addRoomLabel: string;
|
||||
isInvite: boolean;
|
||||
|
@ -57,16 +58,19 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
notificationState: ListNotificationState;
|
||||
menuDisplayed: 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),
|
||||
menuDisplayed: false,
|
||||
};
|
||||
this.state.notificationState.setRooms(this.props.rooms);
|
||||
}
|
||||
|
@ -97,6 +101,26 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
private onOpenMenuClick = (ev: InputEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({menuDisplayed: true});
|
||||
};
|
||||
|
||||
private onCloseMenu = () => {
|
||||
this.setState({menuDisplayed: false});
|
||||
};
|
||||
|
||||
private onUnreadFirstChanged = () => {
|
||||
// TODO: Support per-list algorithm changes
|
||||
console.log("Unread first changed");
|
||||
};
|
||||
|
||||
private onMessagePreviewChanged = () => {
|
||||
this.props.layout.showPreviews = !this.props.layout.showPreviews;
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
private renderTiles(): React.ReactElement[] {
|
||||
const tiles: React.ReactElement[] = [];
|
||||
|
||||
|
@ -106,7 +130,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
<RoomTile2
|
||||
room={room}
|
||||
key={`room-${room.roomId}`}
|
||||
showMessagePreview={this.props.showMessagePreviews}
|
||||
showMessagePreview={this.props.layout.showPreviews}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -115,6 +139,61 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
return tiles;
|
||||
}
|
||||
|
||||
private renderMenu(): React.ReactElement {
|
||||
let contextMenu = null;
|
||||
if (this.state.menuDisplayed) {
|
||||
const elementRect = this.menuButtonRef.current.getBoundingClientRect();
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
left={elementRect.left}
|
||||
top={elementRect.top + elementRect.height}
|
||||
onFinished={this.onCloseMenu}
|
||||
>
|
||||
<div className="mx_RoomSublist2_contextMenu">
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
|
||||
TODO: Radios are blocked by https://github.com/matrix-org/matrix-react-sdk/pull/4731
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
||||
<StyledCheckbox
|
||||
onChange={this.onUnreadFirstChanged}
|
||||
checked={false/*TODO*/}
|
||||
>
|
||||
{_t("Always show first")}
|
||||
</StyledCheckbox>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
||||
<StyledCheckbox
|
||||
onChange={this.onMessagePreviewChanged}
|
||||
checked={this.props.layout.showPreviews}
|
||||
>
|
||||
{_t("Message preview")}
|
||||
</StyledCheckbox>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuButton
|
||||
className="mx_RoomSublist2_menuButton"
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.menuButtonRef}
|
||||
label={_t("List options")}
|
||||
isExpanded={this.state.menuDisplayed}
|
||||
/>
|
||||
{contextMenu}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private renderHeader(): React.ReactElement {
|
||||
// TODO: Title on collapsed
|
||||
// TODO: Incoming call box
|
||||
|
@ -129,22 +208,26 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
const badge = <NotificationBadge allowNoCount={false} notification={this.state.notificationState}/>;
|
||||
|
||||
// TODO: Aux button
|
||||
// let addRoomButton = null;
|
||||
// if (!!this.props.onAddRoom) {
|
||||
// addRoomButton = (
|
||||
// <AccessibleTooltipButton
|
||||
// tabIndex={tabIndex}
|
||||
// onClick={this.onAddRoom}
|
||||
// className="mx_RoomSublist2_addButton"
|
||||
// title={this.props.addRoomLabel || _t("Add room")}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
let addRoomButton = null;
|
||||
if (!!this.props.onAddRoom) {
|
||||
addRoomButton = (
|
||||
<AccessibleButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={this.onAddRoom}
|
||||
className="mx_RoomSublist2_auxButton"
|
||||
aria-label={this.props.addRoomLabel || _t("Add room")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_RoomSublist2_headerContainer': true,
|
||||
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
|
||||
});
|
||||
|
||||
// TODO: a11y (see old component)
|
||||
return (
|
||||
<div className={"mx_RoomSublist2_headerContainer"}>
|
||||
<div className={classes}>
|
||||
<AccessibleButton
|
||||
inputRef={ref}
|
||||
tabIndex={tabIndex}
|
||||
|
@ -157,6 +240,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
<div className="mx_RoomSublist2_badgeContainer">
|
||||
{badge}
|
||||
</div>
|
||||
{this.renderMenu()}
|
||||
{addRoomButton}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
@ -174,6 +259,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
// TODO: Proper collapse support
|
||||
'mx_RoomSublist2': true,
|
||||
'mx_RoomSublist2_collapsed': false, // len && isCollapsed
|
||||
'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
let content = null;
|
||||
|
|
|
@ -1135,6 +1135,13 @@
|
|||
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
|
||||
"Not now": "Not now",
|
||||
"Don't ask me again": "Don't ask me again",
|
||||
"Sort by": "Sort by",
|
||||
"Unread rooms": "Unread rooms",
|
||||
"Always show first": "Always show first",
|
||||
"Show": "Show",
|
||||
"Message preview": "Message preview",
|
||||
"List options": "List options",
|
||||
"Add room": "Add room",
|
||||
"Show %(count)s more|other": "Show %(count)s more",
|
||||
"Show %(count)s more|one": "Show %(count)s more",
|
||||
"Options": "Options",
|
||||
|
@ -2019,7 +2026,6 @@
|
|||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||
"Jump to first unread room.": "Jump to first unread room.",
|
||||
"Jump to first invite.": "Jump to first invite.",
|
||||
"Add room": "Add room",
|
||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||
"Search failed": "Search failed",
|
||||
|
|
|
@ -20,10 +20,12 @@ const TILE_HEIGHT_PX = 44;
|
|||
|
||||
interface ISerializedListLayout {
|
||||
numTiles: number;
|
||||
showPreviews: boolean;
|
||||
}
|
||||
|
||||
export class ListLayout {
|
||||
private _n = 0;
|
||||
private _previews = false;
|
||||
|
||||
constructor(public readonly tagId: TagID) {
|
||||
const serialized = localStorage.getItem(this.key);
|
||||
|
@ -31,9 +33,19 @@ export class ListLayout {
|
|||
// We don't use the setters as they cause writes.
|
||||
const parsed = <ISerializedListLayout>JSON.parse(serialized);
|
||||
this._n = parsed.numTiles;
|
||||
this._previews = parsed.showPreviews;
|
||||
}
|
||||
}
|
||||
|
||||
public get showPreviews(): boolean {
|
||||
return this._previews;
|
||||
}
|
||||
|
||||
public set showPreviews(v: boolean) {
|
||||
this._previews = v;
|
||||
this.save();
|
||||
}
|
||||
|
||||
public get tileHeight(): number {
|
||||
return TILE_HEIGHT_PX;
|
||||
}
|
||||
|
@ -48,7 +60,7 @@ export class ListLayout {
|
|||
|
||||
public set visibleTiles(v: number) {
|
||||
this._n = v;
|
||||
localStorage.setItem(this.key, JSON.stringify(this.serialize()));
|
||||
this.save();
|
||||
}
|
||||
|
||||
public get minVisibleTiles(): number {
|
||||
|
@ -80,9 +92,14 @@ export class ListLayout {
|
|||
return px / this.tileHeight;
|
||||
}
|
||||
|
||||
private save() {
|
||||
localStorage.setItem(this.key, JSON.stringify(this.serialize()));
|
||||
}
|
||||
|
||||
private serialize(): ISerializedListLayout {
|
||||
return {
|
||||
numTiles: this.visibleTiles,
|
||||
showPreviews: this.showPreviews,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue