feat: focus on the first field with error in form validation (#51231)

Co-authored-by: afc163 <afc163@gmail.com>
This commit is contained in:
NathanLao 2024-10-15 01:24:27 -07:00 committed by GitHub
parent d52de7145e
commit 1d76fe94fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 54 additions and 7 deletions

View File

@ -31,6 +31,10 @@ export type RequiredMark =
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
export type FormItemLayout = 'horizontal' | 'vertical';
export type ScrollFocusOptions = Options & {
focus?: boolean;
};
export interface FormProps<Values = any> extends Omit<RcFormProps<Values>, 'form'> {
prefixCls?: string;
colon?: boolean;
@ -44,7 +48,7 @@ export interface FormProps<Values = any> extends Omit<RcFormProps<Values>, 'form
feedbackIcons?: FeedbackIcons;
size?: SizeType;
disabled?: boolean;
scrollToFirstError?: Options | boolean;
scrollToFirstError?: ScrollFocusOptions | boolean;
requiredMark?: RequiredMark;
/** @deprecated Will warning in future branch. Pls use `requiredMark` instead. */
hideRequiredMark?: boolean;
@ -166,13 +170,16 @@ const InternalForm: React.ForwardRefRenderFunction<FormRef, FormProps> = (props,
nativeElement: nativeElementRef.current?.nativeElement,
}));
const scrollToField = (options: boolean | Options, fieldName: InternalNamePath) => {
const scrollToField = (options: ScrollFocusOptions | boolean, fieldName: InternalNamePath) => {
if (options) {
let defaultScrollToFirstError: Options = { block: 'nearest' };
let defaultScrollToFirstError: ScrollFocusOptions = { block: 'nearest' };
if (typeof options === 'object') {
defaultScrollToFirstError = options;
defaultScrollToFirstError = { ...defaultScrollToFirstError, ...options };
}
wrapForm.scrollToField(fieldName, defaultScrollToFirstError);
if (defaultScrollToFirstError.focus) {
wrapForm.focusField(fieldName);
}
}
};

View File

@ -534,6 +534,38 @@ describe('Form', () => {
expect(scrollIntoView).toHaveBeenCalledTimes(3);
});
it('should scrollToFirstError work with focus', async () => {
const onFinishFailed = jest.fn();
const focusSpy = jest.spyOn(HTMLElement.prototype, 'focus');
const { container } = render(
<Form scrollToFirstError={{ block: 'center', focus: true }} onFinishFailed={onFinishFailed}>
<Form.Item name="test" rules={[{ required: true }]}>
<input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>,
);
expect(scrollIntoView).not.toHaveBeenCalled();
expect(focusSpy).not.toHaveBeenCalled();
fireEvent.submit(container.querySelector('form')!);
await waitFakeTimer();
const inputNode = document.getElementById('test');
expect(focusSpy).toHaveBeenCalledWith();
expect(scrollIntoView).toHaveBeenCalledWith(inputNode, {
block: 'center',
focus: true,
scrollMode: 'if-needed',
});
focusSpy.mockRestore();
});
// https://github.com/ant-design/ant-design/issues/28869
it('should work with Upload', async () => {
const uploadRef = React.createRef<any>();

View File

@ -7,7 +7,7 @@ const App = () => {
return (
<Form
form={form}
scrollToFirstError
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
style={{ paddingBlock: 32 }}
labelCol={{ span: 6 }}
wrapperCol={{ span: 14 }}

View File

@ -9,6 +9,7 @@ import { getFieldId, toArray } from '../util';
export interface FormInstance<Values = any> extends RcFormInstance<Values> {
scrollToField: (name: NamePath, options?: ScrollOptions) => void;
focusField: (name: NamePath) => void;
/** @internal: This is an internal usage. Do not use in your prod */
__INTERNAL__: {
/** No! Do not use this in your code! */
@ -67,6 +68,13 @@ export default function useForm<Values = any>(form?: FormInstance<Values>): [For
} as any);
}
},
focusField: (name: NamePath) => {
const node = getFieldDOMNode(name, wrapForm);
if (node) {
node.focus?.();
}
},
getFieldInstance: (name: NamePath) => {
const namePathStr = toNamePathStr(name);
return itemsRef.current[namePathStr];

View File

@ -80,7 +80,7 @@ Common props ref[Common props](/docs/react/common-props)
| name | Form name. Will be the prefix of Field `id` | string | - | |
| preserve | Keep field value even when field removed. You can get the preserve field value by `getFieldsValue(true)` | boolean | true | 4.4.0 |
| requiredMark | Required mark style. Can use required mark or optional mark. You can not config to single Form.Item since this is a Form level config | boolean \| `optional` \| ((label: ReactNode, info: { required: boolean }) => ReactNode) | true | `renderProps`: 5.9.0 |
| scrollToFirstError | Auto scroll to first failed field when submit | boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) | false | |
| scrollToFirstError | Auto scroll to first failed field when submit | boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) \| { focus: boolean } | false | |
| size | Set field component size (antd components only) | `small` \| `middle` \| `large` | - | |
| validateMessages | Validation prompt template, description [see below](#validatemessages) | [ValidateMessages](https://github.com/ant-design/ant-design/blob/6234509d18bac1ac60fbb3f92a5b2c6a6361295a/components/locale/en_US.ts#L88-L134) | - | |
| validateTrigger | Config field validate trigger | string \| string\[] | `onChange` | 4.3.0 |

View File

@ -81,7 +81,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA
| name | 表单名称,会作为表单字段 `id` 前缀使用 | string | - | |
| preserve | 当字段被删除时保留字段值。你可以通过 `getFieldsValue(true)` 来获取保留字段值 | boolean | true | 4.4.0 |
| requiredMark | 必选样式,可以切换为必选或者可选展示样式。此为 Form 配置Form.Item 无法单独配置 | boolean \| `optional` \| ((label: ReactNode, info: { required: boolean }) => ReactNode) | true | `renderProps`: 5.9.0 |
| scrollToFirstError | 提交失败自动滚动到第一个错误字段 | boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) | false | |
| scrollToFirstError | 提交失败自动滚动到第一个错误字段 | boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) \| { focus: boolean } | false | |
| size | 设置字段组件的尺寸(仅限 antd 组件) | `small` \| `middle` \| `large` | - | |
| validateMessages | 验证提示模板,说明[见下](#validatemessages) | [ValidateMessages](https://github.com/ant-design/ant-design/blob/6234509d18bac1ac60fbb3f92a5b2c6a6361295a/components/locale/en_US.ts#L88-L134) | - | |
| validateTrigger | 统一设置字段触发验证的时机 | string \| string\[] | `onChange` | 4.3.0 |