Skip to content

Commit e1ea64d

Browse files
committed
feat(toolkit-react): ✨ create core version of useScrollObserver hook for scroll detection
1 parent 878752d commit e1ea64d

File tree

7 files changed

+87
-171
lines changed

7 files changed

+87
-171
lines changed

apps/dev/src/client/AnotherApp.tsx

+24-15
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
1-
import { useResource } from "@zayne-labs/toolkit-react";
1+
import { useScrollObserver } from "@zayne-labs/toolkit-react";
22

33
function AnotherApp() {
4-
const resource = useResource({
5-
fn: () => fetch("https://jsonplaceholder.typicode.com/todos/1").then((res) => res.json()),
6-
});
4+
const { isScrolled, observedElementRef } = useScrollObserver();
75

8-
console.info(resource);
6+
console.info({ isScrolled });
97

108
return (
11-
<>
9+
<div>
1210
<style>
1311
{`
14-
.btn {
15-
background-color: red;
16-
margin-top: 10px;
12+
@scope {
13+
button {
14+
background-color: red;
15+
margin-top: 10px;
16+
}
17+
18+
header {
19+
position: sticky;
20+
top: 0;
21+
height: 100px;
22+
background-color: blue;
23+
width: 100%;
24+
margin-bottom: 10000px;
25+
}
1726
}
1827
`}
1928
</style>
20-
<div>
21-
<button className="btn" type="button">
22-
Force Render
23-
</button>
24-
</div>
25-
</>
29+
<header ref={observedElementRef}>Header</header>
30+
<div />
31+
<button className="btn" type="button">
32+
Force Render
33+
</button>
34+
</div>
2635
);
2736
}
2837
export default AnotherApp;

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
"packageManager": "[email protected]",
66
"author": "Ryan Zayne",
77
"scripts": {
8-
"build": "pnpm --filter ./packages/* build",
8+
"build": "pnpm --filter \"./packages/*\" build",
99
"build:dev": "pnpm --filter ./packages/* build:dev",
10-
"build:test": "pnpm --filter ./packages/* build:test",
10+
"build:test": "pnpm --filter \"./packages/*\" build:test",
1111
"bump": "bumpp",
1212
"dev": "pnpm --filter ./packages/* dev",
1313
"inspect:eslint-config": "pnpx @eslint/config-inspector@latest",

packages/toolkit-core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"size-limit": [
7474
{
7575
"path": "./src/index.ts",
76-
"limit": "3.6 kb"
76+
"limit": "3.7 kb"
7777
}
7878
]
7979
}

packages/toolkit-core/src/createDragScroll.ts

-127
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { isBrowser } from "./constants";
2+
3+
export type ScrollObserverOptions = IntersectionObserverInit & {
4+
onIntersection?: (entry: IntersectionObserverEntry, observer: IntersectionObserver) => void;
5+
};
6+
7+
export const createScrollObserver = <TElement extends HTMLElement>(
8+
options: ScrollObserverOptions = {}
9+
) => {
10+
const { rootMargin = "10px 0px 0px 0px", ...restOfOptions } = options;
11+
12+
const elementObserver = isBrowser()
13+
? new IntersectionObserver(
14+
(entries, observer) => entries.forEach((entry) => options.onIntersection?.(entry, observer)),
15+
{ rootMargin, ...restOfOptions }
16+
)
17+
: null;
18+
19+
const handleObservation = (element: TElement | null) => {
20+
const scrollWatcher = document.createElement("span");
21+
scrollWatcher.dataset.scrollWatcher = "";
22+
23+
element?.before(scrollWatcher);
24+
25+
if (!elementObserver) return;
26+
27+
elementObserver.observe(scrollWatcher);
28+
29+
const cleanupFn = () => {
30+
scrollWatcher.remove();
31+
elementObserver.disconnect();
32+
};
33+
34+
return cleanupFn;
35+
};
36+
37+
return { elementObserver, handleObservation };
38+
};

packages/toolkit-core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from "./constants";
44
export { copyToClipboard } from "./copyToClipboard";
55
export * from "./createExternalStore";
66
export * from "./createLocationStore";
7+
export * from "./createScrollObserver";
78
export * from "./createStore";
89
export { debounce } from "./debounce";
910
export * from "./handleFileValidation";

packages/toolkit-react/src/hooks/useScrollObserver.ts

+21-26
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,38 @@
1-
import { isBrowser } from "@zayne-labs/toolkit-core";
1+
import { type ScrollObserverOptions, createScrollObserver } from "@zayne-labs/toolkit-core";
22
import { type RefCallback, useState } from "react";
33
import { useCallbackRef } from "./useCallbackRef";
44
import { useConstant } from "./useConstant";
55

6-
const useScrollObserver = <TElement extends HTMLElement>(options: IntersectionObserverInit = {}) => {
7-
const { rootMargin = "10px 0px 0px 0px", ...restOfOptions } = options;
6+
const useScrollObserver = <TElement extends HTMLElement>(options: ScrollObserverOptions = {}) => {
7+
const { onIntersection, rootMargin = "10px 0px 0px 0px", ...restOfOptions } = options;
88

99
const [isScrolled, setIsScrolled] = useState(false);
1010

11-
const elementObserver = useConstant(() => {
12-
if (!isBrowser()) return;
11+
const savedOnIntersection = useCallbackRef(onIntersection);
1312

14-
return new IntersectionObserver(
15-
([entry]) => {
16-
if (!entry) return;
17-
setIsScrolled(!entry.isIntersecting);
18-
},
19-
{ rootMargin, ...restOfOptions }
20-
);
21-
});
22-
23-
const observedElementRef: RefCallback<TElement> = useCallbackRef((element) => {
24-
const scrollWatcher = document.createElement("span");
25-
scrollWatcher.dataset.scrollWatcher = "";
13+
const { handleObservation } = useConstant(() => {
14+
return createScrollObserver({
15+
onIntersection: (entry, observer) => {
16+
const newIsScrolledState = !entry.isIntersecting;
2617

27-
element?.before(scrollWatcher);
18+
setIsScrolled(newIsScrolledState);
2819

29-
if (!elementObserver) return;
20+
// eslint-disable-next-line no-param-reassign -- Mutation is fine here
21+
(entry.target as HTMLElement).dataset.scrolled = String(newIsScrolledState);
3022

31-
elementObserver.observe(scrollWatcher);
23+
savedOnIntersection(entry, observer);
24+
},
25+
rootMargin,
26+
...restOfOptions,
27+
});
28+
});
3229

33-
const cleanupFn = () => {
34-
scrollWatcher.remove();
35-
elementObserver.disconnect();
36-
};
30+
const observedElementRef: RefCallback<TElement> = useCallbackRef((element) => {
31+
const cleanupFn = handleObservation(element);
3732

38-
// React 18 may not call the cleanup function so we need to call it manually on element unmount
33+
// == React 18 may not call the cleanup function so we need to call it manually on element unmount
3934
if (!element) {
40-
cleanupFn();
35+
cleanupFn?.();
4136
return;
4237
}
4338

0 commit comments

Comments
 (0)