diff --git a/components/date-picker/demo/status.vue b/components/date-picker/demo/status.vue index 2ac68fb72..9e8e43265 100644 --- a/components/date-picker/demo/status.vue +++ b/components/date-picker/demo/status.vue @@ -1,7 +1,7 @@ --- order: 19 -version: 4.19.0 +version: 3.3.0 title: zh-CN: 自定义状态 en-US: Status diff --git a/components/select/demo/index.vue b/components/select/demo/index.vue index 43806fc59..6bb7e86d7 100644 --- a/components/select/demo/index.vue +++ b/components/select/demo/index.vue @@ -18,6 +18,8 @@ + + diff --git a/components/select/demo/status.vue b/components/select/demo/status.vue new file mode 100644 index 000000000..ca69965cf --- /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 ca27c7386..bc2f55b77 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 4275c5413..ad567fd34 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 3d9da7c3b..7329a8846 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 c4007cdef..474101f26 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 a914d0b4b..98037eecc 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 65bdc4ae0..e9f2fc2fe 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 000000000..a746a04f6 --- /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 0f4a9541d..e03f32f7b 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(); }; }