Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 3eecd68

Browse files
Fix context menu being opened when clicking message action bar buttons (#9200)
1 parent e269c68 commit 3eecd68

File tree

6 files changed

+156
-60
lines changed

6 files changed

+156
-60
lines changed

src/components/structures/ContextMenu.tsx

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -225,35 +225,57 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
225225

226226
protected renderMenu(hasBackground = this.props.hasBackground) {
227227
const position: Partial<Writeable<DOMRect>> = {};
228-
const props = this.props;
229-
230-
if (props.top) {
231-
position.top = props.top;
228+
const {
229+
top,
230+
bottom,
231+
left,
232+
right,
233+
bottomAligned,
234+
rightAligned,
235+
menuClassName,
236+
menuHeight,
237+
menuWidth,
238+
menuPaddingLeft,
239+
menuPaddingRight,
240+
menuPaddingBottom,
241+
menuPaddingTop,
242+
zIndex,
243+
children,
244+
focusLock,
245+
managed,
246+
wrapperClassName,
247+
chevronFace: propsChevronFace,
248+
chevronOffset: propsChevronOffset,
249+
...props
250+
} = this.props;
251+
252+
if (top) {
253+
position.top = top;
232254
} else {
233-
position.bottom = props.bottom;
255+
position.bottom = bottom;
234256
}
235257

236258
let chevronFace: ChevronFace;
237-
if (props.left) {
238-
position.left = props.left;
259+
if (left) {
260+
position.left = left;
239261
chevronFace = ChevronFace.Left;
240262
} else {
241-
position.right = props.right;
263+
position.right = right;
242264
chevronFace = ChevronFace.Right;
243265
}
244266

245267
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
246268

247269
const chevronOffset: CSSProperties = {};
248-
if (props.chevronFace) {
249-
chevronFace = props.chevronFace;
270+
if (propsChevronFace) {
271+
chevronFace = propsChevronFace;
250272
}
251273
const hasChevron = chevronFace && chevronFace !== ChevronFace.None;
252274

253275
if (chevronFace === ChevronFace.Top || chevronFace === ChevronFace.Bottom) {
254-
chevronOffset.left = props.chevronOffset;
276+
chevronOffset.left = propsChevronOffset;
255277
} else {
256-
chevronOffset.top = props.chevronOffset;
278+
chevronOffset.top = propsChevronOffset;
257279
}
258280

259281
// If we know the dimensions of the context menu, adjust its position to
@@ -262,39 +284,39 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
262284
if (contextMenuRect) {
263285
if (position.top !== undefined) {
264286
let maxTop = windowHeight - WINDOW_PADDING;
265-
if (!this.props.bottomAligned) {
287+
if (!bottomAligned) {
266288
maxTop -= contextMenuRect.height;
267289
}
268290
position.top = Math.min(position.top, maxTop);
269291
// Adjust the chevron if necessary
270292
if (chevronOffset.top !== undefined) {
271-
chevronOffset.top = props.chevronOffset + props.top - position.top;
293+
chevronOffset.top = propsChevronOffset + top - position.top;
272294
}
273295
} else if (position.bottom !== undefined) {
274296
position.bottom = Math.min(
275297
position.bottom,
276298
windowHeight - contextMenuRect.height - WINDOW_PADDING,
277299
);
278300
if (chevronOffset.top !== undefined) {
279-
chevronOffset.top = props.chevronOffset + position.bottom - props.bottom;
301+
chevronOffset.top = propsChevronOffset + position.bottom - bottom;
280302
}
281303
}
282304
if (position.left !== undefined) {
283305
let maxLeft = windowWidth - WINDOW_PADDING;
284-
if (!this.props.rightAligned) {
306+
if (!rightAligned) {
285307
maxLeft -= contextMenuRect.width;
286308
}
287309
position.left = Math.min(position.left, maxLeft);
288310
if (chevronOffset.left !== undefined) {
289-
chevronOffset.left = props.chevronOffset + props.left - position.left;
311+
chevronOffset.left = propsChevronOffset + left - position.left;
290312
}
291313
} else if (position.right !== undefined) {
292314
position.right = Math.min(
293315
position.right,
294316
windowWidth - contextMenuRect.width - WINDOW_PADDING,
295317
);
296318
if (chevronOffset.left !== undefined) {
297-
chevronOffset.left = props.chevronOffset + position.right - props.right;
319+
chevronOffset.left = propsChevronOffset + position.right - right;
298320
}
299321
}
300322
}
@@ -320,36 +342,36 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
320342
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
321343
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
322344
'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom,
323-
'mx_ContextualMenu_rightAligned': this.props.rightAligned === true,
324-
'mx_ContextualMenu_bottomAligned': this.props.bottomAligned === true,
325-
}, this.props.menuClassName);
345+
'mx_ContextualMenu_rightAligned': rightAligned === true,
346+
'mx_ContextualMenu_bottomAligned': bottomAligned === true,
347+
}, menuClassName);
326348

327349
const menuStyle: CSSProperties = {};
328-
if (props.menuWidth) {
329-
menuStyle.width = props.menuWidth;
350+
if (menuWidth) {
351+
menuStyle.width = menuWidth;
330352
}
331353

332-
if (props.menuHeight) {
333-
menuStyle.height = props.menuHeight;
354+
if (menuHeight) {
355+
menuStyle.height = menuHeight;
334356
}
335357

336-
if (!isNaN(Number(props.menuPaddingTop))) {
337-
menuStyle["paddingTop"] = props.menuPaddingTop;
358+
if (!isNaN(Number(menuPaddingTop))) {
359+
menuStyle["paddingTop"] = menuPaddingTop;
338360
}
339-
if (!isNaN(Number(props.menuPaddingLeft))) {
340-
menuStyle["paddingLeft"] = props.menuPaddingLeft;
361+
if (!isNaN(Number(menuPaddingLeft))) {
362+
menuStyle["paddingLeft"] = menuPaddingLeft;
341363
}
342-
if (!isNaN(Number(props.menuPaddingBottom))) {
343-
menuStyle["paddingBottom"] = props.menuPaddingBottom;
364+
if (!isNaN(Number(menuPaddingBottom))) {
365+
menuStyle["paddingBottom"] = menuPaddingBottom;
344366
}
345-
if (!isNaN(Number(props.menuPaddingRight))) {
346-
menuStyle["paddingRight"] = props.menuPaddingRight;
367+
if (!isNaN(Number(menuPaddingRight))) {
368+
menuStyle["paddingRight"] = menuPaddingRight;
347369
}
348370

349371
const wrapperStyle = {};
350-
if (!isNaN(Number(props.zIndex))) {
351-
menuStyle["zIndex"] = props.zIndex + 1;
352-
wrapperStyle["zIndex"] = props.zIndex;
372+
if (!isNaN(Number(zIndex))) {
373+
menuStyle["zIndex"] = zIndex + 1;
374+
wrapperStyle["zIndex"] = zIndex;
353375
}
354376

355377
let background;
@@ -366,10 +388,10 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
366388

367389
let body = <>
368390
{ chevron }
369-
{ props.children }
391+
{ children }
370392
</>;
371393

372-
if (props.focusLock) {
394+
if (focusLock) {
373395
body = <FocusLock>
374396
{ body }
375397
</FocusLock>;
@@ -379,7 +401,7 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
379401
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.onKeyDown}>
380402
{ ({ onKeyDownHandler }) => (
381403
<div
382-
className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
404+
className={classNames("mx_ContextualMenu_wrapper", wrapperClassName)}
383405
style={{ ...position, ...wrapperStyle }}
384406
onClick={this.onClick}
385407
onKeyDown={onKeyDownHandler}
@@ -390,7 +412,8 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
390412
className={menuClasses}
391413
style={menuStyle}
392414
ref={this.collectContextMenuRect}
393-
role={this.props.managed ? "menu" : undefined}
415+
role={managed ? "menu" : undefined}
416+
{...props}
394417
>
395418
{ body }
396419
</div>

src/components/views/emojipicker/EmojiPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
240240
render() {
241241
let heightBefore = 0;
242242
return (
243-
<div className="mx_EmojiPicker">
243+
<div className="mx_EmojiPicker" data-testid='mx_EmojiPicker'>
244244
<Header categories={this.categories} onAnchorClick={this.scrollToCategory} />
245245
<Search query={this.state.filter} onChange={this.onChangeFilter} onEnter={this.onEnterFilter} />
246246
<AutoHideScrollbar

src/components/views/emojipicker/ReactionPicker.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ class ReactionPicker extends React.Component<IProps, IState> {
134134
isEmojiDisabled={this.isEmojiDisabled}
135135
selectedEmojis={this.state.selectedEmojis}
136136
showQuickReactions={true}
137-
data-testid='mx_ReactionPicker'
138137
/>;
139138
}
140139
}

src/components/views/messages/MessageActionBar.tsx

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
1616
limitations under the License.
1717
*/
1818

19-
import React, { ReactElement, useContext, useEffect } from 'react';
19+
import React, { ReactElement, useCallback, useContext, useEffect } from 'react';
2020
import { EventStatus, MatrixEvent, MatrixEventEvent } from 'matrix-js-sdk/src/models/event';
2121
import classNames from 'classnames';
2222
import { MsgType, RelationType } from 'matrix-js-sdk/src/@types/event';
@@ -88,7 +88,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
8888
onFocusChange(menuDisplayed);
8989
}, [onFocusChange, menuDisplayed]);
9090

91-
const onOptionsClick = (e: React.MouseEvent): void => {
91+
const onOptionsClick = useCallback((e: React.MouseEvent): void => {
9292
// Don't open the regular browser or our context menu on right-click
9393
e.preventDefault();
9494
e.stopPropagation();
@@ -97,7 +97,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
9797
// the element that is currently focused is skipped. So we want to call onFocus manually to keep the
9898
// position in the page even when someone is clicking around.
9999
onFocus();
100-
};
100+
}, [openMenu, onFocus]);
101101

102102
let contextMenu: ReactElement | null;
103103
if (menuDisplayed) {
@@ -121,6 +121,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({
121121
className="mx_MessageActionBar_iconButton mx_MessageActionBar_optionsButton"
122122
title={_t("Options")}
123123
onClick={onOptionsClick}
124+
onContextMenu={onOptionsClick}
124125
isExpanded={menuDisplayed}
125126
inputRef={ref}
126127
onFocus={onFocus}
@@ -153,17 +154,24 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
153154
</ContextMenu>;
154155
}
155156

157+
const onClick = useCallback((e: React.MouseEvent) => {
158+
// Don't open the regular browser or our context menu on right-click
159+
e.preventDefault();
160+
e.stopPropagation();
161+
162+
openMenu();
163+
// when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks
164+
// the element that is currently focused is skipped. So we want to call onFocus manually to keep the
165+
// position in the page even when someone is clicking around.
166+
onFocus();
167+
}, [openMenu, onFocus]);
168+
156169
return <React.Fragment>
157170
<ContextMenuTooltipButton
158171
className="mx_MessageActionBar_iconButton"
159172
title={_t("React")}
160-
onClick={() => {
161-
openMenu();
162-
// when the context menu is opened directly, e.g. via mouse click, the onFocus handler which tracks
163-
// the element that is currently focused is skipped. So we want to call onFocus manually to keep the
164-
// position in the page even when someone is clicking around.
165-
onFocus();
166-
}}
173+
onClick={onClick}
174+
onContextMenu={onClick}
167175
isExpanded={menuDisplayed}
168176
inputRef={ref}
169177
onFocus={onFocus}
@@ -193,7 +201,11 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
193201
return null;
194202
}
195203

196-
const onClick = (): void => {
204+
const onClick = (e: React.MouseEvent): void => {
205+
// Don't open the regular browser or our context menu on right-click
206+
e.preventDefault();
207+
e.stopPropagation();
208+
197209
if (firstTimeSeeingThreads) {
198210
localStorage.setItem("mx_seen_feature_thread", "true");
199211
}
@@ -245,6 +257,7 @@ const ReplyInThreadButton = ({ mxEvent }: IReplyInThreadButton) => {
245257
: _t("Can't create a thread from an event with an existing relation")}
246258

247259
onClick={onClick}
260+
onContextMenu={onClick}
248261
>
249262
<ThreadIcon />
250263
{ firstTimeSeeingThreads && !threadsEnabled && (
@@ -265,10 +278,19 @@ const FavouriteButton = ({ mxEvent }: IFavouriteButtonProp) => {
265278
'mx_MessageActionBar_favouriteButton_fillstar': isFavourite(eventId),
266279
});
267280

281+
const onClick = useCallback((e: React.MouseEvent) => {
282+
// Don't open the regular browser or our context menu on right-click
283+
e.preventDefault();
284+
e.stopPropagation();
285+
286+
toggleFavourite(eventId);
287+
}, [toggleFavourite, eventId]);
288+
268289
return <RovingAccessibleTooltipButton
269290
className={classes}
270291
title={_t("Favourite")}
271-
onClick={() => toggleFavourite(eventId)}
292+
onClick={onClick}
293+
onContextMenu={onClick}
272294
data-testid={eventId}
273295
>
274296
<StarIcon />
@@ -335,15 +357,23 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
335357
this.props.onFocusChange?.(focused);
336358
};
337359

338-
private onReplyClick = (ev: React.MouseEvent): void => {
360+
private onReplyClick = (e: React.MouseEvent): void => {
361+
// Don't open the regular browser or our context menu on right-click
362+
e.preventDefault();
363+
e.stopPropagation();
364+
339365
dis.dispatch({
340366
action: 'reply_to_event',
341367
event: this.props.mxEvent,
342368
context: this.context.timelineRenderingType,
343369
});
344370
};
345371

346-
private onEditClick = (): void => {
372+
private onEditClick = (e: React.MouseEvent): void => {
373+
// Don't open the regular browser or our context menu on right-click
374+
e.preventDefault();
375+
e.stopPropagation();
376+
347377
editEvent(this.props.mxEvent, this.context.timelineRenderingType, this.props.getRelationsForEvent);
348378
};
349379

@@ -406,6 +436,10 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
406436
}
407437

408438
private onResendClick = (ev: React.MouseEvent): void => {
439+
// Don't open the regular browser or our context menu on right-click
440+
ev.preventDefault();
441+
ev.stopPropagation();
442+
409443
this.runActionOnFailedEv((tarEv) => Resend.resend(tarEv));
410444
};
411445

@@ -423,6 +457,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
423457
className="mx_MessageActionBar_iconButton"
424458
title={_t("Edit")}
425459
onClick={this.onEditClick}
460+
onContextMenu={this.onEditClick}
426461
key="edit"
427462
>
428463
<EditIcon />
@@ -433,6 +468,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
433468
className="mx_MessageActionBar_iconButton"
434469
title={_t("Delete")}
435470
onClick={this.onCancelClick}
471+
onContextMenu={this.onCancelClick}
436472
key="cancel"
437473
>
438474
<TrashcanIcon />
@@ -453,6 +489,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
453489
className="mx_MessageActionBar_iconButton"
454490
title={_t("Retry")}
455491
onClick={this.onResendClick}
492+
onContextMenu={this.onResendClick}
456493
key="resend"
457494
>
458495
<ResendIcon />
@@ -475,6 +512,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
475512
className="mx_MessageActionBar_iconButton"
476513
title={_t("Reply")}
477514
onClick={this.onReplyClick}
515+
onContextMenu={this.onReplyClick}
478516
key="reply"
479517
>
480518
<ReplyIcon />

0 commit comments

Comments
 (0)