merge feature into master

This commit is contained in:
afc163 2021-06-05 13:30:36 +08:00
commit 8fe1cc9da5
29 changed files with 809 additions and 417 deletions

View File

@ -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';

View File

@ -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

View File

@ -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 }) => (
{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
className={classNames(
baseClassName,
{
[`${baseClassName}-${innerStatus}`]: innerStatus,
},
motionClassName,
)}
key="help"
key={key}
role="alert"
className={classNames(itemClassName, {
[`${baseClassName}-${errorStatus}`]: errorStatus,
})}
style={itemStyle}
>
{memoErrors.map((error, index) => (
// eslint-disable-next-line react/no-array-index-key
<div key={index} role="alert">
{error}
</div>
))}
);
}}
</CSSMotionList>
</div>
)}
);
}}
</CSSMotion>
);
}

View File

@ -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>>({});
// >>>>> 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;
}
if (!isEqual(prevInlineErrors[subName], subErrors)) {
return {
...prevInlineErrors,
[subName]: subErrors,
};
}
return prevInlineErrors;
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>
);

View File

@ -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>
);

View File

@ -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>

View File

@ -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.

View File

@ -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);
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

View File

@ -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();

View File

@ -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'> {

View File

@ -40,6 +40,7 @@ const Demo = () => {
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label="Username"

View 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);
```

View File

@ -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];
}

View 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;
}

View File

@ -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&lt;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&lt;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

View File

@ -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&lt;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&lt;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

View File

@ -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;
&-with-help &-explain {
height: auto;
min-height: @form-item-margin-bottom;
opacity: 1;
}
}
.show-help-motion(show-help, antShowHelp, 0.3s);
// >>>>>>>>>> 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;
@keyframes antShowHelpIn {
0% {
&-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;
}
100% {
&-active {
transform: translateY(0);
opacity: 1;
}
}
@keyframes antShowHelpOut {
to {
&-leave-active {
transform: translateY(-5px);
opacity: 0;
}
}

View File

@ -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;
}
}

View File

@ -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"

View File

@ -14,7 +14,7 @@ title:
For long tableneed to scroll to view the header and scroll barthen 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,16 +93,26 @@ for (let i = 0; i < 100; i++) {
});
}
ReactDOM.render(
const Demo = () => {
const [fixedTop, setFixedTop] = React.useState(false);
return (
<Table
columns={columns}
dataSource={data}
scroll={{ x: 1500 }}
summary={pageData => (
<Table.Summary fixed>
<Table.Summary fixed={fixedTop ? 'top' : 'bottom'}>
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={2}>
Fix Left
<Switch
checkedChildren="Fixed Top"
unCheckedChildren="Fixed Top"
checked={fixedTop}
onChange={() => {
setFixedTop(!fixedTop);
}}
/>
</Table.Summary.Cell>
<Table.Summary.Cell index={2} colSpan={8}>
Scroll Context
@ -112,7 +122,9 @@ ReactDOM.render(
</Table.Summary>
)}
sticky
/>,
mountNode,
/>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -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>
`;

View File

@ -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();

View 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);
```

View File

@ -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\[] | - | |
@ -63,7 +64,7 @@ Tree selection control.
### Tree Methods
| Name | Description | Version |
| --- | --- | --- |
| ------- | ------------ | ------- |
| blur() | Remove focus | |
| focus() | Get focus | |

View File

@ -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}

View File

@ -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\[] | - | |
@ -64,7 +65,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
### Tree 方法
| 名称 | 描述 | 版本 |
| --- | --- | --- |
| ------- | -------- | ---- |
| blur() | 移除焦点 | |
| focus() | 获取焦点 | |
@ -73,7 +74,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg
> 建议使用 treeData 来代替 TreeNode免去手工构造麻烦
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| --------------- | -------------------------------------------------- | --------- | ------ | ---- |
| checkable | 当树为 Checkbox 时,设置独立节点是否展示 Checkbox | boolean | - | |
| disableCheckbox | 禁掉 Checkbox | boolean | false | |
| disabled | 是否禁用 | boolean | false | |

View File

@ -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;
}

View File

@ -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,10 +258,9 @@
}
}
}
}
.@{tree-node-prefix-cls}-leaf-last {
.@{tree-prefix-cls}-switcher {
.@{custom-tree-node-prefix-cls}-leaf-last {
.@{custom-tree-prefix-cls}-switcher {
&-leaf-line {
&::before {
top: auto !important;
@ -272,3 +270,4 @@
}
}
}
}

View File

@ -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",