Skip to content

Commit

Permalink
Adds optional showErrorOnBlurOnly prop to TextInput2
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
antoniodangond committed Sep 17, 2021
1 parent 9f87706 commit 2d68ce6
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 4 deletions.
7 changes: 7 additions & 0 deletions docs/components/TextInput2View.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 15 additions & 3 deletions src/TextInput2/TextInput2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>;
size?: Values<typeof FormElementSize>;
Expand Down Expand Up @@ -61,6 +62,7 @@ const TextInput2: React.FC<Props> = ({
requirement,
initialIsInError,
errorValidation,
showErrorOnBlurOnly,
obscurable,
value,
onChange,
Expand All @@ -74,6 +76,8 @@ const TextInput2: React.FC<Props> = ({

// 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 === "") {
Expand All @@ -83,7 +87,7 @@ const TextInput2: React.FC<Props> = ({

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

Expand All @@ -94,7 +98,11 @@ const TextInput2: React.FC<Props> = ({

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;
}

Expand Down Expand Up @@ -134,7 +142,11 @@ const TextInput2: React.FC<Props> = ({
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)}
/>
Expand Down
101 changes: 100 additions & 1 deletion src/TextInput2/TextInput2_test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
Expand Down Expand Up @@ -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(<TextInput2 {...defaultProps} />);
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(<TextInput2 {...defaultProps} />);
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(<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 in focus even if showErrorOnBlurOnly is true", () => {
defaultProps.showErrorOnBlurOnly = true;
wrapper = mount(<TextInput2 {...defaultProps} />);
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", () => {
Expand Down

0 comments on commit 2d68ce6

Please sign in to comment.