transition Tooltips over to deprecated code

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2019-11-28 20:26:09 +00:00
parent 2daf3a96bf
commit 3847996b5b
5 changed files with 39 additions and 232 deletions

View file

@ -20,8 +20,7 @@ import React, {useRef, useState} from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import {focusCapturedRef} from "../../utils/Accessibility"; import {Key} from "../../Keyboard";
import {Key, KeyCode} from "../../Keyboard";
import sdk from "../../index"; import sdk from "../../index";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
@ -43,194 +42,6 @@ function getOrCreateContainer() {
return container; return container;
} }
export default class ContextualMenu extends React.Component {
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,
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,
// The component to render as the context menu
elementClass: PropTypes.element.isRequired,
// on resize callback
windowResize: PropTypes.func,
// method to close menu
closeMenu: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
contextMenuRect: null,
};
this.onContextMenu = this.onContextMenu.bind(this);
this.collectContextMenuRect = this.collectContextMenuRect.bind(this);
}
collectContextMenuRect(element) {
// We don't need to clean up when unmounting, so ignore
if (!element) return;
// For screen readers to find the thing
focusCapturedRef(element);
this.setState({
contextMenuRect: element.getBoundingClientRect(),
});
}
onContextMenu(e) {
if (this.props.closeMenu) {
this.props.closeMenu();
e.preventDefault();
const x = e.clientX;
const y = e.clientY;
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
// a context menu and its click-guard are up without completely rewriting how the context menus work.
setImmediate(() => {
const clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent(
'contextmenu', true, true, window, 0,
0, 0, x, y, false, false,
false, false, 0, null,
);
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
});
}
}
_onKeyDown = (ev) => {
if (ev.keyCode === KeyCode.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
this.props.closeMenu();
}
};
render() {
const position = {};
let chevronFace = null;
const props = this.props;
if (props.top) {
position.top = props.top;
} else {
position.bottom = props.bottom;
}
if (props.left) {
position.left = props.left;
chevronFace = 'left';
} else {
position.right = props.right;
chevronFace = 'right';
}
const contextMenuRect = this.state.contextMenuRect || null;
const padding = 10;
const chevronOffset = {};
if (props.chevronFace) {
chevronFace = props.chevronFace;
}
const hasChevron = chevronFace && chevronFace !== "none";
if (chevronFace === 'top' || chevronFace === 'bottom') {
chevronOffset.left = props.chevronOffset;
} else {
const target = position.top;
// By default, no adjustment is made
let adjusted = target;
// If we know the dimensions of the context menu, adjust its position
// such that it does not leave the (padded) window.
if (contextMenuRect) {
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
}
position.top = adjusted;
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
}
const chevron = hasChevron ?
<div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} /> :
undefined;
const className = 'mx_ContextualMenu_wrapper';
const menuClasses = classNames({
'mx_ContextualMenu': true,
'mx_ContextualMenu_left': !hasChevron && position.left,
'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',
});
const menuStyle = {};
if (props.menuWidth) {
menuStyle.width = props.menuWidth;
}
if (props.menuHeight) {
menuStyle.height = props.menuHeight;
}
if (!isNaN(Number(props.menuPaddingTop))) {
menuStyle["paddingTop"] = props.menuPaddingTop;
}
if (!isNaN(Number(props.menuPaddingLeft))) {
menuStyle["paddingLeft"] = props.menuPaddingLeft;
}
if (!isNaN(Number(props.menuPaddingBottom))) {
menuStyle["paddingBottom"] = props.menuPaddingBottom;
}
if (!isNaN(Number(props.menuPaddingRight))) {
menuStyle["paddingRight"] = props.menuPaddingRight;
}
const wrapperStyle = {};
if (!isNaN(Number(props.zIndex))) {
menuStyle["zIndex"] = props.zIndex + 1;
wrapperStyle["zIndex"] = props.zIndex;
}
const ElementClass = props.elementClass;
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click!
return <div className={className} style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} tabIndex={0}>
{ chevron }
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.onFinished} />
</div>
{ props.hasBackground && <div className="mx_ContextualMenu_background" style={wrapperStyle}
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
</div>;
}
}
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]); const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
// Generic ContextMenu Portal wrapper // Generic ContextMenu Portal wrapper
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1} // all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
@ -396,7 +207,7 @@ export class ContextMenu extends React.Component {
ev.preventDefault(); ev.preventDefault();
}; };
render() { renderMenu(hasBackground=this.props.hasBackground) {
const position = {}; const position = {};
let chevronFace = null; let chevronFace = null;
const props = this.props; const props = this.props;
@ -488,13 +299,13 @@ export class ContextMenu extends React.Component {
} }
let background; let background;
if (props.hasBackground) { if (hasBackground) {
background = ( background = (
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={props.onFinished} onContextMenu={this.onContextMenu} /> <div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={props.onFinished} onContextMenu={this.onContextMenu} />
); );
} }
const menu = ( return (
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}> <div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role="menu"> <div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role="menu">
{ chevron } { chevron }
@ -503,7 +314,10 @@ export class ContextMenu extends React.Component {
{ background } { background }
</div> </div>
); );
return ReactDOM.createPortal(menu, getOrCreateContainer()); }
render() {
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
} }
} }
@ -589,8 +403,8 @@ MenuItemRadio.propTypes = {
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset // Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
export const toRightOf = (elementRect, chevronOffset=12) => { export const toRightOf = (elementRect, chevronOffset=12) => {
const left = elementRect.right + window.pageXOffset + 3; const left = elementRect.right + window.pageXOffset + 3;
let top = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
top = top - (chevronOffset + 8); // where 8 is half the height of the chevron top -= chevronOffset + 8; // where 8 is half the height of the chevron
return {left, top}; return {left, top};
}; };
@ -626,8 +440,15 @@ export const useContextMenu = () => {
return [isOpen, _button, open, close, setIsOpen]; return [isOpen, _button, open, close, setIsOpen];
}; };
export function createMenu(ElementClass, props, hasBackground=true) { export default class LegacyContextMenu extends ContextMenu {
const closeMenu = function(...args) { render() {
return this.renderMenu(false);
}
}
// XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs.
export function createMenu(ElementClass, props) {
const onFinished = function(...args) {
ReactDOM.unmountComponentAtNode(getOrCreateContainer()); ReactDOM.unmountComponentAtNode(getOrCreateContainer());
if (props && props.onFinished) { if (props && props.onFinished) {
@ -635,16 +456,15 @@ export function createMenu(ElementClass, props, hasBackground=true) {
} }
}; };
// We only reference closeMenu once per call to createMenu const menu = <LegacyContextMenu
const menu = <ContextualMenu
hasBackground={hasBackground}
{...props} {...props}
elementClass={ElementClass} onFinished={onFinished} // eslint-disable-line react/jsx-no-bind
closeMenu={closeMenu} // eslint-disable-line react/jsx-no-bind windowResize={onFinished} // eslint-disable-line react/jsx-no-bind
windowResize={closeMenu} // eslint-disable-line react/jsx-no-bind >
/>; <ElementClass {...props} onFinished={onFinished} />
</LegacyContextMenu>;
ReactDOM.render(menu, getOrCreateContainer()); ReactDOM.render(menu, getOrCreateContainer());
return {close: closeMenu}; return {close: onFinished};
} }

View file

@ -22,6 +22,7 @@ import { _t } from '../../../languageHandler';
import QRCode from 'qrcode-react'; import QRCode from 'qrcode-react';
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks"; import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import * as ContextualMenu from "../../structures/ContextualMenu"; import * as ContextualMenu from "../../structures/ContextualMenu";
import {toRightOf} from "../../structures/ContextualMenu";
const socials = [ const socials = [
{ {
@ -102,18 +103,12 @@ export default class ShareDialog extends React.Component {
console.error('Failed to copy: ', err); console.error('Failed to copy: ', err);
} }
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const buttonRect = e.target.getBoundingClientRect(); const buttonRect = e.target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
// The window X and Y offsets are to adjust position when zoomed in to page
const x = buttonRect.right + window.pageXOffset;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, { const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
chevronOffset: 10, ...toRightOf(buttonRect, 11),
left: x,
top: y,
message: successful ? _t('Copied!') : _t('Failed to copy'), message: successful ? _t('Copied!') : _t('Failed to copy'),
}, false); });
// Drop a reference to this close handler for componentWillUnmount // Drop a reference to this close handler for componentWillUnmount
this.closeCopiedTooltip = e.target.onmouseleave = close; this.closeCopiedTooltip = e.target.onmouseleave = close;
} }

View file

@ -681,10 +681,10 @@ export default class AppTile extends React.Component {
<ContextMenu {...aboveLeft(elementRect, null)} onFinished={this._closeContextMenu}> <ContextMenu {...aboveLeft(elementRect, null)} onFinished={this._closeContextMenu}>
<WidgetContextMenu <WidgetContextMenu
onRevokeClicked={this._onRevokeClicked} onRevokeClicked={this._onRevokeClicked}
onEditClicked={showEditButton && this._onEditClick} onEditClicked={showEditButton ? this._onEditClick : undefined}
onDeleteClicked={showDeleteButton && this._onDeleteClick} onDeleteClicked={showDeleteButton ? this._onDeleteClick : undefined}
onSnapshotClicked={showPictureSnapshotButton && this._onSnapshotClick} onSnapshotClicked={showPictureSnapshotButton ? this._onSnapshotClick : undefined}
onReloadClicked={this.props.showReload && this._onReloadWidgetClick} onReloadClicked={this.props.showReload ? this._onReloadWidgetClick : undefined}
onFinished={this._closeContextMenu} onFinished={this._closeContextMenu}
/> />
</ContextMenu> </ContextMenu>

View file

@ -129,9 +129,6 @@ module.exports = createReactClass({
render: function() { render: function() {
// Render a placeholder // Render a placeholder
return ( return <div className={this.props.className} />;
<div className={this.props.className} >
</div>
);
}, },
}); });

View file

@ -33,6 +33,7 @@ import ReplyThread from "../elements/ReplyThread";
import {pillifyLinks} from '../../../utils/pillify'; import {pillifyLinks} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks"; import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
import {toRightOf} from "../../structures/ContextualMenu";
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'TextualBody', displayName: 'TextualBody',
@ -272,18 +273,12 @@ module.exports = createReactClass({
const copyCode = button.parentNode.getElementsByTagName("code")[0]; const copyCode = button.parentNode.getElementsByTagName("code")[0];
const successful = this.copyToClipboard(copyCode.textContent); const successful = this.copyToClipboard(copyCode.textContent);
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const buttonRect = e.target.getBoundingClientRect(); const buttonRect = e.target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
// The window X and Y offsets are to adjust position when zoomed in to page
const x = buttonRect.right + window.pageXOffset;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, { const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
chevronOffset: 10, ...toRightOf(buttonRect, 11),
left: x,
top: y,
message: successful ? _t('Copied!') : _t('Failed to copy'), message: successful ? _t('Copied!') : _t('Failed to copy'),
}, false); });
e.target.onmouseleave = close; e.target.onmouseleave = close;
}; };