ant-design/components/form/FormItem/ItemHolder.tsx
二货机器人 03ea767cad
fix: Form validate message shaking (#36575)
* refactor: move component out

* refactor: clean up code

* chore: stable it

* chore: back of container

* chore: adjust leave motion

* test: update snapshot

* chore: update motion logic

* fix: list start motion

* test: update snapshot

* test: update snapshot

* test: test for marginBottom
2022-07-19 16:01:31 +08:00

210 lines
6.3 KiB
TypeScript

import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
import classNames from 'classnames';
import * as React from 'react';
import omit from 'rc-util/lib/omit';
import type { Meta } from 'rc-field-form/lib/interface';
import { Row } from '../../grid';
import FormItemLabel from '../FormItemLabel';
import FormItemInput from '../FormItemInput';
import type { FormItemStatusContextProps, ReportMetaChange } from '../context';
import { FormContext, FormItemInputContext, NoStyleItemContext } from '../context';
import type { FormItemProps, ValidateStatus } from '.';
import useDebounce from '../hooks/useDebounce';
const iconMap = {
success: CheckCircleFilled,
warning: ExclamationCircleFilled,
error: CloseCircleFilled,
validating: LoadingOutlined,
};
export interface ItemHolderProps extends FormItemProps {
prefixCls: string;
className?: string;
style?: React.CSSProperties;
errors: React.ReactNode[];
warnings: React.ReactNode[];
meta: Meta;
children?: React.ReactNode;
fieldId?: string;
isRequired?: boolean;
onSubItemMetaChange: ReportMetaChange;
}
export default function ItemHolder(props: ItemHolderProps) {
const {
prefixCls,
className,
style,
help,
errors,
warnings,
validateStatus,
meta,
hasFeedback,
hidden,
children,
fieldId,
isRequired,
onSubItemMetaChange,
...restProps
} = props;
const itemPrefixCls = `${prefixCls}-item`;
const { requiredMark } = React.useContext(FormContext);
// ======================== Margin ========================
const itemRef = React.useRef<HTMLDivElement>(null);
const debounceErrors = useDebounce(errors);
const debounceWarnings = useDebounce(warnings);
const hasHelp = help !== undefined && help !== null;
const hasError = !!(hasHelp || errors.length || warnings.length);
const [marginBottom, setMarginBottom] = React.useState<number | null>(null);
useLayoutEffect(() => {
if (hasError && itemRef.current) {
const itemStyle = getComputedStyle(itemRef.current);
setMarginBottom(parseInt(itemStyle.marginBottom, 10));
}
}, [hasError]);
const onErrorVisibleChanged = (nextVisible: boolean) => {
if (!nextVisible) {
setMarginBottom(null);
}
};
// ======================== 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 = React.useMemo<FormItemStatusContextProps>(() => {
let feedbackIcon: React.ReactNode;
if (hasFeedback) {
const IconNode = mergedValidateStatus && iconMap[mergedValidateStatus];
feedbackIcon = IconNode ? (
<span
className={classNames(
`${itemPrefixCls}-feedback-icon`,
`${itemPrefixCls}-feedback-icon-${mergedValidateStatus}`,
)}
>
<IconNode />
</span>
) : null;
}
return {
status: mergedValidateStatus,
hasFeedback,
feedbackIcon,
isFormItemInput: true,
};
}, [mergedValidateStatus, hasFeedback]);
// ======================== Render ========================
const itemClassName = {
[itemPrefixCls]: true,
[`${itemPrefixCls}-with-help`]: hasHelp || debounceErrors.length || debounceWarnings.length,
[`${className}`]: !!className,
// Status
[`${itemPrefixCls}-has-feedback`]: mergedValidateStatus && hasFeedback,
[`${itemPrefixCls}-has-success`]: mergedValidateStatus === 'success',
[`${itemPrefixCls}-has-warning`]: mergedValidateStatus === 'warning',
[`${itemPrefixCls}-has-error`]: mergedValidateStatus === 'error',
[`${itemPrefixCls}-is-validating`]: mergedValidateStatus === 'validating',
[`${itemPrefixCls}-hidden`]: hidden,
};
return (
<div className={classNames(itemClassName)} style={style} ref={itemRef}>
<Row
className={`${itemPrefixCls}-row`}
{...omit(restProps, [
'_internalItemRender' as any,
'colon',
'dependencies',
'extra',
'fieldKey',
'getValueFromEvent',
'getValueProps',
'htmlFor',
'id', // It is deprecated because `htmlFor` is its replacement.
'initialValue',
'isListField',
'label',
'labelAlign',
'labelCol',
'labelWrap',
'messageVariables',
'name',
'normalize',
'noStyle',
'preserve',
'required',
'requiredMark',
'rules',
'shouldUpdate',
'trigger',
'tooltip',
'validateFirst',
'validateTrigger',
'valuePropName',
'wrapperCol',
])}
>
{/* Label */}
<FormItemLabel
htmlFor={fieldId}
required={isRequired}
requiredMark={requiredMark}
{...props}
prefixCls={prefixCls}
/>
{/* Input Group */}
<FormItemInput
{...props}
{...meta}
errors={debounceErrors}
warnings={debounceWarnings}
prefixCls={prefixCls}
status={mergedValidateStatus}
help={help}
marginBottom={marginBottom}
onErrorVisibleChanged={onErrorVisibleChanged}
>
<NoStyleItemContext.Provider value={onSubItemMetaChange}>
<FormItemInputContext.Provider value={formItemStatusContext}>
{children}
</FormItemInputContext.Provider>
</NoStyleItemContext.Provider>
</FormItemInput>
</Row>
{!!marginBottom && (
<div
className={`${itemPrefixCls}-margin-offset`}
style={{
marginBottom: -marginBottom,
}}
/>
)}
</div>
);
}