Skip to content

Commit

Permalink
Add error and help aria labels to form fields (#728)
Browse files Browse the repository at this point in the history
* Add error and help aria labels to form fields.
  • Loading branch information
huwshimi authored Mar 23, 2022
1 parent 3af9492 commit ca8ec96
Show file tree
Hide file tree
Showing 21 changed files with 278 additions and 54 deletions.
10 changes: 10 additions & 0 deletions src/__mocks__/nanoid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
let id = 0;

beforeEach(() => {
id = 0;
});

export const nanoid = (): string => {
id++;
return `mock-nanoid-${id}`;
};
15 changes: 13 additions & 2 deletions src/components/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { mount, shallow } from "enzyme";
import React from "react";
import * as nanoid from "nanoid";

import { MOCK_UUID } from "../../setupTests";
import Accordion from "./Accordion";

describe("Accordion", () => {
beforeEach(() => {
jest.spyOn(nanoid, "nanoid").mockReturnValue("mocked-nanoid");
});

afterEach(() => {
jest.restoreAllMocks();
});

it("renders", () => {
const wrapper = shallow(
<Accordion
Expand Down Expand Up @@ -63,7 +71,10 @@ describe("Accordion", () => {
);
const title = wrapper.find(".p-accordion__tab").at(0);
title.simulate("click");
expect(onExpandedChange).toHaveBeenCalledWith(MOCK_UUID, "Advanced topics");
expect(onExpandedChange).toHaveBeenCalledWith(
"mocked-nanoid",
"Advanced topics"
);
// Clicking the title again should close the accordion section.
title.simulate("click");
expect(onExpandedChange).toHaveBeenCalledWith(null, null);
Expand Down
5 changes: 2 additions & 3 deletions src/components/AccordionSection/AccordionSection.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { shallow } from "enzyme";
import React from "react";

import { MOCK_UUID } from "../../setupTests";
import AccordionSection from "./AccordionSection";

describe("AccordionSection ", () => {
Expand Down Expand Up @@ -50,12 +49,12 @@ describe("AccordionSection ", () => {
);
wrapper.find(".p-accordion__tab").at(0).simulate("click");

expect(onTitleClick.mock.calls[0]).toEqual([true, MOCK_UUID]);
expect(onTitleClick.mock.calls[0]).toEqual([true, "mock-nanoid-1"]);
wrapper.setProps({ expanded });
// Clicking the title again should close the accordion section.
wrapper.find(".p-accordion__tab").at(0).simulate("click");

expect(onTitleClick.mock.calls[1]).toEqual([false, MOCK_UUID]);
expect(onTitleClick.mock.calls[1]).toEqual([false, "mock-nanoid-1"]);
});

it("can use a key for expanded state", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports[`AccordionSection renders 1`] = `
role="heading"
>
<button
aria-controls="#Uakgb_J5m9g-0JDMbcJqLJ"
aria-controls="#mock-nanoid-1"
aria-expanded="false"
className="p-accordion__tab"
onClick={[Function]}
Expand All @@ -22,9 +22,9 @@ exports[`AccordionSection renders 1`] = `
</div>
<section
aria-hidden="true"
aria-labelledby="Uakgb_J5m9g-0JDMbcJqLJ"
aria-labelledby="mock-nanoid-1"
className="p-accordion__panel"
id="Uakgb_J5m9g-0JDMbcJqLJ"
id="mock-nanoid-1"
role="tabpanel"
>
<span>
Expand All @@ -44,7 +44,7 @@ exports[`AccordionSection renders headings for titles 1`] = `
role={null}
>
<button
aria-controls="#Uakgb_J5m9g-0JDMbcJqLJ"
aria-controls="#mock-nanoid-1"
aria-expanded="false"
className="p-accordion__tab"
onClick={[Function]}
Expand All @@ -56,9 +56,9 @@ exports[`AccordionSection renders headings for titles 1`] = `
</h4>
<section
aria-hidden="true"
aria-labelledby="Uakgb_J5m9g-0JDMbcJqLJ"
aria-labelledby="mock-nanoid-1"
className="p-accordion__panel"
id="Uakgb_J5m9g-0JDMbcJqLJ"
id="mock-nanoid-1"
role="tabpanel"
>
<span>
Expand Down
40 changes: 26 additions & 14 deletions src/components/Field/Field.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,65 +22,77 @@ describe("Field ", () => {
});

it("can display a caution message", () => {
const wrapper = shallow(<Field caution="Are you sure?" />);
const wrapper = shallow(
<Field caution="Are you sure?" validationId="id-1" />
);
compareJSX(
wrapper.find(".p-form-validation__message"),
<p className="p-form-validation__message">
<p className="p-form-validation__message" id="id-1">
<strong>{"Caution"}:</strong> {"Are you sure?"}
</p>
);
expect(wrapper.prop("className").includes("is-caution")).toBe(true);
});

it("can display a caution node", () => {
const wrapper = shallow(<Field caution={<span>Are you sure?</span>} />);
const wrapper = shallow(
<Field caution={<span>Are you sure?</span>} validationId="id-1" />
);
compareJSX(
wrapper.find(".p-form-validation__message"),
<p className="p-form-validation__message">
<p className="p-form-validation__message" id="id-1">
<strong>{"Caution"}:</strong> <span>Are you sure?</span>
</p>
);
expect(wrapper.prop("className").includes("is-caution")).toBe(true);
});

it("can display an error message", () => {
const wrapper = shallow(<Field error="You can't do that" />);
const wrapper = shallow(
<Field error="You can't do that" validationId="id-1" />
);
compareJSX(
wrapper.find(".p-form-validation__message"),
<p className="p-form-validation__message">
<p className="p-form-validation__message" id="id-1">
<strong>{"Error"}:</strong> {"You can't do that"}
</p>
);
expect(wrapper.prop("className").includes("is-error")).toBe(true);
});

it("can display an error node", () => {
const wrapper = shallow(<Field error={<span>You can't do that</span>} />);
const wrapper = shallow(
<Field error={<span>You can't do that</span>} validationId="id-1" />
);
compareJSX(
wrapper.find(".p-form-validation__message"),
<p className="p-form-validation__message">
<p className="p-form-validation__message" id="id-1">
<strong>{"Error"}:</strong> <span>You can't do that</span>
</p>
);
expect(wrapper.prop("className").includes("is-error")).toBe(true);
});

it("can display a success message", () => {
const wrapper = shallow(<Field success="You did it!" />);
const wrapper = shallow(
<Field success="You did it!" validationId="id-1" />
);
compareJSX(
wrapper.find(".p-form-validation__message"),
<p className="p-form-validation__message">
<p className="p-form-validation__message" id="id-1">
<strong>{"Success"}:</strong> {"You did it!"}
</p>
);
expect(wrapper.prop("className").includes("is-success")).toBe(true);
});

it("can display a success node", () => {
const wrapper = shallow(<Field success={<span>You did it!</span>} />);
const wrapper = shallow(
<Field success={<span>You did it!</span>} validationId="id-1" />
);
compareJSX(
wrapper.find(".p-form-validation__message"),
<p className="p-form-validation__message">
<p className="p-form-validation__message" id="id-1">
<strong>{"Success"}:</strong> <span>You did it!</span>
</p>
);
Expand All @@ -96,11 +108,11 @@ describe("Field ", () => {

it("can display a help node", () => {
const wrapper = shallow(
<Field help={<span>This is how you do it</span>} />
<Field help={<span>This is how you do it</span>} helpId="id-1" />
);
compareJSX(
wrapper.find(".p-form-help-text"),
<p className="p-form-help-text">
<p className="p-form-help-text" id="id-1">
<span>This is how you do it</span>
</p>
);
Expand Down
35 changes: 27 additions & 8 deletions src/components/Field/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export type Props = {
* Help text to show below the field.
*/
help?: ReactNode;
/**
* An id to give to the help element.
*/
helpId?: string;
/**
* Whether the component is wrapping a select element.
*/
Expand Down Expand Up @@ -63,23 +67,32 @@ export type Props = {
* The content for success validation.
*/
success?: ReactNode;
/**
* An id to give to the caution, error or success element.
*/
validationId?: string;
};

const generateHelp = (help: Props["help"]) =>
help && <p className="p-form-help-text">{help}</p>;
const generateHelp = (help: Props["help"], helpId: Props["helpId"]) =>
help && (
<p className="p-form-help-text" id={helpId}>
{help}
</p>
);

const generateError = (
error: Props["error"],
caution: Props["caution"],
success: Props["success"]
success: Props["success"],
validationId: Props["validationId"]
) => {
if (!error && !caution && !success) {
return null;
}
const messageType =
(error && "Error") || (caution && "Caution") || (success && "Success");
return (
<p className="p-form-validation__message">
<p className="p-form-validation__message" id={validationId}>
<strong>{messageType}:</strong> {error || caution || success}
</p>
);
Expand Down Expand Up @@ -114,7 +127,9 @@ const generateContent = (
help: Props["help"],
error: Props["error"],
caution: Props["caution"],
success: Props["success"]
success: Props["success"],
validationId: string,
helpId: string
) => (
<div className="p-form__control u-clearfix">
{isSelect ? (
Expand All @@ -123,8 +138,8 @@ const generateContent = (
children
)}
{!labelFirst && labelNode}
{generateHelp(help)}
{generateError(error, caution, success)}
{generateHelp(help, helpId)}
{generateError(error, caution, success, validationId)}
</div>
);

Expand All @@ -135,13 +150,15 @@ const Field = ({
error,
forId,
help,
helpId,
isSelect,
label,
labelClassName,
labelFirst = true,
required,
stacked,
success,
validationId,
}: Props): JSX.Element => {
const labelNode = generateLabel(
forId,
Expand All @@ -158,7 +175,9 @@ const Field = ({
help,
error,
caution,
success
success,
validationId,
helpId
);
return (
<div
Expand Down
1 change: 0 additions & 1 deletion src/components/Form/Form.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ Form controls have global styling defined at the HTML element level. Labels and
type="text"
id="address-inline22"
aria-invalid="true"
aria-describedby="input-error-message-inline"
label="Email address"
error="Please enter a valid email address."
/>
Expand Down
39 changes: 38 additions & 1 deletion src/components/Input/Input.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { render, screen } from "@testing-library/react";
import { mount, shallow } from "enzyme";
import React from "react";

import Input from "./Input";

describe("Input ", () => {
describe("Input", () => {
it("renders", () => {
const wrapper = shallow(<Input type="text" id="test-id" />);
expect(wrapper).toMatchSnapshot();
Expand Down Expand Up @@ -31,6 +32,7 @@ describe("Input ", () => {
document.body.appendChild(container);
const wrapper = mount(<Input takeFocus />, { attachTo: container });
expect(wrapper.find("input").getDOMNode()).toBe(document.activeElement);
document.body.removeChild(container);
});

it("sets aria-invalid to false when there is no error", () => {
Expand Down Expand Up @@ -76,3 +78,38 @@ describe("Input ", () => {
).toBe(true);
});
});

describe("Input RTL", () => {
it("can display an error for a text input", async () => {
render(<Input error="Uh oh!" type="text" />);
expect(screen.getByRole("textbox")).toHaveErrorMessage("Error: Uh oh!");
});

it("can display an error for a radiobutton", async () => {
render(<Input error="Uh oh!" type="radio" />);
expect(screen.getByRole("radio")).toHaveErrorMessage("Error: Uh oh!");
});

it("can display an error for a checkbox", async () => {
render(<Input error="Uh oh!" type="checkbox" />);
expect(screen.getByRole("checkbox")).toHaveErrorMessage("Error: Uh oh!");
});

it("can display help for a text input", async () => {
const help = "Save me!";
render(<Input help={help} type="text" />);
expect(screen.getByRole("textbox")).toHaveAccessibleDescription(help);
});

it("can display help for a radiobutton", async () => {
const help = "Save me!";
render(<Input help={help} type="radio" />);
expect(screen.getByRole("radio")).toHaveAccessibleDescription(help);
});

it("can display help for a checkbox", async () => {
const help = "Save me!";
render(<Input help={help} type="checkbox" />);
expect(screen.getByRole("checkbox")).toHaveAccessibleDescription(help);
});
});
Loading

0 comments on commit ca8ec96

Please sign in to comment.