ark-floating-scroll 1.0.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/README.md +87 -0
- package/dist/index.d.mts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +101 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# optimised-scroll
|
|
2
|
+
|
|
3
|
+
A high-performance virtualized list component for React. Renders only the items visible in the viewport, dramatically reducing DOM nodes and improving performance for long lists.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install optimised-scroll
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer dependencies:** React ≥ 16.8.0
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { VirtualList } from "optimised-scroll";
|
|
17
|
+
|
|
18
|
+
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
|
|
19
|
+
|
|
20
|
+
function App() {
|
|
21
|
+
return (
|
|
22
|
+
<VirtualList
|
|
23
|
+
items={items}
|
|
24
|
+
itemHeight={40}
|
|
25
|
+
height={500}
|
|
26
|
+
renderItem={(item, index) => (
|
|
27
|
+
<div style={{ padding: "8px", borderBottom: "1px solid #eee" }}>
|
|
28
|
+
{item}
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
### `<VirtualList<T>>`
|
|
39
|
+
|
|
40
|
+
| Prop | Type | Default | Description |
|
|
41
|
+
|------|------|---------|-------------|
|
|
42
|
+
| `items` | `T[]` | *required* | Array of items to render |
|
|
43
|
+
| `itemHeight` | `number` | *required* | Fixed height of each item in pixels |
|
|
44
|
+
| `height` | `number` | `400` | Height of the scrollable container |
|
|
45
|
+
| `width` | `number \| string` | `"100%"` | Width of the scrollable container |
|
|
46
|
+
| `overscan` | `number` | `5` | Extra items rendered above/below viewport |
|
|
47
|
+
| `renderItem` | `(item: T, index: number) => ReactNode` | `String(item)` | Custom render function |
|
|
48
|
+
| `className` | `string` | — | CSS class for the outer container |
|
|
49
|
+
| `style` | `CSSProperties` | — | Inline styles for the outer container |
|
|
50
|
+
|
|
51
|
+
## How It Works
|
|
52
|
+
|
|
53
|
+
1. **Scroll Detection** — Listens to the container's `scroll` event, throttled via `requestAnimationFrame` for smooth 60fps updates.
|
|
54
|
+
2. **Range Calculation** — Computes `startIndex` and `endIndex` from `scrollTop / itemHeight`, adding an `overscan` buffer.
|
|
55
|
+
3. **DOM Recycling** — Only the visible slice of items is mounted in the DOM. Items outside the viewport are unmounted, keeping memory usage constant regardless of list size.
|
|
56
|
+
|
|
57
|
+
## TypeScript
|
|
58
|
+
|
|
59
|
+
Full type definitions are included. The component is generic — your `items` type flows through to `renderItem`:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
interface User {
|
|
63
|
+
id: number;
|
|
64
|
+
name: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
<VirtualList<User>
|
|
68
|
+
items={users}
|
|
69
|
+
itemHeight={60}
|
|
70
|
+
renderItem={(user) => <div>{user.name}</div>}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Examples
|
|
75
|
+
|
|
76
|
+
The `examples/` directory includes interactive demos (simple list, typed objects, dynamic add/remove).
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm run demo
|
|
80
|
+
# opens at http://localhost:5173
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Open DevTools → Elements to confirm only ~20-30 DOM nodes exist regardless of list size.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React, { ReactNode, CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
interface VirtualListProps<T> {
|
|
4
|
+
/** Array of items to render */
|
|
5
|
+
items: T[];
|
|
6
|
+
/** Fixed height of each item in pixels */
|
|
7
|
+
itemHeight: number;
|
|
8
|
+
/** Height of the scrollable container in pixels (default: 400) */
|
|
9
|
+
height?: number;
|
|
10
|
+
/** Width of the scrollable container (default: "100%") */
|
|
11
|
+
width?: number | string;
|
|
12
|
+
/** Number of extra items to render above/below the viewport (default: 5) */
|
|
13
|
+
overscan?: number;
|
|
14
|
+
/** Custom render function for each item */
|
|
15
|
+
renderItem?: (item: T, index: number) => ReactNode;
|
|
16
|
+
/** CSS class for the outer container */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Inline styles for the outer container */
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A virtualized list component that renders only the items visible
|
|
23
|
+
* in the viewport, plus a configurable overscan buffer.
|
|
24
|
+
*/
|
|
25
|
+
declare function VirtualList<T>({ items, itemHeight, height, width, overscan, renderItem, className, style, }: VirtualListProps<T>): React.JSX.Element;
|
|
26
|
+
|
|
27
|
+
export { VirtualList, type VirtualListProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React, { ReactNode, CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
interface VirtualListProps<T> {
|
|
4
|
+
/** Array of items to render */
|
|
5
|
+
items: T[];
|
|
6
|
+
/** Fixed height of each item in pixels */
|
|
7
|
+
itemHeight: number;
|
|
8
|
+
/** Height of the scrollable container in pixels (default: 400) */
|
|
9
|
+
height?: number;
|
|
10
|
+
/** Width of the scrollable container (default: "100%") */
|
|
11
|
+
width?: number | string;
|
|
12
|
+
/** Number of extra items to render above/below the viewport (default: 5) */
|
|
13
|
+
overscan?: number;
|
|
14
|
+
/** Custom render function for each item */
|
|
15
|
+
renderItem?: (item: T, index: number) => ReactNode;
|
|
16
|
+
/** CSS class for the outer container */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Inline styles for the outer container */
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A virtualized list component that renders only the items visible
|
|
23
|
+
* in the viewport, plus a configurable overscan buffer.
|
|
24
|
+
*/
|
|
25
|
+
declare function VirtualList<T>({ items, itemHeight, height, width, overscan, renderItem, className, style, }: VirtualListProps<T>): React.JSX.Element;
|
|
26
|
+
|
|
27
|
+
export { VirtualList, type VirtualListProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
VirtualList: () => VirtualList
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/VirtualList.tsx
|
|
38
|
+
var import_react = __toESM(require("react"));
|
|
39
|
+
function defaultRenderItem(item, index) {
|
|
40
|
+
return /* @__PURE__ */ import_react.default.createElement("div", null, String(item));
|
|
41
|
+
}
|
|
42
|
+
function VirtualList({
|
|
43
|
+
items,
|
|
44
|
+
itemHeight,
|
|
45
|
+
height = 400,
|
|
46
|
+
width = "100%",
|
|
47
|
+
overscan = 5,
|
|
48
|
+
renderItem = defaultRenderItem,
|
|
49
|
+
className,
|
|
50
|
+
style
|
|
51
|
+
}) {
|
|
52
|
+
const containerRef = (0, import_react.useRef)(null);
|
|
53
|
+
const [scrollTop, setScrollTop] = (0, import_react.useState)(0);
|
|
54
|
+
const rafRef = (0, import_react.useRef)(null);
|
|
55
|
+
const totalHeight = items.length * itemHeight;
|
|
56
|
+
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
|
|
57
|
+
const visibleCount = Math.ceil(height / itemHeight);
|
|
58
|
+
const endIndex = Math.min(
|
|
59
|
+
items.length,
|
|
60
|
+
Math.floor(scrollTop / itemHeight) + visibleCount + overscan
|
|
61
|
+
);
|
|
62
|
+
const handleScroll = (0, import_react.useCallback)(() => {
|
|
63
|
+
const container = containerRef.current;
|
|
64
|
+
if (!container) return;
|
|
65
|
+
if (rafRef.current !== null) {
|
|
66
|
+
cancelAnimationFrame(rafRef.current);
|
|
67
|
+
}
|
|
68
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
69
|
+
setScrollTop(container.scrollTop);
|
|
70
|
+
rafRef.current = null;
|
|
71
|
+
});
|
|
72
|
+
}, []);
|
|
73
|
+
(0, import_react.useEffect)(() => {
|
|
74
|
+
return () => {
|
|
75
|
+
if (rafRef.current !== null) {
|
|
76
|
+
cancelAnimationFrame(rafRef.current);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
80
|
+
const visibleItems = [];
|
|
81
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
82
|
+
visibleItems.push(
|
|
83
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
84
|
+
"div",
|
|
85
|
+
{
|
|
86
|
+
key: i,
|
|
87
|
+
style: {
|
|
88
|
+
position: "absolute",
|
|
89
|
+
top: i * itemHeight,
|
|
90
|
+
left: 0,
|
|
91
|
+
right: 0,
|
|
92
|
+
height: itemHeight,
|
|
93
|
+
overflow: "hidden"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
renderItem(items[i], i)
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const containerStyle = {
|
|
101
|
+
overflow: "auto",
|
|
102
|
+
height,
|
|
103
|
+
width,
|
|
104
|
+
position: "relative",
|
|
105
|
+
willChange: "transform",
|
|
106
|
+
...style
|
|
107
|
+
};
|
|
108
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
109
|
+
"div",
|
|
110
|
+
{
|
|
111
|
+
ref: containerRef,
|
|
112
|
+
className,
|
|
113
|
+
style: containerStyle,
|
|
114
|
+
onScroll: handleScroll
|
|
115
|
+
},
|
|
116
|
+
/* @__PURE__ */ import_react.default.createElement(
|
|
117
|
+
"div",
|
|
118
|
+
{
|
|
119
|
+
style: {
|
|
120
|
+
height: totalHeight,
|
|
121
|
+
position: "relative",
|
|
122
|
+
width: "100%"
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
visibleItems
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
130
|
+
0 && (module.exports = {
|
|
131
|
+
VirtualList
|
|
132
|
+
});
|
|
133
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/VirtualList.tsx"],"sourcesContent":["export { VirtualList } from \"./VirtualList\";\nexport type { VirtualListProps } from \"./VirtualList\";\n","import React, {\n useState,\n useRef,\n useCallback,\n useEffect,\n CSSProperties,\n ReactNode,\n} from \"react\";\n\nexport interface VirtualListProps<T> {\n /** Array of items to render */\n items: T[];\n /** Fixed height of each item in pixels */\n itemHeight: number;\n /** Height of the scrollable container in pixels (default: 400) */\n height?: number;\n /** Width of the scrollable container (default: \"100%\") */\n width?: number | string;\n /** Number of extra items to render above/below the viewport (default: 5) */\n overscan?: number;\n /** Custom render function for each item */\n renderItem?: (item: T, index: number) => ReactNode;\n /** CSS class for the outer container */\n className?: string;\n /** Inline styles for the outer container */\n style?: CSSProperties;\n}\n\nfunction defaultRenderItem<T>(item: T, index: number): ReactNode {\n return <div>{String(item)}</div>;\n}\n\n/**\n * A virtualized list component that renders only the items visible\n * in the viewport, plus a configurable overscan buffer.\n */\nexport function VirtualList<T>({\n items,\n itemHeight,\n height = 400,\n width = \"100%\",\n overscan = 5,\n renderItem = defaultRenderItem,\n className,\n style,\n}: VirtualListProps<T>) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState(0);\n const rafRef = useRef<number | null>(null);\n\n const totalHeight = items.length * itemHeight;\n\n // Calculate visible range with overscan buffer\n const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);\n const visibleCount = Math.ceil(height / itemHeight);\n const endIndex = Math.min(\n items.length,\n Math.floor(scrollTop / itemHeight) + visibleCount + overscan\n );\n\n const handleScroll = useCallback(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Use rAF to throttle scroll updates for smooth performance\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n }\n\n rafRef.current = requestAnimationFrame(() => {\n setScrollTop(container.scrollTop);\n rafRef.current = null;\n });\n }, []);\n\n // Clean up rAF on unmount\n useEffect(() => {\n return () => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n }\n };\n }, []);\n\n // Build only the visible items\n const visibleItems: ReactNode[] = [];\n for (let i = startIndex; i < endIndex; i++) {\n visibleItems.push(\n <div\n key={i}\n style={{\n position: \"absolute\",\n top: i * itemHeight,\n left: 0,\n right: 0,\n height: itemHeight,\n overflow: \"hidden\",\n }}\n >\n {renderItem(items[i], i)}\n </div>\n );\n }\n\n const containerStyle: CSSProperties = {\n overflow: \"auto\",\n height,\n width,\n position: \"relative\",\n willChange: \"transform\",\n ...style,\n };\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={containerStyle}\n onScroll={handleScroll}\n >\n {/* Spacer div to maintain correct scrollbar size */}\n <div\n style={{\n height: totalHeight,\n position: \"relative\",\n width: \"100%\",\n }}\n >\n {visibleItems}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAOO;AAqBP,SAAS,kBAAqB,MAAS,OAA0B;AAC/D,SAAO,6BAAAA,QAAA,cAAC,aAAK,OAAO,IAAI,CAAE;AAC5B;AAMO,SAAS,YAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,aAAa;AAAA,EACb;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,mBAAe,qBAAuB,IAAI;AAChD,QAAM,CAAC,WAAW,YAAY,QAAI,uBAAS,CAAC;AAC5C,QAAM,aAAS,qBAAsB,IAAI;AAEzC,QAAM,cAAc,MAAM,SAAS;AAGnC,QAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,UAAU,IAAI,QAAQ;AAC5E,QAAM,eAAe,KAAK,KAAK,SAAS,UAAU;AAClD,QAAM,WAAW,KAAK;AAAA,IACpB,MAAM;AAAA,IACN,KAAK,MAAM,YAAY,UAAU,IAAI,eAAe;AAAA,EACtD;AAEA,QAAM,mBAAe,0BAAY,MAAM;AACrC,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,UAAW;AAGhB,QAAI,OAAO,YAAY,MAAM;AAC3B,2BAAqB,OAAO,OAAO;AAAA,IACrC;AAEA,WAAO,UAAU,sBAAsB,MAAM;AAC3C,mBAAa,UAAU,SAAS;AAChC,aAAO,UAAU;AAAA,IACnB,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAGL,8BAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,OAAO,YAAY,MAAM;AAC3B,6BAAqB,OAAO,OAAO;AAAA,MACrC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,eAA4B,CAAC;AACnC,WAAS,IAAI,YAAY,IAAI,UAAU,KAAK;AAC1C,iBAAa;AAAA,MACX,6BAAAA,QAAA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,OAAO;AAAA,YACL,UAAU;AAAA,YACV,KAAK,IAAI;AAAA,YACT,MAAM;AAAA,YACN,OAAO;AAAA,YACP,QAAQ;AAAA,YACR,UAAU;AAAA,UACZ;AAAA;AAAA,QAEC,WAAW,MAAM,CAAC,GAAG,CAAC;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,iBAAgC;AAAA,IACpC,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,GAAG;AAAA,EACL;AAEA,SACE,6BAAAA,QAAA;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA,OAAO;AAAA,MACP,UAAU;AAAA;AAAA,IAGV,6BAAAA,QAAA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,OAAO;AAAA,QACT;AAAA;AAAA,MAEC;AAAA,IACH;AAAA,EACF;AAEJ;","names":["React"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/VirtualList.tsx
|
|
2
|
+
import React, {
|
|
3
|
+
useState,
|
|
4
|
+
useRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect
|
|
7
|
+
} from "react";
|
|
8
|
+
function defaultRenderItem(item, index) {
|
|
9
|
+
return /* @__PURE__ */ React.createElement("div", null, String(item));
|
|
10
|
+
}
|
|
11
|
+
function VirtualList({
|
|
12
|
+
items,
|
|
13
|
+
itemHeight,
|
|
14
|
+
height = 400,
|
|
15
|
+
width = "100%",
|
|
16
|
+
overscan = 5,
|
|
17
|
+
renderItem = defaultRenderItem,
|
|
18
|
+
className,
|
|
19
|
+
style
|
|
20
|
+
}) {
|
|
21
|
+
const containerRef = useRef(null);
|
|
22
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
23
|
+
const rafRef = useRef(null);
|
|
24
|
+
const totalHeight = items.length * itemHeight;
|
|
25
|
+
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
|
|
26
|
+
const visibleCount = Math.ceil(height / itemHeight);
|
|
27
|
+
const endIndex = Math.min(
|
|
28
|
+
items.length,
|
|
29
|
+
Math.floor(scrollTop / itemHeight) + visibleCount + overscan
|
|
30
|
+
);
|
|
31
|
+
const handleScroll = useCallback(() => {
|
|
32
|
+
const container = containerRef.current;
|
|
33
|
+
if (!container) return;
|
|
34
|
+
if (rafRef.current !== null) {
|
|
35
|
+
cancelAnimationFrame(rafRef.current);
|
|
36
|
+
}
|
|
37
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
38
|
+
setScrollTop(container.scrollTop);
|
|
39
|
+
rafRef.current = null;
|
|
40
|
+
});
|
|
41
|
+
}, []);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
return () => {
|
|
44
|
+
if (rafRef.current !== null) {
|
|
45
|
+
cancelAnimationFrame(rafRef.current);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
const visibleItems = [];
|
|
50
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
51
|
+
visibleItems.push(
|
|
52
|
+
/* @__PURE__ */ React.createElement(
|
|
53
|
+
"div",
|
|
54
|
+
{
|
|
55
|
+
key: i,
|
|
56
|
+
style: {
|
|
57
|
+
position: "absolute",
|
|
58
|
+
top: i * itemHeight,
|
|
59
|
+
left: 0,
|
|
60
|
+
right: 0,
|
|
61
|
+
height: itemHeight,
|
|
62
|
+
overflow: "hidden"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
renderItem(items[i], i)
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const containerStyle = {
|
|
70
|
+
overflow: "auto",
|
|
71
|
+
height,
|
|
72
|
+
width,
|
|
73
|
+
position: "relative",
|
|
74
|
+
willChange: "transform",
|
|
75
|
+
...style
|
|
76
|
+
};
|
|
77
|
+
return /* @__PURE__ */ React.createElement(
|
|
78
|
+
"div",
|
|
79
|
+
{
|
|
80
|
+
ref: containerRef,
|
|
81
|
+
className,
|
|
82
|
+
style: containerStyle,
|
|
83
|
+
onScroll: handleScroll
|
|
84
|
+
},
|
|
85
|
+
/* @__PURE__ */ React.createElement(
|
|
86
|
+
"div",
|
|
87
|
+
{
|
|
88
|
+
style: {
|
|
89
|
+
height: totalHeight,
|
|
90
|
+
position: "relative",
|
|
91
|
+
width: "100%"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
visibleItems
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
VirtualList
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/VirtualList.tsx"],"sourcesContent":["import React, {\n useState,\n useRef,\n useCallback,\n useEffect,\n CSSProperties,\n ReactNode,\n} from \"react\";\n\nexport interface VirtualListProps<T> {\n /** Array of items to render */\n items: T[];\n /** Fixed height of each item in pixels */\n itemHeight: number;\n /** Height of the scrollable container in pixels (default: 400) */\n height?: number;\n /** Width of the scrollable container (default: \"100%\") */\n width?: number | string;\n /** Number of extra items to render above/below the viewport (default: 5) */\n overscan?: number;\n /** Custom render function for each item */\n renderItem?: (item: T, index: number) => ReactNode;\n /** CSS class for the outer container */\n className?: string;\n /** Inline styles for the outer container */\n style?: CSSProperties;\n}\n\nfunction defaultRenderItem<T>(item: T, index: number): ReactNode {\n return <div>{String(item)}</div>;\n}\n\n/**\n * A virtualized list component that renders only the items visible\n * in the viewport, plus a configurable overscan buffer.\n */\nexport function VirtualList<T>({\n items,\n itemHeight,\n height = 400,\n width = \"100%\",\n overscan = 5,\n renderItem = defaultRenderItem,\n className,\n style,\n}: VirtualListProps<T>) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState(0);\n const rafRef = useRef<number | null>(null);\n\n const totalHeight = items.length * itemHeight;\n\n // Calculate visible range with overscan buffer\n const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);\n const visibleCount = Math.ceil(height / itemHeight);\n const endIndex = Math.min(\n items.length,\n Math.floor(scrollTop / itemHeight) + visibleCount + overscan\n );\n\n const handleScroll = useCallback(() => {\n const container = containerRef.current;\n if (!container) return;\n\n // Use rAF to throttle scroll updates for smooth performance\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n }\n\n rafRef.current = requestAnimationFrame(() => {\n setScrollTop(container.scrollTop);\n rafRef.current = null;\n });\n }, []);\n\n // Clean up rAF on unmount\n useEffect(() => {\n return () => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n }\n };\n }, []);\n\n // Build only the visible items\n const visibleItems: ReactNode[] = [];\n for (let i = startIndex; i < endIndex; i++) {\n visibleItems.push(\n <div\n key={i}\n style={{\n position: \"absolute\",\n top: i * itemHeight,\n left: 0,\n right: 0,\n height: itemHeight,\n overflow: \"hidden\",\n }}\n >\n {renderItem(items[i], i)}\n </div>\n );\n }\n\n const containerStyle: CSSProperties = {\n overflow: \"auto\",\n height,\n width,\n position: \"relative\",\n willChange: \"transform\",\n ...style,\n };\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={containerStyle}\n onScroll={handleScroll}\n >\n {/* Spacer div to maintain correct scrollbar size */}\n <div\n style={{\n height: totalHeight,\n position: \"relative\",\n width: \"100%\",\n }}\n >\n {visibleItems}\n </div>\n </div>\n );\n}\n"],"mappings":";AAAA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAqBP,SAAS,kBAAqB,MAAS,OAA0B;AAC/D,SAAO,oCAAC,aAAK,OAAO,IAAI,CAAE;AAC5B;AAMO,SAAS,YAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,aAAa;AAAA,EACb;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,eAAe,OAAuB,IAAI;AAChD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,CAAC;AAC5C,QAAM,SAAS,OAAsB,IAAI;AAEzC,QAAM,cAAc,MAAM,SAAS;AAGnC,QAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,UAAU,IAAI,QAAQ;AAC5E,QAAM,eAAe,KAAK,KAAK,SAAS,UAAU;AAClD,QAAM,WAAW,KAAK;AAAA,IACpB,MAAM;AAAA,IACN,KAAK,MAAM,YAAY,UAAU,IAAI,eAAe;AAAA,EACtD;AAEA,QAAM,eAAe,YAAY,MAAM;AACrC,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,UAAW;AAGhB,QAAI,OAAO,YAAY,MAAM;AAC3B,2BAAqB,OAAO,OAAO;AAAA,IACrC;AAEA,WAAO,UAAU,sBAAsB,MAAM;AAC3C,mBAAa,UAAU,SAAS;AAChC,aAAO,UAAU;AAAA,IACnB,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAGL,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,OAAO,YAAY,MAAM;AAC3B,6BAAqB,OAAO,OAAO;AAAA,MACrC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,eAA4B,CAAC;AACnC,WAAS,IAAI,YAAY,IAAI,UAAU,KAAK;AAC1C,iBAAa;AAAA,MACX;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,OAAO;AAAA,YACL,UAAU;AAAA,YACV,KAAK,IAAI;AAAA,YACT,MAAM;AAAA,YACN,OAAO;AAAA,YACP,QAAQ;AAAA,YACR,UAAU;AAAA,UACZ;AAAA;AAAA,QAEC,WAAW,MAAM,CAAC,GAAG,CAAC;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,iBAAgC;AAAA,IACpC,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,GAAG;AAAA,EACL;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA,OAAO;AAAA,MACP,UAAU;AAAA;AAAA,IAGV;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,OAAO;AAAA,QACT;AAAA;AAAA,MAEC;AAAA,IACH;AAAA,EACF;AAEJ;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ark-floating-scroll",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A high-performance virtualized list component for React that renders only visible items",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"test": "jest",
|
|
21
|
+
"demo": "vite examples",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"react",
|
|
26
|
+
"virtual-list",
|
|
27
|
+
"virtualized",
|
|
28
|
+
"scroll",
|
|
29
|
+
"performance",
|
|
30
|
+
"windowing"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=16.8.0",
|
|
36
|
+
"react-dom": ">=16.8.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
40
|
+
"@testing-library/react": "^16.3.2",
|
|
41
|
+
"@types/jest": "^30.0.0",
|
|
42
|
+
"@types/react": "^19.2.14",
|
|
43
|
+
"@types/react-dom": "^19.2.3",
|
|
44
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
45
|
+
"jest": "^30.2.0",
|
|
46
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
47
|
+
"react": "^19.2.4",
|
|
48
|
+
"react-dom": "^19.2.4",
|
|
49
|
+
"ts-jest": "^29.4.6",
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"vite": "^7.3.1"
|
|
53
|
+
}
|
|
54
|
+
}
|