mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-07 17:44:35 +08:00
merge feature into master
This commit is contained in:
commit
8fe1cc9da5
@ -3,7 +3,10 @@ import { MotionEvent } from 'rc-motion/lib/interface';
|
||||
|
||||
// ================== Collapse Motion ==================
|
||||
const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 });
|
||||
const getRealHeight: MotionEventHandler = node => ({ height: node.scrollHeight, opacity: 1 });
|
||||
const getRealHeight: MotionEventHandler = node => {
|
||||
const { scrollHeight } = node;
|
||||
return { height: scrollHeight, opacity: 1 };
|
||||
};
|
||||
const getCurrentHeight: MotionEventHandler = node => ({ height: node.offsetHeight });
|
||||
const skipOpacityTransition: MotionEndEventHandler = (_, event: MotionEvent) =>
|
||||
event?.deadline === true || (event as TransitionEvent).propertyName === 'height';
|
||||
|
@ -13272,9 +13272,10 @@ exports[`ConfigProvider components Form configProvider 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="config-form-item-explain config-form-item-explain-error"
|
||||
class="config-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="config-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Bamboo is Light
|
||||
@ -13309,9 +13310,10 @@ exports[`ConfigProvider components Form configProvider componentSize large 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="config-form-item-explain config-form-item-explain-error"
|
||||
class="config-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="config-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Bamboo is Light
|
||||
@ -13346,9 +13348,10 @@ exports[`ConfigProvider components Form configProvider componentSize middle 1`]
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="config-form-item-explain config-form-item-explain-error"
|
||||
class="config-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="config-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Bamboo is Light
|
||||
@ -13383,9 +13386,10 @@ exports[`ConfigProvider components Form configProvider virtual and dropdownMatch
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Bamboo is Light
|
||||
@ -13420,9 +13424,10 @@ exports[`ConfigProvider components Form normal 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Bamboo is Light
|
||||
@ -13457,9 +13462,10 @@ exports[`ConfigProvider components Form prefixCls 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="prefix-Form-item-explain prefix-Form-item-explain-error"
|
||||
class="prefix-Form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="prefix-Form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Bamboo is Light
|
||||
|
@ -1,97 +1,114 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import CSSMotion from 'rc-motion';
|
||||
import useMemo from 'rc-util/lib/hooks/useMemo';
|
||||
import useCacheErrors from './hooks/useCacheErrors';
|
||||
import useForceUpdate from '../_util/hooks/useForceUpdate';
|
||||
import CSSMotion, { CSSMotionList } from 'rc-motion';
|
||||
import { FormItemPrefixContext } from './context';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import { ValidateStatus } from './FormItem';
|
||||
import collapseMotion from '../_util/motion';
|
||||
|
||||
const EMPTY_LIST: React.ReactNode[] = [];
|
||||
|
||||
interface ErrorEntity {
|
||||
error: React.ReactNode;
|
||||
errorStatus?: ValidateStatus;
|
||||
key: string;
|
||||
}
|
||||
|
||||
function toErrorEntity(
|
||||
error: React.ReactNode,
|
||||
errorStatus: ValidateStatus | undefined,
|
||||
prefix: string,
|
||||
index: number = 0,
|
||||
): ErrorEntity {
|
||||
return {
|
||||
key: typeof error === 'string' ? error : `${prefix}-${index}`,
|
||||
error,
|
||||
errorStatus,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ErrorListProps {
|
||||
errors?: React.ReactNode[];
|
||||
/** @private Internal Usage. Do not use in your production */
|
||||
help?: React.ReactNode;
|
||||
/** @private Internal Usage. Do not use in your production */
|
||||
onDomErrorVisibleChange?: (visible: boolean) => void;
|
||||
helpStatus?: ValidateStatus;
|
||||
errors?: React.ReactNode[];
|
||||
warnings?: React.ReactNode[];
|
||||
}
|
||||
|
||||
export default function ErrorList({
|
||||
errors = EMPTY_LIST,
|
||||
help,
|
||||
onDomErrorVisibleChange,
|
||||
helpStatus,
|
||||
errors = EMPTY_LIST,
|
||||
warnings = EMPTY_LIST,
|
||||
}: ErrorListProps) {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const { prefixCls, status } = React.useContext(FormItemPrefixContext);
|
||||
const { prefixCls } = React.useContext(FormItemPrefixContext);
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
|
||||
const [visible, cacheErrors] = useCacheErrors(
|
||||
errors,
|
||||
changedVisible => {
|
||||
if (changedVisible) {
|
||||
/**
|
||||
* We trigger in sync to avoid dom shaking but this get warning in react 16.13.
|
||||
*
|
||||
* So use Promise to keep in micro async to handle this.
|
||||
* https://github.com/ant-design/ant-design/issues/21698#issuecomment-593743485
|
||||
*/
|
||||
Promise.resolve().then(() => {
|
||||
onDomErrorVisibleChange?.(true);
|
||||
});
|
||||
}
|
||||
forceUpdate();
|
||||
},
|
||||
!!help,
|
||||
);
|
||||
|
||||
const memoErrors = useMemo(
|
||||
() => cacheErrors,
|
||||
visible,
|
||||
(_, nextVisible) => nextVisible,
|
||||
);
|
||||
|
||||
// Memo status in same visible
|
||||
const [innerStatus, setInnerStatus] = React.useState(status);
|
||||
React.useEffect(() => {
|
||||
if (visible && status) {
|
||||
setInnerStatus(status);
|
||||
}
|
||||
}, [visible, status]);
|
||||
|
||||
const baseClassName = `${prefixCls}-item-explain`;
|
||||
const rootPrefixCls = getPrefixCls();
|
||||
|
||||
const fullKeyList = React.useMemo(() => {
|
||||
if (help !== undefined && help !== null) {
|
||||
return [toErrorEntity(help, helpStatus, 'help')];
|
||||
}
|
||||
|
||||
return [
|
||||
...errors.map((error, index) => toErrorEntity(error, 'error', 'error', index)),
|
||||
...warnings.map((warning, index) => toErrorEntity(warning, 'warning', 'warning', index)),
|
||||
];
|
||||
}, [help, helpStatus, errors, warnings]);
|
||||
|
||||
return (
|
||||
<CSSMotion
|
||||
motionDeadline={500}
|
||||
visible={visible}
|
||||
{...collapseMotion}
|
||||
motionName={`${rootPrefixCls}-show-help`}
|
||||
onLeaveEnd={() => {
|
||||
onDomErrorVisibleChange?.(false);
|
||||
}}
|
||||
motionAppear
|
||||
motionAppear={false}
|
||||
motionEnter={false}
|
||||
motionLeave
|
||||
visible={!!fullKeyList.length}
|
||||
removeOnLeave
|
||||
onLeaveStart={node => {
|
||||
// Force disable css override style in index.less configured
|
||||
node.style.height = 'auto';
|
||||
return { height: node.offsetHeight };
|
||||
}}
|
||||
>
|
||||
{({ className: motionClassName }: { className?: string }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
baseClassName,
|
||||
{
|
||||
[`${baseClassName}-${innerStatus}`]: innerStatus,
|
||||
},
|
||||
motionClassName,
|
||||
)}
|
||||
key="help"
|
||||
>
|
||||
{memoErrors.map((error, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={index} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{holderProps => {
|
||||
const { className: holderClassName, style: holderStyle } = holderProps;
|
||||
|
||||
return (
|
||||
<div className={classNames(baseClassName, holderClassName)} style={holderStyle}>
|
||||
<CSSMotionList
|
||||
keys={fullKeyList}
|
||||
{...collapseMotion}
|
||||
motionName={`${rootPrefixCls}-show-help-item`}
|
||||
component={false}
|
||||
>
|
||||
{itemProps => {
|
||||
const {
|
||||
key,
|
||||
error,
|
||||
errorStatus,
|
||||
className: itemClassName,
|
||||
style: itemStyle,
|
||||
} = itemProps;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
role="alert"
|
||||
className={classNames(itemClassName, {
|
||||
[`${baseClassName}-${errorStatus}`]: errorStatus,
|
||||
})}
|
||||
style={itemStyle}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</CSSMotionList>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</CSSMotion>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useContext, useRef } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Field, FormInstance } from 'rc-field-form';
|
||||
import { FieldProps } from 'rc-field-form/lib/Field';
|
||||
@ -14,14 +13,20 @@ import { tuple } from '../_util/type';
|
||||
import devWarning from '../_util/devWarning';
|
||||
import FormItemLabel, { FormItemLabelProps, LabelTooltipType } from './FormItemLabel';
|
||||
import FormItemInput, { FormItemInputProps } from './FormItemInput';
|
||||
import { FormContext, FormItemContext } from './context';
|
||||
import { FormContext, NoStyleItemContext } from './context';
|
||||
import { toArray, getFieldId } from './util';
|
||||
import { cloneElement, isValidElement } from '../_util/reactNode';
|
||||
import useFrameState from './hooks/useFrameState';
|
||||
import useDebounce from './hooks/useDebounce';
|
||||
import useItemRef from './hooks/useItemRef';
|
||||
|
||||
const NAME_SPLIT = '__SPLIT__';
|
||||
|
||||
interface FieldError {
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
|
||||
export type ValidateStatus = typeof ValidateStatuses[number];
|
||||
|
||||
@ -31,7 +36,7 @@ type ChildrenType<Values = any> = RenderChildren<Values> | React.ReactNode;
|
||||
|
||||
interface MemoInputProps {
|
||||
value: any;
|
||||
update: number;
|
||||
update: any;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@ -68,6 +73,16 @@ function hasValidName(name?: NamePath): Boolean {
|
||||
return !(name === undefined || name === null);
|
||||
}
|
||||
|
||||
function genEmptyMeta(): Meta {
|
||||
return {
|
||||
errors: [],
|
||||
warnings: [],
|
||||
touched: false,
|
||||
validating: false,
|
||||
name: [],
|
||||
};
|
||||
}
|
||||
|
||||
function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElement {
|
||||
const {
|
||||
name,
|
||||
@ -91,104 +106,109 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
|
||||
hidden,
|
||||
...restProps
|
||||
} = props;
|
||||
const destroyRef = useRef(false);
|
||||
const { getPrefixCls } = useContext(ConfigContext);
|
||||
const { name: formName, requiredMark } = useContext(FormContext);
|
||||
const { updateItemErrors } = useContext(FormItemContext);
|
||||
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
|
||||
const [inlineErrors, setInlineErrors] = useFrameState<Record<string, string[]>>({});
|
||||
const isRenderProps = typeof children === 'function';
|
||||
const notifyParentMetaChange = useContext(NoStyleItemContext);
|
||||
|
||||
const { validateTrigger: contextValidateTrigger } = useContext(FieldContext);
|
||||
const mergedValidateTrigger =
|
||||
validateTrigger !== undefined ? validateTrigger : contextValidateTrigger;
|
||||
|
||||
function setDomErrorVisible(visible: boolean) {
|
||||
if (!destroyRef.current) {
|
||||
innerSetDomErrorVisible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
const hasName = hasValidName(name);
|
||||
|
||||
// Cache Field NamePath
|
||||
const nameRef = useRef<(string | number)[]>([]);
|
||||
|
||||
// Should clean up if Field removed
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
destroyRef.current = true;
|
||||
updateItemErrors(nameRef.current.join(NAME_SPLIT), []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const prefixCls = getPrefixCls('form', customizePrefixCls);
|
||||
|
||||
// ======================== Errors ========================
|
||||
// Collect noStyle Field error to the top FormItem
|
||||
const updateChildItemErrors = noStyle
|
||||
? updateItemErrors
|
||||
: (subName: string, subErrors: string[], originSubName?: string) => {
|
||||
setInlineErrors((prevInlineErrors = {}) => {
|
||||
// Clean up origin error when name changed
|
||||
if (originSubName && originSubName !== subName) {
|
||||
delete prevInlineErrors[originSubName];
|
||||
}
|
||||
// >>>>> Collect sub field errors
|
||||
const [subFieldErrors, setSubFieldErrors] = useFrameState<Record<string, FieldError>>({});
|
||||
|
||||
if (!isEqual(prevInlineErrors[subName], subErrors)) {
|
||||
return {
|
||||
...prevInlineErrors,
|
||||
[subName]: subErrors,
|
||||
};
|
||||
}
|
||||
return prevInlineErrors;
|
||||
});
|
||||
// >>>>> Current field errors
|
||||
const [meta, setMeta] = React.useState<Meta>(() => genEmptyMeta());
|
||||
|
||||
const onMetaChange = (nextMeta: Meta & { destroy?: boolean }) => {
|
||||
// Destroy will reset all the meta
|
||||
setMeta(nextMeta.destroy ? genEmptyMeta() : nextMeta);
|
||||
|
||||
// Bump to parent since noStyle
|
||||
if (noStyle && notifyParentMetaChange) {
|
||||
let namePath = nextMeta.name;
|
||||
if (fieldKey !== undefined) {
|
||||
namePath = Array.isArray(fieldKey) ? fieldKey : [fieldKey!];
|
||||
}
|
||||
notifyParentMetaChange(nextMeta, namePath);
|
||||
}
|
||||
};
|
||||
|
||||
// >>>>> Collect noStyle Field error to the top FormItem
|
||||
const onSubItemMetaChange = (subMeta: Meta & { destroy: boolean }, uniqueKeys: React.Key[]) => {
|
||||
// Only `noStyle` sub item will trigger
|
||||
setSubFieldErrors(prevSubFieldErrors => {
|
||||
const clone = {
|
||||
...prevSubFieldErrors,
|
||||
};
|
||||
|
||||
// name: ['user', 1] + key: [4] = ['user', 4]
|
||||
const mergedNamePath = [...subMeta.name.slice(0, -1), ...uniqueKeys];
|
||||
const mergedNameKey = mergedNamePath.join(NAME_SPLIT);
|
||||
|
||||
if (subMeta.destroy) {
|
||||
// Remove
|
||||
delete clone[mergedNameKey];
|
||||
} else {
|
||||
// Update
|
||||
clone[mergedNameKey] = subMeta;
|
||||
}
|
||||
|
||||
return clone;
|
||||
});
|
||||
};
|
||||
|
||||
// >>>>> Get merged errors
|
||||
const [mergedErrors, mergedWarnings] = React.useMemo(() => {
|
||||
const errorList: string[] = [...meta.errors];
|
||||
const warningList: string[] = [...meta.warnings];
|
||||
|
||||
Object.values(subFieldErrors).forEach(subFieldError => {
|
||||
errorList.push(...(subFieldError.errors || []));
|
||||
warningList.push(...(subFieldError.warnings || []));
|
||||
});
|
||||
|
||||
return [errorList, warningList];
|
||||
}, [subFieldErrors, meta.errors, meta.warnings]);
|
||||
|
||||
const debounceErrors = useDebounce(mergedErrors);
|
||||
const debounceWarnings = useDebounce(mergedWarnings);
|
||||
|
||||
// ===================== Children Ref =====================
|
||||
const getItemRef = useItemRef();
|
||||
|
||||
// ======================== Render ========================
|
||||
function renderLayout(
|
||||
baseChildren: React.ReactNode,
|
||||
fieldId?: string,
|
||||
meta?: Meta,
|
||||
isRequired?: boolean,
|
||||
): React.ReactNode {
|
||||
if (noStyle && !hidden) {
|
||||
return baseChildren;
|
||||
}
|
||||
|
||||
// ======================== Errors ========================
|
||||
// >>> collect sub errors
|
||||
let subErrorList: string[] = [];
|
||||
Object.keys(inlineErrors).forEach(subName => {
|
||||
subErrorList = [...subErrorList, ...(inlineErrors[subName] || [])];
|
||||
});
|
||||
|
||||
// >>> merged errors
|
||||
let mergedErrors: React.ReactNode[];
|
||||
if (help !== undefined && help !== null) {
|
||||
mergedErrors = toArray(help);
|
||||
} else {
|
||||
mergedErrors = meta ? meta.errors : [];
|
||||
mergedErrors = [...mergedErrors, ...subErrorList];
|
||||
}
|
||||
|
||||
// ======================== Status ========================
|
||||
let mergedValidateStatus: ValidateStatus = '';
|
||||
if (validateStatus !== undefined) {
|
||||
mergedValidateStatus = validateStatus;
|
||||
} else if (meta?.validating) {
|
||||
mergedValidateStatus = 'validating';
|
||||
} else if (meta?.errors?.length || subErrorList.length) {
|
||||
} else if (debounceErrors.length) {
|
||||
mergedValidateStatus = 'error';
|
||||
} else if (debounceWarnings.length) {
|
||||
mergedValidateStatus = 'warning';
|
||||
} else if (meta?.touched) {
|
||||
mergedValidateStatus = 'success';
|
||||
}
|
||||
|
||||
const itemClassName = {
|
||||
[`${prefixCls}-item`]: true,
|
||||
[`${prefixCls}-item-with-help`]: domErrorVisible || !!help,
|
||||
[`${prefixCls}-item-with-help`]: help || debounceErrors.length || debounceWarnings.length,
|
||||
[`${className}`]: !!className,
|
||||
|
||||
// Status
|
||||
@ -238,26 +258,21 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
|
||||
<FormItemInput
|
||||
{...props}
|
||||
{...meta}
|
||||
errors={mergedErrors}
|
||||
errors={debounceErrors}
|
||||
warnings={debounceWarnings}
|
||||
prefixCls={prefixCls}
|
||||
status={mergedValidateStatus}
|
||||
onDomErrorVisibleChange={setDomErrorVisible}
|
||||
validateStatus={mergedValidateStatus}
|
||||
help={help}
|
||||
>
|
||||
<FormItemContext.Provider value={{ updateItemErrors: updateChildItemErrors }}>
|
||||
<NoStyleItemContext.Provider value={onSubItemMetaChange}>
|
||||
{baseChildren}
|
||||
</FormItemContext.Provider>
|
||||
</NoStyleItemContext.Provider>
|
||||
</FormItemInput>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const isRenderProps = typeof children === 'function';
|
||||
|
||||
// Record for real component render
|
||||
const updateRef = useRef(0);
|
||||
updateRef.current += 1;
|
||||
|
||||
if (!hasName && !isRenderProps && !dependencies) {
|
||||
return renderLayout(children) as JSX.Element;
|
||||
}
|
||||
@ -270,46 +285,31 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
|
||||
variables = { ...variables, ...messageVariables };
|
||||
}
|
||||
|
||||
// >>>>> With Field
|
||||
return (
|
||||
<Field
|
||||
{...props}
|
||||
messageVariables={variables}
|
||||
trigger={trigger}
|
||||
validateTrigger={mergedValidateTrigger}
|
||||
onReset={() => {
|
||||
setDomErrorVisible(false);
|
||||
}}
|
||||
onMetaChange={onMetaChange}
|
||||
>
|
||||
{(control, meta, context) => {
|
||||
const { errors } = meta;
|
||||
|
||||
const mergedName = toArray(name).length && meta ? meta.name : [];
|
||||
{(control, renderMeta, context) => {
|
||||
const mergedName = toArray(name).length && renderMeta ? renderMeta.name : [];
|
||||
const fieldId = getFieldId(mergedName, formName);
|
||||
|
||||
if (noStyle) {
|
||||
// Clean up origin one
|
||||
const originErrorName = nameRef.current.join(NAME_SPLIT);
|
||||
|
||||
nameRef.current = [...mergedName];
|
||||
if (fieldKey) {
|
||||
const fieldKeys = Array.isArray(fieldKey) ? fieldKey : [fieldKey];
|
||||
nameRef.current = [...mergedName.slice(0, -1), ...fieldKeys];
|
||||
}
|
||||
updateItemErrors(nameRef.current.join(NAME_SPLIT), errors, originErrorName);
|
||||
}
|
||||
|
||||
const isRequired =
|
||||
required !== undefined
|
||||
? required
|
||||
: !!(
|
||||
rules &&
|
||||
rules.some(rule => {
|
||||
if (rule && typeof rule === 'object' && rule.required) {
|
||||
if (rule && typeof rule === 'object' && rule.required && !rule.warningOnly) {
|
||||
return true;
|
||||
}
|
||||
if (typeof rule === 'function') {
|
||||
const ruleEntity = rule(context);
|
||||
return ruleEntity && ruleEntity.required;
|
||||
return ruleEntity && ruleEntity.required && !ruleEntity.warningOnly;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
@ -377,10 +377,7 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
|
||||
});
|
||||
|
||||
childNode = (
|
||||
<MemoInput
|
||||
value={mergedControl[props.valuePropName || 'value']}
|
||||
update={updateRef.current}
|
||||
>
|
||||
<MemoInput value={mergedControl[props.valuePropName || 'value']} update={children}>
|
||||
{cloneElement(children, childProps)}
|
||||
</MemoInput>
|
||||
);
|
||||
@ -395,7 +392,7 @@ function FormItem<Values = any>(props: FormItemProps<Values>): React.ReactElemen
|
||||
childNode = children;
|
||||
}
|
||||
|
||||
return renderLayout(childNode, fieldId, meta, isRequired);
|
||||
return renderLayout(childNode, fieldId, isRequired);
|
||||
}}
|
||||
</Field>
|
||||
);
|
||||
|
@ -14,9 +14,9 @@ interface FormItemInputMiscProps {
|
||||
prefixCls: string;
|
||||
children: React.ReactNode;
|
||||
errors: React.ReactNode[];
|
||||
warnings: React.ReactNode[];
|
||||
hasFeedback?: boolean;
|
||||
validateStatus?: ValidateStatus;
|
||||
onDomErrorVisibleChange: (visible: boolean) => void;
|
||||
/** @private Internal Usage, do not use in any of your production. */
|
||||
_internalItemRender?: {
|
||||
mark: string;
|
||||
@ -33,9 +33,9 @@ interface FormItemInputMiscProps {
|
||||
|
||||
export interface FormItemInputProps {
|
||||
wrapperCol?: ColProps;
|
||||
help?: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
status?: ValidateStatus;
|
||||
help?: React.ReactNode;
|
||||
}
|
||||
|
||||
const iconMap: { [key: string]: any } = {
|
||||
@ -51,13 +51,13 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = pro
|
||||
status,
|
||||
wrapperCol,
|
||||
children,
|
||||
help,
|
||||
errors,
|
||||
onDomErrorVisibleChange,
|
||||
warnings,
|
||||
hasFeedback,
|
||||
_internalItemRender: formItemRender,
|
||||
validateStatus,
|
||||
extra,
|
||||
help,
|
||||
} = props;
|
||||
const baseClassName = `${prefixCls}-item`;
|
||||
|
||||
@ -67,13 +67,6 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = pro
|
||||
|
||||
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className);
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
onDomErrorVisibleChange(false);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Should provides additional icon if `hasFeedback`
|
||||
const IconNode = validateStatus && iconMap[validateStatus];
|
||||
const icon =
|
||||
@ -96,7 +89,7 @@ const FormItemInput: React.FC<FormItemInputProps & FormItemInputMiscProps> = pro
|
||||
);
|
||||
const errorListDom = (
|
||||
<FormItemPrefixContext.Provider value={{ prefixCls, status }}>
|
||||
<ErrorList errors={errors} help={help} onDomErrorVisibleChange={onDomErrorVisibleChange} />
|
||||
<ErrorList errors={errors} warnings={warnings} help={help} helpStatus={status} />
|
||||
</FormItemPrefixContext.Provider>
|
||||
);
|
||||
|
||||
|
@ -25,7 +25,7 @@ export interface FormListProps {
|
||||
children: (
|
||||
fields: FormListFieldData[],
|
||||
operation: FormListOperation,
|
||||
meta: { errors: React.ReactNode[] },
|
||||
meta: { errors: React.ReactNode[]; warnings: React.ReactNode[] },
|
||||
) => React.ReactNode;
|
||||
}
|
||||
|
||||
@ -48,6 +48,7 @@ const FormList: React.FC<FormListProps> = ({
|
||||
operation,
|
||||
{
|
||||
errors: meta.errors,
|
||||
warnings: meta.warnings,
|
||||
},
|
||||
)}
|
||||
</FormItemPrefixContext.Provider>
|
||||
|
@ -306,6 +306,7 @@ exports[`renders ./components/form/demo/advanced-search.md correctly 1`] = `
|
||||
|
||||
exports[`renders ./components/form/demo/basic.md correctly 1`] = `
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ant-form ant-form-horizontal"
|
||||
id="basic"
|
||||
>
|
||||
@ -1073,9 +1074,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Buggy!
|
||||
@ -1115,9 +1117,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Buggy!
|
||||
@ -1188,9 +1191,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Buggy!
|
||||
@ -1230,9 +1234,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Buggy!
|
||||
@ -1329,9 +1334,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Buggy!
|
||||
@ -1384,9 +1390,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Buggy!
|
||||
@ -1475,9 +1482,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Buggy!
|
||||
@ -1526,9 +1534,10 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Buggy!
|
||||
@ -6472,9 +6481,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Should be combination of numbers & alphabets
|
||||
@ -6597,9 +6607,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-validating"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-validating"
|
||||
role="alert"
|
||||
>
|
||||
The information is being validated...
|
||||
@ -6774,9 +6785,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Should be combination of numbers & alphabets
|
||||
@ -7154,9 +7166,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-validating"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-validating"
|
||||
role="alert"
|
||||
>
|
||||
The information is being validated...
|
||||
@ -7242,9 +7255,10 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item-explain ant-form-item-explain-error"
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-explain-error"
|
||||
role="alert"
|
||||
>
|
||||
Please select the correct date
|
||||
@ -7720,6 +7734,97 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/form/demo/warning-only.md correctly 1`] = `
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ant-form ant-form-vertical"
|
||||
>
|
||||
<div
|
||||
style="overflow:hidden"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="url"
|
||||
title="URL"
|
||||
>
|
||||
URL
|
||||
</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"
|
||||
>
|
||||
<input
|
||||
class="ant-input"
|
||||
id="url"
|
||||
placeholder="input placeholder"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-control"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
<button
|
||||
class="ant-btn ant-btn-primary"
|
||||
type="submit"
|
||||
>
|
||||
<span>
|
||||
Submit
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Fill
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/form/demo/without-form-create.md correctly 1`] = `
|
||||
<form
|
||||
class="ant-form ant-form-horizontal"
|
||||
@ -7828,6 +7933,7 @@ exports[`renders ./components/form/demo/without-form-create.md correctly 1`] = `
|
||||
class="ant-form-item-explain"
|
||||
>
|
||||
<div
|
||||
class=""
|
||||
role="alert"
|
||||
>
|
||||
A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import Form from '..';
|
||||
import Input from '../../input';
|
||||
@ -20,10 +21,17 @@ describe('Form', () => {
|
||||
scrollIntoView.mockImplementation(() => {});
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
async function change(wrapper, index, value) {
|
||||
async function change(wrapper, index, value, executeMockTimer) {
|
||||
wrapper.find(Input).at(index).simulate('change', { target: { value } });
|
||||
await sleep(200);
|
||||
wrapper.update();
|
||||
|
||||
if (executeMockTimer) {
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
wrapper.update();
|
||||
});
|
||||
await sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@ -42,6 +50,8 @@ describe('Form', () => {
|
||||
|
||||
describe('noStyle Form.Item', () => {
|
||||
it('work', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
@ -54,14 +64,18 @@ describe('Form', () => {
|
||||
</Form>,
|
||||
);
|
||||
|
||||
await change(wrapper, 0, '');
|
||||
await change(wrapper, 0, '', true);
|
||||
expect(wrapper.find('.ant-form-item-with-help').length).toBeTruthy();
|
||||
expect(wrapper.find('.ant-form-item-has-error').length).toBeTruthy();
|
||||
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should clean up', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const Demo = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
@ -105,12 +119,14 @@ describe('Form', () => {
|
||||
};
|
||||
|
||||
const wrapper = mount(<Demo />);
|
||||
await change(wrapper, 0, '1');
|
||||
await change(wrapper, 0, '1', true);
|
||||
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('aaa');
|
||||
await change(wrapper, 0, '2');
|
||||
await change(wrapper, 0, '2', true);
|
||||
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('ccc');
|
||||
await change(wrapper, 0, '1');
|
||||
await change(wrapper, 0, '1', true);
|
||||
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('aaa');
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@ -315,6 +331,8 @@ describe('Form', () => {
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/20706
|
||||
it('Error change should work', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const wrapper = mount(
|
||||
<Form>
|
||||
<Form.Item
|
||||
@ -338,15 +356,17 @@ describe('Form', () => {
|
||||
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
await change(wrapper, 0, '');
|
||||
await change(wrapper, 0, '', true);
|
||||
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual("'name' is required");
|
||||
|
||||
await change(wrapper, 0, 'p');
|
||||
await change(wrapper, 0, 'p', true);
|
||||
await sleep(100);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('.ant-form-item-explain').first().text()).toEqual('not a p');
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/20813
|
||||
@ -428,6 +448,8 @@ describe('Form', () => {
|
||||
});
|
||||
|
||||
it('Form.Item with `help` should display error style when validate failed', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const wrapper = mount(
|
||||
<Form>
|
||||
<Form.Item name="test" help="help" rules={[{ required: true, message: 'message' }]}>
|
||||
@ -436,12 +458,16 @@ describe('Form', () => {
|
||||
</Form>,
|
||||
);
|
||||
|
||||
await change(wrapper, 0, '');
|
||||
await change(wrapper, 0, '', true);
|
||||
expect(wrapper.find('.ant-form-item').first().hasClass('ant-form-item-has-error')).toBeTruthy();
|
||||
expect(wrapper.find('.ant-form-item-explain').text()).toEqual('help');
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('clear validation message when ', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const wrapper = mount(
|
||||
<Form>
|
||||
<Form.Item name="username" rules={[{ required: true, message: 'message' }]}>
|
||||
@ -449,14 +475,18 @@ describe('Form', () => {
|
||||
</Form.Item>
|
||||
</Form>,
|
||||
);
|
||||
await change(wrapper, 0, '1');
|
||||
await change(wrapper, 0, '1', true);
|
||||
expect(wrapper.find('.ant-form-item-explain').length).toBeFalsy();
|
||||
await change(wrapper, 0, '');
|
||||
|
||||
await change(wrapper, 0, '', true);
|
||||
expect(wrapper.find('.ant-form-item-explain').length).toBeTruthy();
|
||||
await change(wrapper, 0, '123');
|
||||
|
||||
await change(wrapper, 0, '123', true);
|
||||
await sleep(800);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('.ant-form-item-explain').length).toBeFalsy();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/21167
|
||||
|
@ -200,34 +200,6 @@ describe('Form.List', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('ErrorList component', () => {
|
||||
it('should trigger onDomErrorVisibleChange by motion end', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const onDomErrorVisibleChange = jest.fn();
|
||||
const wrapper = mount(
|
||||
<Form.ErrorList
|
||||
errors={['bamboo is light']}
|
||||
onDomErrorVisibleChange={onDomErrorVisibleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await sleep();
|
||||
jest.runAllTimers();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper.find('CSSMotion').props().onLeaveEnd();
|
||||
});
|
||||
|
||||
expect(onDomErrorVisibleChange).toHaveBeenCalledWith(false);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render empty without errors', () => {
|
||||
const wrapper = mount(<Form.ErrorList />);
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import omit from 'rc-util/lib/omit';
|
||||
import { Meta } from 'rc-field-form/lib/interface';
|
||||
import { FormProvider as RcFormProvider } from 'rc-field-form';
|
||||
import { FormProviderProps as RcFormProviderProps } from 'rc-field-form/lib/FormContext';
|
||||
import { ColProps } from '../grid/col';
|
||||
@ -25,14 +26,9 @@ export const FormContext = React.createContext<FormContextProps>({
|
||||
itemRef: (() => {}) as any,
|
||||
});
|
||||
|
||||
/** Form Item Context. Used for Form noStyle Item error collection */
|
||||
export interface FormItemContextProps {
|
||||
updateItemErrors: (name: string, errors: string[], originName?: string) => void;
|
||||
}
|
||||
|
||||
export const FormItemContext = React.createContext<FormItemContextProps>({
|
||||
updateItemErrors: () => {},
|
||||
});
|
||||
/** `noStyle` Form Item Context. Used for error collection */
|
||||
export type ReportMetaChange = (meta: Meta, uniqueKeys: React.Key[]) => void;
|
||||
export const NoStyleItemContext = React.createContext<ReportMetaChange | null>(null);
|
||||
|
||||
/** Form Provider */
|
||||
export interface FormProviderProps extends Omit<RcFormProviderProps, 'validateMessages'> {
|
||||
|
@ -40,6 +40,7 @@ const Demo = () => {
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
label="Username"
|
||||
|
73
components/form/demo/warning-only.md
Normal file
73
components/form/demo/warning-only.md
Normal file
@ -0,0 +1,73 @@
|
||||
---
|
||||
order: 3.2
|
||||
title:
|
||||
zh-CN: 非阻塞校验
|
||||
en-US: No block rule
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
`rule` 添加 `warningOnly` 后校验不再阻塞表单提交。
|
||||
|
||||
## en-US
|
||||
|
||||
`rule` with `warningOnly` will not block form submit.
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Form, Input, message, Button, Space } from 'antd';
|
||||
|
||||
const Demo = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = () => {
|
||||
message.success('Submit success!');
|
||||
};
|
||||
|
||||
const onFinishFailed = () => {
|
||||
message.error('Submit failed!');
|
||||
};
|
||||
|
||||
const onFill = () => {
|
||||
form.setFieldsValue({
|
||||
url: 'https://taobao.com/',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<Form.Item
|
||||
name="url"
|
||||
label="URL"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ type: 'url', warningOnly: true },
|
||||
{ type: 'string', min: 6 },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="input placeholder" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<Button htmlType="button" onClick={onFill}>
|
||||
Fill
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Demo />, mountNode);
|
||||
```
|
@ -1,47 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import useForceUpdate from '../../_util/hooks/useForceUpdate';
|
||||
|
||||
/** Always debounce error to avoid [error -> null -> error] blink */
|
||||
export default function useCacheErrors(
|
||||
errors: React.ReactNode[],
|
||||
changeTrigger: (visible: boolean) => void,
|
||||
directly: boolean,
|
||||
): [boolean, React.ReactNode[]] {
|
||||
const cacheRef = React.useRef({
|
||||
errors,
|
||||
visible: !!errors.length,
|
||||
});
|
||||
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const update = () => {
|
||||
const prevVisible = cacheRef.current.visible;
|
||||
const newVisible = !!errors.length;
|
||||
|
||||
const prevErrors = cacheRef.current.errors;
|
||||
cacheRef.current.errors = errors;
|
||||
cacheRef.current.visible = newVisible;
|
||||
|
||||
if (prevVisible !== newVisible) {
|
||||
changeTrigger(newVisible);
|
||||
} else if (
|
||||
prevErrors.length !== errors.length ||
|
||||
prevErrors.some((prevErr, index) => prevErr !== errors[index])
|
||||
) {
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!directly) {
|
||||
const timeout = setTimeout(update, 10);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
if (directly) {
|
||||
update();
|
||||
}
|
||||
|
||||
return [cacheRef.current.visible, cacheRef.current.errors];
|
||||
}
|
19
components/form/hooks/useDebounce.ts
Normal file
19
components/form/hooks/useDebounce.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default function useDebounce<T>(value: T[]): T[] {
|
||||
const [cacheValue, setCacheValue] = React.useState(value);
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
setCacheValue(value);
|
||||
},
|
||||
value.length ? 0 : 10,
|
||||
);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [value]);
|
||||
|
||||
return cacheValue;
|
||||
}
|
@ -302,22 +302,23 @@ Rule supports a config object, or a function returning config object:
|
||||
type Rule = RuleConfig | ((form: FormInstance) => RuleConfig);
|
||||
```
|
||||
|
||||
| Name | Description | Type |
|
||||
| --- | --- | --- |
|
||||
| defaultField | Validate rule for all array elements, valid when `type` is `array` | [rule](#Rule) |
|
||||
| enum | Match enum value. You need to set `type` to `enum` to enable this | any\[] |
|
||||
| fields | Validate rule for child elements, valid when `type` is `array` or `object` | Record<string, [rule](#Rule)> |
|
||||
| len | Length of string, number, array | number |
|
||||
| max | `type` required: max length of `string`, `number`, `array` | number |
|
||||
| message | Error message. Will auto generate by [template](#validateMessages) if not provided | string |
|
||||
| min | `type` required: min length of `string`, `number`, `array` | number |
|
||||
| pattern | Regex pattern | RegExp |
|
||||
| required | Required field | boolean |
|
||||
| transform | Transform value to the rule before validation | (value) => any |
|
||||
| type | Normally `string` \|`number` \|`boolean` \|`url` \| `email`. More type to ref [here](https://github.com/yiminghe/async-validator#type) | string |
|
||||
| validateTrigger | Set validate trigger event. Must be the sub set of `validateTrigger` in Form.Item | string \| string\[] |
|
||||
| validator | Customize validation rule. Accept Promise as return. See [example](#components-form-demo-register) | ([rule](#Rule), value) => Promise |
|
||||
| whitespace | Failed if only has whitespace | boolean |
|
||||
| Name | Description | Type | Version |
|
||||
| --- | --- | --- | --- |
|
||||
| defaultField | Validate rule for all array elements, valid when `type` is `array` | [rule](#Rule) | |
|
||||
| enum | Match enum value. You need to set `type` to `enum` to enable this | any\[] | |
|
||||
| fields | Validate rule for child elements, valid when `type` is `array` or `object` | Record<string, [rule](#Rule)> | |
|
||||
| len | Length of string, number, array | number | |
|
||||
| max | `type` required: max length of `string`, `number`, `array` | number | |
|
||||
| message | Error message. Will auto generate by [template](#validateMessages) if not provided | string | |
|
||||
| min | `type` required: min length of `string`, `number`, `array` | number | |
|
||||
| pattern | Regex pattern | RegExp | |
|
||||
| required | Required field | boolean | |
|
||||
| transform | Transform value to the rule before validation | (value) => any | |
|
||||
| type | Normally `string` \|`number` \|`boolean` \|`url` \| `email`. More type to ref [here](https://github.com/yiminghe/async-validator#type) | string | |
|
||||
| validateTrigger | Set validate trigger event. Must be the sub set of `validateTrigger` in Form.Item | string \| string\[] | |
|
||||
| validator | Customize validation rule. Accept Promise as return. See [example](#components-form-demo-register) | ([rule](#Rule), value) => Promise | |
|
||||
| warningOnly | Warning only. Not block form submit | boolean | 4.17.0 |
|
||||
| whitespace | Failed if only has whitespace | boolean | |
|
||||
|
||||
## Migrate to v4
|
||||
|
||||
|
@ -301,22 +301,23 @@ Rule 支持接收 object 进行配置,也支持 function 来动态获取 form
|
||||
type Rule = RuleConfig | ((form: FormInstance) => RuleConfig);
|
||||
```
|
||||
|
||||
| 名称 | 说明 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| defaultField | 仅在 `type` 为 `array` 类型时有效,用于指定数组元素的校验规则 | [rule](#Rule) |
|
||||
| enum | 是否匹配枚举中的值(需要将 `type` 设置为 `enum`) | any\[] |
|
||||
| fields | 仅在 `type` 为 `array` 或 `object` 类型时有效,用于指定子元素的校验规则 | Record<string, [rule](#Rule)> |
|
||||
| len | string 类型时为字符串长度;number 类型时为确定数字; array 类型时为数组长度 | number |
|
||||
| max | 必须设置 `type`:string 类型为字符串最大长度;number 类型时为最大值;array 类型时为数组最大长度 | number |
|
||||
| message | 错误信息,不设置时会通过[模板](#validateMessages)自动生成 | string |
|
||||
| min | 必须设置 `type`:string 类型为字符串最小长度;number 类型时为最小值;array 类型时为数组最小长度 | number |
|
||||
| pattern | 正则表达式匹配 | RegExp |
|
||||
| required | 是否为必选字段 | boolean |
|
||||
| transform | 将字段值转换成目标值后进行校验 | (value) => any |
|
||||
| type | 类型,常见有 `string` \|`number` \|`boolean` \|`url` \| `email`。更多请参考[此处](https://github.com/yiminghe/async-validator#type) | string |
|
||||
| validateTrigger | 设置触发验证时机,必须是 Form.Item 的 `validateTrigger` 的子集 | string \| string\[] |
|
||||
| validator | 自定义校验,接收 Promise 作为返回值。[示例](#components-form-demo-register)参考 | ([rule](#Rule), value) => Promise |
|
||||
| whitespace | 如果字段仅包含空格则校验不通过 | boolean |
|
||||
| 名称 | 说明 | 类型 | 版本 |
|
||||
| --- | --- | --- | --- |
|
||||
| defaultField | 仅在 `type` 为 `array` 类型时有效,用于指定数组元素的校验规则 | [rule](#Rule) | |
|
||||
| enum | 是否匹配枚举中的值(需要将 `type` 设置为 `enum`) | any\[] | |
|
||||
| fields | 仅在 `type` 为 `array` 或 `object` 类型时有效,用于指定子元素的校验规则 | Record<string, [rule](#Rule)> | |
|
||||
| len | string 类型时为字符串长度;number 类型时为确定数字; array 类型时为数组长度 | number | |
|
||||
| max | 必须设置 `type`:string 类型为字符串最大长度;number 类型时为最大值;array 类型时为数组最大长度 | number | |
|
||||
| message | 错误信息,不设置时会通过[模板](#validateMessages)自动生成 | string | |
|
||||
| min | 必须设置 `type`:string 类型为字符串最小长度;number 类型时为最小值;array 类型时为数组最小长度 | number | |
|
||||
| pattern | 正则表达式匹配 | RegExp | |
|
||||
| required | 是否为必选字段 | boolean | |
|
||||
| transform | 将字段值转换成目标值后进行校验 | (value) => any | |
|
||||
| type | 类型,常见有 `string` \|`number` \|`boolean` \|`url` \| `email`。更多请参考[此处](https://github.com/yiminghe/async-validator#type) | string | |
|
||||
| validateTrigger | 设置触发验证时机,必须是 Form.Item 的 `validateTrigger` 的子集 | string \| string\[] | |
|
||||
| validator | 自定义校验,接收 Promise 作为返回值。[示例](#components-form-demo-register)参考 | ([rule](#Rule), value) => Promise | |
|
||||
| warningOnly | 仅警告,不阻塞表单提交 | boolean | 4.17.0 |
|
||||
| whitespace | 如果字段仅包含空格则校验不通过 | boolean | |
|
||||
|
||||
## 从 v3 升级到 v4
|
||||
|
||||
|
@ -61,9 +61,12 @@
|
||||
|
||||
margin-bottom: @form-item-margin-bottom;
|
||||
vertical-align: top;
|
||||
// We delay one frame (0.017s) here to let CSSMotion goes
|
||||
transition: margin-bottom @animation-duration-slow 0.017s linear;
|
||||
|
||||
&-with-help {
|
||||
margin-bottom: 0;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&-hidden,
|
||||
@ -89,7 +92,6 @@
|
||||
|
||||
> label {
|
||||
position: relative;
|
||||
// display: inline;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: @form-item-label-height;
|
||||
@ -179,10 +181,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// = Explain =
|
||||
// ==============================================================
|
||||
&-explain,
|
||||
&-extra {
|
||||
clear: both;
|
||||
min-height: @form-item-margin-bottom;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
line-height: @line-height-base;
|
||||
@ -190,43 +194,64 @@
|
||||
.explainAndExtraDistance((@form-item-margin-bottom - @form-font-height) / 2);
|
||||
}
|
||||
|
||||
&-explain {
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&-extra {
|
||||
min-height: @form-item-margin-bottom;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-input-textarea-show-count {
|
||||
&::after {
|
||||
margin-bottom: -22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-help-motion(@className, @keyframeName, @duration: @animation-duration-slow) {
|
||||
@name: ~'@{ant-prefix}-@{className}';
|
||||
.make-motion(@name, @keyframeName, @duration);
|
||||
.@{name}-enter,
|
||||
.@{name}-appear {
|
||||
opacity: 0;
|
||||
animation-timing-function: @ease-in-out;
|
||||
}
|
||||
.@{name}-leave {
|
||||
animation-timing-function: @ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.show-help-motion(show-help, antShowHelp, 0.3s);
|
||||
|
||||
@keyframes antShowHelpIn {
|
||||
0% {
|
||||
transform: translateY(-5px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
&-with-help &-explain {
|
||||
height: auto;
|
||||
min-height: @form-item-margin-bottom;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antShowHelpOut {
|
||||
to {
|
||||
// >>>>>>>>>> Motion <<<<<<<<<<
|
||||
// Explain holder
|
||||
.@{ant-prefix}-show-help {
|
||||
transition: height @animation-duration-slow linear, min-height @animation-duration-slow linear,
|
||||
margin-bottom @animation-duration-slow @ease-in-out,
|
||||
opacity @animation-duration-slow @ease-in-out;
|
||||
|
||||
&-leave {
|
||||
min-height: @form-item-margin-bottom;
|
||||
|
||||
&-active {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explain
|
||||
.@{ant-prefix}-show-help-item {
|
||||
overflow: hidden;
|
||||
transition: height @animation-duration-slow @ease-in-out,
|
||||
opacity @animation-duration-slow @ease-in-out, transform @animation-duration-slow @ease-in-out !important;
|
||||
|
||||
&-appear,
|
||||
&-enter {
|
||||
transform: translateY(-5px);
|
||||
opacity: 0;
|
||||
|
||||
&-active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,11 +9,11 @@
|
||||
// ========================= Explain =========================
|
||||
/* To support leave along ErrorList. We add additional className to handle explain style */
|
||||
&-explain {
|
||||
&&-error {
|
||||
&-error {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&&-warning {
|
||||
&-warning {
|
||||
color: @warning-color;
|
||||
}
|
||||
}
|
||||
|
@ -16705,7 +16705,21 @@ exports[`renders ./components/table/demo/sticky.md correctly 1`] = `
|
||||
colspan="2"
|
||||
style="position:sticky;left:0"
|
||||
>
|
||||
Fix Left
|
||||
<button
|
||||
aria-checked="false"
|
||||
class="ant-switch"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="ant-switch-handle"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner"
|
||||
>
|
||||
Fixed Top
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
|
@ -14,7 +14,7 @@ title:
|
||||
For long table,need to scroll to view the header and scroll bar,then you can now set the fixed header and scroll bar to follow the page.
|
||||
|
||||
```jsx
|
||||
import { Table } from 'antd';
|
||||
import { Table, Switch } from 'antd';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@ -93,26 +93,38 @@ for (let i = 0; i < 100; i++) {
|
||||
});
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
scroll={{ x: 1500 }}
|
||||
summary={pageData => (
|
||||
<Table.Summary fixed>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell index={0} colSpan={2}>
|
||||
Fix Left
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={2} colSpan={8}>
|
||||
Scroll Context
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={10}>Fix Right</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
</Table.Summary>
|
||||
)}
|
||||
sticky
|
||||
/>,
|
||||
mountNode,
|
||||
);
|
||||
const Demo = () => {
|
||||
const [fixedTop, setFixedTop] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
scroll={{ x: 1500 }}
|
||||
summary={pageData => (
|
||||
<Table.Summary fixed={fixedTop ? 'top' : 'bottom'}>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell index={0} colSpan={2}>
|
||||
<Switch
|
||||
checkedChildren="Fixed Top"
|
||||
unCheckedChildren="Fixed Top"
|
||||
checked={fixedTop}
|
||||
onChange={() => {
|
||||
setFixedTop(!fixedTop);
|
||||
}}
|
||||
/>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={2} colSpan={8}>
|
||||
Scroll Context
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={10}>Fix Right</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
</Table.Summary>
|
||||
)}
|
||||
sticky
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Demo />, mountNode);
|
||||
```
|
||||
|
@ -381,3 +381,111 @@ exports[`renders ./components/tree-select/demo/treeData.md correctly 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/tree-select/demo/treeLine.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-bottom:8px"
|
||||
>
|
||||
<button
|
||||
aria-checked="true"
|
||||
class="ant-switch ant-switch-checked"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="ant-switch-handle"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner"
|
||||
>
|
||||
treeLine
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-bottom:8px"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
class="ant-switch"
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="ant-switch-handle"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner"
|
||||
>
|
||||
showLeafIcon
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-select ant-tree-select ant-select-single ant-select-show-arrow"
|
||||
style="width:300px"
|
||||
>
|
||||
<div
|
||||
class="ant-select-selector"
|
||||
>
|
||||
<span
|
||||
class="ant-select-selection-search"
|
||||
>
|
||||
<input
|
||||
aria-activedescendant="undefined_list_0"
|
||||
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"
|
||||
/>
|
||||
</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>
|
||||
`;
|
||||
|
@ -45,6 +45,7 @@ class Demo extends React.Component {
|
||||
treeData: this.state.treeData.concat([
|
||||
this.genTreeNode(id, false),
|
||||
this.genTreeNode(id, true),
|
||||
this.genTreeNode(id, true),
|
||||
]),
|
||||
});
|
||||
resolve();
|
||||
|
56
components/tree-select/demo/treeLine.md
Normal file
56
components/tree-select/demo/treeLine.md
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
order: 6
|
||||
title:
|
||||
zh-CN: 线性样式
|
||||
en-US: Show Tree Line
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
通过 `treeLine` 配置线性样式。
|
||||
|
||||
## en-US
|
||||
|
||||
Use `treeLine` to show the line style.
|
||||
|
||||
```tsx
|
||||
import { TreeSelect, Switch, Space } from 'antd';
|
||||
|
||||
const { TreeNode } = TreeSelect;
|
||||
|
||||
const Demo = () => {
|
||||
const [treeLine, setTreeLine] = React.useState(true);
|
||||
const [showLeafIcon, setShowLeafIcon] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<Switch
|
||||
checkedChildren="treeLine"
|
||||
unCheckedChildren="treeLine"
|
||||
checked={treeLine}
|
||||
onChange={() => setTreeLine(!treeLine)}
|
||||
/>
|
||||
<Switch
|
||||
disabled={!treeLine}
|
||||
checkedChildren="showLeafIcon"
|
||||
unCheckedChildren="showLeafIcon"
|
||||
checked={showLeafIcon}
|
||||
onChange={() => setShowLeafIcon(!showLeafIcon)}
|
||||
/>
|
||||
<TreeSelect treeLine={treeLine && { showLeafIcon }} style={{ width: 300 }}>
|
||||
<TreeNode value="parent 1" title="parent 1">
|
||||
<TreeNode value="parent 1-0" title="parent 1-0">
|
||||
<TreeNode value="leaf1" title="my leaf" />
|
||||
<TreeNode value="leaf2" title="your leaf" />
|
||||
</TreeNode>
|
||||
<TreeNode value="parent 1-1" title="parent 1-1">
|
||||
<TreeNode value="sss" title="sss" />
|
||||
</TreeNode>
|
||||
</TreeNode>
|
||||
</TreeSelect>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Demo />, mountNode);
|
||||
```
|
@ -50,6 +50,7 @@ Tree selection control.
|
||||
| treeDefaultExpandedKeys | Default expanded treeNodes | string\[] | - | |
|
||||
| treeExpandedKeys | Set expanded keys | string\[] | - | |
|
||||
| treeIcon | Shows the icon before a TreeNode's title. There is no default style; you must set a custom style for it if set to `true` | boolean | false | |
|
||||
| treeLine | Show the line. Ref [Tree - showLine](/components/tree/#components-tree-demo-line) | boolean \| object | false | 4.17.0 |
|
||||
| treeNodeFilterProp | Will be used for filtering if `filterTreeNode` returns true | string | `value` | |
|
||||
| treeNodeLabelProp | Will render as content of select | string | `title` | |
|
||||
| value | To set the current selected treeNode(s) | string \| string\[] | - | |
|
||||
@ -62,10 +63,10 @@ Tree selection control.
|
||||
|
||||
### Tree Methods
|
||||
|
||||
| Name | Description | Version |
|
||||
| --- | --- | --- |
|
||||
| blur() | Remove focus | |
|
||||
| focus() | Get focus | |
|
||||
| Name | Description | Version |
|
||||
| ------- | ------------ | ------- |
|
||||
| blur() | Remove focus | |
|
||||
| focus() | Get focus | |
|
||||
|
||||
### TreeNode props
|
||||
|
||||
|
@ -11,7 +11,7 @@ import omit from 'rc-util/lib/omit';
|
||||
import { DefaultValueType } from 'rc-tree-select/lib/interface';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import devWarning from '../_util/devWarning';
|
||||
import { AntTreeNodeProps } from '../tree';
|
||||
import { AntTreeNodeProps, TreeProps } from '../tree';
|
||||
import getIcons from '../select/utils/iconUtil';
|
||||
import renderSwitcherIcon from '../tree/utils/iconUtil';
|
||||
import SizeContext, { SizeType } from '../config-provider/SizeContext';
|
||||
@ -30,11 +30,18 @@ export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[];
|
||||
export interface TreeSelectProps<T>
|
||||
extends Omit<
|
||||
RcTreeSelectProps<T>,
|
||||
'showTreeIcon' | 'treeMotion' | 'inputIcon' | 'mode' | 'getInputElement' | 'backfill'
|
||||
| 'showTreeIcon'
|
||||
| 'treeMotion'
|
||||
| 'inputIcon'
|
||||
| 'mode'
|
||||
| 'getInputElement'
|
||||
| 'backfill'
|
||||
| 'treeLine'
|
||||
> {
|
||||
suffixIcon?: React.ReactNode;
|
||||
size?: SizeType;
|
||||
bordered?: boolean;
|
||||
treeLine?: TreeProps['showLine'];
|
||||
}
|
||||
|
||||
export interface RefTreeSelectProps {
|
||||
@ -140,6 +147,7 @@ const InternalTreeSelect = <T extends DefaultValueType>(
|
||||
treeCheckable={
|
||||
treeCheckable ? <span className={`${prefixCls}-tree-checkbox-inner`} /> : treeCheckable
|
||||
}
|
||||
treeLine={!!treeLine}
|
||||
inputIcon={suffixIcon}
|
||||
multiple={multiple}
|
||||
removeIcon={removeIcon}
|
||||
|
@ -51,6 +51,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
|
||||
| treeDefaultExpandedKeys | 默认展开的树节点 | string\[] | - | |
|
||||
| treeExpandedKeys | 设置展开的树节点 | string\[] | - | |
|
||||
| treeIcon | 是否展示 TreeNode title 前的图标,没有默认样式,如设置为 true,需要自行定义图标相关样式 | boolean | false | |
|
||||
| treeLine | 是否展示线条样式,请参考 [Tree - showLine](/components/tree/#components-tree-demo-line) | boolean \| object | false | 4.17.0 |
|
||||
| treeNodeFilterProp | 输入项过滤对应的 treeNode 属性 | string | `value` | |
|
||||
| treeNodeLabelProp | 作为显示的 prop 设置 | string | `title` | |
|
||||
| value | 指定当前选中的条目 | string \| string\[] | - | |
|
||||
@ -63,25 +64,25 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
|
||||
|
||||
### Tree 方法
|
||||
|
||||
| 名称 | 描述 | 版本 |
|
||||
| --- | --- | --- |
|
||||
| blur() | 移除焦点 | |
|
||||
| focus() | 获取焦点 | |
|
||||
| 名称 | 描述 | 版本 |
|
||||
| ------- | -------- | ---- |
|
||||
| blur() | 移除焦点 | |
|
||||
| focus() | 获取焦点 | |
|
||||
|
||||
### TreeNode props
|
||||
|
||||
> 建议使用 treeData 来代替 TreeNode,免去手工构造麻烦
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| checkable | 当树为 Checkbox 时,设置独立节点是否展示 Checkbox | boolean | - | |
|
||||
| disableCheckbox | 禁掉 Checkbox | boolean | false | |
|
||||
| disabled | 是否禁用 | boolean | false | |
|
||||
| isLeaf | 是否是叶子节点 | boolean | false | |
|
||||
| key | 此项必须设置(其值在整个树范围内唯一) | string | - | |
|
||||
| selectable | 是否可选 | boolean | true | |
|
||||
| title | 树节点显示的内容 | ReactNode | `---` | |
|
||||
| value | 默认根据此属性值进行筛选(其值在整个树范围内唯一) | string | - | |
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --------------- | -------------------------------------------------- | --------- | ------ | ---- |
|
||||
| checkable | 当树为 Checkbox 时,设置独立节点是否展示 Checkbox | boolean | - | |
|
||||
| disableCheckbox | 禁掉 Checkbox | boolean | false | |
|
||||
| disabled | 是否禁用 | boolean | false | |
|
||||
| isLeaf | 是否是叶子节点 | boolean | false | |
|
||||
| key | 此项必须设置(其值在整个树范围内唯一) | string | - | |
|
||||
| selectable | 是否可选 | boolean | true | |
|
||||
| title | 树节点显示的内容 | ReactNode | `---` | |
|
||||
| value | 默认根据此属性值进行筛选(其值在整个树范围内唯一) | string | - | |
|
||||
|
||||
## FAQ
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
.@{tree-select-prefix-cls} {
|
||||
// ======================= Dropdown =======================
|
||||
&-dropdown {
|
||||
padding: @padding-xs (@padding-xs / 2) 0;
|
||||
padding: @padding-xs (@padding-xs / 2);
|
||||
|
||||
&-rtl {
|
||||
direction: rtl;
|
||||
@ -24,8 +24,6 @@
|
||||
align-items: stretch;
|
||||
|
||||
.@{select-tree-prefix-cls}-treenode {
|
||||
padding-bottom: @padding-xs;
|
||||
|
||||
.@{select-tree-prefix-cls}-node-content-wrapper {
|
||||
flex: auto;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
@import '../../style/mixins/index';
|
||||
|
||||
@tree-prefix-cls: ~'@{ant-prefix}-tree';
|
||||
@tree-node-prefix-cls: ~'@{tree-prefix-cls}-treenode';
|
||||
@select-tree-prefix-cls: ~'@{ant-prefix}-select-tree';
|
||||
@tree-motion: ~'@{ant-prefix}-motion-collapse';
|
||||
@tree-node-padding: (@padding-xs / 2);
|
||||
@ -259,15 +258,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{tree-node-prefix-cls}-leaf-last {
|
||||
.@{tree-prefix-cls}-switcher {
|
||||
&-leaf-line {
|
||||
&::before {
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
height: @tree-title-height - 10px !important;
|
||||
.@{custom-tree-node-prefix-cls}-leaf-last {
|
||||
.@{custom-tree-prefix-cls}-switcher {
|
||||
&-leaf-line {
|
||||
&::before {
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
height: @tree-title-height - 10px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,12 +122,12 @@
|
||||
"rc-dialog": "~8.5.1",
|
||||
"rc-drawer": "~4.3.0",
|
||||
"rc-dropdown": "~3.2.0",
|
||||
"rc-field-form": "~1.20.0",
|
||||
"rc-field-form": "~1.21.0-2",
|
||||
"rc-image": "~5.2.4",
|
||||
"rc-input-number": "~7.1.0",
|
||||
"rc-mentions": "~1.6.1",
|
||||
"rc-menu": "~9.0.9",
|
||||
"rc-motion": "^2.4.0",
|
||||
"rc-motion": "^2.4.4",
|
||||
"rc-notification": "~4.5.7",
|
||||
"rc-pagination": "~3.1.6",
|
||||
"rc-picker": "~2.5.10",
|
||||
@ -138,7 +138,7 @@
|
||||
"rc-slider": "~9.7.1",
|
||||
"rc-steps": "~4.1.0",
|
||||
"rc-switch": "~3.2.0",
|
||||
"rc-table": "~7.15.1",
|
||||
"rc-table": "~7.16.0",
|
||||
"rc-tabs": "~11.9.1",
|
||||
"rc-textarea": "~0.3.0",
|
||||
"rc-tooltip": "~5.1.1",
|
||||
|
Loading…
Reference in New Issue
Block a user