mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-27 12:39:49 +08:00
feat: focus on the first field with error in form validation (#51231)
Co-authored-by: afc163 <afc163@gmail.com>
This commit is contained in:
parent
d52de7145e
commit
1d76fe94fa
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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>();
|
||||
|
@ -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 }}
|
||||
|
@ -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];
|
||||
|
@ -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 |
|
||||
|
@ -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 |
|
||||
|
Loading…
Reference in New Issue
Block a user