diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index bda19432a8..e861e3d45f 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -419,7 +419,7 @@ export const toRightOf = (elementRect, 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 - return {left, top}; + return {left, top, chevronOffset}; }; // Placement method for to position context menu right-aligned and flowing to the left of elementRect diff --git a/src/components/views/dialogs/ShareDialog.js b/src/components/views/dialogs/ShareDialog.js index a730e3859d..f9e77101fc 100644 --- a/src/components/views/dialogs/ShareDialog.js +++ b/src/components/views/dialogs/ShareDialog.js @@ -108,7 +108,7 @@ export default class ShareDialog extends React.Component { const buttonRect = e.target.getBoundingClientRect(); const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu'); const {close} = ContextMenu.createMenu(GenericTextContextMenu, { - ...toRightOf(buttonRect, 11), + ...toRightOf(buttonRect, 2), message: successful ? _t('Copied!') : _t('Failed to copy'), }); // Drop a reference to this close handler for componentWillUnmount diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index bc861eb156..a21b091145 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk'; @@ -27,6 +27,7 @@ import classNames from 'classnames'; import MatrixClientPeg from "../../../MatrixClientPeg"; import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu"; +// XXX this class copies a lot from RoomTile.js export default createReactClass({ displayName: 'GroupInviteTile', @@ -47,10 +48,6 @@ export default createReactClass({ }); }, - componentDidMount: function() { - this._contextMenuButton = createRef(); - }, - onClick: function(e) { dis.dispatch({ action: 'view_group', @@ -74,16 +71,12 @@ export default createReactClass({ }); }, - openMenu: function(e) { + _showContextMenu: function(boundingClientRect) { // Only allow non-guests to access the context menu if (MatrixClientPeg.get().isGuest()) return; - // Prevent the GroupInviteTile onClick event firing as well - e.stopPropagation(); - e.preventDefault(); - const state = { - menuDisplayed: true, + contextMenuPosition: boundingClientRect, }; // If the badge is clicked, then no longer show tooltip @@ -94,9 +87,28 @@ export default createReactClass({ this.setState(state); }, + onContextMenuButtonClick: function(e) { + // Prevent the RoomTile onClick event firing as well + e.stopPropagation(); + e.preventDefault(); + + this._showContextMenu(e.target.getBoundingClientRect()); + }, + + onContextMenu: function(e) { + // Prevent the native context menu + e.preventDefault(); + + this._showContextMenu({ + right: e.clientX, + top: e.clientY, + height: 0, + }); + }, + closeMenu: function() { this.setState({ - menuDisplayed: false, + contextMenuPosition: null, }); }, @@ -110,15 +122,16 @@ export default createReactClass({ const av = ; + const isMenuDisplayed = Boolean(this.state.contextMenuPosition); const nameClasses = classNames('mx_RoomTile_name mx_RoomTile_invite mx_RoomTile_badgeShown', { - 'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed, + 'mx_RoomTile_badgeShown': this.state.badgeHover || isMenuDisplayed, }); const label =
{ groupName }
; - const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed; + const badgeEllipsis = this.state.badgeHover || isMenuDisplayed; const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', { 'mx_RoomTile_badgeButton': badgeEllipsis, }); @@ -127,10 +140,9 @@ export default createReactClass({ const badge = ( { badgeContent } @@ -143,17 +155,16 @@ export default createReactClass({ } const classes = classNames('mx_RoomTile mx_RoomTile_highlight', { - 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, + 'mx_RoomTile_menuDisplayed': isMenuDisplayed, 'mx_RoomTile_selected': this.state.selected, 'mx_GroupInviteTile': true, }); let contextMenu; - if (this.state.menuDisplayed) { - const elementRect = this._contextMenuButton.current.getBoundingClientRect(); + if (isMenuDisplayed) { const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); contextMenu = ( - + ); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 774c4a3e8f..6bf45d9193 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -280,7 +280,7 @@ module.exports = createReactClass({ const buttonRect = e.target.getBoundingClientRect(); const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu'); const {close} = ContextMenu.createMenu(GenericTextContextMenu, { - ...toRightOf(buttonRect, 11), + ...toRightOf(buttonRect, 2), message: successful ? _t('Copied!') : _t('Failed to copy'), }); e.target.onmouseleave = close; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 313f013de4..817ada9706 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; @@ -59,7 +59,7 @@ module.exports = createReactClass({ return ({ hover: false, badgeHover: false, - menuDisplayed: false, + contextMenuPosition: null, // DOM bounding box, null if non-shown roomName: this.props.room.name, notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), notificationCount: this.props.room.getUnreadNotificationCount(), @@ -145,8 +145,6 @@ module.exports = createReactClass({ }, componentDidMount: function() { - this._contextMenuButton = createRef(); - const cli = MatrixClientPeg.get(); cli.on("accountData", this.onAccountData); cli.on("Room.name", this.onRoomName); @@ -241,16 +239,12 @@ module.exports = createReactClass({ this.setState( { badgeHover: false } ); }, - openMenu: function(e) { + _showContextMenu: function(boundingClientRect) { // Only allow non-guests to access the context menu if (MatrixClientPeg.get().isGuest()) return; - // Prevent the RoomTile onClick event firing as well - e.stopPropagation(); - e.preventDefault(); - const state = { - menuDisplayed: true, + contextMenuPosition: boundingClientRect, }; // If the badge is clicked, then no longer show tooltip @@ -261,9 +255,28 @@ module.exports = createReactClass({ this.setState(state); }, + onContextMenuButtonClick: function(e) { + // Prevent the RoomTile onClick event firing as well + e.stopPropagation(); + e.preventDefault(); + + this._showContextMenu(e.target.getBoundingClientRect()); + }, + + onContextMenu: function(e) { + // Prevent the native context menu + e.preventDefault(); + + this._showContextMenu({ + right: e.clientX, + top: e.clientY, + height: 0, + }); + }, + closeMenu: function() { this.setState({ - menuDisplayed: false, + contextMenuPosition: null, }); this.props.refreshSubList(); }, @@ -282,6 +295,8 @@ module.exports = createReactClass({ subtext = this.state.statusMessage; } + const isMenuDisplayed = Boolean(this.state.contextMenuPosition); + const classes = classNames({ 'mx_RoomTile': true, 'mx_RoomTile_selected': this.state.selected, @@ -289,7 +304,7 @@ module.exports = createReactClass({ 'mx_RoomTile_unreadNotify': notifBadges, 'mx_RoomTile_highlight': mentionBadges, 'mx_RoomTile_invited': isInvite, - 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, + 'mx_RoomTile_menuDisplayed': isMenuDisplayed, 'mx_RoomTile_noBadges': !badges, 'mx_RoomTile_transparent': this.props.transparent, 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, @@ -301,7 +316,7 @@ module.exports = createReactClass({ const badgeClasses = classNames({ 'mx_RoomTile_badge': true, - 'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menuDisplayed, + 'mx_RoomTile_badgeButton': this.state.badgeHover || isMenuDisplayed, }); let name = this.state.roomName; @@ -323,7 +338,7 @@ module.exports = createReactClass({ const nameClasses = classNames({ 'mx_RoomTile_name': true, 'mx_RoomTile_invite': this.props.isInvite, - 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed, + 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || isMenuDisplayed, }); subtextLabel = subtext ? { subtext } : null; @@ -346,10 +361,9 @@ module.exports = createReactClass({ contextMenuButton = ( + isExpanded={isMenuDisplayed} + onClick={this.onContextMenuButtonClick} /> ); } @@ -382,11 +396,10 @@ module.exports = createReactClass({ } let contextMenu; - if (this.state.menuDisplayed && this._contextMenuButton.current) { - const elementRect = this._contextMenuButton.current.getBoundingClientRect(); + if (isMenuDisplayed) { const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu'); contextMenu = ( - + ); @@ -399,7 +412,7 @@ module.exports = createReactClass({ onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} - onContextMenu={this.openMenu} + onContextMenu={this.onContextMenu} aria-label={ariaLabel} aria-selected={this.state.selected} role="treeitem"