Skip to content

Commit

Permalink
Cass sb 8 fix connectformtypes (#2963)
Browse files Browse the repository at this point in the history
* build errors in forms

* fixed build errors

* resolve build error

* keep getError message internal

* more test tweaks
  • Loading branch information
dreamwasp authored Nov 4, 2024
1 parent 68140fc commit dac579d
Showing 14 changed files with 87 additions and 73 deletions.
2 changes: 1 addition & 1 deletion packages/gamut/package.json
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
"polished": "^4.1.2",
"react-aria-tabpanel": "^4.4.0",
"react-focus-on": "^3.5.1",
"react-hook-form": "^7.21.2",
"react-hook-form": "^7.53.1",
"react-player": "^2.16.0",
"react-select": "^5.2.2",
"react-truncate-markup": "^5.1.2",
6 changes: 4 additions & 2 deletions packages/gamut/src/ConnectedForm/ConnectedFormGroup.tsx
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ import { Anchor } from '../Anchor';
import { HiddenText } from '../HiddenText';
import { Markdown } from '../Markdown';
import { ConnectedField, FieldProps, SubmitContextProps } from './types';
import { useField } from './utils';
import { getErrorMessage, useField } from './utils';

const ErrorAnchor = styled(Anchor)(
css({
@@ -88,6 +88,8 @@ export function ConnectedFormGroup<T extends ConnectedField>({
</FormGroupLabel>
);

const textError = customError || getErrorMessage(error);

return (
<FormGroup spacing={hideLabel ? 'tight' : spacing}>
{hideLabel ? <HiddenText>{renderedLabel}</HiddenText> : renderedLabel}
@@ -112,7 +114,7 @@ export function ConnectedFormGroup<T extends ConnectedField>({
}}
skipDefaultOverrides={{ a: true }}
inline
text={error || customError}
text={textError}
spacing="none"
/>
</FormError>
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ export const ConnectedInput: React.FC<ConnectedInputProps> = ({
return (
<Input
disabled={isDisabled}
error={error}
error={Boolean(error)}
aria-required={isRequired}
{...ref}
{...rest}
Original file line number Diff line number Diff line change
@@ -14,5 +14,7 @@ export const ConnectedRadio: React.FC<ConnectedRadioProps> = ({
disabled,
});

return <Radio disabled={isDisabled} error={error} {...ref} {...rest} />;
return (
<Radio disabled={isDisabled} error={Boolean(error)} {...ref} {...rest} />
);
};
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ export const ConnectedSelect: React.FC<ConnectedSelectProps> = ({
return (
<Select
disabled={isDisabled}
error={error}
error={Boolean(error)}
aria-required={isRequired}
{...ref}
{...rest}
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ export const ConnectedTextArea: React.FC<ConnectedTextAreaProps> = ({
return (
<TextArea
disabled={isDisabled}
error={error}
error={Boolean(error)}
aria-required={isRequired}
{...ref}
{...rest}
12 changes: 6 additions & 6 deletions packages/gamut/src/ConnectedForm/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ComponentType } from 'react';
import * as React from 'react';
import { FormState } from 'react-hook-form';
import { FieldValues, FormState } from 'react-hook-form';

import { Box, FlexBox } from '../Box';
import { ButtonProps, FillButton } from '../Button';
@@ -9,17 +9,17 @@ import { Spinner } from '../Loading/Spinner';
import { useSubmitState } from './utils';

export interface SubmitContextProps {
loading?: FormStateCallback | boolean;
disabled?: FormStateCallback | boolean;
loading?: FormStateCallback<FieldValues> | boolean;
disabled?: FormStateCallback<FieldValues> | boolean;
}

export type FormStateCallback<Values = {}> = (
export type FormStateCallback<Values extends FieldValues = {}> = (
formState: FormState<Values>
) => boolean;

export interface SubmitContextProps {
loading?: FormStateCallback | boolean;
disabled?: FormStateCallback | boolean;
loading?: FormStateCallback<FieldValues> | boolean;
disabled?: FormStateCallback<FieldValues> | boolean;
}
export type SubmitButtonProps = Omit<ButtonProps, 'as' | 'disabled'> &
SubmitContextProps & {
47 changes: 15 additions & 32 deletions packages/gamut/src/ConnectedForm/__tests__/ConnectedForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { setupRtl } from '@codecademy/gamut-tests';
import { fireEvent, queries } from '@testing-library/dom';
import { act, RenderResult, waitFor } from '@testing-library/react';
import * as rhf from 'react-hook-form';

import { createPromise } from '../../utils';
import { ConnectedForm } from '..';
@@ -87,9 +86,8 @@ describe('ConnectedForm', () => {

const { view } = renderView({ onSubmit });

const { checkboxField, selectField, textField, radioOption } = getBaseCases(
view
);
const { checkboxField, selectField, textField, radioOption } =
getBaseCases(view);

doBaseFormActions(selectField, textField, checkboxField, radioOption);

@@ -125,21 +123,23 @@ describe('ConnectedForm', () => {
it('calls clearError onError so error text is re-read by screen reader', async () => {
const mockHandleSubmit = jest.fn();

// the return type of useForm is optionally undefined, so we have to do some seemingly unnecessary nullish coalescing here for type safety
/* We need a different approach to mock useForm and clearErrors, though since this is blocking the Storybook upgrade we'll use a stopgap for now.
const mockedUseForm =
jest.spyOn(rhf, 'useForm').getMockImplementation() ?? mockHandleSubmit;
jest.spyOn(rhf, 'useForm').mockImplementationOnce(() => {
const returnMock = mockedUseForm();
return { ...returnMock, clearErrors: mockHandleSubmit };
});
*/

const api = createPromise<{}>();
const onSubmit = async (values: {}) => api.resolve(values);
const { view } = renderView({
validationRules,
defaultValues,
onSubmit,
onError: mockHandleSubmit,
});

await act(async () => {
@@ -158,9 +158,8 @@ describe('ConnectedForm', () => {
onSubmit,
});

const { checkboxField, selectField, textField, radioGroup } = getBaseCases(
view
);
const { checkboxField, selectField, textField, radioGroup } =
getBaseCases(view);

expect(checkboxField).toHaveAttribute('aria-required');
expect(selectField).toHaveAttribute('aria-required');
@@ -218,12 +217,8 @@ describe('ConnectedForm', () => {
onSubmit,
});

const {
checkboxField,
selectField,
textField,
radioOption,
} = getBaseCases(view);
const { checkboxField, selectField, textField, radioOption } =
getBaseCases(view);

doBaseFormActions(selectField, textField, checkboxField, radioOption);

@@ -246,12 +241,8 @@ describe('ConnectedForm', () => {
onSubmit,
});

const {
checkboxField,
selectField,
textField,
radioOption,
} = getBaseCases(view);
const { checkboxField, selectField, textField, radioOption } =
getBaseCases(view);

await act(async () => {
fireEvent.submit(view.getByRole('button'));
@@ -274,12 +265,8 @@ describe('ConnectedForm', () => {
disableFieldsOnSubmit: true,
});

const {
checkboxField,
selectField,
textField,
radioOption,
} = getBaseCases(view);
const { checkboxField, selectField, textField, radioOption } =
getBaseCases(view);
await act(async () => {
fireEvent.submit(view.getByRole('button'));
});
@@ -305,12 +292,8 @@ describe('ConnectedForm', () => {
resetOnSubmit: true,
});

const {
checkboxField,
selectField,
textField,
radioOption,
} = getBaseCases(view);
const { checkboxField, selectField, textField, radioOption } =
getBaseCases(view);

doBaseFormActions(selectField, textField, checkboxField, radioOption);

11 changes: 10 additions & 1 deletion packages/gamut/src/ConnectedForm/index.ts
Original file line number Diff line number Diff line change
@@ -2,4 +2,13 @@ export * from './ConnectedFormGroup';
export * from './ConnectedForm';
export * from './ConnectedInputs';
export * from './SubmitButton';
export * from './utils';
export {
submitSuccessStatus,
USE_DEBOUNCED_FIELD_DIRTY_KEY,
useConnectedForm,
useDebouncedField,
useField,
useFormState,
useGetInitialFormValue,
useSubmitState,
} from './utils';
4 changes: 2 additions & 2 deletions packages/gamut/src/ConnectedForm/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FormState } from 'react-hook-form';
import { FieldValues, FormState } from 'react-hook-form';

import {
ConnectedCheckbox,
@@ -21,7 +21,7 @@ export interface FieldProps<FieldComponent extends ConnectedField>
component: FieldComponent;
}

export type FormStateCallback<Values = {}> = (
export type FormStateCallback<Values extends FieldValues = {}> = (
formState: FormState<Values>
) => boolean;

33 changes: 27 additions & 6 deletions packages/gamut/src/ConnectedForm/utils.tsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,9 @@ import {
useState,
} from 'react';
import {
FieldError,
FieldErrorsImpl,
Merge,
RegisterOptions,
useFieldArray,
useFormContext,
@@ -65,7 +68,8 @@ export const useConnectedForm = <
}: UseConnectedFormProps<Values, Partial<ValidationRules>>) => {
return useMemo(
() => ({
ConnectedFormGroup: ConnectedFormGroup as ConnectedGroupStrictProps<Values>,
ConnectedFormGroup:
ConnectedFormGroup as ConnectedGroupStrictProps<Values>,
ConnectedForm: ConnectedForm as ConnectedFormStrictProps<
Values,
ValidationRules
@@ -246,11 +250,12 @@ export const useGetInitialFormValue = ({
// For some reason including `updated` trips a lint error about unnecessary deps
// but it doesn't throw anything in the editor :shrug:
// eslint-disable-next-line react-hooks/exhaustive-deps
const initialValue: string | null = useMemo(() => getValues(name), [
name,
getValues,
updated,
]);
const initialValue: string | null = useMemo(
() => getValues(name),

// eslint-disable-next-line react-hooks/exhaustive-deps
[name, getValues, updated]
);

useEffect(() => {
if (!(isNull(initialValue) || isUndefined(initialValue))) {
@@ -391,3 +396,19 @@ export function useDebouncedField<T extends InputTypes>({
value: localValue as T extends 'checkbox' ? boolean : string,
};
}

export const getErrorMessage = (
error:
| string
| FieldError
| Merge<FieldError, FieldErrorsImpl<any>>
| undefined
) => {
if (error) {
if (typeof error === 'string') return error;
if ('message' in error && typeof error?.message === 'string')
return error.message;
return 'An error has occurred';
}
return undefined;
};
5 changes: 3 additions & 2 deletions packages/gamut/src/GridForm/GridForm.tsx
Original file line number Diff line number Diff line change
@@ -2,14 +2,15 @@ import { Fragment, useMemo } from 'react';
import * as React from 'react';
import {
DeepPartial,
DefaultValues,
Mode,
SubmitHandler,
UnpackNestedValue,
} from 'react-hook-form';

import { ButtonProps } from '../Button';
import { ConnectedForm, FormContextProps } from '../ConnectedForm';
import { FormRequiredText } from '../Form';
import { FormRequiredText } from '../Form/elements/FormRequiredText';
import { FormValues } from '../Form/types';
import { Column, LayoutGrid, LayoutGridProps } from '../Layout';
import { GridFormButtons, GridFormSubmitProps } from './GridFormButtons';
@@ -139,7 +140,7 @@ export function GridForm<Values extends FormValues<Values>>({
return (
<ConnectedForm<Values>
validation={validation}
defaultValues={defaultValues}
defaultValues={defaultValues as DefaultValues<Values>}
display="flex"
flexDirection="column"
isSoloField={hasComputedSoloField}
22 changes: 9 additions & 13 deletions packages/gamut/src/GridForm/GridFormSections/GridFormContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import { useField } from '../../ConnectedForm';
import { getErrorMessage, useField } from '../../ConnectedForm/utils';
import { GridFormInputGroup } from '../GridFormInputGroup';
import { GridFormField } from '../types';

@@ -9,27 +9,23 @@ export type GridFormContentProps = {
};

export const GridFormContent: React.FC<GridFormContentProps> = ({ field }) => {
const {
error,
isFirstError,
register,
setValue,
isDisabled,
isSoloField,
} = useField({
name: field.name,
disabled: field.disabled,
});
const { error, isFirstError, register, setValue, isDisabled, isSoloField } =
useField({
name: field.name,
disabled: field.disabled,
});

const requiredBoolean = !!(
field.type !== 'hidden' &&
field.type !== 'sweet-container' &&
field.validation?.required
);

const textError = getErrorMessage(error);

return (
<GridFormInputGroup
error={error}
error={textError}
isFirstError={isFirstError}
isDisabled={isDisabled}
field={field}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -12198,10 +12198,10 @@ react-helmet@6.1.0:
react-fast-compare "^3.1.1"
react-side-effect "^2.1.0"

react-hook-form@^7.21.2:
version "7.21.2"
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.21.2.tgz#3faf1292b66b97f6897ba46138aeaf5a1f6031c6"
integrity sha512-G2qcUsZSoVolLMZi83nRcZR3VquUWI8lgAfMUyL2teL7vAFPKKwPMfKv2+tXFZjBMzNXHb+HxARdUGcETo8IqQ==
react-hook-form@^7.53.1:
version "7.53.1"
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz#3f2cd1ed2b3af99416a4ac674da2d526625add67"
integrity sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==

react-is@18.1.0:
version "18.1.0"

0 comments on commit dac579d

Please sign in to comment.