diff --git a/packages/components/cascader/_example-ts/virtual-scroll.vue b/packages/components/cascader/_example-ts/virtual-scroll.vue new file mode 100644 index 0000000000..0244e4bd1c --- /dev/null +++ b/packages/components/cascader/_example-ts/virtual-scroll.vue @@ -0,0 +1,54 @@ + + + diff --git a/packages/components/cascader/_example/virtual-scroll.vue b/packages/components/cascader/_example/virtual-scroll.vue new file mode 100644 index 0000000000..0244e4bd1c --- /dev/null +++ b/packages/components/cascader/_example/virtual-scroll.vue @@ -0,0 +1,54 @@ + + + diff --git a/packages/components/cascader/cascader-panel.tsx b/packages/components/cascader/cascader-panel.tsx index 2808cefcfa..6e9b44f850 100644 --- a/packages/components/cascader/cascader-panel.tsx +++ b/packages/components/cascader/cascader-panel.tsx @@ -15,6 +15,7 @@ export default defineComponent({ trigger={props.trigger} cascaderContext={cascaderContext.value} empty={props.empty} + scroll={props.scroll} v-slots={{ empty: slots.empty, option: slots.option, loadingText: slots.loadingText }} /> ); diff --git a/packages/components/cascader/cascader.en-US.md b/packages/components/cascader/cascader.en-US.md index 20c9ad645b..c1cac2a91e 100644 --- a/packages/components/cascader/cascader.en-US.md +++ b/packages/components/cascader/cascader.en-US.md @@ -37,6 +37,7 @@ defaultPopupVisible | Boolean | - | uncontrolled property | N prefixIcon | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N readonly | Boolean | undefined | \- | N reserveKeyword | Boolean | true | \- | N +scroll | \- | - | Lazy loading and virtual scrolling. To maximize the benefits of the component, when the amount of data is less than the threshold `scroll.threshold`, regardless of whether the virtual scrolling configuration exists, virtual scrolling will not be enabled within the component. `scroll.threshold` defaults to `100`。Typescript:`TScroll`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N selectInputProps | Object | - | Typescript:`SelectInputProps`,[SelectInput API Documents](./select-input?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/cascader/type.ts) | N showAllLevels | Boolean | true | \- | N size | String | medium | options: large/medium/small。Typescript:`SizeEnum`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N diff --git a/packages/components/cascader/cascader.md b/packages/components/cascader/cascader.md index 56e51b18f7..45d2858b4d 100644 --- a/packages/components/cascader/cascader.md +++ b/packages/components/cascader/cascader.md @@ -37,6 +37,7 @@ defaultPopupVisible | Boolean | - | 是否显示下拉框。非受控属性 | N prefixIcon | Slot / Function | - | 组件前置图标。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N readonly | Boolean | undefined | 只读状态,值为真会隐藏输入框,且无法打开下拉框 | N reserveKeyword | Boolean | true | 多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词 | N +scroll | \- | - | 懒加载和虚拟滚动。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动,`scroll.threshold` 默认为 `100`。TS 类型:`TScroll`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N selectInputProps | Object | - | 透传 SelectInput 筛选器输入框组件的全部属性。TS 类型:`SelectInputProps`,[SelectInput API Documents](./select-input?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/cascader/type.ts) | N showAllLevels | Boolean | true | 选中值使用完整路径,输入框在单选时也显示完整路径 | N size | String | medium | 组件尺寸。可选项:large/medium/small。TS 类型:`SizeEnum`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N diff --git a/packages/components/cascader/cascader.tsx b/packages/components/cascader/cascader.tsx index f84f1899cf..ac145894b1 100644 --- a/packages/components/cascader/cascader.tsx +++ b/packages/components/cascader/cascader.tsx @@ -190,6 +190,7 @@ export default defineComponent({ loading={props.loading} loadingText={props.loadingText} cascaderContext={cascaderContext.value} + scroll={props.scroll} v-slots={{ option: slots.option, empty: slots.empty, loadingText: slots.loadingText }} /> {renderTNodeJSX('panelBottomContent')} diff --git a/packages/components/cascader/components/List.tsx b/packages/components/cascader/components/List.tsx new file mode 100644 index 0000000000..a4b03ddbb9 --- /dev/null +++ b/packages/components/cascader/components/List.tsx @@ -0,0 +1,168 @@ +import { computed, defineComponent, h, nextTick, PropType, ref, watch } from 'vue'; +import { expandClickEffect, valueChangeEffect } from '../utils'; +import { CascaderContextType, TreeNode } from '../types'; +import { usePrefixClass, useTNodeDefault } from '@tdesign/shared-hooks'; +import Item from './Item'; +import { getDefaultNode } from '@tdesign/shared-utils'; +import CascaderProps from '../props'; +import { useListVirtualScroll } from '../../list/hooks'; + +const props = { + treeNodes: { + type: Array as PropType, + default: [] as PropType, + }, + isFilter: { + type: Boolean, + default: false, + }, + segment: { + type: Boolean, + default: true, + }, + listKey: { + type: String, + }, + level: { + type: Number, + default: 0, + }, + option: CascaderProps.option, + trigger: CascaderProps.trigger, + scroll: CascaderProps.scroll, + cascaderContext: { + type: Object as PropType, + }, +}; + +export default defineComponent({ + name: 'TCascaderList', + props, + setup(props) { + const renderTNodeJSXDefault = useTNodeDefault(); + const COMPONENT_NAME = usePrefixClass('cascader'); + const panelWrapperRef = ref(null); + + const treeNodes = computed(() => props.treeNodes); + const isVisible = computed(() => props.cascaderContext.visible); + const scroll = computed(() => ({ + rowHeight: 28, + ...props.scroll, + })); + + const { virtualConfig, cursorStyle, listStyle, isVirtualScroll, onInnerVirtualScroll, scrollToElement } = + useListVirtualScroll(scroll.value, panelWrapperRef, treeNodes as any); + + const handleExpand = (node: TreeNode, trigger: 'hover' | 'click') => { + const { trigger: propsTrigger, cascaderContext } = props; + expandClickEffect(propsTrigger, trigger, node, cascaderContext); + }; + + const renderItem = (node: TreeNode, index: number) => { + const optionChild = node.data.content + ? getDefaultNode(node.data.content(h)) + : renderTNodeJSXDefault('option', { + params: { + item: node.data, + index, + onExpand: () => handleExpand(node, 'click'), + onChange: () => valueChangeEffect(node, props.cascaderContext), + }, + }); + return ( + { + handleExpand(node, 'click'); + }} + onMouseenter={() => { + handleExpand(node, 'hover'); + }} + onChange={() => { + valueChangeEffect(node, props.cascaderContext); + }} + /> + ); + }; + + const onScrollIntoView = () => { + const { level, treeNodes, cascaderContext } = props; + const checkedNodes = cascaderContext.treeStore.getCheckedNodes(); + let lastCheckedNodes = checkedNodes[checkedNodes.length - 1]; + let index = -1; + if (lastCheckedNodes?.level === level) { + index = treeNodes.findLastIndex((item) => item.value === lastCheckedNodes.value); + } else { + while (lastCheckedNodes) { + if (lastCheckedNodes?.level === level) { + // eslint-disable-next-line no-loop-func + index = treeNodes.findIndex((item) => item.value === lastCheckedNodes.value); + break; + } + lastCheckedNodes = lastCheckedNodes?.parent; + } + } + if (index !== -1) { + scrollToElement({ + index, + }); + } + }; + + const handleScroll = (event: WheelEvent): void => { + if (isVirtualScroll.value) onInnerVirtualScroll(event as unknown as WheelEvent); + }; + + watch( + isVisible, + () => { + if (props.scroll && props.cascaderContext.visible) { + setTimeout(() => { + nextTick(() => { + onScrollIntoView(); + }); + }, 16); + } + }, + { + immediate: true, + }, + ); + + return () => { + const { treeNodes, isFilter, segment, listKey: key } = props; + + return ( +
+ {isVirtualScroll.value ? ( + <> +
+
    + {virtualConfig.visibleData.value.map((node, index) => renderItem(node, index))} +
+ + ) : ( +
    {treeNodes.map((node: TreeNode, index: number) => renderItem(node, index))}
+ )} +
+ ); + }; + }, +}); diff --git a/packages/components/cascader/components/Panel.tsx b/packages/components/cascader/components/Panel.tsx index 3748f09cf5..67f6cafe4a 100644 --- a/packages/components/cascader/components/Panel.tsx +++ b/packages/components/cascader/components/Panel.tsx @@ -1,12 +1,10 @@ -import { defineComponent, PropType, computed, h } from 'vue'; +import { defineComponent, PropType, computed } from 'vue'; -import Item from './Item'; -import { TreeNode, CascaderContextType } from '../types'; +import { CascaderContextType } from '../types'; import CascaderProps from '../props'; import { useConfig, usePrefixClass, useTNodeDefault } from '@tdesign/shared-hooks'; - -import { getDefaultNode } from '@tdesign/shared-utils'; -import { getPanels, expandClickEffect, valueChangeEffect } from '../utils'; +import { getPanels } from '../utils'; +import List from './List'; export default defineComponent({ name: 'TCascaderSubPanel', @@ -21,6 +19,7 @@ export default defineComponent({ cascaderContext: { type: Object as PropType, }, + scroll: CascaderProps.scroll, }, setup(props) { @@ -30,66 +29,36 @@ export default defineComponent({ const panels = computed(() => getPanels(props.cascaderContext.treeNodes)); - const handleExpand = (node: TreeNode, trigger: 'hover' | 'click') => { - const { trigger: propsTrigger, cascaderContext } = props; - expandClickEffect(propsTrigger, trigger, node, cascaderContext); - }; + const renderPanels = () => { + const { inputVal, treeNodes } = props.cascaderContext; - const renderItem = (node: TreeNode, index: number) => { - const optionChild = node.data.content - ? getDefaultNode(node.data.content(h)) - : renderTNodeJSXDefault('option', { - params: { - item: node.data, - index, - onExpand: () => handleExpand(node, 'click'), - onChange: () => valueChangeEffect(node, props.cascaderContext), - }, - }); - return ( - { - handleExpand(node, 'click'); - }} - onMouseenter={() => { - handleExpand(node, 'hover'); - }} - onChange={() => { - valueChangeEffect(node, props.cascaderContext); - }} + scroll={props.scroll} + trigger={props.trigger} /> + ) : ( + panels.value.map((treeNodes, index: number) => ( + + )) ); }; - const renderList = (treeNodes: TreeNode[], isFilter = false, segment = true, index = 1) => ( -
    - {treeNodes.map((node: TreeNode) => renderItem(node, index))} -
- ); - - const renderPanels = () => { - const { inputVal, treeNodes } = props.cascaderContext; - return inputVal - ? renderList(treeNodes, true) - : panels.value.map((treeNodes, index: number) => - renderList(treeNodes, false, index !== panels.value.length - 1, index), - ); - }; - return () => { let content; if (props.loading) { diff --git a/packages/components/cascader/props.ts b/packages/components/cascader/props.ts index 3399ec4498..1b048c6b97 100644 --- a/packages/components/cascader/props.ts +++ b/packages/components/cascader/props.ts @@ -120,6 +120,12 @@ export default { type: Boolean, default: true, }, + /** + * 懒加载和虚拟滚动。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动,`scroll.threshold` 默认为 `100` + */ + scroll: { + type: Object as PropType, + }, /** 透传 SelectInput 筛选器输入框组件的全部属性 */ selectInputProps: { type: Object as PropType, diff --git a/packages/components/cascader/type.ts b/packages/components/cascader/type.ts index 4398d98e7e..f67a5a78db 100644 --- a/packages/components/cascader/type.ts +++ b/packages/components/cascader/type.ts @@ -12,7 +12,7 @@ import { TagInputProps } from '../tag-input'; import { TagProps } from '../tag'; import { TreeNodeModel } from '../tree'; import { PopupVisibleChangeContext } from '../popup'; -import { TNode, TreeOptionData, SizeEnum, TreeKeysType } from '../common'; +import { TNode, TreeOptionData, SizeEnum, TreeKeysType, TScroll } from '../common'; export interface TdCascaderProps { /** @@ -151,6 +151,10 @@ export interface TdCascaderProps