From 2d68ce6fd8c7c5bf712efa890308256e68830b95 Mon Sep 17 00:00:00 2001 From: antoniodangond Date: Thu, 16 Sep 2021 19:37:26 -0700 Subject: [PATCH] Adds optional showErrorOnBlurOnly prop to TextInput2 When this prop is set to true, error state will only be shown after the input is "dirty" (value has been modified by user) and focus has been removed from the input element. Adds new local state "isInputDirty" to help manage this functionality. Error state can still be removed while the input is in focus, which is consistent with the current functionality. --- docs/components/TextInput2View.jsx | 7 ++ src/TextInput2/TextInput2.tsx | 18 ++++- src/TextInput2/TextInput2_test.tsx | 101 ++++++++++++++++++++++++++++- 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/docs/components/TextInput2View.jsx b/docs/components/TextInput2View.jsx index 8cc4ef952..342faf7d2 100644 --- a/docs/components/TextInput2View.jsx +++ b/docs/components/TextInput2View.jsx @@ -270,6 +270,13 @@ export default class TextInput2View extends React.PureComponent { description: "Any custom validation that you want on the input. Returns null for no error, otherwise returns a string as an error message to be displayed", }, + { + name: "showErrorOnBlurOnly", + type: "boolean", + description: + "Only show error state after user has modified initial input value and removed focus from the input", + optional: true, + }, { name: "value", type: "String", diff --git a/src/TextInput2/TextInput2.tsx b/src/TextInput2/TextInput2.tsx index fccaddf41..899c69fde 100644 --- a/src/TextInput2/TextInput2.tsx +++ b/src/TextInput2/TextInput2.tsx @@ -23,6 +23,7 @@ export interface Props { initialIsInError?: boolean; // returns an error message, null for no error errorValidation?: (value: string) => string | null; + showErrorOnBlurOnly?: boolean; value: string; onChange: React.ChangeEventHandler; size?: Values; @@ -61,6 +62,7 @@ const TextInput2: React.FC = ({ requirement, initialIsInError, errorValidation, + showErrorOnBlurOnly, obscurable, value, onChange, @@ -74,6 +76,8 @@ const TextInput2: React.FC = ({ // empty string is an error state with no message (e.g. required) const [errorMessage, setErrorMessage] = useState(initialIsInError ? "" : null); + // input is considered dirty once the value has been modified by the user + const [isInputDirty, setIsInputDirty] = useState(false); useEffect(() => { if (requirement === FormElementRequirement.REQUIRED && value === "") { @@ -83,7 +87,7 @@ const TextInput2: React.FC = ({ useEffect(() => { // don't show error if nothing has happened yet - if (!isFocused && errorMessage === null) { + if (!showErrorOnBlurOnly && !isFocused && errorMessage === null) { return; } @@ -94,7 +98,11 @@ const TextInput2: React.FC = ({ const newErrorMessage = errorValidation(value); if (newErrorMessage) { - setErrorMessage(newErrorMessage); + // if showErrorOnBlurOnly, only display error after user has + // modified initial input value and removed focus from input + if (!showErrorOnBlurOnly || (!isFocused && isInputDirty)) { + setErrorMessage(newErrorMessage); + } return; } @@ -134,7 +142,11 @@ const TextInput2: React.FC = ({ value={value} placeholder={placeholder} disabled={requirement === FormElementRequirement.DISABLED} - onChange={onChange} + onChange={(e) => { + // set isInputDirty to true after value is changed for the first time + if (!isInputDirty) setIsInputDirty(true); + onChange(e); + }} onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} /> diff --git a/src/TextInput2/TextInput2_test.tsx b/src/TextInput2/TextInput2_test.tsx index c40d741dc..c009c40a2 100644 --- a/src/TextInput2/TextInput2_test.tsx +++ b/src/TextInput2/TextInput2_test.tsx @@ -1,9 +1,26 @@ 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.ERROR_MESSAGE}`); +const changeInputValue = (input, value) => { + input.simulate("change", { target: { value } }); +}; + describe("TextInput2", () => { + let defaultProps; + beforeEach(() => { + defaultProps = { + name: "", + label: "", + value: "", + onChange: jest.fn(), + }; + }); + afterEach(() => { jest.resetAllMocks(); }); @@ -44,6 +61,88 @@ 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) => { + if (wrapper) { + wrapper.setProps({ value: e.target.value }); + wrapper.update(); + } + }, + }; + }); + + it("displays error while focused if provided errorValidation function returns error message", () => { + wrapper = mount(); + const input = getInput(wrapper); + let errorContainer = getErrorContainer(wrapper); + expect(errorContainer).not.toExist(); + + input.simulate("focus"); + changeInputValue(input, invalidValue); + errorContainer = getErrorContainer(wrapper); + expect(errorContainer).toExist(); + }); + + it("removes error while focused if provided errorValidation function returns null", () => { + wrapper = mount(); + const input = getInput(wrapper); + + input.simulate("focus"); + changeInputValue(input, invalidValue); + let errorContainer = getErrorContainer(wrapper); + // error is displayed while in focus + expect(errorContainer).toExist(); + + changeInputValue(input, validValue); + errorContainer = getErrorContainer(wrapper); + // error is removed while in focus + expect(errorContainer).not.toExist(); + }); + + it("only displays error on blur if showErrorOnBlurOnly is true", () => { + defaultProps.showErrorOnBlurOnly = true; + 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 in focus even if showErrorOnBlurOnly is true", () => { + defaultProps.showErrorOnBlurOnly = true; + wrapper = mount(); + const input = getInput(wrapper); + + input.simulate("focus"); + changeInputValue(input, invalidValue); + input.simulate("blur"); + let errorContainer = getErrorContainer(wrapper); + // error is displayed after focus is removed + expect(errorContainer).toExist(); + + input.simulate("focus"); + changeInputValue(input, validValue); + errorContainer = getErrorContainer(wrapper); + // error is removed after valid value is set while in focus + expect(errorContainer).not.toExist(); + }); + }); + // TODO: Test any relevant state changes/event handling/prop-driven rendering. /* it("propagates event", () => {