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

View File

@ -3,7 +3,7 @@ category: Components
type: 数据录入 type: 数据录入
title: Cascader title: Cascader
subtitle: 级联选择 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\[] | \[] | | | defaultValue | 默认的选中项 | string\[] \| number\[] | \[] | |
| disabled | 禁用 | boolean | false | | | disabled | 禁用 | boolean | false | |
| displayRender | 选择后展示的渲染函数,可使用 #displayRender="{labels, selectedOptions}" | `({labels, selectedOptions}) => VNode` | `labels => labels.join(' / ')` | | | displayRender | 选择后展示的渲染函数,可使用 #displayRender="{labels, selectedOptions}" | `({labels, selectedOptions}) => VNode` | `labels => labels.join(' / ')` | |
| dropdownClassName | 自定义浮层类名 | string | - | 3.0 | | popupClassName | 自定义浮层类名 | string | - | 4.0 |
| dropdownStyle | 自定义浮层样式 | CSSProperties | {} | 3.0 | | dropdownStyle | 自定义浮层样式 | CSSProperties | {} | 3.0 |
| expandIcon | 自定义次级菜单展开图标 | slot | - | 3.0 | | expandIcon | 自定义次级菜单展开图标 | slot | - | 3.0 |
| expandTrigger | 次级菜单的展开方式 | `click` \| `hover` | 'click' | | | 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 './switch/style';
import './auto-complete/style'; import './auto-complete/style';
// import './affix/style'; // import './affix/style';
import './cascader/style'; // import './cascader/style';
// import './back-top/style'; // import './back-top/style';
// import './modal/style'; // import './modal/style';
// import './alert/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 CalendarComponentToken } from '../../calendar/style';
import type { ComponentToken as CardComponentToken } from '../../card/style'; import type { ComponentToken as CardComponentToken } from '../../card/style';
import type { ComponentToken as CarouselComponentToken } from '../../carousel/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 CheckboxComponentToken } from '../../checkbox/style';
import type { ComponentToken as CollapseComponentToken } from '../../collapse/style'; import type { ComponentToken as CollapseComponentToken } from '../../collapse/style';
import type { ComponentToken as DatePickerComponentToken } from '../../date-picker/style'; import type { ComponentToken as DatePickerComponentToken } from '../../date-picker/style';
@ -62,7 +62,7 @@ export interface ComponentTokenMap {
Breadcrumb?: {}; Breadcrumb?: {};
Card?: CardComponentToken; Card?: CardComponentToken;
Carousel?: CarouselComponentToken; Carousel?: CarouselComponentToken;
// Cascader?: CascaderComponentToken; Cascader?: CascaderComponentToken;
Checkbox?: CheckboxComponentToken; Checkbox?: CheckboxComponentToken;
Collapse?: CollapseComponentToken; Collapse?: CollapseComponentToken;
Comment?: {}; Comment?: {};

View File

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