Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
37bd76c
add raw list sticky sections
zombieJ May 18, 2026
751d986
improve raw list coverage
zombieJ May 18, 2026
c1ce642
cover raw list scroll info
zombieJ May 18, 2026
c69d6cc
refactor raw list scroll hook
zombieJ May 18, 2026
3026e77
fix raw list ref typing
zombieJ May 18, 2026
7b34872
simplify raw list scroll target lookup
zombieJ May 19, 2026
9207b3b
simplify raw list scroll config
zombieJ May 19, 2026
49ab398
use native scroll into view
zombieJ May 19, 2026
f1f8cb4
query raw list key directly
zombieJ May 19, 2026
adb2649
inline raw list scroll lookup
zombieJ May 19, 2026
7e5e65b
hide raw scroll info from ref
zombieJ May 19, 2026
e3e2f74
use div for raw group section
zombieJ May 20, 2026
35b69f1
refactor virtual list implementation
zombieJ May 20, 2026
c38dfc6
add basic demo group scroll button
zombieJ May 20, 2026
799589e
align basic demo scroll target
zombieJ May 20, 2026
8ce487e
offset basic demo item scroll
zombieJ May 20, 2026
f24320e
remove fixed basic demo scroll offset
zombieJ May 20, 2026
6e73039
Support sticky scroll offset
zombieJ May 20, 2026
23de8e5
Move list types into List
zombieJ May 20, 2026
b5d26c8
Keep group type with group segments
zombieJ May 20, 2026
ac10b64
Simplify internal list ref types
zombieJ May 20, 2026
b28267e
Wrap list items with item class
zombieJ May 20, 2026
a14d6d8
Add no virtual item scroll button
zombieJ May 20, 2026
1217446
Offset raw item scroll for sticky headers
zombieJ May 20, 2026
f8a742a
Update no virtual item scroll target
zombieJ May 20, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ npm start

### ListyRef

- `scrollTo(config: number | { key?: React.Key; index?: number; align?: 'top' | 'bottom' | 'auto'; offset?: number; } | { groupKey: React.Key; align?: 'top' | 'bottom' | 'auto'; offset?: number; })`
- `scrollTo(config?: number | null | { left?: number; top?: number } | { key: React.Key; align?: 'top' | 'bottom' | 'auto'; offset?: number } | { groupKey: React.Key; align?: 'top' | 'bottom' | 'auto'; offset?: number })`
- 传入 `groupKey` 时会直接滚动到对应组头(需启用 `group`

## Test Case
Expand Down
5 changes: 5 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
top: 0;
left: 0;
right: 0;
z-index: 1;
}

&-fixed {
Expand All @@ -22,6 +23,10 @@
}
}

&-group-section {
position: relative;
}

&-scrollbar {
z-index: 1;
}
Expand Down
13 changes: 12 additions & 1 deletion docs/examples/basic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default () => {
const groupItemsCount = 20;
const groupIndex = Math.floor(index / groupItemsCount);
return {
id: index + 1,
id: index,
name: `${index} (group ${groupIndex})`,
type: `Group ${groupIndex * groupItemsCount}`,
};
Expand Down Expand Up @@ -62,6 +62,17 @@ export default () => {
>
Scroll To 100
</button>

<button
onClick={() =>
listRef.current?.scrollTo({
groupKey: 'Group 120',
align: 'top',
})
}
>
Scroll To Group 120
</button>
</div>
);
};
7 changes: 7 additions & 0 deletions docs/examples/no-virtual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export default () => {
[],
);

const handleScrollToItem = useCallback((itemId: string) => {
listRef.current?.scrollTo({ key: itemId, align: 'top' });
}, []);

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Listy
Expand Down Expand Up @@ -155,6 +159,9 @@ export default () => {
</button>
);
})}
<button onClick={() => handleScrollToItem('desserts-3')}>
Scroll to Panna Cotta
</button>
<span>Total Items: {items.length}</span>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/util": "^1.3.1",
"clsx": "^2.1.1",
"@rc-component/virtual-list": "^1.1.0"
"@rc-component/virtual-list": "^1.2.0"
},
"devDependencies": {
"@rc-component/father-plugin": "^2.1.3",
Expand Down
14 changes: 12 additions & 2 deletions src/GroupHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ export interface GroupHeaderProps<T, K extends React.Key = React.Key> {
style?: React.CSSProperties;
}

export default function GroupHeader<T, K extends React.Key = React.Key>(
function GroupHeader<T, K extends React.Key = React.Key>(
props: GroupHeaderProps<T, K>,
ref: React.Ref<HTMLDivElement>,
) {
const {
group,
Expand All @@ -31,8 +32,17 @@ export default function GroupHeader<T, K extends React.Key = React.Key>(
});

return (
<div className={className} style={style}>
<div ref={ref} className={className} style={style}>
{group.title(groupKey, groupItems)}
</div>
);
}

const GroupHeaderWithRef = React.forwardRef(GroupHeader) as <
T,
K extends React.Key = React.Key,
>(
props: GroupHeaderProps<T, K> & { ref?: React.Ref<HTMLDivElement> },
) => React.ReactElement;

export default GroupHeaderWithRef;
165 changes: 51 additions & 114 deletions src/List.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
import * as React from 'react';
import VirtualList, {
type ListRef,
type ScrollTo,
} from '@rc-component/virtual-list';
import { useImperativeHandle, forwardRef } from 'react';
import useGroupSegments from './hooks/useGroupSegments';
import { forwardRef } from 'react';
import RawList from './RawList';
import VirtualList from './VirtualList';
import type { Group } from './hooks/useGroupSegments';
import useFlattenRows from './hooks/useFlattenRows';
import type { Row } from './hooks/useFlattenRows';
import useStickyGroupHeader from './hooks/useStickyGroupHeader';
import GroupHeader from './GroupHeader';
import { useEvent } from '@rc-component/util';

type RowKey<T> = keyof T | ((item: T) => React.Key);
export type RowKey<T> = keyof T | ((item: T) => React.Key);

export type ScrollAlign = 'top' | 'bottom' | 'auto';

export interface GroupScrollToConfig {
groupKey: string;
groupKey: React.Key;
align?: ScrollAlign;
offset?: number;
}

export interface KeyScrollToConfig {
key: React.Key;
align?: ScrollAlign;
offset?: number;
}

export interface PositionScrollToConfig {
left?: number;
top?: number;
}

export type ListyScrollToConfig =
| Parameters<ScrollTo>[0]
| number
| null
| KeyScrollToConfig
| PositionScrollToConfig
| GroupScrollToConfig;

export interface ListyRef {
Expand All @@ -43,120 +49,51 @@ export interface ListyProps<T, K extends React.Key = React.Key> {
itemRender: (item: T, index: number) => React.ReactNode;
}

export interface ListComponentProps<T, K extends React.Key = React.Key> {
data: T[];
sticky?: boolean;
itemHeight?: number;
height?: number;
group?: Group<T, K>;
prefixCls: string;
rowKey: RowKey<T>;
onScroll?: React.UIEventHandler<HTMLElement>;
itemRender: (item: T, index: number) => React.ReactNode;
}

function Listy<T, K extends React.Key = React.Key>(
props: ListyProps<T, K>,
ref: React.Ref<ListyRef>,
) {
// ============================== Props ==============================
const {
items,
itemRender,
group,
onScroll,
rowKey,
height,
itemHeight,
sticky,
virtual = true,
prefixCls = 'rc-listy',
} = props;
const { items, virtual = true, prefixCls = 'rc-listy', ...restProps } = props;

// =============================== Data ===============================
const data = React.useMemo(() => items || [], [items]);

// =============================== Refs ===============================
const listRef = React.useRef<ListRef>(null);

// ========================== Imperative API ==========================
useImperativeHandle(ref, () => ({
scrollTo: (config) => {
if (config && typeof config === 'object' && 'groupKey' in config) {
const { groupKey, align, offset } = config;
listRef.current?.scrollTo({
key: groupKey,
align,
offset,
});
return;
}
listRef.current?.scrollTo(config as Parameters<ScrollTo>[0]);
},
}));

// ============================= Grouping =============================
const groupData = useGroupSegments<T, K>(data, group);

// ============================= Row Keys =============================
const getKey = useEvent((row: Row<T, K>): React.Key => {
if (row.type === 'header') {
return row.groupKey;
}

if (typeof rowKey === 'function') {
return rowKey(row.item);
}
return row.item[rowKey] as React.Key;
});

// ============================= Flat Rows =============================
const { rows, headerRows, groupKeyToItems } = useFlattenRows<T, K>(
// ============================== Render ===============================
const sharedListProps = {
...restProps,
data,
groupData,
group,
);

// =========================== Sticky Header ===========================
const extraRender = useStickyGroupHeader<T, K>({
enabled: !!(sticky && group && virtual),
group,
headerRows,
groupKeyToItems,
prefixCls,
});

// ============================= Row Render ============================
const renderHeaderRow = React.useCallback(
(groupKey: K) => {
if (!group) {
return null;
}

const groupItems = groupKeyToItems.get(groupKey) || [];

return (
<GroupHeader
group={group}
groupKey={groupKey}
groupItems={groupItems}
prefixCls={prefixCls}
sticky={sticky && !virtual}
/>
);
},
[group, groupKeyToItems, prefixCls, sticky, virtual],
);
};

const listNode =
virtual === false ? (
<RawList
ref={ref}
{...sharedListProps}
/>
) : (
<VirtualList
ref={ref}
{...sharedListProps}
/>
);

// ============================== Render ===============================
return (
<div className={prefixCls}>
<VirtualList
virtual={virtual}
ref={listRef}
data={rows}
fullHeight={false}
itemHeight={itemHeight}
itemKey={getKey}
height={height}
extraRender={extraRender}
onScroll={onScroll}
prefixCls={prefixCls}
>
{(row: Row<T, K>) =>
row.type === 'header'
? renderHeaderRow(row.groupKey)
: itemRender(row.item, row.index)
}
</VirtualList>
{listNode}
</div>
);
}
Expand Down
Loading
Loading