Skip to content

Commit

Permalink
Modifies TextInput2 error validation behavior
Browse files Browse the repository at this point in the history
Changes initialIsInError prop to errorOnEmpty, to more accurately describe the prop's use case. If errorOnEmpty, empty error state is displayed if the input has no value and is out of focus (the error state is removed on focus). If not errorOnEmpty, error state is only displayed on blur, and is removed if the value is empty or if the error is resolved.
  • Loading branch information
antoniodangond committed Sep 30, 2021
1 parent 099b799 commit 2733131
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 35 deletions.
17 changes: 9 additions & 8 deletions docs/components/TextInput2View.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default class TextInput2View extends React.PureComponent {
showIcon: false,
requirement: FormElementRequirement.REQUIRED,
obscurable: false,
initialIsInError: false,
errorOnEmpty: false,
size: FormElementSize.MEDIUM,
};

Expand Down Expand Up @@ -72,7 +72,7 @@ export default class TextInput2View extends React.PureComponent {
requirement={this.state.requirement}
obscurable={this.state.obscurable}
value={this.state.value}
initialIsInError={this.state.initialIsInError}
errorOnEmpty={this.state.errorOnEmpty}
errorValidation={(value) => (value.toLowerCase() !== value ? "only lowercase" : null)}
onChange={(e) => this.setState({ value: e.target.value })}
size={this.state.size}
Expand All @@ -95,7 +95,7 @@ export default class TextInput2View extends React.PureComponent {
showIcon,
requirement,
obscurable,
initialIsInError,
errorOnEmpty,
size,
} = this.state;

Expand Down Expand Up @@ -162,10 +162,10 @@ export default class TextInput2View extends React.PureComponent {
<input
type="checkbox"
className={cssClass.CONFIG_TOGGLE}
checked={initialIsInError}
onChange={(e) => this.setState({ initialIsInError: e.target.checked })}
checked={errorOnEmpty}
onChange={(e) => this.setState({ errorOnEmpty: e.target.checked })}
/>{" "}
<span className={cssClass.CONFIG_TEXT}>Initial error</span>
<span className={cssClass.CONFIG_TEXT}>Error on empty</span>
</label>
<div className={cssClass.CONFIG}>
<span className={cssClass.CONFIG_TEXT}>Requirement:</span>
Expand Down Expand Up @@ -259,9 +259,10 @@ export default class TextInput2View extends React.PureComponent {
optional: true,
},
{
name: "initialIsInError",
name: "errorOnEmpty",
type: "boolean",
description: "Intialize the component in an error state",
description:
"Display an error state when the input value is empty and the input is out of focus",
optional: true,
},
{
Expand Down
39 changes: 15 additions & 24 deletions src/TextInput2/TextInput2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface Props {
icon?: React.ReactNode;
requirement?: Values<typeof FormElementRequirement>;
obscurable?: boolean;
initialIsInError?: boolean;
errorOnEmpty?: boolean;
// returns an error message, null for no error
errorValidation?: (value: string) => string | null;
value: string;
Expand Down Expand Up @@ -61,7 +61,7 @@ const TextInput2: React.FC<Props> = ({
helpText,
icon,
requirement,
initialIsInError,
errorOnEmpty,
errorValidation,
obscurable,
value,
Expand All @@ -72,38 +72,29 @@ const TextInput2: React.FC<Props> = ({
}) => {
const id = name;
const [isFocused, setIsFocused] = useState(false);

const [isObscured, setIsObscured] = useState(true);
const inputType = obscurable && isObscured ? "password" : "text";

// empty string is an error state with no message (e.g. required)
const [errorMessage, setErrorMessage] = useState(initialIsInError ? "" : null);

useEffect(() => {
if (requirement === FormElementRequirement.REQUIRED && value === "") {
setErrorMessage(initialIsInError ? "" : null);
}
}, [initialIsInError]);
// NOTE: setting errorMessage to an empty string displays an error state with no message (e.g. required)
const [errorMessage, setErrorMessage] = useState(null);

useEffect(() => {
// don't show error if nothing has happened yet
if (!isFocused && errorMessage === null) {
return;
}

if (requirement === FormElementRequirement.REQUIRED && value === "") {
setErrorMessage("");
// if value is empty, either:
// 1) display empty error if errorOnEmpty is true and input isn't in focus
// 2) remove error
if (!value) {
setErrorMessage(errorOnEmpty && !isFocused ? "" : null);
return;
}

const newErrorMessage = errorValidation(value);
if (newErrorMessage) {
// only show error when input isn't in focus
if (newErrorMessage && !isFocused) {
setErrorMessage(newErrorMessage);
return;
}

setErrorMessage(null);
}, [value, isFocused]);
// remove error if it has been corrected
if (!newErrorMessage) setErrorMessage(null);
}, [value, isFocused, errorOnEmpty]);

return (
<div className={classnames(cssClass.CONTAINER, formElementSizeClassName(size), className)}>
Expand Down Expand Up @@ -170,7 +161,7 @@ const TextInput2: React.FC<Props> = ({
};

TextInput2.defaultProps = {
initialIsInError: false,
errorOnEmpty: false,
errorValidation: () => null,
size: FormElementSize.FULL_WIDTH,
};
Expand Down
100 changes: 97 additions & 3 deletions src/TextInput2/TextInput2_test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import * as React from "react";
import { shallow } from "enzyme";
import { shallow, mount } from "enzyme";

import TextInput2, { cssClass } from "./TextInput2";

// util functions
const getInput = (wrapper) => wrapper.find("input");
const getErrorContainer = (wrapper) => wrapper.find(`.${cssClass.INPUT_CONTAINER_ERROR}`);
const changeInputValue = (input, value) => {
input.simulate("change", { target: { value } });
};

describe("TextInput2", () => {
let defaultProps;
beforeEach(() => {
Expand Down Expand Up @@ -54,18 +61,105 @@ describe("TextInput2", () => {
expect(myComponent.props().className).toMatch("my--custom--class");
});

describe("error validation", () => {
const validValue = "valid";
const invalidValue = "invalid";
let wrapper;
beforeEach(() => {
defaultProps = {
...defaultProps,
errorValidation: (value) => (value === invalidValue ? "test error message" : null),
onChange: (e) => {
wrapper.setProps({ value: e.target.value });
wrapper.update();
},
};
});

it("displays error on blur if provided errorValidation function returns a string", () => {
wrapper = mount(<TextInput2 {...defaultProps} />);
const input = getInput(wrapper);

input.simulate("focus");
changeInputValue(input, invalidValue);
let errorContainer = getErrorContainer(wrapper);
// error should not be displayed while element is still in focus
expect(errorContainer).not.toExist();

input.simulate("blur");
errorContainer = getErrorContainer(wrapper);
// error is displayed after focus is removed
expect(errorContainer).toExist();
});

it("removes error while focused if provided errorValidation function returns null", () => {
defaultProps.value = invalidValue;
wrapper = mount(<TextInput2 {...defaultProps} />);
let errorContainer = getErrorContainer(wrapper);
// error is already present from invalid value
expect(errorContainer).toExist();

const input = getInput(wrapper);
input.simulate("focus");
changeInputValue(input, validValue);
errorContainer = getErrorContainer(wrapper);
// error is removed after valid value is set
expect(errorContainer).not.toExist();
});

it("removes error if input value is empty string", () => {
defaultProps.value = invalidValue;
wrapper = mount(<TextInput2 {...defaultProps} />);
let errorContainer = getErrorContainer(wrapper);
// error is already present from invalid value
expect(errorContainer).toExist();

const input = getInput(wrapper);
input.simulate("focus");
changeInputValue(input, "");
errorContainer = getErrorContainer(wrapper);
// error is removed after value is set to empty string
expect(errorContainer).not.toExist();
});

it("displays error if value is empty string and errorOnEmpty is true", () => {
defaultProps.errorOnEmpty = true;
defaultProps.value = "";
wrapper = mount(<TextInput2 {...defaultProps} />);
const errorContainer = getErrorContainer(wrapper);
// error is displayed for empty value
expect(errorContainer).toExist();
});

it("removes error on focus if value is empty string and errorOnEmpty is true", () => {
defaultProps.errorOnEmpty = true;
defaultProps.value = "";
wrapper = mount(<TextInput2 {...defaultProps} />);
const input = getInput(wrapper);

let errorContainer = getErrorContainer(wrapper);
// error is displayed for empty value
expect(errorContainer).toExist();

input.simulate("focus");
errorContainer = getErrorContainer(wrapper);
// empty error is removed on focus
expect(errorContainer).not.toExist();
});
});

describe("event handling", () => {
it("executes onFocus prop if provided", () => {
defaultProps.onFocus = jest.fn();
const wrapper = shallow(<TextInput2 {...defaultProps} />);
wrapper.find("input").simulate("focus");
getInput(wrapper).simulate("focus");
expect(defaultProps.onFocus).toHaveBeenCalled();
});

it("executes onBlur prop if provided", () => {
defaultProps.onBlur = jest.fn();
const wrapper = shallow(<TextInput2 {...defaultProps} />);
wrapper.find("input").simulate("blur");
getInput(wrapper).simulate("blur");
expect(defaultProps.onBlur).toHaveBeenCalled();
});
});
Expand Down

0 comments on commit 2733131

Please sign in to comment.