From 4f14d3f5ae86cd2a07f95f2f6130643ec0f13328 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 6 Dec 2024 11:41:40 +0000 Subject: [PATCH] Stash Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 6 +- src/@types/react.d.ts | 7 +- src/NodeAnimator.tsx | 11 +- src/accessibility/RovingTabIndex.tsx | 2 +- .../context_menu/ContextMenuButton.tsx | 8 +- .../context_menu/ContextMenuTooltipButton.tsx | 8 +- src/accessibility/context_menu/MenuItem.tsx | 12 +- .../roving/RovingAccessibleButton.tsx | 9 +- .../structures/AutoHideScrollbar.tsx | 19 +-- src/components/structures/ContextMenu.tsx | 2 +- .../structures/IndicatorScrollbar.tsx | 12 +- .../auth/header/AuthHeaderContext.tsx | 6 +- .../auth/header/AuthHeaderProvider.tsx | 4 +- .../context_menus/IconizedContextMenu.tsx | 12 +- .../context_menus/MessageContextMenu.tsx | 1 - .../views/dialogs/spotlight/TooltipOption.tsx | 6 +- .../views/elements/AccessibleButton.tsx | 122 +++++++++--------- src/components/views/elements/AppTile.tsx | 9 +- src/components/views/location/ShareType.tsx | 2 +- src/components/views/messages/IBodyProps.ts | 3 + src/components/views/messages/MFileBody.tsx | 9 +- .../views/messages/MessageEvent.tsx | 14 +- .../views/room_settings/AliasSettings.tsx | 6 +- src/components/views/rooms/ReplyTile.tsx | 5 +- .../views/rooms/RoomContextDetails.tsx | 6 +- .../rooms/wysiwyg_composer/hooks/utils.ts | 8 +- .../views/settings/UserProfileSettings.tsx | 1 - .../devices/DeviceExpandDetailsButton.tsx | 4 +- .../views/spaces/SpaceTreeLevel.tsx | 4 +- src/events/EventTileFactory.tsx | 5 +- src/modules/ProxiedModuleApi.ts | 4 +- src/utils/react.tsx | 4 +- src/vector/app.tsx | 4 +- test/unit-tests/HtmlUtils-test.tsx | 7 +- .../components/structures/RoomView-test.tsx | 2 +- .../views/audio_messages/SeekBar-test.tsx | 2 +- .../rooms/VoiceRecordComposerTile-test.tsx | 2 +- .../modules/ProxiedModuleApi-test.tsx | 4 +- yarn.lock | 29 ++--- 39 files changed, 182 insertions(+), 199 deletions(-) diff --git a/package.json b/package.json index 71801370e5..086cc8b255 100644 --- a/package.json +++ b/package.json @@ -71,9 +71,13 @@ "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js" }, "resolutions": { + "@types/react": "19.0.0", + "@types/react-dom": "19.0.0", "oidc-client-ts": "3.1.0", "jwt-decode": "4.0.0", "caniuse-lite": "1.0.30001684", + "react": "19.0.0", + "react-dom": "19.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, @@ -179,7 +183,7 @@ "@svgr/webpack": "^8.0.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.8", - "@testing-library/react": "^16.0.0", + "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", diff --git a/src/@types/react.d.ts b/src/@types/react.d.ts index 0c069e8324..3c2395971d 100644 --- a/src/@types/react.d.ts +++ b/src/@types/react.d.ts @@ -15,5 +15,10 @@ declare module "react" { ): (props: P & React.RefAttributes) => React.ReactElement | null; // Fix lazy types - https://stackoverflow.com/a/71017028 - function lazy>(factory: () => Promise<{ default: T }>): T; + // function lazy>(factory: () => Promise<{ default: T }>): T; + + // Workaround for generics in React 19 + interface FunctionComponent { + defaultProps?: {}; + } } diff --git a/src/NodeAnimator.tsx b/src/NodeAnimator.tsx index 4f8c7e92e6..d723b98914 100644 --- a/src/NodeAnimator.tsx +++ b/src/NodeAnimator.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, { Key, MutableRefObject, ReactElement, RefCallback } from "react"; +import React, { HTMLAttributes, Key, MutableRefObject, ReactElement, RefCallback } from "react"; interface IChildProps { style: React.CSSProperties; @@ -68,21 +68,22 @@ export default class NodeAnimator extends React.Component { this.children = {}; React.Children.toArray(newChildren).forEach((c) => { if (!isReactElement(c)) return; + const props = c.props as HTMLAttributes; if (oldChildren[c.key!]) { const old = oldChildren[c.key!]; const oldNode = this.nodes[old.key!]; - if (oldNode && oldNode.style.left !== c.props.style.left) { - this.applyStyles(oldNode, { left: c.props.style.left }); + if (oldNode && oldNode.style.left !== props.style!.left) { + this.applyStyles(oldNode, { left: props.style!.left }); } // clone the old element with the props (and children) of the new element // so prop updates are still received by the children. - this.children[c.key!] = React.cloneElement(old, c.props, c.props.children); + this.children[c.key!] = React.cloneElement(old, props, props.children); } else { // new element. If we have a startStyle, use that as the style and go through // the enter animations const newProps: Partial = {}; - const restingStyle = c.props.style; + const restingStyle = props.style!; const startStyles = this.props.startStyles; if (startStyles.length > 0) { diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 9d56e09422..7ad04c37ca 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -212,7 +212,7 @@ export const RovingTabIndexProvider: React.FC = ({ scrollIntoView, onKeyDown, }) => { - const [state, dispatch] = useReducer>(reducer, { + const [state, dispatch] = useReducer(reducer, { nodes: [], }); diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index e665ff9178..64c8b11d3c 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -8,25 +8,25 @@ 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, { ComponentProps, forwardRef, Ref, type JSX } from "react"; +import React, { ComponentProps, forwardRef, Ref } from "react"; import AccessibleButton from "../../components/views/elements/AccessibleButton"; -type Props = ComponentProps> & { +type Props = ComponentProps> & { label?: string; // whether the context menu is currently open isExpanded: boolean; }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuButton = forwardRef(function ( +export const ContextMenuButton = forwardRef(function ( { label, isExpanded, children, onClick, onContextMenu, element, ...props }: Props, ref: Ref, ) { return ( = ComponentProps> & { +type Props = ComponentProps> & { // whether the context menu is currently open isExpanded: boolean; }; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuTooltipButton = forwardRef(function ( +export const ContextMenuTooltipButton = forwardRef(function ( { isExpanded, children, onClick, onContextMenu, element, ...props }: Props, ref: Ref, ) { return ( { +type IProps = React.ComponentProps> & { label?: string; -} +}; // Semantic component for representing a role=menuitem -export const MenuItem: React.FC = ({ children, label, ...props }) => { +export const MenuItem = ({ + children, + label, + ...props +}: IProps): JSX.Element => { const ariaLabel = props["aria-label"] || label; return ( diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index ce10a086e1..432b8a9bc2 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -12,16 +12,13 @@ import AccessibleButton from "../../components/views/elements/AccessibleButton"; import { useRovingTabIndex } from "../RovingTabIndex"; import { Ref } from "./types"; -type Props = Omit< - ComponentProps>, - "inputRef" | "tabIndex" -> & { +type Props = Omit>, "inputRef" | "tabIndex"> & { inputRef?: Ref; focusOnMouseOver?: boolean; }; // Wrapper to allow use of useRovingTabIndex for simple AccessibleButtons outside of React Functional Components. -export const RovingAccessibleButton = ({ +export const RovingAccessibleButton = ({ inputRef, onFocus, onMouseOver, @@ -33,7 +30,7 @@ export const RovingAccessibleButton = ({ return ( { onFocusInternal(); onFocus?.(event); diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 1885041226..a5f98b6048 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -8,16 +8,11 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { HTMLAttributes, ReactHTML, ReactNode, WheelEvent, type JSX } from "react"; +import React, { ReactNode, WheelEvent } from "react"; -type DynamicHtmlElementProps = - JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">; -type DynamicElementProps = Partial>; - -export type IProps = Omit, "onScroll"> & { - element: T; +export type IProps = React.ComponentPropsWithoutRef & { + element?: T; className?: string; - onScroll?: (event: Event) => void; onWheel?: (event: WheelEvent) => void; style?: React.CSSProperties; tabIndex?: number; @@ -25,11 +20,7 @@ export type IProps = Omit extends React.Component> { - public static defaultProps = { - element: "div" as keyof ReactHTML, - }; - +export default class AutoHideScrollbar extends React.Component> { public readonly containerRef: React.RefObject = React.createRef(); public componentDidMount(): void { @@ -55,7 +46,7 @@ export default class AutoHideScrollbar ex const { element, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = this.props; return React.createElement( - element, + element ?? "div", { ...otherProps, ref: this.containerRef, diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 55cd63cc64..6e037f597b 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -440,7 +440,7 @@ export default class ContextMenu extends React.PureComponent | number | string { + public render(): JSX.Element { if (this.props.mountAsChild) { // Render as a child of the current parent return this.renderMenu(); diff --git a/src/components/structures/IndicatorScrollbar.tsx b/src/components/structures/IndicatorScrollbar.tsx index c7fbe8e02c..b64995e6d5 100644 --- a/src/components/structures/IndicatorScrollbar.tsx +++ b/src/components/structures/IndicatorScrollbar.tsx @@ -5,13 +5,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, { createRef, type JSX } from "react"; +import React, { ComponentProps, createRef } from "react"; -import AutoHideScrollbar, { IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar"; +import AutoHideScrollbar from "./AutoHideScrollbar"; import UIStore, { UI_EVENTS } from "../../stores/UIStore"; -export type IProps = Omit, "onWheel" | "element"> & { - element?: T; +export type IProps = Omit>, "onWheel"> & { // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator // and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning // by the parent element. @@ -30,10 +29,7 @@ interface IState { rightIndicatorOffset: string; } -export default class IndicatorScrollbar extends React.Component< - IProps, - IState -> { +export default class IndicatorScrollbar extends React.Component, IState> { private autoHideScrollbar = createRef>(); private scrollElement?: HTMLDivElement; private likelyTrackpadUser: boolean | null = null; diff --git a/src/components/structures/auth/header/AuthHeaderContext.tsx b/src/components/structures/auth/header/AuthHeaderContext.tsx index 97d587f40b..e407be0f43 100644 --- a/src/components/structures/auth/header/AuthHeaderContext.tsx +++ b/src/components/structures/auth/header/AuthHeaderContext.tsx @@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { createContext, Dispatch, ReducerAction, ReducerState } from "react"; +import { createContext, Dispatch, ReducerState } from "react"; -import type { AuthHeaderReducer } from "./AuthHeaderProvider"; +import type { AuthHeaderAction, AuthHeaderReducer } from "./AuthHeaderProvider"; interface AuthHeaderContextType { state: ReducerState; - dispatch: Dispatch>; + dispatch: Dispatch; } export const AuthHeaderContext = createContext(undefined); diff --git a/src/components/structures/auth/header/AuthHeaderProvider.tsx b/src/components/structures/auth/header/AuthHeaderProvider.tsx index 0626020a5e..ceabb12e5f 100644 --- a/src/components/structures/auth/header/AuthHeaderProvider.tsx +++ b/src/components/structures/auth/header/AuthHeaderProvider.tsx @@ -17,7 +17,7 @@ export enum AuthHeaderActionType { Remove, } -interface AuthHeaderAction { +export interface AuthHeaderAction { type: AuthHeaderActionType; value: ComponentProps; } @@ -25,7 +25,7 @@ interface AuthHeaderAction { export type AuthHeaderReducer = Reducer[], AuthHeaderAction>; export function AuthHeaderProvider({ children }: PropsWithChildren<{}>): JSX.Element { - const [state, dispatch] = useReducer( + const [state, dispatch] = useReducer( (state: ComponentProps[], action: AuthHeaderAction) => { switch (action.type) { case AuthHeaderActionType.Add: diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx index d664c87018..092ff8c58a 100644 --- a/src/components/views/context_menus/IconizedContextMenu.tsx +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -17,6 +17,7 @@ import ContextMenu, { MenuItemRadio, } from "../../structures/ContextMenu"; import { _t } from "../../../languageHandler"; +import AccessibleButton from "../elements/AccessibleButton.tsx"; interface IProps extends IContextMenuProps { className?: string; @@ -31,10 +32,10 @@ interface IOptionListProps { children: ReactNode; } -interface IOptionProps extends React.ComponentProps { +type IOptionProps = React.ComponentProps> & { iconClassName?: string; isDestructive?: boolean; -} +}; interface ICheckboxProps extends React.ComponentProps { iconClassName: string; @@ -110,18 +111,19 @@ export const IconizedContextMenuCheckbox: React.FC = ({ ); }; -export const IconizedContextMenuOption: React.FC = ({ +export const IconizedContextMenuOption = ({ + element, label, className, iconClassName, children, isDestructive, ...props -}) => { +}: IOptionProps): JSX.Element => { return ( openInMapSiteButton = ( = ButtonProps & { +type TooltipOptionProps = ButtonProps & { endAdornment?: ReactNode; inputRef?: Ref; }; -export const TooltipOption = ({ +export const TooltipOption = ({ inputRef, className, element, @@ -34,7 +34,7 @@ export const TooltipOption = ({ tabIndex={-1} aria-selected={isActive} role="option" - element={element as keyof JSX.IntrinsicElements} + element={element as React.ElementType} /> ); }; diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 4d842a1c20..ca7fa7856a 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -6,22 +6,14 @@ 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, { - ComponentProps, - forwardRef, - FunctionComponent, - HTMLAttributes, - InputHTMLAttributes, - Ref, - type JSX, -} from "react"; +import React, { ComponentProps, ComponentPropsWithRef, FunctionComponent, type JSX, PropsWithChildren } from "react"; import classnames from "classnames"; import { Tooltip } from "@vector-im/compound-web"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -export type ButtonEvent = React.MouseEvent | React.KeyboardEvent | React.FormEvent; +export type ButtonEvent = React.MouseEvent | React.KeyboardEvent | React.FormEvent; /** * The kind of button, similar to how Bootstrap works. @@ -54,25 +46,11 @@ export type AccessibleButtonKind = * * To remain compatible with existing code, we’ll continue to support InputHTMLAttributes */ -type DynamicHtmlElementProps = - JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">; -type DynamicElementProps = Partial< - Omit -> & - Omit, "onClick">; - -type TooltipProps = ComponentProps; - -/** - * Type of props accepted by {@link AccessibleButton}. - * - * Extends props accepted by the underlying element specified using the `element` prop. - */ -type Props = DynamicHtmlElementProps & { +type ButtonOwnProps = PropsWithChildren<{ /** * The base element type. "div" by default. */ - element?: T; + element?: C; /** * The kind of button, similar to how Bootstrap works. */ @@ -88,7 +66,7 @@ type Props = DynamicHtmlElementProps & /** * Event handler for button activation. Should be implemented exactly like a normal `onClick` handler. */ - onClick: ((e: ButtonEvent) => void | Promise) | null; + onClick: ((e: ButtonEvent) => void | Promise) | null; /** * The tooltip to show on hover or focus. */ @@ -111,16 +89,46 @@ type Props = DynamicHtmlElementProps & * Whether the tooltip should be disabled. */ disableTooltip?: TooltipProps["disabled"]; -}; +}>; -export type ButtonProps = Props; +// type UnstyledButtonPropsFor = React.ComponentPropsWithoutRef & { +// ref?: React.Ref>; +// }; + +type ButtonPropsFor = ButtonOwnProps & ComponentProps; + +// type DynamicHtmlElementProps = +// JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">; +// type DynamicElementProps = Partial< +// Omit +// > & +// Omit, "onClick">; + +type TooltipProps = ComponentProps; + +/** + * Type of props accepted by {@link AccessibleButton}. + * + * Extends props accepted by the underlying element specified using the `element` prop. + */ +type Props = ButtonPropsFor; + +export type ButtonProps = Props; + +type SupportedElement = "div"; // | "a" | "button"; /** * Type of the props passed to the element that is rendered by AccessibleButton. */ -interface RenderedElementProps extends React.InputHTMLAttributes { - ref?: React.Ref; -} +type RenderedElementProps = ComponentPropsWithRef & { + // TODO + disabled?: boolean; + + /** + * Event handler for button activation. Should be implemented exactly like a normal `onClick` handler. + */ + onClick: ((e: ButtonEvent) => void | Promise) | null; +}; /** * AccessibleButton is a generic wrapper for any element that should be treated @@ -132,27 +140,27 @@ interface RenderedElementProps extends React.InputHTMLAttributes { * @param {Object} props react element properties * @returns {Object} rendered react */ -const AccessibleButton = forwardRef(function ( - { - element = "div" as T, - onClick, - children, - kind, - disabled, - className, - onKeyDown, - onKeyUp, - triggerOnMouseDown, - title, - caption, - placement = "right", - onTooltipOpenChange, - disableTooltip, - ...restProps - }: Props, - ref: Ref, -): JSX.Element { - const newProps: RenderedElementProps = restProps; +const AccessibleButton = function ({ + element = "div" as T, + onClick, + children, + kind, + disabled, + className, + onKeyDown, + onKeyUp, + triggerOnMouseDown, + title, + caption, + placement = "right", + onTooltipOpenChange, + disableTooltip, + role = "button", + tabIndex = 0, + ref, + ...restProps +}: Props): JSX.Element { + const newProps: RenderedElementProps = { ...restProps, role, tabIndex }; newProps["aria-label"] = newProps["aria-label"] ?? title; if (disabled) { newProps["aria-disabled"] = true; @@ -170,7 +178,7 @@ const AccessibleButton = forwardRef(function { + newProps.onKeyDown = (e: React.KeyboardEvent) => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { @@ -232,13 +240,9 @@ const AccessibleButton = forwardRef(function & { onClick?: ((e: ButtonEvent) => void | Promise) | null; }; const ShareTypeOption: React.FC = ({ onClick, label, shareType, ...rest }) => ( - + {shareType === LocationShareType.Own && } {shareType === LocationShareType.Pin && ( diff --git a/src/components/views/messages/IBodyProps.ts b/src/components/views/messages/IBodyProps.ts index 2e1e6ee4b6..eb89763519 100644 --- a/src/components/views/messages/IBodyProps.ts +++ b/src/components/views/messages/IBodyProps.ts @@ -51,4 +51,7 @@ export interface IBodyProps { // Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order. // This may be useful when displaying a preview of the event. inhibitInteraction?: boolean; + + /* Whether to show the default placeholder for files. Defaults to true. */ + showGenericPlaceholder?: boolean; } diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 9d6f4d1a02..28ec202afe 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -91,16 +91,11 @@ export function computedStyle(element: HTMLElement | null): string { return cssText; } -interface IProps extends IBodyProps { - /* whether or not to show the default placeholder for the file. Defaults to true. */ - showGenericPlaceholder: boolean; -} - interface IState { decryptedBlob?: Blob; } -export default class MFileBody extends React.Component { +export default class MFileBody extends React.Component { public static contextType = RoomContext; declare public context: React.ContextType; @@ -147,7 +142,7 @@ export default class MFileBody extends React.Component { }); } - public componentDidUpdate(prevProps: IProps, prevState: IState): void { + public componentDidUpdate(prevProps: IBodyProps, prevState: IState): void { if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) { this.props.onHeightChanged(); } diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index d8a3d3b813..7c53c711de 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -58,7 +58,7 @@ export interface IOperableEventTile { getEventTileOps(): IEventTileOps | null; } -const baseBodyTypes = new Map([ +const baseBodyTypes = new Map>([ [MsgType.Text, TextualBody], [MsgType.Notice, TextualBody], [MsgType.Emote, TextualBody], @@ -80,14 +80,14 @@ const baseEvTypes = new Map>([ export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { private body: React.RefObject = createRef(); private mediaHelper?: MediaEventHelper; - private bodyTypes = new Map(baseBodyTypes.entries()); - private evTypes = new Map>(baseEvTypes.entries()); + private bodyTypes = new Map(baseBodyTypes.entries()); + private evTypes = new Map(baseEvTypes.entries()); public static contextType = MatrixClientContext; declare public context: React.ContextType; - public constructor(props: IProps, context: React.ContextType) { - super(props, context); + public constructor(props: IProps) { + super(props); if (MediaEventHelper.isEligible(this.props.mxEvent)) { this.mediaHelper = new MediaEventHelper(this.props.mxEvent); @@ -115,12 +115,12 @@ export default class MessageEvent extends React.Component implements IMe } private updateComponentMaps(): void { - this.bodyTypes = new Map(baseBodyTypes.entries()); + this.bodyTypes = new Map(baseBodyTypes.entries()); for (const [bodyType, bodyComponent] of Object.entries(this.props.overrideBodyTypes ?? {})) { this.bodyTypes.set(bodyType, bodyComponent); } - this.evTypes = new Map>(baseEvTypes.entries()); + this.evTypes = new Map(baseEvTypes.entries()); for (const [evType, evComponent] of Object.entries(this.props.overrideEventTypes ?? {})) { this.evTypes.set(evType, evComponent); } diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index 6392b21eb5..318069f3db 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.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, { ChangeEvent, ContextType, createRef, SyntheticEvent, type JSX } from "react"; +import React, { ChangeEvent, ToggleEvent, ContextType, createRef, SyntheticEvent, type JSX } from "react"; import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { RoomCanonicalAliasEventContent } from "matrix-js-sdk/src/types"; @@ -278,9 +278,9 @@ export default class AliasSettings extends React.Component { }); }; - private onLocalAliasesToggled = (event: ChangeEvent): void => { + private onLocalAliasesToggled = (event: ToggleEvent): void => { // expanded - if (event.target.open) { + if (event.currentTarget.open) { // if local aliases haven't been preloaded yet at component mount if (!this.props.canSetCanonicalAlias && this.state.localAliases.length === 0) { this.loadLocalAliases(); diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 6841d984de..13021803b4 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -26,6 +26,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { renderReplyTile } from "../../../events/EventTileFactory"; import { GetRelationsForEvent } from "../rooms/EventTile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { IBodyProps } from "../messages/IBodyProps.ts"; interface IProps { mxEvent: MatrixEvent; @@ -139,13 +140,13 @@ export default class ReplyTile extends React.PureComponent { ); } - const msgtypeOverrides: Record = { + const msgtypeOverrides: Record> = { [MsgType.Image]: MImageReplyBody, // Override audio and video body with file body. We also hide the download/decrypt button using CSS [MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody, [MsgType.Video]: MFileBody, }; - const evOverrides: Record = { + const evOverrides: Record> = { // Use MImageReplyBody so that the sticker isn't taking up a lot of space [EventType.Sticker]: MImageReplyBody, }; diff --git a/src/components/views/rooms/RoomContextDetails.tsx b/src/components/views/rooms/RoomContextDetails.tsx index 25a7e408f7..480cef0ad0 100644 --- a/src/components/views/rooms/RoomContextDetails.tsx +++ b/src/components/views/rooms/RoomContextDetails.tsx @@ -7,16 +7,16 @@ Please see LICENSE files in the repository root for full details. */ import { Room } from "matrix-js-sdk/src/matrix"; -import React, { HTMLAttributes, ReactHTML, type JSX } from "react"; +import React, { ElementType, HTMLAttributes, type JSX } from "react"; import { roomContextDetails } from "../../../utils/i18n-helpers"; -type Props = HTMLAttributes & { +type Props = HTMLAttributes & { component?: T; room: Room; }; -export function RoomContextDetails({ room, component, ...other }: Props): JSX.Element { +export function RoomContextDetails({ room, component, ...other }: Props): JSX.Element { const contextDetails = roomContextDetails(room); if (contextDetails) { return React.createElement( diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts index 01763fda6a..c4af09fb10 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/utils.ts @@ -80,19 +80,19 @@ export function handleEventWithAutocomplete( switch (autocompleteAction) { case KeyBindingAction.ForceCompleteAutocomplete: case KeyBindingAction.CompleteAutocomplete: - autocompleteRef.current.onConfirmCompletion(); + component.onConfirmCompletion(); handled = true; break; case KeyBindingAction.PrevSelectionInAutocomplete: - autocompleteRef.current.moveSelection(-1); + component.moveSelection(-1); handled = true; break; case KeyBindingAction.NextSelectionInAutocomplete: - autocompleteRef.current.moveSelection(1); + component.moveSelection(1); handled = true; break; case KeyBindingAction.CancelAutocomplete: - autocompleteRef.current.onEscape(event as {} as React.KeyboardEvent); + component.onEscape(event as {} as React.KeyboardEvent); handled = true; break; default: diff --git a/src/components/views/settings/UserProfileSettings.tsx b/src/components/views/settings/UserProfileSettings.tsx index 83a00c122d..23e5b9fa94 100644 --- a/src/components/views/settings/UserProfileSettings.tsx +++ b/src/components/views/settings/UserProfileSettings.tsx @@ -59,7 +59,6 @@ interface ManageAccountButtonProps { const ManageAccountButton: React.FC = ({ externalAccountManagementUrl }) => ( = Omit< +type Props = Omit< ComponentProps>, "aria-label" | "title" | "kind" | "className" | "onClick" | "element" > & { @@ -21,7 +21,7 @@ type Props = Omit< onClick: () => void; }; -export const DeviceExpandDetailsButton = ({ +export const DeviceExpandDetailsButton = ({ isExpanded, onClick, ...rest diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 3ac279120c..cfff57c910 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -40,7 +40,7 @@ import SpaceContextMenu from "../context_menus/SpaceContextMenu"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -type ButtonProps = Omit< +type ButtonProps = Omit< ComponentProps>, "title" | "onClick" | "size" | "element" > & { @@ -58,7 +58,7 @@ type ButtonProps = Omit< onClick?(ev?: ButtonEvent): void; }; -export const SpaceButton = ({ +export const SpaceButton = ({ space, spaceKey: _spaceKey, className, diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 379c54c6ae..45abb107f6 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -42,6 +42,7 @@ import HiddenBody from "../components/views/messages/HiddenBody"; import ViewSourceEvent from "../components/views/messages/ViewSourceEvent"; import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline"; import { ElementCall } from "../models/Call"; +import { IBodyProps } from "../components/views/messages/IBodyProps.ts"; // Subset of EventTile's IProps plus some mixins export interface EventTileTypeProps @@ -64,8 +65,8 @@ export interface EventTileTypeProps ref?: React.RefObject; // `any` because it's effectively impossible to convince TS of a reasonable type timestamp?: JSX.Element; maxImageHeight?: number; // pixels - overrideBodyTypes?: Record; - overrideEventTypes?: Record; + overrideBodyTypes?: Record>; + overrideEventTypes?: Record>; } type FactoryProps = Omit; diff --git a/src/modules/ProxiedModuleApi.ts b/src/modules/ProxiedModuleApi.ts index dbffacdecc..9cdeaa5cdf 100644 --- a/src/modules/ProxiedModuleApi.ts +++ b/src/modules/ProxiedModuleApi.ts @@ -10,7 +10,7 @@ import { ModuleApi } from "@matrix-org/react-sdk-module-api/lib/ModuleApi"; import { TranslationStringsObject, PlainSubstitution } from "@matrix-org/react-sdk-module-api/lib/types/translations"; import { Optional } from "matrix-events-sdk"; import { DialogContent, DialogProps } from "@matrix-org/react-sdk-module-api/lib/components/DialogContent"; -import React from "react"; +import React, { type JSX } from "react"; import { AccountAuthInfo } from "@matrix-org/react-sdk-module-api/lib/types/AccountAuthInfo"; import * as Matrix from "matrix-js-sdk/src/matrix"; import { IRegisterRequestParams } from "matrix-js-sdk/src/matrix"; @@ -78,7 +78,7 @@ export class ProxiedModuleApi implements ModuleApi { */ public openDialog>( initialTitleOrOptions: string | ModuleUiDialogOptions, - body: (props: P, ref: React.RefObject) => React.ReactNode, + body: (props: P, ref: React.RefObject) => JSX.Element, props?: Omit, ): Promise<{ didOkOrSubmit: boolean; model: M }> { const initialOptions: ModuleUiDialogOptions = diff --git a/src/utils/react.tsx b/src/utils/react.tsx index b78f574fa9..4d2c8e12ca 100644 --- a/src/utils/react.tsx +++ b/src/utils/react.tsx @@ -5,7 +5,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 { ReactNode } from "react"; +import { type JSX } from "react"; import { createRoot, Root } from "react-dom/client"; /** @@ -27,7 +27,7 @@ export class ReactRootManager { * @param rootElement the root element to render the component into * @param revertElement the element to replace the root element with when unmounting */ - public render(children: ReactNode, rootElement: Element, revertElement?: Element): void { + public render(children: JSX.Element, rootElement: Element, revertElement?: Element): void { const root = createRoot(rootElement); this.roots.push(root); this.rootElements.push(rootElement); diff --git a/src/vector/app.tsx b/src/vector/app.tsx index 5558f23b28..6486d117bf 100644 --- a/src/vector/app.tsx +++ b/src/vector/app.tsx @@ -12,7 +12,7 @@ Please see LICENSE files in the repository root for full details. // To ensure we load the browser-matrix version first import "matrix-js-sdk/src/browser-index"; -import React, { ReactElement, StrictMode } from "react"; +import React, { type JSX, StrictMode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { createClient, AutoDiscovery, ClientConfig } from "matrix-js-sdk/src/matrix"; import { WrapperLifecycle, WrapperOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WrapperLifecycle"; @@ -54,7 +54,7 @@ function onTokenLoginCompleted(): void { window.history.replaceState(null, "", url.href); } -export async function loadApp(fragParams: {}, matrixChatRef: React.Ref): Promise> { +export async function loadApp(fragParams: {}, matrixChatRef: React.Ref): Promise { initRouting(); const platform = PlatformPeg.get(); diff --git a/test/unit-tests/HtmlUtils-test.tsx b/test/unit-tests/HtmlUtils-test.tsx index a13c04af03..71635b6626 100644 --- a/test/unit-tests/HtmlUtils-test.tsx +++ b/test/unit-tests/HtmlUtils-test.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, { ReactElement } from "react"; +import React, { DOMAttributes, ReactElement } from "react"; import { mocked } from "jest-mock"; import { render, screen } from "jest-matrix-react"; import { IContent } from "matrix-js-sdk/src/matrix"; @@ -57,8 +57,9 @@ describe("topicToHtml", () => { }); describe("bodyToHtml", () => { - function getHtml(content: IContent, highlights?: string[]): string { - return (bodyToSpan(content, highlights, {}) as ReactElement).props.dangerouslySetInnerHTML.__html; + function getHtml(content: IContent, highlights?: string[]): string | TrustedHTML | undefined { + return ((bodyToSpan(content, highlights, {}) as ReactElement).props as DOMAttributes) + .dangerouslySetInnerHTML?.__html; } it("should apply highlights to HTML messages", () => { diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index 385204c01b..6cee390069 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -118,7 +118,7 @@ describe("RoomView", () => { cleanup(); }); - const mountRoomView = async (ref?: RefObject): Promise => { + const mountRoomView = async (ref?: RefObject): Promise => { if (stores.roomViewStore.getRoomId() !== room.roomId) { const switchedRoom = new Promise((resolve) => { const subFn = () => { diff --git a/test/unit-tests/components/views/audio_messages/SeekBar-test.tsx b/test/unit-tests/components/views/audio_messages/SeekBar-test.tsx index 2ea445b499..996d1d7d88 100644 --- a/test/unit-tests/components/views/audio_messages/SeekBar-test.tsx +++ b/test/unit-tests/components/views/audio_messages/SeekBar-test.tsx @@ -18,7 +18,7 @@ describe("SeekBar", () => { let playback: Playback; let renderResult: RenderResult; let frameRequestCallback: FrameRequestCallback; - let seekBarRef: RefObject; + let seekBarRef: RefObject; beforeEach(() => { seekBarRef = createRef(); diff --git a/test/unit-tests/components/views/rooms/VoiceRecordComposerTile-test.tsx b/test/unit-tests/components/views/rooms/VoiceRecordComposerTile-test.tsx index a8d4b022a6..5f7ff56f9f 100644 --- a/test/unit-tests/components/views/rooms/VoiceRecordComposerTile-test.tsx +++ b/test/unit-tests/components/views/rooms/VoiceRecordComposerTile-test.tsx @@ -34,7 +34,7 @@ jest.mock("../../../../../src/stores/VoiceRecordingStore", () => ({ })); describe("", () => { - let voiceRecordComposerTile: RefObject; + let voiceRecordComposerTile: RefObject; let mockRecorder: VoiceMessageRecording; let mockUpload: IUpload; let mockClient: MatrixClient; diff --git a/test/unit-tests/modules/ProxiedModuleApi-test.tsx b/test/unit-tests/modules/ProxiedModuleApi-test.tsx index 9a93a3c76c..a554a32818 100644 --- a/test/unit-tests/modules/ProxiedModuleApi-test.tsx +++ b/test/unit-tests/modules/ProxiedModuleApi-test.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 from "react"; +import React, { type JSX } from "react"; import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api/lib/types/translations"; import { AccountAuthInfo } from "@matrix-org/react-sdk-module-api/lib/types/AccountAuthInfo"; import { DialogContent, DialogProps } from "@matrix-org/react-sdk-module-api/lib/components/DialogContent"; @@ -236,7 +236,7 @@ describe("ProxiedApiModule", () => { super(props); } trySubmit = async () => ({ result: true }); - render = () => ( + render = (): JSX.Element => ( diff --git a/yarn.lock b/yarn.lock index 8609995279..967b828c28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2702,10 +2702,10 @@ lodash "^4.17.21" redent "^3.0.0" -"@testing-library/react@^16.0.0": - version "16.0.1" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.1.tgz#29c0ee878d672703f5e7579f239005e4e0faa875" - integrity sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg== +"@testing-library/react@^16.1.0": + version "16.1.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.1.0.tgz#aa0c61398bac82eaf89776967e97de41ac742d71" + integrity sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg== dependencies: "@babel/runtime" "^7.12.5" @@ -3126,11 +3126,6 @@ resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.5.tgz#a9495a58d8c75be4ffe9a0bd749a307715c07404" integrity sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA== -"@types/prop-types@*": - version "15.7.13" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" - integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== - "@types/qrcode@^1.3.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac" @@ -3155,7 +3150,7 @@ dependencies: "@types/react" "*" -"@types/react-dom@^19": +"@types/react-dom@19.0.0", "@types/react-dom@^19": version "19.0.0" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.0.tgz#e7f5d618a080486eaf9952246dbf59eaa2c64130" integrity sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w== @@ -3179,15 +3174,7 @@ dependencies: "@types/react" "*" -"@types/react@*": - version "18.3.3" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" - integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@types/react@^19": +"@types/react@*", "@types/react@19.0.0", "@types/react@^19": version "19.0.0" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.0.tgz#fbbb53ce223f4e2b750ad5dd09580b2c43522bbf" integrity sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg== @@ -9932,7 +9919,7 @@ react-clientside-effect@^1.2.6: dependencies: "@babel/runtime" "^7.12.13" -react-dom@^19: +react-dom@19.0.0, react-dom@^19: version "19.0.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== @@ -10016,7 +10003,7 @@ react-transition-group@^4.4.1: loose-envify "^1.4.0" prop-types "^15.6.2" -react@^19: +react@19.0.0, react@^19: version "19.0.0" resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==