Skip to content

Commit 05ea3cb

Browse files
authored
feat: scrollWidth support (#210)
* refactor: scrollbar * refactor: merge func * chore: init hor prop * chore: xy * chore: scrollbar rtl * chore: size of it * chore: base rtl * chore: rtl * test: add test case * test: fix test case
1 parent b458210 commit 05ea3cb

File tree

9 files changed

+526
-218
lines changed

9 files changed

+526
-218
lines changed

docs/demo/horizontal-scroll.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## horizontal scroll
2+
3+
<code src="../../examples/horizontal-scroll.tsx">

examples/horizontal-scroll.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import * as React from 'react';
2+
import List from '../src/List';
3+
4+
interface Item {
5+
id: number;
6+
height: number;
7+
}
8+
9+
const MyItem: React.ForwardRefRenderFunction<HTMLElement, Item> = ({ id, height }, ref) => {
10+
return (
11+
<span
12+
ref={ref}
13+
style={{
14+
border: '1px solid gray',
15+
padding: '0 16px',
16+
height,
17+
lineHeight: '30px',
18+
boxSizing: 'border-box',
19+
display: 'inline-block',
20+
whiteSpace: 'nowrap',
21+
overflow: 'hidden',
22+
textOverflow: 'ellipsis',
23+
}}
24+
>
25+
{id} {'longText '.repeat(100)}
26+
</span>
27+
);
28+
};
29+
30+
const ForwardMyItem = React.forwardRef(MyItem);
31+
32+
const data: Item[] = [];
33+
for (let i = 0; i < 100; i += 1) {
34+
data.push({
35+
id: i,
36+
height: 30,
37+
});
38+
}
39+
40+
const Demo = () => {
41+
const [rtl, setRTL] = React.useState(false);
42+
return (
43+
<React.StrictMode>
44+
<div>
45+
<button
46+
onClick={() => {
47+
setRTL(!rtl);
48+
}}
49+
>
50+
RTL: {String(rtl)}
51+
</button>
52+
53+
<div style={{ width: 500, margin: 64 }}>
54+
<List
55+
direction={rtl ? 'rtl' : 'ltr'}
56+
data={data}
57+
height={300}
58+
itemHeight={30}
59+
itemKey="id"
60+
scrollWidth={2328}
61+
// scrollWidth={100}
62+
style={{
63+
border: '1px solid red',
64+
boxSizing: 'border-box',
65+
}}
66+
onScroll={(e) => {
67+
console.log('Scroll:', e);
68+
}}
69+
>
70+
{(item) => <ForwardMyItem {...item} />}
71+
</List>
72+
</div>
73+
</div>
74+
</React.StrictMode>
75+
);
76+
};
77+
78+
export default Demo;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
"react-dom": "*"
4343
},
4444
"devDependencies": {
45+
"@testing-library/jest-dom": "^5.17.0",
46+
"@testing-library/react": "^12.1.5",
4547
"@types/classnames": "^2.2.10",
4648
"@types/enzyme": "^3.10.5",
4749
"@types/jest": "^25.1.3",

src/Filler.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,36 @@ interface FillerProps {
99
/** Virtual filler height. Should be `count * itemMinHeight` */
1010
height: number;
1111
/** Set offset of visible items. Should be the top of start item position */
12-
offset?: number;
12+
offsetY?: number;
13+
offsetX?: number;
14+
15+
scrollWidth?: number;
1316

1417
children: React.ReactNode;
1518

1619
onInnerResize?: () => void;
1720

1821
innerProps?: InnerProps;
22+
23+
rtl: boolean;
1924
}
2025

2126
/**
2227
* Fill component to provided the scroll content real height.
2328
*/
2429
const Filler = React.forwardRef(
2530
(
26-
{ height, offset, children, prefixCls, onInnerResize, innerProps }: FillerProps,
31+
{
32+
height,
33+
offsetY,
34+
offsetX,
35+
scrollWidth,
36+
children,
37+
prefixCls,
38+
onInnerResize,
39+
innerProps,
40+
rtl,
41+
}: FillerProps,
2742
ref: React.Ref<HTMLDivElement>,
2843
) => {
2944
let outerStyle: React.CSSProperties = {};
@@ -33,12 +48,18 @@ const Filler = React.forwardRef(
3348
flexDirection: 'column',
3449
};
3550

36-
if (offset !== undefined) {
37-
outerStyle = { height, position: 'relative', overflow: 'hidden' };
51+
if (offsetY !== undefined) {
52+
outerStyle = {
53+
height,
54+
width: scrollWidth,
55+
minWidth: '100%',
56+
position: 'relative',
57+
overflow: 'hidden',
58+
};
3859

3960
innerStyle = {
4061
...innerStyle,
41-
transform: `translateY(${offset}px)`,
62+
transform: `translate(${rtl ? offsetX : -offsetX}px, ${offsetY}px)`,
4263
position: 'absolute',
4364
left: 0,
4465
right: 0,

src/List.tsx

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useRef, useState } from 'react';
33
import classNames from 'classnames';
44
import Filler from './Filler';
55
import type { InnerProps } from './Filler';
6-
import type { ScrollBarDirectionType } from './ScrollBar';
6+
import type { ScrollBarDirectionType, ScrollBarRef } from './ScrollBar';
77
import ScrollBar from './ScrollBar';
88
import type { RenderFunc, SharedConfig, GetKey } from './interface';
99
import useChildren from './hooks/useChildren';
@@ -52,6 +52,12 @@ export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'
5252
/** Set `false` will always use real scroll instead of virtual one */
5353
virtual?: boolean;
5454
direction?: ScrollBarDirectionType;
55+
/**
56+
* By default `scrollWidth` is same as container.
57+
* When set this, it will show the horizontal scrollbar and
58+
* `scrollWidth` will be used as the real width instead of container width.
59+
*/
60+
scrollWidth?: number;
5561

5662
onScroll?: React.UIEventHandler<HTMLElement>;
5763
/** Trigger when render list item changed */
@@ -74,6 +80,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
7480
itemKey,
7581
virtual,
7682
direction,
83+
scrollWidth,
7784
component: Component = 'div',
7885
onScroll,
7986
onVisibleChange,
@@ -84,19 +91,26 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
8491
// ================================= MISC =================================
8592
const useVirtual = !!(virtual !== false && height && itemHeight);
8693
const inVirtual = useVirtual && data && itemHeight * data.length > height;
94+
const isRTL = direction === 'rtl';
8795

88-
const [scrollTop, setScrollTop] = useState(0);
89-
const [scrollMoving, setScrollMoving] = useState(false);
90-
91-
const mergedClassName = classNames(
92-
prefixCls,
93-
{ [`${prefixCls}-rtl`]: direction === 'rtl' },
94-
className,
95-
);
96+
const mergedClassName = classNames(prefixCls, { [`${prefixCls}-rtl`]: isRTL }, className);
9697
const mergedData = data || EMPTY_DATA;
9798
const componentRef = useRef<HTMLDivElement>();
9899
const fillerInnerRef = useRef<HTMLDivElement>();
99-
const scrollBarRef = useRef<any>(); // Hack on scrollbar to enable flash call
100+
const scrollBarRef = useRef<ScrollBarRef>(); // Hack on scrollbar to enable flash call
101+
102+
// =============================== Item Key ===============================
103+
104+
const [offsetTop, setOffsetTop] = useState(0);
105+
const [offsetLeft, setOffsetLeft] = useState(0);
106+
const [scrollMoving, setScrollMoving] = useState(false);
107+
108+
const onScrollbarStartMove = () => {
109+
setScrollMoving(true);
110+
};
111+
const onScrollbarStopMove = () => {
112+
setScrollMoving(false);
113+
};
100114

101115
// =============================== Item Key ===============================
102116
const getKey = React.useCallback<GetKey<T>>(
@@ -115,7 +129,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
115129

116130
// ================================ Scroll ================================
117131
function syncScrollTop(newTop: number | ((prev: number) => number)) {
118-
setScrollTop((origin) => {
132+
setOffsetTop((origin) => {
119133
let value: number;
120134
if (typeof newTop === 'function') {
121135
value = newTop(origin);
@@ -180,13 +194,13 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
180194
const currentItemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
181195

182196
// Check item top in the range
183-
if (currentItemBottom >= scrollTop && startIndex === undefined) {
197+
if (currentItemBottom >= offsetTop && startIndex === undefined) {
184198
startIndex = i;
185199
startOffset = itemTop;
186200
}
187201

188202
// Check item bottom in the range. We will render additional one item for motion usage
189-
if (currentItemBottom > scrollTop + height && endIndex === undefined) {
203+
if (currentItemBottom > offsetTop + height && endIndex === undefined) {
190204
endIndex = i;
191205
}
192206

@@ -213,7 +227,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
213227
end: endIndex,
214228
offset: startOffset,
215229
};
216-
}, [inVirtual, useVirtual, scrollTop, mergedData, heightUpdatedMark, height]);
230+
}, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height]);
217231

218232
rangeRef.current.start = start;
219233
rangeRef.current.end = end;
@@ -232,21 +246,26 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
232246
return newTop;
233247
}
234248

235-
const isScrollAtTop = scrollTop <= 0;
236-
const isScrollAtBottom = scrollTop >= maxScrollHeight;
249+
const isScrollAtTop = offsetTop <= 0;
250+
const isScrollAtBottom = offsetTop >= maxScrollHeight;
237251

238252
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
239253

240254
// ================================ Scroll ================================
241-
function onScrollBar(newScrollTop: number) {
242-
const newTop = newScrollTop;
243-
syncScrollTop(newTop);
255+
function onScrollBar(newScrollOffset: number, horizontal?: boolean) {
256+
const newOffset = newScrollOffset;
257+
258+
if (horizontal) {
259+
setOffsetLeft(newOffset);
260+
} else {
261+
syncScrollTop(newOffset);
262+
}
244263
}
245264

246265
// When data size reduce. It may trigger native scroll event back to fit scroll position
247266
function onFallbackScroll(e: React.UIEvent<HTMLDivElement>) {
248267
const { scrollTop: newScrollTop } = e.currentTarget;
249-
if (newScrollTop !== scrollTop) {
268+
if (newScrollTop !== offsetTop) {
250269
syncScrollTop(newScrollTop);
251270
}
252271

@@ -285,19 +304,15 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
285304
}
286305
}
287306

288-
componentRef.current.addEventListener('wheel', onRawWheel);
289-
componentRef.current.addEventListener('DOMMouseScroll', onFireFoxScroll as any);
290-
componentRef.current.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
307+
const componentEle = componentRef.current;
308+
componentEle.addEventListener('wheel', onRawWheel);
309+
componentEle.addEventListener('DOMMouseScroll', onFireFoxScroll as any);
310+
componentEle.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
291311

292312
return () => {
293-
if (componentRef.current) {
294-
componentRef.current.removeEventListener('wheel', onRawWheel);
295-
componentRef.current.removeEventListener('DOMMouseScroll', onFireFoxScroll as any);
296-
componentRef.current.removeEventListener(
297-
'MozMousePixelScroll',
298-
onMozMousePixelScroll as any,
299-
);
300-
}
313+
componentEle.removeEventListener('wheel', onRawWheel);
314+
componentEle.removeEventListener('DOMMouseScroll', onFireFoxScroll as any);
315+
componentEle.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll as any);
301316
};
302317
}, [useVirtual]);
303318

@@ -339,19 +354,29 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
339354
if (useVirtual) {
340355
componentStyle.overflowY = 'hidden';
341356

357+
if (scrollWidth) {
358+
componentStyle.overflowX = 'hidden';
359+
}
360+
342361
if (scrollMoving) {
343362
componentStyle.pointerEvents = 'none';
344363
}
345364
}
346365
}
347366

367+
const containerProps: React.HTMLAttributes<HTMLDivElement> = {};
368+
if (isRTL) {
369+
containerProps.dir = 'rtl';
370+
}
371+
348372
return (
349373
<div
350374
style={{
351375
...style,
352376
position: 'relative',
353377
}}
354378
className={mergedClassName}
379+
{...containerProps}
355380
{...restProps}
356381
>
357382
<Component
@@ -363,10 +388,13 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
363388
<Filler
364389
prefixCls={prefixCls}
365390
height={scrollHeight}
366-
offset={offset}
391+
offsetX={offsetLeft}
392+
offsetY={offset}
393+
scrollWidth={scrollWidth}
367394
onInnerResize={collectHeight}
368395
ref={fillerInnerRef}
369396
innerProps={innerProps}
397+
rtl={isRTL}
370398
>
371399
{listChildren}
372400
</Filler>
@@ -376,18 +404,27 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
376404
<ScrollBar
377405
ref={scrollBarRef}
378406
prefixCls={prefixCls}
379-
scrollTop={scrollTop}
407+
scrollOffset={offsetTop}
408+
scrollRange={scrollHeight}
409+
rtl={isRTL}
410+
onScroll={onScrollBar}
411+
onStartMove={onScrollbarStartMove}
412+
onStopMove={onScrollbarStopMove}
380413
height={height}
381-
scrollHeight={scrollHeight}
382-
count={mergedData.length}
383-
direction={direction}
414+
/>
415+
)}
416+
417+
{useVirtual && scrollWidth && (
418+
<ScrollBar
419+
ref={scrollBarRef}
420+
prefixCls={prefixCls}
421+
scrollOffset={offsetLeft}
422+
scrollRange={scrollWidth}
423+
rtl={isRTL}
384424
onScroll={onScrollBar}
385-
onStartMove={() => {
386-
setScrollMoving(true);
387-
}}
388-
onStopMove={() => {
389-
setScrollMoving(false);
390-
}}
425+
onStartMove={onScrollbarStartMove}
426+
onStopMove={onScrollbarStopMove}
427+
horizontal
391428
/>
392429
)}
393430
</div>

0 commit comments

Comments
 (0)