Skip to content

Commit 8049c43

Browse files
committed
feat(): add height detection to end item calc
1 parent 58c8b81 commit 8049c43

File tree

9 files changed

+1261
-1177
lines changed

9 files changed

+1261
-1177
lines changed

example/index.html

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<!DOCTYPE html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
7-
<title>Playground</title>
8-
</head>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
7+
<title>Playground</title>
8+
</head>
99

10-
<body>
11-
<div id="root"></div>
12-
<script src="./index.tsx"></script>
13-
</body>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="./index.tsx"></script>
13+
</body>
1414
</html>

example/index.tsx

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
11
import "react-app-polyfill/ie11"
22
import * as React from "react"
33
import * as ReactDOM from "react-dom"
4-
import VisualWindow, {VisualWindowChildProps} from "../src"
4+
import VisualWindow, {VisualWindowChildProps} from "../"
5+
6+
import "./style.css"
7+
8+
const random = (min = 0, max = 50) => {
9+
let num = Math.random() * (max - min) + min
10+
11+
return Math.floor(num)
12+
}
513

614
interface IData {
715
odd: boolean
16+
height: number
817
}
918

10-
const data = new Array(1000).fill(true).map(
19+
const data = new Array(100).fill(true).map(
1120
(_, x) =>
1221
({
1322
odd: x % 2 !== 0,
23+
height: (((x + 0) % 10) + 2) * 10,
1424
} as IData),
1525
)
1626

1727
const Row = React.forwardRef<HTMLDivElement, VisualWindowChildProps>((props, ref) => {
1828
const {data, index, style} = props
29+
const [first, setfirst] = React.useState(index === 0)
30+
const item = (data as IData[])[index]
1931
return (
20-
<div ref={ref} style={style}>
32+
<div ref={ref} style={{...style}} className={item.odd ? "odd" : "even"}>
2133
<div>
22-
Row {index} - odd: {(data as IData[])[index].odd ? `true` : `false`}
34+
Row {index} - odd: {item.odd ? `true` : `false`} - height: {item.height} <button onClick={() => setfirst(x => !x)}>click</button>
35+
{first && <div className="new">xxxx</div>}
2336
</div>
2437
</div>
2538
)
@@ -28,7 +41,7 @@ const Row = React.forwardRef<HTMLDivElement, VisualWindowChildProps>((props, ref
2841
const App = () => {
2942
return (
3043
<div>
31-
<VisualWindow defaultItemHeight={18} itemData={data} className="tableClass">
44+
<VisualWindow defaultItemHeight={21} itemData={data} className="tableClass">
3245
{Row}
3346
</VisualWindow>
3447
</div>

example/package.json

+22-22
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
{
2-
"name": "example",
3-
"version": "1.0.0",
4-
"main": "index.js",
5-
"license": "MIT",
6-
"scripts": {
7-
"start": "parcel index.html",
8-
"build": "parcel build index.html"
9-
},
10-
"dependencies": {
11-
"react-app-polyfill": "^1.0.0"
12-
},
13-
"alias": {
14-
"react": "../node_modules/react",
15-
"react-dom": "../node_modules/react-dom/profiling",
16-
"scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
17-
},
18-
"devDependencies": {
19-
"@types/react": "^16.9.11",
20-
"@types/react-dom": "^16.8.4",
21-
"parcel": "2.0.0-beta.2",
22-
"typescript": "^3.4.5"
23-
}
2+
"name": "example",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"start": "parcel --no-cache index.html",
8+
"build": "parcel build index.html"
9+
},
10+
"dependencies": {
11+
"react-app-polyfill": "^3.0.0"
12+
},
13+
"alias": {
14+
"react": "../node_modules/react",
15+
"react-dom": "../node_modules/react-dom/profiling",
16+
"scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
17+
},
18+
"devDependencies": {
19+
"@types/react": "^17.0.40",
20+
"@types/react-dom": "^17.0.13",
21+
"parcel": "2.3.2",
22+
"typescript": "^4.6.2"
23+
}
2424
}

example/style.css

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.odd {
2+
background-color: #eebefa;
3+
}
4+
.even {
5+
background-color: #a3daff;
6+
}
7+
8+
.new {
9+
height: 200px;
10+
}

package.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-visual-window",
3-
"version": "0.0.7",
3+
"version": "0.0.8",
44
"author": "Patrick Malleier <github@numero33.com>",
55
"license": "MIT",
66
"bugs": {
@@ -73,16 +73,16 @@
7373
}
7474
],
7575
"devDependencies": {
76-
"@size-limit/preset-small-lib": "^7.0.4",
77-
"@testing-library/react": "^12.1.2",
78-
"@types/react": "^17.0.37",
79-
"@types/react-dom": "^17.0.11",
76+
"@size-limit/preset-small-lib": "^7.0.8",
77+
"@testing-library/react": "^12.1.4",
78+
"@types/react": "^17.0.40",
79+
"@types/react-dom": "^17.0.13",
8080
"husky": "^7.0.4",
8181
"react": "^17.0.2",
8282
"react-dom": "^17.0.2",
83-
"size-limit": "^7.0.4",
83+
"size-limit": "^7.0.8",
8484
"tsdx": "^0.14.1",
8585
"tslib": "^2.3.1",
86-
"typescript": "^4.5.4"
86+
"typescript": "^4.6.2"
8787
}
8888
}

src/VisualWindow.tsx

+46-25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface VisualWindowProps {
77
itemData: Array<unknown>
88
className?: string
99
detectHeight?: boolean
10+
overhang?: number
1011
}
1112

1213
export interface VisualWindowChildProps {
@@ -15,19 +16,25 @@ export interface VisualWindowChildProps {
1516
style: React.CSSProperties
1617
}
1718

18-
export default function VisualWindow({children, defaultItemHeight, className, itemData, detectHeight = true}: VisualWindowProps) {
19+
export default function VisualWindow({children, defaultItemHeight, className, itemData, detectHeight = true, overhang = 0}: VisualWindowProps) {
1920
const itemCount = useMemo(() => itemData.length, [itemData])
2021

21-
const [measurements, setMeasurement] = useState<{[k: number]: DOMRect}>({})
22+
const [measurements, setMeasurement] = useState<{
23+
[k: number]: {
24+
width: number
25+
height: number
26+
}
27+
}>({})
2228

23-
const ref = useRef<HTMLDivElement>(null)
29+
const mainRef = useRef<HTMLDivElement>(null)
2430
const childRef = useRef(new Map<number, HTMLElement>()).current
2531
const {scrollY: scrollPosition} = useScrollPosition()
2632

2733
const checkMeasurements = useCallback(() => {
2834
for (const [i, c] of childRef.entries()) {
2935
const bounding = c.getBoundingClientRect()
30-
if (bounding.height !== defaultItemHeight && (measurements[i] === undefined || measurements[i].height !== bounding.height)) setMeasurement(x => ({...x, [i]: bounding}))
36+
37+
if (bounding.height !== defaultItemHeight && measurements?.[i]?.height !== bounding?.height) setMeasurement(x => ({...x, [i]: {width: bounding.width, height: bounding.height}}))
3138
if (bounding.height === defaultItemHeight && measurements[i] !== undefined) {
3239
setMeasurement(x => {
3340
const tmp = {...x}
@@ -47,30 +54,45 @@ export default function VisualWindow({children, defaultItemHeight, className, it
4754

4855
const height = useMemo(() => defaultItemHeight * itemCount + Object.values(measurements).reduce((sum, val) => (sum += val.height - defaultItemHeight), 0), [defaultItemHeight, itemCount, measurements])
4956

50-
const itemRenderCount = useMemo(() => (defaultItemHeight === 0 ? 0 : Math.min(Math.ceil(Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0) / defaultItemHeight), itemCount)), [defaultItemHeight, itemCount])
57+
const maxViewWindow = useMemo(() => Math.max(document?.documentElement?.clientHeight ?? 0, window?.innerHeight ?? 0), [document?.documentElement?.clientHeight, window?.innerHeight])
5158

5259
const startItem = useMemo(() => {
53-
if (ref.current === null || itemCount === 0) return 0
54-
55-
const windowOffset = ref.current.getBoundingClientRect().top
56-
if (windowOffset > 0) return 0
60+
if (itemCount === 0) return 0
5761

58-
let tmpOffset = windowOffset
5962
let start = 0
60-
if (Object.keys(measurements).length > 0) {
61-
for (; start < itemCount && tmpOffset < 0; start++) tmpOffset += measurements[start] !== undefined ? measurements[start].height : defaultItemHeight
62-
start--
63-
} else start = Math.floor(Math.abs(windowOffset) / defaultItemHeight)
63+
const windowOffset = mainRef?.current?.getBoundingClientRect()?.top ?? 0
64+
if (windowOffset < 0) {
65+
let tmpOffset = windowOffset
66+
67+
if (Object.keys(measurements).length > 0) {
68+
for (; start < itemCount && tmpOffset < 0; start++) tmpOffset += measurements?.[start]?.height ?? defaultItemHeight
69+
start--
70+
} else start = Math.floor(Math.abs(windowOffset) / defaultItemHeight)
6471

65-
const max = Math.max(itemCount - itemRenderCount, 0)
72+
start = Math.max(start - overhang, 0)
73+
}
74+
return start
75+
}, [mainRef, scrollPosition, defaultItemHeight, itemCount, measurements, overhang])
76+
77+
const endItem = useMemo(() => {
78+
if (itemCount === 0) return 0
79+
80+
let end = startItem
81+
let height = 0
82+
83+
if (Object.keys(measurements).length > 0) {
84+
while (height <= maxViewWindow + (measurements?.[startItem]?.height ?? defaultItemHeight)) {
85+
height += measurements?.[end]?.height ?? defaultItemHeight
86+
end++
87+
}
88+
} else end = Math.ceil(maxViewWindow / defaultItemHeight) + startItem
6689

67-
return Math.min(Math.max(start, 0), max)
68-
// eslint-disable-next-line react-hooks/exhaustive-deps
69-
}, [ref, scrollPosition, defaultItemHeight, itemCount, itemRenderCount, measurements])
90+
return Math.max(Math.min(end + overhang, itemCount - 1), 0)
91+
}, [startItem, defaultItemHeight, itemCount, measurements, maxViewWindow, overhang])
7092

7193
const calcChildren = useMemo(() => {
7294
const output = []
73-
for (let i = startItem; i < itemRenderCount + startItem; i++) {
95+
for (let i = startItem; i <= endItem; i++) {
7496
let addRef = {}
7597

7698
if (typeof children === "object") {
@@ -108,14 +130,13 @@ export default function VisualWindow({children, defaultItemHeight, className, it
108130
},
109131
output,
110132
)
111-
// eslint-disable-next-line react-hooks/exhaustive-deps
112-
}, [startItem, defaultItemHeight, itemRenderCount, children, itemData, detectHeight])
133+
}, [startItem, endItem, children, itemData, detectHeight])
113134

114135
useEffect(() => {
115-
if (!detectHeight || !ref.current) return () => {}
136+
if (!detectHeight || !mainRef.current) return () => {}
116137

117138
const observer = new MutationObserver(() => checkMeasurements())
118-
observer.observe(ref.current, {
139+
observer.observe(mainRef.current, {
119140
attributes: false,
120141
childList: true,
121142
subtree: true,
@@ -126,10 +147,10 @@ export default function VisualWindow({children, defaultItemHeight, className, it
126147
return createElement(
127148
"div",
128149
{
129-
ref,
150+
ref: mainRef,
130151
style: {position: "relative", height},
131152
className,
132153
},
133-
itemCount > 0 && itemRenderCount > 0 && calcChildren,
154+
itemCount > 0 && calcChildren,
134155
)
135156
}

test/VisualWindow.spec.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as React from "react"
21
import {render} from "@testing-library/react"
32
import VisualWindow from "../src"
43

@@ -25,7 +24,7 @@ describe("it", () => {
2524
{Row}
2625
</VisualWindow>,
2726
)
28-
expect(itemRenderer).toHaveBeenCalledTimes(8)
27+
expect(itemRenderer).toHaveBeenCalledTimes(9)
2928
})
3029

3130
it("should render a list of rows disable detectHeight", () => {
@@ -36,7 +35,7 @@ describe("it", () => {
3635
{Row}
3736
</VisualWindow>,
3837
)
39-
expect(itemRenderer).toHaveBeenCalledTimes(8)
38+
expect(itemRenderer).toHaveBeenCalledTimes(9)
4039
})
4140

4241
// it("should render a list of rows and scroll to 100", () => {

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
// use Node's module resolution algorithm, instead of the legacy TS one
2323
"moduleResolution": "node",
2424
// transpile JSX to React.createElement
25-
"jsx": "react",
25+
"jsx": "react-jsx",
2626
// interop between ESM and CJS modules. Recommended by TS
2727
"esModuleInterop": true,
2828
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS

0 commit comments

Comments
 (0)