) => {
+ const { value } = currentTarget;
+ if (this.state.error) {
+ this.setState(
+ {
+ value,
+ },
+ this.checkValidity,
+ );
+ } else {
+ this.setState({
+ value,
+ });
+ }
+ };
+
+ onValidityStateUpdateHandler = (error: ValidityState | TextInputValidationError) => {
+ if ((error as ValidityState).valid !== undefined) {
+ this.setErrorFromValidityState(error as ValidityState);
+ } else {
+ this.setState({
+ error: error as TextInputValidationError,
+ });
+ }
+ };
+
+ setErrorFromValidityState(validityState: ValidityState) {
+ const { badInput, customError, patternMismatch, tooLong, tooShort, typeMismatch, valid, valueMissing } =
+ validityState;
+
+ const { isRequired, minLength, maxLength, type, validation } = this.props;
+
+ const { value } = this.state;
+
+ let error: TextInputValidationError | null | undefined;
+
+ if (valid) {
+ error = null;
+ } else if (badInput) {
+ error = messages.badInput();
+ } else if (patternMismatch) {
+ error = messages.patternMismatch();
+ } else if (tooShort && typeof minLength !== 'undefined') {
+ error = messages.tooShort(minLength);
+ } else if (tooLong && typeof maxLength !== 'undefined') {
+ error = messages.tooLong(maxLength);
+ } else if (typeMismatch && type === 'email') {
+ error = messages.typeMismatchEmail();
+ } else if (typeMismatch && type === 'url') {
+ error = messages.typeMismatchUrl();
+ } else if (valueMissing) {
+ error = messages.valueMissing();
+ } else if (customError && (isRequired || value.trim().length) && validation) {
+ error = validation(value);
+ }
+
+ this.setState({
+ error,
+ });
+ }
+
+ input: HTMLInputElement | null | undefined;
+
+ // Updates component value and validity state
+ checkValidity = () => {
+ const { isRequired, validation } = this.props;
+ const { input } = this;
+ if (!input) {
+ return;
+ }
+
+ if (validation && (isRequired || input.value.trim().length)) {
+ const error = validation(input.value);
+ this.setState({
+ error,
+ value: input.value,
+ });
+
+ if (error) {
+ input.setCustomValidity(error.code);
+ } else {
+ input.setCustomValidity('');
+ }
+ } else {
+ this.setErrorFromValidityState(input.validity);
+ }
+ };
+
+ render() {
+ const {
+ autoFocus,
+ className = '',
+ isDisabled,
+ isRequired,
+ label,
+ maxLength,
+ minLength,
+ name,
+ onFocus,
+ pattern,
+ placeholder,
+ type,
+ isReadOnly,
+ isLoading,
+ labelTooltip,
+ hideLabel,
+ } = this.props;
+
+ const { error, value } = this.state;
+
+ return (
+
+
+ {
+ this.input = input;
+ }}
+ type={type}
+ value={value}
+ readOnly={isReadOnly}
+ isLoading={isLoading}
+ labelTooltip={labelTooltip}
+ hideLabel={hideLabel}
+ />
+
+
+ );
+ }
+}
+
+export default TextInput;
diff --git a/src/components/form-elements/text-input/__tests__/TextInput.test.js b/src/components/form-elements/text-input/__tests__/TextInput.test.tsx
similarity index 86%
rename from src/components/form-elements/text-input/__tests__/TextInput.test.js
rename to src/components/form-elements/text-input/__tests__/TextInput.test.tsx
index 7c25aca20c..9e01149fdd 100644
--- a/src/components/form-elements/text-input/__tests__/TextInput.test.js
+++ b/src/components/form-elements/text-input/__tests__/TextInput.test.tsx
@@ -3,9 +3,12 @@ import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
import TextInput from '..';
+// @ts-ignore flow import
import { FormContext } from '../../form/FormContext';
const sandbox = sinon.sandbox.create();
+const getTextInputInstance = (wrapper: { instance: () => React.Component }) =>
+ wrapper.instance() as InstanceType;
describe('components/form-elements/text-input/TextInput', () => {
afterEach(() => {
@@ -15,7 +18,7 @@ describe('components/form-elements/text-input/TextInput', () => {
test('should correctly render default component', () => {
const wrapper = shallow();
- expect(wrapper.find('TextInput').length).toEqual(1);
+ expect(wrapper.find('TextInput')).toHaveLength(1);
});
test('should update state if value prop changes', () => {
@@ -49,7 +52,7 @@ describe('components/form-elements/text-input/TextInput', () => {
wrapper.setProps({ value: 'a' });
input.simulate('blur');
- const inputEl = input.getDOMNode();
+ const inputEl = input.getDOMNode() as HTMLInputElement;
inputEl.value = 'a';
input.simulate('change', {
currentTarget: inputEl,
@@ -74,8 +77,8 @@ describe('components/form-elements/text-input/TextInput', () => {
test('should mark url fields invalid when invalid', () => {
const wrapper = mount();
- const instance = wrapper.instance();
- instance.input = { validity: { typeMismatch: true } };
+ const instance = getTextInputInstance(wrapper);
+ instance.input = { validity: { typeMismatch: true } } as HTMLInputElement;
act(() => {
instance.checkValidity();
});
@@ -86,8 +89,8 @@ describe('components/form-elements/text-input/TextInput', () => {
test('should mark url fields valid when valid', () => {
const wrapper = mount();
- const instance = wrapper.instance();
- instance.input = { validity: { valid: true } };
+ const instance = getTextInputInstance(wrapper);
+ instance.input = { validity: { valid: true } } as HTMLInputElement;
act(() => {
instance.checkValidity();
});
@@ -98,13 +101,15 @@ describe('components/form-elements/text-input/TextInput', () => {
});
test('should set an input as valid when the validityFn returns an void', () => {
- function validityFn() {}
+ function validityFn() {
+ return undefined;
+ }
const wrapper = mount();
const input = wrapper.find('input');
input.simulate('blur');
- expect(input.getDOMNode().validity.valid).toBeTruthy();
+ expect((input.getDOMNode() as HTMLInputElement).validity.valid).toBeTruthy();
});
test('should set an input as invalid when the validityFn returns an error string and input is not empty', () => {
@@ -119,7 +124,7 @@ describe('components/form-elements/text-input/TextInput', () => {
,
);
const input = wrapper.find('input');
- const setCustomValiditySpy = jest.spyOn(input.getDOMNode(), 'setCustomValidity');
+ const setCustomValiditySpy = jest.spyOn(input.getDOMNode() as HTMLInputElement, 'setCustomValidity');
input.simulate('blur');
expect(setCustomValiditySpy).toHaveBeenCalledWith('errCode');
@@ -133,18 +138,18 @@ describe('components/form-elements/text-input/TextInput', () => {
message: 'errMessage',
});
- stub.onCall(1).returns();
+ stub.onCall(1).returns(undefined);
const wrapper = mount();
let input = wrapper.find('input');
- let setCustomValiditySpy = jest.spyOn(input.getDOMNode(), 'setCustomValidity');
+ let setCustomValiditySpy = jest.spyOn(input.getDOMNode() as HTMLInputElement, 'setCustomValidity');
input.simulate('blur');
expect(setCustomValiditySpy).toHaveBeenCalledWith('errCode');
// Get the re-rendered input again
input = wrapper.find('input');
- setCustomValiditySpy = jest.spyOn(input.getDOMNode(), 'setCustomValidity');
+ setCustomValiditySpy = jest.spyOn(input.getDOMNode() as HTMLInputElement, 'setCustomValidity');
input.simulate('blur');
expect(setCustomValiditySpy).toHaveBeenCalledWith('');
@@ -162,7 +167,7 @@ describe('components/form-elements/text-input/TextInput', () => {
const input = wrapper.find('input');
input.simulate('blur');
- expect(input.getDOMNode().validity.valid).toBeTruthy();
+ expect((input.getDOMNode() as HTMLInputElement).validity.valid).toBeTruthy();
});
test('should set input invalid when the validityFn returns an error string, input is empty and is required', () => {
@@ -177,7 +182,7 @@ describe('components/form-elements/text-input/TextInput', () => {
,
);
const input = wrapper.find('input');
- const setCustomValiditySpy = jest.spyOn(input.getDOMNode(), 'setCustomValidity');
+ const setCustomValiditySpy = jest.spyOn(input.getDOMNode() as HTMLInputElement, 'setCustomValidity');
input.simulate('blur');
expect(setCustomValiditySpy).toHaveBeenCalledWith('errCode');
@@ -193,7 +198,7 @@ describe('components/form-elements/text-input/TextInput', () => {
wrapper.setProps({ value: 'abba' });
input.simulate('blur');
- const inputEl = input.getDOMNode();
+ const inputEl = input.getDOMNode() as HTMLInputElement;
inputEl.value = 'a';
input.simulate('change', {
currentTarget: inputEl,
@@ -210,7 +215,7 @@ describe('components/form-elements/text-input/TextInput', () => {
expect(wrapper.find('.text-input-container').hasClass('show-error')).toBeTruthy();
- const inputEl = input.getDOMNode();
+ const inputEl = input.getDOMNode() as HTMLInputElement;
inputEl.value = 'a';
input.simulate('change', {
currentTarget: inputEl,
@@ -241,7 +246,7 @@ describe('components/form-elements/text-input/TextInput', () => {
validityStateHandlerSpy.callArgWith(1, error);
});
- expect(component.find('TextInput').first().instance().state.error).toEqual(error);
+ expect(getTextInputInstance(component.find('TextInput').first()).state.error).toEqual(error);
});
test('should set validity state when set validity state handler is called with ValidityState object', () => {
@@ -264,7 +269,7 @@ describe('components/form-elements/text-input/TextInput', () => {
act(() => {
validityStateHandlerSpy.callArgWith(1, error);
});
- expect(component.find('TextInput').first().instance().state.error.code).toEqual('badInput');
+ expect(getTextInputInstance(component.find('TextInput').first()).state.error?.code).toEqual('badInput');
});
/**
@@ -291,7 +296,7 @@ describe('components/form-elements/text-input/TextInput', () => {
act(() => {
validityStateHandlerSpy.callArgWith(1, error);
});
- expect(component.find('TextInput').first().instance().state.error.code).toEqual('patternMismatch');
+ expect(getTextInputInstance(component.find('TextInput').first()).state.error?.code).toEqual('patternMismatch');
});
test('should correctly validate tooLong', () => {
@@ -314,7 +319,7 @@ describe('components/form-elements/text-input/TextInput', () => {
act(() => {
validityStateHandlerSpy.callArgWith(1, error);
});
- expect(component.find('TextInput').first().instance().state.error.code).toEqual('tooLong');
+ expect(getTextInputInstance(component.find('TextInput').first()).state.error?.code).toEqual('tooLong');
});
test('should correctly validate tooShort', () => {
@@ -338,6 +343,6 @@ describe('components/form-elements/text-input/TextInput', () => {
act(() => {
validityStateHandlerSpy.callArgWith(1, error);
});
- expect(component.find('TextInput').first().instance().state.error.code).toEqual('tooShort');
+ expect(getTextInputInstance(component.find('TextInput').first()).state.error?.code).toEqual('tooShort');
});
});
diff --git a/src/components/form-elements/text-input/index.js b/src/components/form-elements/text-input/index.js.flow
similarity index 100%
rename from src/components/form-elements/text-input/index.js
rename to src/components/form-elements/text-input/index.js.flow
diff --git a/src/components/form-elements/text-input/index.ts b/src/components/form-elements/text-input/index.ts
new file mode 100644
index 0000000000..3bb5243696
--- /dev/null
+++ b/src/components/form-elements/text-input/index.ts
@@ -0,0 +1,2 @@
+export { default } from './TextInput';
+export type { TextInputProps, TextInputValidationError } from './TextInput';