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
4 changes: 2 additions & 2 deletions .dumirc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import path from 'path';

export default defineConfig({
alias: {
'rc-listy$': path.resolve('src'),
'rc-listy/es': path.resolve('src'),
'@rc-component/listy$': path.resolve('src'),
'@rc-component/listy/es': path.resolve('src'),
},
mfsu: false,
favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'],
Expand Down
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
8 changes: 8 additions & 0 deletions docs/demos/scroll-to.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Scroll To
nav:
title: Demo
path: /demo
---

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

const GROUP_SIZE = 15;
const GROUP_COUNT = 6;

interface Row {
id: number;
name: string;
group: string;
}

const items: Row[] = Array.from(
{ length: GROUP_SIZE * GROUP_COUNT },
(_, index) => ({
id: index,
name: `Item ${index}`,
group: `Group ${Math.floor(index / GROUP_SIZE)}`,
}),
);

const GROUP_KEYS = Array.from({ length: GROUP_COUNT }, (_, i) => `Group ${i}`);

const controlRow: React.CSSProperties = {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: 8,
};

export default () => {
const listRef = useRef<ListyRef>(null);
const [virtual, setVirtual] = useState(true);
const [align, setAlign] = useState<ScrollAlign>('top');
const [offset, setOffset] = useState(0);
const [itemKey, setItemKey] = useState(50);
const [groupKey, setGroupKey] = useState('Group 3');
const [lastConfig, setLastConfig] = useState<ListyScrollToConfig>();

// Single entry point so the demo can echo the exact config it passes in.
const run = (config: ListyScrollToConfig) => {
setLastConfig(config);
listRef.current?.scrollTo(config);
};

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={controlRow}>
<label>
<input
type="checkbox"
checked={virtual}
onChange={(e) => setVirtual(e.target.checked)}
/>{' '}
virtual
</label>

<label>
align{' '}
<select
value={align}
onChange={(e) => setAlign(e.target.value as ScrollAlign)}
>
<option value="top">top</option>
<option value="bottom">bottom</option>
<option value="auto">auto</option>
</select>
</label>

<label>
offset{' '}
<input
type="number"
value={offset}
style={{ width: 64 }}
onChange={(e) => setOffset(Number(e.target.value) || 0)}
/>
</label>
</div>

{/* number | { top } — absolute pixel scroll */}
<div style={controlRow}>
<button type="button" onClick={() => run(0)}>
scrollTo(0)
</button>
<button type="button" onClick={() => run(400)}>
scrollTo(400)
</button>
<button type="button" onClick={() => run({ top: 200 })}>
scrollTo({'{ top: 200 }'})
</button>
</div>

{/* { key, align, offset } — scroll to an item */}
<div style={controlRow}>
<label>
key{' '}
<input
type="number"
value={itemKey}
style={{ width: 64 }}
onChange={(e) => setItemKey(Number(e.target.value) || 0)}
/>
</label>
<button
type="button"
onClick={() => run({ key: itemKey, align, offset })}
>
scrollTo item
</button>

{/* { groupKey, align, offset } — scroll to a group header */}
<label>
group{' '}
<select
value={groupKey}
onChange={(e) => setGroupKey(e.target.value)}
>
{GROUP_KEYS.map((key) => (
<option key={key} value={key}>
{key}
</option>
))}
</select>
</label>
<button type="button" onClick={() => run({ groupKey, align, offset })}>
scrollTo group
</button>
</div>

<div>
last config:{' '}
<code>
{lastConfig === undefined ? '—' : JSON.stringify(lastConfig)}
</code>
</div>

<Listy
// Remount on mode switch so both branches start from a clean scroll.
key={virtual ? 'virtual' : 'raw'}
ref={listRef}
virtual={virtual}
height={360}
itemHeight={40}
items={items}
rowKey="id"
sticky
itemRender={(item, index) => {
const height = 40 + (index % 3) * 16;
return (
<div
style={{
padding: '0 16px',
height,
lineHeight: `${height}px`,
borderBottom: '1px solid #f0f0f0',
background: '#fff',
}}
>
{item.name}
</div>
);
}}
group={{
key: (item) => item.group,
title: (key, groupItems) => (
<div
style={{
padding: '10px 16px',
fontWeight: 600,
background: '#e6f4ff',
borderBottom: '1px solid #91caff',
}}
>
{key} · {groupItems.length} items
</div>
),
}}
/>
</div>
);
};
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export default defineConfig([
files: ['tests/**/*.{js,jsx,ts,tsx}', '**/*.{test,spec}.{js,jsx,ts,tsx}'],
extends: [jest.configs['flat/recommended']],
rules: {
'@typescript-eslint/no-require-imports': 'off',
'jest/no-disabled-tests': 'off',
'jest/no-done-callback': 'off',
'jest/no-identical-title': 'off',
Expand Down
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,26 @@
"build": "dumi build",
"compile": "father build && lessc assets/index.less assets/index.css",
"prepublishOnly": "npm run compile && rc-np",
"lint": "eslint src/ docs/examples/ --ext .tsx,.ts,.jsx,.js",
"lint": "eslint src/ tests/ docs/examples/ --ext .tsx,.ts,.jsx,.js",
"test": "rc-test",
"prepare": "husky",
"prettier": "prettier --write .",
"coverage": "rc-test --coverage"
},
"lint-staged": {
"*.{ts,tsx,js,jsx,json,less,md}": "prettier --write"
},
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/portal": "^2.0.0",
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/util": "^1.3.1",
"clsx": "^2.1.1",
"@rc-component/virtual-list": "^1.2.0"
"@rc-component/virtual-list": "^1.4.0",
"clsx": "^2.1.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@eslint/compat": "^2.1.0",
"@eslint/js": "^10.0.1",
"@rc-component/father-plugin": "^2.2.0",
"@rc-component/np": "^1.0.4",
"@testing-library/jest-dom": "^6.9.1",
Expand All @@ -66,7 +70,9 @@
"eslint-plugin-react-hooks": "^7.1.1",
"father": "^4.6.24",
"globals": "^17.7.0",
"husky": "^9.1.7",
"less": "^4.6.7",
"lint-staged": "^17.0.8",
"prettier": "^3.9.4",
"rc-test": "^7.1.3",
"react": "^19.2.7",
Expand Down
15 changes: 8 additions & 7 deletions src/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export type ListySemanticName = 'root' | 'item' | 'groupHeader';

export type ListyClassNames = Partial<Record<ListySemanticName, string>>;

export type ListyStyles = Partial<Record<ListySemanticName, React.CSSProperties>>;
export type ListyStyles = Partial<
Record<ListySemanticName, React.CSSProperties>
>;

export interface GroupScrollToConfig {
groupKey: React.Key;
Expand Down Expand Up @@ -92,12 +94,11 @@ function Listy<T, K extends React.Key = React.Key>(
ref,
};

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

return listNode;
}
Expand Down
38 changes: 14 additions & 24 deletions src/RawList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as React from 'react';
import clsx from 'clsx';
import { useEvent } from '@rc-component/util';
import GroupHeader from '../GroupHeader';
import useGroupSegments from '../hooks/useGroupSegments';
import useItemKey from '../hooks/useItemKey';
import useRawListScroll from './useRawListScroll';
import { toTaggedKey, type KeyType } from '../util';
import type { ListComponentProps, ListyRef } from '../List';

// ============================== Types ===============================
export type RawListProps<T, K extends React.Key = React.Key> =
ListComponentProps<T, K>;
export type RawListProps<
T,
K extends React.Key = React.Key,
> = ListComponentProps<T, K>;

function RawList<T, K extends React.Key = React.Key>(
props: RawListProps<T, K>,
Expand Down Expand Up @@ -36,38 +39,26 @@ function RawList<T, K extends React.Key = React.Key>(
const groupData = useGroupSegments<T, K>(data, group);

// ============================== Utils ===============================
const getItemKey = useEvent((item: T): React.Key => {
if (typeof rowKey === 'function') {
return rowKey(item);
}
return item[rowKey] as React.Key;
});
const getItemKey = useItemKey(rowKey);

const getScrollTargetProps = React.useCallback(
(key: React.Key) => ({
'data-key': String(key),
(key: React.Key, type: KeyType) => ({
'data-key': toTaggedKey(key, type),
}),
[],
);

// ============================ Render Item ===========================
const renderItem = React.useCallback(
(item: T, index: number, groupKey?: K) => {
(item: T, index: number) => {
const key = getItemKey(item);
const scrollTargetProps = getScrollTargetProps(key);
const scrollTargetProps = getScrollTargetProps(key, 'item');

return (
<div
key={key}
className={clsx(`${prefixCls}-item`, classNames?.item)}
style={{
...styles?.item,
...(sticky && groupKey !== undefined
? {
scrollMarginTop: `var(--${prefixCls}-item-scroll-margin-top, 0px)`,
}
: undefined),
}}
style={styles?.item}
{...scrollTargetProps}
>
{itemRender(item, index)}
Expand All @@ -80,7 +71,6 @@ function RawList<T, K extends React.Key = React.Key>(
getScrollTargetProps,
itemRender,
prefixCls,
sticky,
styles?.item,
],
);
Expand All @@ -94,7 +84,7 @@ function RawList<T, K extends React.Key = React.Key>(
<div
key={groupKey}
className={`${prefixCls}-group-section`}
{...getScrollTargetProps(groupKey)}
{...getScrollTargetProps(groupKey, 'group')}
>
<GroupHeader
group={group}
Expand All @@ -106,7 +96,7 @@ function RawList<T, K extends React.Key = React.Key>(
style={styles?.groupHeader}
/>
{groupItems.map(({ item, index }) => {
return renderItem(item, index, groupKey);
return renderItem(item, index);
})}
</div>
);
Expand Down
Loading
Loading