Skip to content

Commit

Permalink
Merge pull request #746 from Clever/DISC-2758-enhance-TextInput2
Browse files Browse the repository at this point in the history
Disc 2758 enhance text input2
  • Loading branch information
antoniodangond authored Oct 1, 2021
2 parents 099b799 + 8467631 commit 148263e
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 36 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clever-components",
"version": "2.165.7",
"version": "2.166.0",
"description": "A library of helpful React components and less styles",
"repository": {
"type": "git",
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 148263e

Please sign in to comment.