feat: Input support status (#33995)

* feat: remove form status style && input support status

* test: update snapshot

* feat: update status prop in config provider

* fix: form item validateStatus support

* chore: code clean

* feat: status classname change

* test: update snapshot

* refactor: move formItemStatusContext to form folder

* refactor: merge utils

* refactor: rename statusUtils

* chore: code clean

* test: fix coverage

* chore: remove status prop of Form.Item

* chore: code clean

* docs: update demo

* test: fix lint

* feat: status only success and warning

* test: fix lint

* docs: update deamo
This commit is contained in:
MadCcc 2022-02-14 17:09:35 +08:00 committed by GitHub
parent 2843bd32dd
commit 37e042358d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1604 additions and 1054 deletions

View File

@ -0,0 +1,41 @@
import React from 'react';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import classNames from 'classnames';
import { ValidateStatus } from '../form/FormItem';
import { tuple } from './type';
const InputStatuses = tuple('warning', 'error', '');
export type InputStatus = typeof InputStatuses[number];
const iconMap = {
success: CheckCircleFilled,
warning: ExclamationCircleFilled,
error: CloseCircleFilled,
validating: LoadingOutlined,
};
export const getFeedbackIcon = (prefixCls: string, status?: ValidateStatus) => {
const IconNode = status && iconMap[status];
return IconNode ? (
<span className={`${prefixCls}-feedback-icon`}>
<IconNode />
</span>
) : null;
};
export function getStatusClassNames(
prefixCls: string,
status?: ValidateStatus,
hasFeedback?: boolean,
) {
return classNames({
[`${prefixCls}-status-success`]: status === 'success',
[`${prefixCls}-status-warning`]: status === 'warning',
[`${prefixCls}-status-error`]: status === 'error',
[`${prefixCls}-status-validating`]: status === 'validating',
[`${prefixCls}-has-feedback`]: hasFeedback,
});
}

View File

@ -13367,7 +13367,7 @@ exports[`ConfigProvider components Form configProvider 1`] = `
class="config-form-item-control-input-content"
>
<input
class="config-input"
class="config-input config-input-status-error"
type="text"
value=""
/>
@ -13405,7 +13405,7 @@ exports[`ConfigProvider components Form configProvider componentSize large 1`] =
class="config-form-item-control-input-content"
>
<input
class="config-input config-input-lg"
class="config-input config-input-lg config-input-status-error"
type="text"
value=""
/>
@ -13443,7 +13443,7 @@ exports[`ConfigProvider components Form configProvider componentSize middle 1`]
class="config-form-item-control-input-content"
>
<input
class="config-input"
class="config-input config-input-status-error"
type="text"
value=""
/>
@ -13481,7 +13481,7 @@ exports[`ConfigProvider components Form configProvider virtual and dropdownMatch
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
class="ant-input ant-input-status-error"
type="text"
value=""
/>
@ -13519,7 +13519,7 @@ exports[`ConfigProvider components Form normal 1`] = `
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
class="ant-input ant-input-status-error"
type="text"
value=""
/>
@ -13557,7 +13557,7 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
class="prefix-Form-item-control-input-content"
>
<input
class="prefix-Form"
class="prefix-Form prefix-Form-status-error"
type="text"
value=""
/>

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import { useContext } from 'react';
import { useContext, useMemo } from 'react';
import classNames from 'classnames';
import { Field, FormInstance, FieldContext, ListContext } from 'rc-field-form';
import { FieldProps } from 'rc-field-form/lib/Field';
@ -12,7 +12,12 @@ import { tuple } from '../_util/type';
import devWarning from '../_util/devWarning';
import FormItemLabel, { FormItemLabelProps, LabelTooltipType } from './FormItemLabel';
import FormItemInput, { FormItemInputProps } from './FormItemInput';
import { FormContext, NoStyleItemContext } from './context';
import {
FormContext,
FormItemStatusContext,
NoStyleItemContext,
FormItemStatusContextProps,
} from './context';
import { toArray, getFieldId } from './util';
import { cloneElement, isValidElement } from '../_util/reactNode';
import useFrameState from './hooks/useFrameState';
@ -199,6 +204,28 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
// ===================== Children Ref =====================
const getItemRef = useItemRef();
// ======================== Status ========================
let mergedValidateStatus: ValidateStatus = '';
if (validateStatus !== undefined) {
mergedValidateStatus = validateStatus;
} else if (meta?.validating) {
mergedValidateStatus = 'validating';
} else if (debounceErrors.length) {
mergedValidateStatus = 'error';
} else if (debounceWarnings.length) {
mergedValidateStatus = 'warning';
} else if (meta?.touched) {
mergedValidateStatus = 'success';
}
const formItemStatusContext = useMemo<FormItemStatusContextProps>(
() => ({
status: mergedValidateStatus,
hasFeedback,
}),
[mergedValidateStatus, hasFeedback],
);
// ======================== Render ========================
function renderLayout(
baseChildren: React.ReactNode,
@ -208,19 +235,6 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
if (noStyle && !hidden) {
return baseChildren;
}
// ======================== Status ========================
let mergedValidateStatus: ValidateStatus = '';
if (validateStatus !== undefined) {
mergedValidateStatus = validateStatus;
} else if (meta?.validating) {
mergedValidateStatus = 'validating';
} else if (debounceErrors.length) {
mergedValidateStatus = 'error';
} else if (debounceWarnings.length) {
mergedValidateStatus = 'warning';
} else if (meta?.touched) {
mergedValidateStatus = 'success';
}
const itemClassName = {
[`${prefixCls}-item`]: true,
@ -281,11 +295,12 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
warnings={debounceWarnings}
prefixCls={prefixCls}
status={mergedValidateStatus}
validateStatus={mergedValidateStatus}
help={help}
>
<NoStyleItemContext.Provider value={onSubItemMetaChange}>
{baseChildren}
<FormItemStatusContext.Provider value={formItemStatusContext}>
{baseChildren}
</FormItemStatusContext.Provider>
</NoStyleItemContext.Provider>
</FormItemInput>
</Row>

View File

@ -1,10 +1,5 @@
import * as React from 'react';
import classNames from 'classnames';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import Col, { ColProps } from '../grid/col';
import { ValidateStatus } from './FormItem';
import { FormContext, FormItemPrefixContext } from './context';
@ -15,8 +10,6 @@ interface FormItemInputMiscProps {
children: React.ReactNode;
errors: React.ReactNode[];
warnings: React.ReactNode[];
hasFeedback?: boolean;
validateStatus?: ValidateStatus;
/** @private Internal Usage, do not use in any of your production. */
_internalItemRender?: {
mark: string;
@ -38,13 +31,6 @@ export interface FormItemInputProps {
help?: React.ReactNode;
}
const iconMap: { [key: string]: any } = {
success: CheckCircleFilled,
warning: ExclamationCircleFilled,
error: CloseCircleFilled,
validating: LoadingOutlined,
};
const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = props => {
const {
prefixCls,
@ -53,9 +39,7 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = pro
children,
errors,
warnings,
hasFeedback,
_internalItemRender: formItemRender,
validateStatus,
extra,
help,
} = props;
@ -67,15 +51,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = pro
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className);
// Should provides additional icon if `hasFeedback`
const IconNode = validateStatus && iconMap[validateStatus];
const icon =
hasFeedback && IconNode ? (
<span className={`${baseClassName}-children-icon`}>
<IconNode />
</span>
) : null;
// Pass to sub FormItem should not with col info
const subFormContext = React.useMemo(() => ({ ...formContext }), [formContext]);
delete subFormContext.labelCol;
@ -84,7 +59,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = pro
const inputDom = (
<div className={`${baseClassName}-control-input`}>
<div className={`${baseClassName}-control-input-content`}>{children}</div>
{icon}
</div>
);
const formItemContext = React.useMemo(() => ({ prefixCls, status }), [prefixCls, status]);

View File

@ -1197,7 +1197,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
class="ant-input ant-input-status-error"
placeholder="unavailable choice"
type="text"
value="Buggy!"
@ -1239,7 +1239,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
class="ant-form-item-control-input-content"
>
<input
class="ant-input ant-input-disabled"
class="ant-input ant-input-disabled ant-input-status-error"
disabled=""
placeholder="unavailable choice"
type="text"
@ -1314,7 +1314,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
class="ant-input ant-input-status-error"
placeholder="unavailable choice"
type="text"
value="Buggy!"
@ -1356,7 +1356,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
class="ant-form-item-control-input-content"
>
<input
class="ant-input ant-input-disabled"
class="ant-input ant-input-disabled ant-input-status-error"
disabled=""
placeholder="unavailable choice"
type="text"
@ -1444,7 +1444,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
class="ant-form-item-control-input-content"
>
<span
class="ant-input-group-wrapper"
class="ant-input-group-wrapper ant-input-group-wrapper-status-error"
>
<span
class="ant-input-wrapper ant-input-group"
@ -1455,7 +1455,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
Buggy!
</span>
<input
class="ant-input"
class="ant-input ant-input-status-error"
placeholder="unavailable choice"
type="text"
value=""
@ -1499,7 +1499,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
class="ant-form-item-control-input-content"
>
<span
class="ant-input-group-wrapper"
class="ant-input-group-wrapper ant-input-group-wrapper-status-error"
>
<span
class="ant-input-wrapper ant-input-group"
@ -1510,7 +1510,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
Buggy!
</span>
<input
class="ant-input ant-input-disabled"
class="ant-input ant-input-disabled ant-input-status-error"
disabled=""
placeholder="unavailable choice"
type="text"
@ -1596,7 +1596,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper"
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-error"
>
<span
class="ant-input-prefix"
@ -1647,7 +1647,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-disabled"
class="ant-input-affix-wrapper ant-input-affix-wrapper-disabled ant-input-affix-wrapper-status-error"
>
<span
class="ant-input-prefix"
@ -3630,7 +3630,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper ant-input-password"
class="ant-input-affix-wrapper ant-input-password ant-input-affix-wrapper-has-feedback"
>
<input
action="click"
@ -3695,7 +3695,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper ant-input-password"
class="ant-input-affix-wrapper ant-input-password ant-input-affix-wrapper-has-feedback"
>
<input
action="click"
@ -7229,7 +7229,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
class="ant-input ant-input-status-error"
id="error"
placeholder="unavailable choice"
type="text"
@ -7272,7 +7272,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper"
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-warning"
>
<span
class="ant-input-prefix"
@ -7330,38 +7330,46 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
id="validating"
placeholder="I'm the content is being validated"
type="text"
value=""
/>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="loading"
class="anticon anticon-loading anticon-spin"
role="img"
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-validating ant-input-affix-wrapper-has-feedback"
>
<svg
aria-hidden="true"
data-icon="loading"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
<input
class="ant-input"
id="validating"
placeholder="I'm the content is being validated"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<path
d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
/>
</svg>
<span
class="ant-input-feedback-icon"
>
<span
aria-label="loading"
class="anticon anticon-loading anticon-spin"
role="img"
>
<svg
aria-hidden="true"
data-icon="loading"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-connected"
@ -7396,38 +7404,46 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
id="success"
placeholder="I'm the content"
type="text"
value=""
/>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-success ant-input-affix-wrapper-has-feedback"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<input
class="ant-input"
id="success"
placeholder="I'm the content"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
<span
class="ant-input-feedback-icon"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
@ -7452,38 +7468,46 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
id="warning2"
placeholder="Warning"
type="text"
value=""
/>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle"
role="img"
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-warning ant-input-affix-wrapper-has-feedback"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<input
class="ant-input"
id="warning2"
placeholder="Warning"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
/>
</svg>
<span
class="ant-input-feedback-icon"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
@ -7508,38 +7532,46 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
id="error2"
placeholder="unavailable choice"
type="text"
value=""
/>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-error ant-input-affix-wrapper-has-feedback"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<input
class="ant-input"
id="error2"
placeholder="unavailable choice"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
<span
class="ant-input-feedback-icon"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-connected"
@ -7616,29 +7648,6 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div>
</div>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
@ -7708,29 +7717,6 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div>
</div>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
@ -7783,7 +7769,9 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
<span
class="ant-select-selection-placeholder"
/>
>
I'm Select
</span>
</div>
<span
aria-hidden="true"
@ -7813,29 +7801,6 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
</div>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
@ -7887,7 +7852,9 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
<span
class="ant-select-selection-placeholder"
/>
>
I'm Cascader
</span>
</div>
<span
aria-hidden="true"
@ -7917,29 +7884,6 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</span>
</div>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="loading"
class="anticon anticon-loading anticon-spin"
role="img"
>
<svg
aria-hidden="true"
data-icon="loading"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-connected"
@ -7953,6 +7897,99 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div>
</div>
</div>
<div
class="ant-row ant-form-item ant-form-item-with-help ant-form-item-has-feedback ant-form-item-has-warning"
>
<div
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
>
<label
class=""
title="Warning"
>
Warning
</label>
</div>
<div
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<div
class="ant-select ant-tree-select ant-select-single ant-select-allow-clear ant-select-show-arrow"
>
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-search"
>
<input
aria-autocomplete="list"
aria-controls="undefined_list"
aria-haspopup="listbox"
aria-owns="undefined_list"
autocomplete="off"
class="ant-select-selection-search-input"
readonly=""
role="combobox"
style="opacity:0"
type="search"
unselectable="on"
value=""
/>
</span>
<span
class="ant-select-selection-placeholder"
>
I'm TreeSelect
</span>
</div>
<span
aria-hidden="true"
class="ant-select-arrow"
style="user-select:none;-webkit-user-select:none"
unselectable="on"
>
<span
aria-label="down"
class="anticon anticon-down ant-select-suffix"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-warning"
role="alert"
>
Need to be checked
</div>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
style="margin-bottom:0"
@ -8204,29 +8241,6 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div>
</div>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
@ -8253,7 +8267,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper"
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-success ant-input-affix-wrapper-has-feedback"
>
<input
class="ant-input"
@ -8266,7 +8280,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon-has-suffix ant-input-clear-icon"
role="button"
tabindex="-1"
>
@ -8284,32 +8298,32 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
/>
</svg>
</span>
<span
class="ant-input-feedback-icon"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
</span>
</span>
</span>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
@ -8336,7 +8350,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper ant-input-password"
class="ant-input-affix-wrapper ant-input-password ant-input-affix-wrapper-status-warning ant-input-affix-wrapper-has-feedback"
>
<input
action="click"
@ -8371,32 +8385,32 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
/>
</svg>
</span>
<span
class="ant-input-feedback-icon"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
/>
</svg>
</span>
</span>
</span>
</span>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
@ -8423,7 +8437,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
<span
class="ant-input-affix-wrapper ant-input-password"
class="ant-input-affix-wrapper ant-input-password ant-input-affix-wrapper-status-error ant-input-affix-wrapper-has-feedback"
>
<input
action="click"
@ -8478,32 +8492,32 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
/>
</svg>
</span>
<span
class="ant-input-feedback-icon"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
</span>
</div>
<span
class="ant-form-item-children-icon"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
@ -8541,6 +8555,97 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
</div>
</div>
</div>
<div
class="ant-row ant-form-item ant-form-item-with-help ant-form-item-has-feedback ant-form-item-has-error"
>
<div
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
>
<label
class=""
title="Fail"
>
Fail
</label>
</div>
<div
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<div
class="ant-input-textarea ant-input-textarea-show-count ant-input-textarea-status-error ant-input-textarea-has-feedback"
data-count="0"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn ant-input-affix-wrapper-status-error ant-input-affix-wrapper-has-feedback"
>
<textarea
class="ant-input ant-input-status-error"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
<span
class="ant-input-feedback-icon"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</div>
</div>
</div>
<div
class="ant-form-item-explain ant-form-item-explain-connected"
>
<div
class="ant-form-item-explain-error"
role="alert"
>
Should have something
</div>
</div>
</div>
</div>
</form>
`;

View File

@ -50,3 +50,10 @@ export interface FormItemPrefixContextProps {
export const FormItemPrefixContext = React.createContext<FormItemPrefixContextProps>({
prefixCls: '',
});
export interface FormItemStatusContextProps {
status?: ValidateStatus;
hasFeedback?: boolean;
}
export const FormItemStatusContext = React.createContext<FormItemStatusContextProps>({});

View File

@ -23,7 +23,17 @@ We provide properties like `validateStatus` `help` `hasFeedback` to customize yo
```tsx
import { SmileOutlined } from '@ant-design/icons';
import { Form, Input, DatePicker, TimePicker, Select, Cascader, InputNumber, Mentions } from 'antd';
import {
Form,
Input,
DatePicker,
TimePicker,
Select,
Cascader,
InputNumber,
Mentions,
TreeSelect,
} from 'antd';
const { Option } = Select;
@ -87,7 +97,7 @@ ReactDOM.render(
</Form.Item>
<Form.Item label="Error" hasFeedback validateStatus="error">
<Select allowClear>
<Select placeholder="I'm Select" allowClear>
<Option value="1">Option 1</Option>
<Option value="2">Option 2</Option>
<Option value="3">Option 3</Option>
@ -100,7 +110,15 @@ ReactDOM.render(
validateStatus="validating"
help="The information is being validated..."
>
<Cascader options={[{ value: 'xx', label: 'xx' }]} allowClear />
<Cascader placeholder="I'm Cascader" options={[{ value: 'xx', label: 'xx' }]} allowClear />
</Form.Item>
<Form.Item label="Warning" hasFeedback validateStatus="warning" help="Need to be checked">
<TreeSelect
placeholder="I'm TreeSelect"
treeData={[{ value: 'xx', label: 'xx' }]}
allowClear
/>
</Form.Item>
<Form.Item label="inline" style={{ marginBottom: 0 }}>
@ -140,6 +158,10 @@ ReactDOM.render(
<Form.Item label="Fail" validateStatus="error">
<Mentions />
</Form.Item>
<Form.Item label="Fail" validateStatus="error" hasFeedback help="Should have something">
<Input.TextArea allowClear showCount />
</Form.Item>
</Form>,
mountNode,
);

View File

@ -19,9 +19,10 @@ import { Form, InputNumber } from 'antd';
type ValidateStatus = Parameters<typeof Form.Item>[0]['validateStatus'];
function validatePrimeNumber(
number: number,
): { validateStatus: ValidateStatus; errorMsg: string | null } {
function validatePrimeNumber(number: number): {
validateStatus: ValidateStatus;
errorMsg: string | null;
} {
if (number === 11) {
return {
validateStatus: 'success',

View File

@ -10,40 +10,10 @@
.@{ant-prefix}-form-item-split {
color: @text-color;
}
// 输入框的不同校验状态
:not(.@{ant-prefix}-input-disabled):not(.@{ant-prefix}-input-borderless).@{ant-prefix}-input,
:not(.@{ant-prefix}-input-affix-wrapper-disabled):not(.@{ant-prefix}-input-affix-wrapper-borderless).@{ant-prefix}-input-affix-wrapper,
:not(.@{ant-prefix}-input-number-affix-wrapper-disabled):not(.@{ant-prefix}-input-number-affix-wrapper-borderless).@{ant-prefix}-input-number-affix-wrapper {
&,
&:hover {
background-color: @background-color;
border-color: @border-color;
}
&:focus,
&-focused {
.active(@border-color, @hoverBorderColor, @outlineColor);
}
}
.@{ant-prefix}-calendar-picker-open .@{ant-prefix}-calendar-picker-input {
.active(@border-color, @hoverBorderColor, @outlineColor);
}
.@{ant-prefix}-input-prefix,
.@{ant-prefix}-input-number-prefix {
color: @text-color;
}
.@{ant-prefix}-input-group-addon,
.@{ant-prefix}-input-number-group-addon {
color: @text-color;
border-color: @border-color;
}
.has-feedback {
color: @text-color;
}
}
// Reset form styles

View File

@ -24,287 +24,19 @@
}
&-has-feedback {
// ========================= Input =========================
.@{ant-prefix}-input {
padding-right: 24px;
}
// https://github.com/ant-design/ant-design/issues/19884
.@{ant-prefix}-input-affix-wrapper {
.@{ant-prefix}-input-suffix {
padding-right: 18px;
}
}
// Fix issue: https://github.com/ant-design/ant-design/issues/7854
.@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
.@{ant-prefix}-input-suffix {
right: 28px;
}
}
// ======================== Switch =========================
.@{ant-prefix}-switch {
margin: 2px 0 4px;
}
// ======================== Select =========================
// Fix overlapping between feedback icon and <Select>'s arrow.
// https://github.com/ant-design/ant-design/issues/4431
> .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
> .@{ant-prefix}-select .@{ant-prefix}-select-clear,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
:not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-clear,
:not(.@{ant-prefix}-input-number-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-arrow,
:not(.@{ant-prefix}-input-number-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-clear {
right: 32px;
}
> .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
:not(.@{ant-prefix}-input-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection-selected-value,
:not(.@{ant-prefix}-input-number-group-addon)
> .@{ant-prefix}-select
.@{ant-prefix}-select-selection-selected-value {
padding-right: 42px;
}
// ======================= Cascader ========================
.@{ant-prefix}-cascader-picker {
&-arrow {
margin-right: 19px;
}
&-clear {
right: 32px;
}
}
// ======================== Picker =========================
// Fix issue: https://github.com/ant-design/ant-design/issues/4783
.@{ant-prefix}-picker {
padding-right: @input-padding-horizontal-base + @font-size-base * 1.3;
&-large {
padding-right: @input-padding-horizontal-lg + @font-size-base * 1.3;
}
&-small {
padding-right: @input-padding-horizontal-sm + @font-size-base * 1.3;
}
}
// ===================== Status Group ======================
&.@{form-item-prefix-cls} {
&-has-success,
&-has-warning,
&-has-error,
&-is-validating {
// ====================== Icon ======================
.@{form-item-prefix-cls}-children-icon {
position: absolute;
top: 50%;
right: 0;
z-index: 1;
width: @input-height-base;
height: 20px;
margin-top: -10px;
font-size: @font-size-base;
line-height: 20px;
text-align: center;
visibility: visible;
animation: zoomIn 0.3s @ease-out-back;
pointer-events: none;
}
}
}
}
// ======================== Success ========================
&-has-success {
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
color: @success-color;
animation-name: diffZoomIn1 !important;
}
}
// ======================== Warning ========================
&-has-warning {
.form-control-validation(@warning-color; @warning-color; @form-warning-input-bg; @warning-color-hover; @warning-color-outline);
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
color: @warning-color;
animation-name: diffZoomIn3 !important;
}
// Select
.@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) {
.@{ant-prefix}-select-selector {
background-color: @form-warning-input-bg;
border-color: @warning-color !important;
}
&.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
&.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
.active(@warning-color, @warning-color-hover, @warning-color-outline);
}
}
// InputNumber, TimePicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-picker {
background-color: @form-warning-input-bg;
border-color: @warning-color;
&-focused,
&:focus {
.active(@warning-color, @warning-color-hover, @warning-color-outline);
}
&:not([disabled]):hover {
background-color: @form-warning-input-bg;
border-color: @warning-color;
}
}
.@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
.active(@warning-color, @warning-color-hover, @warning-color-outline);
}
}
// ========================= Error =========================
&-has-error {
.form-control-validation(@error-color; @error-color; @form-error-input-bg; @error-color-hover; @error-color-outline);
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
color: @error-color;
animation-name: diffZoomIn2 !important;
}
// Select
.@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) {
.@{ant-prefix}-select-selector {
background-color: @form-error-input-bg;
border-color: @error-color !important;
}
&.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
&.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
.active(@error-color, @error-color-hover, @error-color-outline);
}
}
// fixes https://github.com/ant-design/ant-design/issues/20482
.@{ant-prefix}-input-group-addon,
.@{ant-prefix}-input-number-group-addon {
.@{ant-prefix}-select {
&.@{ant-prefix}-select-single:not(.@{ant-prefix}-select-customize-input)
.@{ant-prefix}-select-selector {
background-color: inherit;
border: 0;
box-shadow: none;
}
}
}
.@{ant-prefix}-select.@{ant-prefix}-select-auto-complete {
.@{ant-prefix}-input:focus {
border-color: @error-color;
}
}
// InputNumber, TimePicker
.@{ant-prefix}-input-number,
.@{ant-prefix}-picker {
background-color: @form-error-input-bg;
border-color: @error-color;
&-focused,
&:focus {
.active(@error-color, @error-color-hover, @error-color-outline);
}
&:not([disabled]):hover {
background-color: @form-error-input-bg;
border-color: @error-color;
}
}
.@{ant-prefix}-mention-wrapper {
.@{ant-prefix}-mention-editor {
&,
&:not([disabled]):hover {
background-color: @form-error-input-bg;
border-color: @error-color;
}
}
&.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor,
.@{ant-prefix}-mention-editor:not([disabled]):focus {
.active(@error-color, @error-color-hover, @error-color-outline);
}
}
// Cascader
.@{ant-prefix}-cascader-picker {
&:hover
.@{ant-prefix}-cascader-picker-label:hover
+ .@{ant-prefix}-cascader-input.@{ant-prefix}-input {
border-color: @error-color;
}
&:focus .@{ant-prefix}-cascader-input {
background-color: @form-error-input-bg;
.active(@error-color, @error-color-hover, @error-color-outline);
}
}
// Transfer
.@{ant-prefix}-transfer {
&-list {
border-color: @error-color;
&-search:not([disabled]) {
border-color: @input-border-color;
&:hover {
.hover();
}
&:focus {
.active();
}
}
}
}
// Radio.Group
.@{ant-prefix}-radio-button-wrapper {
border-color: @error-color !important;
&:not(:first-child) {
&::before {
background-color: @error-color;
}
}
}
// Mentions
.@{ant-prefix}-mentions {
border-color: @error-color !important;
&-focused,
&:focus {
.active(@error-color, @error-color-hover, @error-color-outline);
}
}
}
// ====================== Validating =======================
&-is-validating {
&.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
display: inline-block;
color: @primary-color;
}
}
}

View File

@ -1,11 +1,13 @@
import * as React from 'react';
import classNames from 'classnames';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import { tuple } from '../_util/type';
import type { InputProps } from './Input';
import classNames from 'classnames';
import * as React from 'react';
import { DirectionType } from '../config-provider';
import { SizeType } from '../config-provider/SizeContext';
import { FormItemStatusContext, FormItemStatusContextProps } from '../form/context';
import { cloneElement } from '../_util/reactNode';
import { getStatusClassNames, InputStatus } from '../_util/statusUtils';
import { tuple } from '../_util/type';
import type { InputProps } from './Input';
import { getInputClassName, hasPrefixSuffix } from './utils';
const ClearableInputType = tuple('text', 'input');
@ -40,6 +42,7 @@ export interface ClearableInputProps extends BasicProps {
addonBefore?: React.ReactNode;
addonAfter?: React.ReactNode;
triggerFocus?: () => void;
status?: InputStatus;
}
class ClearableLabeledInput extends React.Component<ClearableInputProps> {
@ -91,7 +94,11 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
return null;
}
renderLabeledIcon(prefixCls: string, element: React.ReactElement) {
renderLabeledIcon(
prefixCls: string,
element: React.ReactElement,
statusContext: FormItemStatusContextProps,
) {
const {
focused,
value,
@ -106,7 +113,11 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
readOnly,
bordered,
hidden,
status: customStatus,
} = this.props;
const { status: contextStatus, hasFeedback } = statusContext;
if (!hasPrefixSuffix(this.props)) {
return cloneElement(element, {
value,
@ -116,18 +127,22 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
const suffixNode = this.renderSuffix(prefixCls);
const prefixNode = prefix ? <span className={`${prefixCls}-prefix`}>{prefix}</span> : null;
const affixWrapperCls = classNames(`${prefixCls}-affix-wrapper`, {
[`${prefixCls}-affix-wrapper-focused`]: focused,
[`${prefixCls}-affix-wrapper-disabled`]: disabled,
[`${prefixCls}-affix-wrapper-sm`]: size === 'small',
[`${prefixCls}-affix-wrapper-lg`]: size === 'large',
[`${prefixCls}-affix-wrapper-input-with-clear-btn`]: suffix && allowClear && value,
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-readonly`]: readOnly,
[`${prefixCls}-affix-wrapper-borderless`]: !bordered,
// className will go to addon wrapper
[`${className}`]: !hasAddon(this.props) && className,
});
const affixWrapperCls = classNames(
`${prefixCls}-affix-wrapper`,
{
[`${prefixCls}-affix-wrapper-focused`]: focused,
[`${prefixCls}-affix-wrapper-disabled`]: disabled,
[`${prefixCls}-affix-wrapper-sm`]: size === 'small',
[`${prefixCls}-affix-wrapper-lg`]: size === 'large',
[`${prefixCls}-affix-wrapper-input-with-clear-btn`]: suffix && allowClear && value,
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-readonly`]: readOnly,
[`${prefixCls}-affix-wrapper-borderless`]: !bordered,
// className will go to addon wrapper
[`${className}`]: !hasAddon(this.props) && className,
},
getStatusClassNames(`${prefixCls}-affix-wrapper`, contextStatus || customStatus, hasFeedback),
);
return (
<span
ref={this.containerRef}
@ -147,8 +162,22 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
);
}
renderInputWithLabel(prefixCls: string, labeledElement: React.ReactElement) {
const { addonBefore, addonAfter, style, size, className, direction, hidden } = this.props;
renderInputWithLabel(
prefixCls: string,
labeledElement: React.ReactElement,
statusContext: FormItemStatusContextProps,
) {
const {
addonBefore,
addonAfter,
style,
size,
className,
direction,
hidden,
status: customStatus,
} = this.props;
const { status: contextStatus, hasFeedback } = statusContext;
// Not wrap when there is not addons
if (!hasAddon(this.props)) {
return labeledElement;
@ -172,6 +201,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
[`${prefixCls}-group-wrapper-lg`]: size === 'large',
[`${prefixCls}-group-wrapper-rtl`]: direction === 'rtl',
},
getStatusClassNames(`${prefixCls}-group-wrapper`, contextStatus || customStatus, hasFeedback),
className,
);
@ -188,8 +218,24 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
);
}
renderTextAreaWithClearIcon(prefixCls: string, element: React.ReactElement) {
const { value, allowClear, className, style, direction, bordered, hidden } = this.props;
renderTextAreaWithClearIcon(
prefixCls: string,
element: React.ReactElement,
statusContext: FormItemStatusContextProps,
) {
const {
value,
allowClear,
className,
style,
direction,
bordered,
hidden,
status: customStatus,
} = this.props;
const { status: contextStatus, hasFeedback } = statusContext;
if (!allowClear) {
return cloneElement(element, {
value,
@ -198,6 +244,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
const affixWrapperCls = classNames(
`${prefixCls}-affix-wrapper`,
`${prefixCls}-affix-wrapper-textarea-with-clear-btn`,
getStatusClassNames(`${prefixCls}-affix-wrapper`, contextStatus || customStatus, hasFeedback),
{
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-borderless`]: !bordered,
@ -217,11 +264,21 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
}
render() {
const { prefixCls, inputType, element } = this.props;
if (inputType === ClearableInputType[0]) {
return this.renderTextAreaWithClearIcon(prefixCls, element);
}
return this.renderInputWithLabel(prefixCls, this.renderLabeledIcon(prefixCls, element));
return (
<FormItemStatusContext.Consumer>
{statusContext => {
const { prefixCls, inputType, element } = this.props;
if (inputType === ClearableInputType[0]) {
return this.renderTextAreaWithClearIcon(prefixCls, element, statusContext);
}
return this.renderInputWithLabel(
prefixCls,
this.renderLabeledIcon(prefixCls, element, statusContext),
statusContext,
);
}}
</FormItemStatusContext.Consumer>
);
}
}

View File

@ -1,6 +1,7 @@
import * as React from 'react';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
import { ValidateStatus } from '../form/FormItem';
import type Group from './Group';
import type Search from './Search';
import type TextArea from './TextArea';
@ -11,6 +12,8 @@ import { ConfigConsumer, ConfigConsumerProps, DirectionType } from '../config-pr
import SizeContext, { SizeType } from '../config-provider/SizeContext';
import devWarning from '../_util/devWarning';
import { getInputClassName, hasPrefixSuffix } from './utils';
import { FormItemStatusContext } from '../form/context';
import { getFeedbackIcon, InputStatus } from '../_util/statusUtils';
export interface InputFocusOptions extends FocusOptions {
cursor?: 'start' | 'end' | 'all';
@ -59,6 +62,7 @@ export interface InputProps
showCount?: boolean | ShowCountProps;
bordered?: boolean;
htmlSize?: number;
status?: InputStatus;
}
export function fixControlledValue<T>(value: T) {
@ -280,6 +284,7 @@ class Input extends React.Component<InputProps, InputState> {
prefixCls: string,
size: SizeType | undefined,
bordered: boolean,
status?: ValidateStatus,
input: ConfigConsumerProps['input'] = {},
) => {
const {
@ -307,7 +312,9 @@ class Input extends React.Component<InputProps, InputState> {
'bordered',
'htmlSize',
'showCount',
'status',
]);
return (
<input
autoComplete={input.autoComplete}
@ -317,7 +324,14 @@ class Input extends React.Component<InputProps, InputState> {
onBlur={this.onBlur}
onKeyDown={this.handleKeyDown}
className={classNames(
getInputClassName(prefixCls, bordered, customizeSize || size, disabled, this.direction),
getInputClassName(
prefixCls,
bordered,
customizeSize || size,
disabled,
this.direction,
status,
),
{
[className!]: className && !addonBefore && !addonAfter,
},
@ -369,49 +383,66 @@ class Input extends React.Component<InputProps, InputState> {
dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
}
return (
<>
{!!showCount && (
<span
className={classNames(`${prefixCls}-show-count-suffix`, {
[`${prefixCls}-show-count-has-suffix`]: !!suffix,
})}
>
{dataCount}
</span>
)}
{suffix}
</>
!!showCount && (
<span
className={classNames(`${prefixCls}-show-count-suffix`, {
[`${prefixCls}-show-count-has-suffix`]: !!suffix,
})}
>
{dataCount}
</span>
)
);
}
return null;
};
renderSuffix = (prefixCls: string, hasFeedback?: boolean, status?: ValidateStatus) => {
const { suffix, showCount } = this.props;
return (
(showCount || suffix || hasFeedback) && (
<>
{this.renderShowCountSuffix(prefixCls)}
{suffix}
{hasFeedback && getFeedbackIcon(prefixCls, status)}
</>
)
);
};
renderComponent = ({ getPrefixCls, direction, input }: ConfigConsumerProps) => {
const { value, focused } = this.state;
const { prefixCls: customizePrefixCls, bordered = true } = this.props;
const { prefixCls: customizePrefixCls, bordered = true, status: customStatus } = this.props;
const prefixCls = getPrefixCls('input', customizePrefixCls);
this.direction = direction;
const showCountSuffix = this.renderShowCountSuffix(prefixCls);
return (
<SizeContext.Consumer>
{size => (
<ClearableLabeledInput
size={size}
{...this.props}
prefixCls={prefixCls}
inputType="input"
value={fixControlledValue(value)}
element={this.renderInput(prefixCls, size, bordered, input)}
handleReset={this.handleReset}
ref={this.saveClearableInput}
direction={direction}
focused={focused}
triggerFocus={this.focus}
bordered={bordered}
suffix={showCountSuffix}
/>
<FormItemStatusContext.Consumer>
{({ status: contextStatus, hasFeedback }) => {
const mergedStatus = contextStatus || customStatus;
return (
<ClearableLabeledInput
size={size}
{...this.props}
prefixCls={prefixCls}
inputType="input"
value={fixControlledValue(value)}
element={this.renderInput(prefixCls, size, bordered, mergedStatus, input)}
handleReset={this.handleReset}
ref={this.saveClearableInput}
direction={direction}
focused={focused}
triggerFocus={this.focus}
bordered={bordered}
suffix={this.renderSuffix(prefixCls, hasFeedback, mergedStatus)}
/>
);
}}
</FormItemStatusContext.Consumer>
)}
</SizeContext.Consumer>
);

View File

@ -1,13 +1,15 @@
import * as React from 'react';
import classNames from 'classnames';
import RcTextArea, { TextAreaProps as RcTextAreaProps } from 'rc-textarea';
import ResizableTextArea from 'rc-textarea/lib/ResizableTextArea';
import omit from 'rc-util/lib/omit';
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import ClearableLabeledInput from './ClearableLabeledInput';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import { ConfigContext } from '../config-provider';
import { fixControlledValue, resolveOnChange, triggerFocus, InputFocusOptions } from './Input';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
import { FormItemStatusContext } from '../form/context';
import { getFeedbackIcon, getStatusClassNames, InputStatus } from '../_util/statusUtils';
import ClearableLabeledInput from './ClearableLabeledInput';
import { fixControlledValue, InputFocusOptions, resolveOnChange, triggerFocus } from './Input';
interface ShowCountProps {
formatter: (args: { count: number; maxLength?: number }) => string;
@ -22,6 +24,7 @@ export interface TextAreaProps extends RcTextAreaProps {
bordered?: boolean;
showCount?: boolean | ShowCountProps;
size?: SizeType;
status?: InputStatus;
}
export interface TextAreaRef {
@ -43,6 +46,7 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
onCompositionStart,
onCompositionEnd,
onChange,
status: customStatus,
...props
},
ref,
@ -50,6 +54,9 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const size = React.useContext(SizeContext);
const { status: contextStatus, hasFeedback } = React.useContext(FormItemStatusContext);
const mergedStatus = contextStatus || customStatus;
const innerRef = React.useRef<RcTextArea>(null);
const clearableInputRef = React.useRef<ClearableLabeledInput>(null);
@ -124,12 +131,15 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
const textArea = (
<RcTextArea
{...omit(props, ['allowClear'])}
className={classNames({
[`${prefixCls}-borderless`]: !bordered,
[className!]: className && !showCount,
[`${prefixCls}-sm`]: size === 'small' || customizeSize === 'small',
[`${prefixCls}-lg`]: size === 'large' || customizeSize === 'large',
})}
className={classNames(
{
[`${prefixCls}-borderless`]: !bordered,
[className!]: className && !showCount,
[`${prefixCls}-sm`]: size === 'small' || customizeSize === 'small',
[`${prefixCls}-lg`]: size === 'large' || customizeSize === 'large',
},
getStatusClassNames(prefixCls, mergedStatus),
)}
style={showCount ? undefined : style}
prefixCls={prefixCls}
onCompositionStart={onInternalCompositionStart}
@ -158,12 +168,13 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
handleReset={handleReset}
ref={clearableInputRef}
bordered={bordered}
status={customStatus}
style={showCount ? undefined : style}
/>
);
// Only show text area wrapper when needed
if (showCount) {
if (showCount || hasFeedback) {
const valueLength = [...val].length;
let dataCount = '';
@ -180,14 +191,16 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
`${prefixCls}-textarea`,
{
[`${prefixCls}-textarea-rtl`]: direction === 'rtl',
[`${prefixCls}-textarea-show-count`]: showCount,
},
`${prefixCls}-textarea-show-count`,
getStatusClassNames(`${prefixCls}-textarea`, mergedStatus, hasFeedback),
className,
)}
style={style}
data-count={dataCount}
>
{textareaNode}
{hasFeedback && getFeedbackIcon(prefixCls, mergedStatus)}
</div>
);
}

View File

@ -9529,6 +9529,117 @@ Array [
]
`;
exports[`renders ./components/input/demo/status.md extend context correctly 1`] = `
<div
class="ant-space ant-space-vertical"
style="width:100%"
>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<input
class="ant-input ant-input-status-error"
placeholder="Error"
type="text"
value=""
/>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<input
class="ant-input ant-input-status-warning"
placeholder="Warning"
type="text"
value=""
/>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-error"
>
<span
class="ant-input-prefix"
>
<span
aria-label="clock-circle"
class="anticon anticon-clock-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="clock-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder="Error with prefix"
type="text"
value=""
/>
</span>
</div>
<div
class="ant-space-item"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-warning"
>
<span
class="ant-input-prefix"
>
<span
aria-label="clock-circle"
class="anticon anticon-clock-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="clock-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder="Warning with prefix"
type="text"
value=""
/>
</span>
</div>
</div>
`;
exports[`renders ./components/input/demo/textarea.md extend context correctly 1`] = `
<textarea
class="ant-input"

View File

@ -3303,6 +3303,117 @@ Array [
]
`;
exports[`renders ./components/input/demo/status.md correctly 1`] = `
<div
class="ant-space ant-space-vertical"
style="width:100%"
>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<input
class="ant-input ant-input-status-error"
placeholder="Error"
type="text"
value=""
/>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<input
class="ant-input ant-input-status-warning"
placeholder="Warning"
type="text"
value=""
/>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-error"
>
<span
class="ant-input-prefix"
>
<span
aria-label="clock-circle"
class="anticon anticon-clock-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="clock-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder="Error with prefix"
type="text"
value=""
/>
</span>
</div>
<div
class="ant-space-item"
>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-status-warning"
>
<span
class="ant-input-prefix"
>
<span
aria-label="clock-circle"
class="anticon anticon-clock-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="clock-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
/>
<path
d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder="Warning with prefix"
type="text"
value=""
/>
</span>
</div>
</div>
`;
exports[`renders ./components/input/demo/textarea.md correctly 1`] = `
<textarea
class="ant-input"

View File

@ -0,0 +1,30 @@
---
order: 19
title:
zh-CN: 自定义状态
en-US: Status
---
## zh-CN
使用 `status` 为 Input 添加状态。可选 `error` 或者 `warning`
## en-US
Add status to Input with `status`, which could be `error` or `warning`
```tsx
import { Input, Space } from 'antd';
import ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined';
const ValidateInputs: React.FC = () => (
<Space direction="vertical" style={{ width: '100%' }}>
<Input status="error" placeholder="Error" />
<Input status="warning" placeholder="Warning" />
<Input status="error" prefix={<ClockCircleOutlined />} placeholder="Error with prefix" />
<Input status="warning" prefix={<ClockCircleOutlined />} placeholder="Warning with prefix" />
</Space>
);
ReactDOM.render(<ValidateInputs />, mountNode);
```

View File

@ -27,6 +27,7 @@ A basic widget for getting the user input is a text field. Keyboard and mouse ca
| id | The ID for input | string | - | |
| maxLength | The max length | number | - | |
| showCount | Whether show text count | boolean \| { formatter: ({ count: number, maxLength?: number }) => ReactNode } | false | 4.18.0 |
| status | Set validation status | 'error' \| 'warning' | - | 4.19.0 |
| prefix | The prefix icon for the Input | ReactNode | - | |
| size | The size of the input box. Note: in the context of a form, the `large` size is used | `large` \| `middle` \| `small` | - | |
| suffix | The suffix icon for the Input | ReactNode | - | |

View File

@ -28,6 +28,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/xS9YEJhfe/Input.svg
| id | 输入框的 id | string | - | |
| maxLength | 最大长度 | number | - | |
| showCount | 是否展示字数 | boolean \| { formatter: ({ count: number, maxLength?: number }) => ReactNode } | false | 4.18.0 |
| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
| prefix | 带有前缀图标的 input | ReactNode | - | |
| size | 控件大小。注:标准表单内的输入框大小限制为 `large` | `large` \| `middle` \| `small` | - | |
| suffix | 带有后缀图标的 input | ReactNode | - | |

View File

@ -50,6 +50,10 @@
display: flex;
flex: none;
align-items: center;
> *:not(:last-child) {
margin-right: 8px;
}
}
&-show-count-suffix {

View File

@ -3,6 +3,7 @@
@import './mixin';
@import './affix';
@import './allow-clear';
@import './status';
@input-prefix-cls: ~'@{ant-prefix}-input';

View File

@ -1,5 +1,6 @@
import '../../style/index.less';
import './index.less';
// deps-lint-skip: form
// style dependencies
import '../../button/style';

View File

@ -0,0 +1,132 @@
@import './mixin';
@input-prefix-cls: ~'@{ant-prefix}-input';
.status-color(
@prefix-cls: @input-prefix-cls;
@text-color: @input-color;
@border-color: @input-border-color;
@background-color: @input-bg;
@hoverBorderColor: @primary-color-hover;
@outlineColor: @primary-color-outline;
) {
&:not(.@{prefix-cls}-disabled):not(.@{prefix-cls}-borderless).@{prefix-cls} {
&,
&:hover {
background: @background-color;
border-color: @border-color;
}
&:focus,
&-focused {
.active(@text-color, @hoverBorderColor, @outlineColor);
}
}
}
.status-color-common(
@prefix-cls: @input-prefix-cls;
@text-color: @input-color;
@border-color: @input-border-color;
@background-color: @input-bg;
@hoverBorderColor: @primary-color-hover;
@outlineColor: @primary-color-outline;
) {
.@{prefix-cls}-feedback-icon {
color: @text-color;
}
.@{prefix-cls}-prefix {
color: @text-color;
}
}
.group-status-color(
@prefix-cls: @input-prefix-cls;
@text-color: @input-color;
@border-color: @input-border-color;
) {
.@{prefix-cls}-group-addon {
color: @text-color;
border-color: @border-color;
}
}
@input-wrapper-cls: @input-prefix-cls, ~'@{input-prefix-cls}-affix-wrapper';
each(@input-wrapper-cls, {
.@{value} {
&-status-error {
.status-color(@value, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline);
.status-color-common(@input-prefix-cls, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline);
}
&-status-warning {
.status-color(@value, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
.status-color-common(@input-prefix-cls, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
}
}
});
.@{input-prefix-cls}-textarea,
.@{input-prefix-cls}-affix-wrapper {
&-status-validating {
.@{input-prefix-cls}-feedback-icon {
display: inline-block;
color: @primary-color;
}
}
&-status-success {
.@{input-prefix-cls}-feedback-icon {
color: @success-color;
animation-name: diffZoomIn1 !important;
}
}
}
.@{input-prefix-cls}-textarea {
&-status-error {
.@{input-prefix-cls}-feedback-icon {
color: @error-color;
}
}
&-status-warning {
.@{input-prefix-cls}-feedback-icon {
color: @warning-color;
}
}
.@{input-prefix-cls}-feedback-icon {
position: absolute;
top: 0;
right: @input-padding-horizontal-base;
bottom: 0;
z-index: 1;
display: inline-flex;
align-items: center;
margin: auto;
}
&-status-error,
&-status-warning,
&-status-success,
&-status-validating {
&.@{input-prefix-cls}-textarea-has-feedback {
.@{input-prefix-cls} {
padding-right: 24px;
}
}
}
}
.@{input-prefix-cls}-group-wrapper {
&-status-error {
.group-status-color(@input-prefix-cls, @error-color, @error-color);
}
&-status-warning {
.group-status-color(@input-prefix-cls, @warning-color, @warning-color);
}
}

View File

@ -1,8 +1,10 @@
import classNames from 'classnames';
import { ValidateStatus } from '../form/FormItem';
import type { DirectionType } from '../config-provider';
import type { SizeType } from '../config-provider/SizeContext';
import type { ClearableInputProps } from './ClearableLabeledInput';
import type { InputProps } from './Input';
import { getStatusClassNames } from '../_util/statusUtils';
export function getInputClassName(
prefixCls: string,
@ -10,14 +12,20 @@ export function getInputClassName(
size?: SizeType,
disabled?: boolean,
direction?: DirectionType,
status?: ValidateStatus,
hasFeedback?: boolean,
) {
return classNames(prefixCls, {
[`${prefixCls}-sm`]: size === 'small',
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-borderless`]: !bordered,
});
return classNames(
prefixCls,
{
[`${prefixCls}-sm`]: size === 'small',
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-borderless`]: !bordered,
},
getStatusClassNames(prefixCls, status, hasFeedback),
);
}
export function hasPrefixSuffix(props: InputProps | ClearableInputProps) {