Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default () => (
| `group` | Group configuration. | `{ key: (item: T) => K; title: (groupKey: K, items: T[]) => React.ReactNode }` | - |
| `sticky` | Enable sticky group headers. | `boolean` | `false` |
| `virtual` | Enable virtual scrolling. | `boolean` | `true` |
| `scrollWidth` | Real content width. Only takes effect when `virtual` is enabled; a horizontal scrollbar shows up once set. | `number` | - |
| `direction` | Layout direction; set `rtl` for right-to-left. | `'ltr' \| 'rtl'` | `'ltr'` |
| `onScroll` | Triggered when the inner scroll container scrolls. | `React.UIEventHandler<HTMLElement>` | - |
| `prefixCls` | Component class name prefix. | `string` | `rc-listy` |
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default () => (
| `group` | 分组配置。 | `{ key: (item: T) => K; title: (groupKey: K, items: T[]) => React.ReactNode }` | - |
| `sticky` | 启用粘性组头。 | `boolean` | `false` |
| `virtual` | 启用虚拟滚动。 | `boolean` | `true` |
| `scrollWidth` | 内容实际宽度,仅在 `virtual` 开启时生效。设置后会展示横向滚动条。 | `number` | - |
| `direction` | 布局方向,设为 `rtl` 时启用从右到左布局。 | `'ltr' \| 'rtl'` | `'ltr'` |
| `onScroll` | 内部滚动容器滚动时触发。 | `React.UIEventHandler<HTMLElement>` | - |
| `prefixCls` | 组件样式前缀。 | `string` | `rc-listy` |
Expand Down
8 changes: 8 additions & 0 deletions docs/demos/horizontal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Horizontal Scroll
nav:
title: Demo
path: /demo
---

<code src="../examples/horizontal.tsx"></code>
165 changes: 165 additions & 0 deletions docs/examples/horizontal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React, { useRef, useState } from 'react';
import Listy, { type ListyRef } from '@rc-component/listy';
import '../../assets/index.less';

const COLUMNS = [
{ key: 'id', title: 'ID', width: 80 },
{ key: 'name', title: 'Name', width: 200 },
{ key: 'email', title: 'Email', width: 260 },
{ key: 'address', title: 'Address', width: 320 },
{ key: 'note', title: 'Note', width: 240 },
];

const SCROLL_WIDTH = COLUMNS.reduce((acc, col) => acc + col.width, 0);
const VIEWPORT_WIDTH = 480;
const GROUP_SIZE = 10;
const TOTAL = 200;

const items = Array.from({ length: TOTAL }, (_, index) => ({
id: index + 1,
index,
groupIndex: Math.floor(index / GROUP_SIZE),
}));

const cellStyle: React.CSSProperties = {
flex: 'none',
padding: '0 12px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
borderInlineEnd: '1px solid #f0f0f0',
boxSizing: 'border-box',
};

export default () => {
const listRef = useRef<ListyRef>(null);
const [virtual, setVirtual] = useState(true);
const [sticky, setSticky] = useState(true);
const [grouped, setGrouped] = useState(true);
const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr');
const [scrollWidth, setScrollWidth] = useState(SCROLL_WIDTH);

const renderRow = (cells: React.ReactNode[], background: string) => (
<div
style={{
display: 'flex',
width: scrollWidth,
height: 32,
lineHeight: '32px',
background,
borderBottom: '1px solid #efefef',
}}
>
{COLUMNS.map((col, i) => (
<div key={col.key} style={{ ...cellStyle, width: col.width }}>
{cells[i]}
</div>
))}
</div>
);

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button type="button" onClick={() => setVirtual((v) => !v)}>
virtual: {String(virtual)}
</button>
<button type="button" onClick={() => setGrouped((g) => !g)}>
group: {String(grouped)}
</button>
<button type="button" onClick={() => setSticky((s) => !s)}>
sticky: {String(sticky)}
</button>
<button
type="button"
onClick={() => setDirection((d) => (d === 'ltr' ? 'rtl' : 'ltr'))}
>
direction: {direction}
</button>
<label>
scrollWidth:{' '}
<input
type="number"
step={100}
value={scrollWidth}
style={{ width: 80 }}
onChange={(e) => setScrollWidth(Number(e.target.value))}
/>
</label>
</div>

<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button
type="button"
onClick={() => listRef.current?.scrollTo({ left: 0 })}
>
scrollTo left: 0
</button>
<button
type="button"
onClick={() => listRef.current?.scrollTo({ left: 300 })}
>
scrollTo left: 300
</button>
<button
type="button"
onClick={() =>
listRef.current?.scrollTo({ left: scrollWidth, top: 0 })
}
>
scrollTo left: end
</button>
<button
type="button"
onClick={() => listRef.current?.scrollTo({ key: 150 })}
>
scrollTo key: 150
</button>
</div>

<div style={{ width: VIEWPORT_WIDTH, border: '1px solid #d9d9d9' }}>
<Listy
ref={listRef}
height={320}
itemHeight={32}
items={items}
virtual={virtual}
direction={direction}
rowKey="id"
sticky={sticky}
scrollWidth={scrollWidth}
group={
grouped
? {
key: (item) => item.groupIndex,
title: (groupKey) =>
renderRow(
[
`G${groupKey}`,
`Group ${groupKey}`,
'',
'header should follow horizontal scroll',
'',
],
'#f5f5f5',
),
}
: undefined
}
itemRender={(item) =>
renderRow(
[
item.id,
`User ${item.index}`,
`user${item.index}@example.com`,
`No.${item.index} Some Long Street, Some City`,
`note-${item.index}`,
],
'#fff',
)
}
/>
</div>
</div>
);
};
2 changes: 2 additions & 0 deletions src/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface ListyProps<T, K extends React.Key = React.Key> {
height?: number;
group?: Group<T, K>;
virtual?: boolean;
scrollWidth?: number;
direction?: 'ltr' | 'rtl';
prefixCls?: string;
rowKey: RowKey<T>;
Expand All @@ -67,6 +68,7 @@ export interface ListComponentProps<T, K extends React.Key = React.Key> {
itemHeight?: number;
height?: number;
group?: Group<T, K>;
scrollWidth?: number;
direction?: 'ltr' | 'rtl';
prefixCls: string;
rowKey: RowKey<T>;
Expand Down
14 changes: 12 additions & 2 deletions src/VirtualList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function VirtualList<T, K extends React.Key = React.Key>(
prefixCls,
rowKey,
sticky,
scrollWidth,
direction,
classNames,
styles,
Expand Down Expand Up @@ -137,6 +138,12 @@ function VirtualList<T, K extends React.Key = React.Key>(
[scrollTo],
);

// ============================== Width ===============================
const rowStyle = React.useMemo<React.CSSProperties | undefined>(
() => (scrollWidth ? { width: scrollWidth } : undefined),
[scrollWidth],
);

// ============================== Sticky ==============================
const extraRender = useStickyGroupHeader<T, K>({
enabled: !!(sticky && group),
Expand All @@ -145,6 +152,7 @@ function VirtualList<T, K extends React.Key = React.Key>(
groupKeyToItems,
prefixCls,
listRef,
scrollWidth,
headerClassName: classNames?.groupHeader,
headerStyle: styles?.groupHeader,
});
Expand All @@ -161,7 +169,7 @@ function VirtualList<T, K extends React.Key = React.Key>(
groupItems={groupItems}
prefixCls={prefixCls}
className={classNames?.groupHeader}
style={styles?.groupHeader}
style={{ ...styles?.groupHeader, ...rowStyle }}
/>
);
},
Expand All @@ -170,6 +178,7 @@ function VirtualList<T, K extends React.Key = React.Key>(
group,
groupKeyToItems,
prefixCls,
rowStyle,
styles?.groupHeader,
],
);
Expand All @@ -185,6 +194,7 @@ function VirtualList<T, K extends React.Key = React.Key>(
itemHeight={itemHeight}
itemKey="taggedKey"
onScroll={onScroll}
scrollWidth={scrollWidth}
prefixCls={prefixCls}
virtual
extraRender={extraRender}
Expand All @@ -197,7 +207,7 @@ function VirtualList<T, K extends React.Key = React.Key>(
) : (
<div
className={clsx(`${prefixCls}-item`, classNames?.item)}
style={styles?.item}
style={{ ...styles?.item, ...rowStyle }}
>
{itemRender(row.item, row.index)}
</div>
Expand Down
14 changes: 12 additions & 2 deletions src/VirtualList/useStickyGroupHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface StickyHeaderParams<T, K extends React.Key = React.Key> {
groupKeyToItems: Map<K, T[]>;
prefixCls: string;
listRef: React.RefObject<RcVirtualListRef | null>;
scrollWidth?: number;
headerClassName?: string;
headerStyle?: React.CSSProperties;
}
Expand All @@ -63,14 +64,15 @@ export default function useStickyGroupHeader<
groupKeyToItems,
prefixCls,
listRef,
scrollWidth,
headerClassName,
headerStyle,
} = params;

// ============================ Extra Render ==========================
const extraRender = React.useCallback(
(info: ExtraRenderInfo) => {
const { getSize, scrollTop, virtual } = info;
const { getSize, scrollTop, virtual, offsetX, rtl } = info;

if (!enabled || !group || !groupKeys.length || !virtual) {
return null;
Expand Down Expand Up @@ -106,6 +108,13 @@ export default function useStickyGroupHeader<
)
: 0;

const horizontalStyle: React.CSSProperties | undefined = scrollWidth
? {
width: scrollWidth,
transform: `translateX(${rtl ? offsetX : -offsetX}px)`,
}
: undefined;

// Render a cloned header pinned over the virtual list.
return (
<Portal open getContainer={() => container}>
Expand All @@ -119,7 +128,7 @@ export default function useStickyGroupHeader<
className={headerClassName}
// `top` is the computed sticky-push offset and must win over any
// user-supplied top in headerStyle, or the sticky behavior breaks.
style={{ ...headerStyle, top }}
style={{ ...headerStyle, ...horizontalStyle, top }}
/>
</div>
</Portal>
Expand All @@ -132,6 +141,7 @@ export default function useStickyGroupHeader<
groupKeyToItems,
prefixCls,
listRef,
scrollWidth,
headerClassName,
headerStyle,
],
Expand Down
Loading
Loading