-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathvirtual-item.tsx
125 lines (98 loc) · 2.97 KB
/
virtual-item.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { type JSX, createSignal, onCleanup, runWithOwner } from 'solid-js';
import { UNSAFE_routerEvents, UNSAFE_useViewContext } from '~/lib/navigation/router';
import { intersectionCallback } from '~/lib/observer';
import { requestIdle } from '~/lib/utils/misc';
const intersectionObserver = new IntersectionObserver(intersectionCallback, { rootMargin: `106.25% 0%` });
const createVirtualStore = (ctx: ReturnType<typeof UNSAFE_useViewContext>) => {
return runWithOwner(ctx.owner, () => {
let disabled = false;
onCleanup(
UNSAFE_routerEvents.on(ctx.route.id, (event) => {
disabled = !event.focus;
}),
);
return {
get disabled() {
return disabled;
},
};
})!;
};
const virtualStoreMap = new WeakMap<
ReturnType<typeof UNSAFE_useViewContext>,
ReturnType<typeof createVirtualStore>
>();
const dummyStore: ReturnType<typeof createVirtualStore> = {
disabled: false,
};
const getVirtualStore = (ctx: ReturnType<typeof UNSAFE_useViewContext> | undefined) => {
if (ctx === undefined) {
return dummyStore;
}
let store = virtualStoreMap.get(ctx);
if (store === undefined) {
virtualStoreMap.set(ctx, (store = createVirtualStore(ctx)));
}
return store;
};
export interface VirtualItemProps {
estimateHeight?: number;
children?: JSX.Element;
}
const VirtualItem = (props: VirtualItemProps) => {
let _entry: IntersectionObserverEntry | undefined;
let _height: number | undefined = props.estimateHeight;
let _intersecting: boolean = false;
const store = getVirtualStore(UNSAFE_useViewContext());
const [intersecting, setIntersecting] = createSignal(_intersecting);
const shouldHide = () => !intersecting() && _height !== undefined;
const handleIntersect = (nextEntry: IntersectionObserverEntry) => {
_entry = undefined;
if (store.disabled) {
return;
}
const prev = _intersecting;
const next = nextEntry.isIntersecting;
if (!prev && next) {
// hidden -> visible
setIntersecting((_intersecting = next));
} else if (prev && !next) {
// visible -> hidden
// unmounting is cheap, but we don't need to immediately unmount it, say
// for scenarios where layout is still being figured out and we don't
// actually know where the virtual container is gonna end up.
_entry = nextEntry;
requestIdle(() => {
// bail out if it's no longer us.
if (_entry !== nextEntry) {
return;
}
// reduce the precision
_height = ((_entry.boundingClientRect.height * 1000) | 0) / 1000;
_entry = undefined;
setIntersecting((_intersecting = next));
});
}
};
return (
<article
ref={startMeasure}
class="shrink-0 contain-content"
style={{ height: shouldHide() ? `${_height ?? 0}px` : undefined }}
prop:$onintersect={handleIntersect}
>
{(() => {
if (!shouldHide()) {
return props.children;
}
})()}
</article>
);
};
export default VirtualItem;
const startMeasure = (node: HTMLElement) => {
intersectionObserver.observe(node);
onCleanup(() => {
intersectionObserver.unobserve(node);
});
};