Prepare for switching AccessibleButton and derivatives to forwardRef (#12072)

* Improve AccessibleButton props & docs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve AccessibleTooltipButton props docs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Simplify roving tab index hook usage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Ditch RefObject type casts

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Convert AccessibleTooltipButton to a Functional Component

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2023-12-20 10:58:24 +00:00 committed by GitHub
parent 2212fbadd0
commit bf61d93bf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 140 additions and 103 deletions

View file

@ -26,7 +26,10 @@ import {
import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import getEntryComponentForLoginType, { IStageComponent } from "../views/auth/InteractiveAuthEntryComponents"; import getEntryComponentForLoginType, {
ContinueKind,
IStageComponent,
} from "../views/auth/InteractiveAuthEntryComponents";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
@ -59,7 +62,7 @@ export interface InteractiveAuthProps<T> {
continueIsManaged?: boolean; continueIsManaged?: boolean;
// continueText and continueKind are passed straight through to the AuthEntryComponent. // continueText and continueKind are passed straight through to the AuthEntryComponent.
continueText?: string; continueText?: string;
continueKind?: string; continueKind?: ContinueKind;
// callback // callback
makeRequest(auth: IAuthDict | null): Promise<T>; makeRequest(auth: IAuthDict | null): Promise<T>;
// callback called when the auth process has finished, // callback called when the auth process has finished,

View file

@ -16,7 +16,7 @@ limitations under the License.
import { EventType, RoomType, JoinRule, Preset, Room, RoomEvent } from "matrix-js-sdk/src/matrix"; import { EventType, RoomType, JoinRule, Preset, Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; 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 MatrixClientContext from "../../contexts/MatrixClientContext";
import createRoom, { IOpts } from "../../createRoom"; import createRoom, { IOpts } from "../../createRoom";
@ -499,7 +499,7 @@ const SpaceSetupPrivateInvite: React.FC<{
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const numFields = 3; 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 [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
const fields = new Array(numFields).fill(0).map((x, i) => { const fields = new Array(numFields).fill(0).map((x, i) => {
const name = "emailAddress" + i; const name = "emailAddress" + i;

View file

@ -25,7 +25,7 @@ import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { LocalisedPolicy, Policies } from "../../../Terms"; import { LocalisedPolicy, Policies } from "../../../Terms";
import { AuthHeaderModifier } from "../../structures/auth/header/AuthHeaderModifier"; import { AuthHeaderModifier } from "../../structures/auth/header/AuthHeaderModifier";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import AccessibleButton, { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import Field from "../elements/Field"; import Field from "../elements/Field";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
@ -780,9 +780,12 @@ export class RegistrationTokenAuthEntry extends React.Component<IAuthEntryProps,
} }
} }
// Subset of AccessibleButtonKind which can be specified for the continue button
export type ContinueKind = Extract<AccessibleButtonKind, "primary" | "danger">;
interface ISSOAuthEntryProps extends IAuthEntryProps { interface ISSOAuthEntryProps extends IAuthEntryProps {
continueText?: string; continueText?: string;
continueKind?: string; continueKind?: ContinueKind;
onCancel?: () => void; onCancel?: () => void;
} }
@ -866,7 +869,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
const cancelButton = ( const cancelButton = (
<AccessibleButton <AccessibleButton
onClick={this.props.onCancel ?? null} onClick={this.props.onCancel ?? null}
kind={this.props.continueKind ? this.props.continueKind + "_outline" : "primary_outline"} kind={this.props.continueKind ? `${this.props.continueKind}_outline` : "primary_outline"}
> >
{_t("action|cancel")} {_t("action|cancel")}
</AccessibleButton> </AccessibleButton>
@ -985,7 +988,7 @@ export interface IStageComponentProps extends IAuthEntryProps {
inputs?: IInputs; inputs?: IInputs;
stageState?: IStageStatus; stageState?: IStageStatus;
continueText?: string; continueText?: string;
continueKind?: string; continueKind?: ContinueKind;
setEmailSid?(sid: string): void; setEmailSid?(sid: string): void;
onCancel?(): void; onCancel?(): void;
requestEmailToken?(): Promise<void>; requestEmailToken?(): Promise<void>;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 classNames from "classnames";
import { Room, EventType } from "matrix-js-sdk/src/matrix"; import { Room, EventType } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils"; import { sleep } from "matrix-js-sdk/src/utils";
@ -144,7 +144,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
[cli, msc3946ProcessDynamicPredecessor], [cli, msc3946ProcessDynamicPredecessor],
); );
const scrollRef = useRef() as RefObject<AutoHideScrollbar<"div">>; const scrollRef = useRef<AutoHideScrollbar<"div">>(null);
const [scrollState, setScrollState] = useState<IScrollState>({ const [scrollState, setScrollState] = useState<IScrollState>({
// these are estimates which update as soon as it mounts // these are estimates which update as soon as it mounts
scrollTop: 0, scrollTop: 0,

View file

@ -23,7 +23,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import InteractiveAuth, { ERROR_USER_CANCELLED, InteractiveAuthCallback } from "../../structures/InteractiveAuth"; import InteractiveAuth, { ERROR_USER_CANCELLED, InteractiveAuthCallback } from "../../structures/InteractiveAuth";
import { DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import { ContinueKind, DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
@ -34,7 +34,7 @@ type DialogAesthetics = Partial<{
[x: number]: { [x: number]: {
body: string; body: string;
continueText?: string; continueText?: string;
continueKind?: string; continueKind?: ContinueKind;
}; };
}; };
}>; }>;
@ -53,7 +53,7 @@ interface IState {
// next to the InteractiveAuth component. // next to the InteractiveAuth component.
bodyText?: string; bodyText?: string;
continueText?: string; continueText?: string;
continueKind?: string; continueKind?: ContinueKind;
} }
export default class DeactivateAccountDialog extends React.Component<IProps, IState> { export default class DeactivateAccountDialog extends React.Component<IProps, IState> {
@ -98,7 +98,7 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
const aesthetics = DEACTIVATE_AESTHETICS[stage]; const aesthetics = DEACTIVATE_AESTHETICS[stage];
let bodyText: string | undefined; let bodyText: string | undefined;
let continueText: string | undefined; let continueText: string | undefined;
let continueKind: string | undefined; let continueKind: ContinueKind | undefined;
if (aesthetics) { if (aesthetics) {
const phaseAesthetics = aesthetics[phase]; const phaseAesthetics = aesthetics[phase];
if (phaseAesthetics) { if (phaseAesthetics) {

View file

@ -27,7 +27,7 @@ import InteractiveAuth, {
InteractiveAuthCallback, InteractiveAuthCallback,
InteractiveAuthProps, InteractiveAuthProps,
} from "../../structures/InteractiveAuth"; } from "../../structures/InteractiveAuth";
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import { ContinueKind, SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
type DialogAesthetics = Partial<{ type DialogAesthetics = Partial<{
@ -36,7 +36,7 @@ type DialogAesthetics = Partial<{
title: string; title: string;
body: string; body: string;
continueText: string; continueText: string;
continueKind: string; continueKind: ContinueKind;
}; };
}; };
}>; }>;
@ -146,7 +146,7 @@ export default class InteractiveAuthDialog<T> extends React.Component<Interactiv
let title = this.state.authError ? "Error" : this.props.title || _t("common|authentication"); let title = this.state.authError ? "Error" : this.props.title || _t("common|authentication");
let body = this.state.authError ? null : this.props.body; let body = this.state.authError ? null : this.props.body;
let continueText: string | undefined; let continueText: string | undefined;
let continueKind: string | undefined; let continueKind: ContinueKind | undefined;
const dialogAesthetics = this.props.aestheticsForStagePhases || this.getDefaultDialogAesthetics(); const dialogAesthetics = this.props.aestheticsForStagePhases || this.getDefaultDialogAesthetics();
if (!this.state.authError && dialogAesthetics) { if (!this.state.authError && dialogAesthetics) {
if ( if (

View file

@ -33,7 +33,7 @@ import {
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import { _t, getUserLanguage } from "../../../languageHandler"; import { _t, getUserLanguage } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton, { AccessibleButtonKind } from "../elements/AccessibleButton";
import { StopGapWidgetDriver } from "../../../stores/widgets/StopGapWidgetDriver"; import { StopGapWidgetDriver } from "../../../stores/widgets/StopGapWidgetDriver";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { OwnProfileStore } from "../../../stores/OwnProfileStore"; import { OwnProfileStore } from "../../../stores/OwnProfileStore";
@ -158,7 +158,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
.slice(0, MAX_BUTTONS) .slice(0, MAX_BUTTONS)
.reverse() .reverse()
.map((def) => { .map((def) => {
let kind = "secondary"; let kind: AccessibleButtonKind = "secondary";
switch (def.kind) { switch (def.kind) {
case ModalButtonKind.Primary: case ModalButtonKind.Primary:
kind = "primary"; kind = "primary";

View file

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
*/ */
import React, { HTMLAttributes, InputHTMLAttributes, ReactNode } from "react"; import React, { HTMLAttributes, InputHTMLAttributes } from "react";
import classnames from "classnames"; import classnames from "classnames";
import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { getKeyBindingsManager } from "../../../KeyBindingsManager";
@ -22,7 +22,10 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>; export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>;
type AccessibleButtonKind = /**
* The kind of button, similar to how Bootstrap works.
*/
export type AccessibleButtonKind =
| "primary" | "primary"
| "primary_outline" | "primary_outline"
| "primary_sm" | "primary_sm"
@ -58,25 +61,31 @@ type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<
Omit<InputHTMLAttributes<Element>, "onClick">; Omit<InputHTMLAttributes<Element>, "onClick">;
/** /**
* children: React's magic prop. Represents all children given to the element. * Type of props accepted by {@link AccessibleButton}.
* element: (optional) The base element type. "div" by default. *
* onClick: (required) Event handler for button activation. Should be * Extends props accepted by the underlying element specified using the `element` prop.
* implemented exactly like a normal onClick handler.
*/ */
type IProps<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> & { type Props<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> & {
inputRef?: React.Ref<Element>; inputRef?: React.Ref<Element>;
/**
* The base element type. "div" by default.
*/
element?: T; element?: T;
children?: ReactNode; /**
// The kind of button, similar to how Bootstrap works. * The kind of button, similar to how Bootstrap works.
// See available classes for AccessibleButton for options. */
kind?: AccessibleButtonKind | string; kind?: AccessibleButtonKind;
// The ARIA role /**
role?: string; * Whether the button should be disabled.
// The tabIndex */
tabIndex?: number;
disabled?: boolean; disabled?: boolean;
className?: string; /**
* Whether the button should trigger on mousedown event instead of on click event. Defaults to false (click event).
*/
triggerOnMouseDown?: boolean; triggerOnMouseDown?: boolean;
/**
* Event handler for button activation. Should be implemented exactly like a normal `onClick` handler.
*/
onClick: ((e: ButtonEvent) => void | Promise<void>) | null; onClick: ((e: ButtonEvent) => void | Promise<void>) | null;
}; };
@ -104,7 +113,7 @@ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>(
onKeyUp, onKeyUp,
triggerOnMouseDown, triggerOnMouseDown,
...restProps ...restProps
}: IProps<T>): JSX.Element { }: Props<T>): JSX.Element {
const newProps: IAccessibleButtonProps = restProps; const newProps: IAccessibleButtonProps = restProps;
if (disabled) { if (disabled) {
newProps["aria-disabled"] = true; newProps["aria-disabled"] = true;

View file

@ -15,85 +15,105 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { SyntheticEvent, FocusEvent } from "react"; import React, { SyntheticEvent, FocusEvent, useEffect, useState } from "react";
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import Tooltip, { Alignment } from "./Tooltip"; import Tooltip, { Alignment } from "./Tooltip";
interface IProps extends React.ComponentProps<typeof AccessibleButton> { /**
* Type of props accepted by {@link AccessibleTooltipButton}.
*
* Extends that of {@link AccessibleButton}.
*/
interface Props extends React.ComponentProps<typeof AccessibleButton> {
/**
* Title to show in the tooltip and use as aria-label
*/
title?: string; title?: string;
/**
* Tooltip node to show in the tooltip, takes precedence over `title`
*/
tooltip?: React.ReactNode; tooltip?: React.ReactNode;
/**
* Trigger label to render
*/
label?: string; label?: string;
/**
* Classname to apply to the tooltip
*/
tooltipClassName?: string; tooltipClassName?: string;
/**
* Force the tooltip to be hidden
*/
forceHide?: boolean; forceHide?: boolean;
/**
* Alignment to render the tooltip with
*/
alignment?: Alignment; alignment?: Alignment;
/**
* Function to call when the children are hovered over
*/
onHover?: (hovering: boolean) => void; onHover?: (hovering: boolean) => void;
/**
* Function to call when the tooltip goes from shown to hidden.
*/
onHideTooltip?(ev: SyntheticEvent): void; onHideTooltip?(ev: SyntheticEvent): void;
} }
interface IState { function AccessibleTooltipButton({
hover: boolean; title,
} tooltip,
children,
forceHide,
alignment,
onHideTooltip,
tooltipClassName,
...props
}: Props): JSX.Element {
const [hover, setHover] = useState(false);
export default class AccessibleTooltipButton extends React.PureComponent<IProps, IState> { useEffect(() => {
public constructor(props: IProps) { // If forceHide is set then force hover to off to hide the tooltip
super(props); if (forceHide && hover) {
this.state = { setHover(false);
hover: false,
};
}
public componentDidUpdate(prevProps: Readonly<IProps>): void {
if (!prevProps.forceHide && this.props.forceHide && this.state.hover) {
this.setState({
hover: false,
});
} }
} }, [forceHide, hover]);
private showTooltip = (): void => { const showTooltip = (): void => {
if (this.props.onHover) this.props.onHover(true); props.onHover?.(true);
if (this.props.forceHide) return; if (forceHide) return;
this.setState({ setHover(true);
hover: true,
});
}; };
private hideTooltip = (ev: SyntheticEvent): void => { const hideTooltip = (ev: SyntheticEvent): void => {
if (this.props.onHover) this.props.onHover(false); props.onHover?.(false);
this.setState({ setHover(false);
hover: false, onHideTooltip?.(ev);
});
this.props.onHideTooltip?.(ev);
}; };
private onFocus = (ev: FocusEvent): void => { const onFocus = (ev: FocusEvent): void => {
// We only show the tooltip if focus arrived here from some other // We only show the tooltip if focus arrived here from some other
// element, to avoid leaving tooltips hanging around when a modal closes // element, to avoid leaving tooltips hanging around when a modal closes
if (ev.relatedTarget) this.showTooltip(); if (ev.relatedTarget) showTooltip();
}; };
public render(): React.ReactNode { const tip = hover && (title || tooltip) && (
// eslint-disable-next-line @typescript-eslint/no-unused-vars <Tooltip tooltipClassName={tooltipClassName} label={tooltip || title} alignment={alignment} />
const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip, ...props } = );
this.props; return (
<AccessibleButton
const tip = this.state.hover && (title || tooltip) && ( {...props}
<Tooltip tooltipClassName={tooltipClassName} label={tooltip || title} alignment={alignment} /> onMouseOver={showTooltip}
); onMouseLeave={hideTooltip}
return ( onFocus={onFocus}
<AccessibleButton onBlur={hideTooltip}
{...props} aria-label={title || props["aria-label"]}
onMouseOver={this.showTooltip || props.onMouseOver} >
onMouseLeave={this.hideTooltip || props.onMouseLeave} {children}
onFocus={this.onFocus || props.onFocus} {props.label}
onBlur={this.hideTooltip || props.onBlur} {(tooltip || title) && tip}
aria-label={title || props["aria-label"]} </AccessibleButton>
> );
{children}
{this.props.label}
{(tooltip || title) && tip}
</AccessibleButton>
);
}
} }
export default AccessibleTooltipButton;

View file

@ -28,13 +28,14 @@ import {
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import type { ButtonEvent } from "../elements/AccessibleButton"; import { AccessibleButtonKind, ButtonEvent } from "../elements/AccessibleButton";
import MemberAvatar from "../avatars/MemberAvatar"; import MemberAvatar from "../avatars/MemberAvatar";
import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary"; import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary";
import FacePile from "../elements/FacePile"; import FacePile from "../elements/FacePile";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { CallDuration, SessionDuration } from "../voip/CallDuration"; import { CallDuration, SessionDuration } from "../voip/CallDuration";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { ContinueKind } from "../auth/InteractiveAuthEntryComponents";
const MAX_FACES = 8; const MAX_FACES = 8;
@ -43,7 +44,7 @@ interface ActiveCallEventProps {
call: ElementCall | null; call: ElementCall | null;
participatingMembers: RoomMember[]; participatingMembers: RoomMember[];
buttonText: string; buttonText: string;
buttonKind: string; buttonKind: AccessibleButtonKind;
buttonDisabledTooltip?: string; buttonDisabledTooltip?: string;
onButtonClick: ((ev: ButtonEvent) => void) | null; onButtonClick: ((ev: ButtonEvent) => void) | null;
} }
@ -125,7 +126,9 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
[call], [call],
); );
const [buttonText, buttonKind, onButtonClick] = useMemo(() => { const [buttonText, buttonKind, onButtonClick] = useMemo<
[string, ContinueKind, null | ((ev: ButtonEvent) => void)]
>(() => {
switch (connectionState) { switch (connectionState) {
case ConnectionState.Disconnected: case ConnectionState.Disconnected:
return [_t("action|join"), "primary", connect]; return [_t("action|join"), "primary", connect];

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { PropsWithChildren, useRef } from "react"; import React, { PropsWithChildren } from "react";
import { User } from "matrix-js-sdk/src/matrix"; import { User } from "matrix-js-sdk/src/matrix";
import ReadReceiptMarker, { IReadReceiptInfo } from "./ReadReceiptMarker"; import ReadReceiptMarker, { IReadReceiptInfo } from "./ReadReceiptMarker";
@ -284,8 +284,7 @@ interface ISectionHeaderProps {
} }
function SectionHeader({ className, children }: PropsWithChildren<ISectionHeaderProps>): JSX.Element { function SectionHeader({ className, children }: PropsWithChildren<ISectionHeaderProps>): JSX.Element {
const ref = useRef<HTMLHeadingElement>(null); const [onFocus, , ref] = useRovingTabIndex<HTMLHeadingElement>();
const [onFocus] = useRovingTabIndex(ref);
return ( return (
<h3 className={className} role="menuitem" onFocus={onFocus} tabIndex={-1} ref={ref}> <h3 className={className} role="menuitem" onFocus={onFocus} tabIndex={-1} ref={ref}>

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { createRef, RefObject } from "react"; import React, { createRef } from "react";
import { RoomMember } from "matrix-js-sdk/src/matrix"; import { RoomMember } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -73,7 +73,7 @@ interface IReadReceiptMarkerStyle {
} }
export default class ReadReceiptMarker extends React.PureComponent<IProps, IState> { export default class ReadReceiptMarker extends React.PureComponent<IProps, IState> {
private avatar: React.RefObject<HTMLDivElement | HTMLImageElement | HTMLSpanElement> = createRef(); private avatar = createRef<HTMLDivElement>();
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@ -199,7 +199,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
public render(): React.ReactNode { public render(): React.ReactNode {
if (this.state.suppressDisplay) { if (this.state.suppressDisplay) {
return <div ref={this.avatar as RefObject<HTMLDivElement>} />; return <div ref={this.avatar} />;
} }
const style = { const style = {

View file

@ -20,7 +20,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
import Field from "../elements/Field"; import Field from "../elements/Field";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton, { AccessibleButtonKind } from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { UserFriendlyError, _t, _td } from "../../../languageHandler"; import { UserFriendlyError, _t, _td } from "../../../languageHandler";
@ -45,7 +45,7 @@ interface IProps {
onError: (error: Error) => void; onError: (error: Error) => void;
rowClassName?: string; rowClassName?: string;
buttonClassName?: string; buttonClassName?: string;
buttonKind?: string; buttonKind?: AccessibleButtonKind;
buttonLabel?: string; buttonLabel?: string;
confirm?: boolean; confirm?: boolean;
// Whether to autoFocus the new password input // Whether to autoFocus the new password input