From 2733131a6ee5bd928ce5dd008e2bb4a0675ecaa9 Mon Sep 17 00:00:00 2001 From: antoniodangond Date: Wed, 29 Sep 2021 18:06:32 -0700 Subject: [PATCH] Modifies TextInput2 error validation behavior 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. --- docs/components/TextInput2View.jsx | 17 ++--- src/TextInput2/TextInput2.tsx | 39 +++++------ src/TextInput2/TextInput2_test.tsx | 100 ++++++++++++++++++++++++++++- 3 files changed, 121 insertions(+), 35 deletions(-) diff --git a/docs/components/TextInput2View.jsx b/docs/components/TextInput2View.jsx index ba6cfa3b6..ed91b20eb 100644 --- a/docs/components/TextInput2View.jsx +++ b/docs/components/TextInput2View.jsx @@ -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, }; @@ -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} @@ -95,7 +95,7 @@ export default class TextInput2View extends React.PureComponent { showIcon, requirement, obscurable, - initialIsInError, + errorOnEmpty, size, } = this.state; @@ -162,10 +162,10 @@ export default class TextInput2View extends React.PureComponent { this.setState({ initialIsInError: e.target.checked })} + checked={errorOnEmpty} + onChange={(e) => this.setState({ errorOnEmpty: e.target.checked })} />{" "} - Initial error + Error on empty
Requirement: @@ -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, }, { diff --git a/src/TextInput2/TextInput2.tsx b/src/TextInput2/TextInput2.tsx index 22e0f92bb..8a1ee5d25 100644 --- a/src/TextInput2/TextInput2.tsx +++ b/src/TextInput2/TextInput2.tsx @@ -20,7 +20,7 @@ export interface Props { icon?: React.ReactNode; requirement?: Values; obscurable?: boolean; - initialIsInError?: boolean; + errorOnEmpty?: boolean; // returns an error message, null for no error errorValidation?: (value: string) => string | null; value: string; @@ -61,7 +61,7 @@ const TextInput2: React.FC = ({ helpText, icon, requirement, - initialIsInError, + errorOnEmpty, errorValidation, obscurable, value, @@ -72,38 +72,29 @@ const TextInput2: React.FC = ({ }) => { 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 (
@@ -170,7 +161,7 @@ const TextInput2: React.FC = ({ }; TextInput2.defaultProps = { - initialIsInError: false, + errorOnEmpty: false, errorValidation: () => null, size: FormElementSize.FULL_WIDTH, }; diff --git a/src/TextInput2/TextInput2_test.tsx b/src/TextInput2/TextInput2_test.tsx index 16c65fee0..2e2f64916 100644 --- a/src/TextInput2/TextInput2_test.tsx +++ b/src/TextInput2/TextInput2_test.tsx @@ -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(() => { @@ -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(); + 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(); + 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(); + 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(); + 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(); + 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(); - 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(); - wrapper.find("input").simulate("blur"); + getInput(wrapper).simulate("blur"); expect(defaultProps.onBlur).toHaveBeenCalled(); }); });