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:
Travis Ralston 2020-06-10 07:43:31 -06:00 committed by GitHub
commit 64a8767c5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 238 additions and 23 deletions

View File

@ -41,6 +41,36 @@ limitations under the License.
justify-content: flex-end; 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 { .mx_RoomSublist2_headerText {
text-transform: uppercase; text-transform: uppercase;
opacity: 0.5; opacity: 0.5;
@ -132,10 +162,82 @@ limitations under the License.
} }
// The aforementioned selector for the hover state. // The aforementioned selector for the hover state.
&:hover .react-resizable-handle { &:hover, &.mx_RoomSublist2_hasMenuOpen {
opacity: 0.2; .react-resizable-handle {
opacity: 0.2;
// Update the render() function for RoomSublist2 if this changes // Update the render() function for RoomSublist2 if this changes
border: 2px solid $primary-fg-color; 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;
} }
} }

View File

@ -51,7 +51,7 @@ limitations under the License.
.mx_RoomTile2_name { .mx_RoomTile2_name {
font-size: $font-14px; font-size: $font-14px;
line-height: $font-19px; line-height: $font-18px;
} }
.mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents { .mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents {
@ -63,6 +63,10 @@ limitations under the License.
line-height: $font-18px; line-height: $font-18px;
color: $roomtile2-preview-color; color: $roomtile2-preview-color;
} }
.mx_RoomTile2_nameWithPreview {
margin-top: -4px; // shift the name up a bit more
}
} }
.mx_RoomTile2_badgeContainer { .mx_RoomTile2_badgeContainer {

View File

@ -178,6 +178,7 @@ $roomtile2-preview-color: #9e9e9e;
$roomtile2-badge-color: #61708b; $roomtile2-badge-color: #61708b;
$roomtile2-selected-bg-color: #FFF; $roomtile2-selected-bg-color: #FFF;
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$roomsublist2-divider-color: #e9eaeb;
$roomtile-name-color: #61708b; $roomtile-name-color: #61708b;
$roomtile-badge-fg-color: $accent-fg-color; $roomtile-badge-fg-color: $accent-fg-color;

View File

@ -200,7 +200,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
addRoomLabel={aesthetics.addRoomLabel} addRoomLabel={aesthetics.addRoomLabel}
isInvite={aesthetics.isInvite} isInvite={aesthetics.isInvite}
layout={this.state.layouts.get(orderedTagId)} layout={this.state.layouts.get(orderedTagId)}
showMessagePreviews={orderedTagId === DefaultTagID.DM}
/> />
); );
} }

View File

@ -27,6 +27,8 @@ import RoomTile2 from "./RoomTile2";
import { ResizableBox, ResizeCallbackData } from "react-resizable"; import { ResizableBox, ResizeCallbackData } from "react-resizable";
import { ListLayout } from "../../../stores/room-list/ListLayout"; import { ListLayout } from "../../../stores/room-list/ListLayout";
import NotificationBadge, { ListNotificationState } from "./NotificationBadge"; import NotificationBadge, { ListNotificationState } from "./NotificationBadge";
import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import StyledCheckbox from "../elements/StyledCheckbox";
/******************************************************************* /*******************************************************************
* CAUTION * * CAUTION *
@ -41,7 +43,6 @@ interface IProps {
rooms?: Room[]; rooms?: Room[];
startAsHidden: boolean; startAsHidden: boolean;
label: string; label: string;
showMessagePreviews: boolean;
onAddRoom?: () => void; onAddRoom?: () => void;
addRoomLabel: string; addRoomLabel: string;
isInvite: boolean; isInvite: boolean;
@ -57,16 +58,19 @@ interface IProps {
interface IState { interface IState {
notificationState: ListNotificationState; notificationState: ListNotificationState;
menuDisplayed: boolean;
} }
export default class RoomSublist2 extends React.Component<IProps, IState> { export default class RoomSublist2 extends React.Component<IProps, IState> {
private headerButton = createRef(); private headerButton = createRef();
private menuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
notificationState: new ListNotificationState(this.props.isInvite), notificationState: new ListNotificationState(this.props.isInvite),
menuDisplayed: false,
}; };
this.state.notificationState.setRooms(this.props.rooms); 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 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[] { private renderTiles(): React.ReactElement[] {
const tiles: React.ReactElement[] = []; const tiles: React.ReactElement[] = [];
@ -106,7 +130,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<RoomTile2 <RoomTile2
room={room} room={room}
key={`room-${room.roomId}`} 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; 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 { private renderHeader(): React.ReactElement {
// TODO: Title on collapsed // TODO: Title on collapsed
// TODO: Incoming call box // 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}/>; const badge = <NotificationBadge allowNoCount={false} notification={this.state.notificationState}/>;
// TODO: Aux button let addRoomButton = null;
// let addRoomButton = null; if (!!this.props.onAddRoom) {
// if (!!this.props.onAddRoom) { addRoomButton = (
// addRoomButton = ( <AccessibleButton
// <AccessibleTooltipButton tabIndex={tabIndex}
// tabIndex={tabIndex} onClick={this.onAddRoom}
// onClick={this.onAddRoom} className="mx_RoomSublist2_auxButton"
// className="mx_RoomSublist2_addButton" aria-label={this.props.addRoomLabel || _t("Add room")}
// title={this.props.addRoomLabel || _t("Add room")} />
// /> );
// ); }
// }
const classes = classNames({
'mx_RoomSublist2_headerContainer': true,
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
});
// TODO: a11y (see old component) // TODO: a11y (see old component)
return ( return (
<div className={"mx_RoomSublist2_headerContainer"}> <div className={classes}>
<AccessibleButton <AccessibleButton
inputRef={ref} inputRef={ref}
tabIndex={tabIndex} tabIndex={tabIndex}
@ -157,6 +240,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<div className="mx_RoomSublist2_badgeContainer"> <div className="mx_RoomSublist2_badgeContainer">
{badge} {badge}
</div> </div>
{this.renderMenu()}
{addRoomButton}
</div> </div>
); );
}} }}
@ -174,6 +259,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// TODO: Proper collapse support // TODO: Proper collapse support
'mx_RoomSublist2': true, 'mx_RoomSublist2': true,
'mx_RoomSublist2_collapsed': false, // len && isCollapsed 'mx_RoomSublist2_collapsed': false, // len && isCollapsed
'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed,
}); });
let content = null; let content = null;

View File

@ -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>", "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", "Not now": "Not now",
"Don't ask me again": "Don't ask me again", "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|other": "Show %(count)s more",
"Show %(count)s more|one": "Show %(count)s more", "Show %(count)s more|one": "Show %(count)s more",
"Options": "Options", "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>?", "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 unread room.": "Jump to first unread room.",
"Jump to first invite.": "Jump to first invite.", "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 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?", "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", "Search failed": "Search failed",

View File

@ -20,10 +20,12 @@ const TILE_HEIGHT_PX = 44;
interface ISerializedListLayout { interface ISerializedListLayout {
numTiles: number; numTiles: number;
showPreviews: boolean;
} }
export class ListLayout { export class ListLayout {
private _n = 0; private _n = 0;
private _previews = false;
constructor(public readonly tagId: TagID) { constructor(public readonly tagId: TagID) {
const serialized = localStorage.getItem(this.key); const serialized = localStorage.getItem(this.key);
@ -31,9 +33,19 @@ export class ListLayout {
// We don't use the setters as they cause writes. // We don't use the setters as they cause writes.
const parsed = <ISerializedListLayout>JSON.parse(serialized); const parsed = <ISerializedListLayout>JSON.parse(serialized);
this._n = parsed.numTiles; 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 { public get tileHeight(): number {
return TILE_HEIGHT_PX; return TILE_HEIGHT_PX;
} }
@ -48,7 +60,7 @@ export class ListLayout {
public set visibleTiles(v: number) { public set visibleTiles(v: number) {
this._n = v; this._n = v;
localStorage.setItem(this.key, JSON.stringify(this.serialize())); this.save();
} }
public get minVisibleTiles(): number { public get minVisibleTiles(): number {
@ -80,9 +92,14 @@ export class ListLayout {
return px / this.tileHeight; return px / this.tileHeight;
} }
private save() {
localStorage.setItem(this.key, JSON.stringify(this.serialize()));
}
private serialize(): ISerializedListLayout { private serialize(): ISerializedListLayout {
return { return {
numTiles: this.visibleTiles, numTiles: this.visibleTiles,
showPreviews: this.showPreviews,
}; };
} }
} }