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

Improved a11y for Field feedback and Secure Phrase input #10320

Merged
merged 8 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ interface IState {
*/
export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> {
private fileUpload = React.createRef<HTMLInputElement>();
private inputRef = React.createRef<HTMLInputElement>();

public constructor(props: IProps) {
super(props);
Expand Down Expand Up @@ -178,7 +179,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement> | React.MouseEvent): Promise<void> => {
ev.preventDefault();

if (this.state.passPhrase.length <= 0) return;
if (this.state.passPhrase.length <= 0) {
this.inputRef.current?.focus();
return;
}

this.setState({ keyMatches: null });
const input = { passphrase: this.state.passPhrase };
Expand All @@ -187,6 +191,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
this.props.onFinished(input);
} else {
this.setState({ keyMatches });
this.inputRef.current?.focus();
}
};

Expand Down Expand Up @@ -351,6 +356,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp

<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
<Field
inputRef={this.inputRef}
id="mx_passPhraseInput"
className="mx_AccessSecretStorageDialog_passPhraseInput"
type="password"
Expand Down
8 changes: 8 additions & 0 deletions src/components/views/elements/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,20 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
// Handle displaying feedback on validity
let fieldTooltip;
if (tooltipContent || this.state.feedback) {
let role: React.AriaRole;
if (tooltipContent) {
role = "tooltip";
} else {
role = this.state.valid ? "status" : "alert";
}

fieldTooltip = (
<Tooltip
tooltipClassName={classNames("mx_Field_tooltip", "mx_Tooltip_noMargin", tooltipClassName)}
visible={(this.state.focused && forceTooltipVisible) || this.state.feedbackVisible}
label={tooltipContent || this.state.feedback}
alignment={Tooltip.Alignment.Right}
role={role}
/>
);
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/views/elements/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export interface ITooltipProps {
id?: string;
// If the parent is over this width, act as if it is only this wide
maxParentWidth?: number;
// aria-role passed to the tooltip
role?: React.AriaRole;
}

type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transform" | "left">>;
Expand Down Expand Up @@ -186,7 +188,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
style.display = this.props.visible ? "block" : "none";

const tooltip = (
<div role="tooltip" className={tooltipClasses} style={style}>
<div role={this.props.role || "tooltip"} className={tooltipClasses} style={style}>
<div className="mx_Tooltip_chevron" />
{this.props.label}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,7 @@ describe("AccessSecretStorageDialog", () => {
"👎 Unable to access secret storage. Please verify that you entered the correct Security Phrase.",
),
).toBeInTheDocument();

expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus();
});
});
59 changes: 58 additions & 1 deletion test/components/views/elements/Field-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import React from "react";
import { render, screen } from "@testing-library/react";
import { act, fireEvent, render, screen } from "@testing-library/react";

import Field from "../../../../src/components/views/elements/Field";

Expand Down Expand Up @@ -51,4 +51,61 @@ describe("Field", () => {
expect(screen.getByRole("textbox")).not.toHaveAttribute("placeholder", "my placeholder");
});
});

describe("Feedback", () => {
it("Should mark the feedback as alert if invalid", async () => {
render(
<Field
value=""
validateOnFocus
onValidate={() => Promise.resolve({ valid: false, feedback: "Invalid" })}
/>,
);

// When invalid
await act(async () => {
fireEvent.focus(screen.getByRole("textbox"));
});

// Expect 'alert' role
expect(screen.queryByRole("alert")).toBeInTheDocument();
});

it("Should mark the feedback as status if valid", async () => {
render(
<Field
value=""
validateOnFocus
onValidate={() => Promise.resolve({ valid: true, feedback: "Valid" })}
/>,
);

// When valid
await act(async () => {
fireEvent.focus(screen.getByRole("textbox"));
});

// Expect 'status' role
expect(screen.queryByRole("status")).toBeInTheDocument();
});

it("Should mark the feedback as tooltip if custom tooltip set", async () => {
render(
<Field
value=""
validateOnFocus
onValidate={() => Promise.resolve({ valid: true, feedback: "Valid" })}
tooltipContent="Tooltip"
/>,
);

// When valid or invalid and 'tooltipContent' set
await act(async () => {
fireEvent.focus(screen.getByRole("textbox"));
});

// Expect 'tooltip' role
expect(screen.queryByRole("tooltip")).toBeInTheDocument();
});
});
});