Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Switch AccessibleButton and derivatives to using forwardRef #12054

Merged
merged 22 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/@types/react.d.ts
Original file line number Diff line number Diff line change
@@ -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<T, P = {}>(
render: (props: P, ref: React.ForwardedRef<T>) => React.ReactElement | null,
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}
24 changes: 10 additions & 14 deletions src/accessibility/context_menu/ContextMenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof AccessibleButton> {
type Props<T extends keyof JSX.IntrinsicElements> = AccessibleButtonProps<T> & {
label?: string;
// whether or not the context menu is currently open
isExpanded: boolean;
}
};

// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
export const ContextMenuButton: React.FC<IProps> = ({
label,
isExpanded,
children,
onClick,
onContextMenu,
...props
}) => {
export const ContextMenuButton = forwardRef(function <T extends keyof JSX.IntrinsicElements>(
{ label, isExpanded, children, onClick, onContextMenu, ...props }: Props<T>,
ref: Ref<HTMLElement>,
) {
return (
<AccessibleButton
{...props}
Expand All @@ -44,8 +39,9 @@ export const ContextMenuButton: React.FC<IProps> = ({
aria-label={label}
aria-haspopup={true}
aria-expanded={isExpanded}
ref={ref}
>
{children}
</AccessibleButton>
);
};
});
24 changes: 12 additions & 12 deletions src/accessibility/context_menu/ContextMenuTooltipButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof AccessibleTooltipButton> {
type Props<T extends keyof JSX.IntrinsicElements> = AccessibleTooltipButtonProps<T> & {
// whether or not the context menu is currently open
isExpanded: boolean;
}
};

// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
export const ContextMenuTooltipButton: React.FC<IProps> = ({
isExpanded,
children,
onClick,
onContextMenu,
...props
}) => {
export const ContextMenuTooltipButton = forwardRef(function <T extends keyof JSX.IntrinsicElements>(
{ isExpanded, children, onClick, onContextMenu, ...props }: Props<T>,
ref: Ref<HTMLElement>,
) {
return (
<AccessibleTooltipButton
{...props}
Expand All @@ -41,8 +40,9 @@ export const ContextMenuTooltipButton: React.FC<IProps> = ({
aria-haspopup={true}
aria-expanded={isExpanded}
forceHide={isExpanded}
ref={ref}
>
{children}
</AccessibleTooltipButton>
);
};
});
2 changes: 1 addition & 1 deletion src/accessibility/roving/RovingAccessibleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const RovingAccessibleButton: React.FC<IProps> = ({
if (focusOnMouseOver) onFocusInternal();
onMouseOver?.(event);
}}
inputRef={ref}
ref={ref}
tabIndex={isActive ? 0 : -1}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const RovingAccessibleTooltipButton: React.FC<IProps> = ({ inputRef, onFo
onFocusInternal();
onFocus?.(event);
}}
inputRef={ref}
ref={ref}
tabIndex={isActive ? 0 : -1}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/GenericDropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export function GenericDropdownMenu<T>({
<>
<ContextMenuButton
className="mx_GenericDropdownMenu_button"
inputRef={button}
ref={button}
isExpanded={menuDisplayed}
onClick={(ev: ButtonEvent) => {
openMenu();
Expand Down
5 changes: 3 additions & 2 deletions src/components/structures/SpaceHierarchy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/

import React, {
ComponentProps,
Dispatch,
KeyboardEvent,
KeyboardEventHandler,
Expand Down Expand Up @@ -349,7 +350,7 @@ const Tile: React.FC<ITileProps> = ({
})}
onClick={hasPermissions && onToggleClick ? onToggleClick : onPreviewClick}
onKeyDown={onKeyDown}
inputRef={ref}
ref={ref}
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
>
Expand Down Expand Up @@ -664,7 +665,7 @@ const ManageButtons: React.FC<IManageButtonsProps> = ({ hierarchy, selected, set
const disabled = !selectedRelations.length || removing || saving;

let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
let props = {};
let props: Partial<ComponentProps<typeof AccessibleTooltipButton>> = {};
if (!selectedRelations.length) {
Button = AccessibleTooltipButton;
props = {
Expand Down
6 changes: 3 additions & 3 deletions src/components/structures/SpaceRoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.

import { EventType, RoomType, JoinRule, Preset, Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import React, { RefObject, useCallback, useContext, useRef, useState } from "react";
import React, { useCallback, useContext, useRef, useState } from "react";

import MatrixClientContext from "../../contexts/MatrixClientContext";
import createRoom, { IOpts } from "../../createRoom";
Expand Down Expand Up @@ -196,7 +196,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => {
<>
<ContextMenuButton
kind="primary"
inputRef={handle}
ref={handle}
onClick={openMenu}
isExpanded={menuDisplayed}
label={_t("action|add")}
Expand Down Expand Up @@ -499,7 +499,7 @@ const SpaceSetupPrivateInvite: React.FC<{
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const numFields = 3;
const fieldRefs = [useRef(), useRef(), useRef()] as RefObject<Field>[];
const fieldRefs = [useRef<Field>(null), useRef<Field>(null), useRef<Field>(null)];
const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
const fields = new Array(numFields).fill(0).map((x, i) => {
const name = "emailAddress" + i;
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/ThreadPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const ThreadPanelHeader: React.FC<{
<>
<ContextMenuButton
className="mx_ThreadPanel_dropdown"
inputRef={button}
ref={button}
isExpanded={menuDisplayed}
onClick={(ev: ButtonEvent) => {
openMenu();
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
<ContextMenuButton
className="mx_UserMenu_contextMenuButton"
onClick={this.onOpenMenuClick}
inputRef={this.buttonRef}
ref={this.buttonRef}
label={_t("a11y|user_menu")}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
Expand Down
3 changes: 2 additions & 1 deletion src/components/views/audio_messages/PlayPauseButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../audio/Playback";

// omitted props are handled by render function
interface IProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButton>, "title" | "onClick" | "disabled"> {
interface IProps
extends Omit<React.ComponentProps<typeof AccessibleTooltipButton>, "title" | "onClick" | "disabled" | "ref"> {
// Playback instance to manipulate. Cannot change during the component lifecycle.
playback: Playback;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,7 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
}
return (
<div>
<AccessibleButton kind="link" inputRef={this.fallbackButton} onClick={this.onShowFallbackClick}>
<AccessibleButton kind="link" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>
{_t("auth|uia|fallback_button")}
</AccessibleButton>
{errorSection}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/context_menus/KebabContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const KebabContextMenu: React.FC<KebabContextMenuProps> = ({ options, tit

return (
<>
<ContextMenuButton {...props} onClick={openMenu} title={title} isExpanded={menuDisplayed} inputRef={button}>
<ContextMenuButton {...props} onClick={openMenu} title={title} isExpanded={menuDisplayed} ref={button}>
<ContextMenuIcon className="mx_KebabContextMenu_icon" />
</ContextMenuButton>
{menuDisplayed && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const ThreadListContextMenu: React.FC<ThreadListContextMenuProps> = ({
onClick={openMenu}
title={_t("right_panel|thread_list|context_menu_label")}
isExpanded={menuDisplayed}
inputRef={button}
ref={button}
data-testid="threadlist-dropdown-button"
/>
{menuDisplayed && (
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/AddExistingToSpaceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { ReactElement, ReactNode, RefObject, useContext, useMemo, useRef, useState } from "react";
import React, { ReactElement, ReactNode, useContext, useMemo, useRef, useState } from "react";
import classNames from "classnames";
import { Room, EventType } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";
Expand Down Expand Up @@ -144,7 +144,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
[cli, msc3946ProcessDynamicPredecessor],
);

const scrollRef = useRef() as RefObject<AutoHideScrollbar<"div">>;
const scrollRef = useRef<AutoHideScrollbar<"div">>(null);
const [scrollState, setScrollState] = useState<IScrollState>({
// these are estimates which update as soon as it mounts
scrollTop: 0,
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/dialogs/spotlight/Option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const Option: React.FC<OptionProps> = ({ inputRef, children, endAdornment
{...props}
className={classNames(className, "mx_SpotlightDialog_option")}
onFocus={onFocus}
inputRef={ref}
ref={ref}
tabIndex={-1}
aria-selected={isActive}
role="option"
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/dialogs/spotlight/TooltipOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const TooltipOption: React.FC<TooltipOptionProps> = ({ inputRef, classNam
{...props}
className={classNames(className, "mx_SpotlightDialog_option")}
onFocus={onFocus}
inputRef={ref}
ref={ref}
tabIndex={-1}
aria-selected={isActive}
role="option"
Expand Down
48 changes: 26 additions & 22 deletions src/components/views/elements/AccessibleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
*/

import React, { HTMLAttributes, InputHTMLAttributes, ReactNode } from "react";
import React, { forwardRef, FunctionComponent, HTMLAttributes, InputHTMLAttributes, ReactNode, Ref } from "react";
import classnames from "classnames";

import { getKeyBindingsManager } from "../../../KeyBindingsManager";
Expand Down Expand Up @@ -50,7 +50,7 @@ type AccessibleButtonKind =
*
* To remain compatible with existing code, we’ll continue to support InputHTMLAttributes<Element>
*/
type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
export type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps<T> : DynamicElementProps<"div">;
type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<
Omit<JSX.IntrinsicElements[T], "ref" | "onClick" | "onMouseDown" | "onKeyUp" | "onKeyDown">
Expand All @@ -63,8 +63,7 @@ type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<
* onClick: (required) Event handler for button activation. Should be
* implemented exactly like a normal onClick handler.
*/
type IProps<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> & {
inputRef?: React.Ref<Element>;
export type Props<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> & {
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
element?: T;
children?: ReactNode;
// The kind of button, similar to how Bootstrap works.
Expand All @@ -80,7 +79,7 @@ type IProps<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T>
onClick: ((e: ButtonEvent) => void | Promise<void>) | null;
};

export interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
ref?: React.Ref<Element>;
}

Expand All @@ -92,20 +91,23 @@ export interface IAccessibleButtonProps extends React.InputHTMLAttributes<Elemen
* @param {Object} props react element properties
* @returns {Object} rendered react
*/
export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>({
element = "div" as T,
onClick,
children,
kind,
disabled,
inputRef,
className,
onKeyDown,
onKeyUp,
triggerOnMouseDown,
...restProps
}: IProps<T>): JSX.Element {
const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicElements>(
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
{
element = "div" as T,
onClick,
children,
kind,
disabled,
className,
onKeyDown,
onKeyUp,
triggerOnMouseDown,
...restProps
}: Props<T>,
ref: Ref<HTMLElement>,
): JSX.Element {
const newProps: IAccessibleButtonProps = restProps;

if (disabled) {
newProps["aria-disabled"] = true;
newProps["disabled"] = true;
Expand Down Expand Up @@ -158,7 +160,7 @@ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>(
}

// 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,
Expand All @@ -168,11 +170,13 @@ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>(

// React.createElement expects InputHTMLAttributes
return React.createElement(element, newProps, children);
}
});

AccessibleButton.defaultProps = {
// Type assertion required due to forwardRef type workaround in react.d.ts
(AccessibleButton as FunctionComponent).defaultProps = {
role: "button",
tabIndex: 0,
};
(AccessibleButton as FunctionComponent).displayName = "AccessibleButton";

AccessibleButton.displayName = "AccessibleButton";
export default AccessibleButton;
Loading
Loading