From 63fa4152a7c017feaad8dce22d5bcfd16a46f9c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Dec 2023 12:33:01 +0000 Subject: [PATCH 01/16] Prevent Cypress typechecking react-sdk components without strict mode This prevented us from switching to `forwardRef` in a bunch of places due to it behaving different with & without strict mode. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- cypress/global.d.ts | 23 +++------ cypress/support/settings.ts | 93 ++----------------------------------- 2 files changed, 10 insertions(+), 106 deletions(-) diff --git a/cypress/global.d.ts b/cypress/global.d.ts index f8caad1f89e..dd021533d94 100644 --- a/cypress/global.d.ts +++ b/cypress/global.d.ts @@ -14,9 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import "../src/@types/global"; -import "../src/@types/svg"; -import "../src/@types/raw-loader"; +import "../src/@types/react"; // eslint-disable-next-line no-restricted-imports import "matrix-js-sdk/src/@types/global"; import type { @@ -31,20 +29,19 @@ import type { RoomMemberEvent, ICreateClientOpts, } from "matrix-js-sdk/src/matrix"; -import type { MatrixDispatcher } from "../src/dispatcher/dispatcher"; -import type PerformanceMonitor from "../src/performance"; -import type SettingsStore from "../src/settings/SettingsStore"; +import type { SettingLevel } from "../src/settings/SettingLevel"; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface ApplicationWindow { - mxSettingsStore: typeof SettingsStore; + // XXX: Importing SettingsStore causes a bunch of type lint errors + mxSettingsStore: { + setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise; + }; mxMatrixClientPeg: { matrixClient?: MatrixClient; }; - mxDispatcher: MatrixDispatcher; - mxPerformanceMonitor: PerformanceMonitor; beforeReload?: boolean; // for detecting reloads // Partial type for the matrix-js-sdk module, exported by browser-matrix matrixcs: { @@ -61,14 +58,6 @@ declare global { }; } } - - interface Window { - // to appease the MatrixDispatcher import - mxDispatcher: MatrixDispatcher; - // to appease the PerformanceMonitor import - mxPerformanceMonitor: PerformanceMonitor; - mxPerformanceEntryNames: any; - } } export { MatrixClient }; diff --git a/cypress/support/settings.ts b/cypress/support/settings.ts index 51767cc0bd1..0bd29297d3d 100644 --- a/cypress/support/settings.ts +++ b/cypress/support/settings.ts @@ -22,7 +22,7 @@ import Timeoutable = Cypress.Timeoutable; import Withinable = Cypress.Withinable; import Shadow = Cypress.Shadow; import type { SettingLevel } from "../../src/settings/SettingLevel"; -import type SettingsStore from "../../src/settings/SettingsStore"; +import ApplicationWindow = Cypress.ApplicationWindow; export enum Filter { People = "people", @@ -36,7 +36,7 @@ declare global { /** * Returns the SettingsStore */ - getSettingsStore(): Chainable; // XXX: Importing SettingsStore causes a bunch of type lint errors + getSettingsStore(): Chainable; /** * Open the top left user menu, returning a handle to the resulting context menu. */ @@ -48,17 +48,6 @@ declare global { */ openUserSettings(tab?: string): Chainable>; - /** - * Open room creation dialog. - */ - openCreateRoomDialog(): Chainable>; - - /** - * Open room settings (via room header menu), returning a handle to the resulting dialog. - * @param tab the name of the tab to switch to after opening, optional. - */ - openRoomSettings(tab?: string): Chainable>; - /** * Switch settings tab to the one by the given name, ideally call this in the context of the dialog. * @param tab the name of the tab to switch to. @@ -70,20 +59,6 @@ declare global { */ closeDialog(): Chainable>; - /** - * Join the given beta, the `Labs` tab must already be opened, - * ideally call this in the context of the dialog. - * @param name the name of the beta to join. - */ - joinBeta(name: string): Chainable>; - - /** - * Leave the given beta, the `Labs` tab must already be opened, - * ideally call this in the context of the dialog. - * @param name the name of the beta to leave. - */ - leaveBeta(name: string): Chainable>; - /** * Sets the value for a setting. The room ID is optional if the * setting is not being set for a particular room, otherwise it @@ -98,20 +73,6 @@ declare global { */ setSettingValue(settingName: string, roomId: string, level: SettingLevel, value: any): Chainable; - /** - * Gets the value of a setting. The room ID is optional if the - * setting is not to be applied to any particular room, otherwise it - * should be supplied. - * @param {string} settingName The name of the setting to read the - * value of. - * @param {String} roomId The room ID to read the setting value in, - * may be null. - * @param {boolean} excludeDefault True to disable using the default - * value. - * @return {*} The value, or null if not found - */ - getSettingValue(settingName: string, roomId?: string, excludeDefault?: boolean): Chainable; - /** * Opens the spotlight dialog */ @@ -135,29 +96,19 @@ declare global { } } -Cypress.Commands.add("getSettingsStore", (): Chainable => { +Cypress.Commands.add("getSettingsStore", (): Chainable => { return cy.window({ log: false }).then((win) => win.mxSettingsStore); }); Cypress.Commands.add( "setSettingValue", (name: string, roomId: string, level: SettingLevel, value: any): Chainable => { - return cy.getSettingsStore().then((store: typeof SettingsStore) => { + return cy.getSettingsStore().then((store: ApplicationWindow["mxSettingsStore"]) => { return cy.wrap(store.setValue(name, roomId, level, value)); }); }, ); -// eslint-disable-next-line max-len -Cypress.Commands.add( - "getSettingValue", - (name: string, roomId?: string, excludeDefault?: boolean): Chainable => { - return cy.getSettingsStore().then((store: typeof SettingsStore) => { - return store.getValue(name, roomId, excludeDefault); - }); - }, -); - Cypress.Commands.add("openUserMenu", (): Chainable> => { cy.findByRole("button", { name: "User menu" }).click(); return cy.get(".mx_ContextualMenu"); @@ -174,24 +125,6 @@ Cypress.Commands.add("openUserSettings", (tab?: string): Chainable> => { - cy.findByRole("button", { name: "Add room" }).click(); - cy.findByRole("menuitem", { name: "New room" }).click(); - return cy.get(".mx_CreateRoomDialog"); -}); - -Cypress.Commands.add("openRoomSettings", (tab?: string): Chainable> => { - cy.findByRole("button", { name: "Room options" }).click(); - cy.get(".mx_RoomTile_contextMenu").within(() => { - cy.findByRole("menuitem", { name: "Settings" }).click(); - }); - return cy.get(".mx_RoomSettingsDialog").within(() => { - if (tab) { - cy.switchTab(tab); - } - }); -}); - Cypress.Commands.add("switchTab", (tab: string): Chainable> => { return cy.get(".mx_TabbedView_tabLabels").within(() => { cy.contains(".mx_TabbedView_tabLabel", tab).click(); @@ -202,24 +135,6 @@ Cypress.Commands.add("closeDialog", (): Chainable> => { return cy.findByRole("button", { name: "Close dialog" }).click(); }); -Cypress.Commands.add("joinBeta", (name: string): Chainable> => { - return cy - .contains(".mx_BetaCard_title", name) - .closest(".mx_BetaCard") - .within(() => { - return cy.get(".mx_BetaCard_buttons").findByRole("button", { name: "Join the beta" }).click(); - }); -}); - -Cypress.Commands.add("leaveBeta", (name: string): Chainable> => { - return cy - .contains(".mx_BetaCard_title", name) - .closest(".mx_BetaCard") - .within(() => { - return cy.get(".mx_BetaCard_buttons").findByRole("button", { name: "Leave the beta" }).click(); - }); -}); - Cypress.Commands.add( "openSpotlightDialog", (options?: Partial): Chainable> => { From 1b09a8c9eab918ff8c0deb681d8e344901353670 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Dec 2023 12:33:49 +0000 Subject: [PATCH 02/16] Update global.d.ts --- cypress/global.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cypress/global.d.ts b/cypress/global.d.ts index dd021533d94..330b66f6db2 100644 --- a/cypress/global.d.ts +++ b/cypress/global.d.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import "../src/@types/react"; // eslint-disable-next-line no-restricted-imports import "matrix-js-sdk/src/@types/global"; import type { From 65b0dc583a472a53a0de2686982341f94cc02f80 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Dec 2023 12:44:24 +0000 Subject: [PATCH 03/16] Switch AccessibleButton and derivatives to using `forwardRef` Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/@types/react.d.ts | 24 +++++ .../context_menu/ContextMenuButton.tsx | 23 ++-- .../context_menu/ContextMenuTooltipButton.tsx | 23 ++-- .../roving/RovingAccessibleButton.tsx | 2 +- .../roving/RovingAccessibleTooltipButton.tsx | 2 +- .../structures/GenericDropdownMenu.tsx | 2 +- src/components/structures/SpaceHierarchy.tsx | 5 +- src/components/structures/SpaceRoomView.tsx | 2 +- src/components/structures/ThreadPanel.tsx | 2 +- src/components/structures/UserMenu.tsx | 2 +- .../views/audio_messages/PlayPauseButton.tsx | 3 +- .../auth/InteractiveAuthEntryComponents.tsx | 2 +- .../views/context_menus/KebabContextMenu.tsx | 2 +- .../context_menus/ThreadListContextMenu.tsx | 2 +- .../views/dialogs/spotlight/Option.tsx | 2 +- .../views/dialogs/spotlight/TooltipOption.tsx | 2 +- .../views/elements/AccessibleButton.tsx | 54 +++++----- .../elements/AccessibleTooltipButton.tsx | 101 ++++++++---------- src/components/views/elements/AppTile.tsx | 2 +- src/components/views/elements/Dropdown.tsx | 2 +- src/components/views/elements/ImageView.tsx | 2 +- src/components/views/elements/LearnMore.tsx | 4 +- .../views/elements/PollCreateDialog.tsx | 2 +- src/components/views/messages/IBodyProps.ts | 4 +- .../views/messages/MessageActionBar.tsx | 4 +- .../views/messages/ReactionsRow.tsx | 2 +- .../views/right_panel/WidgetCard.tsx | 2 +- .../views/rooms/LegacyRoomHeader.tsx | 4 +- .../views/rooms/ReadReceiptGroup.tsx | 2 +- .../views/rooms/RoomBreadcrumbs.tsx | 2 +- src/components/views/rooms/RoomList.tsx | 4 +- src/components/views/rooms/RoomListHeader.tsx | 4 +- src/components/views/rooms/RoomSublist.tsx | 2 +- src/components/views/rooms/RoomTile.tsx | 2 +- .../components/WysiwygAutocomplete.tsx | 2 - .../views/spaces/QuickSettingsButton.tsx | 2 +- .../views/spaces/SpaceTreeLevel.tsx | 2 +- src/components/views/voip/CallView.tsx | 2 +- .../LegacyCallView/LegacyCallViewButtons.tsx | 6 +- src/contexts/MatrixClientContext.tsx | 17 +-- 40 files changed, 165 insertions(+), 165 deletions(-) create mode 100644 src/@types/react.d.ts diff --git a/src/@types/react.d.ts b/src/@types/react.d.ts new file mode 100644 index 00000000000..5071b09a98b --- /dev/null +++ b/src/@types/react.d.ts @@ -0,0 +1,24 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +declare module "react" { + // Fix forwardRef types for Generic components - https://stackoverflow.com/a/58473012 + function forwardRef( + render: (props: P, ref: React.ForwardedRef) => React.ReactElement | null, + ): (props: P & React.RefAttributes) => React.ReactElement | null; +} diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index 090b42333ff..3b8dceb2f7c 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -16,25 +16,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef, Ref } from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; +import AccessibleButton, { Props as AccessibleButtonProps } from "../../components/views/elements/AccessibleButton"; -interface IProps extends React.ComponentProps { +type Props = AccessibleButtonProps & { label?: string; // whether or not the context menu is currently open isExpanded: boolean; -} +}; -// Semantic component for representing the AccessibleButton which launches a -export const ContextMenuButton: React.FC = ({ - label, - isExpanded, - children, - onClick, - onContextMenu, - ...props -}) => { +export const ContextMenuButton = forwardRef(function ( + { label, isExpanded, children, onClick, onContextMenu, ...props }: Props, + ref: Ref, +) { return ( = ({ {children} ); -}; +}); diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index 750d47b08ad..940783eaabc 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -16,23 +16,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { forwardRef, Ref } from "react"; -import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; +import AccessibleTooltipButton, { + Props as AccessibleTooltipButtonProps, +} from "../../components/views/elements/AccessibleTooltipButton"; -interface IProps extends React.ComponentProps { +type Props = AccessibleTooltipButtonProps & { // whether or not the context menu is currently open isExpanded: boolean; -} +}; // Semantic component for representing the AccessibleButton which launches a -export const ContextMenuTooltipButton: React.FC = ({ - isExpanded, - children, - onClick, - onContextMenu, - ...props -}) => { +export const ContextMenuTooltipButton = forwardRef(function ( + { isExpanded, children, onClick, onContextMenu, ...props }: Props, + ref: Ref, +) { return ( = ({ {children} ); -}; +}); diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index 28748de73fb..d87f4fe776c 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -45,7 +45,7 @@ export const RovingAccessibleButton: React.FC = ({ if (focusOnMouseOver) onFocusInternal(); onMouseOver?.(event); }} - inputRef={ref} + ref={ref} tabIndex={isActive ? 0 : -1} /> ); diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index f06cc934bbc..247a5a0cb88 100644 --- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -35,7 +35,7 @@ export const RovingAccessibleTooltipButton: React.FC = ({ inputRef, onFo onFocusInternal(); onFocus?.(event); }} - inputRef={ref} + ref={ref} tabIndex={isActive ? 0 : -1} /> ); diff --git a/src/components/structures/GenericDropdownMenu.tsx b/src/components/structures/GenericDropdownMenu.tsx index 0a38db3dbe4..e0fd3b7f9b6 100644 --- a/src/components/structures/GenericDropdownMenu.tsx +++ b/src/components/structures/GenericDropdownMenu.tsx @@ -195,7 +195,7 @@ export function GenericDropdownMenu({ <> { openMenu(); diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index aa57114e5ac..feeacb45818 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import React, { + ComponentProps, Dispatch, KeyboardEvent, KeyboardEventHandler, @@ -349,7 +350,7 @@ const Tile: React.FC = ({ })} onClick={hasPermissions && onToggleClick ? onToggleClick : onPreviewClick} onKeyDown={onKeyDown} - inputRef={ref} + ref={ref} onFocus={onFocus} tabIndex={isActive ? 0 : -1} > @@ -664,7 +665,7 @@ const ManageButtons: React.FC = ({ hierarchy, selected, set const disabled = !selectedRelations.length || removing || saving; let Button: React.ComponentType> = AccessibleButton; - let props = {}; + let props: Partial> = {}; if (!selectedRelations.length) { Button = AccessibleTooltipButton; props = { diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 1acb6877f92..c769f9e00fb 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -196,7 +196,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => { <> { openMenu(); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 2d82a5c412e..1ed20add205 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -453,7 +453,7 @@ export default class UserMenu extends React.Component { , "title" | "onClick" | "disabled"> { +interface IProps + extends Omit, "title" | "onClick" | "disabled" | "ref"> { // Playback instance to manipulate. Cannot change during the component lifecycle. playback: Playback; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 9aaf02b09f7..7f9bb6d65e9 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -971,7 +971,7 @@ export class FallbackAuthEntry extends React.Component { } return (
- + {_t("auth|uia|fallback_button")} {errorSection} diff --git a/src/components/views/context_menus/KebabContextMenu.tsx b/src/components/views/context_menus/KebabContextMenu.tsx index b81c6aef6f4..7a6b09668dd 100644 --- a/src/components/views/context_menus/KebabContextMenu.tsx +++ b/src/components/views/context_menus/KebabContextMenu.tsx @@ -39,7 +39,7 @@ export const KebabContextMenu: React.FC = ({ options, tit return ( <> - + {menuDisplayed && ( diff --git a/src/components/views/context_menus/ThreadListContextMenu.tsx b/src/components/views/context_menus/ThreadListContextMenu.tsx index 0fb3831b237..76abc23740e 100644 --- a/src/components/views/context_menus/ThreadListContextMenu.tsx +++ b/src/components/views/context_menus/ThreadListContextMenu.tsx @@ -94,7 +94,7 @@ const ThreadListContextMenu: React.FC = ({ onClick={openMenu} title={_t("right_panel|thread_list|context_menu_label")} isExpanded={menuDisplayed} - inputRef={button} + ref={button} data-testid="threadlist-dropdown-button" /> {menuDisplayed && ( diff --git a/src/components/views/dialogs/spotlight/Option.tsx b/src/components/views/dialogs/spotlight/Option.tsx index a1dc41a8524..c7d504aa0b4 100644 --- a/src/components/views/dialogs/spotlight/Option.tsx +++ b/src/components/views/dialogs/spotlight/Option.tsx @@ -35,7 +35,7 @@ export const Option: React.FC = ({ inputRef, children, endAdornment {...props} className={classNames(className, "mx_SpotlightDialog_option")} onFocus={onFocus} - inputRef={ref} + ref={ref} tabIndex={-1} aria-selected={isActive} role="option" diff --git a/src/components/views/dialogs/spotlight/TooltipOption.tsx b/src/components/views/dialogs/spotlight/TooltipOption.tsx index ef4c142f10f..2233e762d46 100644 --- a/src/components/views/dialogs/spotlight/TooltipOption.tsx +++ b/src/components/views/dialogs/spotlight/TooltipOption.tsx @@ -32,7 +32,7 @@ export const TooltipOption: React.FC = ({ inputRef, classNam {...props} className={classNames(className, "mx_SpotlightDialog_option")} onFocus={onFocus} - inputRef={ref} + ref={ref} tabIndex={-1} aria-selected={isActive} role="option" diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index e679955d8ab..a707f1653b1 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -14,7 +14,7 @@ limitations under the License. */ -import React, { HTMLAttributes, InputHTMLAttributes, ReactNode } from "react"; +import React, { forwardRef, HTMLAttributes, InputHTMLAttributes, NamedExoticComponent, ReactNode, Ref } from "react"; import classnames from "classnames"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; @@ -50,7 +50,7 @@ type AccessibleButtonKind = * * To remain compatible with existing code, we’ll continue to support InputHTMLAttributes */ -type DynamicHtmlElementProps = +export type DynamicHtmlElementProps = JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">; type DynamicElementProps = Partial< Omit @@ -63,8 +63,7 @@ type DynamicElementProps = Partial< * onClick: (required) Event handler for button activation. Should be * implemented exactly like a normal onClick handler. */ -type IProps = DynamicHtmlElementProps & { - inputRef?: React.Ref; +export type Props = DynamicHtmlElementProps & { element?: T; children?: ReactNode; // The kind of button, similar to how Bootstrap works. @@ -80,7 +79,7 @@ type IProps = DynamicHtmlElementProps onClick: ((e: ButtonEvent) => void | Promise) | null; }; -export interface IAccessibleButtonProps extends React.InputHTMLAttributes { +interface IAccessibleButtonProps extends React.InputHTMLAttributes { ref?: React.Ref; } @@ -92,20 +91,27 @@ export interface IAccessibleButtonProps extends React.InputHTMLAttributes({ - element = "div" as T, - onClick, - children, - kind, - disabled, - inputRef, - className, - onKeyDown, - onKeyUp, - triggerOnMouseDown, - ...restProps -}: IProps): JSX.Element { +const AccessibleButton = forwardRef(function ( + { + element = "div" as T, + role = "button", + tabIndex = 0, + onClick, + children, + kind, + disabled, + className, + onKeyDown, + onKeyUp, + triggerOnMouseDown, + ...restProps + }: Props, + ref: Ref, +): JSX.Element { const newProps: IAccessibleButtonProps = restProps; + newProps.tabIndex = tabIndex; + newProps.role = role; + if (disabled) { newProps["aria-disabled"] = true; newProps["disabled"] = true; @@ -158,7 +164,7 @@ export default function AccessibleButton( } // Pass through the ref - used for keyboard shortcut access to some buttons - newProps.ref = inputRef; + newProps.ref = ref; newProps.className = classnames("mx_AccessibleButton", className, { mx_AccessibleButton_hasKind: kind, @@ -168,11 +174,9 @@ export default function AccessibleButton( // React.createElement expects InputHTMLAttributes return React.createElement(element, newProps, children); -} +}); -AccessibleButton.defaultProps = { - role: "button", - tabIndex: 0, -}; +// Type assertion required due to forwardRef type workaround in react.d.ts +(AccessibleButton as NamedExoticComponent).displayName = "AccessibleButton"; -AccessibleButton.displayName = "AccessibleButton"; +export default AccessibleButton; diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 26c3825fda1..3abeed85d2e 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -15,12 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { SyntheticEvent, FocusEvent } from "react"; +import React, { SyntheticEvent, FocusEvent, forwardRef, useEffect, Ref, useState } from "react"; -import AccessibleButton from "./AccessibleButton"; +import AccessibleButton, { Props as AccessibleButtonProps } from "./AccessibleButton"; import Tooltip, { Alignment } from "./Tooltip"; -interface IProps extends React.ComponentProps { +export type Props = AccessibleButtonProps & { title?: string; tooltip?: React.ReactNode; label?: string; @@ -29,71 +29,56 @@ interface IProps extends React.ComponentProps { alignment?: Alignment; onHover?: (hovering: boolean) => void; onHideTooltip?(ev: SyntheticEvent): void; -} +}; -interface IState { - hover: boolean; -} +const AccessibleTooltipButton = forwardRef(function ( + { title, tooltip, children, forceHide, alignment, onHideTooltip, tooltipClassName, ...props }: Props, + ref: Ref, +) { + const [hover, setHover] = useState(false); -export default class AccessibleTooltipButton extends React.PureComponent { - public constructor(props: IProps) { - super(props); - this.state = { - hover: false, - }; - } - - public componentDidUpdate(prevProps: Readonly): void { - if (!prevProps.forceHide && this.props.forceHide && this.state.hover) { - this.setState({ - hover: false, - }); + useEffect(() => { + if (forceHide && hover) { + setHover(false); } - } + }, [forceHide, hover]); - private showTooltip = (): void => { - if (this.props.onHover) this.props.onHover(true); - if (this.props.forceHide) return; - this.setState({ - hover: true, - }); + const showTooltip = (): void => { + props.onHover?.(true); + if (forceHide) return; + setHover(true); }; - private hideTooltip = (ev: SyntheticEvent): void => { - if (this.props.onHover) this.props.onHover(false); - this.setState({ - hover: false, - }); - this.props.onHideTooltip?.(ev); + const hideTooltip = (ev: SyntheticEvent): void => { + props.onHover?.(false); + setHover(false); + onHideTooltip?.(ev); }; - private onFocus = (ev: FocusEvent): void => { + const onFocus = (ev: FocusEvent): void => { // We only show the tooltip if focus arrived here from some other // element, to avoid leaving tooltips hanging around when a modal closes - if (ev.relatedTarget) this.showTooltip(); + if (ev.relatedTarget) showTooltip(); }; - public render(): React.ReactNode { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip, ...props } = - this.props; + const tip = hover && (title || tooltip) && ( + + ); + return ( + + {children} + {props.label} + {(tooltip || title) && tip} + + ); +}); - const tip = this.state.hover && (title || tooltip) && ( - - ); - return ( - - {children} - {this.props.label} - {(tooltip || title) && tip} - - ); - } -} +export default AccessibleTooltipButton; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 13fa3daac77..da2a21776e6 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -787,7 +787,7 @@ export default class AppTile extends React.Component { className="mx_AppTileMenuBar_widgets_button" label={_t("common|options")} isExpanded={this.state.menuDisplayed} - inputRef={this.contextMenuButton} + ref={this.contextMenuButton} onClick={this.onContextMenuClick} > diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index 0a5786a1cb2..d017ea8641a 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -392,7 +392,7 @@ export default class Dropdown extends React.Component { aria-haspopup="listbox" aria-expanded={this.state.expanded} disabled={this.props.disabled} - inputRef={this.buttonRef} + ref={this.buttonRef} aria-label={this.props.label} aria-describedby={`${this.props.id}_value`} aria-owns={`${this.props.id}_input`} diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index da05239746d..6f5815e95a1 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -506,7 +506,7 @@ export default class ImageView extends React.Component { className="mx_ImageView_button mx_ImageView_button_more" title={_t("common|options")} onClick={this.onOpenContextMenu} - inputRef={this.contextMenuButton} + ref={this.contextMenuButton} isExpanded={this.state.contextMenuDisplayed} /> ); diff --git a/src/components/views/elements/LearnMore.tsx b/src/components/views/elements/LearnMore.tsx index 03f377da763..8b51a5dc4d5 100644 --- a/src/components/views/elements/LearnMore.tsx +++ b/src/components/views/elements/LearnMore.tsx @@ -19,9 +19,9 @@ import React from "react"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import InfoDialog from "../dialogs/InfoDialog"; -import AccessibleButton, { IAccessibleButtonProps } from "./AccessibleButton"; +import AccessibleButton, { Props as AccessibleButtonProps } from "./AccessibleButton"; -export interface LearnMoreProps extends IAccessibleButtonProps { +export interface LearnMoreProps extends Omit, "kind" | "onClick" | "className"> { title: string; description: string | React.ReactNode; } diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 6451bdcbb8c..5049a3b0162 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -248,7 +248,7 @@ export default class PollCreateDialog extends ScrollableBaseModal= MAX_OPTIONS} kind="secondary" className="mx_PollCreateDialog_addOption" - inputRef={this.addOptionRef} + ref={this.addOptionRef} > {_t("poll|options_add_button")} diff --git a/src/components/views/messages/IBodyProps.ts b/src/components/views/messages/IBodyProps.ts index fcc204dae3b..49a36591370 100644 --- a/src/components/views/messages/IBodyProps.ts +++ b/src/components/views/messages/IBodyProps.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { LegacyRef } from "react"; +import React, { LegacyRef, ReactNode } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; @@ -59,4 +59,6 @@ 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; + + children?: ReactNode; } diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 60eee55c2d0..9cf35922089 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -126,7 +126,7 @@ const OptionsButton: React.FC = ({ onClick={onOptionsClick} onContextMenu={onOptionsClick} isExpanded={menuDisplayed} - inputRef={button} + ref={button} onFocus={onFocus} tabIndex={isActive ? 0 : -1} > @@ -183,7 +183,7 @@ const ReactButton: React.FC = ({ mxEvent, reactions, onFocusC onClick={onClick} onContextMenu={onClick} isExpanded={menuDisplayed} - inputRef={button} + ref={button} onFocus={onFocus} tabIndex={isActive ? 0 : -1} > diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index 3aeee9e0ff1..e57326edd73 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -61,7 +61,7 @@ const ReactButton: React.FC = ({ mxEvent, reactions }) => { openMenu(); }} isExpanded={menuDisplayed} - inputRef={button} + ref={button} /> {contextMenu} diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index cbcc961b972..ca7cdebb21f 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -78,7 +78,7 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { = ({ room, busy, setBusy, behavi return ( <> = ({ call }) => { return ( <> v title={room.name} tooltipClassName="mx_RoomBreadcrumbs_Tooltip" onFocus={onFocus} - inputRef={ref} + ref={ref} tabIndex={isActive ? 0 : -1} > = ({ tabIndex, dispatcher = default aria-label={_t("action|add_people")} title={_t("action|add_people")} isExpanded={menuDisplayed} - inputRef={handle} + ref={handle} /> {contextMenu} @@ -359,7 +359,7 @@ const UntaggedAuxButton: React.FC = ({ tabIndex }) => { aria-label={_t("room_list|add_room_label")} title={_t("room_list|add_room_label")} isExpanded={menuDisplayed} - inputRef={handle} + ref={handle} /> {contextMenu} diff --git a/src/components/views/rooms/RoomListHeader.tsx b/src/components/views/rooms/RoomListHeader.tsx index 6734b2e536f..9e184a04ed5 100644 --- a/src/components/views/rooms/RoomListHeader.tsx +++ b/src/components/views/rooms/RoomListHeader.tsx @@ -389,7 +389,7 @@ const RoomListHeader: React.FC = ({ onVisibilityChange }) => { let contextMenuButton: JSX.Element =
{title}
; if (canShowMainMenu) { const commonProps = { - inputRef: mainMenuHandle, + ref: mainMenuHandle, onClick: openMainMenu, isExpanded: mainMenuDisplayed, className: "mx_RoomListHeader_contextMenuButton", @@ -418,7 +418,7 @@ const RoomListHeader: React.FC = ({ onVisibilityChange }) => { ) : null} {canShowPlusMenu && ( {