feat: custom feedback icons (#43894)

* custom feedback icons initial

(cherry picked from commit 22e43ad0357ea5294baf6eda659c900b1ab170f1)

* tests added and snaps updated

* Revert "tests added and snaps updated"

This reverts commit 13b57be30c.

* unittest and documentation changes

* feedback items could be turn off

* documentation fix

* move feedback icons object into the hasFeedback prop

* feedbackIcons added to the form element

* test: commit trigger

* fix: failed form test

* snaps updated

* Update components/form/index.en-US.md

Co-authored-by: afc163 <afc163@gmail.com>
Signed-off-by: Gunay <gladio@gmail.com>

* Update components/form/index.en-US.md

Co-authored-by: afc163 <afc163@gmail.com>
Signed-off-by: Gunay <gladio@gmail.com>

* Update components/form/index.zh-CN.md

Co-authored-by: afc163 <afc163@gmail.com>
Signed-off-by: Gunay <gladio@gmail.com>

* Update components/form/demo/custom-feedback-icons.md

Signed-off-by: afc163 <afc163@gmail.com>

* Update components/form/demo/custom-feedback-icons.md

Signed-off-by: afc163 <afc163@gmail.com>

---------

Signed-off-by: Gunay <gladio@gmail.com>
Signed-off-by: afc163 <afc163@gmail.com>
Co-authored-by: afc163 <afc163@gmail.com>
This commit is contained in:
Gunay 2023-09-04 15:36:45 +03:00 committed by GitHub
parent 4c91896abb
commit 46341b115c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 494 additions and 18 deletions

View File

@ -18,6 +18,7 @@ import useFormWarning from './hooks/useFormWarning';
import type { FormLabelAlign } from './interface';
import useStyle from './style';
import ValidateMessagesContext from './validateMessagesContext';
import type { FeedbackIcons } from './FormItem';
export type RequiredMark =
| boolean
@ -35,6 +36,7 @@ export interface FormProps<Values = any> extends Omit<RcFormProps<Values>, 'form
labelCol?: ColProps;
wrapperCol?: ColProps;
form?: FormInstance<Values>;
feedbackIcons?: FeedbackIcons;
size?: SizeType;
disabled?: boolean;
scrollToFirstError?: Options | boolean;
@ -67,6 +69,7 @@ const InternalForm: React.ForwardRefRenderFunction<FormInstance, FormProps> = (p
onFinishFailed,
name,
style,
feedbackIcons,
...restFormProps
} = props;
@ -132,8 +135,19 @@ const InternalForm: React.ForwardRefRenderFunction<FormInstance, FormProps> = (p
requiredMark: mergedRequiredMark,
itemRef: __INTERNAL__.itemRef,
form: wrapForm,
feedbackIcons,
}),
[name, labelAlign, labelCol, wrapperCol, layout, mergedColon, mergedRequiredMark, wrapForm],
[
name,
labelAlign,
labelCol,
wrapperCol,
layout,
mergedColon,
mergedRequiredMark,
wrapForm,
feedbackIcons,
],
);
React.useImperativeHandle(ref, () => wrapForm);

View File

@ -59,7 +59,7 @@ export default function ItemHolder(props: ItemHolderProps) {
} = props;
const itemPrefixCls = `${prefixCls}-item`;
const { requiredMark } = React.useContext(FormContext);
const { requiredMark, feedbackIcons } = React.useContext(FormContext);
// ======================== Margin ========================
const itemRef = React.useRef<HTMLDivElement>(null);
@ -111,24 +111,29 @@ export default function ItemHolder(props: ItemHolderProps) {
const formItemStatusContext = React.useMemo<FormItemStatusContextProps>(() => {
let feedbackIcon: React.ReactNode;
if (hasFeedback) {
const customIcons = (hasFeedback !== true && hasFeedback.icons) || feedbackIcons;
const customIconNode =
mergedValidateStatus &&
customIcons?.({ status: mergedValidateStatus, errors, warnings })?.[mergedValidateStatus];
const IconNode = mergedValidateStatus && iconMap[mergedValidateStatus];
feedbackIcon = IconNode ? (
<span
className={classNames(
`${itemPrefixCls}-feedback-icon`,
`${itemPrefixCls}-feedback-icon-${mergedValidateStatus}`,
)}
>
<IconNode />
</span>
) : null;
feedbackIcon =
customIconNode !== false && IconNode ? (
<span
className={classNames(
`${itemPrefixCls}-feedback-icon`,
`${itemPrefixCls}-feedback-icon-${mergedValidateStatus}`,
)}
>
{customIconNode || <IconNode />}
</span>
) : null;
}
return {
status: mergedValidateStatus,
errors,
warnings,
hasFeedback,
hasFeedback: !!hasFeedback,
feedbackIcon,
isFormItemInput: true,
};

View File

@ -34,6 +34,12 @@ type RenderChildren<Values = any> = (form: FormInstance<Values>) => React.ReactN
type RcFieldProps<Values = any> = Omit<FieldProps<Values>, 'children'>;
type ChildrenType<Values = any> = RenderChildren<Values> | React.ReactNode;
export type FeedbackIcons = (itemStatus: {
status: ValidateStatus;
errors?: React.ReactNode[];
warnings?: React.ReactNode[];
}) => { [key in ValidateStatus]?: React.ReactNode };
interface MemoInputProps {
value: any;
update: any;
@ -61,7 +67,7 @@ export interface FormItemProps<Values = any>
rootClassName?: string;
children?: ChildrenType<Values>;
id?: string;
hasFeedback?: boolean;
hasFeedback?: boolean | { icons: FeedbackIcons };
validateStatus?: ValidateStatus;
required?: boolean;
hidden?: boolean;

View File

@ -2215,6 +2215,177 @@ exports[`renders components/form/demo/control-ref.tsx extend context correctly 1
exports[`renders components/form/demo/control-ref.tsx extend context correctly 2`] = `[]`;
exports[`renders components/form/demo/custom-feedback-icons.tsx extend context correctly 1`] = `
<form
class="ant-form ant-form-horizontal"
id="custom-feedback-icons"
style="max-width: 600px;"
>
<div
class="ant-form-item acss-140b0ev ant-form-item-with-help"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-form-item-label"
>
<label
class="ant-form-item-required"
for="custom-feedback-icons_custom-feedback-test-item"
title="Test"
>
Test
</label>
</div>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-has-feedback"
>
<input
aria-required="true"
class="ant-input"
id="custom-feedback-icons_custom-feedback-test-item"
type="text"
value=""
/>
<span
class="ant-input-suffix"
/>
</span>
</div>
</div>
<div
style="display: flex; flex-wrap: nowrap;"
>
<div
class="ant-form-item-explain ant-form-show-help-appear ant-form-show-help-appear-start ant-form-show-help ant-form-item-explain-connected"
id="custom-feedback-icons_custom-feedback-test-item_help"
role="alert"
>
<div
class="ant-form-show-help-item-appear ant-form-show-help-item-appear-start ant-form-show-help-item"
style="height: 0px; opacity: 0;"
/>
</div>
<div
style="width: 0px; height: 24px;"
/>
</div>
</div>
</div>
<div
class="ant-form-item-margin-offset"
style="margin-bottom: -24px;"
/>
</div>
<div
class="ant-form-item acss-140b0ev ant-form-item-with-help"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-form-item-label"
>
<label
class="ant-form-item-required"
for="custom-feedback-icons_custom-feedback-test-item2"
title="Test"
>
Test
</label>
</div>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-has-feedback"
>
<input
aria-required="true"
class="ant-input"
id="custom-feedback-icons_custom-feedback-test-item2"
type="text"
value=""
/>
<span
class="ant-input-suffix"
/>
</span>
</div>
</div>
<div
style="display: flex; flex-wrap: nowrap;"
>
<div
class="ant-form-item-explain ant-form-show-help-appear ant-form-show-help-appear-start ant-form-show-help ant-form-item-explain-connected"
id="custom-feedback-icons_custom-feedback-test-item2_help"
role="alert"
>
<div
class="ant-form-show-help-item-appear ant-form-show-help-item-appear-start ant-form-show-help-item"
style="height: 0px; opacity: 0;"
/>
</div>
<div
style="width: 0px; height: 24px;"
/>
</div>
</div>
</div>
<div
class="ant-form-item-margin-offset"
style="margin-bottom: -24px;"
/>
</div>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<button
class="ant-btn ant-btn-default"
type="submit"
>
<span>
Submit
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</form>
`;
exports[`renders components/form/demo/custom-feedback-icons.tsx extend context correctly 2`] = `[]`;
exports[`renders components/form/demo/customized-form-controls.tsx extend context correctly 1`] = `
<form
class="ant-form ant-form-inline"

View File

@ -1641,6 +1641,133 @@ exports[`renders components/form/demo/control-ref.tsx correctly 1`] = `
</form>
`;
exports[`renders components/form/demo/custom-feedback-icons.tsx correctly 1`] = `
<form
class="ant-form ant-form-horizontal"
id="custom-feedback-icons"
style="max-width:600px"
>
<div
class="ant-form-item acss-140b0ev ant-form-item-with-help"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-form-item-label"
>
<label
class="ant-form-item-required"
for="custom-feedback-icons_custom-feedback-test-item"
title="Test"
>
Test
</label>
</div>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-has-feedback"
>
<input
aria-required="true"
class="ant-input"
id="custom-feedback-icons_custom-feedback-test-item"
type="text"
value=""
/>
<span
class="ant-input-suffix"
/>
</span>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item acss-140b0ev ant-form-item-with-help"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-form-item-label"
>
<label
class="ant-form-item-required"
for="custom-feedback-icons_custom-feedback-test-item2"
title="Test"
>
Test
</label>
</div>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-has-feedback"
>
<input
aria-required="true"
class="ant-input"
id="custom-feedback-icons_custom-feedback-test-item2"
type="text"
value=""
/>
<span
class="ant-input-suffix"
/>
</span>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-form-item"
>
<div
class="ant-row ant-form-item-row"
>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<button
class="ant-btn ant-btn-default"
type="submit"
>
<span>
Submit
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</form>
`;
exports[`renders components/form/demo/customized-form-controls.tsx correctly 1`] = `
<form
class="ant-form ant-form-inline"

View File

@ -2,6 +2,7 @@ import classNames from 'classnames';
import type { ChangeEventHandler } from 'react';
import React, { version as ReactVersion, useEffect, useRef, useState } from 'react';
import scrollIntoView from 'scroll-into-view-if-needed';
import { AlertFilled } from '@ant-design/icons';
import type { ColProps } from 'antd/es/grid';
import type { FormInstance } from '..';
import Form from '..';
@ -1239,7 +1240,6 @@ describe('Form', () => {
expect((Util.getFieldId as () => string)()).toBe(itemName);
// make sure input id is parentNode
expect(screen.getByLabelText(itemName)).toHaveAttribute('id', itemName);
expect(screen.getByLabelText(itemName)).toHaveAccessibleName('Search');
fireEvent.click(container.querySelector('button')!);
@ -1781,6 +1781,66 @@ describe('Form', () => {
expect(container.querySelector('.ant-form-item-has-error')).toBeTruthy();
});
it('custom feedback icons should display when pass hasFeedback prop', async () => {
const App = ({ trigger = false }: { trigger?: boolean }) => {
const form = useRef<FormInstance<any>>(null);
useEffect(() => {
if (!trigger) return;
form.current?.validateFields();
}, [trigger]);
return (
<Form
ref={form}
feedbackIcons={() => ({
error: <AlertFilled id="custom-error-icon" />,
})}
>
<Form.Item
label="Success"
name="name1"
hasFeedback
rules={[
{
required: true,
message: 'Please input your value',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Success"
name="name1"
hasFeedback={{
icons: () => ({
error: <AlertFilled id="custom-error-icon2" />,
}),
}}
rules={[
{
required: true,
message: 'Please input your value 3',
},
]}
>
<Input />
</Form.Item>
</Form>
);
};
const { container, rerender } = render(<App />);
expect(container.querySelectorAll('.ant-form-item-has-feedback').length).toBe(0);
rerender(<App trigger />);
await waitFakeTimer();
expect(container.querySelectorAll('.ant-form-item-has-feedback').length).toBe(2);
expect(container.querySelectorAll('#custom-error-icon, #custom-error-icon2').length).toBe(2);
});
// https://github.com/ant-design/ant-design/issues/41621
it('should not override value when pass `undefined` to require', async () => {
// When require is `undefined`, the `isRequire` calculation logic should be preserved

View File

@ -7,7 +7,7 @@ import * as React from 'react';
import { useContext, useMemo } from 'react';
import type { ColProps } from '../grid/col';
import type { FormInstance, RequiredMark } from './Form';
import type { ValidateStatus } from './FormItem';
import type { ValidateStatus, FeedbackIcons } from './FormItem';
import type { FormLabelAlign } from './interface';
/** Form Context. Set top form style and pass to Form Item usage. */
@ -22,6 +22,7 @@ export interface FormContextProps {
requiredMark?: RequiredMark;
itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void;
form?: FormInstance;
feedbackIcons?: FeedbackIcons;
}
export const FormContext = React.createContext<FormContextProps>({

View File

@ -0,0 +1,7 @@
## zh-CN
自定义反馈图标可以通过 `hasFeedback={{ icons: ... }}``<Form FeedbackIcons={icons}>` 传递(`Form.Item` 必须具有 `hasFeedback` 属性)。
## en-US
Custom feedback icons can be passed by `hasFeedback={{ icons: ... }}` or `<Form feedbackIcons={icons}>` (`Form.Item` must has `hasFeedback` attribute).

View File

@ -0,0 +1,77 @@
import React from 'react';
import { uniqueId } from 'lodash';
import { createStyles, css } from 'antd-style';
import { AlertFilled, CloseSquareFilled } from '@ant-design/icons';
import { Button, Form, Input, Tooltip } from 'antd';
const useStyle = createStyles(() => ({
'custom-feedback-icons': css`
.ant-form-item-feedback-icon {
pointer-events: all;
}
`,
}));
const App: React.FC = () => {
const [form] = Form.useForm();
const { styles } = useStyle();
return (
<Form
name="custom-feedback-icons"
form={form}
style={{ maxWidth: 600 }}
feedbackIcons={({ errors }) => ({
error: (
<Tooltip
key="tooltipKey"
title={errors?.map((error) => <div key={uniqueId()}>{error}</div>)}
color="red"
>
<CloseSquareFilled />
</Tooltip>
),
})}
>
<Form.Item
name="custom-feedback-test-item"
label="Test"
className={styles['custom-feedback-icons']}
rules={[{ required: true, type: 'email' }, { min: 10 }]}
help=""
hasFeedback
>
<Input />
</Form.Item>
<Form.Item
name="custom-feedback-test-item2"
label="Test"
className={styles['custom-feedback-icons']}
rules={[{ required: true, type: 'email' }, { min: 10 }]}
help=""
hasFeedback={{
icons: ({ errors }) => ({
error: (
<Tooltip
key="tooltipKey"
title={errors?.map((error) => <div key={uniqueId()}>{error}</div>)}
color="pink"
>
<AlertFilled />
</Tooltip>
),
success: false,
}),
}}
>
<Input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>
);
};
export default App;

View File

@ -52,6 +52,7 @@ High performance Form component with data scope management. Including data colle
<code src="./demo/label-debug.tsx" debug>label ellipsis</code>
<code src="./demo/col-24-debug.tsx" debug>Test col 24 usage</code>
<code src="./demo/ref-item.tsx" debug>Ref item</code>
<code src="./demo/custom-feedback-icons.tsx" debug>Custom feedback icons</code>
<code src="./demo/component-token.tsx" debug>Component Token</code>
## API
@ -67,6 +68,7 @@ Common props ref[Common props](/docs/react/common-props)
| component | Set the Form rendering element. Do not create a DOM node for `false` | ComponentType \| false | form | |
| fields | Control of form fields through state management (such as redux). Not recommended for non-strong demand. View [example](#components-form-demo-global-state) | [FieldData](#fielddata)\[] | - | |
| form | Form control instance created by `Form.useForm()`. Automatically created when not provided | [FormInstance](#forminstance) | - | |
| feedbackIcons | Can be passed custom icons while `Form.Item` element has `hasFeedback` | [FeedbackIcons](#feedbackicons) | - | |
| initialValues | Set value by Form initialization or reset | object | - | |
| labelAlign | The text align of label of all items | `left` \| `right` | `right` | |
| labelWrap | whether label can be wrap | boolean | false | 4.18.0 |
@ -122,7 +124,7 @@ Form field component for data bidirectional binding, validation, layout, and so
| extra | The extra prompt message. It is similar to help. Usage example: to display error message and prompt message at the same time | ReactNode | - | |
| getValueFromEvent | Specify how to get value from event or other onChange arguments | (..args: any\[]) => any | - | |
| getValueProps | Additional props with sub component | (value: any) => any | - | 4.2.0 |
| hasFeedback | Used with `validateStatus`, this option specifies the validation status icon. Recommended to be used only with `Input` | boolean | false | |
| hasFeedback | Used with `validateStatus`, this option specifies the validation status icon. Recommended to be used only with `Input`. Also, It can get feedback icons via icons prop. | boolean \| {icons:[FeedbackIcons](#feedbackicons)} | false | icons: 5.9.0 |
| help | The prompt message. If not provided, the prompt message will be generated by the validation rule. | ReactNode | - | |
| hidden | Whether to hide Form.Item (still collect and validate value) | boolean | false | 4.4.0 |
| htmlFor | Set sub label `htmlFor` | string | - | |
@ -158,6 +160,10 @@ Used when there are dependencies between fields. If a field has the `dependencie
`dependencies` shouldn't be used together with `shouldUpdate`, since it may result in conflicting update logic.
### FeedbackIcons
`({status:ValidateStatus, errors: ReactNode, warnings: ReactNode}) => Record<ValidateStatus,ReactNode>`
### shouldUpdate
Form updates only the modified field-related components for performance optimization purposes by incremental update. In most cases, you only need to write code or do validation with the [`dependencies`](#dependencies) property. In some specific cases, such as when a new field option appears with a field value changed, or you just want to keep some area updating by form update, you can modify the update logic of Form.Item via the `shouldUpdate`.

View File

@ -53,6 +53,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA
<code src="./demo/label-debug.tsx" debug>测试 label 省略</code>
<code src="./demo/col-24-debug.tsx" debug>测试特殊 col 24 用法</code>
<code src="./demo/ref-item.tsx" debug>引用字段</code>
<code src="./demo/custom-feedback-icons.tsx" debug>Custom feedback icons</code>
<code src="./demo/component-token.tsx" debug>组件 Token</code>
## API
@ -68,6 +69,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*ylFATY6w-ygAAA
| component | 设置 Form 渲染元素,为 `false` 则不创建 DOM 节点 | ComponentType \| false | form | |
| fields | 通过状态管理(如 redux控制表单字段如非强需求不推荐使用。查看[示例](#components-form-demo-global-state) | [FieldData](#fielddata)\[] | - | |
| form | 经 `Form.useForm()` 创建的 form 控制实例,不提供时会自动创建 | [FormInstance](#forminstance) | - | |
| feedbackIcons | Can be passed custom icons while `Form.Item` element has `hasFeedback` | ({status:ValidateStatus, errors: ReactNode, warnings: ReactNode}) => Record<ValidateStatus,ReactNode> | - | 5.9.0 |
| initialValues | 表单默认值,只有初始化以及重置时生效 | object | - | |
| labelAlign | label 标签的文本对齐方式 | `left` \| `right` | `right` | |
| labelWrap | label 标签的文本换行方式 | boolean | false | 4.18.0 |
@ -123,7 +125,7 @@ const validateMessages = {
| extra | 额外的提示信息,和 `help` 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 | ReactNode | - | |
| getValueFromEvent | 设置如何将 event 的值转换成字段值 | (..args: any\[]) => any | - | |
| getValueProps | 为子元素添加额外的属性 | (value: any) => any | - | 4.2.0 |
| hasFeedback | 配合 `validateStatus` 属性使用,展示校验状态图标,建议只配合 Input 组件使用 | boolean | false | |
| hasFeedback | 配合 `validateStatus` 属性使用,展示校验状态图标,建议只配合 Input 组件使用 此外,它还可以通过 Icons 属性获取反馈图标。 | boolean \| {icons:({status:ValidateStatus, errors: ReactNode, warnings: ReactNode}) => Record<ValidateStatus,ReactNode>} | false | |
| help | 提示信息,如不设置,则会根据校验规则自动生成 | ReactNode | - | |
| hidden | 是否隐藏字段(依然会收集和校验字段) | boolean | false | 4.4.0 |
| htmlFor | 设置子元素 label `htmlFor` 属性 | string | - | |