From 6c5b777a778616a9f22d3e1df809d11bf9914d40 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 28 Nov 2019 18:16:59 +0000 Subject: [PATCH] Consolidate all except tooltips Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextualMenu.js | 136 +++++++++++++----- .../structures/TopLeftMenuButton.js | 33 ++--- .../avatars/MemberStatusMessageAvatar.js | 40 +++--- .../views/context_menus/WidgetContextMenu.js | 27 ++-- src/components/views/elements/AppTile.js | 90 ++++++------ src/components/views/elements/TagTile.js | 16 ++- .../views/groups/GroupInviteTile.js | 15 +- .../views/messages/MessageActionBar.js | 59 ++------ src/components/views/rooms/RoomTile.js | 11 +- 9 files changed, 231 insertions(+), 196 deletions(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index dcf670f01d..d332f8f824 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -16,13 +16,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {useRef, useState} from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {focusCapturedRef} from "../../utils/Accessibility"; import {Key, KeyCode} from "../../Keyboard"; import sdk from "../../index"; +import AccessibleButton from "../views/elements/AccessibleButton"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -222,7 +223,7 @@ export default class ContextualMenu extends React.Component { return
{ chevron } - +
{ props.hasBackground &&
} @@ -231,8 +232,10 @@ export default class ContextualMenu extends React.Component { } const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]); - -class ContextualMenu2 extends React.Component { +// 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 { propTypes: { top: PropTypes.number, bottom: PropTypes.number, @@ -243,7 +246,7 @@ class ContextualMenu2 extends React.Component { chevronOffset: PropTypes.number, chevronFace: PropTypes.string, // top, bottom, left, right or none // Function to be called on menu close - onFinished: PropTypes.func, + onFinished: PropTypes.func.isRequired, menuPaddingTop: PropTypes.number, menuPaddingRight: PropTypes.number, menuPaddingBottom: PropTypes.number, @@ -258,10 +261,14 @@ class ContextualMenu2 extends React.Component { windowResize: PropTypes.func, }; + static defaultProps = { + hasBackground: true, + }; + constructor() { super(); this.state = { - contextMenuRect: null, + contextMenuElem: null, }; // persist what had focus when we got initialized so we can return it after @@ -277,19 +284,22 @@ class ContextualMenu2 extends React.Component { // We don't need to clean up when unmounting, so ignore if (!element) return; - const first = element.querySelector('[role^="menuitem"]'); + let first = element.querySelector('[role^="menuitem"]'); + if (!first) { + first = element.querySelector('[tab-index]'); + } if (first) { first.focus(); } this.setState({ - contextMenuRect: element.getBoundingClientRect(), + contextMenuElem: element, }); }; onContextMenu = (e) => { - if (this.props.closeMenu) { - this.props.closeMenu(); + if (this.props.onFinished) { + this.props.onFinished(); e.preventDefault(); const x = e.clientX; @@ -347,13 +357,25 @@ class ContextualMenu2 extends React.Component { } }; - _onKeyDown = (ev) => { - let handled = true; + _onMoveFocusHomeEnd = (element, up) => { + let results = element.querySelectorAll('[role^="menuitem"]'); + if (!results) { + results = element.querySelectorAll('[tab-index]'); + } + if (results && results.length) { + if (up) { + results[0].focus(); + } else { + results[results.length - 1].focus(); + } + } + }; + _onKeyDown = (ev) => { switch (ev.key) { case Key.TAB: case Key.ESCAPE: - this.props.closeMenu(); + this.props.onFinished(); break; case Key.ARROW_UP: this._onMoveFocus(ev.target, true); @@ -361,14 +383,17 @@ class ContextualMenu2 extends React.Component { case Key.ARROW_DOWN: this._onMoveFocus(ev.target, false); break; - default: - handled = false; + case Key.HOME: + this._onMoveFocusHomeEnd(this.state.contextMenuElem, true); + break; + case Key.END: + this._onMoveFocusHomeEnd(this.state.contextMenuElem, false); + break; } - if (handled) { - ev.stopPropagation(); - ev.preventDefault(); - } + // consume all other keys in context menu + ev.stopPropagation(); + ev.preventDefault(); }; render() { @@ -390,7 +415,7 @@ class ContextualMenu2 extends React.Component { chevronFace = 'right'; } - const contextMenuRect = this.state.contextMenuRect || null; + const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null; const padding = 10; const chevronOffset = {}; @@ -465,37 +490,36 @@ class ContextualMenu2 extends React.Component { let background; if (props.hasBackground) { background = ( -
+
); } - return ( + const menu = (
-
+
{ chevron } { props.children }
{ background }
); + return ReactDOM.createPortal(menu, getOrCreateContainer()); } } -// 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 const ContextMenu = ({children, onFinished, props, hasBackground=true}) => { - const menu = - { children } - ; - - return ReactDOM.createPortal(menu, getOrCreateContainer()); +// Semantic component for representing the AccessibleButton which launches a +export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + { children } + + ); +}; +ContextMenuButton.propTypes = { + ...AccessibleButton.propTypes, + label: PropTypes.string.isRequired, + isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open }; // Semantic component for representing a role=menuitem @@ -508,6 +532,7 @@ export const MenuItem = ({children, label, ...props}) => { ); }; MenuItem.propTypes = { + ...AccessibleButton.propTypes, label: PropTypes.string, // optional className: PropTypes.string, // optional onClick: PropTypes.func.isRequired, @@ -520,6 +545,7 @@ export const MenuGroup = ({children, label, ...props}) => {
; }; MenuGroup.propTypes = { + ...AccessibleButton.propTypes, label: PropTypes.string.isRequired, className: PropTypes.string, // optional }; @@ -534,6 +560,7 @@ export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ); }; MenuItemCheckbox.propTypes = { + ...AccessibleButton.propTypes, label: PropTypes.string, // optional active: PropTypes.bool.isRequired, disabled: PropTypes.bool, // optional @@ -551,6 +578,7 @@ export const MenuItemRadio = ({children, label, active=false, disabled=false, .. ); }; MenuItemRadio.propTypes = { + ...AccessibleButton.propTypes, label: PropTypes.string, // optional active: PropTypes.bool.isRequired, disabled: PropTypes.bool, // optional @@ -566,6 +594,38 @@ export const toRightOf = (elementRect, chevronOffset=12) => { return {left, top}; }; +// Placement method for to position context menu right-aligned and flowing to the left of elementRect +export const aboveLeft = (elementRect, chevronFace="none") => { + const menuOptions = { chevronFace }; + + const buttonRight = elementRect.right + window.pageXOffset; + const buttonBottom = elementRect.bottom + window.pageYOffset; + const buttonTop = elementRect.top + window.pageYOffset; + // Align the right edge of the menu to the right edge of the button + menuOptions.right = window.innerWidth - buttonRight; + // Align the menu vertically on whichever side of the button has more space available. + if (buttonBottom < window.innerHeight / 2) { + menuOptions.top = buttonBottom; + } else { + menuOptions.bottom = window.innerHeight - buttonTop; + } + + return menuOptions; +}; + +export const useContextMenu = () => { + const _button = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const open = () => { + setIsOpen(true); + }; + const close = () => { + setIsOpen(false); + }; + + return [isOpen, _button, open, close, setIsOpen]; +}; + export function createMenu(ElementClass, props, hasBackground=true) { const closeMenu = function(...args) { ReactDOM.unmountComponentAtNode(getOrCreateContainer()); diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js index e0875efb42..49e3ab949c 100644 --- a/src/components/structures/TopLeftMenuButton.js +++ b/src/components/structures/TopLeftMenuButton.js @@ -23,7 +23,7 @@ import MatrixClientPeg from '../../MatrixClientPeg'; import Avatar from '../../Avatar'; import { _t } from '../../languageHandler'; import dis from "../../dispatcher"; -import {ContextMenu} from "./ContextualMenu"; +import {ContextMenu, ContextMenuButton} from "./ContextualMenu"; import sdk from "../../index"; const AVATAR_SIZE = 28; @@ -119,29 +119,26 @@ export default class TopLeftMenuButton extends React.Component { let contextMenu; if (this.state.menuDisplayed) { const elementRect = this._buttonRef.getBoundingClientRect(); - const x = elementRect.left; - const y = elementRect.top + elementRect.height; - const props = { - chevronFace: "none", - left: x, - top: y, - }; - - contextMenu = - - ; + contextMenu = ( + + + + ); } - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return - this._buttonRef = r} - aria-label={_t("Your profile")} - aria-haspopup={true} - aria-expanded={this.state.menuDisplayed} + label={_t("Your profile")} + isExpanded={this.state.menuDisplayed} > { nameElement } { chevronElement } - + { contextMenu } ; diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index df84964a6e..cf7ca9057e 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -17,12 +17,12 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import AccessibleButton from '../elements/AccessibleButton'; +import {_t} from "../../../languageHandler"; import MemberAvatar from '../avatars/MemberAvatar'; import classNames from 'classnames'; import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu"; import SettingsStore from "../../../settings/SettingsStore"; -import {ContextMenu} from "../../structures/ContextualMenu"; +import {ContextMenu, ContextMenuButton} from "../../structures/ContextualMenu"; export default class MemberStatusMessageAvatar extends React.Component { static propTypes = { @@ -118,29 +118,33 @@ export default class MemberStatusMessageAvatar extends React.Component { if (this.state.menuDisplayed) { const elementRect = this._button.current.getBoundingClientRect(); - const x = (elementRect.left + window.pageXOffset); const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom - const chevronOffset = (elementRect.width - chevronWidth) / 2; const chevronMargin = 1; // Add some spacing away from target - const y = elementRect.top + window.pageYOffset - chevronMargin; - const props = { - chevronOffset: chevronOffset, - chevronFace: 'bottom', - left: x, - top: y, - menuWidth: 226, - }; - - contextMenu = - - ; + contextMenu = ( + + + + ); } return - + {avatar} - + { contextMenu } ; diff --git a/src/components/views/context_menus/WidgetContextMenu.js b/src/components/views/context_menus/WidgetContextMenu.js index 43e7e172cc..a8decd0f6a 100644 --- a/src/components/views/context_menus/WidgetContextMenu.js +++ b/src/components/views/context_menus/WidgetContextMenu.js @@ -16,8 +16,8 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; import {_t} from '../../../languageHandler'; +import {MenuItem} from "../../structures/ContextualMenu"; export default class WidgetContextMenu extends React.Component { static propTypes = { @@ -71,50 +71,45 @@ export default class WidgetContextMenu extends React.Component { }; render() { - const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); - const options = []; if (this.props.onEditClicked) { options.push( - + {_t("Edit")} - , + , ); } if (this.props.onReloadClicked) { options.push( - + {_t("Reload")} - , + , ); } if (this.props.onSnapshotClicked) { options.push( - + {_t("Take picture")} - , + , ); } if (this.props.onDeleteClicked) { options.push( - + {_t("Remove for everyone")} - , + , ); } // Push this last so it appears last. It's always present. options.push( - + {_t("Remove for me")} - , + , ); // Put separators between the options diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 9a29843d3b..b04de8a9c4 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -18,7 +18,7 @@ limitations under the License. import url from 'url'; import qs from 'querystring'; -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; import WidgetMessaging from '../../../WidgetMessaging'; @@ -35,7 +35,7 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; -import {createMenu} from "../../structures/ContextualMenu"; +import {aboveLeft, ContextMenu, ContextMenuButton} from "../../structures/ContextualMenu"; import PersistedElement from "./PersistedElement"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; @@ -62,6 +62,8 @@ export default class AppTile extends React.Component { this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this); this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this); this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this); + + this._contextMenuButton = createRef(); } /** @@ -89,6 +91,7 @@ export default class AppTile extends React.Component { error: null, deleting: false, widgetPageTitle: newProps.widgetPageTitle, + menuDisplayed: false, }; } @@ -555,45 +558,12 @@ export default class AppTile extends React.Component { this.refs.appFrame.src = this.refs.appFrame.src; } - _getMenuOptions(ev) { - // TODO: This block of code gets copy/pasted a lot. We should make that happen less. - const menuOptions = {}; - const buttonRect = ev.target.getBoundingClientRect(); - // The window X and Y offsets are to adjust position when zoomed in to page - const buttonLeft = buttonRect.left + window.pageXOffset; - const buttonTop = buttonRect.top + window.pageYOffset; - // Align the right edge of the menu to the left edge of the button - menuOptions.right = window.innerWidth - buttonLeft; - // Align the menu vertically on whichever side of the button has more - // space available. - if (buttonTop < window.innerHeight / 2) { - menuOptions.top = buttonTop; - } else { - menuOptions.bottom = window.innerHeight - buttonTop; - } - return menuOptions; - } + _onContextMenuClick = () => { + this.setState({ menuDisplayed: true }); + }; - _onContextMenuClick = (ev) => { - const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu'); - const menuOptions = { - ...this._getMenuOptions(ev), - - // A revoke handler is always required - onRevokeClicked: this._onRevokeClicked, - }; - - const canUserModify = this._canUserModify(); - const showEditButton = Boolean(this._scalarClient && canUserModify); - const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify; - const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; - - if (showEditButton) menuOptions.onEditClicked = this._onEditClick; - if (showDeleteButton) menuOptions.onDeleteClicked = this._onDeleteClick; - if (showPictureSnapshotButton) menuOptions.onSnapshotClicked = this._onSnapshotClick; - if (this.props.showReload) menuOptions.onReloadClicked = this._onReloadWidgetClick; - - createMenu(WidgetContextMenu, menuOptions); + _closeContextMenu = () => { + this.setState({ menuDisplayed: false }); }; render() { @@ -601,7 +571,7 @@ export default class AppTile extends React.Component { // Don't render widget if it is in the process of being deleted if (this.state.deleting) { - return
; + return
; } // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin @@ -697,7 +667,31 @@ export default class AppTile extends React.Component { mx_AppTileMenuBar_expanded: this.props.show, }); - return ( + let contextMenu; + if (this.state.menuDisplayed) { + const elementRect = this._contextMenuButton.current.getBoundingClientRect(); + + const canUserModify = this._canUserModify(); + const showEditButton = Boolean(this._scalarClient && canUserModify); + const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify; + const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; + + const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu'); + contextMenu = ( + + + + ); + } + + return
{ this.props.showMenubar &&
@@ -725,20 +719,24 @@ export default class AppTile extends React.Component { onClick={this._onPopoutWidgetClick} /> } { /* Context menu */ } - { }
} { appTileBody }
- ); + + { contextMenu } +
; } } -AppTile.displayName ='AppTile'; +AppTile.displayName = 'AppTile'; AppTile.propTypes = { id: PropTypes.string.isRequired, diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js index c9afd487cb..636ea7a255 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -23,13 +23,14 @@ import classNames from 'classnames'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; +import {_t} from '../../../languageHandler'; import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import FlairStore from '../../../stores/FlairStore'; import GroupStore from '../../../stores/GroupStore'; import TagOrderStore from '../../../stores/TagOrderStore'; -import {ContextMenu, toRightOf} from "../../structures/ContextualMenu"; +import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextualMenu"; // A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents // a thing to click on for the user to filter the visible rooms in the RoomList to: @@ -139,7 +140,6 @@ export default createReactClass({ render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const Tooltip = sdk.getComponent('elements.Tooltip'); const profile = this.state.profile || {}; const name = profile.name || this.props.tag; @@ -178,14 +178,20 @@ export default createReactClass({ const elementRect = this._contextMenuButton.current.getBoundingClientRect(); const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu'); contextMenu = ( - + ); } return - +
- + { contextMenu } ; diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index cfc50cc008..17e426a49d 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -22,9 +22,10 @@ import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; +import {_t} from '../../../languageHandler'; import classNames from 'classnames'; import MatrixClientPeg from "../../../MatrixClientPeg"; -import {ContextMenu, toRightOf} from "../../structures/ContextualMenu"; +import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextualMenu"; export default createReactClass({ displayName: 'GroupInviteTile', @@ -124,9 +125,15 @@ export default createReactClass({ const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!'; const badge = ( - + { badgeContent } - + ); let tooltip; @@ -146,7 +153,7 @@ export default createReactClass({ const elementRect = this._contextMenuButton.current.getBoundingClientRect(); const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); contextMenu = ( - + ); diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index d59e74623a..ba12bf21a0 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -16,50 +16,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState, useEffect, useRef} from 'react'; +import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; -import {ContextMenu} from '../../structures/ContextualMenu'; +import {aboveLeft, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextualMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import {RoomContext} from "../../structures/RoomView"; -const contextMenuProps = (elementRect) => { - const menuOptions = { - chevronFace: "none", - }; - - const buttonRight = elementRect.right + window.pageXOffset; - const buttonBottom = elementRect.bottom + window.pageYOffset; - const buttonTop = elementRect.top + window.pageYOffset; - // Align the right edge of the menu to the right edge of the button - menuOptions.right = window.innerWidth - buttonRight; - // Align the menu vertically on whichever side of the button has more space available. - if (buttonBottom < window.innerHeight / 2) { - menuOptions.top = buttonBottom; - } else { - menuOptions.bottom = window.innerHeight - buttonTop; - } - - return menuOptions; -}; - -const useContextMenu = () => { - const _button = useRef(null); - const [isOpen, setIsOpen] = useState(false); - const open = () => { - setIsOpen(true); - }; - const close = () => { - setIsOpen(false); - }; - - return [isOpen, _button, open, close, setIsOpen]; -}; - const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const [menuDisplayed, _button, openMenu, closeMenu] = useContextMenu(); useEffect(() => { @@ -86,7 +53,7 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo } const buttonRect = _button.current.getBoundingClientRect(); - contextMenu = + contextMenu = ; } - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return - @@ -120,19 +85,17 @@ const ReactButton = ({mxEvent, reactions}) => { if (menuDisplayed) { const buttonRect = _button.current.getBoundingClientRect(); const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker'); - contextMenu = + contextMenu = ; } - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return - @@ -227,7 +190,7 @@ export default class MessageActionBar extends React.PureComponent { getReplyThread={this.props.getReplyThread} getTile={this.props.getTile} permalinkCreator={this.props.permalinkCreator} - onFocusChange={this.props.onFocusChange} + onFocusChange={this.onFocusChange} />
; } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index edd50d0330..3095981ccb 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -25,7 +25,7 @@ import dis from '../../../dispatcher'; import MatrixClientPeg from '../../../MatrixClientPeg'; import DMRoomMap from '../../../utils/DMRoomMap'; import sdk from '../../../index'; -import {ContextMenu, toRightOf} from '../../structures/ContextualMenu'; +import {ContextMenu, ContextMenuButton, toRightOf} from '../../structures/ContextualMenu'; import * as RoomNotifs from '../../../RoomNotifs'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import ActiveRoomObserver from '../../../ActiveRoomObserver'; @@ -344,7 +344,12 @@ module.exports = createReactClass({ let contextMenuButton; if (!MatrixClientPeg.get().isGuest()) { contextMenuButton = ( - + ); } @@ -381,7 +386,7 @@ module.exports = createReactClass({ const elementRect = this._contextMenuButton.current.getBoundingClientRect(); const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); contextMenu = ( - + );