Merge branch 'develop' into element

This commit is contained in:
Bruno Windels 2020-07-08 15:50:17 +02:00
commit 7dad56ca86
40 changed files with 1326 additions and 482 deletions

View file

@ -16,13 +16,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useRef, useState} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import React, {CSSProperties, useRef, useState} from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
import {Key} from "../../Keyboard";
import * as sdk from "../../index";
import AccessibleButton from "../views/elements/AccessibleButton";
import {Writeable} from "../../@types/common";
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@ -30,8 +29,8 @@ import AccessibleButton from "../views/elements/AccessibleButton";
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
function getOrCreateContainer() {
let container = document.getElementById(ContextualMenuContainerId);
function getOrCreateContainer(): HTMLDivElement {
let container = document.getElementById(ContextualMenuContainerId) as HTMLDivElement;
if (!container) {
container = document.createElement("div");
@ -43,50 +42,70 @@ function getOrCreateContainer() {
}
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
interface IPosition {
top?: number;
bottom?: number;
left?: number;
right?: number;
}
export enum ChevronFace {
Top = "top",
Bottom = "bottom",
Left = "left",
Right = "right",
None = "none",
}
interface IProps extends IPosition {
menuWidth?: number;
menuHeight?: number;
chevronOffset?: number;
chevronFace?: ChevronFace;
menuPaddingTop?: number;
menuPaddingBottom?: number;
menuPaddingLeft?: number;
menuPaddingRight?: number;
zIndex?: number;
// If true, insert an invisible screen-sized element behind the menu that when clicked will close it.
hasBackground?: boolean;
// whether this context menu should be focus managed. If false it must handle itself
managed?: boolean;
// Function to be called on menu close
onFinished();
// on resize callback
windowResize?();
}
interface IState {
contextMenuElem: HTMLDivElement;
}
// Generic ContextMenu Portal wrapper
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
export class ContextMenu extends React.Component {
static propTypes = {
top: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
menuWidth: PropTypes.number,
menuHeight: PropTypes.number,
chevronOffset: PropTypes.number,
chevronFace: PropTypes.string, // top, bottom, left, right or none
// Function to be called on menu close
onFinished: PropTypes.func.isRequired,
menuPaddingTop: PropTypes.number,
menuPaddingRight: PropTypes.number,
menuPaddingBottom: PropTypes.number,
menuPaddingLeft: PropTypes.number,
zIndex: PropTypes.number,
// If true, insert an invisible screen-sized element behind the
// menu that when clicked will close it.
hasBackground: PropTypes.bool,
// on resize callback
windowResize: PropTypes.func,
managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself
};
export class ContextMenu extends React.PureComponent<IProps, IState> {
private initialFocus: HTMLElement;
static defaultProps = {
hasBackground: true,
managed: true,
};
constructor() {
super();
constructor(props, context) {
super(props, context);
this.state = {
contextMenuElem: null,
};
// persist what had focus when we got initialized so we can return it after
this.initialFocus = document.activeElement;
this.initialFocus = document.activeElement as HTMLElement;
}
componentWillUnmount() {
@ -94,7 +113,7 @@ export class ContextMenu extends React.Component {
this.initialFocus.focus();
}
collectContextMenuRect = (element) => {
private collectContextMenuRect = (element) => {
// We don't need to clean up when unmounting, so ignore
if (!element) return;
@ -111,7 +130,7 @@ export class ContextMenu extends React.Component {
});
};
onContextMenu = (e) => {
private onContextMenu = (e) => {
if (this.props.onFinished) {
this.props.onFinished();
@ -134,20 +153,20 @@ export class ContextMenu extends React.Component {
}
};
onContextMenuPreventBubbling = (e) => {
private onContextMenuPreventBubbling = (e) => {
// stop propagation so that any context menu handlers don't leak out of this context menu
// but do not inhibit the default browser menu
e.stopPropagation();
};
// Prevent clicks on the background from going through to the component which opened the menu.
_onFinished = (ev: InputEvent) => {
private onFinished = (ev: React.MouseEvent) => {
ev.stopPropagation();
ev.preventDefault();
if (this.props.onFinished) this.props.onFinished();
};
_onMoveFocus = (element, up) => {
private onMoveFocus = (element: Element, up: boolean) => {
let descending = false; // are we currently descending or ascending through the DOM tree?
do {
@ -181,25 +200,25 @@ export class ContextMenu extends React.Component {
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
if (element) {
element.focus();
(element as HTMLElement).focus();
}
};
_onMoveFocusHomeEnd = (element, up) => {
private onMoveFocusHomeEnd = (element: Element, up: boolean) => {
let results = element.querySelectorAll('[role^="menuitem"]');
if (!results) {
results = element.querySelectorAll('[tab-index]');
}
if (results && results.length) {
if (up) {
results[0].focus();
(results[0] as HTMLElement).focus();
} else {
results[results.length - 1].focus();
(results[results.length - 1] as HTMLElement).focus();
}
}
};
_onKeyDown = (ev) => {
private onKeyDown = (ev: React.KeyboardEvent) => {
if (!this.props.managed) {
if (ev.key === Key.ESCAPE) {
this.props.onFinished();
@ -217,16 +236,16 @@ export class ContextMenu extends React.Component {
this.props.onFinished();
break;
case Key.ARROW_UP:
this._onMoveFocus(ev.target, true);
this.onMoveFocus(ev.target as Element, true);
break;
case Key.ARROW_DOWN:
this._onMoveFocus(ev.target, false);
this.onMoveFocus(ev.target as Element, false);
break;
case Key.HOME:
this._onMoveFocusHomeEnd(this.state.contextMenuElem, true);
this.onMoveFocusHomeEnd(this.state.contextMenuElem, true);
break;
case Key.END:
this._onMoveFocusHomeEnd(this.state.contextMenuElem, false);
this.onMoveFocusHomeEnd(this.state.contextMenuElem, false);
break;
default:
handled = false;
@ -239,9 +258,8 @@ export class ContextMenu extends React.Component {
}
};
renderMenu(hasBackground=this.props.hasBackground) {
const position = {};
let chevronFace = null;
protected renderMenu(hasBackground = this.props.hasBackground) {
const position: Partial<Writeable<DOMRect>> = {};
const props = this.props;
if (props.top) {
@ -250,23 +268,24 @@ export class ContextMenu extends React.Component {
position.bottom = props.bottom;
}
let chevronFace: ChevronFace;
if (props.left) {
position.left = props.left;
chevronFace = 'left';
chevronFace = ChevronFace.Left;
} else {
position.right = props.right;
chevronFace = 'right';
chevronFace = ChevronFace.Right;
}
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
const chevronOffset = {};
const chevronOffset: CSSProperties = {};
if (props.chevronFace) {
chevronFace = props.chevronFace;
}
const hasChevron = chevronFace && chevronFace !== "none";
const hasChevron = chevronFace && chevronFace !== ChevronFace.None;
if (chevronFace === 'top' || chevronFace === 'bottom') {
if (chevronFace === ChevronFace.Top || chevronFace === ChevronFace.Bottom) {
chevronOffset.left = props.chevronOffset;
} else if (position.top !== undefined) {
const target = position.top;
@ -296,13 +315,13 @@ export class ContextMenu extends React.Component {
'mx_ContextualMenu_right': !hasChevron && position.right,
'mx_ContextualMenu_top': !hasChevron && position.top,
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
'mx_ContextualMenu_withChevron_left': chevronFace === 'left',
'mx_ContextualMenu_withChevron_right': chevronFace === 'right',
'mx_ContextualMenu_withChevron_top': chevronFace === 'top',
'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom',
'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left,
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom,
});
const menuStyle = {};
const menuStyle: CSSProperties = {};
if (props.menuWidth) {
menuStyle.width = props.menuWidth;
}
@ -333,13 +352,28 @@ export class ContextMenu extends React.Component {
let background;
if (hasBackground) {
background = (
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={this._onFinished} onContextMenu={this.onContextMenu} />
<div
className="mx_ContextualMenu_background"
style={wrapperStyle}
onClick={this.onFinished}
onContextMenu={this.onContextMenu}
/>
);
}
return (
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown} onContextMenu={this.onContextMenuPreventBubbling}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role={this.props.managed ? "menu" : undefined}>
<div
className="mx_ContextualMenu_wrapper"
style={{...position, ...wrapperStyle}}
onKeyDown={this.onKeyDown}
onContextMenu={this.onContextMenuPreventBubbling}
>
<div
className={menuClasses}
style={menuStyle}
ref={this.collectContextMenuRect}
role={this.props.managed ? "menu" : undefined}
>
{ chevron }
{ props.children }
</div>
@ -348,99 +382,13 @@ export class ContextMenu extends React.Component {
);
}
render() {
render(): React.ReactChild {
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
}
}
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
export const ContextMenuButton = ({ label, isExpanded, children, onClick, onContextMenu, ...props }) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton
{...props}
onClick={onClick}
onContextMenu={onContextMenu || onClick}
title={label}
aria-label={label}
aria-haspopup={true}
aria-expanded={isExpanded}
>
{ children }
</AccessibleButton>
);
};
ContextMenuButton.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string,
isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open
};
// Semantic component for representing a role=menuitem
export const MenuItem = ({children, label, ...props}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
{ children }
</AccessibleButton>
);
};
MenuItem.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string, // optional
className: PropTypes.string, // optional
onClick: PropTypes.func.isRequired,
};
// Semantic component for representing a role=group for grouping menu radios/checkboxes
export const MenuGroup = ({children, label, ...props}) => {
return <div {...props} role="group" aria-label={label}>
{ children }
</div>;
};
MenuGroup.propTypes = {
label: PropTypes.string.isRequired,
className: PropTypes.string, // optional
};
// Semantic component for representing a role=menuitemcheckbox
export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton {...props} role="menuitemcheckbox" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
{ children }
</AccessibleButton>
);
};
MenuItemCheckbox.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string, // optional
active: PropTypes.bool.isRequired,
disabled: PropTypes.bool, // optional
className: PropTypes.string, // optional
onClick: PropTypes.func.isRequired,
};
// Semantic component for representing a role=menuitemradio
export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<AccessibleButton {...props} role="menuitemradio" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
{ children }
</AccessibleButton>
);
};
MenuItemRadio.propTypes = {
...AccessibleButton.propTypes,
label: PropTypes.string, // optional
active: PropTypes.bool.isRequired,
disabled: PropTypes.bool, // optional
className: PropTypes.string, // optional
onClick: PropTypes.func.isRequired,
};
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
export const toRightOf = (elementRect, chevronOffset=12) => {
export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
const left = elementRect.right + window.pageXOffset + 3;
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
top -= chevronOffset + 8; // where 8 is half the height of the chevron
@ -448,8 +396,8 @@ export const toRightOf = (elementRect, chevronOffset=12) => {
};
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
export const aboveLeftOf = (elementRect, chevronFace="none") => {
const menuOptions = { chevronFace };
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonRight = elementRect.right + window.pageXOffset;
const buttonBottom = elementRect.bottom + window.pageYOffset;
@ -507,3 +455,12 @@ export function createMenu(ElementClass, props) {
return {close: onFinished};
}
// re-export the semantic helper components for simplicity
export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton";
export {MenuGroup} from "../../accessibility/context_menu/MenuGroup";
export {MenuItem} from "../../accessibility/context_menu/MenuItem";
export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox";
export {MenuItemRadio} from "../../accessibility/context_menu/MenuItemRadio";
export {StyledMenuItemCheckbox} from "../../accessibility/context_menu/StyledMenuItemCheckbox";
export {StyledMenuItemRadio} from "../../accessibility/context_menu/StyledMenuItemRadio";

View file

@ -21,6 +21,7 @@ import classNames from "classnames";
import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
import RoomList2 from "../views/rooms/RoomList2";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist2";
import { Action } from "../../dispatcher/actions";
import UserMenu from "./UserMenu";
import RoomSearch from "./RoomSearch";
@ -56,12 +57,20 @@ interface IState {
showTagPanel: boolean;
}
// List of CSS classes which should be included in keyboard navigation within the room list
const cssClasses = [
"mx_RoomSearch_input",
"mx_RoomSearch_icon", // minimized <RoomSearch />
"mx_RoomSublist2_headerText",
"mx_RoomTile2",
"mx_RoomSublist2_showNButton",
];
export default class LeftPanel2 extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private tagPanelWatcherRef: string;
private focusedElement = null;
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
private isDoingStickyHeaders = false;
constructor(props: IProps) {
super(props);
@ -106,11 +115,27 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
};
private handleStickyHeaders(list: HTMLDivElement) {
// TODO: Evaluate if this has any performance benefit or detriment.
// See https://github.com/vector-im/riot-web/issues/14035
if (this.isDoingStickyHeaders) return;
this.isDoingStickyHeaders = true;
if (window.requestAnimationFrame) {
window.requestAnimationFrame(() => {
this.doStickyHeaders(list);
this.isDoingStickyHeaders = false;
});
} else {
this.doStickyHeaders(list);
this.isDoingStickyHeaders = false;
}
}
private doStickyHeaders(list: HTMLDivElement) {
const rlRect = list.getBoundingClientRect();
const bottom = rlRect.bottom;
const top = rlRect.top;
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
const headerHeight = 32; // Note: must match the CSS!
const headerRightMargin = 24; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = rlRect.width - headerRightMargin;
@ -121,22 +146,27 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
const slRect = sublist.getBoundingClientRect();
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
header.style.removeProperty("display"); // always clear display:none first
if (slRect.top + headerHeight > bottom && !gotBottom) {
if (slRect.top + HEADER_HEIGHT > bottom && !gotBottom) {
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
header.style.width = `${headerStickyWidth}px`;
header.style.removeProperty("top");
gotBottom = true;
} else if ((slRect.top - (headerHeight / 3)) < top) {
} else if (((slRect.top - (HEADER_HEIGHT * 0.6) + HEADER_HEIGHT) < top) || sublist === sublists[0]) {
// the header should become sticky once it is 60% or less out of view at the top.
// We also add HEADER_HEIGHT because the sticky header is put above the scrollable area,
// into the padding of .mx_LeftPanel2_roomListWrapper,
// by subtracting HEADER_HEIGHT from the top below.
// We also always try to make the first sublist header sticky.
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
header.style.top = `${rlRect.top}px`;
header.style.width = `${headerStickyWidth}px`;
header.style.top = `${rlRect.top - HEADER_HEIGHT}px`;
if (lastTopHeader) {
lastTopHeader.style.display = "none";
}
// first unset it, if set in last iteration
header.style.removeProperty("display");
lastTopHeader = header;
} else {
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
@ -146,6 +176,26 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.style.removeProperty("top");
}
}
// add appropriate sticky classes to wrapper so it has
// the necessary top/bottom padding to put the sticky header in
const listWrapper = list.parentElement;
if (gotBottom) {
listWrapper.classList.add("stickyBottom");
} else {
listWrapper.classList.remove("stickyBottom");
}
if (lastTopHeader) {
listWrapper.classList.add("stickyTop");
} else {
listWrapper.classList.remove("stickyTop");
}
// ensure scroll doesn't go above the gap left by the header of
// the first sublist always being sticky if no other header is sticky
if (list.scrollTop < HEADER_HEIGHT) {
list.scrollTop = HEADER_HEIGHT;
}
}
// TODO: Improve header reliability: https://github.com/vector-im/riot-web/issues/14232
@ -211,10 +261,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
if (element) {
classes = element.classList;
}
} while (element && !(
classes.contains("mx_RoomTile2") ||
classes.contains("mx_RoomSublist2_headerText") ||
classes.contains("mx_RoomSearch_input")));
} while (element && !cssClasses.some(c => classes.contains(c)));
if (element) {
element.focus();
@ -245,17 +292,21 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private renderSearchExplore(): React.ReactNode {
return (
<div className="mx_LeftPanel2_filterContainer" onFocus={this.onFocus} onBlur={this.onBlur}>
<div
className="mx_LeftPanel2_filterContainer"
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
>
<RoomSearch
onQueryUpdate={this.onSearch}
isMinimized={this.props.isMinimized}
onVerticalArrow={this.onKeyDown}
/>
<AccessibleButton
// TODO fix the accessibility of this: https://github.com/vector-im/riot-web/issues/14180
className="mx_LeftPanel2_exploreButton"
onClick={this.onExplore}
alt={_t("Explore rooms")}
title={_t("Explore rooms")}
/>
</div>
);
@ -298,15 +349,17 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
<aside className="mx_LeftPanel2_roomListContainer">
{this.renderHeader()}
{this.renderSearchExplore()}
<div
className={roomListClasses}
onScroll={this.onScroll}
ref={this.listContainerRef}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
>
{roomList}
<div className="mx_LeftPanel2_roomListWrapper">
<div
className={roomListClasses}
onScroll={this.onScroll}
ref={this.listContainerRef}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
>
{roomList}
</div>
</div>
</aside>
</div>

View file

@ -19,7 +19,6 @@ limitations under the License.
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { DragDropContext } from 'react-beautiful-dnd';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
@ -53,6 +52,7 @@ import {
} from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions";
import LeftPanel2 from "./LeftPanel2";
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -409,20 +409,6 @@ class LoggedInView extends React.Component<IProps, IState> {
};
_onKeyDown = (ev) => {
/*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
// Will need to find a better meta key if anyone actually cares about using this.
if (ev.altKey && ev.ctrlKey && ev.keyCode > 48 && ev.keyCode < 58) {
dis.dispatch({
action: 'view_indexed_room',
roomIndex: ev.keyCode - 49,
});
ev.stopPropagation();
ev.preventDefault();
return;
}
*/
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
@ -474,8 +460,8 @@ class LoggedInView extends React.Component<IProps, IState> {
case Key.ARROW_UP:
case Key.ARROW_DOWN:
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
dis.dispatch({
action: 'view_room_delta',
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: ev.key === Key.ARROW_UP ? -1 : 1,
unread: ev.shiftKey,
});

View file

@ -596,15 +596,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
break;
}
case 'view_prev_room':
this.viewNextRoom(-1);
break;
case 'view_next_room':
this.viewNextRoom(1);
break;
case 'view_indexed_room':
this.viewIndexedRoom(payload.roomIndex);
break;
case Action.ViewUserSettings: {
const tabPayload = payload as OpenToTabPayload;
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
@ -812,19 +806,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
// TODO: Move to RoomViewStore
private viewIndexedRoom(roomIndex: number) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
if (allRooms[roomIndex]) {
dis.dispatch({
action: 'view_room',
room_id: allRooms[roomIndex].roomId,
});
}
}
// switch view to the given room
//
// @param {Object} roomInfo Object containing data about the room to be joined

View file

@ -149,7 +149,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
let clearButton = (
<AccessibleButton
tabIndex={-1}
className='mx_RoomSearch_clearButton'
title={_t("Clear filter")}
className="mx_RoomSearch_clearButton"
onClick={this.clearInput}
/>
);
@ -157,8 +158,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
if (this.props.isMinimized) {
icon = (
<AccessibleButton
tabIndex={-1}
className='mx_RoomSearch_icon'
title={_t("Search rooms")}
className="mx_RoomSearch_icon"
onClick={this.openSearch}
/>
);

View file

@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from "react";
import React, { createRef } from "react";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { createRef } from "react";
import { _t } from "../../languageHandler";
import {ContextMenu, ContextMenuButton, MenuItem} from "./ContextMenu";
import { ChevronFace, ContextMenu, ContextMenuButton, MenuItem } from "./ContextMenu";
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
@ -122,7 +121,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
}
};
private onOpenMenuClick = (ev: InputEvent) => {
private onOpenMenuClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
@ -235,7 +234,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
return (
<ContextMenu
chevronFace="none"
chevronFace={ChevronFace.None}
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
@ -332,7 +331,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
className={classes}
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
label={_t("Account settings")}
label={_t("User menu")}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
>