refactor: cascader

This commit is contained in:
tangjinzhou 2023-02-20 14:08:09 +08:00
parent 3741931363
commit 62dda88ea0
10 changed files with 262 additions and 198 deletions

View File

@ -2,7 +2,7 @@
category: Components
type: Data Entry
title: Cascader
cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*tokLTp73TsQAAAAAAAAAAAAADrJ8AQ/original
---
Cascade selection box.
@ -28,7 +28,7 @@ Cascade selection box.
| changeOnSelect | (Work on single select) change value on each selection if set to true, see above demo for details | boolean | false | |
| disabled | whether disabled select | boolean | false | |
| displayRender | render function of displaying selected options, you can use #displayRender="{labels, selectedOptions}". | `({labels, selectedOptions}) => VNode` | `labels => labels.join(' / ')` | |
| dropdownClassName | additional className of popup overlay | string | - | 3.0 |
| popupClassName | additional className of popup overlay | string | - | 4.0 |
| dropdownStyle | additional style of popup overlay | CSSProperties | {} | 3.0 |
| expandIcon | Customize the current item expand icon | slot | - | 3.0 |
| expandTrigger | expand current item when click or hover | `click` \| `hover` | 'click' | |

View File

@ -26,7 +26,11 @@ import type { ValueType } from '../vc-cascader/Cascader';
import type { InputStatus } from '../_util/statusUtils';
import { getStatusClassNames, getMergedStatus } from '../_util/statusUtils';
import { FormItemInputContext } from '../form/FormItemContext';
import { useCompactItemContext } from '../space/Compact';
import useSelectStyle from '../select/style';
import useStyle from './style';
import { useInjectDisabled } from '../config-provider/DisabledContext';
// Align the design since we use `rc-select` in root. This help:
// - List search content will show all content
// - Hover opacity style
@ -38,7 +42,7 @@ export type FieldNamesType = FieldNames;
export type FilledFieldNamesType = Required<FieldNamesType>;
function highlightKeyword(str: string, lowerKeyword: string, prefixCls: string | undefined) {
function highlightKeyword(str: string, lowerKeyword: string, prefixCls?: string) {
const cells = str
.toLowerCase()
.split(lowerKeyword)
@ -108,6 +112,9 @@ export function cascaderProps<DataNodeType extends CascaderOptionType = Cascader
suffixIcon: PropTypes.any,
status: String as PropType<InputStatus>,
options: Array as PropType<DataNodeType[]>,
popupClassName: String,
/** @deprecated Please use `popupClassName` instead */
dropdownClassName: String,
'onUpdate:value': Function as PropType<(value: ValueType) => void>,
};
}
@ -129,6 +136,14 @@ const Cascader = defineComponent({
allowClear: true,
}),
setup(props, { attrs, expose, slots, emit }) {
// ====================== Warning ======================
if (process.env.NODE_ENV !== 'production') {
devWarning(
!props.dropdownClassName,
'Cascader',
'`dropdownClassName` is deprecated. Please use `popupClassName` instead.',
);
}
const formItemContext = useInjectFormItemContext();
const formItemInputContext = FormItemInputContext.useInject();
const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status));
@ -139,9 +154,18 @@ const Cascader = defineComponent({
direction,
getPopupContainer,
renderEmpty,
size,
size: contextSize,
disabled,
} = useConfigInject('cascader', props);
const prefixCls = computed(() => getPrefixCls('select', props.prefixCls));
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
const mergedSize = computed(() => compactSize.value || contextSize.value);
const contextDisabled = useInjectDisabled();
const mergedDisabled = computed(() => disabled.value ?? contextDisabled.value);
const [wrapSelectSSR, hashId] = useSelectStyle(prefixCls);
const [wrapCascaderSSR] = useStyle(cascaderPrefixCls);
const isRtl = computed(() => direction.value === 'rtl');
// =================== Warning =====================
if (process.env.NODE_ENV !== 'production') {
@ -176,11 +200,12 @@ const Cascader = defineComponent({
// =================== Dropdown ====================
const mergedDropdownClassName = computed(() =>
classNames(
props.dropdownClassName || props.popupClassName,
props.popupClassName || props.dropdownClassName,
`${cascaderPrefixCls.value}-dropdown`,
{
[`${cascaderPrefixCls.value}-dropdown-rtl`]: isRtl.value,
},
hashId.value,
),
);
@ -253,61 +278,66 @@ const Cascader = defineComponent({
},
slots,
);
return (
<VcCascader
{...restProps}
{...attrs}
id={id}
prefixCls={prefixCls.value}
class={[
cascaderPrefixCls.value,
{
[`${prefixCls.value}-lg`]: size.value === 'large',
[`${prefixCls.value}-sm`]: size.value === 'small',
[`${prefixCls.value}-rtl`]: isRtl.value,
[`${prefixCls.value}-borderless`]: !bordered,
[`${prefixCls.value}-in-form-item`]: formItemInputContext.isFormItemInput,
},
getStatusClassNames(
prefixCls.value,
mergedStatus.value,
formItemInputContext.hasFeedback,
),
attrs.class,
]}
direction={direction.value}
placement={placement.value}
notFoundContent={mergedNotFoundContent}
allowClear={allowClear}
showSearch={mergedShowSearch.value}
expandIcon={mergedExpandIcon}
inputIcon={suffixIcon}
removeIcon={removeIcon}
clearIcon={clearIcon}
loadingIcon={loadingIcon}
checkable={!!multiple}
dropdownClassName={mergedDropdownClassName.value}
dropdownPrefixCls={cascaderPrefixCls.value}
choiceTransitionName={getTransitionName(rootPrefixCls.value, '', choiceTransitionName)}
transitionName={getTransitionName(
rootPrefixCls.value,
getTransitionDirection(placement.value),
transitionName,
)}
getPopupContainer={getPopupContainer.value}
customSlots={{
...slots,
checkable: () => <span class={`${cascaderPrefixCls.value}-checkbox-inner`} />,
}}
tagRender={props.tagRender || slots.tagRender}
displayRender={props.displayRender || slots.displayRender}
maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder}
showArrow={formItemInputContext.hasFeedback || props.showArrow}
onChange={handleChange}
onBlur={handleBlur}
v-slots={slots}
ref={selectRef}
/>
return wrapCascaderSSR(
wrapSelectSSR(
<VcCascader
{...restProps}
{...attrs}
id={id}
prefixCls={prefixCls.value}
class={[
cascaderPrefixCls.value,
{
[`${prefixCls.value}-lg`]: mergedSize.value === 'large',
[`${prefixCls.value}-sm`]: mergedSize.value === 'small',
[`${prefixCls.value}-rtl`]: isRtl.value,
[`${prefixCls.value}-borderless`]: !bordered,
[`${prefixCls.value}-in-form-item`]: formItemInputContext.isFormItemInput,
},
getStatusClassNames(
prefixCls.value,
mergedStatus.value,
formItemInputContext.hasFeedback,
),
compactItemClassnames.value,
attrs.class,
hashId.value,
]}
disabled={mergedDisabled.value}
direction={direction.value}
placement={placement.value}
notFoundContent={mergedNotFoundContent}
allowClear={allowClear}
showSearch={mergedShowSearch.value}
expandIcon={mergedExpandIcon}
inputIcon={suffixIcon}
removeIcon={removeIcon}
clearIcon={clearIcon}
loadingIcon={loadingIcon}
checkable={!!multiple}
dropdownClassName={mergedDropdownClassName.value}
dropdownPrefixCls={cascaderPrefixCls.value}
choiceTransitionName={getTransitionName(rootPrefixCls.value, '', choiceTransitionName)}
transitionName={getTransitionName(
rootPrefixCls.value,
getTransitionDirection(placement.value),
transitionName,
)}
getPopupContainer={getPopupContainer.value}
customSlots={{
...slots,
checkable: () => <span class={`${cascaderPrefixCls.value}-checkbox-inner`} />,
}}
tagRender={props.tagRender || slots.tagRender}
displayRender={props.displayRender || slots.displayRender}
maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder}
showArrow={formItemInputContext.hasFeedback || props.showArrow}
onChange={handleChange}
onBlur={handleBlur}
v-slots={slots}
ref={selectRef}
/>,
),
);
};
},

View File

@ -3,7 +3,7 @@ category: Components
type: 数据录入
title: Cascader
subtitle: 级联选择
cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*tokLTp73TsQAAAAAAAAAAAAADrJ8AQ/original
---
级联选择框。
@ -30,7 +30,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
| defaultValue | 默认的选中项 | string\[] \| number\[] | \[] | |
| disabled | 禁用 | boolean | false | |
| displayRender | 选择后展示的渲染函数,可使用 #displayRender="{labels, selectedOptions}" | `({labels, selectedOptions}) => VNode` | `labels => labels.join(' / ')` | |
| dropdownClassName | 自定义浮层类名 | string | - | 3.0 |
| popupClassName | 自定义浮层类名 | string | - | 4.0 |
| dropdownStyle | 自定义浮层样式 | CSSProperties | {} | 3.0 |
| expandIcon | 自定义次级菜单展开图标 | slot | - | 3.0 |
| expandTrigger | 次级菜单的展开方式 | `click` \| `hover` | 'click' | |

View File

@ -1,105 +0,0 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../input/style/mixin';
@import '../../checkbox/style/mixin';
@cascader-prefix-cls: ~'@{ant-prefix}-cascader';
.antCheckboxFn(@checkbox-prefix-cls: ~'@{cascader-prefix-cls}-checkbox');
.@{cascader-prefix-cls} {
width: 184px;
&-checkbox {
top: 0;
margin-right: @padding-xs;
}
&-menus {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
&.@{cascader-prefix-cls}-menu-empty {
.@{cascader-prefix-cls}-menu {
width: 100%;
height: auto;
}
}
}
&-menu {
flex-grow: 1;
min-width: 111px;
height: 180px;
margin: 0;
margin: -@dropdown-edge-child-vertical-padding 0;
padding: @cascader-dropdown-edge-child-vertical-padding 0;
overflow: auto;
vertical-align: top;
list-style: none;
border-right: @border-width-base @border-style-base @cascader-menu-border-color-split;
-ms-overflow-style: -ms-autohiding-scrollbar; // https://github.com/ant-design/ant-design/issues/11857
&-item {
display: flex;
flex-wrap: nowrap;
align-items: center;
padding: @cascader-dropdown-vertical-padding @control-padding-horizontal;
overflow: hidden;
line-height: @cascader-dropdown-line-height;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: @item-hover-bg;
}
&-disabled {
color: @disabled-color;
cursor: not-allowed;
&:hover {
background: transparent;
}
}
.@{cascader-prefix-cls}-menu-empty & {
color: @disabled-color;
cursor: default;
pointer-events: none;
}
&-active:not(&-disabled) {
&,
&:hover {
font-weight: @select-item-selected-font-weight;
background-color: @cascader-item-selected-bg;
}
}
&-content {
flex: auto;
}
&-expand &-expand-icon,
&-loading-icon {
margin-left: @padding-xss;
color: @text-color-secondary;
font-size: 10px;
.@{cascader-prefix-cls}-menu-item-disabled& {
color: @disabled-color;
}
}
&-keyword {
color: @highlight-color;
}
}
}
}
@import './rtl';

View File

@ -0,0 +1,165 @@
import { getStyle as getCheckboxStyle } from '../../checkbox/style';
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook } from '../../theme/internal';
import { textEllipsis } from '../../_style';
import { genCompactItemStyle } from '../../_style/compact-item';
export interface ComponentToken {
controlWidth: number;
controlItemWidth: number;
dropdownHeight: number;
}
type CascaderToken = FullToken<'Cascader'>;
// =============================== Base ===============================
const genBaseStyle: GenerateStyle<CascaderToken> = token => {
const { prefixCls, componentCls, antCls } = token;
const cascaderMenuItemCls = `${componentCls}-menu-item`;
const iconCls = `
&${cascaderMenuItemCls}-expand ${cascaderMenuItemCls}-expand-icon,
${cascaderMenuItemCls}-loading-icon
`;
const itemPaddingVertical = Math.round(
(token.controlHeight - token.fontSize * token.lineHeight) / 2,
);
return [
// =====================================================
// == Control ==
// =====================================================
{
[componentCls]: {
width: token.controlWidth,
},
},
// =====================================================
// == Popup ==
// =====================================================
{
[`${componentCls}-dropdown`]: [
// ==================== Checkbox ====================
getCheckboxStyle(`${prefixCls}-checkbox`, token),
{
[`&${antCls}-select-dropdown`]: {
padding: 0,
},
},
{
[componentCls]: {
// ================== Checkbox ==================
'&-checkbox': {
top: 0,
marginInlineEnd: token.paddingXS,
},
// ==================== Menu ====================
// >>> Menus
'&-menus': {
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'flex-start',
[`&${componentCls}-menu-empty`]: {
[`${componentCls}-menu`]: {
width: '100%',
height: 'auto',
[cascaderMenuItemCls]: {
color: token.colorTextDisabled,
},
},
},
},
// >>> Menu
'&-menu': {
flexGrow: 1,
minWidth: token.controlItemWidth,
height: token.dropdownHeight,
margin: 0,
padding: token.paddingXXS,
overflow: 'auto',
verticalAlign: 'top',
listStyle: 'none',
'-ms-overflow-style': '-ms-autohiding-scrollbar', // https://github.com/ant-design/ant-design/issues/11857
'&:not(:last-child)': {
borderInlineEnd: `${token.lineWidth}px ${token.lineType} ${token.colorSplit}`,
},
'&-item': {
...textEllipsis,
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
padding: `${itemPaddingVertical}px ${token.paddingSM}px`,
lineHeight: token.lineHeight,
cursor: 'pointer',
transition: `all ${token.motionDurationMid}`,
borderRadius: token.borderRadiusSM,
'&:hover': {
background: token.controlItemBgHover,
},
'&-disabled': {
color: token.colorTextDisabled,
cursor: 'not-allowed',
'&:hover': {
background: 'transparent',
},
[iconCls]: {
color: token.colorTextDisabled,
},
},
[`&-active:not(${cascaderMenuItemCls}-disabled)`]: {
[`&, &:hover`]: {
fontWeight: token.fontWeightStrong,
backgroundColor: token.controlItemBgActive,
},
},
'&-content': {
flex: 'auto',
},
[iconCls]: {
marginInlineStart: token.paddingXXS,
color: token.colorTextDescription,
fontSize: token.fontSizeIcon,
},
'&-keyword': {
color: token.colorHighlight,
},
},
},
},
},
],
},
// =====================================================
// == RTL ==
// =====================================================
{
[`${componentCls}-dropdown-rtl`]: {
direction: 'rtl',
},
},
// =====================================================
// == Space Compact ==
// =====================================================
genCompactItemStyle(token),
];
};
// ============================== Export ==============================
export default genComponentStyleHook('Cascader', token => [genBaseStyle(token)], {
controlWidth: 184,
controlItemWidth: 111,
dropdownHeight: 180,
});

View File

@ -1,8 +0,0 @@
import '../../style/index.less';
import './index.less';
// style dependencies
import '../../empty/style';
import '../../select/style';
// deps-lint-skip: form

View File

@ -1,19 +0,0 @@
// We can not import reference of `./index` directly since it will make dead loop in less
@import (reference) '../../style/themes/index';
@cascader-prefix-cls: ~'@{ant-prefix}-cascader';
.@{cascader-prefix-cls}-rtl {
.@{cascader-prefix-cls}-menu-item {
&-expand-icon,
&-loading-icon {
margin-right: @padding-xss;
margin-left: 0;
}
}
.@{cascader-prefix-cls}-checkbox {
top: 0;
margin-right: 0;
margin-left: @padding-xs;
}
}

View File

@ -27,7 +27,7 @@ import './radio/style';
// import './switch/style';
import './auto-complete/style';
// import './affix/style';
import './cascader/style';
// import './cascader/style';
// import './back-top/style';
// import './modal/style';
// import './alert/style';

View File

@ -7,7 +7,7 @@ import type { ComponentToken as ButtonComponentToken } from '../../button/style'
import type { ComponentToken as CalendarComponentToken } from '../../calendar/style';
import type { ComponentToken as CardComponentToken } from '../../card/style';
import type { ComponentToken as CarouselComponentToken } from '../../carousel/style';
// import type { ComponentToken as CascaderComponentToken } from '../../cascader/style';
import type { ComponentToken as CascaderComponentToken } from '../../cascader/style';
import type { ComponentToken as CheckboxComponentToken } from '../../checkbox/style';
import type { ComponentToken as CollapseComponentToken } from '../../collapse/style';
import type { ComponentToken as DatePickerComponentToken } from '../../date-picker/style';
@ -62,7 +62,7 @@ export interface ComponentTokenMap {
Breadcrumb?: {};
Card?: CardComponentToken;
Carousel?: CarouselComponentToken;
// Cascader?: CascaderComponentToken;
Cascader?: CascaderComponentToken;
Checkbox?: CheckboxComponentToken;
Collapse?: CollapseComponentToken;
Comment?: {};

View File

@ -5,6 +5,7 @@ import type { DisplayValueType, Placement } from '../vc-select/BaseSelect';
import { baseSelectPropsWithoutPrivate } from '../vc-select/BaseSelect';
import omit from '../_util/omit';
import type { Key, VueNode } from '../_util/type';
import { objectType } from '../_util/type';
import PropTypes from '../_util/vue-types';
import { initDefaultProps } from '../_util/props-util';
import useId from '../vc-select/hooks/useId';
@ -68,7 +69,7 @@ function baseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType
// MISC
id: String,
prefixCls: String,
fieldNames: Object as PropType<FieldNames>,
fieldNames: objectType<FieldNames>(),
children: Array as PropType<VueNode[]>,
// Value