Skip to content

Commit 1d883a8

Browse files
authored
feat: Support scrollTo item index (#8)
* support scroll to item * add test case
1 parent 2739b9b commit 1d883a8

File tree

7 files changed

+421
-150
lines changed

7 files changed

+421
-150
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ module.exports = {
77
'@typescript-eslint/no-explicit-any': 0,
88
'react/no-did-update-set-state': 0,
99
'react/no-find-dom-node': 0,
10+
'no-dupe-class-members': 0,
1011
},
1112
};

examples/basic.tsx

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
1+
/* eslint-disable jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for */
12
import * as React from 'react';
23
import List from '../src/List';
34

45
interface Item {
56
id: number;
67
}
78

8-
const MyItem: React.FC<Item> = ({ id }, ref) => {
9-
return (
10-
<span
11-
ref={ref}
12-
style={{
13-
border: '1px solid gray',
14-
padding: '0 16px',
15-
height: 30,
16-
lineHeight: '30px',
17-
boxSizing: 'border-box',
18-
display: 'inline-block',
19-
}}
20-
>
21-
{id}
22-
</span>
23-
);
24-
};
9+
const MyItem: React.FC<Item> = ({ id }, ref) => (
10+
<span
11+
ref={ref}
12+
style={{
13+
border: '1px solid gray',
14+
padding: '0 16px',
15+
height: 30,
16+
lineHeight: '30px',
17+
boxSizing: 'border-box',
18+
display: 'inline-block',
19+
}}
20+
>
21+
{id}
22+
</span>
23+
);
2524

2625
const ForwardMyItem = React.forwardRef(MyItem);
2726

28-
class TestItem extends React.Component<{ id: number }> {
27+
class TestItem extends React.Component<{ id: number }, {}> {
28+
state = {};
29+
2930
render() {
3031
return <div style={{ lineHeight: '30px' }}>{this.props.id}</div>;
3132
}
@@ -45,6 +46,7 @@ const TYPES = [
4546

4647
const Demo = () => {
4748
const [type, setType] = React.useState('dom');
49+
const listRef = React.useRef<List>(null);
4850

4951
return (
5052
<React.StrictMode>
@@ -65,6 +67,7 @@ const Demo = () => {
6567
))}
6668

6769
<List
70+
ref={listRef}
6871
data={data}
6972
height={200}
7073
itemHeight={30}
@@ -75,16 +78,60 @@ const Demo = () => {
7578
}}
7679
>
7780
{(item, _, props) =>
78-
type === 'dom' ? (
81+
(type === 'dom' ? (
7982
<ForwardMyItem {...item} {...props} />
8083
) : (
8184
<TestItem {...item} {...props} />
82-
)
85+
))
8386
}
8487
</List>
88+
89+
<button
90+
type="button"
91+
onClick={() => {
92+
listRef.current.scrollTo(500);
93+
}}
94+
>
95+
Scroll To 100px
96+
</button>
97+
<button
98+
type="button"
99+
onClick={() => {
100+
listRef.current.scrollTo({
101+
index: 50,
102+
align: 'top',
103+
});
104+
}}
105+
>
106+
Scroll To 50 (top)
107+
</button>
108+
<button
109+
type="button"
110+
onClick={() => {
111+
listRef.current.scrollTo({
112+
index: 50,
113+
align: 'bottom',
114+
});
115+
}}
116+
>
117+
Scroll To 50 (bottom)
118+
</button>
119+
<button
120+
type="button"
121+
onClick={() => {
122+
listRef.current.scrollTo({
123+
index: 50,
124+
align: 'auto',
125+
});
126+
}}
127+
>
128+
Scroll To 50 (auto)
129+
</button>
85130
</div>
86131
</React.StrictMode>
87132
);
88133
};
89134

90135
export default Demo;
136+
137+
/* eslint-enable */

src/List.tsx

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ interface ListState<T> {
107107
* # located item
108108
* The base position item which other items position calculate base on.
109109
*/
110-
class List<T> extends React.Component<ListProps<T>, ListState<T>> {
110+
class List<T = any> extends React.Component<ListProps<T>, ListState<T>> {
111111
static defaultProps = {
112112
itemHeight: 15,
113113
data: [],
@@ -404,7 +404,7 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
404404
return this.getItemKey(item, mergedProps);
405405
};
406406

407-
public getItemKey = (item: T, props?: Partial<ListProps<T>>) => {
407+
public getItemKey = (item: T, props?: Partial<ListProps<T>>): string => {
408408
const { itemKey } = props || this.props;
409409

410410
return typeof itemKey === 'function' ? itemKey(item) : item[itemKey];
@@ -413,8 +413,8 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
413413
/**
414414
* Collect current rendered dom element item heights
415415
*/
416-
public collectItemHeights = () => {
417-
const { startIndex, endIndex } = this.state;
416+
public collectItemHeights = (range?: { startIndex: number; endIndex: number }) => {
417+
const { startIndex, endIndex } = range || this.state;
418418
const { data } = this.props;
419419

420420
// Record here since measure item height will get warning in `render`
@@ -429,8 +429,127 @@ class List<T> extends React.Component<ListProps<T>, ListState<T>> {
429429
}
430430
};
431431

432-
public scrollTo(scrollTop: number) {
433-
this.listRef.current.scrollTop = scrollTop;
432+
public scrollTo(scrollTop: number): void;
433+
434+
public scrollTo(config: { index: number; align?: 'top' | 'bottom' | 'auto' }): void;
435+
436+
public scrollTo(arg0: any) {
437+
// Number top
438+
if (typeof arg0 === 'object') {
439+
const { isVirtual } = this.state;
440+
const { height, itemHeight, data } = this.props;
441+
const { index, align = 'auto' } = arg0;
442+
const itemCount = Math.ceil(height / itemHeight);
443+
const item = data[index];
444+
if (item) {
445+
const { clientHeight } = this.listRef.current;
446+
447+
if (isVirtual) {
448+
// Calculate related data
449+
const { itemIndex, itemOffsetPtg, startIndex, endIndex } = this.state;
450+
451+
const relativeLocatedItemTop = getItemRelativeTop({
452+
itemIndex,
453+
itemOffsetPtg,
454+
itemElementHeights: this.itemElementHeights,
455+
scrollPtg: getElementScrollPercentage(this.listRef.current),
456+
clientHeight,
457+
getItemKey: this.getIndexKey,
458+
});
459+
460+
// We will force render related items to collect height for re-location
461+
this.setState(
462+
{
463+
startIndex: Math.max(0, index - itemCount),
464+
endIndex: Math.min(data.length - 1, index + itemCount),
465+
},
466+
() => {
467+
this.collectItemHeights();
468+
469+
// Calculate related top
470+
let relativeTop: number;
471+
let mergedAlgin = align;
472+
473+
if (align === 'auto') {
474+
let shouldChange = true;
475+
476+
// Check if exist in the visible range
477+
if (Math.abs(itemIndex - index) < itemCount) {
478+
let itemTop = relativeLocatedItemTop;
479+
if (index < itemIndex) {
480+
for (let i = index; i < itemIndex; i += 1) {
481+
const eleKey = this.getIndexKey(i);
482+
itemTop -= this.itemElementHeights[eleKey] || 0;
483+
}
484+
} else {
485+
for (let i = itemIndex; i <= index; i += 1) {
486+
const eleKey = this.getIndexKey(i);
487+
itemTop += this.itemElementHeights[eleKey] || 0;
488+
}
489+
}
490+
491+
shouldChange = itemTop <= 0 || itemTop >= clientHeight;
492+
}
493+
494+
if (shouldChange) {
495+
// Out of range will fall back to position align
496+
mergedAlgin = index < itemIndex ? 'top' : 'bottom';
497+
} else {
498+
this.setState({
499+
startIndex,
500+
endIndex,
501+
});
502+
return;
503+
}
504+
}
505+
506+
// Align with position should make scroll happen
507+
if (mergedAlgin === 'top') {
508+
relativeTop = 0;
509+
} else if (mergedAlgin === 'bottom') {
510+
const eleKey = this.getItemKey(item);
511+
512+
relativeTop = clientHeight - this.itemElementHeights[eleKey] || 0;
513+
}
514+
515+
this.internalScrollTo({
516+
itemIndex: index,
517+
relativeTop,
518+
});
519+
},
520+
);
521+
} else {
522+
// Raw list without virtual scroll set position directly
523+
this.collectItemHeights({ startIndex: 0, endIndex: data.length - 1 });
524+
let mergedAlgin = align;
525+
526+
// Collection index item position
527+
const indexItemHeight = this.itemElementHeights[this.getIndexKey(index)];
528+
let itemTop = 0;
529+
for (let i = 0; i < index; i += 1) {
530+
const eleKey = this.getIndexKey(i);
531+
itemTop += this.itemElementHeights[eleKey] || 0;
532+
}
533+
const itemBottom = itemTop + indexItemHeight;
534+
535+
if (mergedAlgin === 'auto') {
536+
if (itemTop < this.listRef.current.scrollTop) {
537+
mergedAlgin = 'top';
538+
} else if (itemBottom > this.listRef.current.scrollTop + clientHeight) {
539+
mergedAlgin = 'bottom';
540+
}
541+
}
542+
543+
if (mergedAlgin === 'top') {
544+
this.listRef.current.scrollTop = itemTop;
545+
} else if (mergedAlgin === 'bottom') {
546+
this.listRef.current.scrollTop = itemTop - (clientHeight - indexItemHeight);
547+
}
548+
}
549+
}
550+
} else {
551+
this.listRef.current.scrollTop = arg0;
552+
}
434553
}
435554

436555
public internalScrollTo(relativeScroll: RelativeScroll): void {

0 commit comments

Comments
 (0)