Skip to content

Commit d31f593

Browse files
Merge pull request #340 from preactjs/scroll-into-view
2 parents 9f9b280 + ead46e7 commit d31f593

File tree

4 files changed

+338
-25
lines changed

4 files changed

+338
-25
lines changed

src/view/components/elements/TreeView.tsx

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,11 @@ export function TreeView() {
7171
const [updateCount, setUpdateCount] = useState(0);
7272
useResize(() => setUpdateCount(updateCount + 1), [updateCount]);
7373

74-
useEffect(() => {
75-
if (ref.current) {
76-
const selectedNode = ref.current.querySelector(
77-
'[data-selected="true"]',
78-
) as any;
79-
if (selectedNode) {
80-
scrollIntoView(selectedNode);
81-
}
82-
}
83-
}, []);
84-
85-
const { children: listItems, containerHeight } = useVirtualizedList({
74+
const {
75+
children: listItems,
76+
containerHeight,
77+
scrollToItem,
78+
} = useVirtualizedList({
8679
rowHeight: ROW_HEIGHT,
8780
bufferCount: 5,
8881
container: ref,
@@ -91,6 +84,10 @@ export function TreeView() {
9184
renderRow: (id, _, top) => <TreeItem key={id} id={id} top={top} />,
9285
});
9386

87+
useEffect(() => {
88+
scrollToItem(selected);
89+
}, [selected]);
90+
9491
useAutoIndent(paneRef, [listItems]);
9592

9693
// When the devtools is connected, but nothing has been sent to the panel yet
@@ -197,13 +194,6 @@ export function TreeItem(props: { key: any; id: ID; top: number }) {
197194
const onToggle = () => toggle(id);
198195
const ref = useRef<HTMLDivElement>();
199196

200-
const isSelected = as.selected === id;
201-
useEffect(() => {
202-
if (ref.current && isSelected) {
203-
scrollIntoView(ref.current);
204-
}
205-
}, [ref.current, as.selected, id]);
206-
207197
if (!node) return null;
208198

209199
return (
@@ -217,7 +207,7 @@ export function TreeItem(props: { key: any; id: ID; top: number }) {
217207
store.notify("inspect", id);
218208
}}
219209
onMouseEnter={() => highlightNode(store.notify, id)}
220-
data-selected={isSelected}
210+
data-selected={as.selected === id}
221211
data-id={id}
222212
data-depth={node.depth}
223213
style={`top: ${props.top}px;`}

src/view/components/elements/VirtualizedList.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { RefObject, VNode } from "preact";
2-
import { useEffect, useLayoutEffect, useMemo, useState } from "preact/hooks";
2+
import {
3+
useCallback,
4+
useEffect,
5+
useLayoutEffect,
6+
useMemo,
7+
useRef,
8+
useState,
9+
} from "preact/hooks";
310
import { useResize } from "../utils";
411

512
export interface VirtualizedListProps<T> {
@@ -20,6 +27,37 @@ export function useVirtualizedList<T>({
2027
const [height, setHeight] = useState(0);
2128
const [scroll, setScroll] = useState(0);
2229

30+
let idx = Math.max(0, Math.floor(scroll / rowHeight) - bufferCount);
31+
const max = idx + Math.ceil(height / rowHeight) + bufferCount;
32+
let top = idx * rowHeight;
33+
34+
const timeoutRef = useRef<any>(null);
35+
const scrollToItem = useCallback(
36+
(item: T) => {
37+
const nextIdx = items.findIndex(t => t === item);
38+
if (nextIdx < 0) return;
39+
40+
const pos = Math.floor(nextIdx * rowHeight);
41+
if (top > pos || max < pos) {
42+
// Clamp to available range to avoid overflow
43+
const maxScroll = Math.floor(rowHeight * items.length - height);
44+
const nextPos = Math.max(0, Math.min(pos, maxScroll));
45+
46+
// Debounce scroll to avoid flickering when quickly hovering
47+
// a bunch of elements
48+
if (timeoutRef.current) {
49+
clearTimeout(timeoutRef.current);
50+
}
51+
timeoutRef.current = setTimeout(() => {
52+
if (container.current) {
53+
container.current.scrollTop = nextPos;
54+
}
55+
}, 100);
56+
}
57+
},
58+
[items],
59+
);
60+
2361
useLayoutEffect(() => {
2462
if (container.current) {
2563
setHeight(container.current.clientHeight);
@@ -52,10 +90,6 @@ export function useVirtualizedList<T>({
5290
}
5391
}, []);
5492

55-
let idx = Math.max(0, Math.floor(scroll / rowHeight) - bufferCount);
56-
const max = idx + Math.ceil(height / rowHeight) + bufferCount;
57-
let top = idx * rowHeight;
58-
5993
const vnodes = useMemo(() => {
6094
const vnodes: VNode[] = [];
6195
while (idx < items.length && idx <= max) {
@@ -69,5 +103,6 @@ export function useVirtualizedList<T>({
69103
return {
70104
containerHeight: rowHeight * items.length,
71105
children: vnodes,
106+
scrollToItem,
72107
};
73108
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import { h, Fragment, render } from "preact";
2+
3+
const css = "border: 1px solid peachpuff; padding: 1rem;";
4+
5+
let i = 0;
6+
function ChildItemName({ children }) {
7+
return (
8+
<div key={i++} style={css}>
9+
{children}
10+
</div>
11+
);
12+
}
13+
14+
function Foo() {
15+
return (
16+
<h1 id="select-me" data-testid="select-me">
17+
select me
18+
</h1>
19+
);
20+
}
21+
22+
function App() {
23+
return (
24+
<Fragment>
25+
<ChildItemName>
26+
<ChildItemName>
27+
<ChildItemName>
28+
<ChildItemName>
29+
<ChildItemName>
30+
<ChildItemName>
31+
<ChildItemName>
32+
<ChildItemName>
33+
<ChildItemName>
34+
<ChildItemName>
35+
<ChildItemName>
36+
<ChildItemName>
37+
<ChildItemName>
38+
<ChildItemName>
39+
<ChildItemName>
40+
<ChildItemName>
41+
<ChildItemName>Deep</ChildItemName>
42+
</ChildItemName>
43+
</ChildItemName>
44+
</ChildItemName>
45+
</ChildItemName>
46+
</ChildItemName>
47+
</ChildItemName>
48+
</ChildItemName>
49+
</ChildItemName>
50+
</ChildItemName>
51+
</ChildItemName>
52+
</ChildItemName>
53+
</ChildItemName>
54+
</ChildItemName>
55+
</ChildItemName>
56+
</ChildItemName>
57+
</ChildItemName>
58+
<ChildItemName>
59+
<ChildItemName>
60+
<ChildItemName>
61+
<ChildItemName>
62+
<ChildItemName>
63+
<ChildItemName>
64+
<ChildItemName>
65+
<ChildItemName>
66+
<ChildItemName>Deep</ChildItemName>
67+
</ChildItemName>
68+
</ChildItemName>
69+
</ChildItemName>
70+
</ChildItemName>
71+
</ChildItemName>
72+
</ChildItemName>
73+
</ChildItemName>
74+
</ChildItemName>
75+
<ChildItemName>
76+
<ChildItemName>
77+
<ChildItemName>
78+
<ChildItemName>
79+
<ChildItemName>
80+
<ChildItemName>
81+
<ChildItemName>
82+
<ChildItemName>
83+
<ChildItemName>
84+
<ChildItemName>
85+
<ChildItemName>
86+
<ChildItemName>
87+
<ChildItemName>
88+
<ChildItemName>
89+
<ChildItemName>
90+
<ChildItemName>
91+
<ChildItemName>Deep</ChildItemName>
92+
</ChildItemName>
93+
</ChildItemName>
94+
</ChildItemName>
95+
</ChildItemName>
96+
</ChildItemName>
97+
</ChildItemName>
98+
</ChildItemName>
99+
</ChildItemName>
100+
</ChildItemName>
101+
</ChildItemName>
102+
</ChildItemName>
103+
</ChildItemName>
104+
</ChildItemName>
105+
</ChildItemName>
106+
</ChildItemName>
107+
</ChildItemName>
108+
<ChildItemName>
109+
<ChildItemName>
110+
<ChildItemName>
111+
<ChildItemName>
112+
<ChildItemName>
113+
<ChildItemName>
114+
<ChildItemName>
115+
<ChildItemName>
116+
<ChildItemName>
117+
<ChildItemName>
118+
<ChildItemName>
119+
<ChildItemName>
120+
<ChildItemName>
121+
<ChildItemName>
122+
<ChildItemName>
123+
<ChildItemName>
124+
<ChildItemName>Deep</ChildItemName>
125+
</ChildItemName>
126+
</ChildItemName>
127+
</ChildItemName>
128+
</ChildItemName>
129+
</ChildItemName>
130+
</ChildItemName>
131+
</ChildItemName>
132+
</ChildItemName>
133+
</ChildItemName>
134+
</ChildItemName>
135+
</ChildItemName>
136+
</ChildItemName>
137+
</ChildItemName>
138+
</ChildItemName>
139+
</ChildItemName>
140+
</ChildItemName>
141+
<ChildItemName>
142+
<ChildItemName>
143+
<ChildItemName>
144+
<ChildItemName>
145+
<ChildItemName>
146+
<ChildItemName>
147+
<ChildItemName>
148+
<ChildItemName>
149+
<ChildItemName>
150+
<ChildItemName>
151+
<ChildItemName>
152+
<ChildItemName>
153+
<ChildItemName>
154+
<ChildItemName>
155+
<ChildItemName>
156+
<ChildItemName>
157+
<ChildItemName>Deep</ChildItemName>
158+
</ChildItemName>
159+
</ChildItemName>
160+
</ChildItemName>
161+
</ChildItemName>
162+
</ChildItemName>
163+
</ChildItemName>
164+
</ChildItemName>
165+
</ChildItemName>
166+
</ChildItemName>
167+
</ChildItemName>
168+
</ChildItemName>
169+
</ChildItemName>
170+
</ChildItemName>
171+
</ChildItemName>
172+
</ChildItemName>
173+
</ChildItemName>
174+
<ChildItemName>
175+
<ChildItemName>
176+
<ChildItemName>
177+
<ChildItemName>
178+
<ChildItemName>
179+
<ChildItemName>
180+
<ChildItemName>
181+
<ChildItemName>
182+
<ChildItemName>
183+
<ChildItemName>
184+
<ChildItemName>
185+
<ChildItemName>
186+
<ChildItemName>
187+
<ChildItemName>
188+
<ChildItemName>
189+
<ChildItemName>
190+
<ChildItemName>Deep</ChildItemName>
191+
</ChildItemName>
192+
</ChildItemName>
193+
</ChildItemName>
194+
</ChildItemName>
195+
</ChildItemName>
196+
</ChildItemName>
197+
</ChildItemName>
198+
</ChildItemName>
199+
</ChildItemName>
200+
</ChildItemName>
201+
</ChildItemName>
202+
</ChildItemName>
203+
</ChildItemName>
204+
</ChildItemName>
205+
</ChildItemName>
206+
</ChildItemName>
207+
<ChildItemName>
208+
<ChildItemName>
209+
<ChildItemName>
210+
<ChildItemName>
211+
<ChildItemName>
212+
<ChildItemName>
213+
<ChildItemName>
214+
<ChildItemName>
215+
<ChildItemName>
216+
<ChildItemName>
217+
<ChildItemName>
218+
<ChildItemName>
219+
<ChildItemName>
220+
<ChildItemName>
221+
<ChildItemName>
222+
<ChildItemName>
223+
<ChildItemName>Deep</ChildItemName>
224+
</ChildItemName>
225+
</ChildItemName>
226+
</ChildItemName>
227+
</ChildItemName>
228+
</ChildItemName>
229+
</ChildItemName>
230+
<Foo />
231+
</ChildItemName>
232+
</ChildItemName>
233+
</ChildItemName>
234+
</ChildItemName>
235+
</ChildItemName>
236+
</ChildItemName>
237+
</ChildItemName>
238+
</ChildItemName>
239+
</ChildItemName>
240+
</ChildItemName>
241+
</Fragment>
242+
);
243+
}
244+
245+
render(<App />, document.getElementById("app"));
246+
247+
// eslint-disable-next-line no-console
248+
document.addEventListener("click", e => console.log(e), true);

0 commit comments

Comments
 (0)