diff --git a/components/_util/debouncedWatch.ts b/components/_util/debouncedWatch.ts new file mode 100644 index 000000000..c822ab5ca --- /dev/null +++ b/components/_util/debouncedWatch.ts @@ -0,0 +1,89 @@ +// copy from https://github.dev/vueuse/vueuse + +import type { Ref, WatchOptions, WatchStopHandle } from 'vue'; +import { unref, watch } from 'vue'; + +type MaybeRef = T | Ref; + +type Fn = () => void; + +export type FunctionArgs = (...args: Args) => Return; + +export interface FunctionWrapperOptions { + fn: FunctionArgs; + args: Args; + thisArg: This; +} + +export type EventFilter = ( + invoke: Fn, + options: FunctionWrapperOptions, +) => void; + +const bypassFilter: EventFilter = invoke => { + return invoke(); +}; +/** + * Create an EventFilter that debounce the events + * + * @param ms + */ +export function debounceFilter(ms: MaybeRef) { + let timer: ReturnType | undefined; + + const filter: EventFilter = invoke => { + const duration = unref(ms); + + if (timer) clearTimeout(timer); + + if (duration <= 0) return invoke(); + + timer = setTimeout(invoke, duration); + }; + + return filter; +} +export interface DebouncedWatchOptions extends WatchOptions { + debounce?: MaybeRef; +} + +interface ConfigurableEventFilter { + eventFilter?: EventFilter; +} +/** + * @internal + */ +function createFilterWrapper(filter: EventFilter, fn: T) { + function wrapper(this: any, ...args: any[]) { + filter(() => fn.apply(this, args), { fn, thisArg: this, args }); + } + + return wrapper as any as T; +} +export interface WatchWithFilterOptions + extends WatchOptions, + ConfigurableEventFilter {} +// implementation +export function watchWithFilter = false>( + source: any, + cb: any, + options: WatchWithFilterOptions = {}, +): WatchStopHandle { + const { eventFilter = bypassFilter, ...watchOptions } = options; + + return watch(source, createFilterWrapper(eventFilter, cb), watchOptions); +} + +// implementation +export default function debouncedWatch = false>( + source: any, + cb: any, + options: DebouncedWatchOptions = {}, +): WatchStopHandle { + const { debounce = 0, ...watchOptions } = options; + + return watchWithFilter(source, cb, { + ...watchOptions, + eventFilter: debounceFilter(debounce), + }); +} diff --git a/components/_util/hooks/useConfigInject.ts b/components/_util/hooks/useConfigInject.ts index e4a3ffeb1..097e90429 100644 --- a/components/_util/hooks/useConfigInject.ts +++ b/components/_util/hooks/useConfigInject.ts @@ -14,30 +14,40 @@ export default ( direction: ComputedRef; size: ComputedRef; getTargetContainer: ComputedRef<() => HTMLElement>; - getPopupContainer: ComputedRef<() => HTMLElement>; space: ComputedRef<{ size: SizeType | number }>; pageHeader: ComputedRef<{ ghost: boolean }>; form?: ComputedRef<{ requiredMark?: RequiredMark; }>; - autoInsertSpaceInButton: ComputedRef; + autoInsertSpaceInButton: ComputedRef; renderEmpty?: ComputedRef<(componentName?: string) => VNodeChild | JSX.Element>; + virtual: ComputedRef; + dropdownMatchSelectWidth: ComputedRef; + getPopupContainer: ComputedRef; } => { const configProvider = inject>( 'configProvider', defaultConfigProvider, ); const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls)); + const direction = computed(() => props.direction ?? configProvider.direction); const rootPrefixCls = computed(() => configProvider.getPrefixCls()); - const direction = computed(() => configProvider.direction); const autoInsertSpaceInButton = computed(() => configProvider.autoInsertSpaceInButton); const renderEmpty = computed(() => configProvider.renderEmpty); const space = computed(() => configProvider.space); const pageHeader = computed(() => configProvider.pageHeader); const form = computed(() => configProvider.form); + const getTargetContainer = computed( + () => props.getTargetContainer || configProvider.getTargetContainer, + ); + const getPopupContainer = computed( + () => props.getPopupContainer || configProvider.getPopupContainer, + ); + const virtual = computed(() => props.virtual ?? configProvider.virtual); + const dropdownMatchSelectWidth = computed( + () => props.dropdownMatchSelectWidth ?? configProvider.dropdownMatchSelectWidth, + ); const size = computed(() => props.size || configProvider.componentSize); - const getTargetContainer = computed(() => props.getTargetContainer); - const getPopupContainer = computed(() => props.getPopupContainer); return { configProvider, prefixCls, @@ -50,6 +60,8 @@ export default ( form, autoInsertSpaceInButton, renderEmpty, + virtual, + dropdownMatchSelectWidth, rootPrefixCls, }; }; diff --git a/components/_util/omit.ts b/components/_util/omit.ts new file mode 100644 index 000000000..3388fd122 --- /dev/null +++ b/components/_util/omit.ts @@ -0,0 +1,10 @@ +function omit(obj: T, fields: K[]): Omit { + // eslint-disable-next-line prefer-object-spread + const shallowCopy = Object.assign({}, obj); + for (let i = 0; i < fields.length; i += 1) { + const key = fields[i]; + delete shallowCopy[key]; + } + return shallowCopy; +} +export default omit; diff --git a/components/_util/props-util/initDefaultProps.ts b/components/_util/props-util/initDefaultProps.ts index 8c01271ce..99bc6366a 100644 --- a/components/_util/props-util/initDefaultProps.ts +++ b/components/_util/props-util/initDefaultProps.ts @@ -17,7 +17,13 @@ const initDefaultProps = ( Object.keys(defaultProps).forEach(k => { const prop = propTypes[k] as VueTypeValidableDef; if (prop) { - prop.default = defaultProps[k]; + if (prop.type || prop.default) { + prop.default = defaultProps[k]; + } else if (prop.def) { + prop.def(defaultProps[k]); + } else { + propTypes[k] = { type: prop, default: defaultProps[k] }; + } } else { throw new Error(`not have ${k} prop`); } diff --git a/components/auto-complete/index.tsx b/components/auto-complete/index.tsx index d9e43f753..071e6804a 100644 --- a/components/auto-complete/index.tsx +++ b/components/auto-complete/index.tsx @@ -1,6 +1,6 @@ import type { App, Plugin, VNode, ExtractPropTypes } from 'vue'; import { defineComponent, inject, provide } from 'vue'; -import Select, { SelectProps } from '../select'; +import Select, { selectProps } from '../select'; import Input from '../input'; import PropTypes from '../_util/vue-types'; import { defaultConfigProvider } from '../config-provider'; @@ -15,7 +15,7 @@ function isSelectOptionOrSelectOptGroup(child: any): boolean { } const autoCompleteProps = { - ...SelectProps(), + ...selectProps(), dataSource: PropTypes.array, dropdownMenuStyle: PropTypes.style, optionLabelProp: PropTypes.string, @@ -33,7 +33,7 @@ const AutoComplete = defineComponent({ inheritAttrs: false, props: { ...autoCompleteProps, - prefixCls: PropTypes.string.def('ant-select'), + prefixCls: PropTypes.string, showSearch: PropTypes.looseBool, transitionName: PropTypes.string.def('slide-up'), choiceTransitionName: PropTypes.string.def('zoom'), diff --git a/components/components.ts b/components/components.ts index 6f2501775..93986f35e 100644 --- a/components/components.ts +++ b/components/components.ts @@ -169,6 +169,7 @@ export { default as Table, TableColumn, TableColumnGroup } from './table'; export type { TransferProps } from './transfer'; export { default as Transfer } from './transfer'; +export type { TreeProps, DirectoryTreeProps } from './tree'; export { default as Tree, TreeNode, DirectoryTree } from './tree'; export type { TreeSelectProps } from './tree-select'; diff --git a/components/form/Form.tsx b/components/form/Form.tsx index 4e982fa25..d173c97cc 100755 --- a/components/form/Form.tsx +++ b/components/form/Form.tsx @@ -318,14 +318,16 @@ const Form = defineComponent({ e.preventDefault(); e.stopPropagation(); emit('submit', e); - const res = validateFields(); - res - .then(values => { - emit('finish', values); - }) - .catch(errors => { - handleFinishFailed(errors); - }); + if (props.model) { + const res = validateFields(); + res + .then(values => { + emit('finish', values); + }) + .catch(errors => { + handleFinishFailed(errors); + }); + } }; expose({ diff --git a/components/form/useForm.ts b/components/form/useForm.ts index a8c82f65c..07e3281cb 100644 --- a/components/form/useForm.ts +++ b/components/form/useForm.ts @@ -107,6 +107,8 @@ function useForm( validateInfos: validateInfos; resetFields: (newValues?: Props) => void; validate: (names?: namesType, option?: validateOptions) => Promise; + + /** This is an internal usage. Do not use in your prod */ validateField: ( name: string, value: any, @@ -117,19 +119,33 @@ function useForm( clearValidate: (names?: namesType) => void; } { const initialModel = cloneDeep(unref(modelRef)); - let validateInfos: validateInfos = {}; + const validateInfos = reactive({}); const rulesKeys = computed(() => { return Object.keys(unref(rulesRef)); }); - rulesKeys.value.forEach(key => { - validateInfos[key] = { - autoLink: false, - required: isRequired(unref(rulesRef)[key]), - }; - }); - validateInfos = reactive(validateInfos); + watch( + rulesKeys, + () => { + const newValidateInfos = {}; + rulesKeys.value.forEach(key => { + newValidateInfos[key] = validateInfos[key] || { + autoLink: false, + required: isRequired(unref(rulesRef)[key]), + }; + delete validateInfos[key]; + }); + for (const key in validateInfos) { + if (Object.prototype.hasOwnProperty.call(validateInfos, key)) { + delete validateInfos[key]; + } + } + Object.assign(validateInfos, newValidateInfos); + }, + { immediate: true }, + ); + const resetFields = (newValues: Props) => { Object.assign(unref(modelRef), { ...cloneDeep(initialModel), @@ -249,6 +265,9 @@ function useForm( }, !!option.validateFirst, ); + if (!validateInfos[name]) { + return promise.catch((e: any) => e); + } validateInfos[name].validateStatus = 'validating'; promise .catch((e: any) => e) @@ -325,7 +344,9 @@ function useForm( validate(names, { trigger: 'change' }); oldModel = cloneDeep(model); }; + const debounceOptions = options?.debounce; + watch( modelRef, debounceOptions && debounceOptions.wait diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 58ccd6013..407287680 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -68,7 +68,7 @@ export default defineComponent({ 'click', 'update:activeKey', ], - slots: ['expandIcon'], + slots: ['expandIcon', 'overflowedIndicator'], setup(props, { slots, emit }) { const { prefixCls, direction } = useConfigInject('menu', props); const store = ref>({}); @@ -396,7 +396,7 @@ export default defineComponent({ {child} )); - const overflowedIndicator = ; + const overflowedIndicator = slots.overflowedIndicator?.() || ; return ( extends Omit, 'mode'> { - suffixIcon?: VNodeChild; - itemIcon?: VNodeChild; - size?: SizeType; - mode?: 'multiple' | 'tags' | 'SECRET_COMBOBOX_MODE_DO_NOT_USE'; - bordered?: boolean; -} - -export interface SelectPropsTypes - extends Omit< - InternalSelectProps, - 'inputIcon' | 'mode' | 'getInputElement' | 'backfill' | 'class' | 'style' - > { - mode?: 'multiple' | 'tags'; -} -export type SelectTypes = SelectPropsTypes; -export const SelectProps = () => ({ - ...(omit(BaseProps(), [ - 'inputIcon', - 'mode', - 'getInputElement', - 'backfill', - 'class', - 'style', - ]) as Omit< - ReturnType, - 'inputIcon' | 'mode' | 'getInputElement' | 'backfill' | 'class' | 'style' - >), +export const selectProps = () => ({ + ...omit(vcSelectProps(), ['inputIcon', 'mode', 'getInputElement', 'backfill']), value: { type: [Array, Object, String, Number] as PropType, }, defaultValue: { type: [Array, Object, String, Number] as PropType, }, - notFoundContent: PropTypes.VNodeChild, - suffixIcon: PropTypes.VNodeChild, - itemIcon: PropTypes.VNodeChild, + notFoundContent: PropTypes.any, + suffixIcon: PropTypes.any, + itemIcon: PropTypes.any, size: PropTypes.oneOf(tuple('small', 'middle', 'large', 'default')), mode: PropTypes.oneOf(tuple('multiple', 'tags', 'SECRET_COMBOBOX_MODE_DO_NOT_USE')), bordered: PropTypes.looseBool.def(true), @@ -68,12 +41,14 @@ export const SelectProps = () => ({ choiceTransitionName: PropTypes.string.def(''), }); +export type SelectProps = Partial>>; + const Select = defineComponent({ name: 'ASelect', Option, OptGroup, inheritAttrs: false, - props: SelectProps(), + props: selectProps(), SECRET_COMBOBOX_MODE_DO_NOT_USE: 'SECRET_COMBOBOX_MODE_DO_NOT_USE', emits: ['change', 'update:value'], slots: [ @@ -86,7 +61,7 @@ const Select = defineComponent({ 'option', ], setup(props, { attrs, emit, slots, expose }) { - const selectRef = ref(null); + const selectRef = ref(); const focus = () => { if (selectRef.value) { @@ -146,7 +121,7 @@ const Select = defineComponent({ const isMultiple = mode.value === 'multiple' || mode.value === 'tags'; // ===================== Empty ===================== - let mergedNotFound: VNodeChild; + let mergedNotFound: any; if (notFoundContent !== undefined) { mergedNotFound = notFoundContent; } else if (slots.notFoundContent) { diff --git a/components/style/mixins/clearfix.less b/components/style/mixins/clearfix.less index 7999a3e92..20ed1079e 100644 --- a/components/style/mixins/clearfix.less +++ b/components/style/mixins/clearfix.less @@ -1,7 +1,7 @@ // mixins for clearfix // ------------------------ .clearfix() { - zoom: 1; + &::before, &::after { display: table; diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 93a16e985..ace3443a7 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -771,6 +771,7 @@ // Tree // --- +@tree-bg: @component-background; @tree-title-height: 24px; @tree-child-padding: 18px; @tree-directory-selected-color: #fff; diff --git a/components/switch/__tests__/index.test.js b/components/switch/__tests__/index.test.js index 5fb8e501c..5770a9934 100644 --- a/components/switch/__tests__/index.test.js +++ b/components/switch/__tests__/index.test.js @@ -70,4 +70,35 @@ describe('Switch', () => { }); expect(checked.value).toBe(1); }); + + it('customize checked value and children should work', async () => { + resetWarned(); + const checked = ref(1); + const onUpdate = val => (checked.value = val); + const wrapper = mount({ + render() { + return ( + + ); + }, + }); + await asyncExpect(() => { + wrapper.find('button').trigger('click'); + }); + expect(checked.value).toBe(2); + expect(wrapper.find('.ant-switch-inner').text()).toBe('on'); + + await asyncExpect(() => { + wrapper.find('button').trigger('click'); + }); + expect(checked.value).toBe(1); + expect(wrapper.find('.ant-switch-inner').text()).toBe('off'); + }); }); diff --git a/components/switch/index.tsx b/components/switch/index.tsx index 0d038946a..719ef670e 100644 --- a/components/switch/index.tsx +++ b/components/switch/index.tsx @@ -134,6 +134,7 @@ const Switch = defineComponent({ [`${prefixCls.value}-disabled`]: props.disabled, [prefixCls.value]: true, })); + return () => (