@rc-component/listy 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +110 -0
- package/assets/index.css +36 -0
- package/assets/index.less +44 -0
- package/es/GroupHeader.d.ts +15 -0
- package/es/GroupHeader.js +32 -0
- package/es/List.d.ts +49 -0
- package/es/List.js +33 -0
- package/es/RawList/index.d.ts +5 -0
- package/es/RawList/index.js +90 -0
- package/es/RawList/useRawListScroll.d.ts +3 -0
- package/es/RawList/useRawListScroll.js +71 -0
- package/es/VirtualList/index.d.ts +5 -0
- package/es/VirtualList/index.js +151 -0
- package/es/VirtualList/useFlattenRows.d.ts +21 -0
- package/es/VirtualList/useFlattenRows.js +62 -0
- package/es/VirtualList/useStickyGroupHeader.d.ts +14 -0
- package/es/VirtualList/useStickyGroupHeader.js +82 -0
- package/es/hooks/useGroupSegments.d.ts +16 -0
- package/es/hooks/useGroupSegments.js +39 -0
- package/es/index.d.ts +3 -0
- package/es/index.js +2 -0
- package/lib/GroupHeader.d.ts +15 -0
- package/lib/GroupHeader.js +40 -0
- package/lib/List.d.ts +49 -0
- package/lib/List.js +41 -0
- package/lib/RawList/index.d.ts +5 -0
- package/lib/RawList/index.js +98 -0
- package/lib/RawList/useRawListScroll.d.ts +3 -0
- package/lib/RawList/useRawListScroll.js +79 -0
- package/lib/VirtualList/index.d.ts +5 -0
- package/lib/VirtualList/index.js +159 -0
- package/lib/VirtualList/useFlattenRows.d.ts +21 -0
- package/lib/VirtualList/useFlattenRows.js +69 -0
- package/lib/VirtualList/useStickyGroupHeader.d.ts +14 -0
- package/lib/VirtualList/useStickyGroupHeader.js +90 -0
- package/lib/hooks/useGroupSegments.d.ts +16 -0
- package/lib/hooks/useGroupSegments.js +46 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +9 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
|
12
|
+
all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
15
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @rc-component/Listy
|
|
2
|
+
|
|
3
|
+
React Listy Component
|
|
4
|
+
|
|
5
|
+
[![NPM version][npm-image]][npm-url]
|
|
6
|
+
[![npm download][download-image]][download-url]
|
|
7
|
+
[![build status][github-actions-image]][github-actions-url]
|
|
8
|
+
[![Test coverage][codecov-image]][codecov-url]
|
|
9
|
+
[![bundle size][bundlephobia-image]][bundlephobia-url]
|
|
10
|
+
[![dumi][dumi-image]][dumi-url]
|
|
11
|
+
|
|
12
|
+
[npm-image]: http://img.shields.io/npm/v/@rc-component/listy.svg?style=flat-square
|
|
13
|
+
[npm-url]: http://npmjs.org/package/@rc-component/listy
|
|
14
|
+
[github-actions-image]: https://github.com/react-component/listy/workflows/CI/badge.svg
|
|
15
|
+
[github-actions-url]: https://github.com/react-component/listy/actions
|
|
16
|
+
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/listy/master.svg?style=flat-square
|
|
17
|
+
[codecov-url]: https://codecov.io/gh/react-component/listy/branch/master
|
|
18
|
+
[david-url]: https://david-dm.org/react-component/listy
|
|
19
|
+
[david-image]: https://david-dm.org/react-component/listy/status.svg?style=flat-square
|
|
20
|
+
[david-dev-url]: https://david-dm.org/react-component/listy?type=dev
|
|
21
|
+
[david-dev-image]: https://david-dm.org/react-component/listy/dev-status.svg?style=flat-square
|
|
22
|
+
[download-image]: https://img.shields.io/npm/dm/@rc-component/listy.svg?style=flat-square
|
|
23
|
+
[download-url]: https://npmjs.org/package/@rc-component/listy
|
|
24
|
+
[bundlephobia-url]: https://bundlephobia.com/result?p=@rc-component/listy
|
|
25
|
+
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@rc-component/listy
|
|
26
|
+
[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
|
|
27
|
+
[dumi-url]: https://github.com/umijs/dumi
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
[](https://npmjs.org/package/@rc-component/listy)
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Include the default [styling](https://github.com/react-component/listy/blob/master/assets/index.less#L4:L11) and then:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import React from 'react';
|
|
39
|
+
import ReactDOM from 'react-dom/client';
|
|
40
|
+
import Listy from '@rc-component/listy';
|
|
41
|
+
|
|
42
|
+
const items = Array.from({ length: 100 }, (_, index) => ({
|
|
43
|
+
id: index,
|
|
44
|
+
name: `Item ${index}`,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const App = () => (
|
|
48
|
+
<Listy
|
|
49
|
+
items={items}
|
|
50
|
+
height={240}
|
|
51
|
+
itemHeight={32}
|
|
52
|
+
rowKey="id"
|
|
53
|
+
itemRender={(item) => <div>{item.name}</div>}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
ReactDOM.createRoot(container).render(<App />);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Compatibility
|
|
61
|
+
|
|
62
|
+
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/electron/electron_48x48.png" alt="Electron" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Electron |
|
|
63
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
|
65
|
+
|
|
66
|
+
## Example
|
|
67
|
+
|
|
68
|
+
http://localhost:9001
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
npm install
|
|
74
|
+
npm start
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
### props
|
|
80
|
+
|
|
81
|
+
| name | type | default | description |
|
|
82
|
+
| --- | --- | --- | --- |
|
|
83
|
+
| items | `T[]` | `[]` | 列表数据源,虚拟滚动会基于此计算高度。 |
|
|
84
|
+
| rowKey | `keyof T \| (item: T) => React.Key` | required | 返回每一项的唯一标识,用于缓存高度与滚动定位。 |
|
|
85
|
+
| itemRender | `(item: T, index: number) => React.ReactNode` | required | 渲染单行内容的函数。 |
|
|
86
|
+
| height | `number` | - | 列表可视区域高度。 |
|
|
87
|
+
| itemHeight | `number` | - | 每行的基础高度,虚拟滚动会以此做初始估算。 |
|
|
88
|
+
| group | `{ key: ((item: T) => K) \| K; title: (groupKey: K, items: T[]) => React.ReactNode }` | - | 提供分组 key 与标题渲染,开启后会生成组头。 |
|
|
89
|
+
| sticky | `boolean` | `false` | 为分组头启用粘性悬停效果。 |
|
|
90
|
+
| virtual | `boolean` | `true` | 是否启用虚拟列表模式,可根据需要关闭。 |
|
|
91
|
+
| onScroll | `React.UIEventHandler<HTMLElement>` | - | 滚动时触发,透传内部滚动容器的滚动事件。 |
|
|
92
|
+
| prefixCls | `string` | `rc-listy` | 组件样式前缀,方便自定义样式隔离。 |
|
|
93
|
+
|
|
94
|
+
### ListyRef
|
|
95
|
+
|
|
96
|
+
- `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 })`
|
|
97
|
+
- 传入 `groupKey` 时会直接滚动到对应组头(需启用 `group`)
|
|
98
|
+
|
|
99
|
+
## Test Case
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
npm test
|
|
103
|
+
npm run coverage
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
open coverage/ dir
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
@rc-component/listy is released under the MIT license.
|
package/assets/index.css
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.rc-listy {
|
|
2
|
+
position: relative;
|
|
3
|
+
}
|
|
4
|
+
.rc-listy-group-header {
|
|
5
|
+
background-color: #fff;
|
|
6
|
+
}
|
|
7
|
+
.rc-listy-group-header-sticky {
|
|
8
|
+
position: sticky;
|
|
9
|
+
top: 0;
|
|
10
|
+
left: 0;
|
|
11
|
+
right: 0;
|
|
12
|
+
z-index: 1;
|
|
13
|
+
}
|
|
14
|
+
.rc-listy-group-header-fixed {
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 0;
|
|
17
|
+
left: 0;
|
|
18
|
+
right: 0;
|
|
19
|
+
transform: translateY(0);
|
|
20
|
+
pointer-events: auto;
|
|
21
|
+
}
|
|
22
|
+
.rc-listy-group-header-holder {
|
|
23
|
+
position: absolute;
|
|
24
|
+
top: 0;
|
|
25
|
+
left: 0;
|
|
26
|
+
right: 0;
|
|
27
|
+
bottom: 0;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
.rc-listy-group-section {
|
|
32
|
+
position: relative;
|
|
33
|
+
}
|
|
34
|
+
.rc-listy-scrollbar {
|
|
35
|
+
z-index: 1;
|
|
36
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
@listy-prefix-cls: ~'rc-listy';
|
|
2
|
+
|
|
3
|
+
.@{listy-prefix-cls} {
|
|
4
|
+
position: relative;
|
|
5
|
+
|
|
6
|
+
&-group-header {
|
|
7
|
+
background-color: #fff;
|
|
8
|
+
|
|
9
|
+
&-sticky {
|
|
10
|
+
position: sticky;
|
|
11
|
+
top: 0;
|
|
12
|
+
left: 0;
|
|
13
|
+
right: 0;
|
|
14
|
+
z-index: 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&-fixed {
|
|
18
|
+
position: absolute;
|
|
19
|
+
top: 0;
|
|
20
|
+
left: 0;
|
|
21
|
+
right: 0;
|
|
22
|
+
transform: translateY(0);
|
|
23
|
+
pointer-events: auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&-holder {
|
|
27
|
+
position: absolute;
|
|
28
|
+
top: 0;
|
|
29
|
+
left: 0;
|
|
30
|
+
right: 0;
|
|
31
|
+
bottom: 0;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
pointer-events: none;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&-group-section {
|
|
38
|
+
position: relative;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&-scrollbar {
|
|
42
|
+
z-index: 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Group } from './hooks/useGroupSegments';
|
|
3
|
+
export interface GroupHeaderProps<T, K extends React.Key = React.Key> {
|
|
4
|
+
group: Group<T, K>;
|
|
5
|
+
groupKey: K;
|
|
6
|
+
groupItems: T[];
|
|
7
|
+
prefixCls: string;
|
|
8
|
+
fixed?: boolean;
|
|
9
|
+
sticky?: boolean;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
declare const GroupHeaderWithRef: <T, K extends React.Key = React.Key>(props: GroupHeaderProps<T, K> & {
|
|
13
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
14
|
+
}) => React.ReactElement;
|
|
15
|
+
export default GroupHeaderWithRef;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
|
|
4
|
+
// ============================== Types ===============================
|
|
5
|
+
|
|
6
|
+
function GroupHeader(props, ref) {
|
|
7
|
+
// ============================== Props ==============================
|
|
8
|
+
const {
|
|
9
|
+
group,
|
|
10
|
+
groupKey,
|
|
11
|
+
groupItems,
|
|
12
|
+
prefixCls,
|
|
13
|
+
fixed,
|
|
14
|
+
sticky,
|
|
15
|
+
style
|
|
16
|
+
} = props;
|
|
17
|
+
|
|
18
|
+
// ============================= Classes =============================
|
|
19
|
+
const className = clsx(`${prefixCls}-group-header`, {
|
|
20
|
+
[`${prefixCls}-group-header-sticky`]: sticky,
|
|
21
|
+
[`${prefixCls}-group-header-fixed`]: fixed
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ============================== Render ==============================
|
|
25
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
26
|
+
ref: ref,
|
|
27
|
+
className: className,
|
|
28
|
+
style: style
|
|
29
|
+
}, group.title(groupKey, groupItems));
|
|
30
|
+
}
|
|
31
|
+
const GroupHeaderWithRef = /*#__PURE__*/React.forwardRef(GroupHeader);
|
|
32
|
+
export default GroupHeaderWithRef;
|
package/es/List.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Group } from './hooks/useGroupSegments';
|
|
3
|
+
export type RowKey<T> = keyof T | ((item: T) => React.Key);
|
|
4
|
+
export type ScrollAlign = 'top' | 'bottom' | 'auto';
|
|
5
|
+
export interface GroupScrollToConfig {
|
|
6
|
+
groupKey: React.Key;
|
|
7
|
+
align?: ScrollAlign;
|
|
8
|
+
offset?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface KeyScrollToConfig {
|
|
11
|
+
key: React.Key;
|
|
12
|
+
align?: ScrollAlign;
|
|
13
|
+
offset?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface PositionScrollToConfig {
|
|
16
|
+
left?: number;
|
|
17
|
+
top?: number;
|
|
18
|
+
}
|
|
19
|
+
export type ListyScrollToConfig = number | null | KeyScrollToConfig | PositionScrollToConfig | GroupScrollToConfig;
|
|
20
|
+
export interface ListyRef {
|
|
21
|
+
scrollTo: (config?: ListyScrollToConfig) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface ListyProps<T, K extends React.Key = React.Key> {
|
|
24
|
+
items?: T[];
|
|
25
|
+
sticky?: boolean;
|
|
26
|
+
itemHeight?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
group?: Group<T, K>;
|
|
29
|
+
virtual?: boolean;
|
|
30
|
+
prefixCls?: string;
|
|
31
|
+
rowKey: RowKey<T>;
|
|
32
|
+
onScroll?: React.UIEventHandler<HTMLElement>;
|
|
33
|
+
itemRender: (item: T, index: number) => React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
export interface ListComponentProps<T, K extends React.Key = React.Key> {
|
|
36
|
+
data: T[];
|
|
37
|
+
sticky?: boolean;
|
|
38
|
+
itemHeight?: number;
|
|
39
|
+
height?: number;
|
|
40
|
+
group?: Group<T, K>;
|
|
41
|
+
prefixCls: string;
|
|
42
|
+
rowKey: RowKey<T>;
|
|
43
|
+
onScroll?: React.UIEventHandler<HTMLElement>;
|
|
44
|
+
itemRender: (item: T, index: number) => React.ReactNode;
|
|
45
|
+
}
|
|
46
|
+
declare const ListyWithForwardRef: <T, K extends React.Key = React.Key>(props: ListyProps<T, K> & {
|
|
47
|
+
ref?: React.Ref<ListyRef>;
|
|
48
|
+
}) => React.ReactElement;
|
|
49
|
+
export default ListyWithForwardRef;
|
package/es/List.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import RawList from "./RawList";
|
|
4
|
+
import VirtualList from "./VirtualList";
|
|
5
|
+
|
|
6
|
+
// ============================== Types ===============================
|
|
7
|
+
|
|
8
|
+
function Listy(props, ref) {
|
|
9
|
+
// ============================== Props ==============================
|
|
10
|
+
const {
|
|
11
|
+
items,
|
|
12
|
+
virtual = true,
|
|
13
|
+
prefixCls = 'rc-listy',
|
|
14
|
+
...restProps
|
|
15
|
+
} = props;
|
|
16
|
+
|
|
17
|
+
// =============================== Data ===============================
|
|
18
|
+
const data = React.useMemo(() => items || [], [items]);
|
|
19
|
+
|
|
20
|
+
// ============================== Render ===============================
|
|
21
|
+
const sharedListProps = {
|
|
22
|
+
...restProps,
|
|
23
|
+
data,
|
|
24
|
+
prefixCls,
|
|
25
|
+
ref
|
|
26
|
+
};
|
|
27
|
+
const listNode = virtual ? /*#__PURE__*/React.createElement(VirtualList, sharedListProps) : /*#__PURE__*/React.createElement(RawList, sharedListProps);
|
|
28
|
+
return listNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Const to support generic with forwardRef
|
|
32
|
+
const ListyWithForwardRef = /*#__PURE__*/forwardRef(Listy);
|
|
33
|
+
export default ListyWithForwardRef;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useEvent } from '@rc-component/util';
|
|
4
|
+
import GroupHeader from "../GroupHeader";
|
|
5
|
+
import useGroupSegments from "../hooks/useGroupSegments";
|
|
6
|
+
import useRawListScroll from "./useRawListScroll";
|
|
7
|
+
|
|
8
|
+
// ============================== Types ===============================
|
|
9
|
+
|
|
10
|
+
function RawList(props, ref) {
|
|
11
|
+
// ============================== Props ==============================
|
|
12
|
+
const {
|
|
13
|
+
data,
|
|
14
|
+
group,
|
|
15
|
+
height,
|
|
16
|
+
itemRender,
|
|
17
|
+
onScroll,
|
|
18
|
+
prefixCls,
|
|
19
|
+
rowKey,
|
|
20
|
+
sticky
|
|
21
|
+
} = props;
|
|
22
|
+
|
|
23
|
+
// =============================== Refs ===============================
|
|
24
|
+
const holderRef = useRawListScroll(ref, prefixCls, !!(sticky && group));
|
|
25
|
+
|
|
26
|
+
// =============================== Data ===============================
|
|
27
|
+
const groupData = useGroupSegments(data, group);
|
|
28
|
+
|
|
29
|
+
// ============================== Utils ===============================
|
|
30
|
+
const getItemKey = useEvent(item => {
|
|
31
|
+
if (typeof rowKey === 'function') {
|
|
32
|
+
return rowKey(item);
|
|
33
|
+
}
|
|
34
|
+
return item[rowKey];
|
|
35
|
+
});
|
|
36
|
+
const getScrollTargetProps = React.useCallback(key => ({
|
|
37
|
+
'data-key': String(key)
|
|
38
|
+
}), []);
|
|
39
|
+
|
|
40
|
+
// ============================ Render Item ===========================
|
|
41
|
+
const renderItem = React.useCallback((item, index, groupKey) => {
|
|
42
|
+
const key = getItemKey(item);
|
|
43
|
+
const scrollTargetProps = getScrollTargetProps(key);
|
|
44
|
+
return /*#__PURE__*/React.createElement("div", _extends({
|
|
45
|
+
key: key,
|
|
46
|
+
className: `${prefixCls}-item`,
|
|
47
|
+
style: sticky && groupKey !== undefined ? {
|
|
48
|
+
scrollMarginTop: `var(--${prefixCls}-item-scroll-margin-top, 0px)`
|
|
49
|
+
} : undefined
|
|
50
|
+
}, scrollTargetProps), itemRender(item, index));
|
|
51
|
+
}, [getItemKey, getScrollTargetProps, itemRender, prefixCls, sticky]);
|
|
52
|
+
|
|
53
|
+
// ============================= Content ==============================
|
|
54
|
+
const rawContent = group ? Array.from(groupData, ([groupKey, groupItems]) => {
|
|
55
|
+
const currentGroupItems = groupItems.map(({
|
|
56
|
+
item
|
|
57
|
+
}) => item);
|
|
58
|
+
return /*#__PURE__*/React.createElement("div", _extends({
|
|
59
|
+
key: groupKey,
|
|
60
|
+
className: `${prefixCls}-group-section`
|
|
61
|
+
}, getScrollTargetProps(groupKey)), /*#__PURE__*/React.createElement(GroupHeader, {
|
|
62
|
+
group: group,
|
|
63
|
+
groupKey: groupKey,
|
|
64
|
+
groupItems: currentGroupItems,
|
|
65
|
+
prefixCls: prefixCls,
|
|
66
|
+
sticky: sticky
|
|
67
|
+
}), groupItems.map(({
|
|
68
|
+
item,
|
|
69
|
+
index
|
|
70
|
+
}) => {
|
|
71
|
+
return renderItem(item, index, groupKey);
|
|
72
|
+
}));
|
|
73
|
+
}) : data.map((item, index) => {
|
|
74
|
+
return renderItem(item, index);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ============================== Render ==============================
|
|
78
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
79
|
+
ref: holderRef,
|
|
80
|
+
className: prefixCls,
|
|
81
|
+
style: {
|
|
82
|
+
maxHeight: height,
|
|
83
|
+
overflowY: height === undefined ? undefined : 'auto',
|
|
84
|
+
overflowAnchor: 'none'
|
|
85
|
+
},
|
|
86
|
+
onScroll: onScroll
|
|
87
|
+
}, rawContent);
|
|
88
|
+
}
|
|
89
|
+
const RawListWithRef = /*#__PURE__*/React.forwardRef(RawList);
|
|
90
|
+
export default RawListWithRef;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export default function useRawListScroll(ref, prefixCls, stickyGroup) {
|
|
3
|
+
// =============================== Refs ===============================
|
|
4
|
+
const holderRef = React.useRef(null);
|
|
5
|
+
|
|
6
|
+
// ============================== Utils ===============================
|
|
7
|
+
const getStickyHeaderHeight = React.useCallback(targetElement => {
|
|
8
|
+
if (!stickyGroup) {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
const groupSection = targetElement.closest(`.${CSS.escape(`${prefixCls}-group-section`)}`);
|
|
12
|
+
const groupHeader = groupSection?.querySelector(`.${CSS.escape(`${prefixCls}-group-header`)}`);
|
|
13
|
+
if (!groupHeader) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
const rect = groupHeader.getBoundingClientRect();
|
|
17
|
+
const height = rect.height || rect.bottom - rect.top || groupHeader.offsetHeight;
|
|
18
|
+
return Number.isFinite(height) ? height : 0;
|
|
19
|
+
}, [prefixCls, stickyGroup]);
|
|
20
|
+
const setTargetScrollMargin = React.useCallback((targetElement, align) => {
|
|
21
|
+
const marginTop = align === 'top' ? getStickyHeaderHeight(targetElement) : 0;
|
|
22
|
+
targetElement.style.setProperty(`--${prefixCls}-item-scroll-margin-top`, `${marginTop}px`);
|
|
23
|
+
}, [getStickyHeaderHeight, prefixCls]);
|
|
24
|
+
|
|
25
|
+
// ============================== Scroll ==============================
|
|
26
|
+
const scrollTo = React.useCallback(config => {
|
|
27
|
+
const holder = holderRef.current;
|
|
28
|
+
if (!holder || config == null) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (typeof config === 'number') {
|
|
32
|
+
holder.scrollTop = config;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if ('key' in config || 'groupKey' in config) {
|
|
36
|
+
const {
|
|
37
|
+
align = 'top'
|
|
38
|
+
} = config;
|
|
39
|
+
const targetKey = 'groupKey' in config ? config.groupKey : config.key;
|
|
40
|
+
const targetElement = holder.querySelector(`[data-key="${CSS.escape(String(targetKey))}"]`);
|
|
41
|
+
if (targetElement) {
|
|
42
|
+
if ('key' in config) {
|
|
43
|
+
setTargetScrollMargin(targetElement, align);
|
|
44
|
+
}
|
|
45
|
+
targetElement.scrollIntoView({
|
|
46
|
+
block: align === 'bottom' ? 'end' : align === 'auto' ? 'nearest' : 'start',
|
|
47
|
+
inline: 'nearest'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const {
|
|
53
|
+
left,
|
|
54
|
+
top
|
|
55
|
+
} = config;
|
|
56
|
+
if (left !== undefined) {
|
|
57
|
+
holder.scrollLeft = left;
|
|
58
|
+
}
|
|
59
|
+
if (top !== undefined) {
|
|
60
|
+
holder.scrollTop = top;
|
|
61
|
+
}
|
|
62
|
+
}, [setTargetScrollMargin]);
|
|
63
|
+
|
|
64
|
+
// ============================ Imperative ============================
|
|
65
|
+
React.useImperativeHandle(ref, () => ({
|
|
66
|
+
scrollTo
|
|
67
|
+
}), [scrollTo]);
|
|
68
|
+
|
|
69
|
+
// ============================== Return ==============================
|
|
70
|
+
return holderRef;
|
|
71
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import RcVirtualList from '@rc-component/virtual-list';
|
|
3
|
+
import { useEvent } from '@rc-component/util';
|
|
4
|
+
import GroupHeader from "../GroupHeader";
|
|
5
|
+
import useGroupSegments from "../hooks/useGroupSegments";
|
|
6
|
+
import useFlattenRows from "./useFlattenRows";
|
|
7
|
+
import useStickyGroupHeader from "./useStickyGroupHeader";
|
|
8
|
+
|
|
9
|
+
// ============================== Types ===============================
|
|
10
|
+
|
|
11
|
+
function VirtualList(props, ref) {
|
|
12
|
+
// ============================== Props ==============================
|
|
13
|
+
const {
|
|
14
|
+
data,
|
|
15
|
+
group,
|
|
16
|
+
height,
|
|
17
|
+
itemHeight,
|
|
18
|
+
itemRender,
|
|
19
|
+
onScroll,
|
|
20
|
+
prefixCls,
|
|
21
|
+
rowKey,
|
|
22
|
+
sticky
|
|
23
|
+
} = props;
|
|
24
|
+
|
|
25
|
+
// =============================== Refs ===============================
|
|
26
|
+
const listRef = React.useRef(null);
|
|
27
|
+
|
|
28
|
+
// =============================== Data ===============================
|
|
29
|
+
const groupData = useGroupSegments(data, group);
|
|
30
|
+
|
|
31
|
+
// =============================== Keys ===============================
|
|
32
|
+
const getItemKey = useEvent(item => {
|
|
33
|
+
if (typeof rowKey === 'function') {
|
|
34
|
+
return rowKey(item);
|
|
35
|
+
}
|
|
36
|
+
return item[rowKey];
|
|
37
|
+
});
|
|
38
|
+
const getKey = useEvent(row => {
|
|
39
|
+
if (row.type === 'header') {
|
|
40
|
+
return row.groupKey;
|
|
41
|
+
}
|
|
42
|
+
return getItemKey(row.item);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ============================== Rows ================================
|
|
46
|
+
const {
|
|
47
|
+
rows,
|
|
48
|
+
groupKeys,
|
|
49
|
+
groupKeyToItems
|
|
50
|
+
} = useFlattenRows(data, groupData, group);
|
|
51
|
+
|
|
52
|
+
// ============================== Lookup ==============================
|
|
53
|
+
const itemKeyToGroupKey = React.useMemo(() => {
|
|
54
|
+
const itemGroupMap = new Map();
|
|
55
|
+
groupData.forEach((groupItems, groupKey) => {
|
|
56
|
+
groupItems.forEach(({
|
|
57
|
+
item
|
|
58
|
+
}) => {
|
|
59
|
+
itemGroupMap.set(getItemKey(item), groupKey);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
return itemGroupMap;
|
|
63
|
+
}, [getItemKey, groupData]);
|
|
64
|
+
|
|
65
|
+
// ============================== Scroll ==============================
|
|
66
|
+
const scrollTo = useEvent(config => {
|
|
67
|
+
// Group headers are rows in the virtual data, so group scroll maps to key scroll.
|
|
68
|
+
if (config && typeof config === 'object' && 'groupKey' in config) {
|
|
69
|
+
const {
|
|
70
|
+
groupKey,
|
|
71
|
+
align,
|
|
72
|
+
offset
|
|
73
|
+
} = config;
|
|
74
|
+
listRef.current?.scrollTo({
|
|
75
|
+
key: groupKey,
|
|
76
|
+
align,
|
|
77
|
+
offset
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// For sticky grouped lists, top-aligned item scroll should land below its header.
|
|
83
|
+
if (config && typeof config === 'object' && 'key' in config && sticky && group && config.align === 'top') {
|
|
84
|
+
const groupKey = itemKeyToGroupKey.get(config.key);
|
|
85
|
+
if (groupKey !== undefined) {
|
|
86
|
+
const {
|
|
87
|
+
offset = 0
|
|
88
|
+
} = config;
|
|
89
|
+
listRef.current?.scrollTo({
|
|
90
|
+
...config,
|
|
91
|
+
// Use the measured header height so top-aligned items stay below it.
|
|
92
|
+
offset: ({
|
|
93
|
+
getSize
|
|
94
|
+
}) => {
|
|
95
|
+
const headerSize = getSize(groupKey);
|
|
96
|
+
const headerHeight = headerSize.bottom - headerSize.top;
|
|
97
|
+
return offset + (Number.isFinite(headerHeight) ? headerHeight : 0);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Other scroll shapes are already supported by the underlying virtual list.
|
|
105
|
+
listRef.current?.scrollTo(config);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ============================ Imperative ============================
|
|
109
|
+
React.useImperativeHandle(ref, () => ({
|
|
110
|
+
scrollTo
|
|
111
|
+
}), [scrollTo]);
|
|
112
|
+
|
|
113
|
+
// ============================== Sticky ==============================
|
|
114
|
+
const extraRender = useStickyGroupHeader({
|
|
115
|
+
enabled: !!(sticky && group),
|
|
116
|
+
group,
|
|
117
|
+
groupKeys,
|
|
118
|
+
groupKeyToItems,
|
|
119
|
+
prefixCls,
|
|
120
|
+
listRef
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ============================ Render Row ============================
|
|
124
|
+
const renderHeaderRow = React.useCallback(groupKey => {
|
|
125
|
+
const groupItems = groupKeyToItems.get(groupKey) || [];
|
|
126
|
+
return /*#__PURE__*/React.createElement(GroupHeader, {
|
|
127
|
+
group: group,
|
|
128
|
+
groupKey: groupKey,
|
|
129
|
+
groupItems: groupItems,
|
|
130
|
+
prefixCls: prefixCls
|
|
131
|
+
});
|
|
132
|
+
}, [group, groupKeyToItems, prefixCls]);
|
|
133
|
+
|
|
134
|
+
// ============================== Render ==============================
|
|
135
|
+
return /*#__PURE__*/React.createElement(RcVirtualList, {
|
|
136
|
+
ref: listRef,
|
|
137
|
+
data: rows,
|
|
138
|
+
fullHeight: false,
|
|
139
|
+
height: height,
|
|
140
|
+
itemHeight: itemHeight,
|
|
141
|
+
itemKey: getKey,
|
|
142
|
+
onScroll: onScroll,
|
|
143
|
+
prefixCls: prefixCls,
|
|
144
|
+
virtual: true,
|
|
145
|
+
extraRender: extraRender
|
|
146
|
+
}, row => row.type === 'header' ? renderHeaderRow(row.groupKey) : /*#__PURE__*/React.createElement("div", {
|
|
147
|
+
className: `${prefixCls}-item`
|
|
148
|
+
}, itemRender(row.item, row.index)));
|
|
149
|
+
}
|
|
150
|
+
const VirtualListWithRef = /*#__PURE__*/React.forwardRef(VirtualList);
|
|
151
|
+
export default VirtualListWithRef;
|