diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..25fa6215f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/components/Form/Checkbox/Checkbox.js b/components/Form/Checkbox/Checkbox.tsx similarity index 55% rename from components/Form/Checkbox/Checkbox.js rename to components/Form/Checkbox/Checkbox.tsx index 5c86e3bee..423a26fab 100644 --- a/components/Form/Checkbox/Checkbox.js +++ b/components/Form/Checkbox/Checkbox.tsx @@ -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 (
diff --git a/components/Form/Form.js b/components/Form/Form.tsx similarity index 100% rename from components/Form/Form.js rename to components/Form/Form.tsx diff --git a/components/Form/Input/Input.js b/components/Form/Input/Input.tsx similarity index 57% rename from components/Form/Input/Input.js rename to components/Form/Input/Input.tsx index aff67669d..2911efd4d 100644 --- a/components/Form/Input/Input.js +++ b/components/Form/Input/Input.tsx @@ -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; @@ -94,7 +100,7 @@ function Input({ /> - {message => { + {(message: string) => { return hasErrors ? ( {message} diff --git a/components/Form/Input/__stories__/Input.stories.js b/components/Form/Input/__stories__/Input.stories.tsx similarity index 63% rename from components/Form/Input/__stories__/Input.stories.js rename to components/Form/Input/__stories__/Input.stories.tsx index b961fce80..c56d15bba 100644 --- a/components/Form/Input/__stories__/Input.stories.js +++ b/components/Form/Input/__stories__/Input.stories.tsx @@ -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; - return ( - alert(values)} - > -
-
- -
-
-
- ); -}; - -export const Default = InputTemplate.bind({}); - -Default.args = { - field: { name: 'serviceBranchInput' }, - label: 'Which branch did you serve with?', -}; - -export default { - component: Input, +const meta: Meta = { title: 'Form/Input', + component: Input, + args: { + field: { name: 'serviceBranchInput' }, + label: 'Which branch did you serve with?', + }, argTypes: { form: { table: { disable: true }, @@ -54,3 +32,22 @@ export default { ), ], }; + +export default meta; + +export const Default: InputStoryType = { + render: args => ( + alert(values)} + > +
+
+ +
+
+
+ ), +}; diff --git a/components/Form/Input/__tests__/Input.test.js b/components/Form/Input/__tests__/Input.test.tsx similarity index 84% rename from components/Form/Input/__tests__/Input.test.js rename to components/Form/Input/__tests__/Input.test.tsx index afe5973e9..6a4018689 100644 --- a/components/Form/Input/__tests__/Input.test.js +++ b/components/Form/Input/__tests__/Input.test.tsx @@ -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'; @@ -15,12 +16,13 @@ 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( - + , ); @@ -28,7 +30,7 @@ describe('Input', () => { it('should render with label, even if hidden', () => { const component = render( - + , ); @@ -41,14 +43,14 @@ describe('Input', () => { const validate = () => ({ test: 'Required' }); const component = render( - +
,
, ); - fireEvent.blur(component.queryByLabelText(label)); + fireEvent.blur(component.queryByLabelText(label)!); const Alert = await component.findByTestId(INPUT_ERROR); expect(Alert.textContent).toBe(validationErrorMessages.required); @@ -56,14 +58,14 @@ describe('Input', () => { it('should render the label after the input for radio inputs', () => { const component = render( - + , ); - 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 @@ -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', @@ -108,14 +110,14 @@ describe('Input', () => { otherInputTypes.forEach(inputType => { const { container, queryByTestId, unmount } = render( - + , ); - 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 diff --git a/components/Form/Input/__tests__/__snapshots__/Input.test.js.snap b/components/Form/Input/__tests__/__snapshots__/Input.test.tsx.snap similarity index 94% rename from components/Form/Input/__tests__/__snapshots__/Input.test.js.snap rename to components/Form/Input/__tests__/__snapshots__/Input.test.tsx.snap index dc8f4262d..8d94de1ab 100644 --- a/components/Form/Input/__tests__/__snapshots__/Input.test.js.snap +++ b/components/Form/Input/__tests__/__snapshots__/Input.test.tsx.snap @@ -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="" /> diff --git a/components/Form/Label/Label.js b/components/Form/Label/Label.tsx similarity index 50% rename from components/Form/Label/Label.js rename to components/Form/Label/Label.tsx index aac1e66f1..f81f2323d 100644 --- a/components/Form/Label/Label.js +++ b/components/Form/Label/Label.tsx @@ -1,23 +1,38 @@ -import { node, string, bool } from 'prop-types'; import classNames from 'classnames'; import { LABEL } from 'common/constants/testIDs'; import ScreenReaderOnly from 'components/ScreenReaderOnly/ScreenReaderOnly'; -Label.propTypes = { - children: node.isRequired, - className: string, - 'data-testid': string, - for: string.isRequired, - isHidden: bool, // visually hides the label, but maintains accessibility +export type LabelPropsType = { + /** + * Content to be rendered as the label. + */ + children: React.ReactNode; + /** + * Applies a name for the input element. + */ + for: string; + /** + * Applies classnames to the base `label` element for styling. + */ + className?: string; + /** + * Applies a data-testid to the base `label` element for testing. + */ + 'data-testid'?: string; + /** + * Sets if the label is hidden or not. + * @default false + */ + isHidden?: boolean; }; -Label.defaultProps = { - className: undefined, - 'data-testid': LABEL, - isHidden: false, -}; - -function Label({ children, className, 'data-testid': testID, isHidden, ...props }) { +function Label({ + children, + className, + 'data-testid': testID = LABEL, + isHidden = false, + ...props +}: LabelPropsType) { const TheLabel = (
+ +
+ + +
+ + + + + +`; diff --git a/components/Form/Select/__tests__/__snapshots__/ThemedReactSelect.test.js.snap b/components/Form/Select/__tests__/__snapshots__/ThemedReactSelect.test.js.snap deleted file mode 100644 index 6ba6fc8b7..000000000 --- a/components/Form/Select/__tests__/__snapshots__/ThemedReactSelect.test.js.snap +++ /dev/null @@ -1,111 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`ThemedReactSelect > should render with required props 1`] = ` -
- -
-
-
- Select... -
-
-
- -
-
-
-
-
- - -
-
-
-`; diff --git a/components/Form/Select/__tests__/__snapshots__/ThemedReactSelect.test.tsx.snap b/components/Form/Select/__tests__/__snapshots__/ThemedReactSelect.test.tsx.snap new file mode 100644 index 000000000..526ce8022 --- /dev/null +++ b/components/Form/Select/__tests__/__snapshots__/ThemedReactSelect.test.tsx.snap @@ -0,0 +1,104 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ThemedReactSelect > should render with required props 1`] = ` +
+ + +
+
+
+ Select... +
+
+ +
+
+
+ + +
+
+
+`; diff --git a/components/Form/__tests__/Form.test.js b/components/Form/__tests__/Form.test.tsx similarity index 87% rename from components/Form/__tests__/Form.test.js rename to components/Form/__tests__/Form.test.tsx index 49818eb13..9353c79a7 100644 --- a/components/Form/__tests__/Form.test.js +++ b/components/Form/__tests__/Form.test.tsx @@ -6,7 +6,7 @@ import Form from '../Form'; describe('Form', () => { it('should render within the context of Formik', () => { createSnapshotTest( - + {}}> , diff --git a/components/Form/__tests__/__snapshots__/Form.test.js.snap b/components/Form/__tests__/__snapshots__/Form.test.tsx.snap similarity index 100% rename from components/Form/__tests__/__snapshots__/Form.test.js.snap rename to components/Form/__tests__/__snapshots__/Form.test.tsx.snap diff --git a/components/Forms/ResourceSearchForm/__tests__/__snapshots__/ResourceSearchForm.test.js.snap b/components/Forms/ResourceSearchForm/__tests__/__snapshots__/ResourceSearchForm.test.js.snap index 247415bad..83fd641b8 100644 --- a/components/Forms/ResourceSearchForm/__tests__/__snapshots__/ResourceSearchForm.test.js.snap +++ b/components/Forms/ResourceSearchForm/__tests__/__snapshots__/ResourceSearchForm.test.js.snap @@ -55,93 +55,86 @@ exports[`ResourceSearchForm > should render with required props 1`] = ` className="selectFeedbackGrouping" >
+
Start typing a category...
-
- -
-
+ tabIndex={0} + type="text" + value="" + />