diff --git a/playwright/e2e/editing/editing.spec.ts b/playwright/e2e/editing/editing.spec.ts
index 2cc47f8edf0..206d91982e0 100644
--- a/playwright/e2e/editing/editing.spec.ts
+++ b/playwright/e2e/editing/editing.spec.ts
@@ -263,7 +263,6 @@ test.describe("Editing", () => {
checkA11y,
}) => {
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
- axe.exclude(".mx_Tooltip_visible"); // XXX: this is fine but would be good to fix
await page.goto(`#/room/${room.roomId}`);
diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts
index 2973f88cdab..66dd6663892 100644
--- a/playwright/element-web-test.ts
+++ b/playwright/element-web-test.ts
@@ -345,8 +345,7 @@ export const expect = baseExpect.extend({
if (!options?.showTooltips) {
css += `
- [role="tooltip"],
- .mx_Tooltip_visible {
+ [role="tooltip"] {
visibility: hidden !important;
}
`;
diff --git a/res/css/_components.pcss b/res/css/_components.pcss
index 42cd010e08f..c0dd2ee0b02 100644
--- a/res/css/_components.pcss
+++ b/res/css/_components.pcss
@@ -217,7 +217,6 @@
@import "./views/elements/_TagComposer.pcss";
@import "./views/elements/_TextWithTooltip.pcss";
@import "./views/elements/_ToggleSwitch.pcss";
-@import "./views/elements/_Tooltip.pcss";
@import "./views/elements/_UseCaseSelection.pcss";
@import "./views/elements/_UseCaseSelectionButton.pcss";
@import "./views/elements/_Validation.pcss";
diff --git a/res/css/views/auth/_PassphraseField.pcss b/res/css/views/auth/_PassphraseField.pcss
index 6020fb2b33d..293a81cbe2a 100644
--- a/res/css/views/auth/_PassphraseField.pcss
+++ b/res/css/views/auth/_PassphraseField.pcss
@@ -16,7 +16,8 @@ progress.mx_PassphraseField_progress {
border: 0;
height: 4px;
position: absolute;
- top: -12px;
+ top: -10px;
+ left: 0;
@mixin ProgressBarBorderRadius "2px";
@mixin ProgressBarColour $PassphraseStrengthLow;
diff --git a/res/css/views/elements/_Field.pcss b/res/css/views/elements/_Field.pcss
index 92f8e41f0aa..2659c4d3899 100644
--- a/res/css/views/elements/_Field.pcss
+++ b/res/css/views/elements/_Field.pcss
@@ -164,14 +164,6 @@ Please see LICENSE files in the repository root for full details.
}
}
-.mx_Field_tooltip {
- width: 200px;
-}
-
-.mx_Field_tooltip.mx_Field_valid {
- animation: mx_fadeout 1s 2s forwards;
-}
-
/* Customise other components when placed inside a Field */
.mx_Field .mx_Dropdown_input {
diff --git a/res/css/views/elements/_MiniAvatarUploader.pcss b/res/css/views/elements/_MiniAvatarUploader.pcss
index 9b1845b122c..bcb47e28ec1 100644
--- a/res/css/views/elements/_MiniAvatarUploader.pcss
+++ b/res/css/views/elements/_MiniAvatarUploader.pcss
@@ -10,18 +10,6 @@ Please see LICENSE files in the repository root for full details.
position: relative;
width: min-content;
- /* this isn't a floating tooltip so override some things to not need to bother with z-index and floating */
- .mx_Tooltip {
- display: inline-block;
- position: absolute;
- z-index: unset;
- width: max-content;
- left: 72px;
- /* top edge starting at 50 % of parent - 50 % of itself -> centered vertically */
- top: 50%;
- transform: translateY(-50%);
- }
-
.mx_MiniAvatarUploader_indicator {
position: absolute;
diff --git a/res/css/views/elements/_Tooltip.pcss b/res/css/views/elements/_Tooltip.pcss
deleted file mode 100644
index 30308ed2342..00000000000
--- a/res/css/views/elements/_Tooltip.pcss
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
-Copyright 2019-2024 New Vector Ltd.
-Copyright 2015, 2016 OpenMarket Ltd
-
-SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
-Please see LICENSE files in the repository root for full details.
-*/
-
-@keyframes mx_fadein {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
-}
-
-@keyframes mx_fadeout {
- from {
- opacity: 1;
- }
-
- to {
- opacity: 0;
- }
-}
-
-.mx_Tooltip_chevron {
- position: absolute;
- left: -7px;
- top: calc(50% - 6px);
- width: 0;
- height: 0;
- border-top: 7px solid transparent;
- border-right: 7px solid $menu-border-color;
- border-bottom: 7px solid transparent;
-}
-
-.mx_Tooltip_chevron::after {
- content: "";
- width: 0;
- height: 0;
- border-top: 6px solid transparent;
- border-right: 6px solid $menu-bg-color;
- border-bottom: 6px solid transparent;
- position: absolute;
- top: -6px;
- left: 1px;
-}
-
-.mx_Tooltip {
- display: none;
- position: fixed;
- border-radius: 8px;
- z-index: 6000; /* Higher than context menu so tooltips can be used everywhere */
- padding: 10px;
- pointer-events: none;
- line-height: $font-14px;
- font-size: $font-12px;
- font-weight: 500;
- max-width: 300px;
- word-break: break-word;
-
- background-color: var(--cpd-color-alpha-gray-1400);
- color: var(--cpd-color-text-on-solid-primary);
- border: 0;
- text-align: center;
-
- .mx_Tooltip_chevron {
- display: none;
- }
-
- &.mx_Tooltip_visible {
- animation: mx_fadein 0.2s forwards;
- }
-
- &.mx_Tooltip_invisible {
- animation: mx_fadeout 0.1s forwards;
- }
-
- ul,
- ol {
- text-align: start; /* for list items */
- }
-}
-
-/* These tooltips use an older style with a chevron */
-.mx_Field_tooltip {
- background-color: $menu-bg-color;
- color: $primary-content;
- border: 1px solid $menu-border-color;
- text-align: unset;
-
- .mx_Tooltip_chevron {
- display: unset;
- }
-}
-
-.mx_Tooltip_title {
- font-weight: var(--cpd-font-weight-semibold);
-}
-
-.mx_Tooltip_sub {
- opacity: 0.7;
- margin-top: 4px;
-}
diff --git a/res/css/views/elements/_Validation.pcss b/res/css/views/elements/_Validation.pcss
index 24953216df3..d50c4ae7a1e 100644
--- a/res/css/views/elements/_Validation.pcss
+++ b/res/css/views/elements/_Validation.pcss
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
.mx_Validation {
position: relative;
+ max-width: 200px;
}
.mx_Validation_details {
diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx
index 893a7d4d5bb..31deb381ddd 100644
--- a/src/components/structures/HomePage.tsx
+++ b/src/components/structures/HomePage.tsx
@@ -11,7 +11,7 @@ import { useContext, useState } from "react";
import AutoHideScrollbar from "./AutoHideScrollbar";
import { getHomePageUrl } from "../../utils/pages";
-import { _tDom } from "../../languageHandler";
+import { _t, _tDom } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
@@ -66,8 +66,8 @@ const UserWelcomeTop: React.FC = () => {
cli.setAvatarUrl(url)}
isUserAvatar
onClick={(ev) => PosthogTrackers.trackInteraction("WebHomeMiniAvatarUploadButton", ev)}
diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx
index acd5597259b..1a72872b953 100644
--- a/src/components/views/auth/EmailField.tsx
+++ b/src/components/views/auth/EmailField.tsx
@@ -6,13 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import React, { PureComponent, RefCallback, RefObject } from "react";
+import React, { ComponentProps, PureComponent, RefCallback, RefObject } from "react";
import Field, { IInputProps } from "../elements/Field";
import { _t, _td, TranslationKey } from "../../../languageHandler";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import * as Email from "../../../email";
-import { Alignment } from "../elements/Tooltip";
interface IProps extends Omit {
id?: string;
@@ -23,7 +22,7 @@ interface IProps extends Omit {
label: TranslationKey;
labelRequired: TranslationKey;
labelInvalid: TranslationKey;
- tooltipAlignment?: Alignment;
+ tooltipAlignment?: ComponentProps["tooltipAlignment"];
// When present, completely overrides the default validation rules.
validationRules?: (fieldState: IFieldState) => Promise;
diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx
index ec26099dedf..2b27d3ecaf9 100644
--- a/src/components/views/auth/PassphraseConfirmField.tsx
+++ b/src/components/views/auth/PassphraseConfirmField.tsx
@@ -6,12 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import React, { PureComponent, RefCallback, RefObject } from "react";
+import React, { ComponentProps, PureComponent, RefCallback, RefObject } from "react";
import Field, { IInputProps } from "../elements/Field";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { _t, _td, TranslationKey } from "../../../languageHandler";
-import { Alignment } from "../elements/Tooltip";
interface IProps extends Omit {
id?: string;
@@ -23,7 +22,7 @@ interface IProps extends Omit {
label: TranslationKey;
labelRequired: TranslationKey;
labelInvalid: TranslationKey;
- tooltipAlignment?: Alignment;
+ tooltipAlignment?: ComponentProps["tooltipAlignment"];
onChange(ev: React.FormEvent): void;
onValidate?(result: IValidationResult): void;
}
diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx
index 6770b141a50..90201d1ec13 100644
--- a/src/components/views/auth/PassphraseField.tsx
+++ b/src/components/views/auth/PassphraseField.tsx
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import React, { PureComponent, RefCallback, RefObject } from "react";
+import React, { ComponentProps, PureComponent, RefCallback, RefObject } from "react";
import classNames from "classnames";
import type { ZxcvbnResult } from "@zxcvbn-ts/core";
@@ -15,7 +15,6 @@ import withValidation, { IFieldState, IValidationResult } from "../elements/Vali
import { _t, _td, TranslationKey } from "../../../languageHandler";
import Field, { IInputProps } from "../elements/Field";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import { Alignment } from "../elements/Tooltip";
interface IProps extends Omit {
autoFocus?: boolean;
@@ -31,7 +30,7 @@ interface IProps extends Omit {
labelEnterPassword: TranslationKey;
labelStrongPassword: TranslationKey;
labelAllowedButUnsafe: TranslationKey;
- tooltipAlignment?: Alignment;
+ tooltipAlignment?: ComponentProps["tooltipAlignment"];
onChange(ev: React.FormEvent): void;
onValidate?(result: IValidationResult): void;
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index 4df3313758e..540e5905a35 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import React, { BaseSyntheticEvent, ReactNode } from "react";
+import React, { BaseSyntheticEvent, ComponentProps, ReactNode } from "react";
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
@@ -26,7 +26,6 @@ import RegistrationEmailPromptDialog from "../dialogs/RegistrationEmailPromptDia
import CountryDropdown from "./CountryDropdown";
import PassphraseConfirmField from "./PassphraseConfirmField";
import { PosthogAnalytics } from "../../../PosthogAnalytics";
-import { Alignment } from "../elements/Tooltip";
enum RegistrationField {
Email = "field_email",
@@ -441,9 +440,9 @@ export default class RegistrationForm extends React.PureComponent["tooltipAlignment"] | undefined {
if (this.props.mobileRegister) {
- return Alignment.Bottom;
+ return "bottom";
}
return undefined;
}
diff --git a/src/components/views/context_menus/GenericTextContextMenu.tsx b/src/components/views/context_menus/GenericTextContextMenu.tsx
deleted file mode 100644
index ac9a947c7ae..00000000000
--- a/src/components/views/context_menus/GenericTextContextMenu.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
-Copyright 2024 New Vector Ltd.
-Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
-
-SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
-Please see LICENSE files in the repository root for full details.
-*/
-
-import React from "react";
-
-interface IProps {
- message: string;
-}
-
-export default class GenericTextContextMenu extends React.Component {
- public render(): React.ReactNode {
- return (
-
- {this.props.message}
-
- );
- }
-}
diff --git a/src/components/views/elements/CopyableText.tsx b/src/components/views/elements/CopyableText.tsx
index 18c82d59920..c7b0df06793 100644
--- a/src/components/views/elements/CopyableText.tsx
+++ b/src/components/views/elements/CopyableText.tsx
@@ -21,7 +21,7 @@ interface IProps extends React.HTMLAttributes {
className?: string;
}
-const CopyableText: React.FC = ({ children, getTextToCopy, border = true, className, ...props }) => {
+export const CopyTextButton: React.FC> = ({ getTextToCopy, className }) => {
const [tooltip, setTooltip] = useState(undefined);
const onCopyClickInternal = async (e: ButtonEvent): Promise => {
@@ -37,6 +37,19 @@ const CopyableText: React.FC = ({ children, getTextToCopy, border = true
}
};
+ return (
+ {
+ if (!open) onHideTooltip();
+ }}
+ />
+ );
+};
+
+const CopyableText: React.FC = ({ children, getTextToCopy, border = true, className, ...props }) => {
const combinedClassName = classNames("mx_CopyableText", className, {
mx_CopyableText_border: border,
});
@@ -44,14 +57,7 @@ const CopyableText: React.FC = ({ children, getTextToCopy, border = true
return (
{children}
-
{
- if (!open) onHideTooltip();
- }}
- />
+
);
};
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 6cc5dffc40c..9049d2b582b 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -11,14 +11,13 @@ import React, {
TextareaHTMLAttributes,
RefObject,
createRef,
- KeyboardEvent,
+ ComponentProps,
} from "react";
import classNames from "classnames";
import { debounce } from "lodash";
+import { Tooltip } from "@vector-im/compound-web";
import { IFieldState, IValidationResult } from "./Validation";
-import Tooltip, { Alignment } from "./Tooltip";
-import { Key } from "../../../Keyboard";
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@@ -57,11 +56,11 @@ interface IProps {
forceValidity?: boolean;
// If specified, contents will appear as a tooltip on the element and
// validation feedback tooltips will be suppressed.
- tooltipContent?: React.ReactNode;
+ tooltipContent?: JSX.Element;
// If specified the tooltip will be shown regardless of feedback
forceTooltipVisible?: boolean;
// If specified, the tooltip with be aligned accorindly with the field, defaults to Right.
- tooltipAlignment?: Alignment;
+ tooltipAlignment?: ComponentProps["placement"];
// If specified alongside tooltipContent, the class name to apply to the
// tooltip itself.
tooltipClassName?: string;
@@ -112,7 +111,7 @@ type PropShapes = IInputProps | ISelectProps | ITextareaProps | INativeOnChangeI
interface IState {
valid?: boolean;
- feedback?: React.ReactNode;
+ feedback?: JSX.Element;
feedbackVisible: boolean;
focused: boolean;
}
@@ -127,6 +126,7 @@ export default class Field extends React.PureComponent {
validateOnFocus: true,
validateOnBlur: true,
validateOnChange: true,
+ tooltipAlignment: "right",
};
/*
@@ -233,16 +233,10 @@ export default class Field extends React.PureComponent {
return this.props.inputRef ?? this._inputRef;
}
- private onKeyDown = (evt: KeyboardEvent): void => {
- // If the tooltip is displayed to show a feedback and Escape is pressed
- // The tooltip is hided
- if (this.state.feedbackVisible && evt.key === Key.ESCAPE) {
- evt.preventDefault();
- evt.stopPropagation();
- this.setState({
- feedbackVisible: false,
- });
- }
+ private onTooltipOpenChange = (open: boolean): void => {
+ this.setState({
+ feedbackVisible: open,
+ });
};
public render(): React.ReactNode {
@@ -268,31 +262,15 @@ export default class Field extends React.PureComponent {
} = this.props;
// Handle displaying feedback on validity
- let fieldTooltip: JSX.Element | undefined;
+ const tooltipProps: Pick, "aria-live" | "aria-atomic"> = {};
+ let tooltipOpen = false;
if (tooltipContent || this.state.feedback) {
- const tooltipId = `${this.id}_tooltip`;
- const visible = (this.state.focused && forceTooltipVisible) || this.state.feedbackVisible;
- if (visible) {
- inputProps["aria-describedby"] = tooltipId;
- }
+ tooltipOpen = (this.state.focused && forceTooltipVisible) || this.state.feedbackVisible;
- let role: React.AriaRole;
- if (tooltipContent) {
- role = "tooltip";
- } else {
- role = this.state.valid ? "status" : "alert";
+ if (!tooltipContent) {
+ tooltipProps["aria-atomic"] = "true";
+ tooltipProps["aria-live"] = this.state.valid ? "polite" : "assertive";
}
-
- fieldTooltip = (
-
- );
}
inputProps.placeholder = inputProps.placeholder ?? inputProps.label;
@@ -332,12 +310,20 @@ export default class Field extends React.PureComponent {
});
return (
-
+
{prefixContainer}
- {fieldInput}
+
+ {fieldInput}
+
{this.props.label}
{postfixContainer}
- {fieldTooltip}
);
}
diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx
index 24a3a29ce6a..952cc98a2f8 100644
--- a/src/components/views/elements/MiniAvatarUploader.tsx
+++ b/src/components/views/elements/MiniAvatarUploader.tsx
@@ -9,11 +9,11 @@ Please see LICENSE files in the repository root for full details.
import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/matrix";
import React, { useContext, useRef, useState, MouseEvent, ReactNode } from "react";
+import { Tooltip } from "@vector-im/compound-web";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomContext from "../../../contexts/RoomContext";
import { useTimeout } from "../../../hooks/useTimeout";
-import { TranslatedString } from "../../../languageHandler";
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
import AccessibleButton from "./AccessibleButton";
import Spinner from "./Spinner";
@@ -22,8 +22,8 @@ export const AVATAR_SIZE = "52px";
interface IProps {
hasAvatar: boolean;
- noAvatarLabel?: TranslatedString;
- hasAvatarLabel?: TranslatedString;
+ noAvatarLabel?: string;
+ hasAvatarLabel?: string;
setAvatarUrl(url: string): Promise
;
isUserAvatar?: boolean;
onClick?(ev: MouseEvent): void;
@@ -82,34 +82,24 @@ const MiniAvatarUploader: React.FC = ({
accept="image/*"
/>
- {
- uploadRef.current?.click();
- }}
- onMouseOver={() => setHover(true)}
- onMouseLeave={() => setHover(false)}
- >
- {children}
-
-
-
-
+
{
+ uploadRef.current?.click();
+ }}
>
-
- {label}
-
-
+ {children}
+
+
+
+
);
};
diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx
deleted file mode 100644
index 5b7f01588b4..00000000000
--- a/src/components/views/elements/Tooltip.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
-Copyright 2024 New Vector Ltd.
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
-Copyright 2019 New Vector Ltd
-Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-Copyright 2015, 2016 OpenMarket Ltd
-
-SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
-Please see LICENSE files in the repository root for full details.
-*/
-
-import React, { CSSProperties } from "react";
-import ReactDOM from "react-dom";
-import classNames from "classnames";
-
-import UIStore from "../../../stores/UIStore";
-import { objectHasDiff } from "../../../utils/objects";
-
-export enum Alignment {
- Natural, // Pick left or right
- Left,
- Right,
- Top, // Centered
- Bottom, // Centered
- InnerBottom, // Inside the target, at the bottom
- TopRight, // On top of the target, right aligned
-}
-
-export interface ITooltipProps {
- // Class applied to the element used to position the tooltip
- className?: string;
- // Class applied to the tooltip itself
- tooltipClassName?: string;
- // Whether the tooltip is visible or hidden.
- // The hidden state allows animating the tooltip away via CSS.
- // Defaults to visible if unset.
- visible?: boolean;
- // the react element to put into the tooltip
- label: React.ReactNode;
- alignment?: Alignment; // defaults to Natural
- // id describing tooltip
- // used to associate tooltip with target for a11y
- id?: string;
- // If the parent is over this width, act as if it is only this wide
- maxParentWidth?: number;
- // aria-role passed to the tooltip
- role?: React.AriaRole;
-}
-
-type State = Partial>;
-
-/**
- * @deprecated Use [compound tooltip](https://element-hq.github.io/compound-web/?path=/docs/tooltip--docs) instead
- */
-export default class Tooltip extends React.PureComponent {
- private static container: HTMLElement;
- private parent: Element | null = null;
-
- // XXX: This is because some components (Field) are unable to `import` the Tooltip class,
- // so we expose the Alignment options off of us statically.
- public static readonly Alignment = Alignment;
-
- public static readonly defaultProps = {
- visible: true,
- alignment: Alignment.Natural,
- };
-
- public constructor(props: ITooltipProps) {
- super(props);
-
- this.state = {};
-
- // Create a wrapper for the tooltips and attach it to the body element
- if (!Tooltip.container) {
- Tooltip.container = document.createElement("div");
- Tooltip.container.className = "mx_Tooltip_wrapper";
- document.body.appendChild(Tooltip.container);
- }
- }
-
- public componentDidMount(): void {
- window.addEventListener("scroll", this.updatePosition, {
- passive: true,
- capture: true,
- });
-
- this.parent = (ReactDOM.findDOMNode(this)?.parentNode as Element) ?? null;
-
- this.updatePosition();
- }
-
- public componentDidUpdate(prevProps: ITooltipProps): void {
- if (objectHasDiff(prevProps, this.props)) {
- this.updatePosition();
- }
- }
-
- // Remove the wrapper element, as the tooltip has finished using it
- public componentWillUnmount(): void {
- window.removeEventListener("scroll", this.updatePosition, {
- capture: true,
- });
- }
-
- // Add the parent's position to the tooltips, so it's correctly
- // positioned, also taking into account any window zoom
- private updatePosition = (): void => {
- // When the tooltip is hidden, no need to thrash the DOM with `style` attribute updates (performance)
- if (!this.props.visible || !this.parent) return;
-
- const parentBox = this.parent.getBoundingClientRect();
- const width = UIStore.instance.windowWidth;
- const spacing = 6;
- const parentWidth = this.props.maxParentWidth
- ? Math.min(parentBox.width, this.props.maxParentWidth)
- : parentBox.width;
- const baseTop = parentBox.top + window.scrollY;
- const centerTop = parentBox.top + window.scrollY + parentBox.height / 2;
- const right = width - parentBox.left - window.scrollX;
- const left = parentBox.right + window.scrollX;
- const horizontalCenter = parentBox.left - window.scrollX + parentWidth / 2;
-
- const style: State = {};
- switch (this.props.alignment) {
- case Alignment.Natural:
- if (parentBox.right > width / 2) {
- style.right = right + spacing;
- style.top = centerTop;
- style.transform = "translateY(-50%)";
- break;
- }
- // fall through to Right
- case Alignment.Right:
- style.left = left + spacing;
- style.top = centerTop;
- style.transform = "translateY(-50%)";
- break;
- case Alignment.Left:
- style.right = right + spacing;
- style.top = centerTop;
- style.transform = "translateY(-50%)";
- break;
- case Alignment.Top:
- style.top = baseTop - spacing;
- // Attempt to center the tooltip on the element while clamping
- // its horizontal translation to keep it on screen
- // eslint-disable-next-line max-len
- style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))), -100%)`;
- break;
- case Alignment.Bottom:
- style.top = baseTop + parentBox.height + spacing;
- // Attempt to center the tooltip on the element while clamping
- // its horizontal translation to keep it on screen
- // eslint-disable-next-line max-len
- style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
- break;
- case Alignment.InnerBottom:
- style.top = baseTop + parentBox.height - 50;
- // Attempt to center the tooltip on the element while clamping
- // its horizontal translation to keep it on screen
- // eslint-disable-next-line max-len
- style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
- break;
- case Alignment.TopRight:
- style.top = baseTop - spacing;
- style.right = width - parentBox.right - window.scrollX;
- style.transform = "translateY(-100%)";
- break;
- }
-
- this.setState(style);
- };
-
- public render(): React.ReactNode {
- const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, {
- mx_Tooltip_visible: this.props.visible,
- mx_Tooltip_invisible: !this.props.visible,
- });
-
- const style = { ...this.state };
- // Hide the entire container when not visible.
- // This prevents flashing of the tooltip if it is not meant to be visible on first mount.
- style.display = this.props.visible ? "block" : "none";
-
- const tooltip = (
-
- );
-
- return {ReactDOM.createPortal(tooltip, Tooltip.container)}
;
- }
-}
diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx
index db944f8e98a..654c37418dd 100644
--- a/src/components/views/elements/Validation.tsx
+++ b/src/components/views/elements/Validation.tsx
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
-import React, { ReactChild, ReactNode } from "react";
+import React, { ReactNode } from "react";
import classNames from "classnames";
import memoizeOne from "memoize-one";
@@ -44,7 +44,7 @@ export interface IFieldState {
export interface IValidationResult {
valid?: boolean;
- feedback?: React.ReactChild;
+ feedback?: JSX.Element;
}
/**
@@ -189,7 +189,7 @@ export default function withValidation({
summary = content ? {content}
: undefined;
}
- let feedback: ReactChild | undefined;
+ let feedback: JSX.Element | undefined;
if (summary || details) {
feedback = (
diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx
index eb1c94e20da..3a73c4c7587 100644
--- a/src/components/views/messages/TextualBody.tsx
+++ b/src/components/views/messages/TextualBody.tsx
@@ -10,23 +10,20 @@ import React, { createRef, SyntheticEvent, MouseEvent } from "react";
import ReactDOM from "react-dom";
import { MsgType } from "matrix-js-sdk/src/matrix";
import { TooltipProvider } from "@vector-im/compound-web";
+import classNames from "classnames";
import * as HtmlUtils from "../../../HtmlUtils";
import { formatDate } from "../../../DateUtils";
import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher";
import { _t } from "../../../languageHandler";
-import * as ContextMenu from "../../structures/ContextMenu";
-import { ChevronFace, toRightOf } from "../../structures/ContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
import { pillifyLinks, unmountPills } from "../../../utils/pillify";
import { tooltipifyLinks, unmountTooltips } from "../../../utils/tooltipify";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import { isPermalinkHost, tryTransformPermalinkToLocalHref } from "../../../utils/permalinks/Permalinks";
-import { copyPlaintext } from "../../../utils/strings";
import UIStore from "../../../stores/UIStore";
import { Action } from "../../../dispatcher/actions";
-import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
import Spoiler from "../elements/Spoiler";
import QuestionDialog from "../dialogs/QuestionDialog";
import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
@@ -40,6 +37,7 @@ import { getParentEventId } from "../../../utils/Reply";
import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
import { IEventTileOps } from "../rooms/EventTile";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { CopyTextButton } from "../elements/CopyableText.tsx";
const MAX_HIGHLIGHT_LENGTH = 4096;
@@ -57,6 +55,7 @@ export default class TextualBody extends React.Component
{
private unmounted = false;
private pills: Element[] = [];
private tooltips: Element[] = [];
+ private reactRoots: Element[] = [];
public static contextType = RoomContext;
public declare context: React.ContextType;
@@ -168,27 +167,24 @@ export default class TextualBody extends React.Component {
}
private addCodeCopyButton(div: HTMLDivElement): void {
- const button = document.createElement("span");
- button.className = "mx_EventTile_button mx_EventTile_copyButton ";
+ const root = document.createElement("div");
+ div.appendChild(root);
+ this.reactRoots.push(root);
// 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";
-
- button.onclick = async (): Promise => {
- const copyCode = button.parentElement?.getElementsByTagName("code")[0];
- const successful = copyCode?.textContent ? await copyPlaintext(copyCode.textContent) : false;
-
- const buttonRect = button.getBoundingClientRect();
- const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
- ...toRightOf(buttonRect, 0),
- chevronFace: ChevronFace.None,
- message: successful ? _t("common|copied") : _t("error|failed_copy"),
- });
- button.onmouseleave = close;
- };
- div.appendChild(button);
+ ReactDOM.render(
+
+ div.getElementsByTagName("code")[0]?.textContent ?? null}
+ className={classNames("mx_EventTile_button mx_EventTile_copyButton", {
+ mx_EventTile_buttonBottom: expansionButtonExists.length > 0,
+ })}
+ />
+ ,
+ root,
+ );
}
private wrapInDiv(pre: HTMLPreElement): HTMLDivElement {
@@ -285,8 +281,13 @@ export default class TextualBody extends React.Component {
unmountPills(this.pills);
unmountTooltips(this.tooltips);
+ for (const root of this.reactRoots) {
+ ReactDOM.unmountComponentAtNode(root);
+ }
+
this.pills = [];
this.tooltips = [];
+ this.reactRoots = [];
}
public shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean {
diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx
index 5b6645b1642..6b00bfb0e25 100644
--- a/src/components/views/settings/SetIdServer.tsx
+++ b/src/components/views/settings/SetIdServer.tsx
@@ -120,7 +120,7 @@ export default class SetIdServer extends React.Component {
this.setState({ idServer: u });
};
- private getTooltip = (): ReactNode => {
+ private getTooltip = (): JSX.Element | undefined => {
if (this.state.checking) {
return (
@@ -131,7 +131,7 @@ export default class SetIdServer extends React.Component {
} else if (this.state.error) {
return {this.state.error} ;
} else {
- return null;
+ return undefined;
}
};