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

WIP - Migrate Forms to Typescript #1810

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
import { shape, string, node, number, object, objectOf, oneOfType } from 'prop-types';
import classNames from 'classnames';
import { ErrorMessage } from 'formik';
import { ErrorMessage, FieldProps } from 'formik';
import { CHECKBOX, CHECKBOX_ERROR } from 'common/constants/testIDs';
import Alert from 'components/Alert/Alert';
import Label from 'components/Form/Label/Label';
import styles from './Checkbox.module.css';

Checkbox.propTypes = {
field: shape({
name: string.isRequired,
}).isRequired,
form: shape({
// TODO: Resolve why multiselects can end up with touched: { key: array }
// see ThemedReactSelect as well
// touched: objectOf(bool).isRequired,
touched: object.isRequired,
errors: objectOf(string),
}).isRequired,
id: oneOfType([string, number]),
label: oneOfType([node, string]).isRequired,
};

Checkbox.defaultProps = {
id: '',
};
export interface CheckboxPropsType extends FieldProps {
/**
* Applies a label that to the form input.
*/
label: React.ReactNode | string;
/**
* Sets the name and value for the input element.
*/
/**
* Passes an idea to the root input element.
*/
id?: string;
}

function Checkbox({ field: { name, value, ...field }, form: { errors }, id, label }) {
const hasErrors = Boolean(errors[name]);
function Checkbox({
field: { name, value, ...field },
form: { errors },
id,
label,
}: CheckboxPropsType) {
const hasErrors = Boolean(errors?.[name]);

return (
<div className={styles.field} data-testid={CHECKBOX}>
Expand Down
File renamed without changes.
118 changes: 62 additions & 56 deletions components/Form/Input/Input.js → components/Form/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,79 @@
import { shape, string, number, object, objectOf, oneOfType, bool, oneOf } from 'prop-types';
import classNames from 'classnames';
import { ErrorMessage } from 'formik';
import { ErrorMessage, FieldProps } from 'formik';
import { INPUT, INPUT_ERROR, INPUT_FEEDBACK_GROUPING } from 'common/constants/testIDs';
import Alert from 'components/Alert/Alert';
import Label from 'components/Form/Label/Label';
import styles from './Input.module.css';

Input.propTypes = {
className: string,
field: shape({
name: string.isRequired,
}).isRequired,
form: shape({
// TODO: Resolve why multiselects can end up with touched: { key: array }
// see ThemedReactSelect as well
// touched: objectOf(bool).isRequired,
touched: object.isRequired,
errors: objectOf(string),
}).isRequired,
isLabelHidden: bool,
id: oneOfType([string, number]),
label: string.isRequired,
hasValidationStyling: bool,
type: oneOf([
'button',
'color',
'date',
'datetime-local',
'email',
'file',
'hidden',
'image',
'month',
'number',
'password',
'radio',
'range',
'reset',
'search',
'submit',
'tel',
'text',
'time',
'url',
'week',
]),
};
export type InputType =
| 'button'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week';

Input.defaultProps = {
className: '',
hasValidationStyling: true,
isLabelHidden: false,
id: '',
type: 'text',
};
export type InputPropsType = {
/**
* Applies a label that to the form input.
*/
label: string;
/**
* Sets the name and value for the input element.
*/
/**
* Passes the input type to the base input element.
* @default 'text'
*/
type?: InputType;
/**
* Applies classnames to the base `input` element for styling.
*/
className?: string;
/**
* Sets if the label is hidden or not.
* @default false
*/
isLabelHidden?: boolean;
/**
* Passes an idea to the root input element.
*/
id?: string;
/**
* Allows validation styling if validation is being used.
* @default true
*/
hasValidationStyling?: boolean;
} & FieldProps;

function Input({
className,
field: { name, value, ...field },
form: { touched, errors },
hasValidationStyling,
hasValidationStyling = true,
id,
isLabelHidden,
isLabelHidden = false,
label,
type,
type = 'text',
...props // input simply has too many possible attributes... we'd be redocumenting the web
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes_common_to_all_input_types
}) {
const hasErrors = Boolean(errors[name]);
}: // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes_common_to_all_input_types
InputPropsType) {
const hasErrors = Boolean(errors?.[name]);
const isLabelAfterInput = type === 'radio';
const isLabelBeforeInput = !isLabelAfterInput;

Expand All @@ -94,7 +100,7 @@ function Input({
/>

<ErrorMessage name={name}>
{message => {
{(message: string) => {
return hasErrors ? (
<Alert className={styles.errorMessage} data-testid={INPUT_ERROR} type="error">
{message}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,17 @@
import { Meta, StoryObj } from '@storybook/react';
import { Formik, Field } from 'formik';
import Form from '../../Form';
import Input from '../Input';

const InputTemplate = args => {
const {
field: { name },
label,
} = args;
type InputStoryType = StoryObj<typeof Input>;

return (
<Formik
initialValues={{
input1Value: '',
}}
onSubmit={values => alert(values)}
>
<Form>
<div>
<Field name={name} label={label} component={Input} />
</div>
</Form>
</Formik>
);
};

export const Default = InputTemplate.bind({});

Default.args = {
field: { name: 'serviceBranchInput' },
label: 'Which branch did you serve with?',
};

export default {
component: Input,
const meta: Meta<typeof Input> = {
title: 'Form/Input',
component: Input,
args: {
field: { name: 'serviceBranchInput' },
label: 'Which branch did you serve with?',
},
argTypes: {
form: {
table: { disable: true },
Expand All @@ -54,3 +32,22 @@ export default {
),
],
};

export default meta;

export const Default: InputStoryType = {
render: args => (
<Formik
initialValues={{
input1Value: '',
}}
onSubmit={values => alert(values)}
>
<Form>
<div>
<Field name={args.field.name} label={args.label} component={Input} />
</div>
</Form>
</Formik>
),
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { cleanup, fireEvent, render } from '@testing-library/react';
import { INPUT, INPUT_ERROR, INPUT_FEEDBACK_GROUPING, LABEL } from 'common/constants/testIDs';
import { validationErrorMessages } from 'common/constants/messages';
import createSnapshotTest from 'test-utils/createSnapshotTest';
import { InputType } from '../Input';

import Form from '../../Form';
import Input from '../Input';
Expand All @@ -15,20 +16,21 @@ describe('Input', () => {
form: { touched: { someInputName: false }, errors: { someInputName: '' } },
onBlur: vi.fn(),
onChange: vi.fn(),
onSubmit: vi.fn(),
label: 'Some Input:',
};

it('should render with required props', () => {
createSnapshotTest(
<Formik>
<Formik initialValues={{}} onSubmit={requiredProps.onSubmit}>
<Input {...requiredProps} />
</Formik>,
);
});

it('should render with label, even if hidden', () => {
const component = render(
<Formik>
<Formik initialValues={{}} onSubmit={requiredProps.onSubmit}>
<Input {...requiredProps} isLabelHidden />
</Formik>,
);
Expand All @@ -41,29 +43,29 @@ describe('Input', () => {
const validate = () => ({ test: 'Required' });

const component = render(
<Formik validate={validate}>
<Formik validate={validate} initialValues={{}} onSubmit={requiredProps.onSubmit}>
<Form>
<Field id="test" name="test" label={label} component={Input} />,
</Form>
</Formik>,
);

fireEvent.blur(component.queryByLabelText(label));
fireEvent.blur(component.queryByLabelText(label)!);

const Alert = await component.findByTestId(INPUT_ERROR);
expect(Alert.textContent).toBe(validationErrorMessages.required);
});

it('should render the label after the input for radio inputs', () => {
const component = render(
<Formik>
<Formik initialValues={{}} onSubmit={requiredProps.onSubmit}>
<Input {...requiredProps} type="radio" />
</Formik>,
);

const Radio = component.queryByTestId(INPUT);
const Radio = component.queryByTestId(INPUT)!;

const InputFeedbackGrouping = component.queryByTestId(INPUT_FEEDBACK_GROUPING);
const InputFeedbackGrouping = component.queryByTestId(INPUT_FEEDBACK_GROUPING)!;
const Label = component.queryByTestId(LABEL);

// Selectors are rendered
Expand All @@ -79,7 +81,7 @@ describe('Input', () => {
});

it('should render the label before the input for all other input types', () => {
const otherInputTypes = [
const otherInputTypes: InputType[] = [
'button',
'color',
'date',
Expand Down Expand Up @@ -108,14 +110,14 @@ describe('Input', () => {

otherInputTypes.forEach(inputType => {
const { container, queryByTestId, unmount } = render(
<Formik>
<Formik initialValues={{}} onSubmit={requiredProps.onSubmit}>
<Input {...requiredProps} type={inputType} />
</Formik>,
);

const SomeInput = queryByTestId(INPUT);
const SomeInput = queryByTestId(INPUT)!;

const InputFeedbackGrouping = queryByTestId(INPUT_FEEDBACK_GROUPING);
const InputFeedbackGrouping = queryByTestId(INPUT_FEEDBACK_GROUPING)!;
const Label = queryByTestId(LABEL);

// Selectors are rendered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ exports[`Input > should render with required props 1`] = `
name="someInputName"
onBlur={[MockFunction spy]}
onChange={[MockFunction spy]}
onSubmit={[MockFunction spy]}
type="text"
value=""
/>
Expand Down
Loading
Loading