Skip to content

Commit

Permalink
feat(List): support virtual scroll (#3360)
Browse files Browse the repository at this point in the history
* feat(List): support virtual scroll

* chore: update snapshot
  • Loading branch information
uyarn authored Sep 13, 2023
1 parent b46b660 commit a46ef30
Show file tree
Hide file tree
Showing 12 changed files with 628 additions and 36 deletions.
14 changes: 14 additions & 0 deletions src/list/_example/virtual-scroll.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<t-list style="height: 300px" :scroll="{ type: 'virtual' }"
><t-list-item v-for="(item, index) in listRef" :key="index"> {{ item.content }}</t-list-item></t-list
>
</template>
<script setup>
import { ref } from 'vue';
const list = [];
for (let i = 0; i < 10000; i++) {
list.push({ content: `选项${i + 1}` });
}
const listRef = ref(list);
</script>
28 changes: 28 additions & 0 deletions src/list/hooks/useListItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { computed } from 'vue';
import isArray from 'lodash/isArray';

import { useChildComponentSlots } from '../../hooks/slot';

export const useListItems = () => {
const getChildComponentSlots = useChildComponentSlots();

const listItems = computed(() => {
const computedListItems = [];
// 处理 slots
const listItemSlots = getChildComponentSlots('ListItem');

if (isArray(listItemSlots)) {
for (const child of listItemSlots) {
computedListItems.push({
...child.props,
slots: child.children,
} as any);
}
}
return computedListItems;
});

return {
listItems,
};
};
61 changes: 61 additions & 0 deletions src/list/hooks/useListVirtualScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Ref, computed } from 'vue';
import useVirtualScroll from '../../hooks/useVirtualScrollNew';
import { TdListProps } from '../type';
import { Styles } from '../../common';

export const useListVirtualScroll = (
scroll: TdListProps['scroll'],
listRef: Ref<HTMLElement>,
listItems: Ref<any[]>,
) => {
const virtualScrollParams = computed(() => ({
data: listItems.value,
scroll: scroll,
}));
const virtualConfig = useVirtualScroll(listRef, virtualScrollParams);
const isVirtualScroll = computed(() => virtualConfig.isVirtualScroll.value);
let lastScrollY = -1;

const onInnerVirtualScroll = (e: WheelEvent) => {
const target = (e.target || e.srcElement) as HTMLElement;
const top = target.scrollTop;
if (lastScrollY !== top) {
virtualConfig.isVirtualScroll.value && virtualConfig.handleScroll();
} else {
lastScrollY = -1;
}
lastScrollY = top;
};

const cursorStyle = computed(
() =>
({
position: 'absolute',
width: '1px',
height: '1px',
transition: 'transform 0.2s',
transform: `translate(0, ${virtualConfig.scrollHeight.value}px)`,
'-ms-transform': `translate(0, ${virtualConfig.scrollHeight.value}px)`,
'-moz-transform': `translate(0, ${virtualConfig.scrollHeight.value}px)`,
'-webkit-transform': `translate(0, ${virtualConfig.scrollHeight.value}px)`,
} as Styles),
);

const listStyle = computed(
() =>
({
transform: `translate(0, ${virtualConfig.translateY.value}px)`,
'-ms-transform': `translate(0, ${virtualConfig.translateY.value}px)`,
'-moz-transform': `translate(0, ${virtualConfig.translateY.value}px)`,
'-webkit-transform': `translate(0, ${virtualConfig.translateY.value}px)`,
} as Styles),
);

return {
virtualConfig,
cursorStyle,
listStyle,
isVirtualScroll,
onInnerVirtualScroll,
};
};
1 change: 0 additions & 1 deletion src/list/list-item-meta-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* updated at 2021-12-12 19:17:30
* */

import { TdListItemMetaProps } from '../list/type';
Expand Down
1 change: 0 additions & 1 deletion src/list/list-item-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* updated at 2021-12-12 19:17:30
* */

import { TdListItemProps } from '../list/type';
Expand Down
6 changes: 3 additions & 3 deletions src/list/list.en-US.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
:: BASE_DOC ::

## API

### List Props

name | type | default | description | required
-- | -- | -- | -- | --
asyncLoading | String / Slot / Function | - | Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N
footer | String / Slot / Function | - | Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N
header | String / Slot / Function | - | Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N
layout | String | horizontal | options:horizontal/vertical | N
size | String | medium | options:small/medium/large | N
layout | String | horizontal | options: horizontal/vertical | N
scroll | Object | - | lazy load and virtual scroll。Typescript:`TScroll`[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N
size | String | medium | options: small/medium/large | N
split | Boolean | false | \- | N
stripe | Boolean | false | \- | N
onLoadMore | Function | | Typescript:`(options: { e: MouseEvent }) => void`<br/> | N
Expand Down
7 changes: 7 additions & 0 deletions src/list/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@

{{ scroll }}

### 支持虚拟滚动的列表

支持开启虚拟滚动,适用于一次性加载长列表的场景

{{ virtual-scroll }}

## API
### List Props

Expand All @@ -33,6 +39,7 @@ asyncLoading | String / Slot / Function | - | 自定义加载中。值为空不
footer | String / Slot / Function | - | 底部。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N
header | String / Slot / Function | - | 头部。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N
layout | String | horizontal | 排列方式(待设计稿输出)。可选项:horizontal/vertical | N
scroll | Object | - | 懒加载和虚拟滚动。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动,`scroll.threshold` 默认为 `100`。TS 类型:`TScroll`[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N
size | String | medium | 尺寸。可选项:small/medium/large | N
split | Boolean | false | 是否展示分割线 | N
stripe | Boolean | false | 是否展示斑马纹 | N
Expand Down
57 changes: 46 additions & 11 deletions src/list/list.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { defineComponent, VNodeChild, computed } from 'vue';
import { defineComponent, VNodeChild, computed, ref } from 'vue';
import isString from 'lodash/isString';
import { useTNodeJSX } from '../hooks/tnode';
import TLoading from '../loading';
import TListItem from './list-item';
import props from './props';
import { TdListProps } from './type';
import { LOAD_MORE, LOADING } from './const';
import { useConfig, usePrefixClass, useCommonClassName } from '../hooks/useConfig';
import isString from 'lodash/isString';
import { useListItems } from './hooks/useListItems';
import { useListVirtualScroll } from './hooks/useListVirtualScroll';

import type { TdListProps } from './type';

export default defineComponent({
name: 'TList',
props: {
...props,
},
setup(props: TdListProps) {
const listRef = ref();

const { globalConfig } = useConfig('list');
const COMPONENT_NAME = usePrefixClass('list');
const { SIZE } = useCommonClassName();
const renderTNodeJSX = useTNodeJSX();
const { listItems } = useListItems();

const { virtualConfig, cursorStyle, listStyle, isVirtualScroll, onInnerVirtualScroll } = useListVirtualScroll(
props.scroll,
listRef,
listItems,
);

/** 列表基础逻辑 start */
const listClass = computed(() => {
Expand All @@ -33,19 +46,39 @@ export default defineComponent({
const renderContent = (): VNodeChild => {
const propsHeaderContent = renderTNodeJSX('header');
const propsFooterContent = renderTNodeJSX('footer');
return [
propsHeaderContent && <div class={`${COMPONENT_NAME.value}__header`}>{propsHeaderContent}</div>,
<ul class={`${COMPONENT_NAME.value}__inner`}>{renderTNodeJSX('default')}</ul>,
propsFooterContent && <div class={`${COMPONENT_NAME.value}__footer`}>{propsFooterContent}</div>,
];
const isVirtualScroll = virtualConfig.isVirtualScroll.value;
return (
<>
{propsHeaderContent ? <div class={`${COMPONENT_NAME.value}__header`}>{propsHeaderContent}</div> : null}
{isVirtualScroll ? (
<>
<div style={cursorStyle.value}></div>
<ul class={`${COMPONENT_NAME.value}__inner`} style={listStyle.value}>
{virtualConfig.visibleData.value.map((item) => (
<>
<TListItem v-slots={item.slots}></TListItem>
</>
))}
</ul>
</>
) : (
<ul class={`${COMPONENT_NAME.value}__inner`}>
{listItems.value.map((item) => (
<TListItem v-slots={item.slots}></TListItem>
))}
</ul>
)}
{propsFooterContent ? <div class={`${COMPONENT_NAME.value}__footer`}>{propsFooterContent}</div> : null}
</>
);
};
/** 列表基础逻辑 end */

/** 滚动相关逻辑 start */

const handleScroll = (e: WheelEvent | Event) => {
const handleScroll = (e: WheelEvent) => {
const listElement = e.target as HTMLElement;
const { scrollTop, scrollHeight, clientHeight } = listElement;
if (isVirtualScroll.value) onInnerVirtualScroll(e);
props.onScroll?.({
e,
scrollTop,
Expand Down Expand Up @@ -91,6 +124,8 @@ export default defineComponent({
renderContent,
handleScroll,
handleLoadMore,
listRef,
isVirtualScroll,
};
},

Expand All @@ -103,7 +138,7 @@ export default defineComponent({
</div>,
];
return (
<div class={this.listClass} onScroll={this.handleScroll}>
<div class={this.listClass} onScroll={this.handleScroll} ref="listRef">
{listContent}
</div>
);
Expand Down
7 changes: 6 additions & 1 deletion src/list/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* updated at 2021-12-12 19:17:30
* */

import { TdListProps } from './type';
Expand All @@ -26,14 +25,20 @@ export default {
type: String as PropType<TdListProps['layout']>,
default: 'horizontal' as TdListProps['layout'],
validator(val: TdListProps['layout']): boolean {
if (!val) return true;
return ['horizontal', 'vertical'].includes(val);
},
},
/** 懒加载和虚拟滚动。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动,`scroll.threshold` 默认为 `100` */
scroll: {
type: Object as PropType<TdListProps['scroll']>,
},
/** 尺寸 */
size: {
type: String as PropType<TdListProps['size']>,
default: 'medium' as TdListProps['size'],
validator(val: TdListProps['size']): boolean {
if (!val) return true;
return ['small', 'medium', 'large'].includes(val);
},
},
Expand Down
7 changes: 5 additions & 2 deletions src/list/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* updated at 2021-12-12 19:17:30
* */

import { TNode } from '../common';
import { TNode, TScroll } from '../common';

export interface TdListProps {
/**
Expand All @@ -25,6 +24,10 @@ export interface TdListProps {
* @default horizontal
*/
layout?: 'horizontal' | 'vertical';
/**
* 懒加载和虚拟滚动。为保证组件收益最大化,当数据量小于阈值 `scroll.threshold` 时,无论虚拟滚动的配置是否存在,组件内部都不会开启虚拟滚动,`scroll.threshold` 默认为 `100`
*/
scroll?: TScroll;
/**
* 尺寸
* @default medium
Expand Down
Loading

0 comments on commit a46ef30

Please sign in to comment.