Skip to content

Commit 0c1c3f1

Browse files
Sebbonest3chguy
andauthored
Improved a11y for Field feedback and Secure Phrase input (matrix-org#10320)
Co-authored-by: Michael Telatynski <[email protected]>
1 parent f60f7a1 commit 0c1c3f1

File tree

5 files changed

+78
-3
lines changed

5 files changed

+78
-3
lines changed

src/components/views/dialogs/security/AccessSecretStorageDialog.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ interface IState {
6363
*/
6464
export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> {
6565
private fileUpload = React.createRef<HTMLInputElement>();
66+
private inputRef = React.createRef<HTMLInputElement>();
6667

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

181-
if (this.state.passPhrase.length <= 0) return;
182+
if (this.state.passPhrase.length <= 0) {
183+
this.inputRef.current?.focus();
184+
return;
185+
}
182186

183187
this.setState({ keyMatches: null });
184188
const input = { passphrase: this.state.passPhrase };
@@ -187,6 +191,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
187191
this.props.onFinished(input);
188192
} else {
189193
this.setState({ keyMatches });
194+
this.inputRef.current?.focus();
190195
}
191196
};
192197

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

352357
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
353358
<Field
359+
inputRef={this.inputRef}
354360
id="mx_passPhraseInput"
355361
className="mx_AccessSecretStorageDialog_passPhraseInput"
356362
type="password"

src/components/views/elements/Field.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,20 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
289289
// Handle displaying feedback on validity
290290
let fieldTooltip;
291291
if (tooltipContent || this.state.feedback) {
292+
let role: React.AriaRole;
293+
if (tooltipContent) {
294+
role = "tooltip";
295+
} else {
296+
role = this.state.valid ? "status" : "alert";
297+
}
298+
292299
fieldTooltip = (
293300
<Tooltip
294301
tooltipClassName={classNames("mx_Field_tooltip", "mx_Tooltip_noMargin", tooltipClassName)}
295302
visible={(this.state.focused && forceTooltipVisible) || this.state.feedbackVisible}
296303
label={tooltipContent || this.state.feedback}
297304
alignment={Tooltip.Alignment.Right}
305+
role={role}
298306
/>
299307
);
300308
}

src/components/views/elements/Tooltip.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export interface ITooltipProps {
5151
id?: string;
5252
// If the parent is over this width, act as if it is only this wide
5353
maxParentWidth?: number;
54+
// aria-role passed to the tooltip
55+
role?: React.AriaRole;
5456
}
5557

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

188190
const tooltip = (
189-
<div role="tooltip" className={tooltipClasses} style={style}>
191+
<div role={this.props.role || "tooltip"} className={tooltipClasses} style={style}>
190192
<div className="mx_Tooltip_chevron" />
191193
{this.props.label}
192194
</div>

test/components/views/dialogs/AccessSecretStorageDialog-test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,7 @@ describe("AccessSecretStorageDialog", () => {
134134
"👎 Unable to access secret storage. Please verify that you entered the correct Security Phrase.",
135135
),
136136
).toBeInTheDocument();
137+
138+
expect(screen.getByPlaceholderText("Security Phrase")).toHaveFocus();
137139
});
138140
});

test/components/views/elements/Field-test.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616

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

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

@@ -51,4 +51,61 @@ describe("Field", () => {
5151
expect(screen.getByRole("textbox")).not.toHaveAttribute("placeholder", "my placeholder");
5252
});
5353
});
54+
55+
describe("Feedback", () => {
56+
it("Should mark the feedback as alert if invalid", async () => {
57+
render(
58+
<Field
59+
value=""
60+
validateOnFocus
61+
onValidate={() => Promise.resolve({ valid: false, feedback: "Invalid" })}
62+
/>,
63+
);
64+
65+
// When invalid
66+
await act(async () => {
67+
fireEvent.focus(screen.getByRole("textbox"));
68+
});
69+
70+
// Expect 'alert' role
71+
expect(screen.queryByRole("alert")).toBeInTheDocument();
72+
});
73+
74+
it("Should mark the feedback as status if valid", async () => {
75+
render(
76+
<Field
77+
value=""
78+
validateOnFocus
79+
onValidate={() => Promise.resolve({ valid: true, feedback: "Valid" })}
80+
/>,
81+
);
82+
83+
// When valid
84+
await act(async () => {
85+
fireEvent.focus(screen.getByRole("textbox"));
86+
});
87+
88+
// Expect 'status' role
89+
expect(screen.queryByRole("status")).toBeInTheDocument();
90+
});
91+
92+
it("Should mark the feedback as tooltip if custom tooltip set", async () => {
93+
render(
94+
<Field
95+
value=""
96+
validateOnFocus
97+
onValidate={() => Promise.resolve({ valid: true, feedback: "Valid" })}
98+
tooltipContent="Tooltip"
99+
/>,
100+
);
101+
102+
// When valid or invalid and 'tooltipContent' set
103+
await act(async () => {
104+
fireEvent.focus(screen.getByRole("textbox"));
105+
});
106+
107+
// Expect 'tooltip' role
108+
expect(screen.queryByRole("tooltip")).toBeInTheDocument();
109+
});
110+
});
54111
});

0 commit comments

Comments
 (0)