Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disc 2758 enhance text input2 #746

Merged
merged 2 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading about some best practices for error messaging, we can probably rewrite this entire thing. Something like this maybe:

useEffect(() => {
  // if input is empty and requirement error is active, then do nothing and return
  // if input is empty, then clear error and return


  // check error validation function
  // if no error validation, clear error and return
  // if errormessage exists and is not focused, then set error and return
});

// 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