Improved a11y for Field feedback and Secure Phrase input (#10320)

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Sebbones 2023-03-08 12:32:50 +01:00 committed by GitHub
parent f60f7a19af
commit 0c1c3f1cde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 78 additions and 3 deletions

View file

@ -63,6 +63,7 @@ interface IState {
*/ */
export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> { export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> {
private fileUpload = React.createRef<HTMLInputElement>(); private fileUpload = React.createRef<HTMLInputElement>();
private inputRef = React.createRef<HTMLInputElement>();
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@ -178,7 +179,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement> | React.MouseEvent): Promise<void> => { private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement> | React.MouseEvent): Promise<void> => {
ev.preventDefault(); ev.preventDefault();
if (this.state.passPhrase.length <= 0) return; if (this.state.passPhrase.length <= 0) {
this.inputRef.current?.focus();
return;
}
this.setState({ keyMatches: null }); this.setState({ keyMatches: null });
const input = { passphrase: this.state.passPhrase }; const input = { passphrase: this.state.passPhrase };
@ -187,6 +191,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
this.props.onFinished(input); this.props.onFinished(input);
} else { } else {
this.setState({ keyMatches }); this.setState({ keyMatches });
this.inputRef.current?.focus();
} }
}; };
@ -351,6 +356,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}> <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
<Field <Field
inputRef={this.inputRef}
id="mx_passPhraseInput" id="mx_passPhraseInput"
className="mx_AccessSecretStorageDialog_passPhraseInput" className="mx_AccessSecretStorageDialog_passPhraseInput"
type="password" type="password"

View file

@ -289,12 +289,20 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
// Handle displaying feedback on validity // Handle displaying feedback on validity
let fieldTooltip; let fieldTooltip;
if (tooltipContent || this.state.feedback) { if (tooltipContent || this.state.feedback) {
let role: React.AriaRole;
if (tooltipContent) {
role = "tooltip";
} else {
role = this.state.valid ? "status" : "alert";
}
fieldTooltip = ( fieldTooltip = (
<Tooltip <Tooltip
tooltipClassName={classNames("mx_Field_tooltip", "mx_Tooltip_noMargin", tooltipClassName)} tooltipClassName={classNames("mx_Field_tooltip", "mx_Tooltip_noMargin", tooltipClassName)}
visible={(this.state.focused && forceTooltipVisible) || this.state.feedbackVisible} visible={(this.state.focused && forceTooltipVisible) || this.state.feedbackVisible}
label={tooltipContent || this.state.feedback} label={tooltipContent || this.state.feedback}
alignment={Tooltip.Alignment.Right} alignment={Tooltip.Alignment.Right}
role={role}
/> />
); );
} }

View file

@ -51,6 +51,8 @@ export interface ITooltipProps {
id?: string; id?: string;
// If the parent is over this width, act as if it is only this wide // If the parent is over this width, act as if it is only this wide
maxParentWidth?: number; maxParentWidth?: number;
// aria-role passed to the tooltip
role?: React.AriaRole;
} }
type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transform" | "left">>; type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transform" | "left">>;
@ -186,7 +188,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
style.display = this.props.visible ? "block" : "none"; style.display = this.props.visible ? "block" : "none";
const tooltip = ( const tooltip = (
<div role="tooltip" className={tooltipClasses} style={style}> <div role={this.props.role || "tooltip"} className={tooltipClasses} style={style}>
<div className="mx_Tooltip_chevron" /> <div className="mx_Tooltip_chevron" />
{this.props.label} {this.props.label}
</div> </div>

View file

@ -134,5 +134,7 @@ describe("AccessSecretStorageDialog", () => {
"👎 Unable to access secret storage. Please verify that you entered the correct Security Phrase.", "👎 Unable to access secret storage. Please verify that you entered the correct Security Phrase.",
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus();
}); });
}); });

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from "react"; 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"; import Field from "../../../../src/components/views/elements/Field";
@ -51,4 +51,61 @@ describe("Field", () => {
expect(screen.getByRole("textbox")).not.toHaveAttribute("placeholder", "my placeholder"); 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();
});
});
}); });