Skip to content

Commit

Permalink
fix(VirtualList): resolve bugs in jumpIndex functionality, close #4883
Browse files Browse the repository at this point in the history
  • Loading branch information
eternalsky committed Aug 21, 2024
1 parent 0d51216 commit 450cf6a
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 45 deletions.
128 changes: 101 additions & 27 deletions components/virtual-list/__tests__/index-spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const generateData = (len: number) => {

for (let i = 0; i < len; i++) {
dataSource.push(
<li key={`${i}-test`} style={{ lineHeight: '20px' }}>
<li key={`${i}-test`} data-key={`${i}-test`} style={{ lineHeight: '20px' }}>
{i}
</li>
);
Expand Down Expand Up @@ -55,7 +55,101 @@ describe('VirtualList', () => {
cy.get('li').should('have.length.at.most', 20);
});

it('should support scroll', () => {
cy.mount(
<div
className="scrollBox"
style={{
height: '200px',
width: '200px',
overflow: 'auto',
}}
>
<VirtualList>{generateData(100)}</VirtualList>
</div>
);
cy.get('.scrollBox').scrollTo(0, 500);
cy.get('[data-key="25-test"]').should('be.visible');
cy.get('.scrollBox').scrollTo(0, 1000);
cy.get('[data-key="50-test"]').should('be.visible');
});

it('should support jumpIndex', () => {
cy.mount(
<div
style={{
height: '200px',
width: '200px',
overflow: 'auto',
}}
>
<VirtualList jumpIndex={50}>{generateData(100)}</VirtualList>
</div>
);

cy.get('li')
.should('be.visible')
.first()
.invoke('text')
.then(text => {
expect(parseInt(text, 10)).to.be.above(40);
});
});

it('should support jumpIndex with itemSizeGetter', () => {
cy.mount(
<div
style={{
height: '200px',
width: '200px',
overflow: 'auto',
}}
>
<VirtualList
jumpIndex={50}
itemSizeGetter={() => {
return 20;
}}
>
{generateData(100)}
</VirtualList>
</div>
);

cy.get('li')
.should('be.visible')
.first()
.invoke('text')
.then(text => {
expect(parseInt(text, 10)).to.be.above(40);
});
});

it('should support scroll with jumpIndex', () => {
cy.mount(
<div
className="scrollBox"
style={{
height: '200px',
width: '200px',
overflow: 'auto',
}}
>
<VirtualList jumpIndex={50}>{generateData(100)}</VirtualList>
</div>
);
cy.get('.scrollBox').scrollTo(0, 0);
cy.get('[data-key="0-test"]').should('be.visible');
cy.get('.scrollBox').scrollTo(0, 1000);
cy.get('[data-key="50-test"]').should('be.visible');
});

it('should render single item', () => {
const singleItem = (
<li className="test" key="test" style={{ lineHeight: '20px' }}>
{0}
</li>
);
function App() {
return (
<div
Expand All @@ -65,32 +159,18 @@ describe('VirtualList', () => {
overflow: 'auto',
}}
>
<VirtualList
jumpIndex={50}
itemSizeGetter={() => {
return 20;
}}
>
{generateData(100)}
</VirtualList>
<VirtualList>{singleItem}</VirtualList>
</div>
);
}

cy.mount(<App />);

cy.get('li')
.should('be.visible')
.first()
.invoke('text')
.then(text => {
expect(parseInt(text, 10)).to.be.above(40);
});
cy.get('.test').should('exist');
});

it('should render single item', () => {
it('should render single item with abnormal jumpIndex', () => {
const singleItem = (
<li key={`${0}-test`} style={{ lineHeight: '20px' }}>
<li className="test" key="test" style={{ lineHeight: '20px' }}>
{0}
</li>
);
Expand All @@ -103,18 +183,12 @@ describe('VirtualList', () => {
overflow: 'auto',
}}
>
<VirtualList
jumpIndex={50}
itemSizeGetter={() => {
return 20;
}}
>
{singleItem}
</VirtualList>
<VirtualList jumpIndex={100}>{singleItem}</VirtualList>
</div>
);
}

cy.mount(<App />);
cy.get('.test').should('exist');
});
});
4 changes: 2 additions & 2 deletions components/virtual-list/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ export interface VirtualListProps extends React.HTMLAttributes<HTMLElement>, Com
threshold?: number;

/**
* 获取item高度的函数
* 获取 item 高度的函数
* @en the function to get the height of the item
*/
itemSizeGetter?: (index?: number) => void;

/**
* 设置跳转位置,需要设置 itemSizeGetter 才能生效, 不设置认为元素等高并取第一个元素高度作为默认高
* 设置跳转位置,需要设置 itemSizeGetter 才能生效不设置认为元素等高并取第一个元素高度作为默认高
* @en set the jump position, need to set itemSizeGetter to take effect, if not set, the element is assumed to be of equal height and the height of the first element is taken as the default height
* @defaultValue 0
*/
Expand Down
44 changes: 28 additions & 16 deletions components/virtual-list/virtual-list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component, type LegacyRef, type CSSProperties, type ReactInstance } from 'react';
import React, {
Component,
Children,
type LegacyRef,
type CSSProperties,
type ReactInstance,
} from 'react';
import cx from 'classnames';
import { polyfill } from 'react-lifecycles-compat';
import { findDOMNode } from 'react-dom';
Expand Down Expand Up @@ -29,8 +35,7 @@ const getOffset = (el: HTMLElement) => {
};

const constrain = (from: number, size: number, { children, minSize }: VirtualListProps) => {
// @ts-expect-error children 未考虑非数组是单个 ReactElement 的情况
const length = children && children.length;
const length = Children.count(children);
size = Math.max(size, minSize!);
if (size > length!) {
size = length!;
Expand Down Expand Up @@ -268,8 +273,7 @@ class VirtualList extends Component<VirtualListProps, VirtualListState> {

const { start, end } = this.getStartAndEnd();
const { pageSize, children } = this.props;
// @ts-expect-error children 未考虑非数组是单个 ReactElement 的情况
const length = children.length;
const length = Children.count(children);
let space = 0;
let from = 0;
let size = 0;
Expand Down Expand Up @@ -303,13 +307,16 @@ class VirtualList extends Component<VirtualListProps, VirtualListState> {
if (!index) {
return 0;
}
if (cache[index] !== null && cache[index] !== undefined) {
if (cache[index] !== null && cache[index] !== undefined && cache[index] > 0) {
return cache[index] || 0;
}

// Find the closest space to index there is a cached value for.
let from = index;
while (from > 0 && (cache[from] === null || cache[from] === undefined)) {
while (
from > 0 &&
(cache[from] === null || cache[from] === undefined || cache[from] === 0)
) {
from--;
}

Expand All @@ -332,13 +339,18 @@ class VirtualList extends Component<VirtualListProps, VirtualListState> {
cacheSizes() {
const { cache } = this;
const { from } = this.state;
// @ts-expect-error items是ReactInstance,但是通过解构同时获取children和props会报错,后续用 in 做类型判断
const { children, props = {} } = this.items!;
const itemEls = children || props.children || [];
let childrenLength = 0;
if ('children' in this.items! && this.items!.children) {
const itemEls = this.items!.children;
childrenLength = itemEls.length;
} else if ('props' in this.items! && this.items!.props.children) {
const itemEls = this.items!.props.children;
childrenLength = Children.count(itemEls);
}

try {
// <Select useVirtual /> 模式下,在快速点击切换Tab的情况下(Select实例快速出现、消失) 有时会出现this.items不存在,导致页面报错。怀疑是Select的异步timer渲染逻辑引起的
for (let i = 0, l = itemEls.length; i < l; ++i) {
for (let i = 0, l = childrenLength; i < l; ++i) {
const ulRef = findDOMNode(this.items) as HTMLElement;
const height = (ulRef.children[i] as HTMLElement).offsetHeight;
if (height > 0) {
Expand All @@ -365,7 +377,7 @@ class VirtualList extends Component<VirtualListProps, VirtualListState> {
if (!this.defaultItemHeight && jumpIndex! > -1) {
const keysList = Object.keys(this.cache);
const len = keysList.length;
const height = this.cache[len - 1];
const height = this.cache[Number(keysList[len - 1])];
this.defaultItemHeight = height;
}

Expand All @@ -383,9 +395,10 @@ class VirtualList extends Component<VirtualListProps, VirtualListState> {
const { from, size } = this.state;
const items = [];

const childrenArray = Children.toArray(children);

for (let i = 0; i < size!; ++i) {
// @ts-expect-error children 未考虑非数组是单个 ReactElement 的情况
items.push(children![from + i]);
items.push(childrenArray![from + i]);
}

return itemsRenderer!(items, c => {
Expand All @@ -396,8 +409,7 @@ class VirtualList extends Component<VirtualListProps, VirtualListState> {

render() {
const { children = [], prefix, className } = this.props;
// @ts-expect-error children 未考虑非数组是单个 ReactElement 的情况
const length = children.length;
const length = Children.count(children);
const { from } = this.state;
const items = this.renderMenuItems();

Expand Down

0 comments on commit 450cf6a

Please sign in to comment.