Skip to content

Commit

Permalink
feat: scrollWidth support (#210)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
zombieJ committed Aug 16, 2023
1 parent b458210 commit 05ea3cb
Show file tree
Hide file tree
Showing 9 changed files with 526 additions and 218 deletions.
3 changes: 3 additions & 0 deletions docs/demo/horizontal-scroll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## horizontal scroll

<code src="../../examples/horizontal-scroll.tsx">
78 changes: 78 additions & 0 deletions examples/horizontal-scroll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as React from 'react';
import List from '../src/List';

interface Item {
id: number;
height: number;
}

const MyItem: React.ForwardRefRenderFunction<HTMLElement, Item> = ({ id, height }, ref) => {
return (
<span
ref={ref}
style={{
border: '1px solid gray',
padding: '0 16px',
height,
lineHeight: '30px',
boxSizing: 'border-box',
display: 'inline-block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{id} {'longText '.repeat(100)}
</span>
);
};

const ForwardMyItem = React.forwardRef(MyItem);

const data: Item[] = [];
for (let i = 0; i < 100; i += 1) {
data.push({
id: i,
height: 30,
});
}

const Demo = () => {
const [rtl, setRTL] = React.useState(false);
return (
<React.StrictMode>
<div>
<button
onClick={() => {
setRTL(!rtl);
}}
>
RTL: {String(rtl)}
</button>

<div style={{ width: 500, margin: 64 }}>
<List
direction={rtl ? 'rtl' : 'ltr'}
data={data}
height={300}
itemHeight={30}
itemKey="id"
scrollWidth={2328}
// scrollWidth={100}
style={{
border: '1px solid red',
boxSizing: 'border-box',
}}
onScroll={(e) => {
console.log('Scroll:', e);
}}
>
{(item) => <ForwardMyItem {...item} />}
</List>
</div>
</div>
</React.StrictMode>
);
};

export default Demo;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
"react-dom": "*"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^12.1.5",
"@types/classnames": "^2.2.10",
"@types/enzyme": "^3.10.5",
"@types/jest": "^25.1.3",
Expand Down
31 changes: 26 additions & 5 deletions src/Filler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,36 @@ interface FillerProps {
/** Virtual filler height. Should be `count * itemMinHeight` */
height: number;
/** Set offset of visible items. Should be the top of start item position */
offset?: number;
offsetY?: number;
offsetX?: number;

scrollWidth?: number;

children: React.ReactNode;

onInnerResize?: () => void;

innerProps?: InnerProps;

rtl: boolean;
}

/**
* Fill component to provided the scroll content real height.
*/
const Filler = React.forwardRef(
(
{ height, offset, children, prefixCls, onInnerResize, innerProps }: FillerProps,
{
height,
offsetY,
offsetX,
scrollWidth,
children,
prefixCls,
onInnerResize,
innerProps,
rtl,
}: FillerProps,
ref: React.Ref<HTMLDivElement>,
) => {
let outerStyle: React.CSSProperties = {};
Expand All @@ -33,12 +48,18 @@ const Filler = React.forwardRef(
flexDirection: 'column',
};

if (offset !== undefined) {
outerStyle = { height, position: 'relative', overflow: 'hidden' };
if (offsetY !== undefined) {
outerStyle = {
height,
width: scrollWidth,
minWidth: '100%',
position: 'relative',
overflow: 'hidden',
};

innerStyle = {
...innerStyle,
transform: `translateY(${offset}px)`,
transform: `translate(${rtl ? offsetX : -offsetX}px, ${offsetY}px)`,
position: 'absolute',
left: 0,
right: 0,
Expand Down
121 changes: 79 additions & 42 deletions src/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useRef, useState } from 'react';
import classNames from 'classnames';
import Filler from './Filler';
import type { InnerProps } from './Filler';
import type { ScrollBarDirectionType } from './ScrollBar';
import type { ScrollBarDirectionType, ScrollBarRef } from './ScrollBar';
import ScrollBar from './ScrollBar';
import type { RenderFunc, SharedConfig, GetKey } from './interface';
import useChildren from './hooks/useChildren';
Expand Down Expand Up @@ -52,6 +52,12 @@ export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'
/** Set `false` will always use real scroll instead of virtual one */
virtual?: boolean;
direction?: ScrollBarDirectionType;
/**
* By default `scrollWidth` is same as container.
* When set this, it will show the horizontal scrollbar and
* `scrollWidth` will be used as the real width instead of container width.
*/
scrollWidth?: number;

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

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

const mergedClassName = classNames(
prefixCls,
{ [`${prefixCls}-rtl`]: direction === 'rtl' },
className,
);
const mergedClassName = classNames(prefixCls, { [`${prefixCls}-rtl`]: isRTL }, className);
const mergedData = data || EMPTY_DATA;
const componentRef = useRef<HTMLDivElement>();
const fillerInnerRef = useRef<HTMLDivElement>();
const scrollBarRef = useRef<any>(); // Hack on scrollbar to enable flash call
const scrollBarRef = useRef<ScrollBarRef>(); // Hack on scrollbar to enable flash call

// =============================== Item Key ===============================

const [offsetTop, setOffsetTop] = useState(0);
const [offsetLeft, setOffsetLeft] = useState(0);
const [scrollMoving, setScrollMoving] = useState(false);

const onScrollbarStartMove = () => {
setScrollMoving(true);
};
const onScrollbarStopMove = () => {
setScrollMoving(false);
};

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

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

// Check item top in the range
if (currentItemBottom >= scrollTop && startIndex === undefined) {
if (currentItemBottom >= offsetTop && startIndex === undefined) {
startIndex = i;
startOffset = itemTop;
}

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

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

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

const isScrollAtTop = scrollTop <= 0;
const isScrollAtBottom = scrollTop >= maxScrollHeight;
const isScrollAtTop = offsetTop <= 0;
const isScrollAtBottom = offsetTop >= maxScrollHeight;

const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);

// ================================ Scroll ================================
function onScrollBar(newScrollTop: number) {
const newTop = newScrollTop;
syncScrollTop(newTop);
function onScrollBar(newScrollOffset: number, horizontal?: boolean) {
const newOffset = newScrollOffset;

if (horizontal) {
setOffsetLeft(newOffset);
} else {
syncScrollTop(newOffset);
}
}

// When data size reduce. It may trigger native scroll event back to fit scroll position
function onFallbackScroll(e: React.UIEvent<HTMLDivElement>) {
const { scrollTop: newScrollTop } = e.currentTarget;
if (newScrollTop !== scrollTop) {
if (newScrollTop !== offsetTop) {
syncScrollTop(newScrollTop);
}

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

componentRef.current.addEventListener('wheel', onRawWheel);
componentRef.current.addEventListener('DOMMouseScroll', onFireFoxScroll as any);
componentRef.current.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
const componentEle = componentRef.current;
componentEle.addEventListener('wheel', onRawWheel);
componentEle.addEventListener('DOMMouseScroll', onFireFoxScroll as any);
componentEle.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);

return () => {
if (componentRef.current) {
componentRef.current.removeEventListener('wheel', onRawWheel);
componentRef.current.removeEventListener('DOMMouseScroll', onFireFoxScroll as any);
componentRef.current.removeEventListener(
'MozMousePixelScroll',
onMozMousePixelScroll as any,
);
}
componentEle.removeEventListener('wheel', onRawWheel);
componentEle.removeEventListener('DOMMouseScroll', onFireFoxScroll as any);
componentEle.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll as any);
};
}, [useVirtual]);

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

if (scrollWidth) {
componentStyle.overflowX = 'hidden';
}

if (scrollMoving) {
componentStyle.pointerEvents = 'none';
}
}
}

const containerProps: React.HTMLAttributes<HTMLDivElement> = {};
if (isRTL) {
containerProps.dir = 'rtl';
}

return (
<div
style={{
...style,
position: 'relative',
}}
className={mergedClassName}
{...containerProps}
{...restProps}
>
<Component
Expand All @@ -363,10 +388,13 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
<Filler
prefixCls={prefixCls}
height={scrollHeight}
offset={offset}
offsetX={offsetLeft}
offsetY={offset}
scrollWidth={scrollWidth}
onInnerResize={collectHeight}
ref={fillerInnerRef}
innerProps={innerProps}
rtl={isRTL}
>
{listChildren}
</Filler>
Expand All @@ -376,18 +404,27 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
<ScrollBar
ref={scrollBarRef}
prefixCls={prefixCls}
scrollTop={scrollTop}
scrollOffset={offsetTop}
scrollRange={scrollHeight}
rtl={isRTL}
onScroll={onScrollBar}
onStartMove={onScrollbarStartMove}
onStopMove={onScrollbarStopMove}
height={height}
scrollHeight={scrollHeight}
count={mergedData.length}
direction={direction}
/>
)}

{useVirtual && scrollWidth && (
<ScrollBar
ref={scrollBarRef}
prefixCls={prefixCls}
scrollOffset={offsetLeft}
scrollRange={scrollWidth}
rtl={isRTL}
onScroll={onScrollBar}
onStartMove={() => {
setScrollMoving(true);
}}
onStopMove={() => {
setScrollMoving(false);
}}
onStartMove={onScrollbarStartMove}
onStopMove={onScrollbarStopMove}
horizontal
/>
)}
</div>
Expand Down
Loading

0 comments on commit 05ea3cb

Please sign in to comment.