Skip to content

Commit d626bf9

Browse files
authored
refactor: Virtual always use fake scroll bar (#68)
* add useVirtual * scroll to offsetHeight * support show scroll bar * clean up legacy code * fix mobile touch move * show scrollbar only when scorllable * skip when no height * update test case * fix compile * more test case
1 parent 3c57586 commit d626bf9

File tree

10 files changed

+183
-75
lines changed

10 files changed

+183
-75
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = {
44
...base,
55
rules: {
66
...base.rules,
7+
'arrow-parens': 0,
78
'@typescript-eslint/no-explicit-any': 0,
89
'react/no-did-update-set-state': 0,
910
'react/no-find-dom-node': 0,

examples/basic.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ const Demo = () => {
7272
</label>
7373
))}
7474

75+
<button
76+
type="button"
77+
onClick={() => {
78+
listRef.current.scrollTo(null);
79+
}}
80+
>
81+
Show scroll bar
82+
</button>
7583
<button
7684
type="button"
7785
onClick={() => {

examples/no-virtual.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable arrow-body-style */
12
import * as React from 'react';
23
import List from '../src/List';
34

@@ -24,7 +25,7 @@ const MyItem: React.FC<Item> = ({ id, height }, ref) => {
2425
);
2526
};
2627

27-
const ForwardMyItem = React.forwardRef(MyItem);
28+
const ForwardMyItem = React.forwardRef(MyItem as any);
2829

2930
const data: Item[] = [];
3031
for (let i = 0; i < 100; i += 1) {
@@ -38,6 +39,20 @@ const Demo = () => {
3839
return (
3940
<React.StrictMode>
4041
<div>
42+
<h2>Not Data</h2>
43+
<List
44+
data={null}
45+
itemHeight={30}
46+
height={100}
47+
itemKey="id"
48+
style={{
49+
border: '1px solid red',
50+
boxSizing: 'border-box',
51+
}}
52+
>
53+
{item => <ForwardMyItem {...(item as any)} />}
54+
</List>
55+
4156
<h2>Less Count</h2>
4257
<List
4358
data={data.slice(0, 1)}
@@ -49,7 +64,7 @@ const Demo = () => {
4964
boxSizing: 'border-box',
5065
}}
5166
>
52-
{item => <ForwardMyItem {...item} />}
67+
{item => <ForwardMyItem {...(item as any)} />}
5368
</List>
5469

5570
<h2>Less Item Height</h2>
@@ -63,7 +78,7 @@ const Demo = () => {
6378
boxSizing: 'border-box',
6479
}}
6580
>
66-
{item => <ForwardMyItem {...item} />}
81+
{item => <ForwardMyItem {...(item as any)} />}
6782
</List>
6883

6984
<h2>Without Height</h2>
@@ -76,7 +91,7 @@ const Demo = () => {
7691
boxSizing: 'border-box',
7792
}}
7893
>
79-
{item => <ForwardMyItem {...item} />}
94+
{item => <ForwardMyItem {...(item as any)} />}
8095
</List>
8196
</div>
8297
</React.StrictMode>

examples/switch.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for */
22
import * as React from 'react';
3-
import List from '../src/List';
3+
import List, { ListRef } from '../src/List';
44

55
interface Item {
66
id: number;
@@ -22,7 +22,7 @@ const MyItem: React.FC<Item> = ({ id }, ref) => (
2222
</span>
2323
);
2424

25-
const ForwardMyItem = React.forwardRef(MyItem);
25+
const ForwardMyItem = React.forwardRef(MyItem as any);
2626

2727
function getData(count: number) {
2828
const data: Item[] = [];
@@ -35,8 +35,9 @@ function getData(count: number) {
3535
}
3636

3737
const Demo = () => {
38-
const [height, setHeight] = React.useState(100);
38+
const [height, setHeight] = React.useState(200);
3939
const [data, setData] = React.useState(getData(20));
40+
const listRef = React.useRef<ListRef>();
4041

4142
return (
4243
<React.StrictMode>
@@ -54,6 +55,10 @@ const Demo = () => {
5455
<label>
5556
<input type="radio" name="switch" value={2} />2
5657
</label>
58+
<label>
59+
<input type="radio" name="switch" value={20} />
60+
20
61+
</label>
5762
<label>
5863
<input type="radio" name="switch" value={100} />
5964
100
@@ -66,6 +71,14 @@ const Demo = () => {
6671
<input type="radio" name="switch" value={1000} />
6772
1000
6873
</label>
74+
<button
75+
type="button"
76+
onClick={() => {
77+
listRef.current.scrollTo(null);
78+
}}
79+
>
80+
Show scrollbar
81+
</button>
6982
</span>
7083
<span
7184
onChange={(e: any) => {
@@ -87,16 +100,17 @@ const Demo = () => {
87100
</span>
88101

89102
<List
103+
ref={listRef}
90104
data={data}
91105
height={height}
92-
itemHeight={30}
106+
itemHeight={10}
93107
itemKey="id"
94108
style={{
95109
border: '1px solid red',
96110
boxSizing: 'border-box',
97111
}}
98112
>
99-
{(item, _, props) => <ForwardMyItem {...item} {...props} />}
113+
{(item, _, props) => <ForwardMyItem {...(item as any)} {...props} />}
100114
</List>
101115
</div>
102116
</React.StrictMode>

src/Filler.tsx

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,53 +17,55 @@ interface FillerProps {
1717
/**
1818
* Fill component to provided the scroll content real height.
1919
*/
20-
const Filler: React.FC<FillerProps> = ({
21-
height,
22-
offset,
23-
children,
24-
prefixCls,
25-
onInnerResize,
26-
}): React.ReactElement => {
27-
let outerStyle: React.CSSProperties = {};
20+
const Filler = React.forwardRef(
21+
(
22+
{ height, offset, children, prefixCls, onInnerResize }: FillerProps,
23+
ref: React.Ref<HTMLDivElement>,
24+
) => {
25+
let outerStyle: React.CSSProperties = {};
2826

29-
let innerStyle: React.CSSProperties = {
30-
display: 'flex',
31-
flexDirection: 'column',
32-
};
27+
let innerStyle: React.CSSProperties = {
28+
display: 'flex',
29+
flexDirection: 'column',
30+
};
3331

34-
if (offset !== undefined) {
35-
outerStyle = { height, position: 'relative', overflow: 'hidden' };
32+
if (offset !== undefined) {
33+
outerStyle = { height, position: 'relative', overflow: 'hidden' };
3634

37-
innerStyle = {
38-
...innerStyle,
39-
transform: `translateY(${offset}px)`,
40-
position: 'absolute',
41-
left: 0,
42-
right: 0,
43-
top: 0,
44-
};
45-
}
35+
innerStyle = {
36+
...innerStyle,
37+
transform: `translateY(${offset}px)`,
38+
position: 'absolute',
39+
left: 0,
40+
right: 0,
41+
top: 0,
42+
};
43+
}
4644

47-
return (
48-
<div style={outerStyle}>
49-
<ResizeObserver
50-
onResize={({ offsetHeight }) => {
51-
if (offsetHeight && onInnerResize) {
52-
onInnerResize();
53-
}
54-
}}
55-
>
56-
<div
57-
style={innerStyle}
58-
className={classNames({
59-
[`${prefixCls}-holder-inner`]: prefixCls,
60-
})}
45+
return (
46+
<div style={outerStyle}>
47+
<ResizeObserver
48+
onResize={({ offsetHeight }) => {
49+
if (offsetHeight && onInnerResize) {
50+
onInnerResize();
51+
}
52+
}}
6153
>
62-
{children}
63-
</div>
64-
</ResizeObserver>
65-
</div>
66-
);
67-
};
54+
<div
55+
style={innerStyle}
56+
className={classNames({
57+
[`${prefixCls}-holder-inner`]: prefixCls,
58+
})}
59+
ref={ref}
60+
>
61+
{children}
62+
</div>
63+
</ResizeObserver>
64+
</div>
65+
);
66+
},
67+
);
68+
69+
Filler.displayName = 'Filler';
6870

6971
export default Filler;

src/List.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,17 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
7070
} = props;
7171

7272
// ================================= MISC =================================
73-
const inVirtual =
74-
virtual !== false && height && itemHeight && data && itemHeight * data.length > height;
73+
const useVirtual = !!(virtual !== false && height && itemHeight);
74+
const inVirtual = useVirtual && data && itemHeight * data.length > height;
7575

7676
const [scrollTop, setScrollTop] = useState(0);
7777
const [scrollMoving, setScrollMoving] = useState(false);
7878

7979
const mergedClassName = classNames(prefixCls, className);
8080
const mergedData = data || EMPTY_DATA;
8181
const componentRef = useRef<HTMLDivElement>();
82+
const fillerInnerRef = useRef<HTMLDivElement>();
83+
const scrollBarRef = useRef<any>(); // Hack on scrollbar to enable flash call
8284

8385
// =============================== Item Key ===============================
8486
const getKey = React.useCallback<GetKey<T>>(
@@ -129,7 +131,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
129131

130132
// ========================== Visible Calculation =========================
131133
const { scrollHeight, start, end, offset } = React.useMemo(() => {
132-
if (!inVirtual) {
134+
if (!useVirtual) {
133135
return {
134136
scrollHeight: undefined,
135137
start: 0,
@@ -138,6 +140,16 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
138140
};
139141
}
140142

143+
// Always use virtual scroll bar in avoid shaking
144+
if (!inVirtual) {
145+
return {
146+
scrollHeight: fillerInnerRef.current?.offsetHeight || 0,
147+
start: 0,
148+
end: mergedData.length - 1,
149+
offset: undefined,
150+
};
151+
}
152+
141153
let itemTop = 0;
142154
let startIndex: number;
143155
let startOffset: number;
@@ -184,7 +196,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
184196
end: endIndex,
185197
offset: startOffset,
186198
};
187-
}, [inVirtual, scrollTop, mergedData, heightUpdatedMark, height]);
199+
}, [inVirtual, useVirtual, scrollTop, mergedData, heightUpdatedMark, height]);
188200

189201
rangeRef.current.start = start;
190202
rangeRef.current.end = end;
@@ -227,7 +239,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
227239

228240
// Since this added in global,should use ref to keep update
229241
const [onRawWheel, onFireFoxScroll] = useFrameWheel(
230-
inVirtual,
242+
useVirtual,
231243
isScrollAtTop,
232244
isScrollAtBottom,
233245
offsetY => {
@@ -239,7 +251,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
239251
);
240252

241253
// Mobile touch move
242-
useMobileTouchMove(inVirtual, componentRef, (deltaY, smoothOffset) => {
254+
useMobileTouchMove(useVirtual, componentRef, (deltaY, smoothOffset) => {
243255
if (originScroll(deltaY, smoothOffset)) {
244256
return false;
245257
}
@@ -251,7 +263,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
251263
React.useLayoutEffect(() => {
252264
// Firefox only
253265
function onMozMousePixelScroll(e: Event) {
254-
if (inVirtual) {
266+
if (useVirtual) {
255267
e.preventDefault();
256268
}
257269
}
@@ -265,7 +277,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
265277
componentRef.current.removeEventListener('DOMMouseScroll', onFireFoxScroll as any);
266278
componentRef.current.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll as any);
267279
};
268-
}, [inVirtual]);
280+
}, [useVirtual]);
269281

270282
// ================================= Ref ==================================
271283
const scrollTo = useScrollTo<T>(
@@ -276,6 +288,9 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
276288
getKey,
277289
collectHeight,
278290
syncScrollTop,
291+
() => {
292+
scrollBarRef.current?.delayHidden();
293+
},
279294
);
280295

281296
React.useImperativeHandle(ref, () => ({
@@ -289,7 +304,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
289304
if (height) {
290305
componentStyle = { [fullHeight ? 'height' : 'maxHeight']: height, ...ScrollStyle };
291306

292-
if (inVirtual) {
307+
if (useVirtual) {
293308
componentStyle.overflowY = 'hidden';
294309

295310
if (scrollMoving) {
@@ -318,13 +333,15 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
318333
height={scrollHeight}
319334
offset={offset}
320335
onInnerResize={collectHeight}
336+
ref={fillerInnerRef}
321337
>
322338
{listChildren}
323339
</Filler>
324340
</Component>
325341

326-
{inVirtual && (
342+
{useVirtual && (
327343
<ScrollBar
344+
ref={scrollBarRef}
328345
prefixCls={prefixCls}
329346
scrollTop={scrollTop}
330347
height={height}

0 commit comments

Comments
 (0)