From 1d08e80165fce0e4fa63b90736e6691422654e4b Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 10 May 2022 15:35:44 +0800 Subject: [PATCH 001/323] feat: anchor add activeLink arg --- components/anchor/Anchor.tsx | 4 ++-- components/anchor/index.en-US.md | 2 +- components/anchor/index.zh-CN.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/anchor/Anchor.tsx b/components/anchor/Anchor.tsx index 9fd6ac3191..50503ada00 100644 --- a/components/anchor/Anchor.tsx +++ b/components/anchor/Anchor.tsx @@ -57,7 +57,7 @@ export const anchorProps = () => ({ getContainer: Function as PropType<() => AnchorContainer>, wrapperClass: String, wrapperStyle: { type: Object as PropType, default: undefined as CSSProperties }, - getCurrentAnchor: Function as PropType<() => string>, + getCurrentAnchor: Function as PropType<(activeLink: string) => string>, targetOffset: Number, onChange: Function as PropType<(currentActiveLink: string) => void>, onClick: Function as PropType<(e: MouseEvent, link: { title: any; href: string }) => void>, @@ -123,7 +123,7 @@ export default defineComponent({ if (activeLink.value === link) { return; } - activeLink.value = typeof getCurrentAnchor === 'function' ? getCurrentAnchor() : link; + activeLink.value = typeof getCurrentAnchor === 'function' ? getCurrentAnchor(link) : link; emit('change', link); }; const handleScrollTo = (link: string) => { diff --git a/components/anchor/index.en-US.md b/components/anchor/index.en-US.md index 2bb20a28ef..bbe7aa61d9 100644 --- a/components/anchor/index.en-US.md +++ b/components/anchor/index.en-US.md @@ -21,7 +21,7 @@ For displaying anchor hyperlinks on page and jumping between them. | affix | Fixed mode of Anchor | boolean | true | | | bounds | Bounding distance of anchor area | number | 5(px) | | | getContainer | Scrolling container | () => HTMLElement | () => window | | -| getCurrentAnchor | Customize the anchor highlight | () => string | - | 1.5.0 | +| getCurrentAnchor | Customize the anchor highlight | (activeLink: string) => string | - | activeLink(3.3) | | offsetBottom | Pixels to offset from bottom when calculating position of scroll | number | - | | | offsetTop | Pixels to offset from top when calculating position of scroll | number | 0 | | | showInkInFixed | Whether show ink-balls when `:affix="false"` | boolean | false | | diff --git a/components/anchor/index.zh-CN.md b/components/anchor/index.zh-CN.md index 309dc9faf0..097e4fd9f5 100644 --- a/components/anchor/index.zh-CN.md +++ b/components/anchor/index.zh-CN.md @@ -22,7 +22,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_1-C1JwsC/Anchor.svg | affix | 固定模式 | boolean | true | | | bounds | 锚点区域边界 | number | 5(px) | | | getContainer | 指定滚动的容器 | () => HTMLElement | () => window | | -| getCurrentAnchor | 自定义高亮的锚点 | () => string | - | 1.5.0 | +| getCurrentAnchor | 自定义高亮的锚点 | (activeLink: string) => string | - | activeLink(3.3) | | offsetBottom | 距离窗口底部达到指定偏移量后触发 | number | | | | offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | | | | showInkInFixed | `:affix="false"` 时是否显示小圆点 | boolean | false | | From a8dbea7c32653e2e668230b9a24bb6ee804f0ea8 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 10 May 2022 15:36:18 +0800 Subject: [PATCH 002/323] style: update some code --- components/_util/statusUtils.tsx | 23 ++++++++++++++++++++ components/_util/throttleByAnimationFrame.ts | 16 +++++++++----- components/_util/util.ts | 2 +- components/affix/utils.ts | 10 +++------ components/alert/index.en-US.md | 2 +- components/alert/index.zh-CN.md | 2 +- components/vc-checkbox/Checkbox.tsx | 2 +- components/vc-notification/Notice.tsx | 2 +- components/vc-picker/utils/miscUtil.ts | 7 ++---- components/vc-table/utils/legacyUtil.ts | 2 +- 10 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 components/_util/statusUtils.tsx diff --git a/components/_util/statusUtils.tsx b/components/_util/statusUtils.tsx new file mode 100644 index 0000000000..b61614f7d2 --- /dev/null +++ b/components/_util/statusUtils.tsx @@ -0,0 +1,23 @@ +import type { ValidateStatus } from '../form/FormItem'; +import classNames from './classNames'; +import { tuple } from './type'; + +const InputStatuses = tuple('warning', 'error', ''); +export type InputStatus = typeof InputStatuses[number]; + +export function getStatusClassNames( + prefixCls: string, + status?: ValidateStatus, + hasFeedback?: boolean, +) { + return classNames({ + [`${prefixCls}-status-success`]: status === 'success', + [`${prefixCls}-status-warning`]: status === 'warning', + [`${prefixCls}-status-error`]: status === 'error', + [`${prefixCls}-status-validating`]: status === 'validating', + [`${prefixCls}-has-feedback`]: hasFeedback, + }); +} + +export const getMergedStatus = (contextStatus?: ValidateStatus, customStatus?: InputStatus) => + customStatus || contextStatus; diff --git a/components/_util/throttleByAnimationFrame.ts b/components/_util/throttleByAnimationFrame.ts index dd84132a4f..45bae734bb 100644 --- a/components/_util/throttleByAnimationFrame.ts +++ b/components/_util/throttleByAnimationFrame.ts @@ -1,20 +1,26 @@ import raf from './raf'; -export default function throttleByAnimationFrame(fn: (...args: any[]) => void) { - let requestId: number; +export default function throttleByAnimationFrame(fn: (...args: T) => void) { + let requestId: number | null; - const later = (args: any[]) => () => { + const later = (args: T) => () => { requestId = null; fn(...args); }; - const throttled = (...args: any[]) => { + const throttled: { + (...args: T): void; + cancel: () => void; + } = (...args: T) => { if (requestId == null) { requestId = raf(later(args)); } }; - (throttled as any).cancel = () => raf.cancel(requestId!); + throttled.cancel = () => { + raf.cancel(requestId!); + requestId = null; + }; return throttled; } diff --git a/components/_util/util.ts b/components/_util/util.ts index 1adfa26efd..7be837e532 100644 --- a/components/_util/util.ts +++ b/components/_util/util.ts @@ -56,7 +56,7 @@ function resolvePropValue(options, props, key, value) { export function getDataAndAriaProps(props) { return Object.keys(props).reduce((memo, key) => { - if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-') { + if (key.startsWith('data-') || key.startsWith('aria-')) { memo[key] = props[key]; } return memo; diff --git a/components/affix/utils.ts b/components/affix/utils.ts index 4ce8c11bb9..ec91894762 100644 --- a/components/affix/utils.ts +++ b/components/affix/utils.ts @@ -1,5 +1,4 @@ import addEventListener from '../vc-util/Dom/addEventListener'; -import type { ComponentPublicInstance } from 'vue'; import supportsPassive from '../_util/supportsPassive'; export type BindElement = HTMLElement | Window | null | undefined; @@ -42,7 +41,7 @@ const TRIGGER_EVENTS = [ interface ObserverEntity { target: HTMLElement | Window; - affixList: ComponentPublicInstance[]; + affixList: any[]; eventHandlers: { [eventName: string]: any }; } @@ -53,10 +52,7 @@ export function getObserverEntities() { return observerEntities; } -export function addObserveTarget( - target: HTMLElement | Window | null, - affix: ComponentPublicInstance, -): void { +export function addObserveTarget(target: HTMLElement | Window | null, affix: T): void { if (!target) return; let entity: ObserverEntity | undefined = observerEntities.find(item => item.target === target); @@ -88,7 +84,7 @@ export function addObserveTarget( } } -export function removeObserveTarget(affix: ComponentPublicInstance): void { +export function removeObserveTarget(affix: T): void { const observerEntity = observerEntities.find(oriObserverEntity => { const hasAffix = oriObserverEntity.affixList.some(item => item === affix); if (hasAffix) { diff --git a/components/alert/index.en-US.md b/components/alert/index.en-US.md index 798566acb9..de53d0c557 100644 --- a/components/alert/index.en-US.md +++ b/components/alert/index.en-US.md @@ -19,7 +19,7 @@ Alert component for feedback. | afterClose | Called when close animation is finished | () => void | - | | | banner | Whether to show as banner | boolean | false | | | closable | Whether Alert can be closed | boolean | | | -| closeIcon | Custom close icon | slot | | 3.0 | +| closeIcon | Custom close icon | slot | `` | 3.0 | | closeText | Close text to show | string\|slot | - | | | description | Additional content of Alert | string\|slot | - | | | icon | Custom icon, effective when `showIcon` is `true` | vnode \| slot | - | | diff --git a/components/alert/index.zh-CN.md b/components/alert/index.zh-CN.md index f754d25a88..b7ae476aee 100644 --- a/components/alert/index.zh-CN.md +++ b/components/alert/index.zh-CN.md @@ -20,7 +20,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/8emPa3fjl/Alert.svg | afterClose | 关闭动画结束后触发的回调函数 | () => void | - | | | banner | 是否用作顶部公告 | boolean | false | | | closable | 默认不显示关闭按钮 | boolean | 无 | | -| closeIcon | 自定义关闭 Icon | slot | | 3.0 | +| closeIcon | 自定义关闭 Icon | slot | `` | 3.0 | | closeText | 自定义关闭按钮 | string\|slot | 无 | | | description | 警告提示的辅助性文字介绍 | string\|slot | 无 | | | icon | 自定义图标,`showIcon` 为 `true` 时有效 | vnode\|slot | - | | diff --git a/components/vc-checkbox/Checkbox.tsx b/components/vc-checkbox/Checkbox.tsx index a8b7ea89a2..854659f188 100644 --- a/components/vc-checkbox/Checkbox.tsx +++ b/components/vc-checkbox/Checkbox.tsx @@ -114,7 +114,7 @@ export default defineComponent({ onKeyup, } = attrs as HTMLAttributes; const globalProps = Object.keys({ ...others, ...attrs }).reduce((prev, key) => { - if (key.substr(0, 5) === 'aria-' || key.substr(0, 5) === 'data-' || key === 'role') { + if (key.startsWith('data-') || key.startsWith('aria-') || key === 'role') { prev[key] = others[key]; } return prev; diff --git a/components/vc-notification/Notice.tsx b/components/vc-notification/Notice.tsx index e29039a88b..61fda86a62 100644 --- a/components/vc-notification/Notice.tsx +++ b/components/vc-notification/Notice.tsx @@ -101,7 +101,7 @@ export default defineComponent({ const componentClass = `${prefixCls}-notice`; const dataOrAriaAttributeProps = Object.keys(attrs).reduce( (acc: Record, key: string) => { - if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-' || key === 'role') { + if (key.startsWith('data-') || key.startsWith('aria-') || key === 'role') { acc[key] = (attrs as any)[key]; } return acc; diff --git a/components/vc-picker/utils/miscUtil.ts b/components/vc-picker/utils/miscUtil.ts index 4ba6c67f2b..9900e151c5 100644 --- a/components/vc-picker/utils/miscUtil.ts +++ b/components/vc-picker/utils/miscUtil.ts @@ -21,11 +21,8 @@ export default function getDataOrAriaProps(props: any) { Object.keys(props).forEach(key => { if ( - (key.substr(0, 5) === 'data-' || - key.substr(0, 5) === 'aria-' || - key === 'role' || - key === 'name') && - key.substr(0, 7) !== 'data-__' + (key.startsWith('data-') || key.startsWith('aria-') || key === 'role' || key === 'name') && + !key.startsWith('data-__') ) { retProps[key] = props[key]; } diff --git a/components/vc-table/utils/legacyUtil.ts b/components/vc-table/utils/legacyUtil.ts index a4303f4060..7d075d3722 100644 --- a/components/vc-table/utils/legacyUtil.ts +++ b/components/vc-table/utils/legacyUtil.ts @@ -52,7 +52,7 @@ export function getExpandableProps( export function getDataAndAriaProps(props: object) { /* eslint-disable no-param-reassign */ return Object.keys(props).reduce((memo, key) => { - if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-') { + if (key.startsWith('data-') || key.startsWith('aria-')) { memo[key] = props[key]; } return memo; From 894a5b955c1ad78a155fa909f1c62185fb105470 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 10 May 2022 15:41:07 +0800 Subject: [PATCH 003/323] fix: BackTop responsive in RTL --- components/back-top/style/responsive.less | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/components/back-top/style/responsive.less b/components/back-top/style/responsive.less index 7b21a85009..9d5da95b92 100644 --- a/components/back-top/style/responsive.less +++ b/components/back-top/style/responsive.less @@ -1,11 +1,21 @@ @media screen and (max-width: @screen-md) { .@{backtop-prefix-cls} { right: 60px; + + &-rtl { + right: auto; + left: 60px; + } } } @media screen and (max-width: @screen-xs) { .@{backtop-prefix-cls} { right: 20px; + + &-rtl { + right: auto; + left: 20px; + } } } From 78045b4b5bd53faa7fd8bc27a50d00573e021857 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 10 May 2022 15:43:23 +0800 Subject: [PATCH 004/323] fix: Badge Animation enter and leave in RTL --- components/badge/style/rtl.less | 35 ++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/components/badge/style/rtl.less b/components/badge/style/rtl.less index 7d7c02867c..276a6ef6ea 100644 --- a/components/badge/style/rtl.less +++ b/components/badge/style/rtl.less @@ -6,7 +6,7 @@ &:not(&-not-a-wrapper) &-count, &:not(&-not-a-wrapper) &-dot, &:not(&-not-a-wrapper) .@{number-prefix-cls}-custom-component { - .@{badge-prefix-cls}-rtl & { + .@{badge-prefix-cls}-rtl& { right: auto; left: 0; direction: ltr; @@ -30,6 +30,17 @@ } } } + + &:not(&-not-a-wrapper).@{badge-prefix-cls}-rtl { + .@{badge-prefix-cls}-zoom-appear, + .@{badge-prefix-cls}-zoom-enter { + animation-name: antZoomBadgeInRtl; + } + + .@{badge-prefix-cls}-zoom-leave { + animation-name: antZoomBadgeOutRtl; + } + } } .@{ribbon-prefix-cls}-rtl { @@ -65,3 +76,25 @@ } } } + +@keyframes antZoomBadgeInRtl { + 0% { + transform: scale(0) translate(-50%, -50%); + opacity: 0; + } + + 100% { + transform: scale(1) translate(-50%, -50%); + } +} + +@keyframes antZoomBadgeOutRtl { + 0% { + transform: scale(1) translate(-50%, -50%); + } + + 100% { + transform: scale(0) translate(-50%, -50%); + opacity: 0; + } +} From 3aedf48eaf37666618ed02bcb2fae38c406d812a Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 10 May 2022 15:48:14 +0800 Subject: [PATCH 005/323] feat: Breadcrumb accessibility improvements --- components/breadcrumb/Breadcrumb.tsx | 6 +++++- components/breadcrumb/BreadcrumbItem.tsx | 12 ++++++------ components/breadcrumb/style/index.less | 12 ++++++++++-- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/components/breadcrumb/Breadcrumb.tsx b/components/breadcrumb/Breadcrumb.tsx index cbb49077d3..40844ab509 100644 --- a/components/breadcrumb/Breadcrumb.tsx +++ b/components/breadcrumb/Breadcrumb.tsx @@ -152,7 +152,11 @@ export default defineComponent({ [prefixCls.value]: true, [`${prefixCls.value}-rtl`]: direction.value === 'rtl', }; - return
{crumbs}
; + return ( + + ); }; }, }); diff --git a/components/breadcrumb/BreadcrumbItem.tsx b/components/breadcrumb/BreadcrumbItem.tsx index a1ce517578..5ef07f8550 100644 --- a/components/breadcrumb/BreadcrumbItem.tsx +++ b/components/breadcrumb/BreadcrumbItem.tsx @@ -2,7 +2,7 @@ import type { ExtractPropTypes, PropType } from 'vue'; import { defineComponent } from 'vue'; import PropTypes from '../_util/vue-types'; import { getPropsSlot } from '../_util/props-util'; -import DropDown from '../dropdown/dropdown'; +import Dropdown from '../dropdown/dropdown'; import DownOutlined from '@ant-design/icons-vue/DownOutlined'; import useConfigInject from '../_util/hooks/useConfigInject'; import type { MouseEventHandler } from '../_util/EventInterface'; @@ -27,18 +27,18 @@ export default defineComponent({ const { prefixCls } = useConfigInject('breadcrumb', props); /** * if overlay is have - * Wrap a DropDown + * Wrap a Dropdown */ const renderBreadcrumbNode = (breadcrumbItem: JSX.Element, prefixCls: string) => { const overlay = getPropsSlot(slots, props, 'overlay'); if (overlay) { return ( - + {breadcrumbItem} - + ); } return breadcrumbItem; @@ -66,10 +66,10 @@ export default defineComponent({ link = renderBreadcrumbNode(link, prefixCls.value); if (children) { return ( - +
  • {link} {separator && {separator}} - +
  • ); } return null; diff --git a/components/breadcrumb/style/index.less b/components/breadcrumb/style/index.less index ca29afb3a9..79dee8aec9 100644 --- a/components/breadcrumb/style/index.less +++ b/components/breadcrumb/style/index.less @@ -13,6 +13,14 @@ font-size: @breadcrumb-icon-font-size; } + ol { + display: flex; + flex-wrap: wrap; + margin: 0; + padding: 0; + list-style: none; + } + a { color: @breadcrumb-link-color; transition: color 0.3s; @@ -22,7 +30,7 @@ } } - & > span:last-child { + li:last-child { color: @breadcrumb-last-item-color; a { @@ -30,7 +38,7 @@ } } - & > span:last-child &-separator { + li:last-child &-separator { display: none; } From 0244e7f5b416994fe5c9fd9a32d0b392d5b620cd Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 10 May 2022 16:18:44 +0800 Subject: [PATCH 006/323] refactor: Simplify Button Group Style --- components/_util/createContext.ts | 17 +++++++++++++++++ components/button/button-group.tsx | 14 ++++++++++---- components/button/button.tsx | 15 ++++++++------- components/button/style/mixin.less | 22 ---------------------- 4 files changed, 35 insertions(+), 33 deletions(-) create mode 100644 components/_util/createContext.ts diff --git a/components/_util/createContext.ts b/components/_util/createContext.ts new file mode 100644 index 0000000000..aa68a11e8b --- /dev/null +++ b/components/_util/createContext.ts @@ -0,0 +1,17 @@ +import { inject, provide } from 'vue'; + +function createContext() { + const contextKey = Symbol('contextKey'); + const useProvide = (props: T) => { + provide(contextKey, props); + }; + const useInject = () => { + return inject(contextKey, undefined as T) || ({} as T); + }; + return { + useProvide, + useInject, + }; +} + +export default createContext; diff --git a/components/button/button-group.tsx b/components/button/button-group.tsx index fd543d9ff8..19c2bf42ae 100644 --- a/components/button/button-group.tsx +++ b/components/button/button-group.tsx @@ -2,9 +2,10 @@ import { computed, defineComponent } from 'vue'; import { flattenChildren } from '../_util/props-util'; import useConfigInject from '../_util/hooks/useConfigInject'; -import type { ExtractPropTypes, PropType } from 'vue'; +import type { ExtractPropTypes, PropType, ComputedRef } from 'vue'; import type { SizeType } from '../config-provider'; -import UnreachableException from '../_util/unreachableException'; +import devWarning from '../vc-util/devWarning'; +import createContext from '../_util/createContext'; export const buttonGroupProps = () => ({ prefixCls: String, @@ -14,12 +15,17 @@ export const buttonGroupProps = () => ({ }); export type ButtonGroupProps = Partial>>; - +export const GroupSizeContext = createContext<{ + size: ComputedRef; +}>(); export default defineComponent({ name: 'AButtonGroup', props: buttonGroupProps(), setup(props, { slots }) { const { prefixCls, direction } = useConfigInject('btn-group', props); + GroupSizeContext.useProvide({ + size: computed(() => props.size), + }); const classes = computed(() => { const { size } = props; // large => lg @@ -37,7 +43,7 @@ export default defineComponent({ break; default: // eslint-disable-next-line no-console - console.warn(new UnreachableException(size).error); + devWarning(!size, 'Button.Group', 'Invalid prop `size`.'); } return { [`${prefixCls.value}`]: true, diff --git a/components/button/button.tsx b/components/button/button.tsx index d5a1a3960b..658a42ce03 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -18,13 +18,14 @@ import LoadingIcon from './LoadingIcon'; import type { ButtonType } from './buttonTypes'; import type { VNode, Ref } from 'vue'; +import { GroupSizeContext } from './button-group'; type Loading = boolean | number; const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/; const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar); -function isUnborderedButtonType(type: ButtonType | undefined) { +function isUnBorderedButtonType(type: ButtonType | undefined) { return type === 'text' || type === 'link'; } export { buttonProps }; @@ -37,7 +38,7 @@ export default defineComponent({ // emits: ['click', 'mousedown'], setup(props, { slots, attrs, emit }) { const { prefixCls, autoInsertSpaceInButton, direction, size } = useConfigInject('btn', props); - + const { size: groupSize } = GroupSizeContext.useInject(); const buttonNodeRef = ref(null); const delayTimeoutRef = ref(undefined); let isNeedInserted = false; @@ -76,7 +77,7 @@ export default defineComponent({ const pre = prefixCls.value; const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined }; - const sizeFullname = size.value; + const sizeFullname = groupSize?.value || size.value; const sizeCls = sizeFullname ? sizeClassNameMap[sizeFullname] || '' : ''; return { @@ -85,7 +86,7 @@ export default defineComponent({ [`${pre}-${shape}`]: shape !== 'default' && shape, [`${pre}-${sizeCls}`]: sizeCls, [`${pre}-loading`]: innerLoading.value, - [`${pre}-background-ghost`]: ghost && !isUnborderedButtonType(type), + [`${pre}-background-ghost`]: ghost && !isUnBorderedButtonType(type), [`${pre}-two-chinese-chars`]: hasTwoCNChar.value && autoInsertSpace.value, [`${pre}-block`]: block, [`${pre}-dangerous`]: !!danger, @@ -132,7 +133,7 @@ export default defineComponent({ watchEffect(() => { devWarning( - !(props.ghost && isUnborderedButtonType(props.type)), + !(props.ghost && isUnBorderedButtonType(props.type)), 'Button', "`link` or `text` button can't be a `ghost` button.", ); @@ -149,7 +150,7 @@ export default defineComponent({ const { icon = slots.icon?.() } = props; const children = flattenChildren(slots.default?.()); - isNeedInserted = children.length === 1 && !icon && !isUnborderedButtonType(props.type); + isNeedInserted = children.length === 1 && !icon && !isUnBorderedButtonType(props.type); const { type, htmlType, disabled, href, title, target, onMousedown } = props; @@ -202,7 +203,7 @@ export default defineComponent({ ); - if (isUnborderedButtonType(type)) { + if (isUnBorderedButtonType(type)) { return buttonNode; } diff --git a/components/button/style/mixin.less b/components/button/style/mixin.less index 96efadc8d5..084ecaa5bf 100644 --- a/components/button/style/mixin.less +++ b/components/button/style/mixin.less @@ -210,28 +210,6 @@ .@{btnClassName}-icon-only { font-size: @font-size-base; } - // size - &-lg > .@{btnClassName}, - &-lg > span > .@{btnClassName} { - .button-size(@btn-height-lg; @btn-padding-horizontal-lg; @btn-font-size-lg; 0); - } - &-lg .@{btnClassName}.@{btnClassName}-icon-only { - .square(@btn-height-lg); - padding-right: 0; - padding-left: 0; - } - &-sm > .@{btnClassName}, - &-sm > span > .@{btnClassName} { - .button-size(@btn-height-sm; @btn-padding-horizontal-sm; @font-size-base; 0); - > .@{iconfont-css-prefix} { - font-size: @font-size-base; - } - } - &-sm .@{btnClassName}.@{btnClassName}-icon-only { - .square(@btn-height-sm); - padding-right: 0; - padding-left: 0; - } } // Base styles of buttons // -------------------------------------------------- From c4a61f210fb02d5bc52a24fd8030f25928cb6035 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 11 May 2022 09:58:23 +0800 Subject: [PATCH 007/323] feat: menu add itemsType --- components/menu/src/Menu.tsx | 31 +++-- components/menu/src/PopupTrigger.tsx | 2 + components/menu/src/SubMenu.tsx | 4 +- components/menu/src/hooks/useItems.tsx | 136 ++++++++++++++++++++ components/menu/src/hooks/useMenuContext.ts | 9 +- components/menu/src/interface.ts | 68 ++++++++++ 6 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 components/menu/src/hooks/useItems.tsx diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index dbcc976b45..8e8be9fa35 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -1,6 +1,7 @@ import type { Key } from '../../_util/type'; import type { ExtractPropTypes, PropType, VNode } from 'vue'; import { + shallowRef, Teleport, computed, defineComponent, @@ -38,10 +39,14 @@ import { cloneElement } from '../../_util/vnode'; import { OVERFLOW_KEY, PathContext } from './hooks/useKeyPath'; import type { FocusEventHandler, MouseEventHandler } from '../../_util/EventInterface'; import collapseMotion from '../../_util/collapseMotion'; +import type { ItemType } from './hooks/useItems'; +import useItems from './hooks/useItems'; export const menuProps = () => ({ id: String, prefixCls: String, + // donot use items, now only support inner use + items: Array as PropType, disabled: Boolean, inlineCollapsed: Boolean, disabledOverflow: Boolean, @@ -90,7 +95,7 @@ export default defineComponent({ slots: ['expandIcon', 'overflowedIndicator'], setup(props, { slots, emit, attrs }) { const { prefixCls, direction, getPrefixCls } = useConfigInject('menu', props); - const store = ref>({}); + const store = shallowRef>(new Map()); const siderCollapsed = inject(SiderCollapsedKey, ref(undefined)); const inlineCollapsed = computed(() => { if (siderCollapsed.value !== undefined) { @@ -98,7 +103,7 @@ export default defineComponent({ } return props.inlineCollapsed; }); - + const { itemsNodes } = useItems(props); const isMounted = ref(false); onMounted(() => { isMounted.value = true; @@ -115,6 +120,11 @@ export default defineComponent({ 'Menu', '`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.', ); + // devWarning( + // !!props.items && !slots.default, + // 'Menu', + // '`children` will be removed in next major version. Please use `items` instead.', + // ); }); const activeKeys = ref([]); @@ -124,7 +134,7 @@ export default defineComponent({ store, () => { const newKeyMapStore = {}; - for (const menuInfo of Object.values(store.value)) { + for (const menuInfo of store.value.values()) { newKeyMapStore[menuInfo.key] = menuInfo; } keyMapStore.value = newKeyMapStore; @@ -322,8 +332,8 @@ export default defineComponent({ const keys = []; const storeValue = store.value; eventKeys.forEach(eventKey => { - const { key, childrenEventKeys } = storeValue[eventKey]; - keys.push(key, ...getChildrenKeys(childrenEventKeys)); + const { key, childrenEventKeys } = storeValue.get(eventKey); + keys.push(key, ...getChildrenKeys(unref(childrenEventKeys))); }); return keys; }; @@ -355,11 +365,12 @@ export default defineComponent({ }; const registerMenuInfo = (key: string, info: StoreMenuInfo) => { - store.value = { ...store.value, [key]: info as any }; + store.value.set(key, info); + store.value = new Map(store.value); }; const unRegisterMenuInfo = (key: string) => { - delete store.value[key]; - store.value = { ...store.value }; + store.value.delete(key); + store.value = new Map(store.value); }; const lastVisibleIndex = ref(0); @@ -379,7 +390,6 @@ export default defineComponent({ : null, ); useProvideMenu({ - store, prefixCls, activeKeys, openKeys: mergedOpenKeys, @@ -408,9 +418,10 @@ export default defineComponent({ isRootMenu: ref(true), expandIcon, forceSubMenuRender: computed(() => props.forceSubMenuRender), + rootClassName: computed(() => ''), }); return () => { - const childList = flattenChildren(slots.default?.()); + const childList = itemsNodes.value || flattenChildren(slots.default?.()); const allVisible = lastVisibleIndex.value >= childList.length - 1 || mergedMode.value !== 'horizontal' || diff --git a/components/menu/src/PopupTrigger.tsx b/components/menu/src/PopupTrigger.tsx index 09996d427a..661e9f11de 100644 --- a/components/menu/src/PopupTrigger.tsx +++ b/components/menu/src/PopupTrigger.tsx @@ -42,6 +42,7 @@ export default defineComponent({ forceSubMenuRender, motion, defaultMotions, + rootClassName, } = useInjectMenu(); const forceRender = useInjectForceRender(); const placement = computed(() => @@ -86,6 +87,7 @@ export default defineComponent({ [`${prefixCls}-rtl`]: rtl.value, }, popupClassName, + rootClassName.value, )} stretch={mode === 'horizontal' ? 'minWidth' : null} getPopupContainer={ diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index 3eb4269f82..e0e332614f 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -21,6 +21,7 @@ import devWarning from '../../vc-util/devWarning'; import isValid from '../../_util/isValid'; import type { MouseEventHandler } from '../../_util/EventInterface'; import type { Key } from 'ant-design-vue/es/_util/type'; +import type { MenuTheme } from './interface'; let indexGuid = 0; @@ -34,6 +35,7 @@ export const subMenuProps = () => ({ internalPopupClose: Boolean, eventKey: String, expandIcon: Function as PropType<(p?: { isOpen: boolean; [key: string]: any }) => any>, + theme: String as PropType, onMouseenter: Function as PropType, onMouseleave: Function as PropType, onTitleClick: Function as PropType<(e: MouseEvent, key: Key) => void>, @@ -193,7 +195,7 @@ export default defineComponent({ const popupClassName = computed(() => classNames( prefixCls.value, - `${prefixCls.value}-${antdMenuTheme.value}`, + `${prefixCls.value}-${props.theme || antdMenuTheme.value}`, props.popupClassName, ), ); diff --git a/components/menu/src/hooks/useItems.tsx b/components/menu/src/hooks/useItems.tsx new file mode 100644 index 0000000000..a140f1aff5 --- /dev/null +++ b/components/menu/src/hooks/useItems.tsx @@ -0,0 +1,136 @@ +import type { + MenuItemType as VcMenuItemType, + MenuDividerType as VcMenuDividerType, + SubMenuType as VcSubMenuType, + MenuItemGroupType as VcMenuItemGroupType, +} from '../interface'; +import SubMenu from '../SubMenu'; +import ItemGroup from '../ItemGroup'; +import MenuDivider from '../Divider'; +import MenuItem from '../MenuItem'; +import type { Key } from '../../../_util/type'; +import { ref, shallowRef, watch } from 'vue'; +import type { MenuProps } from '../Menu'; +import type { StoreMenuInfo } from './useMenuContext'; + +interface MenuItemType extends VcMenuItemType { + danger?: boolean; + icon?: any; + title?: string; +} + +interface SubMenuType extends Omit { + icon?: any; + theme?: 'dark' | 'light'; + children: ItemType[]; +} + +interface MenuItemGroupType extends Omit { + children?: MenuItemType[]; + key?: Key; +} + +interface MenuDividerType extends VcMenuDividerType { + dashed?: boolean; + key?: Key; +} + +export type ItemType = MenuItemType | SubMenuType | MenuItemGroupType | MenuDividerType | null; + +function convertItemsToNodes( + list: ItemType[], + store: Map, + parentMenuInfo?: { + childrenEventKeys: string[]; + parentKeys: string[]; + }, +) { + return (list || []) + .map((opt, index) => { + if (opt && typeof opt === 'object') { + const { label, children, key, type, ...restProps } = opt as any; + const mergedKey = key ?? `tmp-${index}`; + // 此处 eventKey === key, 移除 children 后可以移除 eventKey + const parentKeys = parentMenuInfo ? parentMenuInfo.parentKeys.slice() : []; + const childrenEventKeys = []; + // if + const menuInfo = { + eventKey: mergedKey, + key: mergedKey, + parentEventKeys: ref(parentKeys), + parentKeys: ref(parentKeys), + childrenEventKeys: ref(childrenEventKeys), + isLeaf: false, + }; + + // MenuItemGroup & SubMenuItem + if (children || type === 'group') { + if (type === 'group') { + const childrenNodes = convertItemsToNodes(children, store, parentMenuInfo); + // Group + return ( + + {childrenNodes} + + ); + } + store.set(mergedKey, menuInfo); + if (parentMenuInfo) { + parentMenuInfo.childrenEventKeys.push(mergedKey); + } + // Sub Menu + const childrenNodes = convertItemsToNodes(children, store, { + childrenEventKeys, + parentKeys: [].concat(parentKeys, mergedKey), + }); + return ( + + {childrenNodes} + + ); + } + + // MenuItem & Divider + if (type === 'divider') { + return ; + } + menuInfo.isLeaf = true; + store.set(mergedKey, menuInfo); + return ( + + {label} + + ); + } + + return null; + }) + .filter(opt => opt); +} + +// FIXME: Move logic here in v4 +/** + * We simply convert `items` to VueNode for reuse origin component logic. But we need move all the + * logic from component into this hooks when in v4 + */ +export default function useItems(props: MenuProps) { + const itemsNodes = shallowRef([]); + const hasItmes = ref(false); + const store = shallowRef>(new Map()); + watch( + () => props.items, + () => { + const newStore = new Map(); + hasItmes.value = false; + if (props.items) { + hasItmes.value = true; + itemsNodes.value = convertItemsToNodes(props.items as ItemType[], newStore); + } else { + itemsNodes.value = undefined; + } + store.value = newStore; + }, + { immediate: true, deep: true }, + ); + return { itemsNodes, store, hasItmes }; +} diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 81bd8c76d7..fc4cfdd371 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -1,5 +1,5 @@ import type { Key } from '../../../_util/type'; -import type { ComputedRef, InjectionKey, PropType, Ref, UnwrapRef } from 'vue'; +import type { ComputedRef, InjectionKey, PropType, Ref } from 'vue'; import { defineComponent, inject, provide, toRef } from 'vue'; import type { BuiltinPlacements, @@ -13,15 +13,14 @@ import type { CSSMotionProps } from '../../../_util/transition'; export interface StoreMenuInfo { eventKey: string; key: Key; - parentEventKeys: ComputedRef; + parentEventKeys: Ref; childrenEventKeys?: Ref; isLeaf?: boolean; - parentKeys: ComputedRef; + parentKeys: Ref; } export interface MenuContextProps { isRootMenu: Ref; - - store: Ref>>; + rootClassName: Ref; registerMenuInfo: (key: string, info: StoreMenuInfo) => void; unRegisterMenuInfo: (key: string) => void; prefixCls: ComputedRef; diff --git a/components/menu/src/interface.ts b/components/menu/src/interface.ts index 19afc7116b..137b67039e 100644 --- a/components/menu/src/interface.ts +++ b/components/menu/src/interface.ts @@ -1,6 +1,74 @@ +import type { CSSProperties } from 'vue'; import type { Key } from '../../_util/type'; import type { MenuItemProps } from './MenuItem'; +// ========================= Options ========================= +interface ItemSharedProps { + style?: CSSProperties; + class?: string; +} + +export interface SubMenuType extends ItemSharedProps { + label?: any; + + children: ItemType[]; + + disabled?: boolean; + + key: string; + + rootClassName?: string; + + // >>>>> Icon + itemIcon?: RenderIconType; + expandIcon?: RenderIconType; + + // >>>>> Active + onMouseenter?: MenuHoverEventHandler; + onMouseleave?: MenuHoverEventHandler; + + // >>>>> Popup + popupClassName?: string; + popupOffset?: number[]; + + // >>>>> Events + onClick?: MenuClickEventHandler; + onTitleClick?: (info: MenuTitleInfo) => void; + onTitleMouseenter?: MenuHoverEventHandler; + onTitleMouseleave?: MenuHoverEventHandler; +} + +export interface MenuItemType extends ItemSharedProps { + label?: any; + + disabled?: boolean; + + itemIcon?: RenderIconType; + + key: Key; + + // >>>>> Active + onMouseenter?: MenuHoverEventHandler; + onMouseleave?: MenuHoverEventHandler; + + // >>>>> Events + onClick?: MenuClickEventHandler; +} + +export interface MenuItemGroupType extends ItemSharedProps { + type: 'group'; + + label?: any; + + children?: ItemType[]; +} + +export interface MenuDividerType extends ItemSharedProps { + type: 'divider'; +} + +export type ItemType = SubMenuType | MenuItemType | MenuItemGroupType | MenuDividerType | null; + export type MenuTheme = 'light' | 'dark'; // ========================== Basic ========================== From 6e1f30666bc532398f841b7aec0765c04c8f81d3 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 11 May 2022 21:52:51 +0800 Subject: [PATCH 008/323] refactor: form --- components/_util/createContext.ts | 4 +- components/calendar/Header.tsx | 12 +- components/calendar/style/index.less | 3 + components/calendar/style/index.tsx | 2 +- components/form/FormItem.tsx | 40 +++- components/form/FormItemContext.ts | 18 +- components/form/FormItemInput.tsx | 21 +- components/form/demo/validate-static.vue | 172 +++++++++++++++ components/form/style/components.less | 58 +---- components/form/style/horizontal.less | 4 +- components/form/style/index.less | 33 ++- components/form/style/mixin.less | 34 --- components/form/style/status.less | 268 ----------------------- 13 files changed, 281 insertions(+), 388 deletions(-) create mode 100644 components/form/demo/validate-static.vue diff --git a/components/_util/createContext.ts b/components/_util/createContext.ts index aa68a11e8b..59c322f1af 100644 --- a/components/_util/createContext.ts +++ b/components/_util/createContext.ts @@ -1,12 +1,12 @@ import { inject, provide } from 'vue'; -function createContext() { +function createContext(defaultValue?: T) { const contextKey = Symbol('contextKey'); const useProvide = (props: T) => { provide(contextKey, props); }; const useInject = () => { - return inject(contextKey, undefined as T) || ({} as T); + return inject(contextKey, defaultValue as T) || ({} as T); }; return { useProvide, diff --git a/components/calendar/Header.tsx b/components/calendar/Header.tsx index 0c122df619..ebb749bd69 100644 --- a/components/calendar/Header.tsx +++ b/components/calendar/Header.tsx @@ -2,9 +2,10 @@ import Select from '../select'; import { Group, Button } from '../radio'; import type { CalendarMode } from './generateCalendar'; import type { Ref } from 'vue'; -import { defineComponent, ref } from 'vue'; +import { reactive, watchEffect, defineComponent, ref } from 'vue'; import type { Locale } from '../vc-picker/interface'; import type { GenerateConfig } from '../vc-picker/generate'; +import { FormItemInputContext } from '../form/FormItemContext'; const YearSelectOffset = 10; const YearSelectTotal = 20; @@ -168,6 +169,15 @@ export default defineComponent>({ ] as any, setup(_props, { attrs }) { const divRef = ref(null); + const formItemInputContext = FormItemInputContext.useInject(); + const newFormItemInputContext = reactive({}); + FormItemInputContext.useProvide(newFormItemInputContext); + watchEffect(() => { + Object.assign(newFormItemInputContext, formItemInputContext, { + isFormItemInput: false, + }); + }); + return () => { const props = { ..._props, ...attrs }; const { prefixCls, fullscreen, mode, onChange, onModeChange } = props; diff --git a/components/calendar/style/index.less b/components/calendar/style/index.less index af0e8a4a6d..124e8d9efb 100644 --- a/components/calendar/style/index.less +++ b/components/calendar/style/index.less @@ -70,6 +70,9 @@ line-height: 18px; } } + .@{calendar-picker-prefix-cls}-cell::before { + pointer-events: none; + } } // ========================== Full ========================== diff --git a/components/calendar/style/index.tsx b/components/calendar/style/index.tsx index 045b1fa400..1ee59b4a6b 100644 --- a/components/calendar/style/index.tsx +++ b/components/calendar/style/index.tsx @@ -2,7 +2,7 @@ import '../../style/index.less'; import './index.less'; // style dependencies -// deps-lint-skip: date-picker +// deps-lint-skip: date-picker, form import '../../select/style'; import '../../radio/style'; import '../../date-picker/style'; diff --git a/components/form/FormItem.tsx b/components/form/FormItem.tsx index 62f4a99fdc..eb9068ee0e 100644 --- a/components/form/FormItem.tsx +++ b/components/form/FormItem.tsx @@ -7,6 +7,7 @@ import type { HTMLAttributes, } from 'vue'; import { + reactive, watch, defineComponent, computed, @@ -16,6 +17,10 @@ import { onBeforeUnmount, toRaw, } from 'vue'; +import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; +import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; +import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled'; +import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled'; import cloneDeep from 'lodash-es/cloneDeep'; import PropTypes from '../_util/vue-types'; import Row from '../grid/Row'; @@ -33,8 +38,10 @@ import { useInjectForm } from './context'; import FormItemLabel from './FormItemLabel'; import FormItemInput from './FormItemInput'; import type { ValidationRule } from './Form'; -import { useProvideFormItemContext } from './FormItemContext'; +import type { FormItemStatusContextProps } from './FormItemContext'; +import { FormItemInputContext, useProvideFormItemContext } from './FormItemContext'; import useDebounce from './utils/useDebounce'; +import classNames from '../_util/classNames'; const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', ''); export type ValidateStatus = typeof ValidateStatuses[number]; @@ -50,6 +57,13 @@ export interface FieldExpose { validateRules: (options: ValidateOptions) => Promise | Promise; } +const iconMap: { [key: string]: any } = { + success: CheckCircleFilled, + warning: ExclamationCircleFilled, + error: CloseCircleFilled, + validating: LoadingOutlined, +}; + function getPropByPath(obj: any, namePathList: any, strict?: boolean) { let tempObj = obj; @@ -391,6 +405,30 @@ export default defineComponent({ [`${prefixCls.value}-item-is-validating`]: mergedValidateStatus.value === 'validating', [`${prefixCls.value}-item-hidden`]: props.hidden, })); + const formItemInputContext = reactive({}); + FormItemInputContext.useProvide(formItemInputContext); + watchEffect(() => { + let feedbackIcon: any; + if (props.hasFeedback) { + const IconNode = mergedValidateStatus.value && iconMap[mergedValidateStatus.value]; + feedbackIcon = IconNode ? ( + + + + ) : null; + } + Object.assign(formItemInputContext, { + status: mergedValidateStatus.value, + hasFeedback: props.hasFeedback, + feedbackIcon, + isFormItemInput: true, + }); + }); return () => { if (props.noStyle) return slots.default?.(); const help = props.help ?? (slots.help ? filterEmpty(slots.help()) : null); diff --git a/components/form/FormItemContext.ts b/components/form/FormItemContext.ts index b2010b730d..7ebb255084 100644 --- a/components/form/FormItemContext.ts +++ b/components/form/FormItemContext.ts @@ -1,4 +1,4 @@ -import type { ComputedRef, InjectionKey, ConcreteComponent } from 'vue'; +import type { ComputedRef, InjectionKey, ConcreteComponent, FunctionalComponent } from 'vue'; import { watch, computed, @@ -10,6 +10,8 @@ import { defineComponent, } from 'vue'; import devWarning from '../vc-util/devWarning'; +import createContext from '../_util/createContext'; +import type { ValidateStatus } from './FormItem'; export type FormItemContext = { id: ComputedRef; @@ -103,3 +105,17 @@ export default defineComponent({ }; }, }); + +export interface FormItemStatusContextProps { + isFormItemInput?: boolean; + status?: ValidateStatus; + hasFeedback?: boolean; + feedbackIcon?: any; +} + +export const FormItemInputContext = createContext({}); + +export const NoFormStatus: FunctionalComponent = (_, { slots }) => { + FormItemInputContext.useProvide({}); + return slots.default?.(); +}; diff --git a/components/form/FormItemInput.tsx b/components/form/FormItemInput.tsx index 630fbec639..3ccaa0a004 100644 --- a/components/form/FormItemInput.tsx +++ b/components/form/FormItemInput.tsx @@ -1,8 +1,3 @@ -import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; -import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; -import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled'; -import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled'; - import type { ColProps } from '../grid/Col'; import Col from '../grid/Col'; import { useProvideForm, useInjectForm, useProvideFormItemPrefix } from './context'; @@ -27,12 +22,6 @@ export interface FormItemInputProps { status?: ValidateStatus; } -const iconMap: { [key: string]: any } = { - success: CheckCircleFilled, - warning: ExclamationCircleFilled, - error: CloseCircleFilled, - validating: LoadingOutlined, -}; const FormItemInput = defineComponent({ slots: ['help', 'extra', 'errors'], inheritAttrs: false, @@ -66,8 +55,8 @@ const FormItemInput = defineComponent({ wrapperCol, help = slots.help?.(), errors = slots.errors?.(), - hasFeedback, - status, + // hasFeedback, + // status, extra = slots.extra?.(), } = props; const baseClassName = `${prefixCls}-item`; @@ -78,7 +67,7 @@ const FormItemInput = defineComponent({ const className = classNames(`${baseClassName}-control`, mergedWrapperCol.class); // Should provides additional icon if `hasFeedback` - const IconNode = status && iconMap[status]; + // const IconNode = status && iconMap[status]; return (
    {slots.default?.()}
    - {hasFeedback && IconNode ? ( + {/* {hasFeedback && IconNode ? ( - ) : null} + ) : null} */}
    +--- +order: 20 +title: + zh-CN: 自定义校验 + en-US: Customized Validation +--- + +## zh-CN + +我们提供了 `validateStatus` `help` `hasFeedback` 等属性,你可以不通过 Form 自己定义校验的时机和内容。 + +1. `validateStatus`: 校验状态,可选 'success', 'warning', 'error', 'validating'。 +2. `hasFeedback`:用于给输入框添加反馈图标。 +3. `help`:设置校验文案。 + +## en-US + +We provide properties like `validateStatus` `help` `hasFeedback` to customize your own validate status and message, without using Form. + +1. `validateStatus`: validate status of form components which could be 'success', 'warning', 'error', 'validating'. +2. `hasFeedback`: display feed icon of input control +3. `help`: display validate message. + + + diff --git a/components/form/style/components.less b/components/form/style/components.less index 53d4773d9b..10bf6cb964 100644 --- a/components/form/style/components.less +++ b/components/form/style/components.less @@ -6,67 +6,11 @@ // ================================================================ // = Children Component = // ================================================================ +// FIXME: useless, remove in v5 .@{form-item-prefix-cls} { - // input[type=file] - .@{ant-prefix}-upload { - background: transparent; - } - .@{ant-prefix}-upload.@{ant-prefix}-upload-drag { - background: @background-color-light; - } - - input[type='radio'], - input[type='checkbox'] { - width: 14px; - height: 14px; - } - - // Radios and checkboxes on same line - .@{ant-prefix}-radio-inline, - .@{ant-prefix}-checkbox-inline { - display: inline-block; - margin-left: 8px; - font-weight: normal; - vertical-align: middle; - cursor: pointer; - - &:first-child { - margin-left: 0; - } - } - - .@{ant-prefix}-checkbox-vertical, - .@{ant-prefix}-radio-vertical { - display: block; - } - - .@{ant-prefix}-checkbox-vertical + .@{ant-prefix}-checkbox-vertical, - .@{ant-prefix}-radio-vertical + .@{ant-prefix}-radio-vertical { - margin-left: 0; - } - .@{ant-prefix}-input-number { + .@{form-prefix-cls}-text { margin-left: 8px; } - - &-handler-wrap { - z-index: 2; // https://github.com/ant-design/ant-design/issues/6289 - } - } - - .@{ant-prefix}-select, - .@{ant-prefix}-cascader-picker { - width: 100%; - } - - // Don't impact select inside input group and calendar header select - .@{ant-prefix}-picker-calendar-year-select, - .@{ant-prefix}-picker-calendar-month-select, - .@{ant-prefix}-input-group .@{ant-prefix}-select, - .@{ant-prefix}-input-group .@{ant-prefix}-cascader-picker, - .@{ant-prefix}-input-number-group .@{ant-prefix}-select, - .@{ant-prefix}-input-number-group .@{ant-prefix}-cascader-picker { - width: auto; } } diff --git a/components/form/style/horizontal.less b/components/form/style/horizontal.less index a879c4bd71..73e6c62343 100644 --- a/components/form/style/horizontal.less +++ b/components/form/style/horizontal.less @@ -14,7 +14,9 @@ min-width: 0; } // https://github.com/ant-design/ant-design/issues/32980 - .@{form-item-prefix-cls}-label.@{ant-prefix}-col-24 + .@{form-item-prefix-cls}-control { + // https://github.com/ant-design/ant-design/issues/34903 + .@{form-item-prefix-cls}-label[class$='-24'] + .@{form-item-prefix-cls}-control, + .@{form-item-prefix-cls}-label[class*='-24 '] + .@{form-item-prefix-cls}-control { min-width: unset; } } diff --git a/components/form/style/index.less b/components/form/style/index.less index eb009fe416..99ee0e90a1 100644 --- a/components/form/style/index.less +++ b/components/form/style/index.less @@ -210,17 +210,38 @@ min-height: @form-item-margin-bottom; } - .@{ant-prefix}-input-textarea-show-count { - &::after { - margin-bottom: -22px; - } - } - &-with-help &-explain { height: auto; min-height: @form-item-margin-bottom; opacity: 1; } + + // ============================================================== + // = Feedback Icon = + // ============================================================== + &-feedback-icon { + font-size: @font-size-base; + text-align: center; + visibility: visible; + animation: zoomIn 0.3s @ease-out-back; + pointer-events: none; + + &-success { + color: @success-color; + } + + &-error { + color: @error-color; + } + + &-warning { + color: @warning-color; + } + + &-validating { + color: @primary-color; + } + } } // >>>>>>>>>> Motion <<<<<<<<<< diff --git a/components/form/style/mixin.less b/components/form/style/mixin.less index 603212a275..c7ca146854 100644 --- a/components/form/style/mixin.less +++ b/components/form/style/mixin.less @@ -10,40 +10,6 @@ .@{ant-prefix}-form-item-split { color: @text-color; } - // 输入框的不同校验状态 - :not(.@{ant-prefix}-input-disabled):not(.@{ant-prefix}-input-borderless).@{ant-prefix}-input, - :not(.@{ant-prefix}-input-affix-wrapper-disabled):not(.@{ant-prefix}-input-affix-wrapper-borderless).@{ant-prefix}-input-affix-wrapper, - :not(.@{ant-prefix}-input-number-affix-wrapper-disabled):not(.@{ant-prefix}-input-number-affix-wrapper-borderless).@{ant-prefix}-input-number-affix-wrapper { - &, - &:hover { - background-color: @background-color; - border-color: @border-color; - } - - &:focus, - &-focused { - .active(@border-color, @hoverBorderColor, @outlineColor); - } - } - - .@{ant-prefix}-calendar-picker-open .@{ant-prefix}-calendar-picker-input { - .active(@border-color, @hoverBorderColor, @outlineColor); - } - - .@{ant-prefix}-input-prefix, - .@{ant-prefix}-input-number-prefix { - color: @text-color; - } - - .@{ant-prefix}-input-group-addon, - .@{ant-prefix}-input-number-group-addon { - color: @text-color; - border-color: @border-color; - } - - .has-feedback { - color: @text-color; - } } // Reset form styles diff --git a/components/form/style/status.less b/components/form/style/status.less index e16cf5753b..1a53d97a96 100644 --- a/components/form/style/status.less +++ b/components/form/style/status.less @@ -24,287 +24,19 @@ } &-has-feedback { - // ========================= Input ========================= - .@{ant-prefix}-input { - padding-right: 24px; - } - // https://github.com/ant-design/ant-design/issues/19884 - .@{ant-prefix}-input-affix-wrapper { - .@{ant-prefix}-input-suffix { - padding-right: 18px; - } - } - - // Fix issue: https://github.com/ant-design/ant-design/issues/7854 - .@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) { - .@{ant-prefix}-input-suffix { - right: 28px; - } - } - // ======================== Switch ========================= .@{ant-prefix}-switch { margin: 2px 0 4px; } - - // ======================== Select ========================= - // Fix overlapping between feedback icon and ; - return withDirectives(inputNode as VNode, [[antInputDirective]]); - }; - - const renderShowCountSuffix = () => { - const value = stateValue.value; - const { maxlength, suffix = slots.suffix?.(), showCount } = props; - // Max length value - const hasMaxLength = Number(maxlength) > 0; - - if (suffix || showCount) { - const valueLength = [...fixControlledValue(value)].length; - let dataCount = null; - if (typeof showCount === 'object') { - dataCount = showCount.formatter({ count: valueLength, maxlength }); - } else { - dataCount = `${valueLength}${hasMaxLength ? ` / ${maxlength}` : ''}`; - } - return ( - <> - {!!showCount && ( - - {dataCount} - - )} - {suffix} - - ); - } - return null; - }; - - return () => { - const inputProps: any = { - ...attrs, - ...props, - prefixCls: prefixCls.value, - inputType: 'input', - value: fixControlledValue(stateValue.value), - handleReset, - focused: focused.value && !props.disabled, - }; - + const suffixNode = (hasFeedback || suffix) && ( + <> + {suffix} + {hasFeedback && feedbackIcon} + + ); + const prefixClsValue = prefixCls.value; + const inputHasPrefixSuffix = hasPrefixSuffix({ prefix, suffix }) || !!hasFeedback; + const clearIcon = slots.clearIcon || (() => ); return ( - + {addonAfter}} + addonBefore={addonBefore && {addonBefore}} + inputClassName={classNames( + { + [`${prefixClsValue}-sm`]: size.value === 'small', + [`${prefixClsValue}-lg`]: size.value === 'large', + [`${prefixClsValue}-rtl`]: direction.value === 'rtl', + [`${prefixClsValue}-borderless`]: !bordered, + }, + !inputHasPrefixSuffix && getStatusClassNames(prefixClsValue, mergedStatus.value), + )} + affixWrapperClassName={classNames( + { + [`${prefixClsValue}-affix-wrapper-sm`]: size.value === 'small', + [`${prefixClsValue}-affix-wrapper-lg`]: size.value === 'large', + [`${prefixClsValue}-affix-wrapper-rtl`]: direction.value === 'rtl', + [`${prefixClsValue}-affix-wrapper-borderless`]: !bordered, + }, + getStatusClassNames(`${prefixClsValue}-affix-wrapper`, mergedStatus.value, hasFeedback), + )} + wrapperClassName={classNames({ + [`${prefixClsValue}-group-rtl`]: direction.value === 'rtl', + })} + groupClassName={classNames( + { + [`${prefixClsValue}-group-wrapper-sm`]: size.value === 'small', + [`${prefixClsValue}-group-wrapper-lg`]: size.value === 'large', + [`${prefixClsValue}-group-wrapper-rtl`]: direction.value === 'rtl', + }, + getStatusClassNames(`${prefixClsValue}-group-wrapper`, mergedStatus.value, hasFeedback), + )} + v-slots={{ ...slots, clearIcon }} + > ); }; }, diff --git a/components/input/Search.tsx b/components/input/Search.tsx index a5e6ac8b50..0ae1131acb 100644 --- a/components/input/Search.tsx +++ b/components/input/Search.tsx @@ -3,15 +3,19 @@ import { computed, ref, defineComponent } from 'vue'; import classNames from '../_util/classNames'; import Input from './Input'; import SearchOutlined from '@ant-design/icons-vue/SearchOutlined'; -import inputProps from './inputProps'; import Button from '../button'; import { cloneElement } from '../_util/vnode'; import PropTypes from '../_util/vue-types'; import isPlainObject from 'lodash-es/isPlainObject'; -import type { ChangeEvent, MouseEventHandler } from '../_util/EventInterface'; +import type { + ChangeEvent, + CompositionEventHandler, + MouseEventHandler, +} from '../_util/EventInterface'; import useConfigInject from '../_util/hooks/useConfigInject'; import omit from '../_util/omit'; import isMobile from '../_util/isMobile'; +import inputProps from './inputProps'; export default defineComponent({ name: 'AInputSearch', @@ -29,6 +33,7 @@ export default defineComponent({ }, setup(props, { slots, attrs, expose, emit }) { const inputRef = ref(); + const composedRef = ref(false); const focus = () => { inputRef.value?.focus(); }; @@ -55,12 +60,28 @@ export default defineComponent({ }; const onSearch = (e: MouseEvent | KeyboardEvent) => { - emit('search', inputRef.value?.stateValue, e); + emit('search', inputRef.value?.input?.stateValue, e); if (!isMobile.tablet) { inputRef.value.focus(); } }; + const onPressEnter = (e: KeyboardEvent) => { + if (composedRef.value) { + return; + } + onSearch(e); + }; + + const handleOnCompositionStart: CompositionEventHandler = e => { + composedRef.value = true; + emit('compositionstart', e); + }; + + const handleOnCompositionEnd: CompositionEventHandler = e => { + composedRef.value = false; + emit('compositionend', e); + }; const { prefixCls, getPrefixCls, direction, size } = useConfigInject('input-search', props); const inputPrefixCls = computed(() => getPrefixCls('input', props.inputPrefixCls)); return () => { @@ -133,7 +154,9 @@ export default defineComponent({ ref={inputRef} {...omit(restProps, ['onUpdate:value', 'onSearch', 'enterButton'])} {...attrs} - onPressEnter={onSearch} + onPressEnter={onPressEnter} + onCompositionstart={handleOnCompositionStart} + onCompositionend={handleOnCompositionEnd} size={size.value} prefixCls={inputPrefixCls.value} addonAfter={button} diff --git a/components/input/TextArea.tsx b/components/input/TextArea.tsx index 85d07e5057..31ed47ffce 100644 --- a/components/input/TextArea.tsx +++ b/components/input/TextArea.tsx @@ -11,14 +11,15 @@ import { import ClearableLabeledInput from './ClearableLabeledInput'; import ResizableTextArea from './ResizableTextArea'; import { textAreaProps } from './inputProps'; -import type { InputFocusOptions } from './Input'; -import { fixControlledValue, resolveOnChange, triggerFocus } from './Input'; +import type { InputFocusOptions } from '../vc-input/utils/commonUtils'; +import { fixControlledValue, resolveOnChange, triggerFocus } from '../vc-input/utils/commonUtils'; import classNames from '../_util/classNames'; -import { useInjectFormItemContext } from '../form/FormItemContext'; +import { FormItemInputContext, useInjectFormItemContext } from '../form/FormItemContext'; import type { FocusEventHandler } from '../_util/EventInterface'; import useConfigInject from '../_util/hooks/useConfigInject'; import omit from '../_util/omit'; import type { VueNode } from '../_util/type'; +import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'; function fixEmojiLength(value: string, maxLength: number) { return [...(value || '')].slice(0, maxLength).join(''); @@ -50,6 +51,8 @@ export default defineComponent({ props: textAreaProps(), setup(props, { attrs, expose, emit }) { const formItemContext = useInjectFormItemContext(); + const formItemInputContext = FormItemInputContext.useInject(); + const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status)); const stateValue = ref(props.value === undefined ? props.defaultValue : props.value); const resizableTextArea = ref(); const mergedValue = ref(''); @@ -186,12 +189,15 @@ export default defineComponent({ ...omit(props, ['allowClear']), ...attrs, style: showCount.value ? {} : style, - class: { - [`${prefixCls.value}-borderless`]: !bordered, - [`${customClass}`]: customClass && !showCount.value, - [`${prefixCls.value}-sm`]: size.value === 'small', - [`${prefixCls.value}-lg`]: size.value === 'large', - }, + class: [ + { + [`${prefixCls.value}-borderless`]: !bordered, + [`${customClass}`]: customClass && !showCount.value, + [`${prefixCls.value}-sm`]: size.value === 'small', + [`${prefixCls.value}-lg`]: size.value === 'large', + }, + getStatusClassNames(prefixCls.value, mergedStatus.value), + ], showCount: null, prefixCls: prefixCls.value, onInput: handleChange, @@ -259,10 +265,11 @@ export default defineComponent({ {...inputProps} value={mergedValue.value} v-slots={{ element: renderTextArea }} + status={props.status} /> ); - if (showCount.value) { + if (showCount.value || formItemInputContext.hasFeedback) { const valueLength = [...mergedValue.value].length; let dataCount: VueNode = ''; if (typeof showCount.value === 'object') { @@ -277,6 +284,8 @@ export default defineComponent({ `${prefixCls.value}-textarea`, { [`${prefixCls.value}-textarea-rtl`]: direction.value === 'rtl', + [`${prefixCls.value}-textarea-show-count`]: showCount.value, + [`${prefixCls.value}-textarea-in-form-item`]: formItemInputContext.isFormItemInput, }, `${prefixCls.value}-textarea-show-count`, customClass, @@ -285,6 +294,11 @@ export default defineComponent({ data-count={typeof dataCount !== 'object' ? dataCount : undefined} > {textareaNode} + {formItemInputContext.hasFeedback && ( + + {formItemInputContext.feedbackIcon} + + )} ); } diff --git a/components/input/demo/basic.vue b/components/input/demo/basic.vue index 1189c45455..6f8f704240 100644 --- a/components/input/demo/basic.vue +++ b/components/input/demo/basic.vue @@ -16,15 +16,26 @@ Basic usage example. + diff --git a/components/input/demo/index.vue b/components/input/demo/index.vue index b382d47131..0f50aba0a7 100644 --- a/components/input/demo/index.vue +++ b/components/input/demo/index.vue @@ -14,6 +14,7 @@ + @@ -32,6 +33,7 @@ import ShowCount from './show-count.vue'; import Addon from './addon.vue'; import Tooltip from './tooltip.vue'; import borderlessVue from './borderless.vue'; +import statusVue from './status.vue'; import CN from '../index.zh-CN.md'; import US from '../index.en-US.md'; import { defineComponent } from 'vue'; @@ -40,6 +42,7 @@ export default defineComponent({ CN, US, components: { + statusVue, Basic, AutosizeTextarea, Presuffix, diff --git a/components/input/demo/status.vue b/components/input/demo/status.vue new file mode 100644 index 0000000000..0a2d8269d5 --- /dev/null +++ b/components/input/demo/status.vue @@ -0,0 +1,43 @@ + +--- +order: 19 +version: 3.3.0 +title: + zh-CN: 自定义状态 + en-US: Status +--- + +## zh-CN + +使用 `status` 为 Input 添加状态,可选 `error` 或者 `warning`。 + +## en-US + +Add status to Input with `status`, which could be `error` or `warning`. + + + + + diff --git a/components/input/index.en-US.md b/components/input/index.en-US.md index 07f1fb68b2..88b4c51fb9 100644 --- a/components/input/index.en-US.md +++ b/components/input/index.en-US.md @@ -22,13 +22,15 @@ A basic widget for getting the user input is a text field. Keyboard and mouse ca | addonBefore | The label text displayed before (on the left side of) the input field. | string\|slot | | | | allowClear | allow to remove input content with clear icon | boolean | | | | bordered | Whether has border style | boolean | true | 4.5.0 | +| clearIcon | custom clear icon when allowClear | slot | `` | 3.3.0 | | defaultValue | The initial input content | string | | | | disabled | Whether the input is disabled. | boolean | false | | | id | The ID for input | string | | | | maxlength | max length | number | | 1.5.0 | | prefix | The prefix icon for the Input. | string\|slot | | | | showCount | Whether show text count | boolean | false | 3.0 | -| size | The size of the input box. Note: in the context of a form, the `large` size is used. Available: `large` `default` `small` | string | `default` | | +| status | Set validation status | 'error' \| 'warning' | - | 3.3.0 | +| size | The size of the input box. Note: in the context of a form, the `middle` size is used. Available: `large` `middle` `small` | string | - | | | suffix | The suffix icon for the Input. | string\|slot | | | | type | The type of input, see: [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types)(use `` instead of `type="textarea"`) | string | `text` | | | value(v-model) | The input content value | string | | | diff --git a/components/input/index.zh-CN.md b/components/input/index.zh-CN.md index 76f993a150..fb798e1f1e 100644 --- a/components/input/index.zh-CN.md +++ b/components/input/index.zh-CN.md @@ -23,13 +23,15 @@ cover: https://gw.alipayobjects.com/zos/alicdn/xS9YEJhfe/Input.svg | addonBefore | 带标签的 input,设置前置标签 | string\|slot | | | | allowClear | 可以点击清除图标删除内容 | boolean | | | | bordered | 是否有边框 | boolean | true | 3.0 | +| clearIcon | 自定义清除图标 (allowClear 为 true 时生效) | slot | `` | 3.3.0 | | defaultValue | 输入框默认内容 | string | | | | disabled | 是否禁用状态,默认为 false | boolean | false | | | id | 输入框的 id | string | | | | maxlength | 最大长度 | number | | 1.5.0 | | prefix | 带有前缀图标的 input | string\|slot | | | | showCount | 是否展示字数 | boolean | false | 3.0 | -| size | 控件大小。注:标准表单内的输入框大小限制为 `large`。可选 `large` `default` `small` | string | `default` | | +| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | +| size | 控件大小。注:标准表单内的输入框大小限制为 `middle`。可选 `large` `middle` `small` | string | - | | | suffix | 带有后缀图标的 input | string\|slot | | | | type | 声明 input 类型,同原生 input 标签的 type 属性,见:[MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#属性)(请直接使用 `` 代替 `type="textarea"`)。 | string | `text` | | | value(v-model) | 输入框内容 | string | | | diff --git a/components/input/inputProps.ts b/components/input/inputProps.ts index 19933f45cd..0b751ac1ec 100644 --- a/components/input/inputProps.ts +++ b/components/input/inputProps.ts @@ -1,92 +1,25 @@ import type { ExtractPropTypes, PropType } from 'vue'; -import PropTypes from '../_util/vue-types'; -import type { SizeType } from '../config-provider'; import omit from '../_util/omit'; -import type { LiteralUnion, VueNode } from '../_util/type'; -import type { - ChangeEventHandler, - CompositionEventHandler, - FocusEventHandler, - KeyboardEventHandler, -} from '../_util/EventInterface'; +import type { VueNode } from '../_util/type'; +import type { CompositionEventHandler } from '../_util/EventInterface'; +import { inputProps as vcInputProps } from '../vc-input/inputProps'; + export const inputDefaultValue = Symbol() as unknown as string; -const inputProps = () => ({ - id: String, - prefixCls: String, - inputPrefixCls: String, - defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - value: { - type: [String, Number, Symbol] as PropType, - default: undefined, - }, - placeholder: { - type: [String, Number] as PropType, - }, - autocomplete: String, - type: { - type: String as PropType< - LiteralUnion< - | 'button' - | 'checkbox' - | 'color' - | 'date' - | 'datetime-local' - | 'email' - | 'file' - | 'hidden' - | 'image' - | 'month' - | 'number' - | 'password' - | 'radio' - | 'range' - | 'reset' - | 'search' - | 'submit' - | 'tel' - | 'text' - | 'time' - | 'url' - | 'week', - string - > - >, - default: 'text', - }, - name: String, - size: { type: String as PropType }, - disabled: { type: Boolean, default: undefined }, - readonly: { type: Boolean, default: undefined }, - addonBefore: PropTypes.any, - addonAfter: PropTypes.any, - prefix: PropTypes.any, - suffix: PropTypes.any, - autofocus: { type: Boolean, default: undefined }, - allowClear: { type: Boolean, default: undefined }, - lazy: { type: Boolean, default: true }, - maxlength: Number, - loading: { type: Boolean, default: undefined }, - bordered: { type: Boolean, default: undefined }, - showCount: { type: [Boolean, Object] as PropType }, - htmlSize: Number, - onPressEnter: Function as PropType, - onKeydown: Function as PropType, - onKeyup: Function as PropType, - onFocus: Function as PropType, - onBlur: Function as PropType, - onChange: Function as PropType, - onInput: Function as PropType, - 'onUpdate:value': Function as PropType<(val: string) => void>, - valueModifiers: Object, - hidden: Boolean, -}); -export default inputProps; -export type InputProps = Partial>>; export interface AutoSizeType { minRows?: number; maxRows?: number; } +const inputProps = () => { + return omit(vcInputProps(), [ + 'wrapperClassName', + 'groupClassName', + 'inputClassName', + 'affixWrapperClassName', + ]); +}; +export default inputProps; +export type InputProps = Partial>>; export interface ShowCountProps { formatter: (args: { count: number; maxlength?: number }) => VueNode; } diff --git a/components/input/style/affix.less b/components/input/style/affix.less index 3cffa963b6..5f92404441 100644 --- a/components/input/style/affix.less +++ b/components/input/style/affix.less @@ -50,6 +50,10 @@ display: flex; flex: none; align-items: center; + + > *:not(:last-child) { + margin-right: 8px; + } } &-show-count-suffix { diff --git a/components/input/style/allow-clear.less b/components/input/style/allow-clear.less index 8f548c8a80..946541ef4f 100644 --- a/components/input/style/allow-clear.less +++ b/components/input/style/allow-clear.less @@ -2,7 +2,8 @@ @input-prefix-cls: ~'@{ant-prefix}-input'; // ========================= Input ========================= -.@{iconfont-css-prefix}.@{ant-prefix}-input-clear-icon { +.@{iconfont-css-prefix}.@{ant-prefix}-input-clear-icon, +.@{ant-prefix}-input-clear-icon { margin: 0; color: @disabled-color; font-size: @font-size-sm; diff --git a/components/input/style/index.less b/components/input/style/index.less index 79c8c00980..36e1c92661 100644 --- a/components/input/style/index.less +++ b/components/input/style/index.less @@ -3,6 +3,7 @@ @import './mixin'; @import './affix'; @import './allow-clear'; +@import './status'; @input-prefix-cls: ~'@{ant-prefix}-input'; @@ -24,7 +25,7 @@ } } - &-password-icon { + &-password-icon.@{iconfont-css-prefix} { color: @text-color-secondary; cursor: pointer; transition: all 0.3s; @@ -60,6 +61,23 @@ content: attr(data-count); pointer-events: none; } + + &.@{input-prefix-cls}-textarea-in-form-item { + &::after { + margin-bottom: -22px; + } + } + } + + &-textarea-suffix { + position: absolute; + top: 0; + right: @input-padding-horizontal-base; + bottom: 0; + z-index: 1; + display: inline-flex; + align-items: center; + margin: auto; } } diff --git a/components/input/style/index.tsx b/components/input/style/index.tsx index 416ec0177e..40150302b3 100644 --- a/components/input/style/index.tsx +++ b/components/input/style/index.tsx @@ -1,5 +1,6 @@ import '../../style/index.less'; import './index.less'; +// deps-lint-skip: form // style dependencies import '../../button/style'; diff --git a/components/input/style/mixin.less b/components/input/style/mixin.less index 60299decb4..fd5f994834 100644 --- a/components/input/style/mixin.less +++ b/components/input/style/mixin.less @@ -30,14 +30,14 @@ border-color: @hoverBorderColor; box-shadow: @input-outline-offset @outline-blur-size @outline-width @outlineColor; } - border-right-width: @border-width-base !important; + border-right-width: @border-width-base; outline: 0; } // == when hover .hover(@color: @input-hover-border-color) { border-color: @color; - border-right-width: @border-width-base !important; + border-right-width: @border-width-base; } .disabled() { @@ -66,7 +66,7 @@ background-color: @input-bg; background-image: none; border: @border-width-base @border-style-base @input-border-color; - border-radius: @border-radius-base; + border-radius: @control-border-radius; transition: all 0.3s; .placeholder(); // Reset placeholder @@ -193,7 +193,7 @@ text-align: center; background-color: @input-addon-bg; border: @border-width-base @border-style-base @input-border-color; - border-radius: @border-radius-base; + border-radius: @control-border-radius; transition: all 0.3s; // Reset Select's style in addon @@ -297,8 +297,8 @@ border-top-right-radius: 0; border-bottom-right-radius: 0; .@{ant-prefix}-input-search & { - border-top-left-radius: @border-radius-base; - border-bottom-left-radius: @border-radius-base; + border-top-left-radius: @control-border-radius; + border-bottom-left-radius: @control-border-radius; } } @@ -384,8 +384,8 @@ & > .@{ant-prefix}-select:first-child > .@{ant-prefix}-select-selector, & > .@{ant-prefix}-select-auto-complete:first-child .@{ant-prefix}-input, & > .@{ant-prefix}-cascader-picker:first-child .@{ant-prefix}-input { - border-top-left-radius: @border-radius-base; - border-bottom-left-radius: @border-radius-base; + border-top-left-radius: @control-border-radius; + border-bottom-left-radius: @control-border-radius; } & > *:last-child, @@ -393,8 +393,8 @@ & > .@{ant-prefix}-cascader-picker:last-child .@{ant-prefix}-input, & > .@{ant-prefix}-cascader-picker-focused:last-child .@{ant-prefix}-input { border-right-width: @border-width-base; - border-top-right-radius: @border-radius-base; - border-bottom-right-radius: @border-radius-base; + border-top-right-radius: @control-border-radius; + border-bottom-right-radius: @control-border-radius; } // https://github.com/ant-design/ant-design/issues/12493 @@ -416,9 +416,55 @@ } & > .@{ant-prefix}-input { - border-radius: @border-radius-base 0 0 @border-radius-base; + border-radius: @control-border-radius 0 0 @control-border-radius; } } } } } + +.status-color( + @prefix-cls: @input-prefix-cls; + @text-color: @input-color; + @border-color: @input-border-color; + @background-color: @input-bg; + @hoverBorderColor: @primary-color-hover; + @outlineColor: @primary-color-outline; +) { + &:not(.@{prefix-cls}-disabled):not(.@{prefix-cls}-borderless).@{prefix-cls} { + &, + &:hover { + background: @background-color; + border-color: @border-color; + } + + &:focus, + &-focused { + .active(@text-color, @hoverBorderColor, @outlineColor); + } + } +} + +.status-color-common( + @prefix-cls: @input-prefix-cls; + @text-color: @input-color; + @border-color: @input-border-color; + @background-color: @input-bg; + @hoverBorderColor: @primary-color-hover; + @outlineColor: @primary-color-outline; +) { + .@{prefix-cls}-prefix { + color: @text-color; + } +} + +.group-status-color( + @prefix-cls: @input-prefix-cls; + @text-color: @input-color; + @border-color: @input-border-color; +) { + .@{prefix-cls}-group-addon { + color: @text-color; + border-color: @border-color; + } +} diff --git a/components/input/style/status.less b/components/input/style/status.less new file mode 100644 index 0000000000..e6562e6024 --- /dev/null +++ b/components/input/style/status.less @@ -0,0 +1,42 @@ +@import './mixin'; + +@input-prefix-cls: ~'@{ant-prefix}-input'; + +@input-wrapper-cls: @input-prefix-cls, ~'@{input-prefix-cls}-affix-wrapper'; + +each(@input-wrapper-cls, { + .@{value} { + &-status-error { + .status-color(@value, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline); + .status-color-common(@input-prefix-cls, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline); + } + + &-status-warning { + .status-color(@value, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline); + .status-color-common(@input-prefix-cls, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline); + } + } +}); + +.@{input-prefix-cls}-textarea { + &-status-error, + &-status-warning, + &-status-success, + &-status-validating { + &.@{input-prefix-cls}-textarea-has-feedback { + .@{input-prefix-cls} { + padding-right: 24px; + } + } + } +} + +.@{input-prefix-cls}-group-wrapper { + &-status-error { + .group-status-color(@input-prefix-cls, @error-color, @error-color); + } + + &-status-warning { + .group-status-color(@input-prefix-cls, @warning-color, @warning-color); + } +} diff --git a/components/input/util.ts b/components/input/util.ts index 8374307b12..5b3be7051f 100644 --- a/components/input/util.ts +++ b/components/input/util.ts @@ -1,22 +1,4 @@ -import type { Direction, SizeType } from '../config-provider'; -import classNames from '../_util/classNames'; import { filterEmpty } from '../_util/props-util'; - -export function getInputClassName( - prefixCls: string, - bordered: boolean, - size?: SizeType, - disabled?: boolean, - direction?: Direction, -) { - return classNames(prefixCls, { - [`${prefixCls}-sm`]: size === 'small', - [`${prefixCls}-lg`]: size === 'large', - [`${prefixCls}-disabled`]: disabled, - [`${prefixCls}-rtl`]: direction === 'rtl', - [`${prefixCls}-borderless`]: !bordered, - }); -} const isValid = (value: any) => { return ( value !== undefined && diff --git a/components/vc-input/BaseInput.tsx b/components/vc-input/BaseInput.tsx new file mode 100644 index 0000000000..6e8ef61e38 --- /dev/null +++ b/components/vc-input/BaseInput.tsx @@ -0,0 +1,151 @@ +import { defineComponent, ref } from 'vue'; +import classNames from '../_util/classNames'; +import type { MouseEventHandler } from '../_util/EventInterface'; +import { cloneElement } from '../_util/vnode'; +import { baseInputProps } from './inputProps'; +import { hasAddon, hasPrefixSuffix } from './utils/commonUtils'; + +export default defineComponent({ + name: 'BaseInput', + inheritAttrs: false, + props: baseInputProps(), + setup(props, { slots, attrs }) { + const containerRef = ref(); + const onInputMouseDown: MouseEventHandler = e => { + if (containerRef.value?.contains(e.target as Element)) { + const { triggerFocus } = props; + triggerFocus?.(); + } + }; + const getClearIcon = () => { + const { + allowClear, + value, + disabled, + readonly, + handleReset, + suffix = slots.suffix, + prefixCls, + } = props; + if (!allowClear) { + return null; + } + const needClear = !disabled && !readonly && value; + const className = `${prefixCls}-clear-icon`; + const iconNode = slots.clearIcon?.() || '*'; + return ( + e.preventDefault()} + class={classNames( + { + [`${className}-hidden`]: !needClear, + [`${className}-has-suffix`]: !!suffix, + }, + className, + )} + role="button" + tabindex={-1} + > + {iconNode} + + ); + }; + + return () => { + const { + focused, + value, + + disabled, + allowClear, + readonly, + hidden, + prefixCls, + prefix = slots.prefix?.(), + suffix = slots.suffix?.(), + addonAfter = slots.addonAfter, + addonBefore = slots.addonBefore, + inputElement, + affixWrapperClassName, + wrapperClassName, + groupClassName, + } = props; + let element = cloneElement(inputElement, { + value, + hidden, + }); + // ================== Prefix & Suffix ================== // + if (hasPrefixSuffix({ prefix, suffix, allowClear })) { + const affixWrapperPrefixCls = `${prefixCls}-affix-wrapper`; + const affixWrapperCls = classNames( + affixWrapperPrefixCls, + { + [`${affixWrapperPrefixCls}-disabled`]: disabled, + [`${affixWrapperPrefixCls}-focused`]: focused, + [`${affixWrapperPrefixCls}-readonly`]: readonly, + [`${affixWrapperPrefixCls}-input-with-clear-btn`]: suffix && allowClear && value, + }, + !hasAddon({ addonAfter, addonBefore }) && attrs.class, + affixWrapperClassName, + ); + + const suffixNode = (suffix || allowClear) && ( + + {getClearIcon()} + {suffix} + + ); + + element = ( + + ); + } + // ================== Addon ================== // + if (hasAddon({ addonAfter, addonBefore })) { + const wrapperCls = `${prefixCls}-group`; + const addonCls = `${wrapperCls}-addon`; + + const mergedWrapperClassName = classNames( + `${prefixCls}-wrapper`, + wrapperCls, + wrapperClassName, + ); + + const mergedGroupClassName = classNames( + `${prefixCls}-group-wrapper`, + attrs.class, + groupClassName, + ); + + // Need another wrapper for changing display:table to display:inline-block + // and put style prop in wrapper + return ( + + ); + } + return element; + }; + }, +}); diff --git a/components/vc-input/Input.tsx b/components/vc-input/Input.tsx new file mode 100644 index 0000000000..97f790fb4a --- /dev/null +++ b/components/vc-input/Input.tsx @@ -0,0 +1,261 @@ +// base 0.0.1-alpha.7 +import type { VNode } from 'vue'; +import { + onMounted, + defineComponent, + getCurrentInstance, + nextTick, + ref, + watch, + withDirectives, +} from 'vue'; +import classNames from '../_util/classNames'; +import type { ChangeEvent, FocusEventHandler } from '../_util/EventInterface'; +import omit from '../_util/omit'; +import type { InputProps } from './inputProps'; +import { inputProps } from './inputProps'; +import type { InputFocusOptions } from './utils/commonUtils'; +import { + fixControlledValue, + hasAddon, + hasPrefixSuffix, + resolveOnChange, + triggerFocus, +} from './utils/commonUtils'; +import antInputDirective from '../_util/antInputDirective'; +import BaseInput from './BaseInput'; + +export default defineComponent({ + name: 'VCInput', + inheritAttrs: false, + props: inputProps(), + setup(props, { slots, attrs, expose, emit }) { + const stateValue = ref(props.value === undefined ? props.defaultValue : props.value); + const focused = ref(false); + const inputRef = ref(); + watch( + () => props.value, + () => { + stateValue.value = props.value; + }, + ); + watch( + () => props.disabled, + () => { + if (props.disabled) { + focused.value = false; + } + }, + ); + const focus = (option?: InputFocusOptions) => { + if (inputRef.value) { + triggerFocus(inputRef.value, option); + } + }; + + const blur = () => { + inputRef.value?.blur(); + }; + + const setSelectionRange = ( + start: number, + end: number, + direction?: 'forward' | 'backward' | 'none', + ) => { + inputRef.value?.setSelectionRange(start, end, direction); + }; + + const select = () => { + inputRef.value?.select(); + }; + + expose({ + focus, + blur, + input: inputRef, + stateValue, + setSelectionRange, + select, + }); + const triggerChange = (e: Event) => { + emit('change', e); + }; + const instance = getCurrentInstance(); + const setValue = (value: string | number, callback?: Function) => { + if (stateValue.value === value) { + return; + } + if (props.value === undefined) { + stateValue.value = value; + } else { + nextTick(() => { + if (inputRef.value.value !== stateValue.value) { + instance.update(); + } + }); + } + nextTick(() => { + callback && callback(); + }); + }; + const handleChange = (e: ChangeEvent) => { + const { value, composing } = e.target as any; + // https://github.com/vueComponent/ant-design-vue/issues/2203 + if ((((e as any).isComposing || composing) && props.lazy) || stateValue.value === value) + return; + const newVal = e.target.value; + resolveOnChange(inputRef.value, e, triggerChange); + setValue(newVal); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.keyCode === 13) { + emit('pressEnter', e); + } + emit('keydown', e); + }; + + const handleFocus: FocusEventHandler = e => { + focused.value = true; + emit('focus', e); + }; + + const handleBlur: FocusEventHandler = e => { + focused.value = false; + emit('blur', e); + }; + + const handleReset = (e: MouseEvent) => { + resolveOnChange(inputRef.value, e, triggerChange); + setValue('', () => { + focus(); + }); + }; + + const getInputElement = () => { + const { + addonBefore = slots.addonBefore, + addonAfter = slots.addonAfter, + disabled, + valueModifiers = {}, + htmlSize, + autocomplete, + prefixCls, + inputClassName, + prefix = slots.prefix?.(), + suffix = slots.suffix?.(), + allowClear, + type = 'text', + } = props; + const otherProps = omit(props as InputProps & { placeholder: string }, [ + 'prefixCls', + 'onPressEnter', + 'addonBefore', + 'addonAfter', + 'prefix', + 'suffix', + 'allowClear', + // Input elements must be either controlled or uncontrolled, + // specify either the value prop, or the defaultValue prop, but not both. + 'defaultValue', + 'size', + 'bordered', + 'htmlSize', + 'lazy', + 'showCount', + 'valueModifiers', + 'showCount', + 'affixWrapperClassName', + 'groupClassName', + 'inputClassName', + 'wrapperClassName', + ]); + const inputProps = { + ...otherProps, + ...attrs, + autocomplete, + onChange: handleChange, + onInput: handleChange, + onFocus: handleFocus, + onBlur: handleBlur, + onKeydown: handleKeyDown, + class: classNames( + prefixCls, + { + [`${prefixCls}-disabled`]: disabled, + }, + inputClassName, + !hasAddon({ addonAfter, addonBefore }) && + !hasPrefixSuffix({ prefix, suffix, allowClear }) && + attrs.class, + ), + ref: inputRef, + key: 'ant-input', + size: htmlSize, + type, + }; + if (valueModifiers.lazy) { + delete inputProps.onInput; + } + if (!inputProps.autofocus) { + delete inputProps.autofocus; + } + const inputNode = ; + return withDirectives(inputNode as VNode, [[antInputDirective]]); + }; + const getSuffix = () => { + const { maxlength, suffix = slots.suffix?.(), showCount, prefixCls } = props; + // Max length value + const hasMaxLength = Number(maxlength) > 0; + + if (suffix || showCount) { + const valueLength = [...fixControlledValue(stateValue.value)].length; + const dataCount = + typeof showCount === 'object' + ? showCount.formatter({ count: valueLength, maxlength }) + : `${valueLength}${hasMaxLength ? ` / ${maxlength}` : ''}`; + + return ( + <> + {!!showCount && ( + + {dataCount} + + )} + {suffix} + + ); + } + return null; + }; + onMounted(() => { + if (process.env.NODE_ENV === 'test') { + if (props.autofocus) { + focus(); + } + } + }); + return () => { + const { prefixCls, disabled, ...rest } = props; + return ( + + ); + }; + }, +}); diff --git a/components/vc-input/inputProps.ts b/components/vc-input/inputProps.ts new file mode 100644 index 0000000000..6ccc8be38a --- /dev/null +++ b/components/vc-input/inputProps.ts @@ -0,0 +1,126 @@ +import type { ExtractPropTypes, PropType } from 'vue'; +import PropTypes from '../_util/vue-types'; +import type { SizeType } from '../config-provider'; +import type { LiteralUnion, VueNode } from '../_util/type'; +import type { + ChangeEventHandler, + CompositionEventHandler, + FocusEventHandler, + KeyboardEventHandler, + MouseEventHandler, +} from '../_util/EventInterface'; +import type { InputStatus } from '../_util/statusUtils'; +import type { InputFocusOptions } from './utils/commonUtils'; +export const inputDefaultValue = Symbol() as unknown as string; +export const commonInputProps = () => { + return { + addonBefore: PropTypes.any, + addonAfter: PropTypes.any, + prefix: PropTypes.any, + suffix: PropTypes.any, + clearIcon: PropTypes.any, + affixWrapperClassName: String, + groupClassName: String, + wrapperClassName: String, + inputClassName: String, + allowClear: { type: Boolean, default: undefined }, + }; +}; +export const baseInputProps = () => { + return { + ...commonInputProps(), + value: { + type: [String, Number, Symbol] as PropType, + default: undefined, + }, + defaultValue: { + type: [String, Number, Symbol] as PropType, + default: undefined, + }, + inputElement: PropTypes.any, + prefixCls: String, + disabled: { type: Boolean, default: undefined }, + focused: { type: Boolean, default: undefined }, + triggerFocus: Function as PropType<() => void>, + readonly: { type: Boolean, default: undefined }, + handleReset: Function as PropType, + hidden: { type: Boolean, default: undefined }, + }; +}; +export const inputProps = () => ({ + ...baseInputProps(), + id: String, + placeholder: { + type: [String, Number] as PropType, + }, + autocomplete: String, + type: { + type: String as PropType< + LiteralUnion< + | 'button' + | 'checkbox' + | 'color' + | 'date' + | 'datetime-local' + | 'email' + | 'file' + | 'hidden' + | 'image' + | 'month' + | 'number' + | 'password' + | 'radio' + | 'range' + | 'reset' + | 'search' + | 'submit' + | 'tel' + | 'text' + | 'time' + | 'url' + | 'week', + string + > + >, + default: 'text', + }, + name: String, + size: { type: String as PropType }, + autofocus: { type: Boolean, default: undefined }, + lazy: { type: Boolean, default: true }, + maxlength: Number, + loading: { type: Boolean, default: undefined }, + bordered: { type: Boolean, default: undefined }, + showCount: { type: [Boolean, Object] as PropType }, + htmlSize: Number, + onPressEnter: Function as PropType, + onKeydown: Function as PropType, + onKeyup: Function as PropType, + onFocus: Function as PropType, + onBlur: Function as PropType, + onChange: Function as PropType, + onInput: Function as PropType, + 'onUpdate:value': Function as PropType<(val: string) => void>, + onCompositionstart: Function as PropType, + onCompositionend: Function as PropType, + valueModifiers: Object, + hidden: { type: Boolean, default: undefined }, + status: String as PropType, +}); +export type InputProps = Partial>>; + +export interface ShowCountProps { + formatter: (args: { count: number; maxlength?: number }) => VueNode; +} + +export interface InputRef { + focus: (options?: InputFocusOptions) => void; + blur: () => void; + setSelectionRange: ( + start: number, + end: number, + direction?: 'forward' | 'backward' | 'none', + ) => void; + select: () => void; + input: HTMLInputElement | null; +} diff --git a/components/vc-input/utils/commonUtils.ts b/components/vc-input/utils/commonUtils.ts new file mode 100644 index 0000000000..54c6379e92 --- /dev/null +++ b/components/vc-input/utils/commonUtils.ts @@ -0,0 +1,104 @@ +import { filterEmpty } from '../../_util/props-util'; +const isValid = (value: any) => { + return ( + value !== undefined && + value !== null && + (Array.isArray(value) ? filterEmpty(value).length : true) + ); +}; + +export function hasPrefixSuffix(propsAndSlots: any) { + return ( + isValid(propsAndSlots.prefix) || + isValid(propsAndSlots.suffix) || + isValid(propsAndSlots.allowClear) + ); +} + +export function hasAddon(propsAndSlots: any) { + return isValid(propsAndSlots.addonBefore) || isValid(propsAndSlots.addonAfter); +} + +export function fixControlledValue(value: string | number) { + if (typeof value === 'undefined' || value === null) { + return ''; + } + return String(value); +} + +export function resolveOnChange( + target: HTMLInputElement, + e: Event, + onChange: Function, + targetValue?: string, +) { + if (!onChange) { + return; + } + const event: any = e; + + if (e.type === 'click') { + Object.defineProperty(event, 'target', { + writable: true, + }); + Object.defineProperty(event, 'currentTarget', { + writable: true, + }); + // click clear icon + //event = Object.create(e); + const currentTarget = target.cloneNode(true); + + event.target = currentTarget; + event.currentTarget = currentTarget; + // change target ref value cause e.target.value should be '' when clear input + (currentTarget as any).value = ''; + onChange(event); + return; + } + // Trigger by composition event, this means we need force change the input value + if (targetValue !== undefined) { + Object.defineProperty(event, 'target', { + writable: true, + }); + Object.defineProperty(event, 'currentTarget', { + writable: true, + }); + event.target = target; + event.currentTarget = target; + target.value = targetValue; + onChange(event); + return; + } + onChange(event); +} +export interface InputFocusOptions extends FocusOptions { + cursor?: 'start' | 'end' | 'all'; +} + +export function triggerFocus( + element?: HTMLInputElement | HTMLTextAreaElement, + option?: InputFocusOptions, +) { + if (!element) return; + + element.focus(option); + + // Selection content + const { cursor } = option || {}; + if (cursor) { + const len = element.value.length; + + switch (cursor) { + case 'start': + element.setSelectionRange(0, 0); + break; + + case 'end': + element.setSelectionRange(len, len); + break; + + default: + element.setSelectionRange(0, len); + } + } +} diff --git a/components/vc-input/utils/types.ts b/components/vc-input/utils/types.ts new file mode 100644 index 0000000000..1bcab64545 --- /dev/null +++ b/components/vc-input/utils/types.ts @@ -0,0 +1,3 @@ +/** https://github.com/Microsoft/TypeScript/issues/29729 */ +// eslint-disable-next-line @typescript-eslint/ban-types +export type LiteralUnion = T | (U & {}); From 6e41fbd01fc125bccfe279df4f66829ba59e14c6 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Thu, 19 May 2022 11:03:26 +0800 Subject: [PATCH 020/323] feat: inputnumber add status & upIcon & downIcon --- components/input-number/demo/icon.vue | 47 +++++++++++++++++ components/input-number/demo/index.vue | 6 +++ components/input-number/demo/status.vue | 43 ++++++++++++++++ components/input-number/index.en-US.md | 3 ++ components/input-number/index.tsx | 61 ++++++++++++++++------- components/input-number/index.zh-CN.md | 3 ++ components/input-number/style/affix.less | 23 ++++++++- components/input-number/style/index.less | 1 + components/input-number/style/status.less | 29 +++++++++++ 9 files changed, 195 insertions(+), 21 deletions(-) create mode 100644 components/input-number/demo/icon.vue create mode 100644 components/input-number/demo/status.vue create mode 100644 components/input-number/style/status.less diff --git a/components/input-number/demo/icon.vue b/components/input-number/demo/icon.vue new file mode 100644 index 0000000000..1c5f60fe35 --- /dev/null +++ b/components/input-number/demo/icon.vue @@ -0,0 +1,47 @@ + +--- +order: 99 +title: + zh-CN: 图标按钮 + en-US: Icon +--- + +## zh-CN + +使用 `upIcon` `downIcon` 插槽自定义图标。 + +## en-US + +use `upIcon` `downIcon` custom icon + + + + + diff --git a/components/input-number/demo/index.vue b/components/input-number/demo/index.vue index 753dc14c94..127741510b 100644 --- a/components/input-number/demo/index.vue +++ b/components/input-number/demo/index.vue @@ -10,6 +10,8 @@ + + diff --git a/components/input-number/index.en-US.md b/components/input-number/index.en-US.md index a24f17c44d..314f314930 100644 --- a/components/input-number/index.en-US.md +++ b/components/input-number/index.en-US.md @@ -31,9 +31,12 @@ When a numeric value needs to be provided. | precision | precision of input value | number | - | | | prefix | The prefix icon for the Input | slot | - | 3.0 | | size | height of input box | string | - | | +| status | Set validation status | 'error' \| 'warning' | - | 3.3.0 | | step | The number to which the current value is increased or decreased. It can be an integer or decimal. | number\|string | 1 | | | stringMode | Set value as string to support high precision decimals. Will return string value by `change` | boolean | false | 3.0 | | value(v-model) | current value | number | | | +| upIcon | custom up icon | slot | `` | 3.3.0 | +| downIcon | custom up down | slot | `` | 3.3.0 | ### events diff --git a/components/input-number/index.tsx b/components/input-number/index.tsx index 682ee07516..88950903d2 100644 --- a/components/input-number/index.tsx +++ b/components/input-number/index.tsx @@ -1,16 +1,22 @@ import type { PropType, ExtractPropTypes, HTMLAttributes, App } from 'vue'; -import { watch, defineComponent, nextTick, onMounted, ref } from 'vue'; +import { watch, defineComponent, nextTick, onMounted, ref, computed } from 'vue'; import classNames from '../_util/classNames'; import UpOutlined from '@ant-design/icons-vue/UpOutlined'; import DownOutlined from '@ant-design/icons-vue/DownOutlined'; import VcInputNumber, { inputNumberProps as baseInputNumberProps } from './src/InputNumber'; import type { SizeType } from '../config-provider'; -import { useInjectFormItemContext } from '../form/FormItemContext'; +import { + FormItemInputContext, + NoFormStatus, + useInjectFormItemContext, +} from '../form/FormItemContext'; import useConfigInject from '../_util/hooks/useConfigInject'; import { cloneElement } from '../_util/vnode'; import omit from '../_util/omit'; import PropTypes from '../_util/vue-types'; import isValidValue from '../_util/isValidValue'; +import type { InputStatus } from '../_util/statusUtils'; +import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils'; const baseProps = baseInputNumberProps(); export const inputNumberProps = () => ({ ...baseProps, @@ -25,6 +31,7 @@ export const inputNumberProps = () => ({ prefix: PropTypes.any, 'onUpdate:value': baseProps.onChange, valueModifiers: Object, + status: String as PropType, }); export type InputNumberProps = Partial>>; @@ -37,6 +44,8 @@ const InputNumber = defineComponent({ slots: ['addonBefore', 'addonAfter', 'prefix'], setup(props, { emit, expose, attrs, slots }) { const formItemContext = useInjectFormItemContext(); + const formItemInputContext = FormItemInputContext.useInject(); + const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status)); const { prefixCls, size, direction } = useConfigInject('input-number', props); const mergedValue = ref(props.value === undefined ? props.defaultValue : props.value); const focused = ref(false); @@ -84,6 +93,7 @@ const InputNumber = defineComponent({ }); }); return () => { + const { hasFeedback, isFormItemInput, feedbackIcon } = formItemInputContext; const { class: className, bordered, @@ -106,7 +116,9 @@ const InputNumber = defineComponent({ [`${preCls}-rtl`]: direction.value === 'rtl', [`${preCls}-readonly`]: readonly, [`${preCls}-borderless`]: !bordered, + [`${preCls}-in-form-item`]: isFormItemInput, }, + getStatusClassNames(preCls, mergedStatus.value), className, ); @@ -123,32 +135,42 @@ const InputNumber = defineComponent({ onBlur={handleBlur} onFocus={handleFocus} v-slots={{ - upHandler: () => , - downHandler: () => , + upHandler: slots.upIcon + ? () => {slots.upIcon()} + : () => , + downHandler: slots.downIcon + ? () => {slots.downIcon()} + : () => , }} /> ); const hasAddon = isValidValue(addonBefore) || isValidValue(addonAfter); - if (isValidValue(prefix)) { - const affixWrapperCls = classNames(`${preCls}-affix-wrapper`, { - [`${preCls}-affix-wrapper-focused`]: focused.value, - [`${preCls}-affix-wrapper-disabled`]: props.disabled, - [`${preCls}-affix-wrapper-sm`]: size.value === 'small', - [`${preCls}-affix-wrapper-lg`]: size.value === 'large', - [`${preCls}-affix-wrapper-rtl`]: direction.value === 'rtl', - [`${preCls}-affix-wrapper-readonly`]: readonly, - [`${preCls}-affix-wrapper-borderless`]: !bordered, - // className will go to addon wrapper - [`${className}`]: !hasAddon && className, - }); + const hasPrefix = isValidValue(prefix); + if (hasPrefix || hasFeedback) { + const affixWrapperCls = classNames( + `${preCls}-affix-wrapper`, + getStatusClassNames(`${preCls}-affix-wrapper`, mergedStatus.value, hasFeedback), + { + [`${preCls}-affix-wrapper-focused`]: focused.value, + [`${preCls}-affix-wrapper-disabled`]: props.disabled, + [`${preCls}-affix-wrapper-sm`]: size.value === 'small', + [`${preCls}-affix-wrapper-lg`]: size.value === 'large', + [`${preCls}-affix-wrapper-rtl`]: direction.value === 'rtl', + [`${preCls}-affix-wrapper-readonly`]: readonly, + [`${preCls}-affix-wrapper-borderless`]: !bordered, + // className will go to addon wrapper + [`${className}`]: !hasAddon && className, + }, + ); element = (
    inputNumberRef.value!.focus()} > - {prefix} + {hasPrefix && {prefix}} {element} + {hasFeedback && {feedbackIcon}}
    ); } @@ -172,14 +194,15 @@ const InputNumber = defineComponent({ [`${preCls}-group-wrapper-lg`]: mergeSize === 'large', [`${preCls}-group-wrapper-rtl`]: direction.value === 'rtl', }, + getStatusClassNames(`${prefixCls}-group-wrapper`, mergedStatus.value, hasFeedback), className, ); element = (
    - {addonBeforeNode} + {addonBeforeNode && {addonBeforeNode}} {element} - {addonAfterNode} + {addonAfterNode && {addonAfterNode}}
    ); diff --git a/components/input-number/index.zh-CN.md b/components/input-number/index.zh-CN.md index 7b9df3cc4c..0096978323 100644 --- a/components/input-number/index.zh-CN.md +++ b/components/input-number/index.zh-CN.md @@ -34,8 +34,11 @@ cover: https://gw.alipayobjects.com/zos/alicdn/XOS8qZ0kU/InputNumber.svg | precision | 数值精度 | number | - | | | prefix | 带有前缀图标的 input | slot | - | 3.0 | | size | 输入框大小 | string | 无 | | +| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | | step | 每次改变步数,可以为小数 | number\|string | 1 | | | stringMode | 字符值模式,开启后支持高精度小数。同时 `change` 事件将返回 string 类型 | boolean | false | 3.0 | +| upIcon | 自定义上箭头图标 | slot | `` | 3.3.0 | +| downIcon | 自定义下箭头图标 | slot | `` | 3.3.0 | | value(v-model) | 当前值 | number | | | ### 事件 diff --git a/components/input-number/style/affix.less b/components/input-number/style/affix.less index 3724907288..357ab94d30 100644 --- a/components/input-number/style/affix.less +++ b/components/input-number/style/affix.less @@ -8,7 +8,7 @@ &-affix-wrapper { .input(); // or number handler will cover form status - position: static; + position: relative; display: inline-flex; width: 90px; padding: 0; @@ -49,14 +49,33 @@ visibility: hidden; content: '\a0'; } + + .@{ant-prefix}-input-number-handler-wrap { + z-index: 2; + } } - &-prefix { + &-prefix, + &-suffix { display: flex; flex: none; align-items: center; + pointer-events: none; + } + + &-prefix { margin-inline-end: @input-affix-margin; } + + &-suffix { + position: absolute; + top: 0; + right: 0; + z-index: 1; + height: 100%; + margin-right: @input-padding-horizontal-base; + margin-left: @input-affix-margin; + } } .@{ant-prefix}-input-number-group-wrapper .@{ant-prefix}-input-number-affix-wrapper { diff --git a/components/input-number/style/index.less b/components/input-number/style/index.less index fd17e7fda4..97662c0c8a 100644 --- a/components/input-number/style/index.less +++ b/components/input-number/style/index.less @@ -2,6 +2,7 @@ @import '../../style/mixins/index'; @import '../../input/style/mixin'; @import './affix'; +@import './status'; @input-number-prefix-cls: ~'@{ant-prefix}-input-number'; @form-item-prefix-cls: ~'@{ant-prefix}-form-item'; diff --git a/components/input-number/style/status.less b/components/input-number/style/status.less new file mode 100644 index 0000000000..f785da9ad6 --- /dev/null +++ b/components/input-number/style/status.less @@ -0,0 +1,29 @@ +@import '../../input/style/mixin'; + +@input-number-prefix-cls: ~'@{ant-prefix}-input-number'; + +@input-number-wrapper-cls: @input-number-prefix-cls, ~'@{input-number-prefix-cls}-affix-wrapper'; + +each(@input-number-wrapper-cls, { + .@{value} { + &-status-error { + .status-color(@value, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline); + .status-color-common(@input-number-prefix-cls, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline) + } + + &-status-warning { + .status-color(@value, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline); + .status-color-common(@input-number-prefix-cls, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline) + } + } +}); + +.@{input-number-prefix-cls}-group-wrapper { + &-status-error { + .group-status-color(@input-number-prefix-cls, @error-color, @error-color); + } + + &-status-warning { + .group-status-color(@input-number-prefix-cls, @warning-color, @warning-color); + } +} From c1a1b93553fba6ae39242ae0f58616bc03e369f3 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Thu, 19 May 2022 16:38:19 +0800 Subject: [PATCH 021/323] style: update message --- components/message/index.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/components/message/index.tsx b/components/message/index.tsx index b6ff7e3291..35e7c93aa6 100644 --- a/components/message/index.tsx +++ b/components/message/index.tsx @@ -105,7 +105,7 @@ const typeToIcon = { warning: ExclamationCircleFilled, loading: LoadingOutlined, }; - +export const typeList = Object.keys(typeToIcon) as NoticeType[]; export interface MessageType extends PromiseLike { (): void; } @@ -220,9 +220,7 @@ export function attachTypeApi(originalApi: MessageApi, type: NoticeType) { }; } -(['success', 'info', 'warning', 'error', 'loading'] as NoticeType[]).forEach(type => - attachTypeApi(api, type), -); +typeList.forEach(type => attachTypeApi(api, type)); api.warn = api.warning; From d7164217457a75964aff6ce398e64679b172f38c Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Thu, 19 May 2022 17:34:41 +0800 Subject: [PATCH 022/323] feat: mentions add status --- components/mentions/demo/index.vue | 3 ++ components/mentions/demo/status.vue | 50 +++++++++++++++++++++++++++ components/mentions/index.en-US.md | 3 +- components/mentions/index.tsx | 45 +++++++++++++++++++----- components/mentions/index.zh-CN.md | 3 +- components/mentions/style/index.less | 12 +++++++ components/mentions/style/index.tsx | 2 ++ components/mentions/style/status.less | 16 +++++++++ 8 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 components/mentions/demo/status.vue create mode 100644 components/mentions/style/status.less diff --git a/components/mentions/demo/index.vue b/components/mentions/demo/index.vue index 9273b77f7d..882b4098b1 100644 --- a/components/mentions/demo/index.vue +++ b/components/mentions/demo/index.vue @@ -6,6 +6,7 @@ + diff --git a/components/mentions/index.en-US.md b/components/mentions/index.en-US.md index 98622483f0..b28e913b5e 100644 --- a/components/mentions/index.en-US.md +++ b/components/mentions/index.en-US.md @@ -16,7 +16,7 @@ When you need to mention someone or something. ### Mention | Property | Description | Type | Default | -| --- | --- | --- | --- | +| --- | --- | --- | --- | --- | | autofocus | Auto get focus when component mounted | boolean | `false` | | defaultValue | Default value | string | | | filterOption | Customize filter option logic | false \| (input: string, option: OptionProps) => boolean | | @@ -25,6 +25,7 @@ When you need to mention someone or something. | placement | Set popup placement | `top` \| `bottom` | `bottom` | | prefix | Set trigger prefix keyword | string \| string\[] | '@' | | split | Set split string before and after selected mention | string | ' ' | +| status | Set validation status | 'error' \| 'warning' \| 'success' \| 'validating' | - | 3.3.0 | | validateSearch | Customize trigger search logic | (text: string, props: MentionsProps) => void | | | value(v-model) | Set value of mentions | string | | diff --git a/components/mentions/index.tsx b/components/mentions/index.tsx index d4e14cfae6..ad805776c6 100644 --- a/components/mentions/index.tsx +++ b/components/mentions/index.tsx @@ -1,15 +1,17 @@ import type { App, PropType, ExtractPropTypes } from 'vue'; -import { watch, ref, onMounted, defineComponent, nextTick } from 'vue'; +import { computed, watch, ref, onMounted, defineComponent, nextTick } from 'vue'; import classNames from '../_util/classNames'; import PropTypes from '../_util/vue-types'; import VcMentions, { Option } from '../vc-mentions'; import { mentionsProps as baseMentionsProps } from '../vc-mentions/src/mentionsProps'; import useConfigInject from '../_util/hooks/useConfigInject'; import { flattenChildren, getOptionProps } from '../_util/props-util'; -import { useInjectFormItemContext } from '../form/FormItemContext'; +import { FormItemInputContext, useInjectFormItemContext } from '../form/FormItemContext'; import omit from '../_util/omit'; import { optionProps } from '../vc-mentions/src/Option'; import type { KeyboardEventHandler } from '../_util/EventInterface'; +import type { InputStatus } from '../_util/statusUtils'; +import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils'; interface MentionsConfig { prefix?: string | string[]; @@ -83,6 +85,7 @@ export const mentionsProps = () => ({ notFoundContent: PropTypes.any, defaultValue: String, id: String, + status: String as PropType, }); export type MentionsProps = Partial>>; @@ -98,6 +101,8 @@ const Mentions = defineComponent({ const vcMentions = ref(null); const value = ref(props.value ?? props.defaultValue ?? ''); const formItemContext = useInjectFormItemContext(); + const formItemInputContext = FormItemInputContext.useInject(); + const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status)); watch( () => props.value, val => { @@ -174,14 +179,19 @@ const Mentions = defineComponent({ id = formItemContext.id.value, ...restProps } = props; + const { hasFeedback, feedbackIcon } = formItemInputContext; const { class: className, ...otherAttrs } = attrs; const otherProps = omit(restProps, ['defaultValue', 'onUpdate:value', 'prefixCls']); - const mergedClassName = classNames(className, { - [`${prefixCls.value}-disabled`]: disabled, - [`${prefixCls.value}-focused`]: focused.value, - [`${prefixCls.value}-rtl`]: direction.value === 'rtl', - }); + const mergedClassName = classNames( + { + [`${prefixCls.value}-disabled`]: disabled, + [`${prefixCls.value}-focused`]: focused.value, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }, + getStatusClassNames(prefixCls.value, mergedStatus.value), + !hasFeedback && className, + ); const mentionsProps = { prefixCls: prefixCls.value, @@ -202,12 +212,31 @@ const Mentions = defineComponent({ value: value.value, id, }; - return ( + const mentions = ( ); + if (hasFeedback) { + return ( +
    + {mentions} + {feedbackIcon} +
    + ); + } + return mentions; }; }, }); diff --git a/components/mentions/index.zh-CN.md b/components/mentions/index.zh-CN.md index 54f4072510..0afd11e1d4 100644 --- a/components/mentions/index.zh-CN.md +++ b/components/mentions/index.zh-CN.md @@ -17,7 +17,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/jPE-itMFM/Mentions.svg ### Mentions | 参数 | 说明 | 类型 | 默认值 | -| --- | --- | --- | --- | +| --- | --- | --- | --- | --- | | autofocus | 自动获得焦点 | boolean | `false` | | defaultValue | 默认值 | string | | | filterOption | 自定义过滤逻辑 | false \| (input: string, option: OptionProps) => boolean | | @@ -26,6 +26,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/jPE-itMFM/Mentions.svg | placement | 弹出层展示位置 | `top` \| `bottom` | `bottom` | | prefix | 设置触发关键字 | string \| string\[] | '@' | | split | 设置选中项前后分隔符 | string | ' ' | +| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | | validateSearch | 自定义触发验证逻辑 | (text: string, props: MentionsProps) => void | | | value(v-model) | 设置值 | string | | diff --git a/components/mentions/style/index.less b/components/mentions/style/index.less index c18683fca6..06e2b1f295 100644 --- a/components/mentions/style/index.less +++ b/components/mentions/style/index.less @@ -1,6 +1,7 @@ @import '../../style/themes/index'; @import '../../style/mixins/index'; @import '../../input/style/mixin'; +@import './status'; @mention-prefix-cls: ~'@{ant-prefix}-mentions'; @@ -162,6 +163,17 @@ } } } + + &-suffix { + position: absolute; + top: 0; + right: @input-padding-horizontal-base; + bottom: 0; + z-index: 1; + display: inline-flex; + align-items: center; + margin: auto; + } } @import './rtl'; diff --git a/components/mentions/style/index.tsx b/components/mentions/style/index.tsx index 5fa72cf4f6..b39860748d 100644 --- a/components/mentions/style/index.tsx +++ b/components/mentions/style/index.tsx @@ -3,3 +3,5 @@ import './index.less'; // style dependencies import '../../empty/style'; import '../../spin/style'; + +// deps-lint-skip: form diff --git a/components/mentions/style/status.less b/components/mentions/style/status.less new file mode 100644 index 0000000000..92d61e3378 --- /dev/null +++ b/components/mentions/style/status.less @@ -0,0 +1,16 @@ +@import '../../input/style/mixin'; + +@mention-prefix-cls: ~'@{ant-prefix}-mentions'; +@input-prefix-cls: ~'@{ant-prefix}-input'; + +.@{mention-prefix-cls} { + &-status-error { + .status-color(@mention-prefix-cls, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline); + .status-color-common(@input-prefix-cls, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline); + } + + &-status-warning { + .status-color(@mention-prefix-cls, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline); + .status-color-common(@input-prefix-cls, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline); + } +} From 5cf2707e11cb410640a8ae801a702869d3508c5c Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Thu, 19 May 2022 17:42:45 +0800 Subject: [PATCH 023/323] feat: notification add top & bottom placement --- components/notification/demo/placement.vue | 26 ++++++-- components/notification/index.en-US.md | 2 +- components/notification/index.tsx | 27 +++++++- components/notification/index.zh-CN.md | 2 +- components/notification/style/index.less | 30 ++------- components/notification/style/placement.less | 68 ++++++++++++++++++++ 6 files changed, 122 insertions(+), 33 deletions(-) create mode 100644 components/notification/style/placement.less diff --git a/components/notification/demo/placement.vue b/components/notification/demo/placement.vue index 4e400515ef..412b214e30 100644 --- a/components/notification/demo/placement.vue +++ b/components/notification/demo/placement.vue @@ -1,6 +1,6 @@ --- -order: 6 +order: 5 title: zh-CN: 位置 en-US: Placement @@ -8,31 +8,39 @@ title: ## zh-CN -可以设置通知从右上角、右下角、左下角、左上角弹出。 +使用 `placement` 可以配置通知从右上角、右下角、左下角、左上角弹出。 ## en-US -A notification box can pop up from `topRight` or `bottomRight` or `bottomLeft` or `topLeft`. +A notification box can appear from the `topRight`, `bottomRight`, `bottomLeft` or `topLeft` of the viewport via `placement`. diff --git a/components/select/demo/status.vue b/components/select/demo/status.vue new file mode 100644 index 0000000000..ca69965cf2 --- /dev/null +++ b/components/select/demo/status.vue @@ -0,0 +1,38 @@ + +--- +order: 19 +version: 3.3.0 +title: + zh-CN: 自定义状态 + en-US: Status +--- + +## zh-CN + +使用 `status` 为 DatePicker 添加状态,可选 `error` 或者 `warning`。 + +## en-US + +Add status to DatePicker with `status`, which could be `error` or `warning`. + + + + + + diff --git a/components/select/index.en-US.md b/components/select/index.en-US.md index ca27c73862..bc2f55b77b 100644 --- a/components/select/index.en-US.md +++ b/components/select/index.en-US.md @@ -57,14 +57,16 @@ Select component to select value from options. | optionLabelProp | Which prop value of option will render as content of select. | string | `children` \| `label`(when use options) | | | options | Data of the selectOption, manual construction work is no longer needed if this property has been set | array<{value, label, [disabled, key, title]}> | \[] | | | placeholder | Placeholder of select | string\|slot | - | | +| placement | The position where the selection box pops up | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | 3.3.0 | | removeIcon | The custom remove icon | VNode \| slot | - | | | searchValue | The current input "search" text | string | - | | | showArrow | Whether to show the drop-down arrow | boolean | true | | | showSearch | Whether show search input in single mode. | boolean | false | | | size | Size of Select input. `default` `large` `small` | string | default | | +| status | Set validation status | 'error' \| 'warning' | - | 3.3.0 | | suffixIcon | The custom suffix icon | VNode \| slot | - | | | tagRender | Customize tag render, only applies when `mode` is set to `multiple` or `tags` | slot \| (props) => any | - | | -| tokenSeparators | Separator used to tokenize on tag/multiple mode | string\[] | | | +| tokenSeparators | Separator used to tokenize, only applies when `mode="tags"` | string\[] | - | | | value(v-model) | Current selected option. | string\|number\|string\[]\|number\[] | - | | | virtual | Disable virtual scroll when set to false | boolean | true | 3.0 | @@ -114,7 +116,7 @@ Select component to select value from options. ### The dropdown is closed when click `dropdownRender` area? -See the [dropdownRender example](/components/select/#components-select-demo-custom-dropdown). +Dropdown menu will be closed if click `dropdownRender` area, you can prevent it by wrapping `@mousedown.prevent` See the [dropdownRender example](/components/select/#components-select-demo-custom-dropdown). ### Why is `placeholder` not displayed? diff --git a/components/select/index.tsx b/components/select/index.tsx index 4275c54133..ad567fd34f 100644 --- a/components/select/index.tsx +++ b/components/select/index.tsx @@ -9,10 +9,13 @@ import getIcons from './utils/iconUtil'; import PropTypes from '../_util/vue-types'; import useConfigInject from '../_util/hooks/useConfigInject'; import omit from '../_util/omit'; -import { useInjectFormItemContext } from '../form/FormItemContext'; -import { getTransitionName } from '../_util/transition'; +import { FormItemInputContext, useInjectFormItemContext } from '../form/FormItemContext'; +import type { SelectCommonPlacement } from '../_util/transition'; +import { getTransitionDirection, getTransitionName } from '../_util/transition'; import type { SizeType } from '../config-provider'; import { initDefaultProps } from '../_util/props-util'; +import type { InputStatus } from '../_util/statusUtils'; +import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils'; type RawValue = string | number; @@ -48,6 +51,8 @@ export const selectProps = () => ({ bordered: { type: Boolean, default: true }, transitionName: String, choiceTransitionName: { type: String, default: '' }, + placement: String as PropType, + status: String as PropType, 'onUpdate:value': Function as PropType<(val: SelectValue) => void>, }); @@ -81,6 +86,8 @@ const Select = defineComponent({ setup(props, { attrs, emit, slots, expose }) { const selectRef = ref(); const formItemContext = useInjectFormItemContext(); + const formItemInputContext = FormItemInputContext.useInject(); + const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status)); const focus = () => { selectRef.value?.focus(); }; @@ -111,16 +118,33 @@ const Select = defineComponent({ props, ); const rootPrefixCls = computed(() => getPrefixCls()); + // ===================== Placement ===================== + const placement = computed(() => { + if (props.placement !== undefined) { + return props.placement; + } + return direction.value === 'rtl' + ? ('bottomRight' as SelectCommonPlacement) + : ('bottomLeft' as SelectCommonPlacement); + }); const transitionName = computed(() => - getTransitionName(rootPrefixCls.value, 'slide-up', props.transitionName), + getTransitionName( + rootPrefixCls.value, + getTransitionDirection(placement.value), + props.transitionName, + ), ); const mergedClassName = computed(() => - classNames({ - [`${prefixCls.value}-lg`]: size.value === 'large', - [`${prefixCls.value}-sm`]: size.value === 'small', - [`${prefixCls.value}-rtl`]: direction.value === 'rtl', - [`${prefixCls.value}-borderless`]: !props.bordered, - }), + classNames( + { + [`${prefixCls.value}-lg`]: size.value === 'large', + [`${prefixCls.value}-sm`]: size.value === 'small', + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + [`${prefixCls.value}-borderless`]: !props.bordered, + [`${prefixCls.value}-in-form-item`]: formItemInputContext.isFormItemInput, + }, + getStatusClassNames(prefixCls.value, mergedStatus.value, formItemInputContext.hasFeedback), + ), ); const triggerChange: SelectProps['onChange'] = (...args) => { emit('update:value', args[0]); @@ -137,6 +161,12 @@ const Select = defineComponent({ scrollTo, }); const isMultiple = computed(() => mode.value === 'multiple' || mode.value === 'tags'); + const mergedShowArrow = computed(() => + props.showArrow !== undefined + ? props.showArrow + : props.loading || !(isMultiple.value || mode.value === 'combobox'), + ); + return () => { const { notFoundContent, @@ -148,8 +178,9 @@ const Select = defineComponent({ dropdownMatchSelectWidth, id = formItemContext.id.value, placeholder = slots.placeholder?.(), + showArrow, } = props; - + const { hasFeedback, feedbackIcon } = formItemInputContext; const { renderEmpty, getPopupContainer: getContextPopupContainer } = configProvider; // ===================== Empty ===================== @@ -170,6 +201,9 @@ const Select = defineComponent({ ...props, multiple: isMultiple.value, prefixCls: prefixCls.value, + hasFeedback, + feedbackIcon, + showArrow: mergedShowArrow.value, }, slots, ); @@ -182,9 +216,10 @@ const Select = defineComponent({ 'clearIcon', 'size', 'bordered', + 'status', ]); - const rcSelectRtlDropDownClassName = classNames(dropdownClassName, { + const rcSelectRtlDropdownClassName = classNames(dropdownClassName, { [`${prefixCls.value}-dropdown-${direction.value}`]: direction.value === 'rtl', }); return ( @@ -207,7 +242,7 @@ const Select = defineComponent({ notFoundContent={mergedNotFound} class={[mergedClassName.value, attrs.class]} getPopupContainer={getPopupContainer || getContextPopupContainer} - dropdownClassName={rcSelectRtlDropDownClassName} + dropdownClassName={rcSelectRtlDropdownClassName} onChange={triggerChange} onBlur={handleBlur} id={id} @@ -218,6 +253,7 @@ const Select = defineComponent({ tagRender={props.tagRender || slots.tagRender} optionLabelRender={slots.optionLabel} maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder} + showArrow={hasFeedback || showArrow} > ); }; diff --git a/components/select/index.zh-CN.md b/components/select/index.zh-CN.md index 3d9da7c3b3..7329a88460 100644 --- a/components/select/index.zh-CN.md +++ b/components/select/index.zh-CN.md @@ -57,14 +57,16 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg | optionLabelProp | 回填到选择框的 Option 的属性值,默认是 Option 的子元素。比如在子元素需要高亮效果时,此值可以设为 `value`。 | string | `children` \| `label`(设置 options 时) | | | options | options 数据,如果设置则不需要手动构造 selectOption 节点 | array<{value, label, [disabled, key, title]}> | \[] | | | placeholder | 选择框默认文字 | string\|slot | - | | +| placement | 选择框弹出的位置 | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | 3.3.0 | | removeIcon | 自定义的多选框清除图标 | VNode \| slot | - | | | searchValue | 控制搜索文本 | string | - | | | showArrow | 是否显示下拉小箭头 | boolean | true | | | showSearch | 使单选模式可搜索 | boolean | false | | | size | 选择框大小,可选 `large` `small` | string | default | | +| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | | suffixIcon | 自定义的选择框后缀图标 | VNode \| slot | - | | | tagRender | 自定义 tag 内容 render,仅在 `mode` 为 `multiple` 或 `tags` 时生效 | slot \| (props) => any | - | 3.0 | -| tokenSeparators | 在 tags 和 multiple 模式下自动分词的分隔符 | string\[] | | | +| tokenSeparators | 自动分词的分隔符,仅在 `mode="tags"` 时生效 | string\[] | - | | | value(v-model) | 指定当前选中的条目 | string\|string\[]\|number\|number\[] | - | | | virtual | 设置 false 时关闭虚拟滚动 | boolean | true | 3.0 | @@ -114,7 +116,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg ### 点击 `dropdownRender` 里的内容浮层关闭怎么办? -看下 [dropdownRender 例子](/components/select-cn/#components-select-demo-custom-dropdown) 里的说明。 +自定义内容点击时会关闭浮层,如果不喜欢关闭,可以添加 `@mousedown.prevent` 进行阻止。 看下 [dropdownRender 例子](/components/select-cn/#components-select-demo-custom-dropdown) 里的说明。 ### 为什么 `placeholder` 不显示 ? diff --git a/components/select/style/index.less b/components/select/style/index.less index c4007cdeff..474101f26c 100644 --- a/components/select/style/index.less +++ b/components/select/style/index.less @@ -3,6 +3,7 @@ @import '../../input/style/mixin'; @import './single'; @import './multiple'; +@import './status'; @select-prefix-cls: ~'@{ant-prefix}-select'; @select-height-without-border: @input-height-base - 2 * @border-width-base; @@ -12,7 +13,7 @@ position: relative; background-color: @select-background; border: @border-width-base @border-style-base @select-border-color; - border-radius: @border-radius-base; + border-radius: @control-border-radius; transition: all 0.3s @ease-in-out; input { @@ -120,7 +121,8 @@ position: absolute; top: 50%; right: @control-padding-horizontal - 1px; - width: @font-size-sm; + display: flex; + align-items: center; height: @font-size-sm; margin-top: (-@font-size-sm / 2); color: @disabled-color; @@ -145,6 +147,10 @@ .@{select-prefix-cls}-disabled & { cursor: not-allowed; } + + > *:not(:last-child) { + margin-inline-end: @padding-xs; + } } // ========================== Clear ========================== @@ -315,6 +321,10 @@ border-color: transparent !important; box-shadow: none !important; } + + &&-in-form-item { + width: 100%; + } } @import './rtl'; diff --git a/components/select/style/index.tsx b/components/select/style/index.tsx index a914d0b4bd..98037eeccf 100644 --- a/components/select/style/index.tsx +++ b/components/select/style/index.tsx @@ -3,3 +3,5 @@ import './index.less'; // style dependencies import '../../empty/style'; + +// deps-lint-skip: form diff --git a/components/select/style/multiple.less b/components/select/style/multiple.less index 65bdc4ae0c..e9f2fc2fe2 100644 --- a/components/select/style/multiple.less +++ b/components/select/style/multiple.less @@ -110,7 +110,7 @@ cursor: pointer; > .@{iconfont-css-prefix} { - vertical-align: -0.2em; + vertical-align: middle; } &:hover { diff --git a/components/select/style/status.less b/components/select/style/status.less new file mode 100644 index 0000000000..a746a04f62 --- /dev/null +++ b/components/select/style/status.less @@ -0,0 +1,48 @@ +@import '../../input/style/mixin'; + +@select-prefix-cls: ~'@{ant-prefix}-select'; + +.select-status-color( + @text-color; + @border-color; + @background-color; + @hoverBorderColor; + @outlineColor; +) { + &.@{select-prefix-cls}:not(.@{select-prefix-cls}-disabled):not(.@{select-prefix-cls}-customize-input) { + .@{select-prefix-cls}-selector { + background-color: @background-color; + border-color: @border-color !important; + } + &.@{select-prefix-cls}-open .@{select-prefix-cls}-selector, + &.@{select-prefix-cls}-focused .@{select-prefix-cls}-selector { + .active(@border-color, @hoverBorderColor, @outlineColor); + } + } +} + +.@{select-prefix-cls} { + &-status-error { + .select-status-color(@error-color, @error-color, @select-background, @error-color-hover, @error-color-outline); + } + + &-status-warning { + .select-status-color(@warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline); + } + + &-status-error, + &-status-warning, + &-status-success, + &-status-validating { + &.@{select-prefix-cls}-has-feedback { + //.@{prefix-cls}-arrow, + .@{select-prefix-cls}-clear { + right: 32px; + } + + .@{select-prefix-cls}-selection-selected-value { + padding-right: 42px; + } + } + } +} diff --git a/components/select/utils/iconUtil.tsx b/components/select/utils/iconUtil.tsx index 0f4a9541dd..e03f32f7b4 100644 --- a/components/select/utils/iconUtil.tsx +++ b/components/select/utils/iconUtil.tsx @@ -6,7 +6,7 @@ import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; import SearchOutlined from '@ant-design/icons-vue/SearchOutlined'; export default function getIcons(props: any, slots: any = {}) { - const { loading, multiple, prefixCls } = props; + const { loading, multiple, prefixCls, hasFeedback, feedbackIcon, showArrow } = props; const suffixIcon = props.suffixIcon || (slots.suffixIcon && slots.suffixIcon()); const clearIcon = props.clearIcon || (slots.clearIcon && slots.clearIcon()); const menuItemSelectedIcon = @@ -17,20 +17,26 @@ export default function getIcons(props: any, slots: any = {}) { if (!clearIcon) { mergedClearIcon = ; } - + // Validation Feedback Icon + const getSuffixIconNode = arrowIcon => ( + <> + {showArrow !== false && arrowIcon} + {hasFeedback && feedbackIcon} + + ); // Arrow item icon let mergedSuffixIcon = null; if (suffixIcon !== undefined) { - mergedSuffixIcon = suffixIcon; + mergedSuffixIcon = getSuffixIconNode(suffixIcon); } else if (loading) { - mergedSuffixIcon = ; + mergedSuffixIcon = getSuffixIconNode(); } else { const iconCls = `${prefixCls}-suffix`; mergedSuffixIcon = ({ open, showSearch }: { open: boolean; showSearch: boolean }) => { if (open && showSearch) { - return ; + return getSuffixIconNode(); } - return ; + return getSuffixIconNode(); }; } From bf1226d3bb6ff89a75a1af93c240ccf6510a9d0c Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sat, 21 May 2022 22:23:52 +0800 Subject: [PATCH 041/323] feat: timepicker add status & placement --- components/date-picker/demo/status.vue | 2 +- components/time-picker/demo/index.vue | 6 +++ components/time-picker/demo/placement.vue | 46 +++++++++++++++++++++++ components/time-picker/demo/status.vue | 35 +++++++++++++++++ components/time-picker/index.en-US.md | 35 ++++++++++++++--- components/time-picker/index.zh-CN.md | 36 +++++++++++++++--- components/time-picker/time-picker.tsx | 18 ++------- 7 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 components/time-picker/demo/placement.vue create mode 100644 components/time-picker/demo/status.vue diff --git a/components/date-picker/demo/status.vue b/components/date-picker/demo/status.vue index 9e8e43265e..c203090e00 100644 --- a/components/date-picker/demo/status.vue +++ b/components/date-picker/demo/status.vue @@ -22,7 +22,7 @@ Add status to DatePicker with `status`, which could be `error` or `warning`. - + diff --git a/components/time-picker/demo/status.vue b/components/time-picker/demo/status.vue new file mode 100644 index 0000000000..9711edca2c --- /dev/null +++ b/components/time-picker/demo/status.vue @@ -0,0 +1,35 @@ + +--- +order: 19 +version: 3.3.0 +title: + zh-CN: 自定义状态 + en-US: Status +--- + +## zh-CN + +使用 `status` 为 DatePicker 添加状态,可选 `error` 或者 `warning`。 + +## en-US + +Add status to DatePicker with `status`, which could be `error` or `warning`. + + + + + diff --git a/components/time-picker/index.en-US.md b/components/time-picker/index.en-US.md index d75ab1a462..0cd4a01245 100644 --- a/components/time-picker/index.en-US.md +++ b/components/time-picker/index.en-US.md @@ -21,9 +21,7 @@ By clicking the input box, you can select a time from a popup panel. | clearIcon | The custom clear icon | v-slot:clearIcon | - | | | clearText | The clear tooltip of icon | string | clear | | | disabled | Determine whether the TimePicker is disabled | boolean | false | | -| disabledHours | To specify the hours that cannot be selected | function() | - | | -| disabledMinutes | To specify the minutes that cannot be selected | function(selectedHour) | - | | -| disabledSeconds | To specify the seconds that cannot be selected | function(selectedHour, selectedMinute) | - | | +| disabledTime | To specify the time that cannot be selected | [DisabledTime](#DisabledTime) | - | 3.3.0 | | format | To set the time format | string | `HH:mm:ss` | | | getPopupContainer | To set the container of the floating layer, while the default is to create a div element in body | function(trigger) | - | | | hideDisabledOptions | Whether hide the options that can not be selected | boolean | false | | @@ -32,6 +30,7 @@ By clicking the input box, you can select a time from a popup panel. | minuteStep | Interval between minutes in picker | number | 1 | | | open(v-model) | Whether to popup panel | boolean | false | | | placeholder | Display when there's no value | string \| \[string, string] | `Select a time` | | +| placement | The position where the selection box pops up | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | | | popupClassName | The className of panel | string | - | | | popupStyle | The style of panel | CSSProperties | - | | | renderExtraFooter | Called from time picker panel to render some addon to its bottom | v-slot:renderExtraFooter | - | | @@ -42,6 +41,16 @@ By clicking the input box, you can select a time from a popup panel. | value(v-model) | To set time | [dayjs](https://day.js.org/) | - | | | valueFormat | optional, format of binding value. If not specified, the binding value will be a Date object | string,[date formats](https://day.js.org/docs/en/display/format) | - | | +#### DisabledTime + +```typescript +type DisabledTime = (now: Dayjs) => { + disabledHours?: () => number[]; + disabledMinutes?: (selectedHour: number) => number[]; + disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[]; +}; +``` + ### events | Events Name | Description | Arguments | @@ -60,9 +69,23 @@ By clicking the input box, you can select a time from a popup panel. Same props from [RangePicker](/components/date-picker/#RangePicker) of DatePicker. And includes additional props: -| Property | Description | Type | Default | Version | -| -------- | ------------------------ | ------- | ------- | ------- | -| order | Order start and end time | boolean | true | | +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| order | Order start and end time | boolean | true | | +| disabledTime | To specify the time that cannot be selected | [RangeDisabledTime](#RangeDisabledTime) | - | 3.3.0 | + +#### RangeDisabledTime + +```typescript +type RangeDisabledTime = ( + now: Dayjs, + type = 'start' | 'end', +) => { + disabledHours?: () => number[]; + disabledMinutes?: (selectedHour: number) => number[]; + disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[]; +}; +``` ## FAQ diff --git a/components/time-picker/index.zh-CN.md b/components/time-picker/index.zh-CN.md index 9fa6c062bf..dbf5b32b57 100644 --- a/components/time-picker/index.zh-CN.md +++ b/components/time-picker/index.zh-CN.md @@ -22,9 +22,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/h04Zsl98I/TimePicker.svg | clearIcon | 自定义的清除图标 | v-slot:clearIcon | - | | | clearText | 清除按钮的提示文案 | string | clear | | | disabled | 禁用全部操作 | boolean | false | | -| disabledHours | 禁止选择部分小时选项 | function() | - | | -| disabledMinutes | 禁止选择部分分钟选项 | function(selectedHour) | - | | -| disabledSeconds | 禁止选择部分秒选项 | function(selectedHour, selectedMinute) | - | | +| disabledTime | 不可选择的时间 | [DisabledTime](#DisabledTime) | - | 3.3.0 | | format | 展示的时间格式 | string | `HH:mm:ss` | | | getPopupContainer | 定义浮层的容器,默认为 body 上新建 div | function(trigger) | - | | | hideDisabledOptions | 隐藏禁止选择的选项 | boolean | false | | @@ -33,16 +31,28 @@ cover: https://gw.alipayobjects.com/zos/alicdn/h04Zsl98I/TimePicker.svg | minuteStep | 分钟选项间隔 | number | 1 | | | open(v-model) | 面板是否打开 | boolean | false | | | placeholder | 没有值的时候显示的内容 | string \| \[string, string] | `请选择时间` | | +| placement | 选择框弹出的位置 | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | | | popupClassName | 弹出层类名 | string | - | | | popupStyle | 弹出层样式对象 | object | - | | | renderExtraFooter | 选择框底部显示自定义的内容 | v-slot:renderExtraFooter | - | | | secondStep | 秒选项间隔 | number | 1 | | | showNow | 面板是否显示“此刻”按钮 | boolean | - | | +| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | | suffixIcon | 自定义的选择框后缀图标 | v-slot:suffixIcon | - | | | use12Hours | 使用 12 小时制,为 true 时 `format` 默认为 `h:mm:ss a` | boolean | false | | | value(v-model) | 当前时间 | [dayjs](https://day.js.org/) | - | | | valueFormat | 可选,绑定值的格式,对 value、defaultValue 起作用。不指定则绑定值为 dayjs 对象 | string,[具体格式](https://day.js.org/docs/zh-CN/display/format) | - | | +#### DisabledTime + +```typescript +type DisabledTime = (now: Dayjs) => { + disabledHours?: () => number[]; + disabledMinutes?: (selectedHour: number) => number[]; + disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[]; +}; +``` + ### 事件 | 事件名称 | 说明 | 回调参数 | @@ -61,9 +71,23 @@ cover: https://gw.alipayobjects.com/zos/alicdn/h04Zsl98I/TimePicker.svg 属性与 DatePicker 的 [RangePicker](/components/date-picker/#RangePicker) 相同。还包含以下属性: -| 参数 | 说明 | 类型 | 默认值 | 版本 | -| ----- | -------------------- | ------- | ------ | ---- | -| order | 始末时间是否自动排序 | boolean | true | | +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| ------------ | -------------------- | --------------------------------------- | ------ | ----- | +| order | 始末时间是否自动排序 | boolean | true | | +| disabledTime | 不可选择的时间 | [RangeDisabledTime](#RangeDisabledTime) | - | 3.3.0 | + +#### RangeDisabledTime + +```typescript +type RangeDisabledTime = ( + now: Dayjs, + type = 'start' | 'end', +) => { + disabledHours?: () => number[]; + disabledMinutes?: (selectedHour: number) => number[]; + disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[]; +}; +``` ## FAQ diff --git a/components/time-picker/time-picker.tsx b/components/time-picker/time-picker.tsx index 49015c84e1..534cb0191f 100644 --- a/components/time-picker/time-picker.tsx +++ b/components/time-picker/time-picker.tsx @@ -1,3 +1,4 @@ +import type { ExtractPropTypes, PropType } from 'vue'; import { defineComponent, ref } from 'vue'; import type { RangePickerTimeProps } from '../date-picker/generatePicker'; import generatePicker from '../date-picker/generatePicker'; @@ -13,6 +14,7 @@ import type { RangePickerSharedProps } from '../vc-picker/RangePicker'; import devWarning from '../vc-util/devWarning'; import { useInjectFormItemContext } from '../form/FormItemContext'; import omit from '../_util/omit'; +import type { InputStatus } from '../_util/statusUtils'; export interface TimePickerLocale { placeholder?: string; @@ -31,21 +33,9 @@ export const timePickerProps = () => ({ secondStep: Number, hideDisabledOptions: { type: Boolean, default: undefined }, popupClassName: String, + status: String as PropType, }); - -export interface CommonTimePickerProps { - format?: string; - showNow?: boolean; - showHour?: boolean; - showMinute?: boolean; - showSecond?: boolean; - use12Hours?: boolean; - hourStep?: number; - minuteStep?: number; - secondStep?: number; - hideDisabledOptions?: boolean; - popupClassName?: string; -} +type CommonTimePickerProps = Partial>>; export type TimeRangePickerProps = Omit< RangePickerTimeProps, 'picker' | 'defaultPickerValue' | 'defaultValue' | 'value' | 'onChange' | 'onPanelChange' | 'onOk' From 03f559a0dc9ab3964d90ccbb7b988cb5835e80bd Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sun, 22 May 2022 10:10:43 +0800 Subject: [PATCH 042/323] feat: tree-select add status & placement --- components/tree-select/demo/index.vue | 6 ++ components/tree-select/demo/placement.vue | 85 +++++++++++++++++++ components/tree-select/demo/status.vue | 33 +++++++ components/tree-select/index.en-US.md | 3 + components/tree-select/index.tsx | 38 ++++++++- components/tree-select/index.zh-CN.md | 3 + components/tree-select/style/index.tsx | 2 +- components/vc-tree-select/OptionList.tsx | 6 +- components/vc-tree-select/TreeSelect.tsx | 8 +- .../vc-tree-select/TreeSelectContext.ts | 3 + components/vc-tree-select/index.tsx | 2 +- 11 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 components/tree-select/demo/placement.vue create mode 100644 components/tree-select/demo/status.vue diff --git a/components/tree-select/demo/index.vue b/components/tree-select/demo/index.vue index dd6177efe7..931e249a78 100644 --- a/components/tree-select/demo/index.vue +++ b/components/tree-select/demo/index.vue @@ -11,6 +11,8 @@ + + diff --git a/components/tree-select/demo/status.vue b/components/tree-select/demo/status.vue new file mode 100644 index 0000000000..77f4b667ae --- /dev/null +++ b/components/tree-select/demo/status.vue @@ -0,0 +1,33 @@ + +--- +order: 19 +version: 3.3.0 +title: + zh-CN: 自定义状态 + en-US: Status +--- + +## zh-CN + +使用 `status` 为 DatePicker 添加状态,可选 `error` 或者 `warning`。 + +## en-US + +Add status to DatePicker with `status`, which could be `error` or `warning`. + + + + + diff --git a/components/tree-select/index.en-US.md b/components/tree-select/index.en-US.md index 19becd3a99..f4ce051630 100644 --- a/components/tree-select/index.en-US.md +++ b/components/tree-select/index.en-US.md @@ -34,12 +34,14 @@ Tree selection control. | multiple | Support multiple or not, will be `true` when enable `treeCheckable`. | boolean | false | | | | notFoundContent | Specify content to show when no result matches | slot | `Not Found` | | | | placeholder | Placeholder of the select input | string\|slot | - | | | +| placement | The position where the selection box pops up | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | 3.3.0 | | replaceFields | Replace the title,value, key and children fields in treeNode with the corresponding fields in treeData | object | { children:'children', label:'title', value: 'value' } | | 1.6.1 (3.0.0 deprecated) | | searchPlaceholder | Placeholder of the search input | string\|slot | - | | | | searchValue(v-model) | work with `search` event to make search value controlled. | string | - | | | | showCheckedStrategy | The way show selected item in box. **Default:** just show child nodes. **`TreeSelect.SHOW_ALL`:** show all checked treeNodes (include parent treeNode). **`TreeSelect.SHOW_PARENT`:** show checked treeNodes (just show parent treeNode). | enum { TreeSelect.SHOW_ALL, TreeSelect.SHOW_PARENT, TreeSelect.SHOW_CHILD } | TreeSelect.SHOW_CHILD | | | | showSearch | Whether to display a search input in the dropdown menu(valid only in the single mode) | boolean | false | | | | size | To set the size of the select input, options: `large` `small` | string | 'default' | | | +| status | Set validation status | 'error' \| 'warning' | - | 3.3.0 | | suffixIcon | The custom suffix icon | VNode \| slot | - | | | | tagRender | Customize tag render when `multiple` | (props) => slot | - | 3.0 | | | title | custom title | slot | | 3.0.0 | | @@ -51,6 +53,7 @@ Tree selection control. | treeDefaultExpandedKeys | Default expanded treeNodes | string\[] \| number\[] | - | | | | treeExpandedKeys(v-model) | Set expanded keys | string\[] \| number\[] | - | | | | 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 | | | +| treeLoadedKeys | (Controlled) Set loaded tree nodes, work with `loadData` only | string[] | [] | 3.3.0 | | treeLine | Show the line. Ref [Tree - showLine](/components/tree/#components-tree-demo-line) | boolean \| object | false | 3.0 | | | treeNodeFilterProp | Will be used for filtering if `filterTreeNode` returns true | string | 'value' | | | | treeNodeLabelProp | Will render as content of select | string | 'title' | | | diff --git a/components/tree-select/index.tsx b/components/tree-select/index.tsx index 2d7b430b5d..4ad7a2cc2b 100644 --- a/components/tree-select/index.tsx +++ b/components/tree-select/index.tsx @@ -20,10 +20,14 @@ import type { SwitcherIconProps } from '../tree/utils/iconUtil'; import renderSwitcherIcon from '../tree/utils/iconUtil'; import { warning } from '../vc-util/warning'; import { flattenChildren } from '../_util/props-util'; -import { useInjectFormItemContext } from '../form/FormItemContext'; +import { FormItemInputContext, useInjectFormItemContext } from '../form/FormItemContext'; import type { BaseSelectRef } from '../vc-select'; import type { BaseOptionType, DefaultOptionType } from '../vc-tree-select/TreeSelect'; import type { TreeProps } from '../tree'; +import type { SelectCommonPlacement } from '../_util/transition'; +import { getTransitionDirection } from '../_util/transition'; +import type { InputStatus } from '../_util/statusUtils'; +import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils'; const getTransitionName = (rootPrefixCls: string, motion: string, transitionName?: string) => { if (transitionName !== undefined) { @@ -62,6 +66,8 @@ export function treeSelectProps< bordered: { type: Boolean, default: undefined }, treeLine: { type: [Boolean, Object] as PropType, default: undefined }, replaceFields: { type: Object as PropType }, + placement: String as PropType, + status: String as PropType, 'onUpdate:value': { type: Function as PropType<(value: any) => void> }, 'onUpdate:treeExpandedKeys': { type: Function as PropType<(keys: Key[]) => void> }, 'onUpdate:searchValue': { type: Function as PropType<(value: string) => void> }, @@ -107,6 +113,8 @@ const TreeSelect = defineComponent({ }); const formItemContext = useInjectFormItemContext(); + const formItemInputContext = FormItemInputContext.useInject(); + const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status)); const { prefixCls, renderEmpty, @@ -118,8 +126,21 @@ const TreeSelect = defineComponent({ getPrefixCls, } = useConfigInject('select', props); const rootPrefixCls = computed(() => getPrefixCls()); + // ===================== Placement ===================== + const placement = computed(() => { + if (props.placement !== undefined) { + return props.placement; + } + return direction.value === 'rtl' + ? ('bottomRight' as SelectCommonPlacement) + : ('bottomLeft' as SelectCommonPlacement); + }); const transitionName = computed(() => - getTransitionName(rootPrefixCls.value, 'slide-up', props.transitionName), + getTransitionName( + rootPrefixCls.value, + getTransitionDirection(placement.value), + props.transitionName, + ), ); const choiceTransitionName = computed(() => getTransitionName(rootPrefixCls.value, '', props.choiceTransitionName), @@ -134,6 +155,9 @@ const TreeSelect = defineComponent({ ); const isMultiple = computed(() => !!(props.treeCheckable || props.multiple)); + const mergedShowArrow = computed(() => + props.showArrow !== undefined ? props.showArrow : props.loading || !isMultiple.value, + ); const treeSelectRef = ref(); expose({ @@ -173,15 +197,20 @@ const TreeSelect = defineComponent({ multiple, treeIcon, treeLine, + showArrow, switcherIcon = slots.switcherIcon?.(), fieldNames = props.replaceFields, id = formItemContext.id.value, } = props; + const { isFormItemInput, hasFeedback, feedbackIcon } = formItemInputContext; // ===================== Icons ===================== const { suffixIcon, removeIcon, clearIcon } = getIcons( { ...props, multiple: isMultiple.value, + showArrow: mergedShowArrow.value, + hasFeedback, + feedbackIcon, prefixCls: prefixCls.value, }, slots, @@ -202,6 +231,7 @@ const TreeSelect = defineComponent({ 'clearIcon', 'switcherIcon', 'bordered', + 'status', 'onUpdate:value', 'onUpdate:treeExpandedKeys', 'onUpdate:searchValue', @@ -214,7 +244,9 @@ const TreeSelect = defineComponent({ [`${prefixCls.value}-sm`]: size.value === 'small', [`${prefixCls.value}-rtl`]: direction.value === 'rtl', [`${prefixCls.value}-borderless`]: !bordered, + [`${prefixCls.value}-in-form-item`]: isFormItemInput, }, + getStatusClassNames(prefixCls.value, mergedStatus.value, hasFeedback), attrs.class, ); const otherProps: any = {}; @@ -263,6 +295,8 @@ const TreeSelect = defineComponent({ treeCheckable: () => , }} maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder} + placement={placement.value} + showArrow={hasFeedback || showArrow} /> ); }; diff --git a/components/tree-select/index.zh-CN.md b/components/tree-select/index.zh-CN.md index e41aef1ae5..7284c01b98 100644 --- a/components/tree-select/index.zh-CN.md +++ b/components/tree-select/index.zh-CN.md @@ -35,12 +35,14 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg | multiple | 支持多选(当设置 treeCheckable 时自动变为 true) | boolean | false | | | | notFoundContent | 当下拉列表为空时显示的内容 | slot | `Not Found` | | | | placeholder | 选择框默认文字 | string\|slot | - | | | +| placement | 选择框弹出的位置 | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | 3.3.0 | | replaceFields | 替换 treeNode 中 title,value,key,children 字段为 treeData 中对应的字段 | object | {children:'children', label:'title', key:'key', value: 'value' } | | 1.6.1 (3.0.0 废弃) | | searchPlaceholder | 搜索框默认文字 | string\|slot | - | | | | searchValue(v-model) | 搜索框的值,可以通过 `search` 事件获取用户输入 | string | - | | | | showCheckedStrategy | 定义选中项回填的方式。`TreeSelect.SHOW_ALL`: 显示所有选中节点(包括父节点). `TreeSelect.SHOW_PARENT`: 只显示父节点(当父节点下所有子节点都选中时). 默认只显示子节点. | enum{TreeSelect.SHOW_ALL, TreeSelect.SHOW_PARENT, TreeSelect.SHOW_CHILD } | TreeSelect.SHOW_CHILD | | | | showSearch | 在下拉中显示搜索框(仅在单选模式下生效) | boolean | false | | | | size | 选择框大小,可选 `large` `small` | string | 'default' | | | +| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | | suffixIcon | 自定义的选择框后缀图标 | VNode \| slot | - | | | | tagRender | 自定义 tag 内容,多选时生效 | slot | - | 3.0 | | | title | 自定义标题 | slot | | 3.0.0 | | @@ -53,6 +55,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/Ax4DA0njr/TreeSelect.svg | treeExpandedKeys(v-model) | 设置展开的树节点 | string\[] \| number\[] | - | | | | treeIcon | 是否展示 TreeNode title 前的图标,没有默认样式,如设置为 true,需要自行定义图标相关样式 | boolean | false | | | | treeLine | 是否展示线条样式,请参考 [Tree - showLine](/components/tree/#components-tree-demo-line) | boolean \| object | false | 3.0 | | +| treeLoadedKeys | (受控)已经加载的节点,需要配合 `loadData` 使用 | string[] | [] | 3.3.0 | | treeNodeFilterProp | 输入项过滤对应的 treeNode 属性 | string | 'value' | | | | treeNodeLabelProp | 作为显示的 prop 设置 | string | 'title' | | | | value(v-model) | 指定当前选中的条目 | string/string\[] | - | | | diff --git a/components/tree-select/style/index.tsx b/components/tree-select/style/index.tsx index 3365572fd9..def4cc2b8c 100644 --- a/components/tree-select/style/index.tsx +++ b/components/tree-select/style/index.tsx @@ -2,6 +2,6 @@ import '../../style/index.less'; import './index.less'; // style dependencies -// deps-lint-skip: tree +// deps-lint-skip: tree, form import '../../select/style'; import '../../empty/style'; diff --git a/components/vc-tree-select/OptionList.tsx b/components/vc-tree-select/OptionList.tsx index fd3bd65a9e..36afa921b7 100644 --- a/components/vc-tree-select/OptionList.tsx +++ b/components/vc-tree-select/OptionList.tsx @@ -181,7 +181,8 @@ export default defineComponent({ open, notFoundContent = slots.notFoundContent?.(), } = baseProps; - const { listHeight, listItemHeight, virtual } = context; + const { listHeight, listItemHeight, virtual, dropdownMatchSelectWidth, treeExpandAction } = + context; const { checkable, treeDefaultExpandAll, @@ -228,7 +229,7 @@ export default defineComponent({ treeData={memoTreeData.value as TreeDataNode[]} height={listHeight} itemHeight={listItemHeight} - virtual={virtual} + virtual={virtual !== false && dropdownMatchSelectWidth !== false} multiple={multiple} icon={treeIcon} showIcon={showTreeIcon} @@ -251,6 +252,7 @@ export default defineComponent({ onExpand={onInternalExpand} onLoad={onTreeLoad} filterTreeNode={filterTreeNode} + expandAction={treeExpandAction} v-slots={{ ...slots, checkable: legacyContext.customSlots.treeCheckable }} /> diff --git a/components/vc-tree-select/TreeSelect.tsx b/components/vc-tree-select/TreeSelect.tsx index 801e528054..5d518e392c 100644 --- a/components/vc-tree-select/TreeSelect.tsx +++ b/components/vc-tree-select/TreeSelect.tsx @@ -30,6 +30,7 @@ import { conductCheck } from '../vc-tree/utils/conductUtil'; import { warning } from '../vc-util/warning'; import { toReactive } from '../_util/toReactive'; import useMaxLevel from '../vc-tree/useMaxLevel'; +import type { ExpandAction } from '../tree/DirectoryTree'; export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void; @@ -180,6 +181,7 @@ export function treeSelectProps< switcherIcon: PropTypes.any, treeMotion: PropTypes.any, children: Array as PropType, + treeExpandAction: String as PropType, showArrow: { type: Boolean, default: undefined }, showSearch: { type: Boolean, default: undefined }, @@ -621,8 +623,10 @@ export default defineComponent({ switcherIcon, treeMotion, customSlots, + + dropdownMatchSelectWidth, + treeExpandAction, } = toRefs(props); - toRaw; useProvideLegacySelectContext( toReactive({ checkable: mergedCheckable, @@ -654,6 +658,8 @@ export default defineComponent({ treeData: filteredTreeData, fieldNames: mergedFieldNames, onSelect: onOptionSelect, + dropdownMatchSelectWidth, + treeExpandAction, } as unknown as TreeSelectContextProps), ); const selectRef = ref(); diff --git a/components/vc-tree-select/TreeSelectContext.ts b/components/vc-tree-select/TreeSelectContext.ts index c8ffe20cc0..3470b76342 100644 --- a/components/vc-tree-select/TreeSelectContext.ts +++ b/components/vc-tree-select/TreeSelectContext.ts @@ -1,14 +1,17 @@ import type { InjectionKey } from 'vue'; import { provide, inject } from 'vue'; +import type { ExpandAction } from '../tree/DirectoryTree'; import type { DefaultOptionType, InternalFieldName, OnInternalSelect } from './TreeSelect'; export interface TreeSelectContextProps { virtual?: boolean; + dropdownMatchSelectWidth?: boolean | number; listHeight: number; listItemHeight: number; treeData: DefaultOptionType[]; fieldNames: InternalFieldName; onSelect: OnInternalSelect; + treeExpandAction?: ExpandAction; } const TreeSelectContextPropsKey: InjectionKey = Symbol( diff --git a/components/vc-tree-select/index.tsx b/components/vc-tree-select/index.tsx index d126930957..7348edc901 100644 --- a/components/vc-tree-select/index.tsx +++ b/components/vc-tree-select/index.tsx @@ -1,4 +1,4 @@ -// base rc-tree-select@5.1.4 +// base rc-tree-select@5.4.0 import type { TreeSelectProps } from './TreeSelect'; import TreeSelect, { treeSelectProps } from './TreeSelect'; import TreeNode from './TreeNode'; From a4b6c0aee4854ea86caa8881ea1ecf1797b0642d Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sun, 22 May 2022 10:35:19 +0800 Subject: [PATCH 043/323] feat: transfer add status --- components/transfer/ListBody.tsx | 7 +++++- components/transfer/demo/index.vue | 3 +++ components/transfer/demo/status.vue | 33 +++++++++++++++++++++++++++ components/transfer/index.en-US.md | 3 ++- components/transfer/index.tsx | 23 ++++++++++++++----- components/transfer/index.zh-CN.md | 3 ++- components/transfer/interface.ts | 3 +++ components/transfer/list.tsx | 2 +- components/transfer/style/index.less | 1 + components/transfer/style/index.tsx | 2 ++ components/transfer/style/status.less | 31 +++++++++++++++++++++++++ 11 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 components/transfer/demo/status.vue create mode 100644 components/transfer/style/status.less diff --git a/components/transfer/ListBody.tsx b/components/transfer/ListBody.tsx index cd5a545133..8acadd1893 100644 --- a/components/transfer/ListBody.tsx +++ b/components/transfer/ListBody.tsx @@ -27,6 +27,9 @@ function parsePagination(pagination) { const defaultPagination = { pageSize: 10, + simple: true, + showSizeChanger: false, + showLessItems: false, }; if (typeof pagination === 'object') { @@ -114,7 +117,9 @@ const ListBody = defineComponent({ if (mergedPagination.value) { paginationNode = ( + diff --git a/components/transfer/index.en-US.md b/components/transfer/index.en-US.md index e1107b0345..815ad72741 100644 --- a/components/transfer/index.en-US.md +++ b/components/transfer/index.en-US.md @@ -29,11 +29,12 @@ One or more elements can be selected from either column, one click on the proper | oneWay | Display as single direction style | boolean | false | 3.0.0 | | operations | A set of operations that are sorted from top to bottom. | string\[] | \['>', '<'] | | | operationStyle | A custom CSS style used for rendering the operations column | CSSProperties | - | 3.0.0 | -| pagination | Use pagination. Not work in render props | boolean \| { pageSize: number } | false | 3.0.0 | +| pagination | Use pagination. Not work in render props | boolean \| { pageSize: number, simple: boolean, showSizeChanger?: boolean, showLessItems?: boolean } | false | 3.0.0 | | render | The function to generate the item shown on a column. Based on an record (element of the dataSource array), this function should return a element which is generated from that record. Also, it can return a plain object with `value` and `label`, `label` is a element and `value` is for title | Function(record) \| slot | | | | selectedKeys(v-model) | A set of keys of selected items. | string\[] | \[] | | | showSearch | If included, a search box is shown on each column. | boolean | false | | | showSelectAll | Show select all checkbox on the header | boolean | true | | +| status | Set validation status | 'error' \| 'warning' | - | 3.3.0 | | targetKeys(v-model) | A set of keys of elements that are listed on the right column. | string\[] | \[] | | | titles | A set of titles that are sorted from left to right. | string\[] | - | | diff --git a/components/transfer/index.tsx b/components/transfer/index.tsx index b772a9d870..ce7e46403d 100644 --- a/components/transfer/index.tsx +++ b/components/transfer/index.tsx @@ -1,5 +1,5 @@ import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'; -import { watchEffect, defineComponent, ref, watch, toRaw } from 'vue'; +import { computed, watchEffect, defineComponent, ref, watch, toRaw } from 'vue'; import PropTypes from '../_util/vue-types'; import { getPropsSlot } from '../_util/props-util'; import classNames from '../_util/classNames'; @@ -12,8 +12,10 @@ import { withInstall } from '../_util/type'; import useConfigInject from '../_util/hooks/useConfigInject'; import type { TransferListBodyProps } from './ListBody'; import type { PaginationType } from './interface'; -import { useInjectFormItemContext } from '../form/FormItemContext'; +import { FormItemInputContext, useInjectFormItemContext } from '../form/FormItemContext'; import type { RenderEmptyHandler } from '../config-provider/renderEmpty'; +import type { InputStatus } from '../_util/statusUtils'; +import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils'; export type { TransferListProps } from './list'; export type { TransferOperationProps } from './operation'; @@ -90,6 +92,7 @@ export const transferProps = () => ({ children: { type: Function as PropType<(props: TransferListBodyProps) => VueNode> }, oneWay: { type: Boolean, default: undefined }, pagination: { type: [Object, Boolean] as PropType, default: undefined }, + status: String as PropType, onChange: Function as PropType< (targetKeys: string[], direction: TransferDirection, moveKeys: string[]) => void >, @@ -125,6 +128,8 @@ const Transfer = defineComponent({ const targetSelectedKeys = ref([]); const formItemContext = useInjectFormItemContext(); + const formItemInputContext = FormItemInputContext.useInject(); + const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status)); watch( () => props.selectedKeys, () => { @@ -334,10 +339,16 @@ const Transfer = defineComponent({ const leftActive = targetSelectedKeys.value.length > 0; const rightActive = sourceSelectedKeys.value.length > 0; - const cls = classNames(prefixCls.value, className, { - [`${prefixCls.value}-disabled`]: disabled, - [`${prefixCls.value}-customize-list`]: !!children, - }); + const cls = classNames( + prefixCls.value, + className, + { + [`${prefixCls.value}-disabled`]: disabled, + [`${prefixCls.value}-customize-list`]: !!children, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }, + getStatusClassNames(prefixCls.value, mergedStatus.value, formItemInputContext.hasFeedback), + ); const titles = props.titles; const leftTitle = (titles && titles[0]) ?? slots.leftTitle?.() ?? (locale.titles || ['', ''])[0]; diff --git a/components/transfer/index.zh-CN.md b/components/transfer/index.zh-CN.md index ad4d190190..c0b34b7f8e 100644 --- a/components/transfer/index.zh-CN.md +++ b/components/transfer/index.zh-CN.md @@ -30,11 +30,12 @@ cover: https://gw.alipayobjects.com/zos/alicdn/QAXskNI4G/Transfer.svg | oneWay | 展示为单向样式 | boolean | false | 3.0.0 | | operations | 操作文案集合,顺序从上至下 | string\[] | \['>', '<'] | | | operationStyle | 操作栏的自定义样式 | CSSProperties | - | 3.0.0 | -| pagination | 使用分页样式,自定义渲染列表下无效 | boolean \| { pageSize: number } | flase | 3.0.0 | +| pagination | 使用分页样式,自定义渲染列表下无效 | boolean \| { pageSize: number, simple: boolean, showSizeChanger?: boolean, showLessItems?: boolean } | flase | 3.0.0 | | render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 element。或者返回一个普通对象,其中 `label` 字段为 element,`value` 字段为 title | Function(record)\| slot | | | | selectedKeys(v-model) | 设置哪些项应该被选中 | string\[] | \[] | | | showSearch | 是否显示搜索框 | boolean | false | | | showSelectAll | 是否展示全选勾选框 | boolean | true | | +| status | 设置校验状态 | 'error' \| 'warning' | - | 3.3.0 | | targetKeys(v-model) | 显示在右侧框数据的 key 集合 | string\[] | \[] | | | titles | 标题集合,顺序从左至右 | string\[] | \['', ''] | | diff --git a/components/transfer/interface.ts b/components/transfer/interface.ts index d45cd7a655..d56acf0954 100644 --- a/components/transfer/interface.ts +++ b/components/transfer/interface.ts @@ -2,4 +2,7 @@ export type PaginationType = | boolean | { pageSize?: number; + simple?: boolean; + showSizeChanger?: boolean; + showLessItems?: boolean; }; diff --git a/components/transfer/list.tsx b/components/transfer/list.tsx index ae8150d9bd..3f78a3cef0 100644 --- a/components/transfer/list.tsx +++ b/components/transfer/list.tsx @@ -15,7 +15,7 @@ import type { TransferDirection, TransferItem } from './index'; const defaultRender = () => null; function isRenderResultPlainObject(result: VNode) { - return ( + return !!( result && !isValidElement(result) && Object.prototype.toString.call(result) === '[object Object]' diff --git a/components/transfer/style/index.less b/components/transfer/style/index.less index e7005bb062..c40b5537c4 100644 --- a/components/transfer/style/index.less +++ b/components/transfer/style/index.less @@ -2,6 +2,7 @@ @import '../../style/mixins/index'; @import '../../checkbox/style/mixin'; @import './customize'; +@import './status'; @transfer-prefix-cls: ~'@{ant-prefix}-transfer'; diff --git a/components/transfer/style/index.tsx b/components/transfer/style/index.tsx index 0cb16d7ce6..4bf2723fe7 100644 --- a/components/transfer/style/index.tsx +++ b/components/transfer/style/index.tsx @@ -9,3 +9,5 @@ import '../../input/style'; import '../../menu/style'; import '../../dropdown/style'; import '../../pagination/style'; + +// deps-lint-skip: form diff --git a/components/transfer/style/status.less b/components/transfer/style/status.less new file mode 100644 index 0000000000..a861223174 --- /dev/null +++ b/components/transfer/style/status.less @@ -0,0 +1,31 @@ +@import '../../input/style/mixin'; + +@transfer-prefix-cls: ~'@{ant-prefix}-transfer'; + +.transfer-status-color(@color) { + .@{transfer-prefix-cls}-list { + border-color: @color; + + &-search:not([disabled]) { + border-color: @input-border-color; + + &:hover { + .hover(); + } + + &:focus { + .active(); + } + } + } +} + +.@{transfer-prefix-cls} { + &-status-error { + .transfer-status-color(@error-color); + } + + &-status-warning { + .transfer-status-color(@warning-color); + } +} From 37d35f7801de5780c40534dadd2cccbab37ffa11 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sun, 22 May 2022 14:23:53 +0800 Subject: [PATCH 044/323] feat: table add filterResetToDefaultFilteredValue & filterSearch funcion --- components/table/demo/filter-search.vue | 113 ++++++++++++++++++ components/table/demo/index.vue | 3 + .../table/hooks/useFilter/FilterDropdown.tsx | 22 +++- .../table/hooks/useFilter/FilterSearch.tsx | 4 +- components/table/hooks/useFilter/index.tsx | 22 ++-- components/table/hooks/useSorter.tsx | 32 ++++- components/table/index.en-US.md | 3 +- components/table/index.zh-CN.md | 3 +- components/table/interface.tsx | 9 +- components/table/style/index.less | 8 +- components/table/style/size.less | 15 +-- 11 files changed, 195 insertions(+), 39 deletions(-) create mode 100644 components/table/demo/filter-search.vue diff --git a/components/table/demo/filter-search.vue b/components/table/demo/filter-search.vue new file mode 100644 index 0000000000..4921f51c58 --- /dev/null +++ b/components/table/demo/filter-search.vue @@ -0,0 +1,113 @@ + +--- +order: 6.2 +version: 4.19.0 +title: + en-US: Filter search + zh-CN: 自定义筛选的搜索 +--- + +## zh-CN + +`filterSearch` 用于开启筛选项的搜索,通过 `filterSearch:(input, record) => boolean` 设置自定义筛选方法 + +## en-US + +`filterSearch` is used to enable search of filter items, and you can set a custom filter method through `filterSearch:(input, record) => boolean`. + + + + + diff --git a/components/table/demo/index.vue b/components/table/demo/index.vue index 5b9dec8057..8c7efd643e 100644 --- a/components/table/demo/index.vue +++ b/components/table/demo/index.vue @@ -18,6 +18,7 @@ + @@ -61,6 +62,7 @@ import Summary from './summary.vue'; import Sticky from './sticky.vue'; import ResizableColumn from './resizable-column.vue'; import Responsive from './responsive.vue'; +import filterSearchVue from './filter-search.vue'; import CN from '../index.zh-CN.md'; import US from '../index.en-US.md'; import { defineComponent } from 'vue'; @@ -69,6 +71,7 @@ export default defineComponent({ CN, US, components: { + filterSearchVue, Basic, Ellipsis, Ajax, diff --git a/components/table/hooks/useFilter/FilterDropdown.tsx b/components/table/hooks/useFilter/FilterDropdown.tsx index dac924de64..b9cd29d1eb 100644 --- a/components/table/hooks/useFilter/FilterDropdown.tsx +++ b/components/table/hooks/useFilter/FilterDropdown.tsx @@ -108,11 +108,12 @@ export interface FilterDropdownProps { filterState?: FilterState; filterMultiple: boolean; filterMode?: 'menu' | 'tree'; - filterSearch?: boolean; + filterSearch?: FilterSearchType; columnKey: Key; triggerFilter: (filterState: FilterState) => void; locale: TableLocale; getPopupContainer?: GetPopupContainer; + filterResetToDefaultFilteredValue?: boolean; } export default defineComponent>({ @@ -266,7 +267,11 @@ export default defineComponent>({ triggerVisible(false); } searchValue.value = ''; - filteredKeys.value = []; + if (props.column.filterResetToDefaultFilteredValue) { + filteredKeys.value = (props.column.defaultFilteredValue || []).map(key => String(key)); + } else { + filteredKeys.value = []; + } }; const doFilter = ({ closeDropdown } = { closeDropdown: true }) => { @@ -432,7 +437,17 @@ export default defineComponent>({ ); }; + const resetDisabled = computed(() => { + const selectedKeys = filteredKeys.value; + if (props.column.filterResetToDefaultFilteredValue) { + return isEqual( + (props.column.defaultFilteredValue || []).map(key => String(key)), + selectedKeys, + ); + } + return selectedKeys.length === 0; + }); return () => { const { tablePrefixCls, prefixCls, column, dropdownPrefixCls, locale, getPopupContainer } = props; @@ -453,7 +468,6 @@ export default defineComponent>({ } else if (filterDropdownRef.value) { dropdownContent = filterDropdownRef.value; } else { - const selectedKeys = filteredKeys.value as any; dropdownContent = ( <> {getFilterComponent()} @@ -461,7 +475,7 @@ export default defineComponent>({
    +
    @@ -62,12 +62,41 @@ exports[`renders ./components/auto-complete/demo/options.vue correctly 1`] = ` `; +exports[`renders ./components/auto-complete/demo/status.vue correctly 1`] = ` +
    +
    + +
    + +
    + +
    + +
    +`; + exports[`renders ./components/auto-complete/demo/uncertain-category.vue correctly 1`] = `
    @@ -16,7 +17,8 @@ exports[`renders ./components/calendar/demo/basic.vue correctly 1`] = `
    Nov -
    +
    @@ -404,7 +406,8 @@ exports[`renders ./components/calendar/demo/card.vue correctly 1`] = `
    2016 -
    +
    @@ -412,7 +415,8 @@ exports[`renders ./components/calendar/demo/card.vue correctly 1`] = `
    Nov -
    +
    @@ -797,27 +801,29 @@ exports[`renders ./components/calendar/demo/customize-header.vue correctly 1`] =
    Custom header
    -
    -
    +
    +
    -
    +
    2016 -
    +
    -
    +
    Nov -
    +
    @@ -1206,7 +1212,8 @@ exports[`renders ./components/calendar/demo/notice-calendar.vue correctly 1`] =
    2016 -
    +
    @@ -1214,7 +1221,8 @@ exports[`renders ./components/calendar/demo/notice-calendar.vue correctly 1`] =
    Nov -
    +
    @@ -1630,7 +1638,8 @@ exports[`renders ./components/calendar/demo/select.vue correctly 1`] = `
    2017 -
    +
    @@ -1638,7 +1647,8 @@ exports[`renders ./components/calendar/demo/select.vue correctly 1`] = `
    Jan -
    +
    diff --git a/components/calendar/__tests__/__snapshots__/index.test.js.snap b/components/calendar/__tests__/__snapshots__/index.test.js.snap index 03a4dbfb2c..8c097fc2d1 100644 --- a/components/calendar/__tests__/__snapshots__/index.test.js.snap +++ b/components/calendar/__tests__/__snapshots__/index.test.js.snap @@ -8,7 +8,8 @@ exports[`Calendar Calendar should support locale 1`] = `
    2018年 -
    +
    @@ -16,7 +17,8 @@ exports[`Calendar Calendar should support locale 1`] = `
    Oct -
    +
    diff --git a/components/card/__tests__/__snapshots__/demo.test.js.snap b/components/card/__tests__/__snapshots__/demo.test.js.snap index 89623db93d..1ae40a182c 100644 --- a/components/card/__tests__/__snapshots__/demo.test.js.snap +++ b/components/card/__tests__/__snapshots__/demo.test.js.snap @@ -100,8 +100,8 @@ exports[`renders ./components/card/demo/grid-card.vue correctly 1`] = ` exports[`renders ./components/card/demo/in-column.vue correctly 1`] = `
    -
    -
    +
    +
    @@ -117,7 +117,7 @@ exports[`renders ./components/card/demo/in-column.vue correctly 1`] = `
    -
    +
    @@ -133,7 +133,7 @@ exports[`renders ./components/card/demo/in-column.vue correctly 1`] = `
    -
    +
    @@ -206,43 +206,43 @@ exports[`renders ./components/card/demo/loading.vue correctly 1`] = `
    -
    -
    +
    +
    -
    -
    +
    +
    -
    +
    -
    -
    +
    +
    -
    +
    -
    -
    +
    +
    -
    +
    -
    -
    +
    +
    -
    +
    -
    +
    diff --git a/components/card/__tests__/__snapshots__/index.test.js.snap b/components/card/__tests__/__snapshots__/index.test.js.snap index 5c0c2a3007..7a41dfbe2e 100644 --- a/components/card/__tests__/__snapshots__/index.test.js.snap +++ b/components/card/__tests__/__snapshots__/index.test.js.snap @@ -6,43 +6,43 @@ exports[`Card should still have padding when card which set padding to 0 is load
    -
    -
    +
    +
    -
    -
    +
    +
    -
    +
    -
    -
    +
    +
    -
    +
    -
    -
    +
    +
    -
    +
    -
    -
    +
    +
    -
    +
    -
    +
    diff --git a/components/cascader/__tests__/__snapshots__/demo.test.js.snap b/components/cascader/__tests__/__snapshots__/demo.test.js.snap index 67102bc6be..cc86cd0d30 100644 --- a/components/cascader/__tests__/__snapshots__/demo.test.js.snap +++ b/components/cascader/__tests__/__snapshots__/demo.test.js.snap @@ -6,7 +6,8 @@ exports[`renders ./components/cascader/demo/basic.vue correctly 1`] = `
    Please select -
    +
    `; @@ -17,7 +18,8 @@ exports[`renders ./components/cascader/demo/change-on-select.vue correctly 1`] =
    Please select -
    +
    `; @@ -28,7 +30,8 @@ exports[`renders ./components/cascader/demo/custom-render.vue correctly 1`] = `
    Zhejiang /Hangzhou /West Lake ( 752100 ) -
    +
    `; @@ -40,7 +43,8 @@ exports[`renders ./components/cascader/demo/disabled-option.vue correctly 1`] =
    Please select -
    +
    `; @@ -51,7 +55,8 @@ exports[`renders ./components/cascader/demo/fields-name.vue correctly 1`] = `
    Please select -
    +
    `; @@ -62,7 +67,8 @@ exports[`renders ./components/cascader/demo/hover.vue correctly 1`] = `
    Please select -
    +
    `; @@ -73,25 +79,57 @@ exports[`renders ./components/cascader/demo/lazy.vue correctly 1`] = `
    Please select -
    +
    `; exports[`renders ./components/cascader/demo/multiple.vue correctly 1`] = ` -
    - +
    +
    +

    Cascader.SHOW_PARENT

    +
    -
    -
    +
    +
    -
    - + +
    +
    + +
    + +
    + +
    Please select
    -
    Please select + +
    +
    + +
    +

    Cascader.SHOW_CHILD

    +
    +
    + + +
    +
    + +
    + +
    + +
    Please select +
    + + +
    +
    `; @@ -102,7 +140,8 @@ exports[`renders ./components/cascader/demo/search.vue correctly 1`] = `
    Please select -
    +
    `; @@ -113,7 +152,8 @@ exports[`renders ./components/cascader/demo/size.vue correctly 1`] = `
    Please select -
    +

    @@ -123,7 +163,8 @@ exports[`renders ./components/cascader/demo/size.vue correctly 1`] = `
    Please select -
    +

    @@ -133,7 +174,8 @@ exports[`renders ./components/cascader/demo/size.vue correctly 1`] = `
    Please select -
    +

    @@ -148,7 +190,8 @@ exports[`renders ./components/cascader/demo/suffix.vue correctly 1`] = `
    Please select -
    +
    @@ -159,7 +202,7 @@ exports[`renders ./components/cascader/demo/suffix.vue correctly 1`] = `
    Please select -
    +
    diff --git a/components/checkbox/__tests__/__snapshots__/demo.test.js.snap b/components/checkbox/__tests__/__snapshots__/demo.test.js.snap index 01ce052f82..04205005c7 100644 --- a/components/checkbox/__tests__/__snapshots__/demo.test.js.snap +++ b/components/checkbox/__tests__/__snapshots__/demo.test.js.snap @@ -44,12 +44,12 @@ exports[`renders ./components/checkbox/demo/group.vue correctly 1`] = ` exports[`renders ./components/checkbox/demo/layout.vue correctly 1`] = `
    -
    -
    -
    -
    -
    -
    +
    +
    +
    +
    +
    +
    `; diff --git a/components/collapse/__tests__/__snapshots__/demo.test.js.snap b/components/collapse/__tests__/__snapshots__/demo.test.js.snap index b07633ada3..580c109f35 100644 --- a/components/collapse/__tests__/__snapshots__/demo.test.js.snap +++ b/components/collapse/__tests__/__snapshots__/demo.test.js.snap @@ -148,7 +148,8 @@ exports[`renders ./components/collapse/demo/extra.vue correctly 1`] = `
    left -
    +
    `; diff --git a/components/comment/__tests__/__snapshots__/demo.test.js.snap b/components/comment/__tests__/__snapshots__/demo.test.js.snap index 002c755860..12e2e15b6e 100644 --- a/components/comment/__tests__/__snapshots__/demo.test.js.snap +++ b/components/comment/__tests__/__snapshots__/demo.test.js.snap @@ -31,25 +31,23 @@ exports[`renders ./components/comment/demo/editor.vue correctly 1`] = `
    -
    +
    -
    +
    -
    -
    +
    -
    +
    -
    diff --git a/components/comment/__tests__/__snapshots__/index.test.js.snap b/components/comment/__tests__/__snapshots__/index.test.js.snap index 99acaabacf..5a74f25d9e 100644 --- a/components/comment/__tests__/__snapshots__/index.test.js.snap +++ b/components/comment/__tests__/__snapshots__/index.test.js.snap @@ -42,25 +42,23 @@ exports[`Comment Comment can be used as editor, user can customize the editor co
    -
    +
    -
    +
    -
    -
    +
    -
    +
    -
    diff --git a/components/date-picker/__tests__/__snapshots__/DatePicker.test.js.snap b/components/date-picker/__tests__/__snapshots__/DatePicker.test.js.snap index 6c3b99d1f6..7ea2d4a8c0 100644 --- a/components/date-picker/__tests__/__snapshots__/DatePicker.test.js.snap +++ b/components/date-picker/__tests__/__snapshots__/DatePicker.test.js.snap @@ -2,7 +2,9 @@ exports[`DatePicker prop locale should works 1`] = `
    -
    +
    + +