Consolidate, simplify and improve copied tooltips (#7799)

This commit is contained in:
Michael Telatynski 2022-02-14 23:54:46 +00:00 committed by GitHub
parent 226eed2a7f
commit 76fb2abae1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 31 additions and 50 deletions

View file

@ -161,10 +161,6 @@ limitations under the License.
flex-direction: column; flex-direction: column;
} }
.mx_CreateSecretStorageDialog_recoveryKeyCopyButtonText {
overflow-y: hidden;
}
.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { .mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton {
flex-grow: 1; flex-grow: 1;
white-space: nowrap; white-space: nowrap;

View file

@ -740,20 +740,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
onClick={this.onCopyClick} onClick={this.onCopyClick}
disabled={this.state.phase === Phase.Storing} disabled={this.state.phase === Phase.Storing}
> >
<span { this.state.copied ? _t("Copied!") : _t("Copy") }
className="mx_CreateSecretStorageDialog_recoveryKeyCopyButtonText"
style={{ height: this.state.copied ? '0' : 'auto' }}
aria-hidden={this.state.copied}
>
{ _t("Copy") }
</span>
<span
className="mx_CreateSecretStorageDialog_recoveryKeyCopyButtonText"
style={{ height: this.state.copied ? 'auto' : '0' }}
aria-hidden={!this.state.copied}
>
{ _t("Copied!") }
</span>
</AccessibleButton> </AccessibleButton>
</div> </div>
</div> </div>

View file

@ -25,6 +25,8 @@ interface IProps {
@replaceableComponent("views.context_menus.GenericTextContextMenu") @replaceableComponent("views.context_menus.GenericTextContextMenu")
export default class GenericTextContextMenu extends React.Component<IProps> { export default class GenericTextContextMenu extends React.Component<IProps> {
public render(): JSX.Element { public render(): JSX.Element {
return <div>{ this.props.message }</div>; return <div className="mx_Tooltip mx_Tooltip_visible" style={{ display: "block" }}>
{ this.props.message }
</div>;
} }
} }

View file

@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { SyntheticEvent } from 'react';
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import Tooltip, { Alignment } from './Tooltip'; import Tooltip, { Alignment } from './Tooltip';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> { interface IProps extends React.ComponentProps<typeof AccessibleButton> {
title: string; title: string;
tooltip?: React.ReactNode; tooltip?: React.ReactNode;
label?: React.ReactNode; label?: React.ReactNode;
@ -29,6 +29,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
forceHide?: boolean; forceHide?: boolean;
yOffset?: number; yOffset?: number;
alignment?: Alignment; alignment?: Alignment;
onHideTooltip?(ev: SyntheticEvent): void;
} }
interface IState { interface IState {
@ -36,15 +37,15 @@ interface IState {
} }
@replaceableComponent("views.elements.AccessibleTooltipButton") @replaceableComponent("views.elements.AccessibleTooltipButton")
export default class AccessibleTooltipButton extends React.PureComponent<ITooltipProps, IState> { export default class AccessibleTooltipButton extends React.PureComponent<IProps, IState> {
constructor(props: ITooltipProps) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
hover: false, hover: false,
}; };
} }
componentDidUpdate(prevProps: Readonly<ITooltipProps>) { componentDidUpdate(prevProps: Readonly<IProps>) {
if (!prevProps.forceHide && this.props.forceHide && this.state.hover) { if (!prevProps.forceHide && this.props.forceHide && this.state.hover) {
this.setState({ this.setState({
hover: false, hover: false,
@ -52,22 +53,24 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
} }
} }
showTooltip = () => { private showTooltip = () => {
if (this.props.forceHide) return; if (this.props.forceHide) return;
this.setState({ this.setState({
hover: true, hover: true,
}); });
}; };
hideTooltip = () => { private hideTooltip = (ev: SyntheticEvent) => {
this.setState({ this.setState({
hover: false, hover: false,
}); });
this.props.onHideTooltip?.(ev);
}; };
render() { render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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 && <Tooltip const tip = this.state.hover && <Tooltip
tooltipClassName={tooltipClassName} tooltipClassName={tooltipClassName}

View file

@ -15,12 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useEffect, useRef } from "react"; import React, { useState } from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { copyPlaintext } from "../../../utils/strings"; import { copyPlaintext } from "../../../utils/strings";
import { toRightOf, createMenu } from "../../structures/ContextMenu";
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
import { ButtonEvent } from "./AccessibleButton"; import { ButtonEvent } from "./AccessibleButton";
import AccessibleTooltipButton from "./AccessibleTooltipButton"; import AccessibleTooltipButton from "./AccessibleTooltipButton";
@ -30,32 +28,27 @@ interface IProps {
} }
const CopyableText: React.FC<IProps> = ({ children, getTextToCopy }) => { const CopyableText: React.FC<IProps> = ({ children, getTextToCopy }) => {
const closeCopiedTooltip = useRef<() => void>(); const [tooltip, setTooltip] = useState<string | undefined>(undefined);
const divRef = useRef<HTMLDivElement>();
useEffect(() => () => {
if (closeCopiedTooltip.current) closeCopiedTooltip.current();
}, [closeCopiedTooltip]);
const onCopyClickInternal = async (e: ButtonEvent) => { const onCopyClickInternal = async (e: ButtonEvent) => {
e.preventDefault(); 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 successful = await copyPlaintext(getTextToCopy());
const buttonRect = target.getBoundingClientRect(); setTooltip(successful ? _t('Copied!') : _t('Failed to copy'));
const { close } = createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
closeCopiedTooltip.current = target.onmouseleave = close;
}; };
return <div className="mx_CopyableText" ref={divRef}> const onHideTooltip = () => {
if (tooltip) {
setTooltip(undefined);
}
};
return <div className="mx_CopyableText">
{ children } { children }
<AccessibleTooltipButton <AccessibleTooltipButton
title={_t("Copy")} title={tooltip ?? _t("Copy")}
onClick={onCopyClickInternal} onClick={onCopyClickInternal}
className="mx_CopyableText_copyButton" className="mx_CopyableText_copyButton"
onHideTooltip={onHideTooltip}
/> />
</div>; </div>;
}; };

View file

@ -26,7 +26,7 @@ import Modal from '../../../Modal';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as ContextMenu from '../../structures/ContextMenu'; import * as ContextMenu from '../../structures/ContextMenu';
import { toRightOf } from '../../structures/ContextMenu'; import { ChevronFace, toRightOf } from '../../structures/ContextMenu';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import ReplyChain from "../elements/ReplyChain"; import ReplyChain from "../elements/ReplyChain";
import { pillifyLinks, unmountPills } from '../../../utils/pillify'; import { pillifyLinks, unmountPills } from '../../../utils/pillify';
@ -177,8 +177,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const button = document.createElement("span"); const button = document.createElement("span");
button.className = "mx_EventTile_button mx_EventTile_copyButton "; button.className = "mx_EventTile_button mx_EventTile_copyButton ";
// Check if expansion button exists. If so // Check if expansion button exists. If so we put the copy button to the bottom
// we put the copy button to the bottom
const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button"); const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button");
if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom"; if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
@ -188,7 +187,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const buttonRect = button.getBoundingClientRect(); const buttonRect = button.getBoundingClientRect();
const { close } = ContextMenu.createMenu(GenericTextContextMenu, { const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2), ...toRightOf(buttonRect, 0),
chevronFace: ChevronFace.None,
message: successful ? _t('Copied!') : _t('Failed to copy'), message: successful ? _t('Copied!') : _t('Failed to copy'),
}); });
button.onmouseleave = close; button.onmouseleave = close;