@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.
Files changed (41) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +110 -0
  3. package/assets/index.css +36 -0
  4. package/assets/index.less +44 -0
  5. package/es/GroupHeader.d.ts +15 -0
  6. package/es/GroupHeader.js +32 -0
  7. package/es/List.d.ts +49 -0
  8. package/es/List.js +33 -0
  9. package/es/RawList/index.d.ts +5 -0
  10. package/es/RawList/index.js +90 -0
  11. package/es/RawList/useRawListScroll.d.ts +3 -0
  12. package/es/RawList/useRawListScroll.js +71 -0
  13. package/es/VirtualList/index.d.ts +5 -0
  14. package/es/VirtualList/index.js +151 -0
  15. package/es/VirtualList/useFlattenRows.d.ts +21 -0
  16. package/es/VirtualList/useFlattenRows.js +62 -0
  17. package/es/VirtualList/useStickyGroupHeader.d.ts +14 -0
  18. package/es/VirtualList/useStickyGroupHeader.js +82 -0
  19. package/es/hooks/useGroupSegments.d.ts +16 -0
  20. package/es/hooks/useGroupSegments.js +39 -0
  21. package/es/index.d.ts +3 -0
  22. package/es/index.js +2 -0
  23. package/lib/GroupHeader.d.ts +15 -0
  24. package/lib/GroupHeader.js +40 -0
  25. package/lib/List.d.ts +49 -0
  26. package/lib/List.js +41 -0
  27. package/lib/RawList/index.d.ts +5 -0
  28. package/lib/RawList/index.js +98 -0
  29. package/lib/RawList/useRawListScroll.d.ts +3 -0
  30. package/lib/RawList/useRawListScroll.js +79 -0
  31. package/lib/VirtualList/index.d.ts +5 -0
  32. package/lib/VirtualList/index.js +159 -0
  33. package/lib/VirtualList/useFlattenRows.d.ts +21 -0
  34. package/lib/VirtualList/useFlattenRows.js +69 -0
  35. package/lib/VirtualList/useStickyGroupHeader.d.ts +14 -0
  36. package/lib/VirtualList/useStickyGroupHeader.js +90 -0
  37. package/lib/hooks/useGroupSegments.d.ts +16 -0
  38. package/lib/hooks/useGroupSegments.js +46 -0
  39. package/lib/index.d.ts +3 -0
  40. package/lib/index.js +9 -0
  41. 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
+ [![@rc-component/listy](https://nodei.co/npm/@rc-component/listy.png)](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.
@@ -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,5 @@
1
+ import * as React from 'react';
2
+ import type { ListComponentProps } from '../List';
3
+ export type RawListProps<T, K extends React.Key = React.Key> = ListComponentProps<T, K>;
4
+ declare const RawListWithRef: any;
5
+ export default RawListWithRef;
@@ -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,3 @@
1
+ import * as React from 'react';
2
+ import type { ListyRef } from '../List';
3
+ export default function useRawListScroll(ref: React.Ref<ListyRef>, prefixCls: string, stickyGroup: boolean): React.RefObject<HTMLDivElement>;
@@ -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,5 @@
1
+ import * as React from 'react';
2
+ import type { ListComponentProps } from '../List';
3
+ export type VirtualListProps<T, K extends React.Key = React.Key> = ListComponentProps<T, K>;
4
+ declare const VirtualListWithRef: any;
5
+ export default VirtualListWithRef;
@@ -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;