Skip to content

Commit d5c4ef0

Browse files
committed
fix: scroll to first field depending on order
1 parent 987d549 commit d5c4ef0

File tree

3 files changed

+81
-31
lines changed

3 files changed

+81
-31
lines changed

src/components/form/Form.tsx

+69-31
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,42 @@ import {
66
type FormikErrors,
77
type FormikProps,
88
} from "formik"
9-
import { type ReactNode, type FC, useRef, useEffect } from "react"
9+
import {
10+
type ReactNode,
11+
type FC,
12+
useRef,
13+
useEffect,
14+
type RefObject,
15+
} from "react"
1016
import type { TypedUseMutation } from "@reduxjs/toolkit/query/react"
1117

18+
import { getKeyPaths } from "../../utils/general"
1219
import {
1320
submitForm,
1421
type SubmitFormOptions,
1522
type FormValues,
1623
} from "../../utils/form"
1724

18-
type NonFieldErrorsProps = Omit<FormHelperTextProps, "error" | "ref">
25+
const SCROLL_INTO_VIEW_OPTIONS: ScrollIntoViewOptions = {
26+
behavior: "smooth",
27+
block: "start",
28+
}
29+
30+
type NonFieldErrorsProps = Omit<FormHelperTextProps, "error" | "ref"> & {
31+
scrollIntoViewOptions?: ScrollIntoViewOptions
32+
}
1933

20-
const NonFieldErrors: FC<NonFieldErrorsProps> = props => {
34+
const NonFieldErrors: FC<NonFieldErrorsProps> = ({
35+
scrollIntoViewOptions = SCROLL_INTO_VIEW_OPTIONS,
36+
...formHelperTextProps
37+
}) => {
2138
const pRef = useRef<HTMLParagraphElement>(null)
2239

2340
useEffect(() => {
24-
if (pRef.current) {
25-
pRef.current.scrollIntoView({ behavior: "smooth", block: "start" })
26-
}
27-
}, [])
41+
if (pRef.current) pRef.current.scrollIntoView(scrollIntoViewOptions)
42+
}, [scrollIntoViewOptions])
2843

29-
return <FormHelperText ref={pRef} error {...props} />
44+
return <FormHelperText ref={pRef} error {...formHelperTextProps} />
3045
}
3146

3247
export type FormErrors<Values> = FormikErrors<
@@ -37,38 +52,61 @@ type _FormikProps<Values> = Omit<FormikProps<Values>, "errors"> & {
3752
errors: FormErrors<Values>
3853
}
3954

40-
type _FormikConfig<Values> = Omit<FormikConfig<Values>, "children"> & {
55+
type BaseFormProps<Values> = Omit<FormikConfig<Values>, "children"> & {
4156
children: ReactNode | ((props: _FormikProps<Values>) => ReactNode)
57+
scrollIntoViewOptions?: ScrollIntoViewOptions
4258
nonFieldErrorsProps?: Omit<NonFieldErrorsProps, "children">
59+
order?: Array<{ name: string; inputRef: RefObject<HTMLInputElement> }>
4360
}
4461

45-
const _ = <Values extends FormValues>({
62+
const BaseForm = <Values extends FormValues>({
4663
children,
64+
scrollIntoViewOptions = SCROLL_INTO_VIEW_OPTIONS,
4765
nonFieldErrorsProps,
66+
order,
4867
...otherFormikProps
49-
}: _FormikConfig<Values>) => (
68+
}: BaseFormProps<Values>) => (
5069
<Formik {...otherFormikProps}>
5170
{/* @ts-expect-error */}
52-
{(formik: _FormikProps<Values>) => (
53-
<>
54-
{typeof formik.errors.non_field_errors === "string" && (
55-
<NonFieldErrors {...nonFieldErrorsProps}>
56-
{formik.errors.non_field_errors}
57-
</NonFieldErrors>
58-
)}
59-
<FormikForm>
60-
{typeof children === "function" ? children(formik) : children}
61-
</FormikForm>
62-
</>
63-
)}
71+
{(formik: _FormikProps<Values>) => {
72+
let nonFieldErrors: undefined | JSX.Element = undefined
73+
if (Object.keys(formik.errors).length) {
74+
if (typeof formik.errors.non_field_errors === "string") {
75+
nonFieldErrors = (
76+
<NonFieldErrors {...nonFieldErrorsProps}>
77+
{formik.errors.non_field_errors}
78+
</NonFieldErrors>
79+
)
80+
} else if (order && order.length) {
81+
const errorNames = getKeyPaths(formik.errors)
82+
83+
const inputRef = order.find(({ name }) =>
84+
errorNames.includes(name),
85+
)?.inputRef
86+
87+
if (inputRef?.current) {
88+
inputRef.current.scrollIntoView(scrollIntoViewOptions)
89+
}
90+
}
91+
}
92+
93+
return (
94+
<>
95+
{nonFieldErrors}
96+
<FormikForm>
97+
{typeof children === "function" ? children(formik) : children}
98+
</FormikForm>
99+
</>
100+
)
101+
}}
64102
</Formik>
65103
)
66104

67105
type SubmitFormProps<
68106
Values extends FormValues,
69107
QueryArg extends FormValues,
70108
ResultType,
71-
> = Omit<_FormikConfig<Values>, "onSubmit"> & {
109+
> = Omit<BaseFormProps<Values>, "onSubmit"> & {
72110
useMutation: TypedUseMutation<ResultType, QueryArg, any>
73111
} & (Values extends QueryArg
74112
? { submitOptions?: SubmitFormOptions<Values, QueryArg, ResultType> }
@@ -81,16 +119,16 @@ const SubmitForm = <
81119
>({
82120
useMutation,
83121
submitOptions,
84-
...formikProps
122+
...baseFormProps
85123
}: SubmitFormProps<Values, QueryArg, ResultType>): JSX.Element => {
86124
const [trigger] = useMutation()
87125

88126
return (
89-
<_
90-
{...formikProps}
127+
<BaseForm
128+
{...baseFormProps}
91129
onSubmit={submitForm<Values, QueryArg, ResultType>(
92130
trigger,
93-
formikProps.initialValues,
131+
baseFormProps.initialValues,
94132
submitOptions as SubmitFormOptions<Values, QueryArg, ResultType>,
95133
)}
96134
/>
@@ -101,10 +139,10 @@ export type FormProps<
101139
Values extends FormValues,
102140
QueryArg extends FormValues,
103141
ResultType,
104-
> = _FormikConfig<Values> | SubmitFormProps<Values, QueryArg, ResultType>
142+
> = BaseFormProps<Values> | SubmitFormProps<Values, QueryArg, ResultType>
105143

106144
const Form: {
107-
<Values extends FormValues>(props: _FormikConfig<Values>): JSX.Element
145+
<Values extends FormValues>(props: BaseFormProps<Values>): JSX.Element
108146
<Values extends FormValues, QueryArg extends FormValues, ResultType>(
109147
props: SubmitFormProps<Values, QueryArg, ResultType>,
110148
): JSX.Element
@@ -115,7 +153,7 @@ const Form: {
115153
>(
116154
props: FormProps<Values, QueryArg, ResultType>,
117155
): JSX.Element => {
118-
return "onSubmit" in props ? <_ {...props} /> : SubmitForm(props)
156+
return "onSubmit" in props ? <BaseForm {...props} /> : SubmitForm(props)
119157
}
120158

121159
export default Form

src/hooks/form.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { useRef } from "react"
2+
3+
/**
4+
* Shorthand for a reference to a HTML input element since this is so common for
5+
* forms.
6+
*
7+
* @returns Ref object to a HTML input element.
8+
*/
9+
export function useInputRef() {
10+
return useRef<HTMLInputElement>(null)
11+
}

src/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./api"
22
export * from "./auth"
3+
export * from "./form"
34
export * from "./general"
45
export * from "./router"

0 commit comments

Comments
 (0)