feat: cascader add clearIcon & removeIcon slot

pull/5485/head
tangjinzhou 2022-04-12 14:03:30 +08:00
parent f1f6085dbb
commit 6d2bcf0ab8
14 changed files with 170 additions and 72 deletions

View File

@ -13,6 +13,17 @@ import {
Transition as T, Transition as T,
TransitionGroup as TG, TransitionGroup as TG,
} from 'vue'; } from 'vue';
import { tuple } from './type';
const SelectPlacements = tuple('bottomLeft', 'bottomRight', 'topLeft', 'topRight');
export type SelectCommonPlacement = typeof SelectPlacements[number];
const getTransitionDirection = (placement: SelectCommonPlacement | undefined) => {
if (placement !== undefined && (placement === 'topLeft' || placement === 'topRight')) {
return `slide-down`;
}
return `slide-up`;
};
export const getTransitionProps = (transitionName: string, opt: TransitionProps = {}) => { export const getTransitionProps = (transitionName: string, opt: TransitionProps = {}) => {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
@ -176,6 +187,6 @@ const getTransitionName = (rootPrefixCls: string, motion: string, transitionName
return `${rootPrefixCls}-${motion}`; return `${rootPrefixCls}-${motion}`;
}; };
export { Transition, TransitionGroup, collapseMotion, getTransitionName }; export { Transition, TransitionGroup, collapseMotion, getTransitionName, getTransitionDirection };
export default Transition; export default Transition;

View File

@ -16,21 +16,23 @@ Custom suffix icon
</docs> </docs>
<template> <template>
<a-cascader <a-space>
v-model:value="value1" <a-cascader
style="margin-top: 1rem" v-model:value="value1"
:options="options" style="margin-top: 1rem"
placeholder="Please select" :options="options"
> placeholder="Please select"
<template #suffixIcon><smile-outlined class="test" /></template> >
</a-cascader> <template #suffixIcon><smile-outlined class="test" /></template>
<a-cascader </a-cascader>
v-model:value="value2" <a-cascader
suffix-icon="ab" v-model:value="value2"
style="margin-top: 1rem" suffix-icon="ab"
:options="options" style="margin-top: 1rem"
placeholder="Please select" :options="options"
/> placeholder="Please select"
/>
</a-space>
</template> </template>
<script lang="ts"> <script lang="ts">
import { SmileOutlined } from '@ant-design/icons-vue'; import { SmileOutlined } from '@ant-design/icons-vue';

View File

@ -23,6 +23,8 @@ Cascade selection box.
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| allowClear | whether allow clear | boolean | true | | | allowClear | whether allow clear | boolean | true | |
| autofocus | get focus when component mounted | boolean | false | | | autofocus | get focus when component mounted | boolean | false | |
| bordered | Whether has border style | boolean | true | 3.2 |
| clearIcon | The custom clear icon | slot | - | 3.2 |
| 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(' / ')` | |
@ -41,6 +43,7 @@ Cascade selection box.
| options | data options of cascade | [Option](#option)\[] | - | | | options | data options of cascade | [Option](#option)\[] | - | |
| placeholder | input placeholder | string | 'Please select' | | | placeholder | input placeholder | string | 'Please select' | |
| placement | Use preset popup align config from builtinPlacements | `bottomLeft` \| `bottomRight` \| `topLeft` \| `topRight` | `bottomLeft` | 3.0 | | placement | Use preset popup align config from builtinPlacements | `bottomLeft` \| `bottomRight` \| `topLeft` \| `topRight` | `bottomLeft` | 3.0 |
| removeIcon | The custom remove icon | slot | - | 3.2 |
| searchValue | Set search valueNeed work with `showSearch` | string | - | 3.0 | | searchValue | Set search valueNeed work with `showSearch` | string | - | 3.0 |
| showSearch | Whether show search input in single mode. | boolean \| [object](#showsearch) | false | | | showSearch | Whether show search input in single mode. | boolean \| [object](#showsearch) | false | |
| size | input size | `large` \| `default` \| `small` | `default` | | | size | input size | `large` \| `default` \| `small` | `default` | |

View File

@ -15,7 +15,8 @@ import useConfigInject from '../_util/hooks/useConfigInject';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import type { SizeType } from '../config-provider'; import type { SizeType } from '../config-provider';
import devWarning from '../vc-util/devWarning'; import devWarning from '../vc-util/devWarning';
import { getTransitionName } from '../_util/transition'; import type { SelectCommonPlacement } from '../_util/transition';
import { getTransitionDirection, getTransitionName } from '../_util/transition';
import { useInjectFormItemContext } from '../form'; import { useInjectFormItemContext } from '../form';
import type { ValueType } from '../vc-cascader/Cascader'; import type { ValueType } from '../vc-cascader/Cascader';
@ -96,7 +97,7 @@ export function cascaderProps<DataNodeType extends CascaderOptionType = Cascader
multiple: { type: Boolean, default: undefined }, multiple: { type: Boolean, default: undefined },
size: String as PropType<SizeType>, size: String as PropType<SizeType>,
bordered: { type: Boolean, default: undefined }, bordered: { type: Boolean, default: undefined },
placement: { type: String as PropType<SelectCommonPlacement> },
suffixIcon: PropTypes.any, suffixIcon: PropTypes.any,
options: Array as PropType<DataNodeType[]>, options: Array as PropType<DataNodeType[]>,
'onUpdate:value': Function as PropType<(value: ValueType) => void>, 'onUpdate:value': Function as PropType<(value: ValueType) => void>,
@ -191,7 +192,17 @@ const Cascader = defineComponent({
emit('blur', ...args); emit('blur', ...args);
formItemContext.onFieldBlur(); formItemContext.onFieldBlur();
}; };
const mergedShowArrow = computed(() =>
props.showArrow !== undefined ? props.showArrow : props.loading || !props.multiple,
);
const placement = computed(() => {
if (props.placement !== undefined) {
return props.placement;
}
return direction.value === 'rtl'
? ('bottomRight' as SelectCommonPlacement)
: ('bottomLeft' as SelectCommonPlacement);
});
return () => { return () => {
const { const {
notFoundContent = slots.notFoundContent?.(), notFoundContent = slots.notFoundContent?.(),
@ -225,6 +236,7 @@ const Cascader = defineComponent({
...props, ...props,
multiple, multiple,
prefixCls: prefixCls.value, prefixCls: prefixCls.value,
showArrow: mergedShowArrow.value,
}, },
slots, slots,
); );
@ -245,6 +257,7 @@ const Cascader = defineComponent({
attrs.class, attrs.class,
]} ]}
direction={direction.value} direction={direction.value}
placement={placement.value}
notFoundContent={mergedNotFoundContent} notFoundContent={mergedNotFoundContent}
allowClear={allowClear} allowClear={allowClear}
showSearch={mergedShowSearch.value} showSearch={mergedShowSearch.value}
@ -257,7 +270,11 @@ const Cascader = defineComponent({
dropdownClassName={mergedDropdownClassName.value} dropdownClassName={mergedDropdownClassName.value}
dropdownPrefixCls={cascaderPrefixCls.value} dropdownPrefixCls={cascaderPrefixCls.value}
choiceTransitionName={getTransitionName(rootPrefixCls.value, '', choiceTransitionName)} choiceTransitionName={getTransitionName(rootPrefixCls.value, '', choiceTransitionName)}
transitionName={getTransitionName(rootPrefixCls.value, 'slide-up', transitionName)} transitionName={getTransitionName(
rootPrefixCls.value,
getTransitionDirection(placement.value),
transitionName,
)}
getPopupContainer={getPopupContainer.value} getPopupContainer={getPopupContainer.value}
customSlots={{ customSlots={{
...slots, ...slots,
@ -265,6 +282,7 @@ const Cascader = defineComponent({
}} }}
displayRender={props.displayRender || slots.displayRender} displayRender={props.displayRender || slots.displayRender}
maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder} maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder}
showArrow={props.showArrow}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
v-slots={slots} v-slots={slots}

View File

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

View File

@ -9,7 +9,7 @@ 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';
import useMergedState from '../_util/hooks/useMergedState'; import useMergedState from '../_util/hooks/useMergedState';
import { fillFieldNames, toPathKey, toPathKeys } from './utils/commonUtil'; import { fillFieldNames, toPathKey, toPathKeys, SHOW_PARENT, SHOW_CHILD } from './utils/commonUtil';
import useEntities from './hooks/useEntities'; import useEntities from './hooks/useEntities';
import useSearchConfig from './hooks/useSearchConfig'; import useSearchConfig from './hooks/useSearchConfig';
import useSearchOptions from './hooks/useSearchOptions'; import useSearchOptions from './hooks/useSearchOptions';
@ -23,6 +23,7 @@ import { BaseSelect } from '../vc-select';
import devWarning from '../vc-util/devWarning'; import devWarning from '../vc-util/devWarning';
import useMaxLevel from '../vc-tree/useMaxLevel'; import useMaxLevel from '../vc-tree/useMaxLevel';
export { SHOW_PARENT, SHOW_CHILD };
export interface ShowSearchType<OptionType extends BaseOptionType = DefaultOptionType> { export interface ShowSearchType<OptionType extends BaseOptionType = DefaultOptionType> {
filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean; filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean;
render?: (arg?: { render?: (arg?: {
@ -49,6 +50,7 @@ export interface InternalFieldNames extends Required<FieldNames> {
export type SingleValueType = (string | number)[]; export type SingleValueType = (string | number)[];
export type ValueType = SingleValueType | SingleValueType[]; export type ValueType = SingleValueType | SingleValueType[];
export type ShowCheckedStrategy = typeof SHOW_PARENT | typeof SHOW_CHILD;
export interface BaseOptionType { export interface BaseOptionType {
disabled?: boolean; disabled?: boolean;
@ -73,14 +75,11 @@ function baseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType
value: { type: [String, Number, Array] as PropType<ValueType> }, value: { type: [String, Number, Array] as PropType<ValueType> },
defaultValue: { type: [String, Number, Array] as PropType<ValueType> }, defaultValue: { type: [String, Number, Array] as PropType<ValueType> },
changeOnSelect: { type: Boolean, default: undefined }, changeOnSelect: { type: Boolean, default: undefined },
onChange: Function as PropType<
(value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void
>,
displayRender: Function as PropType< displayRender: Function as PropType<
(opt: { labels: string[]; selectedOptions?: OptionType[] }) => any (opt: { labels: string[]; selectedOptions?: OptionType[] }) => any
>, >,
checkable: { type: Boolean, default: undefined }, checkable: { type: Boolean, default: undefined },
showCheckedStrategy: { type: String as PropType<ShowCheckedStrategy>, default: SHOW_PARENT },
// Search // Search
showSearch: { showSearch: {
type: [Boolean, Object] as PropType<boolean | ShowSearchType<OptionType>>, type: [Boolean, Object] as PropType<boolean | ShowSearchType<OptionType>>,
@ -184,7 +183,7 @@ function toRawValues(value: ValueType): SingleValueType[] {
return value; return value;
} }
return value.length === 0 ? [] : [value]; return (value.length === 0 ? [] : [value]).map(val => (Array.isArray(val) ? val : [val]));
} }
export default defineComponent({ export default defineComponent({
@ -215,10 +214,10 @@ export default defineComponent({
/** Convert path key back to value format */ /** Convert path key back to value format */
const getValueByKeyPath = (pathKeys: Key[]): SingleValueType[] => { const getValueByKeyPath = (pathKeys: Key[]): SingleValueType[] => {
const ketPathEntities = pathKeyEntities.value; const keyPathEntities = pathKeyEntities.value;
return pathKeys.map(pathKey => { return pathKeys.map(pathKey => {
const { nodes } = ketPathEntities[pathKey]; const { nodes } = keyPathEntities[pathKey];
return nodes.map(node => node[mergedFieldNames.value.value]); return nodes.map(node => node[mergedFieldNames.value.value]);
}); });
@ -275,12 +274,12 @@ export default defineComponent({
} }
const keyPathValues = toPathKeys(existValues); const keyPathValues = toPathKeys(existValues);
const ketPathEntities = pathKeyEntities.value; const keyPathEntities = pathKeyEntities.value;
const { checkedKeys, halfCheckedKeys } = conductCheck( const { checkedKeys, halfCheckedKeys } = conductCheck(
keyPathValues, keyPathValues,
true, true,
ketPathEntities, keyPathEntities,
maxLevel.value, maxLevel.value,
levelEntities.value, levelEntities.value,
); );
@ -295,7 +294,11 @@ export default defineComponent({
const deDuplicatedValues = computed(() => { const deDuplicatedValues = computed(() => {
const checkedKeys = toPathKeys(checkedValues.value); const checkedKeys = toPathKeys(checkedValues.value);
const deduplicateKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value); const deduplicateKeys = formatStrategyValues(
checkedKeys,
pathKeyEntities.value,
props.showCheckedStrategy,
);
return [...missingCheckedValues.value, ...getValueByKeyPath(deduplicateKeys)]; return [...missingCheckedValues.value, ...getValueByKeyPath(deduplicateKeys)];
}); });
@ -330,6 +333,7 @@ export default defineComponent({
// =========================== Select =========================== // =========================== Select ===========================
const onInternalSelect = (valuePath: SingleValueType) => { const onInternalSelect = (valuePath: SingleValueType) => {
setSearchValue('');
if (!multiple.value) { if (!multiple.value) {
triggerChange(valuePath); triggerChange(valuePath);
} else { } else {
@ -379,7 +383,11 @@ export default defineComponent({
} }
// Roll up to parent level keys // Roll up to parent level keys
const deDuplicatedKeys = formatStrategyValues(checkedKeys, pathKeyEntities.value); const deDuplicatedKeys = formatStrategyValues(
checkedKeys,
pathKeyEntities.value,
props.showCheckedStrategy,
);
nextCheckedValues = getValueByKeyPath(deDuplicatedKeys); nextCheckedValues = getValueByKeyPath(deDuplicatedKeys);
} }
@ -537,7 +545,7 @@ export default defineComponent({
return () => { return () => {
const emptyOptions = !(mergedSearchValue.value ? searchOptions.value : mergedOptions.value) const emptyOptions = !(mergedSearchValue.value ? searchOptions.value : mergedOptions.value)
.length; .length;
const { dropdownMatchSelectWidth = false } = props;
const dropdownStyle: CSSProperties = const dropdownStyle: CSSProperties =
// Search to match width // Search to match width
(mergedSearchValue.value && mergedSearchConfig.value.matchInputWidth) || (mergedSearchValue.value && mergedSearchConfig.value.matchInputWidth) ||
@ -555,7 +563,7 @@ export default defineComponent({
ref={selectRef} ref={selectRef}
id={mergedId} id={mergedId}
prefixCls={props.prefixCls} prefixCls={props.prefixCls}
dropdownMatchSelectWidth={false} dropdownMatchSelectWidth={dropdownMatchSelectWidth}
dropdownStyle={{ ...mergedDropdownStyle.value, ...dropdownStyle }} dropdownStyle={{ ...mergedDropdownStyle.value, ...dropdownStyle }}
// Value // Value
displayValues={displayValues.value} displayValues={displayValues.value}

View File

@ -4,7 +4,7 @@ import type { DefaultOptionType, SingleValueType } from '../Cascader';
import { SEARCH_MARK } from '../hooks/useSearchOptions'; import { SEARCH_MARK } from '../hooks/useSearchOptions';
import type { Key } from '../../_util/type'; import type { Key } from '../../_util/type';
import { useInjectCascader } from '../context'; import { useInjectCascader } from '../context';
export const FIX_LABEL = '__cascader_fix_label__';
export interface ColumnProps { export interface ColumnProps {
prefixCls: string; prefixCls: string;
multiple?: boolean; multiple?: boolean;
@ -58,7 +58,7 @@ export default function Column({
{options.map(option => { {options.map(option => {
const { disabled } = option; const { disabled } = option;
const searchOptions = option[SEARCH_MARK]; const searchOptions = option[SEARCH_MARK];
const label = option[fieldNames.value.label]; const label = option[FIX_LABEL] ?? option[fieldNames.value.label];
const value = option[fieldNames.value.value]; const value = option[fieldNames.value.value];
const isMergedLeaf = isLeaf(option, fieldNames.value); const isMergedLeaf = isLeaf(option, fieldNames.value);
@ -132,6 +132,10 @@ export default function Column({
triggerOpenPath(); triggerOpenPath();
} }
}} }}
onMousedown={e => {
// Prevent selector from blurring
e.preventDefault();
}}
> >
{multiple && ( {multiple && (
<Checkbox <Checkbox
@ -145,7 +149,7 @@ export default function Column({
}} }}
/> />
)} )}
<div class={`${menuItemPrefixCls}-content`}>{option[fieldNames.value.label]}</div> <div class={`${menuItemPrefixCls}-content`}>{label}</div>
{!isLoading && expandIcon && !isMergedLeaf && ( {!isLoading && expandIcon && !isMergedLeaf && (
<div class={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div> <div class={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
)} )}

View File

@ -1,16 +1,21 @@
/* eslint-disable default-case */ /* eslint-disable default-case */
import Column from './Column';
import type { DefaultOptionType, SingleValueType } from '../Cascader'; import type { DefaultOptionType, SingleValueType } from '../Cascader';
import { isLeaf, toPathKey, toPathKeys, toPathValueStr } from '../utils/commonUtil'; import {
isLeaf,
toPathKey,
toPathKeys,
toPathValueStr,
scrollIntoParentView,
} from '../utils/commonUtil';
import useActive from './useActive'; import useActive from './useActive';
import useKeyboard from './useKeyboard'; import useKeyboard from './useKeyboard';
import { toPathOptions } from '../utils/treeUtil'; import { toPathOptions } from '../utils/treeUtil';
import { computed, defineComponent, ref, shallowRef, watchEffect } from 'vue'; import { computed, defineComponent, onMounted, ref, shallowRef, watch, watchEffect } from 'vue';
import { useBaseProps } from '../../vc-select'; import { useBaseProps } from '../../vc-select';
import { useInjectCascader } from '../context'; import { useInjectCascader } from '../context';
import type { Key } from '../../_util/type'; import type { Key } from '../../_util/type';
import type { EventHandler } from '../../_util/EventInterface'; import type { EventHandler } from '../../_util/EventInterface';
import Column, { FIX_LABEL } from './Column';
export default defineComponent({ export default defineComponent({
name: 'OptionList', name: 'OptionList',
inheritAttrs: false, inheritAttrs: false,
@ -149,18 +154,29 @@ export default defineComponent({
} }
}; };
useKeyboard( useKeyboard(context, mergedOptions, fieldNames, activeValueCells, onPathOpen, onKeyboardSelect);
context,
mergedOptions,
fieldNames,
activeValueCells,
onPathOpen,
containerRef,
onKeyboardSelect,
);
const onListMouseDown: EventHandler = event => { const onListMouseDown: EventHandler = event => {
event.preventDefault(); event.preventDefault();
}; };
onMounted(() => {
watch(
activeValueCells,
cells => {
for (let i = 0; i < cells.length; i += 1) {
const cellPath = cells.slice(0, i + 1);
const cellKeyPath = toPathKey(cellPath);
const ele = containerRef.value?.querySelector<HTMLElement>(
`li[data-path-key="${cellKeyPath.replace(/\\{0,2}"/g, '\\"')}"]`, // matches unescaped double quotes
);
if (ele) {
scrollIntoParentView(ele);
}
}
},
{ flush: 'post', immediate: true },
);
});
return () => { return () => {
// ========================== Render ========================== // ========================== Render ==========================
const { const {
@ -173,8 +189,8 @@ export default defineComponent({
const emptyList: DefaultOptionType[] = [ const emptyList: DefaultOptionType[] = [
{ {
[fieldNames.value.label as 'label']: notFoundContent,
[fieldNames.value.value as 'value']: '__EMPTY__', [fieldNames.value.value as 'value']: '__EMPTY__',
[FIX_LABEL as 'label']: notFoundContent,
disabled: true, disabled: true,
}, },
]; ];

View File

@ -3,9 +3,9 @@ import type { Key } from '../../_util/type';
import type { Ref, SetupContext } from 'vue'; import type { Ref, SetupContext } from 'vue';
import { computed, ref, watchEffect } from 'vue'; import { computed, ref, watchEffect } from 'vue';
import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader'; import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader';
import { toPathKey } from '../utils/commonUtil';
import { useBaseProps } from '../../vc-select'; import { useBaseProps } from '../../vc-select';
import KeyCode from '../../_util/KeyCode'; import KeyCode from '../../_util/KeyCode';
import { SEARCH_MARK } from '../hooks/useSearchOptions';
export default ( export default (
context: SetupContext, context: SetupContext,
@ -13,7 +13,7 @@ export default (
fieldNames: Ref<InternalFieldNames>, fieldNames: Ref<InternalFieldNames>,
activeValueCells: Ref<Key[]>, activeValueCells: Ref<Key[]>,
setActiveValueCells: (activeValueCells: Key[]) => void, setActiveValueCells: (activeValueCells: Key[]) => void,
containerRef: Ref<HTMLElement>, // containerRef: Ref<HTMLElement>,
onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void, onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void,
) => { ) => {
const baseProps = useBaseProps(); const baseProps = useBaseProps();
@ -32,7 +32,7 @@ export default (
const len = activeValueCells.value.length; const len = activeValueCells.value.length;
// Fill validate active value cells and index // Fill validate active value cells and index
for (let i = 0; i < len; i += 1) { for (let i = 0; i < len && currentOptions; i += 1) {
// Mark the active index for current options // Mark the active index for current options
const nextActiveIndex = currentOptions.findIndex( const nextActiveIndex = currentOptions.findIndex(
option => option[fieldNames.value.value] === activeValueCells.value[i], option => option[fieldNames.value.value] === activeValueCells.value[i],
@ -65,9 +65,6 @@ export default (
// Update active value cells and scroll to target element // Update active value cells and scroll to target element
const internalSetActiveValueCells = (next: Key[]) => { const internalSetActiveValueCells = (next: Key[]) => {
setActiveValueCells(next); setActiveValueCells(next);
const ele = containerRef.value?.querySelector(`li[data-path-key="${toPathKey(next)}"]`);
ele?.scrollIntoView?.({ block: 'nearest' });
}; };
// Same options offset // Same options offset
@ -165,10 +162,18 @@ export default (
// >>> Select // >>> Select
case KeyCode.ENTER: { case KeyCode.ENTER: {
if (validActiveValueCells.value.length) { if (validActiveValueCells.value.length) {
onKeyBoardSelect( const option = lastActiveOptions.value[lastActiveIndex.value];
validActiveValueCells.value,
lastActiveOptions.value[lastActiveIndex.value], // Search option should revert back of origin options
); const originOptions: DefaultOptionType[] = option?.[SEARCH_MARK] || [];
if (originOptions.length) {
onKeyBoardSelect(
originOptions.map(opt => opt[fieldNames.value.value]),
originOptions[originOptions.length - 1],
);
} else {
onKeyBoardSelect(validActiveValueCells.value, option);
}
} }
break; break;
} }

View File

@ -51,10 +51,11 @@ export default (
labels: valueOptions.map(({ option, value }) => option?.[fieldNames.value.label] ?? value), labels: valueOptions.map(({ option, value }) => option?.[fieldNames.value.label] ?? value),
selectedOptions: valueOptions.map(({ option }) => option), selectedOptions: valueOptions.map(({ option }) => option),
}); });
const value = toPathKey(valueCells);
return { return {
label, label,
value: toPathKey(valueCells), value,
key: value,
valueCells, valueCells,
}; };
}); });

View File

@ -39,6 +39,7 @@ export default (
if ( if (
// If is leaf option // If is leaf option
!children || !children ||
children.length === 0 ||
// If is changeOnSelect // If is changeOnSelect
changeOnSelect.value changeOnSelect.value
) { ) {

View File

@ -1,5 +1,9 @@
// rc-cascader@3.0.0-alpha.6 // rc-cascader@3.4.2
import Cascader, { internalCascaderProps as cascaderProps } from './Cascader'; import Cascader, {
internalCascaderProps as cascaderProps,
SHOW_PARENT,
SHOW_CHILD,
} from './Cascader';
export type { export type {
CascaderProps, CascaderProps,
@ -8,5 +12,5 @@ export type {
DefaultOptionType, DefaultOptionType,
BaseOptionType, BaseOptionType,
} from './Cascader'; } from './Cascader';
export { cascaderProps }; export { cascaderProps, SHOW_PARENT, SHOW_CHILD };
export default Cascader; export default Cascader;

View File

@ -6,6 +6,8 @@ import type {
} from '../Cascader'; } from '../Cascader';
export const VALUE_SPLIT = '__RC_CASCADER_SPLIT__'; export const VALUE_SPLIT = '__RC_CASCADER_SPLIT__';
export const SHOW_PARENT = 'SHOW_PARENT';
export const SHOW_CHILD = 'SHOW_CHILD';
export function toPathKey(value: SingleValueType) { export function toPathKey(value: SingleValueType) {
return value.join(VALUE_SPLIT); return value.join(VALUE_SPLIT);
@ -33,3 +35,17 @@ export function fillFieldNames(fieldNames?: FieldNames): InternalFieldNames {
export function isLeaf(option: DefaultOptionType, fieldNames: FieldNames) { export function isLeaf(option: DefaultOptionType, fieldNames: FieldNames) {
return option.isLeaf ?? !option[fieldNames.children]?.length; return option.isLeaf ?? !option[fieldNames.children]?.length;
} }
export function scrollIntoParentView(element: HTMLElement) {
const parent = element.parentElement;
if (!parent) {
return;
}
const elementToParent = element.offsetTop - parent.offsetTop; // offsetParent may not be parent.
if (elementToParent - parent.scrollTop < 0) {
parent.scrollTo({ top: elementToParent });
} else if (elementToParent + element.offsetHeight - parent.scrollTop > parent.offsetHeight) {
parent.scrollTo({ top: elementToParent + element.offsetHeight - parent.offsetHeight });
}
}

View File

@ -1,21 +1,27 @@
import type { Key } from '../../_util/type'; import type { Key } from '../../_util/type';
import type { SingleValueType, DefaultOptionType, InternalFieldNames } from '../Cascader'; import type {
SingleValueType,
DefaultOptionType,
InternalFieldNames,
ShowCheckedStrategy,
} from '../Cascader';
import type { OptionsInfo } from '../hooks/useEntities'; import type { OptionsInfo } from '../hooks/useEntities';
import { SHOW_CHILD } from './commonUtil';
export function formatStrategyValues( export function formatStrategyValues(
pathKeys: Key[], pathKeys: Key[],
keyPathEntities: OptionsInfo['pathKeyEntities'], keyPathEntities: OptionsInfo['pathKeyEntities'],
showCheckedStrategy: ShowCheckedStrategy,
) { ) {
const valueSet = new Set(pathKeys); const valueSet = new Set(pathKeys);
return pathKeys.filter(key => { return pathKeys.filter(key => {
const entity = keyPathEntities[key]; const entity = keyPathEntities[key];
const parent = entity ? entity.parent : null; const parent = entity ? entity.parent : null;
const children = entity ? entity.children : null;
if (parent && !parent.node.disabled && valueSet.has(parent.key)) { return showCheckedStrategy === SHOW_CHILD
return false; ? !(children && children.some(child => child.key && valueSet.has(child.key)))
} : !(parent && !parent.node.disabled && valueSet.has(parent.key));
return true;
}); });
} }