Skip to content

Commit 6d2bcf0

Browse files
committed
feat: cascader add clearIcon & removeIcon slot
1 parent f1f6085 commit 6d2bcf0

14 files changed

+170
-72
lines changed

components/_util/transition.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ import {
1313
Transition as T,
1414
TransitionGroup as TG,
1515
} from 'vue';
16+
import { tuple } from './type';
17+
18+
const SelectPlacements = tuple('bottomLeft', 'bottomRight', 'topLeft', 'topRight');
19+
export type SelectCommonPlacement = typeof SelectPlacements[number];
20+
21+
const getTransitionDirection = (placement: SelectCommonPlacement | undefined) => {
22+
if (placement !== undefined && (placement === 'topLeft' || placement === 'topRight')) {
23+
return `slide-down`;
24+
}
25+
return `slide-up`;
26+
};
1627

1728
export const getTransitionProps = (transitionName: string, opt: TransitionProps = {}) => {
1829
if (process.env.NODE_ENV === 'test') {
@@ -176,6 +187,6 @@ const getTransitionName = (rootPrefixCls: string, motion: string, transitionName
176187
return `${rootPrefixCls}-${motion}`;
177188
};
178189

179-
export { Transition, TransitionGroup, collapseMotion, getTransitionName };
190+
export { Transition, TransitionGroup, collapseMotion, getTransitionName, getTransitionDirection };
180191

181192
export default Transition;

components/cascader/demo/suffix.vue

+17-15
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,23 @@ Custom suffix icon
1616

1717
</docs>
1818
<template>
19-
<a-cascader
20-
v-model:value="value1"
21-
style="margin-top: 1rem"
22-
:options="options"
23-
placeholder="Please select"
24-
>
25-
<template #suffixIcon><smile-outlined class="test" /></template>
26-
</a-cascader>
27-
<a-cascader
28-
v-model:value="value2"
29-
suffix-icon="ab"
30-
style="margin-top: 1rem"
31-
:options="options"
32-
placeholder="Please select"
33-
/>
19+
<a-space>
20+
<a-cascader
21+
v-model:value="value1"
22+
style="margin-top: 1rem"
23+
:options="options"
24+
placeholder="Please select"
25+
>
26+
<template #suffixIcon><smile-outlined class="test" /></template>
27+
</a-cascader>
28+
<a-cascader
29+
v-model:value="value2"
30+
suffix-icon="ab"
31+
style="margin-top: 1rem"
32+
:options="options"
33+
placeholder="Please select"
34+
/>
35+
</a-space>
3436
</template>
3537
<script lang="ts">
3638
import { SmileOutlined } from '@ant-design/icons-vue';

components/cascader/index.en-US.md

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Cascade selection box.
2323
| --- | --- | --- | --- | --- |
2424
| allowClear | whether allow clear | boolean | true | |
2525
| autofocus | get focus when component mounted | boolean | false | |
26+
| bordered | Whether has border style | boolean | true | 3.2 |
27+
| clearIcon | The custom clear icon | slot | - | 3.2 |
2628
| changeOnSelect | (Work on single select) change value on each selection if set to true, see above demo for details | boolean | false | |
2729
| disabled | whether disabled select | boolean | false | |
2830
| displayRender | render function of displaying selected options, you can use #displayRender="{labels, selectedOptions}". | `({labels, selectedOptions}) => VNode` | `labels => labels.join(' / ')` | |
@@ -41,6 +43,7 @@ Cascade selection box.
4143
| options | data options of cascade | [Option](#option)\[] | - | |
4244
| placeholder | input placeholder | string | 'Please select' | |
4345
| placement | Use preset popup align config from builtinPlacements | `bottomLeft` \| `bottomRight` \| `topLeft` \| `topRight` | `bottomLeft` | 3.0 |
46+
| removeIcon | The custom remove icon | slot | - | 3.2 |
4447
| searchValue | Set search value,Need work with `showSearch` | string | - | 3.0 |
4548
| showSearch | Whether show search input in single mode. | boolean \| [object](#showsearch) | false | |
4649
| size | input size | `large` \| `default` \| `small` | `default` | |

components/cascader/index.tsx

+22-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import useConfigInject from '../_util/hooks/useConfigInject';
1515
import classNames from '../_util/classNames';
1616
import type { SizeType } from '../config-provider';
1717
import devWarning from '../vc-util/devWarning';
18-
import { getTransitionName } from '../_util/transition';
18+
import type { SelectCommonPlacement } from '../_util/transition';
19+
import { getTransitionDirection, getTransitionName } from '../_util/transition';
1920
import { useInjectFormItemContext } from '../form';
2021
import type { ValueType } from '../vc-cascader/Cascader';
2122

@@ -96,7 +97,7 @@ export function cascaderProps<DataNodeType extends CascaderOptionType = Cascader
9697
multiple: { type: Boolean, default: undefined },
9798
size: String as PropType<SizeType>,
9899
bordered: { type: Boolean, default: undefined },
99-
100+
placement: { type: String as PropType<SelectCommonPlacement> },
100101
suffixIcon: PropTypes.any,
101102
options: Array as PropType<DataNodeType[]>,
102103
'onUpdate:value': Function as PropType<(value: ValueType) => void>,
@@ -191,7 +192,17 @@ const Cascader = defineComponent({
191192
emit('blur', ...args);
192193
formItemContext.onFieldBlur();
193194
};
194-
195+
const mergedShowArrow = computed(() =>
196+
props.showArrow !== undefined ? props.showArrow : props.loading || !props.multiple,
197+
);
198+
const placement = computed(() => {
199+
if (props.placement !== undefined) {
200+
return props.placement;
201+
}
202+
return direction.value === 'rtl'
203+
? ('bottomRight' as SelectCommonPlacement)
204+
: ('bottomLeft' as SelectCommonPlacement);
205+
});
195206
return () => {
196207
const {
197208
notFoundContent = slots.notFoundContent?.(),
@@ -225,6 +236,7 @@ const Cascader = defineComponent({
225236
...props,
226237
multiple,
227238
prefixCls: prefixCls.value,
239+
showArrow: mergedShowArrow.value,
228240
},
229241
slots,
230242
);
@@ -245,6 +257,7 @@ const Cascader = defineComponent({
245257
attrs.class,
246258
]}
247259
direction={direction.value}
260+
placement={placement.value}
248261
notFoundContent={mergedNotFoundContent}
249262
allowClear={allowClear}
250263
showSearch={mergedShowSearch.value}
@@ -257,14 +270,19 @@ const Cascader = defineComponent({
257270
dropdownClassName={mergedDropdownClassName.value}
258271
dropdownPrefixCls={cascaderPrefixCls.value}
259272
choiceTransitionName={getTransitionName(rootPrefixCls.value, '', choiceTransitionName)}
260-
transitionName={getTransitionName(rootPrefixCls.value, 'slide-up', transitionName)}
273+
transitionName={getTransitionName(
274+
rootPrefixCls.value,
275+
getTransitionDirection(placement.value),
276+
transitionName,
277+
)}
261278
getPopupContainer={getPopupContainer.value}
262279
customSlots={{
263280
...slots,
264281
checkable: () => <span class={`${cascaderPrefixCls.value}-checkbox-inner`} />,
265282
}}
266283
displayRender={props.displayRender || slots.displayRender}
267284
maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder}
285+
showArrow={props.showArrow}
268286
onChange={handleChange}
269287
onBlur={handleBlur}
270288
v-slots={slots}

components/cascader/index.zh-CN.md

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
2424
| --- | --- | --- | --- | --- |
2525
| allowClear | 是否支持清除 | boolean | true | |
2626
| autofocus | 自动获取焦点 | boolean | false | |
27+
| bordered | 是否有边框 | boolean | true | 3.2 |
28+
| clearIcon | 自定义的选择框清空图标 | slot | - | 3.2 |
2729
| changeOnSelect | (单选时生效)当此项为 true 时,点选每级菜单选项值都会发生变化,具体见上面的演示 | boolean | false | |
2830
| defaultValue | 默认的选中项 | string\[] \| number\[] | \[] | |
2931
| disabled | 禁用 | boolean | false | |
@@ -43,6 +45,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
4345
| options | 可选项数据源 | [Option](#option)\[] | - | |
4446
| placeholder | 输入框占位文本 | string | '请选择' | |
4547
| placement | 浮层预设位置 | `bottomLeft` \| `bottomRight` \| `topLeft` \| `topRight` | `bottomLeft` | 3.0 |
48+
| removeIcon | 自定义的多选框清除图标 | slot | - | 3.2 |
4649
| searchValue | 设置搜索的值,需要与 `showSearch` 配合使用 | string | - | 3.0 |
4750
| showSearch | 在选择框中显示搜索框 | boolean \| [object](#showsearch) | false | |
4851
| size | 输入框大小 | `large` \| `default` \| `small` | `default` | |

components/vc-cascader/Cascader.tsx

+22-14
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import PropTypes from '../_util/vue-types';
99
import { initDefaultProps } from '../_util/props-util';
1010
import useId from '../vc-select/hooks/useId';
1111
import useMergedState from '../_util/hooks/useMergedState';
12-
import { fillFieldNames, toPathKey, toPathKeys } from './utils/commonUtil';
12+
import { fillFieldNames, toPathKey, toPathKeys, SHOW_PARENT, SHOW_CHILD } from './utils/commonUtil';
1313
import useEntities from './hooks/useEntities';
1414
import useSearchConfig from './hooks/useSearchConfig';
1515
import useSearchOptions from './hooks/useSearchOptions';
@@ -23,6 +23,7 @@ import { BaseSelect } from '../vc-select';
2323
import devWarning from '../vc-util/devWarning';
2424
import useMaxLevel from '../vc-tree/useMaxLevel';
2525

26+
export { SHOW_PARENT, SHOW_CHILD };
2627
export interface ShowSearchType<OptionType extends BaseOptionType = DefaultOptionType> {
2728
filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean;
2829
render?: (arg?: {
@@ -49,6 +50,7 @@ export interface InternalFieldNames extends Required<FieldNames> {
4950
export type SingleValueType = (string | number)[];
5051

5152
export type ValueType = SingleValueType | SingleValueType[];
53+
export type ShowCheckedStrategy = typeof SHOW_PARENT | typeof SHOW_CHILD;
5254

5355
export interface BaseOptionType {
5456
disabled?: boolean;
@@ -73,14 +75,11 @@ function baseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType
7375
value: { type: [String, Number, Array] as PropType<ValueType> },
7476
defaultValue: { type: [String, Number, Array] as PropType<ValueType> },
7577
changeOnSelect: { type: Boolean, default: undefined },
76-
onChange: Function as PropType<
77-
(value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void
78-
>,
7978
displayRender: Function as PropType<
8079
(opt: { labels: string[]; selectedOptions?: OptionType[] }) => any
8180
>,
8281
checkable: { type: Boolean, default: undefined },
83-
82+
showCheckedStrategy: { type: String as PropType<ShowCheckedStrategy>, default: SHOW_PARENT },
8483
// Search
8584
showSearch: {
8685
type: [Boolean, Object] as PropType<boolean | ShowSearchType<OptionType>>,
@@ -184,7 +183,7 @@ function toRawValues(value: ValueType): SingleValueType[] {
184183
return value;
185184
}
186185

187-
return value.length === 0 ? [] : [value];
186+
return (value.length === 0 ? [] : [value]).map(val => (Array.isArray(val) ? val : [val]));
188187
}
189188

190189
export default defineComponent({
@@ -215,10 +214,10 @@ export default defineComponent({
215214

216215
/** Convert path key back to value format */
217216
const getValueByKeyPath = (pathKeys: Key[]): SingleValueType[] => {
218-
const ketPathEntities = pathKeyEntities.value;
217+
const keyPathEntities = pathKeyEntities.value;
219218

220219
return pathKeys.map(pathKey => {
221-
const { nodes } = ketPathEntities[pathKey];
220+
const { nodes } = keyPathEntities[pathKey];
222221

223222
return nodes.map(node => node[mergedFieldNames.value.value]);
224223
});
@@ -275,12 +274,12 @@ export default defineComponent({
275274
}
276275

277276
const keyPathValues = toPathKeys(existValues);
278-
const ketPathEntities = pathKeyEntities.value;
277+
const keyPathEntities = pathKeyEntities.value;
279278

280279
const { checkedKeys, halfCheckedKeys } = conductCheck(
281280
keyPathValues,
282281
true,
283-
ketPathEntities,
282+
keyPathEntities,
284283
maxLevel.value,
285284
levelEntities.value,
286285
);
@@ -295,7 +294,11 @@ export default defineComponent({
295294

296295
const deDuplicatedValues = computed(() => {
297296
const checkedKeys = toPathKeys(checkedValues.value);
298-
const deduplicateKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value);
297+
const deduplicateKeys = formatStrategyValues(
298+
checkedKeys,
299+
pathKeyEntities.value,
300+
props.showCheckedStrategy,
301+
);
299302
return [...missingCheckedValues.value, ...getValueByKeyPath(deduplicateKeys)];
300303
});
301304

@@ -330,6 +333,7 @@ export default defineComponent({
330333

331334
// =========================== Select ===========================
332335
const onInternalSelect = (valuePath: SingleValueType) => {
336+
setSearchValue('');
333337
if (!multiple.value) {
334338
triggerChange(valuePath);
335339
} else {
@@ -379,7 +383,11 @@ export default defineComponent({
379383
}
380384

381385
// Roll up to parent level keys
382-
const deDuplicatedKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value);
386+
const deDuplicatedKeys = formatStrategyValues(
387+
checkedKeys,
388+
pathKeyEntities.value,
389+
props.showCheckedStrategy,
390+
);
383391
nextCheckedValues = getValueByKeyPath(deDuplicatedKeys);
384392
}
385393

@@ -537,7 +545,7 @@ export default defineComponent({
537545
return () => {
538546
const emptyOptions = !(mergedSearchValue.value ? searchOptions.value : mergedOptions.value)
539547
.length;
540-
548+
const { dropdownMatchSelectWidth = false } = props;
541549
const dropdownStyle: CSSProperties =
542550
// Search to match width
543551
(mergedSearchValue.value && mergedSearchConfig.value.matchInputWidth) ||
@@ -555,7 +563,7 @@ export default defineComponent({
555563
ref={selectRef}
556564
id={mergedId}
557565
prefixCls={props.prefixCls}
558-
dropdownMatchSelectWidth={false}
566+
dropdownMatchSelectWidth={dropdownMatchSelectWidth}
559567
dropdownStyle={{ ...mergedDropdownStyle.value, ...dropdownStyle }}
560568
// Value
561569
displayValues={displayValues.value}

components/vc-cascader/OptionList/Column.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { DefaultOptionType, SingleValueType } from '../Cascader';
44
import { SEARCH_MARK } from '../hooks/useSearchOptions';
55
import type { Key } from '../../_util/type';
66
import { useInjectCascader } from '../context';
7-
7+
export const FIX_LABEL = '__cascader_fix_label__';
88
export interface ColumnProps {
99
prefixCls: string;
1010
multiple?: boolean;
@@ -58,7 +58,7 @@ export default function Column({
5858
{options.map(option => {
5959
const { disabled } = option;
6060
const searchOptions = option[SEARCH_MARK];
61-
const label = option[fieldNames.value.label];
61+
const label = option[FIX_LABEL] ?? option[fieldNames.value.label];
6262
const value = option[fieldNames.value.value];
6363

6464
const isMergedLeaf = isLeaf(option, fieldNames.value);
@@ -132,6 +132,10 @@ export default function Column({
132132
triggerOpenPath();
133133
}
134134
}}
135+
onMousedown={e => {
136+
// Prevent selector from blurring
137+
e.preventDefault();
138+
}}
135139
>
136140
{multiple && (
137141
<Checkbox
@@ -145,7 +149,7 @@ export default function Column({
145149
}}
146150
/>
147151
)}
148-
<div class={`${menuItemPrefixCls}-content`}>{option[fieldNames.value.label]}</div>
152+
<div class={`${menuItemPrefixCls}-content`}>{label}</div>
149153
{!isLoading && expandIcon && !isMergedLeaf && (
150154
<div class={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
151155
)}

0 commit comments

Comments
 (0)