react-intersection-observer
Version:
Monitor if a component is inside the viewport, using IntersectionObserver API
436 lines (429 loc) • 11.8 kB
JavaScript
"use client";
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/InView.tsx
import * as React from "react";
// src/observe.ts
var observerMap = /* @__PURE__ */ new Map();
var RootIds = /* @__PURE__ */ new WeakMap();
var rootId = 0;
var unsupportedValue;
function defaultFallbackInView(inView) {
unsupportedValue = inView;
}
function getRootId(root) {
if (!root) return "0";
if (RootIds.has(root)) return RootIds.get(root);
rootId += 1;
RootIds.set(root, rootId.toString());
return RootIds.get(root);
}
function optionsToId(options) {
return Object.keys(options).sort().filter(
(key) => options[key] !== void 0
).map((key) => {
return `${key}_${key === "root" ? getRootId(options.root) : options[key]}`;
}).toString();
}
function createObserver(options) {
const id = optionsToId(options);
let instance = observerMap.get(id);
if (!instance) {
const elements = /* @__PURE__ */ new Map();
let thresholds;
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
var _a;
const inView = entry.isIntersecting && thresholds.some((threshold) => entry.intersectionRatio >= threshold);
if (options.trackVisibility && typeof entry.isVisible === "undefined") {
entry.isVisible = inView;
}
[...(_a = elements.get(entry.target)) != null ? _a : []].forEach((callback) => {
callback(inView, entry);
});
});
}, options);
thresholds = observer.thresholds || (Array.isArray(options.threshold) ? options.threshold : [options.threshold || 0]);
instance = {
id,
observer,
elements
};
observerMap.set(id, instance);
}
return instance;
}
function observe(element, callback, options = {}, fallbackInView = unsupportedValue) {
if (typeof window.IntersectionObserver === "undefined" && fallbackInView !== void 0) {
const bounds = element.getBoundingClientRect();
callback(fallbackInView, {
isIntersecting: fallbackInView,
target: element,
intersectionRatio: typeof options.threshold === "number" ? options.threshold : 0,
time: 0,
boundingClientRect: bounds,
intersectionRect: bounds,
rootBounds: bounds
});
return () => {
};
}
const { id, observer, elements } = createObserver(options);
const callbacks = elements.get(element) || [];
if (!elements.has(element)) {
elements.set(element, callbacks);
}
callbacks.push(callback);
observer.observe(element);
let unobserved = false;
return function unobserve() {
if (unobserved) return;
unobserved = true;
callbacks.splice(callbacks.indexOf(callback), 1);
if (callbacks.length === 0) {
elements.delete(element);
observer.unobserve(element);
}
if (elements.size === 0) {
observer.disconnect();
observerMap.delete(id);
}
};
}
// src/InView.tsx
function isPlainChildren(props) {
return typeof props.children !== "function";
}
var InView = class extends React.Component {
constructor(props) {
super(props);
__publicField(this, "node", null);
__publicField(this, "_unobserveCb", null);
__publicField(this, "lastInView");
__publicField(this, "handleNode", (node) => {
if (this.node) {
this.unobserve();
if (!node && !this.props.triggerOnce && !this.props.skip) {
this.setState({ inView: !!this.props.initialInView, entry: void 0 });
this.lastInView = this.props.initialInView;
}
}
this.node = node ? node : null;
this.observeNode();
});
__publicField(this, "handleChange", (inView, entry) => {
const previousInView = this.lastInView;
this.lastInView = inView;
if (previousInView === void 0 && !inView) {
return;
}
if (inView && this.props.triggerOnce) {
this.unobserve();
}
if (!isPlainChildren(this.props)) {
this.setState({ inView, entry });
}
if (this.props.onChange) {
this.props.onChange(inView, entry);
}
});
this.state = {
inView: !!props.initialInView,
entry: void 0
};
this.lastInView = props.initialInView;
}
componentDidMount() {
this.unobserve();
this.observeNode();
}
componentDidUpdate(prevProps) {
if (prevProps.rootMargin !== this.props.rootMargin || prevProps.scrollMargin !== this.props.scrollMargin || prevProps.root !== this.props.root || prevProps.threshold !== this.props.threshold || prevProps.skip !== this.props.skip || prevProps.trackVisibility !== this.props.trackVisibility || prevProps.delay !== this.props.delay) {
this.unobserve();
this.observeNode();
}
}
componentWillUnmount() {
this.unobserve();
}
observeNode() {
if (!this.node || this.props.skip) return;
const {
threshold,
root,
rootMargin,
scrollMargin,
trackVisibility,
delay,
fallbackInView
} = this.props;
if (this.lastInView === void 0) {
this.lastInView = this.props.initialInView;
}
this._unobserveCb = observe(
this.node,
this.handleChange,
{
threshold,
root,
rootMargin,
scrollMargin,
trackVisibility,
delay
},
fallbackInView
);
}
unobserve() {
if (this._unobserveCb) {
this._unobserveCb();
this._unobserveCb = null;
}
}
render() {
const { children } = this.props;
if (typeof children === "function") {
const { inView, entry } = this.state;
return children({ inView, entry, ref: this.handleNode });
}
const {
as,
triggerOnce,
threshold,
root,
rootMargin,
scrollMargin,
onChange,
skip,
trackVisibility,
delay,
initialInView,
fallbackInView,
...props
} = this.props;
return React.createElement(
as || "div",
{ ref: this.handleNode, ...props },
children
);
}
};
// src/useInView.tsx
import * as React3 from "react";
// src/useIntersectionObserverRef.ts
import * as React2 from "react";
var useInsertionEffect = Reflect.get(React2, "useInsertionEffect");
var useSyncEffect = useInsertionEffect != null ? useInsertionEffect : React2.useEffect;
function supportsRefCleanup(version2) {
return (version2 == null ? void 0 : version2.startsWith("19.")) || false;
}
var canUseRefCleanup = supportsRefCleanup(React2.version);
function useIntersectionObserverRef(onIntersectionChange, {
threshold,
root,
rootMargin,
scrollMargin,
trackVisibility,
delay,
fallbackInView,
skip,
triggerOnce
}) {
const onIntersectionChangeRef = React2.useRef(onIntersectionChange);
const observerStateRef = React2.useRef({
node: null,
stop: void 0,
owner: null
});
if (!useInsertionEffect) {
onIntersectionChangeRef.current = onIntersectionChange;
}
useSyncEffect(() => {
onIntersectionChangeRef.current = onIntersectionChange;
}, [onIntersectionChange]);
return React2.useCallback(
function setRef(element) {
const observerState = observerStateRef.current;
if (!element && observerState.owner !== setRef) {
return;
}
if (element === observerState.node) {
observerState.owner = setRef;
return canUseRefCleanup ? observerState.stop : void 0;
}
const cleanup = observerState.stop;
observerState.stop = void 0;
cleanup == null ? void 0 : cleanup();
if (!element || skip) {
observerState.node = null;
observerState.owner = element ? setRef : null;
return;
}
observerState.node = element;
observerState.owner = setRef;
let destroyObserver;
let previousInView;
function stopObserving() {
destroyObserver == null ? void 0 : destroyObserver();
if (observerState.stop === stopObserving) {
observerState.node = null;
observerState.stop = void 0;
}
}
observerState.stop = stopObserving;
destroyObserver = observe(
element,
(inView, entry) => {
onIntersectionChangeRef.current(
inView,
entry,
previousInView
);
previousInView = inView;
if (triggerOnce && inView) stopObserving();
},
{
threshold,
root,
rootMargin,
scrollMargin,
trackVisibility,
delay
},
fallbackInView
);
if (observerState.stop !== stopObserving) destroyObserver();
return canUseRefCleanup ? observerState.stop : void 0;
},
[
Array.isArray(threshold) ? threshold.toString() : threshold,
root,
rootMargin,
scrollMargin,
trackVisibility,
delay,
fallbackInView,
skip,
triggerOnce
]
);
}
// src/useInView.tsx
var useIsomorphicLayoutEffect = typeof window === "undefined" ? React3.useEffect : React3.useLayoutEffect;
function useInView({
threshold,
delay,
trackVisibility,
rootMargin,
scrollMargin,
root,
triggerOnce,
skip,
initialInView,
fallbackInView,
onChange
} = {}) {
const lastInViewRef = React3.useRef(initialInView);
const [state, setState] = React3.useState({
inView: !!initialInView,
entry: void 0
});
const observerRef = useIntersectionObserverRef(
(inView, entry) => {
const previousInView = lastInViewRef.current;
lastInViewRef.current = inView;
if (previousInView === void 0 && !inView) {
return;
}
setState({ inView, entry });
onChange == null ? void 0 : onChange(inView, entry);
},
{
threshold,
root,
rootMargin,
scrollMargin,
trackVisibility,
delay,
fallbackInView,
skip,
triggerOnce
}
);
const refState = React3.useRef({
node: null,
reset: false
});
const setRef = React3.useCallback(
function setRef2(node) {
if (node) {
refState.current.node = node;
refState.current.reset = false;
} else if (refState.current.node) {
refState.current.node = null;
refState.current.reset = true;
}
const cleanup = observerRef(node);
if (!cleanup) return;
return () => {
cleanup();
if (refState.current.node === node) {
refState.current.node = null;
refState.current.reset = true;
}
};
},
[observerRef]
);
useIsomorphicLayoutEffect(() => {
if (!refState.current.reset) return;
refState.current.reset = false;
if (triggerOnce || skip) return;
setState({ inView: !!initialInView, entry: void 0 });
lastInViewRef.current = initialInView;
});
const result = [setRef, state.inView, state.entry];
result.ref = result[0];
result.inView = result[1];
result.entry = result[2];
return result;
}
// src/useOnInView.tsx
var useOnInView = (onIntersectionChange, {
threshold,
root,
rootMargin,
scrollMargin,
trackVisibility,
delay,
triggerOnce,
skip
} = {}) => {
return useIntersectionObserverRef(
(inView, entry, previousInView) => {
if (previousInView === void 0 && !inView) {
return;
}
onIntersectionChange(inView, entry);
},
{
threshold,
root,
rootMargin,
scrollMargin,
trackVisibility,
delay,
triggerOnce,
skip
}
);
};
export {
InView,
defaultFallbackInView,
observe,
useInView,
useOnInView
};
//# sourceMappingURL=index.mjs.map