From 76fb2abae1a0ebd5348bc11e3fcafd41a12c2436 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Feb 2022 23:54:46 +0000 Subject: [PATCH] Consolidate, simplify and improve copied tooltips (#7799) --- .../security/_CreateSecretStorageDialog.scss | 4 --- .../security/CreateSecretStorageDialog.tsx | 15 +-------- .../context_menus/GenericTextContextMenu.tsx | 4 ++- .../elements/AccessibleTooltipButton.tsx | 19 +++++++----- .../views/elements/CopyableText.tsx | 31 +++++++------------ src/components/views/messages/TextualBody.tsx | 8 ++--- 6 files changed, 31 insertions(+), 50 deletions(-) diff --git a/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss index 9813162467..e5b97c4f1b 100644 --- a/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss @@ -161,10 +161,6 @@ limitations under the License. flex-direction: column; } -.mx_CreateSecretStorageDialog_recoveryKeyCopyButtonText { - overflow-y: hidden; -} - .mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { flex-grow: 1; white-space: nowrap; diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index 3d2115a8f3..3253d7ba33 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -740,20 +740,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent - - { _t("Copy") } - - - { _t("Copied!") } - + { this.state.copied ? _t("Copied!") : _t("Copy") } diff --git a/src/components/views/context_menus/GenericTextContextMenu.tsx b/src/components/views/context_menus/GenericTextContextMenu.tsx index a8edf6af3c..d25c708622 100644 --- a/src/components/views/context_menus/GenericTextContextMenu.tsx +++ b/src/components/views/context_menus/GenericTextContextMenu.tsx @@ -25,6 +25,8 @@ interface IProps { @replaceableComponent("views.context_menus.GenericTextContextMenu") export default class GenericTextContextMenu extends React.Component { public render(): JSX.Element { - return
{ this.props.message }
; + return
+ { this.props.message } +
; } } diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 7f40662efe..be3bbcfdb9 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -15,13 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { SyntheticEvent } from 'react'; import AccessibleButton from "./AccessibleButton"; import Tooltip, { Alignment } from './Tooltip'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -interface ITooltipProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { title: string; tooltip?: React.ReactNode; label?: React.ReactNode; @@ -29,6 +29,7 @@ interface ITooltipProps extends React.ComponentProps { forceHide?: boolean; yOffset?: number; alignment?: Alignment; + onHideTooltip?(ev: SyntheticEvent): void; } interface IState { @@ -36,15 +37,15 @@ interface IState { } @replaceableComponent("views.elements.AccessibleTooltipButton") -export default class AccessibleTooltipButton extends React.PureComponent { - constructor(props: ITooltipProps) { +export default class AccessibleTooltipButton extends React.PureComponent { + constructor(props: IProps) { super(props); this.state = { hover: false, }; } - componentDidUpdate(prevProps: Readonly) { + componentDidUpdate(prevProps: Readonly) { if (!prevProps.forceHide && this.props.forceHide && this.state.hover) { this.setState({ hover: false, @@ -52,22 +53,24 @@ export default class AccessibleTooltipButton extends React.PureComponent { + private showTooltip = () => { if (this.props.forceHide) return; this.setState({ hover: true, }); }; - hideTooltip = () => { + private hideTooltip = (ev: SyntheticEvent) => { this.setState({ hover: false, }); + this.props.onHideTooltip?.(ev); }; render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props } = this.props; + const { title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, onHideTooltip, + ...props } = this.props; const tip = this.state.hover && = ({ children, getTextToCopy }) => { - const closeCopiedTooltip = useRef<() => void>(); - const divRef = useRef(); - - useEffect(() => () => { - if (closeCopiedTooltip.current) closeCopiedTooltip.current(); - }, [closeCopiedTooltip]); + const [tooltip, setTooltip] = useState(undefined); const onCopyClickInternal = async (e: ButtonEvent) => { e.preventDefault(); - const target = e.target as HTMLDivElement; // copy target before we go async and React throws it away - const successful = await copyPlaintext(getTextToCopy()); - const buttonRect = target.getBoundingClientRect(); - const { close } = createMenu(GenericTextContextMenu, { - ...toRightOf(buttonRect, 2), - message: successful ? _t('Copied!') : _t('Failed to copy'), - }); - closeCopiedTooltip.current = target.onmouseleave = close; + setTooltip(successful ? _t('Copied!') : _t('Failed to copy')); }; - return
+ const onHideTooltip = () => { + if (tooltip) { + setTooltip(undefined); + } + }; + + return
{ children }
; }; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 91feace2eb..3b6fe38bdf 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -26,7 +26,7 @@ import Modal from '../../../Modal'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import * as ContextMenu from '../../structures/ContextMenu'; -import { toRightOf } from '../../structures/ContextMenu'; +import { ChevronFace, toRightOf } from '../../structures/ContextMenu'; import SettingsStore from "../../../settings/SettingsStore"; import ReplyChain from "../elements/ReplyChain"; import { pillifyLinks, unmountPills } from '../../../utils/pillify'; @@ -177,8 +177,7 @@ export default class TextualBody extends React.Component { const button = document.createElement("span"); button.className = "mx_EventTile_button mx_EventTile_copyButton "; - // Check if expansion button exists. If so - // we put the copy button to the bottom + // Check if expansion button exists. If so we put the copy button to the bottom const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button"); if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom"; @@ -188,7 +187,8 @@ export default class TextualBody extends React.Component { const buttonRect = button.getBoundingClientRect(); const { close } = ContextMenu.createMenu(GenericTextContextMenu, { - ...toRightOf(buttonRect, 2), + ...toRightOf(buttonRect, 0), + chevronFace: ChevronFace.None, message: successful ? _t('Copied!') : _t('Failed to copy'), }); button.onmouseleave = close;