react-use-section-observer 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 +38 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.es.js +65 -0
- package/dist/index.umd.js +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# react-use-section-observer
|
|
2
|
+
|
|
3
|
+
A lightweight React hook to track element visibility using the Intersection Observer API and automatically apply an active class to matching links.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🎥 Demo
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ✨ Features
|
|
14
|
+
|
|
15
|
+
- Tracks visibility of multiple sections
|
|
16
|
+
- Automatically updates matching anchor links
|
|
17
|
+
- Supports custom root containers
|
|
18
|
+
- Supports multiple class names (e.g. Tailwind)
|
|
19
|
+
- No DOM mutation via `data-*`
|
|
20
|
+
- No external dependencies
|
|
21
|
+
- Fully typed (TypeScript)
|
|
22
|
+
- Adjustable to your DOM structure
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## ⚠️ Limitations
|
|
27
|
+
|
|
28
|
+
- Only vertical scrolling (Y-axis) is supported.
|
|
29
|
+
- Links must use the `href="#id"` format.
|
|
30
|
+
- Requires browser support for `IntersectionObserver`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📦 Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install react-use-section-observer
|
|
38
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type DependencyList } from "react";
|
|
2
|
+
export interface SectionObserverOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Array of section IDs to observe.
|
|
5
|
+
* Each ID should correspond to an element in the DOM that you want to track.
|
|
6
|
+
*/
|
|
7
|
+
ids: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Enable or disable the observer.
|
|
10
|
+
* If your sections are dynamically added or removed, you can set this to false
|
|
11
|
+
* until the sections are ready, then set it to true to start observing.
|
|
12
|
+
*/
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The root element for the Intersection Observer.
|
|
16
|
+
* Defaults to the viewport if not specified.
|
|
17
|
+
*/
|
|
18
|
+
viewportRoot?: Element | null;
|
|
19
|
+
/** Class name(s) to apply to the active link */
|
|
20
|
+
activeClassName?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Callback function invoked when the active element changes.
|
|
23
|
+
* Receives an object containing the active link and the corresponding active section element.
|
|
24
|
+
*/
|
|
25
|
+
onActiveChange?: (params: {
|
|
26
|
+
link: Element;
|
|
27
|
+
activeElement: Element | null;
|
|
28
|
+
}) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Optional accessors to customize how links are selected and how active state is determined.
|
|
31
|
+
* This allows you to adapt the hook to different DOM structures and link formats.
|
|
32
|
+
*/
|
|
33
|
+
linkAccessors?: {
|
|
34
|
+
getLinkSelector: (id: string) => string;
|
|
35
|
+
getIsLinkActive: (params: {
|
|
36
|
+
link: Element;
|
|
37
|
+
activeElement: Element;
|
|
38
|
+
}) => boolean;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* React hook to observe sections and update corresponding links
|
|
43
|
+
* based on their visibility in the viewport.
|
|
44
|
+
*/
|
|
45
|
+
export declare function useSectionObserver({ ids, enabled, viewportRoot, activeClassName, onActiveChange, linkAccessors: linkAccessor, }: SectionObserverOptions, deps?: DependencyList): void;
|
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useRef as b, useEffect as p } from "react";
|
|
2
|
+
function L({
|
|
3
|
+
ids: n,
|
|
4
|
+
enabled: r = !0,
|
|
5
|
+
viewportRoot: t = null,
|
|
6
|
+
activeClassName: o = "active",
|
|
7
|
+
onActiveChange: l,
|
|
8
|
+
linkAccessors: g
|
|
9
|
+
}, d = []) {
|
|
10
|
+
const {
|
|
11
|
+
getLinkSelector: v = (c) => `a[href$="#${c}"]`,
|
|
12
|
+
getIsLinkActive: E = ({
|
|
13
|
+
link: c,
|
|
14
|
+
activeElement: i
|
|
15
|
+
}) => c.getAttribute("href")?.endsWith(`#${i?.id}`)
|
|
16
|
+
} = g || {}, u = b(/* @__PURE__ */ new Map());
|
|
17
|
+
p(() => {
|
|
18
|
+
if (!r || !n.length) return;
|
|
19
|
+
const c = n.map((e) => document.getElementById(e)).filter((e) => !!e), i = n.map((e) => document.querySelector(v(e))).filter((e) => !!e), a = new IntersectionObserver(
|
|
20
|
+
(e) => {
|
|
21
|
+
e.forEach((s) => {
|
|
22
|
+
u.current.set(s.target, s);
|
|
23
|
+
});
|
|
24
|
+
const f = A(u.current);
|
|
25
|
+
i.forEach((s) => {
|
|
26
|
+
const h = f && E({ link: s, activeElement: f });
|
|
27
|
+
m(s, o, !!h), h && l && l({ link: s, activeElement: f });
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
root: t,
|
|
32
|
+
threshold: B()
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
return c.forEach((e) => a.observe(e)), () => {
|
|
36
|
+
a.disconnect(), u.current.clear(), i.forEach((e) => m(e, o, !1));
|
|
37
|
+
};
|
|
38
|
+
}, [
|
|
39
|
+
n.join(","),
|
|
40
|
+
r,
|
|
41
|
+
t,
|
|
42
|
+
o,
|
|
43
|
+
l,
|
|
44
|
+
...d
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
function A(n) {
|
|
48
|
+
const r = Array.from(n.values()).filter(
|
|
49
|
+
(t) => t.isIntersecting
|
|
50
|
+
);
|
|
51
|
+
return r.length ? (r.sort((t, o) => o.intersectionRatio - t.intersectionRatio), r[0].target) : null;
|
|
52
|
+
}
|
|
53
|
+
function m(n, r, t) {
|
|
54
|
+
const o = r.trim().split(/\s+/);
|
|
55
|
+
n.classList[t ? "add" : "remove"](...o);
|
|
56
|
+
}
|
|
57
|
+
function B(n = 0.1) {
|
|
58
|
+
const r = [];
|
|
59
|
+
for (let t = 0; t <= 1; t += n)
|
|
60
|
+
r.push(Number(t.toFixed(2)));
|
|
61
|
+
return r;
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
L as useSectionObserver
|
|
65
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(o,i){typeof exports=="object"&&typeof module<"u"?i(exports,require("react")):typeof define=="function"&&define.amd?define(["exports","react"],i):(o=typeof globalThis<"u"?globalThis:o||self,i(o.useSectionObserver={},o.React))})(this,(function(o,i){"use strict";function g({ids:n,enabled:r=!0,viewportRoot:t=null,activeClassName:s="active",onActiveChange:l,linkAccessors:E},S=[]){const{getLinkSelector:y=u=>`a[href$="#${u}"]`,getIsLinkActive:O=({link:u,activeElement:f})=>u.getAttribute("href")?.endsWith(`#${f?.id}`)}=E||{},d=i.useRef(new Map);i.useEffect(()=>{if(!r||!n.length)return;const u=n.map(e=>document.getElementById(e)).filter(e=>!!e),f=n.map(e=>document.querySelector(y(e))).filter(e=>!!e),m=new IntersectionObserver(e=>{e.forEach(c=>{d.current.set(c.target,c)});const a=p(d.current);f.forEach(c=>{const b=a&&O({link:c,activeElement:a});h(c,s,!!b),b&&l&&l({link:c,activeElement:a})})},{root:t,threshold:v()});return u.forEach(e=>m.observe(e)),()=>{m.disconnect(),d.current.clear(),f.forEach(e=>h(e,s,!1))}},[n.join(","),r,t,s,l,...S])}function p(n){const r=Array.from(n.values()).filter(t=>t.isIntersecting);return r.length?(r.sort((t,s)=>s.intersectionRatio-t.intersectionRatio),r[0].target):null}function h(n,r,t){const s=r.trim().split(/\s+/);n.classList[t?"add":"remove"](...s)}function v(n=.1){const r=[];for(let t=0;t<=1;t+=n)r.push(Number(t.toFixed(2)));return r}o.useSectionObserver=g,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-use-section-observer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A react hook to track the visibility of elements in the viewport using Intersection Observer API",
|
|
5
|
+
"homepage": "https://github.com/theo-js/react-use-section-observer#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/theo-js/react-use-section-observer/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/theo-js/react-use-section-observer.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": {
|
|
15
|
+
"email": "dev.js.theo@gmail.com",
|
|
16
|
+
"name": "theo-js",
|
|
17
|
+
"url": "https://github.com/theo-js"
|
|
18
|
+
},
|
|
19
|
+
"main": "dist/index.umd.js",
|
|
20
|
+
"module": "dist/index.es.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build:types": "tsc",
|
|
28
|
+
"build:js": "vite build",
|
|
29
|
+
"copy:types": "xcopy types\\* dist /E /I /Y",
|
|
30
|
+
"build": "npm run build:types && npm run build:js && npm run copy:types"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"react",
|
|
34
|
+
"hook",
|
|
35
|
+
"intersection-observer",
|
|
36
|
+
"visibility-tracker"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"react": "^19.2.4",
|
|
40
|
+
"react-dom": "^19.2.4"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/react": "^19.2.14",
|
|
44
|
+
"@types/react-dom": "^19.2.3",
|
|
45
|
+
"tsup": "^8.5.1",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vite": "^7.3.1",
|
|
48
|
+
"@vitejs/plugin-react": "^5.1.4"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": "^19.0.0",
|
|
52
|
+
"react-dom": "^19.0.0"
|
|
53
|
+
},
|
|
54
|
+
"workspaces": [
|
|
55
|
+
"playground"
|
|
56
|
+
]
|
|
57
|
+
}
|