mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 17:25:50 +03:00
Improved a11y for Field feedback and Secure Phrase input (#10320)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
f60f7a19af
commit
0c1c3f1cde
5 changed files with 78 additions and 3 deletions
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue