From 593f86fe3c49526e8fcc4352db0afdb986d9fd57 Mon Sep 17 00:00:00 2001 From: danranVm Date: Fri, 29 Apr 2022 09:22:29 +0800 Subject: [PATCH] feat(comp: tree-selct): add customAdditional,getKey and overlayContainer (#869) synchronization with cascader and tree BREAKING CHANGE: `nodeKey` was deprecated, please use `getKey` instead. `target` was deprecated, please use `overlayContainer` instead. `dataSource.additional` was deprecated, please use `customAdditional` instead. --- .../_private/selector/src/Selector.tsx | 32 +- .../selector/src/composables/useInputState.ts | 14 +- .../cascader/__tests__/cascader.spec.ts | 2 +- packages/components/cascader/docs/Index.zh.md | 2 +- packages/components/cascader/src/Cascader.tsx | 19 +- .../cascader/src/composables/useSearchable.ts | 26 +- .../cascader/src/contents/OverlayContent.tsx | 2 +- .../cascader/src/contents/OverlayOption.tsx | 10 +- .../src/contents/OverlayOptionGroup.tsx | 4 +- packages/components/cascader/src/types.ts | 9 + .../components/config/src/defaultConfig.ts | 6 +- packages/components/config/src/types.ts | 21 +- packages/components/input/style/mixin.less | 2 +- .../components/select/demo/CustomLabel.vue | 2 +- packages/components/select/demo/Key.md | 4 +- packages/components/select/src/Select.tsx | 33 +- .../components/select/src/content/Content.tsx | 2 +- .../components/select/src/content/Option.tsx | 6 +- packages/components/select/src/token.ts | 5 +- .../components/tree-select/demo/CustomKey.md | 2 +- .../components/tree-select/demo/CustomKey.vue | 2 +- .../components/tree-select/docs/Index.zh.md | 25 +- packages/components/tree-select/index.ts | 7 +- .../components/tree-select/src/TreeSelect.tsx | 40 ++- .../src/composables/useDataSource.ts | 44 +-- .../src/composables/useGetNodeKey.ts | 5 +- .../src/composables/useSelectedState.ts | 16 +- .../tree-select/src/content/Content.tsx | 36 ++- packages/components/tree-select/src/token.ts | 12 +- packages/components/tree-select/src/types.ts | 28 +- .../__tests__/__snapshots__/tree.spec.ts.snap | 302 +++++++++--------- packages/components/tree/docs/Index.zh.md | 10 +- packages/components/tree/index.ts | 1 + packages/components/tree/src/Tree.tsx | 14 +- .../tree/src/composables/useDataSource.ts | 28 +- .../tree/src/composables/useEvents.ts | 6 +- .../tree/src/composables/useExpandable.ts | 17 +- .../tree/src/composables/useGetNodeKey.ts | 5 +- .../tree/src/composables/useSearchable.ts | 48 +-- .../components/tree/src/node/TreeNode.tsx | 10 +- packages/components/tree/src/token.ts | 2 +- packages/components/tree/src/types.ts | 23 +- .../__snapshots__/proTransfer.spec.ts.snap | 93 ++++++ 43 files changed, 621 insertions(+), 356 deletions(-) diff --git a/packages/components/_private/selector/src/Selector.tsx b/packages/components/_private/selector/src/Selector.tsx index adf85eecd..535926277 100644 --- a/packages/components/_private/selector/src/Selector.tsx +++ b/packages/components/_private/selector/src/Selector.tsx @@ -38,6 +38,21 @@ export default defineComponent({ setup(props, { expose, slots }) { const common = useGlobalConfig('common') const mergedPrefixCls = computed(() => `${common.prefixCls}-selector`) + const mergedClearable = computed(() => { + return !props.disabled && !props.readonly && props.clearable && props.value.length > 0 + }) + const mergedClearIcon = computed(() => props.clearIcon ?? props.config.clearIcon) + const mergedSearchable = computed(() => { + return !props.disabled && !props.readonly && props.searchable === true + }) + const mergedSize = useFormSize(props, props.config) + const mergedSuffix = computed(() => { + return props.suffix ?? (mergedSearchable.value && isFocused.value ? 'search' : props.config.suffix) + }) + const showPlaceholder = computed(() => { + return props.value.length === 0 && !isComposing.value && !inputValue.value + }) + const { mirrorRef, inputRef, @@ -50,27 +65,12 @@ export default defineComponent({ handleCompositionEnd, handleInput, clearInput, - } = useInputState(props) + } = useInputState(props, mergedSearchable) const getBoundingClientRect = () => elementRef.value?.getBoundingClientRect() expose({ focus, blur, clearInput, getBoundingClientRect }) - const mergedClearable = computed(() => { - return !props.disabled && !props.readonly && props.clearable && props.value.length > 0 - }) - const mergedClearIcon = computed(() => props.clearIcon ?? props.config.clearIcon) - const mergedSearchable = computed(() => { - return !props.disabled && !props.readonly && props.searchable === true - }) - const mergedSize = useFormSize(props, props.config) - const mergedSuffix = computed(() => { - return props.suffix ?? (mergedSearchable.value && isFocused.value ? 'search' : props.config.suffix) - }) - const showPlaceholder = computed(() => { - return props.value.length === 0 && !isComposing.value && !inputValue.value - }) - const classes = computed(() => { const config = props.config const { allowInput, className, borderless = config.borderless, multiple } = props diff --git a/packages/components/_private/selector/src/composables/useInputState.ts b/packages/components/_private/selector/src/composables/useInputState.ts index 3344a26c4..bc6019ce3 100644 --- a/packages/components/_private/selector/src/composables/useInputState.ts +++ b/packages/components/_private/selector/src/composables/useInputState.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { type Ref, onMounted, ref } from 'vue' +import { type ComputedRef, type Ref, onMounted, ref } from 'vue' import { callEmit } from '@idux/cdk/utils' import { useFormFocusMonitor } from '@idux/components/utils' @@ -28,7 +28,7 @@ export interface InputStateContext { clearInput: () => void } -export function useInputState(props: SelectorProps): InputStateContext { +export function useInputState(props: SelectorProps, mergedSearchable: ComputedRef): InputStateContext { const mirrorRef = ref() const inputValue = ref('') const isComposing = ref(false) @@ -73,17 +73,21 @@ export function useInputState(props: SelectorProps): InputStateContext { const handleInput = (evt: Event, emitInput = true) => { emitInput && callEmit(props.onInput, evt) + + const inputEnabled = props.allowInput || mergedSearchable.value + if (isComposing.value) { - syncMirrorWidth(evt) + inputEnabled && syncMirrorWidth(evt) return } - if (props.allowInput || props.searchable) { + + if (inputEnabled) { const { value } = evt.target as HTMLInputElement if (value !== inputValue.value) { inputValue.value = value callEmit(props.onInputValueChange, value) } - callEmit(props.onSearch, value) + mergedSearchable.value && callEmit(props.onSearch, value) syncMirrorWidth() } } diff --git a/packages/components/cascader/__tests__/cascader.spec.ts b/packages/components/cascader/__tests__/cascader.spec.ts index 3cb9fa968..b634b038d 100644 --- a/packages/components/cascader/__tests__/cascader.spec.ts +++ b/packages/components/cascader/__tests__/cascader.spec.ts @@ -125,7 +125,7 @@ const defaultMultipleValue = [ ] const defaultExpandedKeys = ['components', 'general'] -describe.only('Cascader', () => { +describe('Cascader', () => { describe('single work', () => { const CascaderMount = (options?: MountingOptions>) => { const { props, ...rest } = options || {} diff --git a/packages/components/cascader/docs/Index.zh.md b/packages/components/cascader/docs/Index.zh.md index d4ba6c6b4..9dc691c98 100644 --- a/packages/components/cascader/docs/Index.zh.md +++ b/packages/components/cascader/docs/Index.zh.md @@ -25,6 +25,7 @@ subtitle: 级联选择 | `childrenKey` | 替代[CascaderData](#CascaderData)中的`children`字段 | `string` | `children` | ✅ | - | | `clearable` | 是否显示清除图标 | `boolean` | `false` | - | - | | `clearIcon` | 设置清除图标 | `string \| #clearIcon` | `'close-circle'` | ✅ | - | +| `customAdditional` | 自定义下拉选项的额外属性 | `CascaderCustomAdditional` | - | - | 例如 `class`, 或者原生事件 | | `dataSource` | 树型数据数组,参见[CascaderData](#CascaderData) | `CascaderData[]` | `[]` | - | - | | `disabled` | 禁用选择器 | `boolean` | - | - | - | | `empty` | 空数据时的内容 | `string \| EmptyProps \| #empty` | - | - | - | @@ -60,7 +61,6 @@ subtitle: 级联选择 | 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | | --- | --- | --- | --- | --- | --- | -| `additional` | 节点的扩展属性 | `object` | - | - | 可以用于设置节点的 `class`, `style` 或者其他属性 | | `children` | 子节点数据 | `CascaderData[]` | - | - | - | | `disabled` | 禁用节点 | `boolean` | - | - | - | | `isLeaf` | 设置为叶子节点 | `boolean` | - | - | 不为 `true` 且设置了 `loadChildren` 时会强制将其作为父节点 | diff --git a/packages/components/cascader/src/Cascader.tsx b/packages/components/cascader/src/Cascader.tsx index fdd34ed02..cfef426be 100644 --- a/packages/components/cascader/src/Cascader.tsx +++ b/packages/components/cascader/src/Cascader.tsx @@ -74,14 +74,22 @@ export default defineComponent({ mergedDataMap, ) + watch(overlayOpened, opened => { + opened && focus() + clearInput() + }) + + const handleOverlayClick = () => { + if (props.searchable !== 'overlay') { + focus() + } + } + const handleBlur = () => accessor.markAsBlurred() const handleItemRemove = (key: VKey) => { focus() selectedStateContext.handleSelect(key) } - const handleOverlayClick = () => { - overlayOpened.value && focus() - } provide(cascaderToken, { props, @@ -107,11 +115,6 @@ export default defineComponent({ ...expandableContext, }) - watch(overlayOpened, opened => { - opened ? focus() : blur() - clearInput() - }) - const overlayClasses = computed(() => { const { overlayClassName, multiple } = props const prefixCls = mergedPrefixCls.value diff --git a/packages/components/cascader/src/composables/useSearchable.ts b/packages/components/cascader/src/composables/useSearchable.ts index b105a8ed9..9799e8f29 100644 --- a/packages/components/cascader/src/composables/useSearchable.ts +++ b/packages/components/cascader/src/composables/useSearchable.ts @@ -11,8 +11,7 @@ import { isFunction } from 'lodash-es' import { NoopArray, type VKey } from '@idux/cdk/utils' -import { type CascaderData, type CascaderProps, CascaderSearchFn } from '../types' -import { getChildrenKeys } from '../utils' +import { type CascaderData, type CascaderProps, type CascaderSearchFn } from '../types' import { type MergedData } from './useDataSource' export interface SearchableContext { @@ -45,10 +44,7 @@ export function useSearchable( if (_parentEnabled || data.isLeaf) { keySet.add(key) } - const childrenKeys = getChildrenKeys(data, true) - if (childrenKeys.length) { - childrenKeys.forEach(key => keySet.add(key)) - } + processChildren(keySet, data, _parentEnabled) } }) return [...keySet] @@ -78,3 +74,21 @@ function getDefaultSearchFn(labelKey: string): CascaderSearchFn { return label ? label.toLowerCase().includes(searchValue.toLowerCase()) : false } } + +function processChildren(keySet: Set, data: MergedData, parentEnabled: boolean) { + if (!data || !data.children) { + return + } + + data.children.forEach(child => { + if (child.rawData.disabled || keySet.has(child.key)) { + return + } + + if (parentEnabled || child.isLeaf) { + keySet.add(child.key) + } + + processChildren(keySet, child, parentEnabled) + }) +} diff --git a/packages/components/cascader/src/contents/OverlayContent.tsx b/packages/components/cascader/src/contents/OverlayContent.tsx index fb216da85..41e50af56 100644 --- a/packages/components/cascader/src/contents/OverlayContent.tsx +++ b/packages/components/cascader/src/contents/OverlayContent.tsx @@ -78,7 +78,7 @@ export default defineComponent({ , ) - return overlayRender ? overlayRender(children) :
{children}
+ return
{overlayRender ? overlayRender(children) : children}
} }, }) diff --git a/packages/components/cascader/src/contents/OverlayOption.tsx b/packages/components/cascader/src/contents/OverlayOption.tsx index eebba490c..9cff1334c 100644 --- a/packages/components/cascader/src/contents/OverlayOption.tsx +++ b/packages/components/cascader/src/contents/OverlayOption.tsx @@ -17,6 +17,7 @@ import { type CascaderData } from '../types' export default defineComponent({ props: { children: { type: Array as PropType, default: undefined }, + index: { type: Number, required: true }, isLeaf: { type: Boolean, required: true }, label: { type: String, required: true }, parentKey: { type: [String, Number, Symbol], default: undefined }, @@ -91,15 +92,22 @@ export default defineComponent({ const searchValue = inputValue.value const mergedLabel = searchValue ? label : (rawData[mergedLabelKey.value] as string) + // 优先显示 selectedLimitTitle + const title = (!(disabled || selected) && selectedLimitTitle.value) || mergedLabel + const customAdditional = cascaderProps.customAdditional + ? cascaderProps.customAdditional({ data: rawData, index: props.index }) + : undefined + return (
{multiple && ( 0) { const { overlayHeight, overlayItemHeight, virtual } = cascaderProps - const itemRender: VirtualItemRenderFn = ({ item }) => + const itemRender: VirtualItemRenderFn = ({ item, index }) => ( + + ) children = ( , default: undefined }, dataSource: { type: Array as PropType, default: () => [] }, disabled: { type: Boolean, default: false }, empty: { type: [String, Object] as PropType, default: undefined }, @@ -87,7 +88,15 @@ export type CascaderInstance = InstanceType Record | undefined + export interface CascaderData { + /** + * @deprecated please use `customAdditional` instead' + */ additional?: { // eslint-disable-next-line @typescript-eslint/no-explicit-any class?: any diff --git a/packages/components/config/src/defaultConfig.ts b/packages/components/config/src/defaultConfig.ts index 3a8cc4778..945d6b41e 100644 --- a/packages/components/config/src/defaultConfig.ts +++ b/packages/components/config/src/defaultConfig.ts @@ -332,8 +332,10 @@ export const defaultConfig: GlobalConfig = { }, tree: { blocked: false, + childrenKey: 'children', expandIcon: 'right', - nodeKey: 'key', + getKey: 'key', + labelKey: 'label', showLine: false, }, treeSelect: { @@ -341,7 +343,7 @@ export const defaultConfig: GlobalConfig = { childrenKey: 'children', clearIcon: 'close-circle', labelKey: 'label', - nodeKey: 'key', + getKey: 'key', overlayMatchWidth: true, size: 'md', suffix: 'down', diff --git a/packages/components/config/src/types.ts b/packages/components/config/src/types.ts index ed08891cf..c1954afe3 100644 --- a/packages/components/config/src/types.ts +++ b/packages/components/config/src/types.ts @@ -5,10 +5,10 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { VKey } from '@idux/cdk' import type { BreakpointKey } from '@idux/cdk/breakpoint' import type { PopperPlacement, PopperTrigger } from '@idux/cdk/popper' import type { PortalTargetType } from '@idux/cdk/portal' +import type { VKey } from '@idux/cdk/utils' import type { AlertType } from '@idux/components/alert' import type { AvatarShape, AvatarSize } from '@idux/components/avatar' import type { ButtonSize } from '@idux/components/button' @@ -33,6 +33,7 @@ import type { StepperLabelPlacement, StepperSize } from '@idux/components/steppe import type { TableColumnAlign, TableColumnSortOrder, TablePaginationPosition, TableSize } from '@idux/components/table' import type { TagShape } from '@idux/components/tag' import type { TextareaAutoRows, TextareaResize } from '@idux/components/textarea' +import type { TreeNode } from '@idux/components/tree' import type { UploadFilesType, UploadIconType, UploadRequestMethod, UploadRequestOption } from '@idux/components/upload' import type { VNode } from 'vue' @@ -501,8 +502,14 @@ export interface TooltipConfig { export interface TreeConfig { blocked: boolean + childrenKey: string expandIcon: string - nodeKey: string + getKey: string | ((data: TreeNode) => VKey) + labelKey: string + /** + * @deprecated please use `labelKey` instead' + */ + nodeKey?: string showLine: boolean } @@ -510,11 +517,19 @@ export interface TreeSelectConfig { borderless: boolean childrenKey: string clearIcon: string + getKey: string | ((data: TreeNode) => VKey) labelKey: string - nodeKey: string + /** + * @deprecated please use `labelKey` instead' + */ + nodeKey?: string overlayMatchWidth: boolean + overlayContainer?: PortalTargetType size: FormSize suffix: string + /** + * @deprecated please use `overlayContainer` instead' + */ target?: PortalTargetType } diff --git a/packages/components/input/style/mixin.less b/packages/components/input/style/mixin.less index fa38d9289..7050d41ef 100644 --- a/packages/components/input/style/mixin.less +++ b/packages/components/input/style/mixin.less @@ -15,7 +15,7 @@ .input-size(@font-size; @padding-vertical; @padding-horizontal;) { font-size: @font-size; - padding: @padding-vertical @padding-horizontal; + padding: @padding-vertical (@padding-horizontal - 4) @padding-vertical @padding-horizontal; } .input-hover(@color: @input-hover-color) { diff --git a/packages/components/select/demo/CustomLabel.vue b/packages/components/select/demo/CustomLabel.vue index 6e6e41f04..bcdf0b5ea 100644 --- a/packages/components/select/demo/CustomLabel.vue +++ b/packages/components/select/demo/CustomLabel.vue @@ -1,7 +1,7 @@