@react-spring/parallax
Version:
```bash yarn add @react-spring/parallax ```
229 lines (227 loc) • 7.38 kB
JavaScript
import * as React from "react";
import { useContext, useEffect, useRef, useState } from "react";
import { raf, useMemoOne, useOnce } from "@react-spring/shared";
import { Controller, a, config } from "@react-spring/web";
//#region src/index.tsx
const ParentContext = React.createContext(null);
function getScrollType(horizontal) {
return horizontal ? "scrollLeft" : "scrollTop";
}
function mapChildrenRecursive(children, callback) {
const isReactFragment = (node) => {
if (node.type) return node.type === React.Fragment;
return node === React.Fragment;
};
return React.Children.map(children, (child) => isReactFragment(child) ? mapChildrenRecursive(child.props.children, callback) : callback(child));
}
const START_TRANSLATE_3D = "translate3d(0px,0px,0px)";
const START_TRANSLATE = "translate(0px,0px)";
const ParallaxLayer = React.memo(React.forwardRef(({ horizontal, factor = 1, offset = 0, speed = 0, sticky, ...rest }, ref) => {
const parent = useContext(ParentContext);
const ctrl = useMemoOne(() => {
let translate;
if (sticky) translate = (sticky.start || 0) * parent.space;
else {
const targetScroll = Math.floor(offset) * parent.space;
const distance = parent.space * offset + targetScroll * speed;
translate = -(parent.current * speed) + distance;
}
return new Controller({
space: sticky ? parent.space : parent.space * factor,
translate
});
}, []);
const layer = useMemoOne(() => ({
horizontal: horizontal === void 0 || sticky ? parent.horizontal : horizontal,
sticky: void 0,
isSticky: false,
setPosition(height, scrollTop, immediate = false) {
if (sticky) setSticky(height, scrollTop);
else {
const targetScroll = Math.floor(offset) * height;
const distance = height * offset + targetScroll * speed;
ctrl.start({
translate: -(scrollTop * speed) + distance,
config: parent.config,
immediate
});
}
},
setHeight(height, immediate = false) {
ctrl.start({
space: sticky ? height : height * factor,
config: parent.config,
immediate
});
}
}), []);
useOnce(() => {
if (sticky) {
const start = sticky.start || 0;
layer.sticky = {
start,
end: sticky.end || start + 1
};
}
});
React.useImperativeHandle(ref, () => layer);
const layerRef = useRef(void 0);
const setSticky = (height, scrollTop) => {
const start = layer.sticky.start * height;
const end = layer.sticky.end * height;
const isSticky = scrollTop >= start && scrollTop <= end;
if (isSticky === layer.isSticky) return;
layer.isSticky = isSticky;
const ref = layerRef.current;
ref.style.position = isSticky ? "sticky" : "absolute";
ctrl.set({ translate: isSticky ? 0 : scrollTop < start ? start : end });
};
useOnce(() => {
if (parent) {
parent.layers.add(layer);
parent.update();
return () => {
parent.layers.delete(layer);
parent.update();
};
}
});
const translate3d = ctrl.springs.translate.to(layer.horizontal ? (x) => `translate3d(${x}px,0,0)` : (y) => `translate3d(0,${y}px,0)`);
return /* @__PURE__ */ React.createElement(a.div, {
...rest,
ref: layerRef,
style: {
position: "absolute",
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundSize: "auto",
backgroundRepeat: "no-repeat",
willChange: "transform",
[layer.horizontal ? "height" : "width"]: "100%",
[layer.horizontal ? "width" : "height"]: ctrl.springs.space,
WebkitTransform: translate3d,
msTransform: translate3d,
transform: translate3d,
...rest.style
}
});
}));
const Parallax = React.memo(React.forwardRef((props, ref) => {
const [ready, setReady] = useState(false);
const { pages, innerStyle: _innerStyle, config: config$1 = config.slow, enabled = true, horizontal = false, children, ...rest } = props;
const containerRef = useRef(void 0);
const contentRef = useRef(void 0);
const state = useMemoOne(() => ({
config: config$1,
horizontal,
busy: false,
space: 0,
current: 0,
offset: 0,
controller: new Controller({ scroll: 0 }),
layers: /* @__PURE__ */ new Set(),
container: containerRef,
content: contentRef,
update: () => update(),
scrollTo: (offset) => scrollTo(offset),
stop: () => state.controller.stop()
}), []);
useEffect(() => {
state.config = config$1;
}, [config$1]);
React.useImperativeHandle(ref, () => state);
const update = () => {
const container = containerRef.current;
if (!container) return;
state.space = container[horizontal ? "clientWidth" : "clientHeight"];
const scrollType = getScrollType(horizontal);
if (enabled) state.current = container[scrollType];
else container[scrollType] = state.current = state.offset * state.space;
const content = contentRef.current;
if (content) {
const sizeProp = horizontal ? "width" : "height";
content.style[sizeProp] = `${state.space * pages}px`;
}
state.layers.forEach((layer) => {
layer.setHeight(state.space, true);
layer.setPosition(state.space, state.current, true);
});
};
const scrollTo = (offset) => {
const container = containerRef.current;
const scrollType = getScrollType(horizontal);
state.offset = offset;
state.controller.set({ scroll: state.current });
state.controller.stop().start({
scroll: offset * state.space,
config: config$1,
onChange({ value: { scroll } }) {
container[scrollType] = scroll;
}
});
};
const onScroll = (event) => {
if (!state.busy) {
state.busy = true;
state.current = event.target[getScrollType(horizontal)];
raf.onStart(() => {
state.layers.forEach((layer) => layer.setPosition(state.space, state.current));
state.busy = false;
});
}
};
useEffect(() => state.update());
useOnce(() => {
setReady(true);
const onResize = () => {
const update = () => state.update();
raf.onFrame(update);
setTimeout(update, 150);
};
window.addEventListener("resize", onResize, false);
return () => window.removeEventListener("resize", onResize, false);
});
const overflow = enabled ? {
overflowY: horizontal ? "hidden" : "scroll",
overflowX: horizontal ? "scroll" : "hidden"
} : {
overflowY: "hidden",
overflowX: "hidden"
};
return /* @__PURE__ */ React.createElement(a.div, {
...rest,
ref: containerRef,
onScroll,
onWheel: enabled ? state.stop : void 0,
onTouchStart: enabled ? state.stop : void 0,
style: {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
...overflow,
WebkitOverflowScrolling: "touch",
WebkitTransform: START_TRANSLATE,
msTransform: START_TRANSLATE,
transform: START_TRANSLATE_3D,
...rest.style
}
}, ready && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(a.div, {
ref: contentRef,
style: {
overflow: "hidden",
position: "absolute",
[horizontal ? "height" : "width"]: "100%",
[horizontal ? "width" : "height"]: state.space * pages,
WebkitTransform: START_TRANSLATE,
msTransform: START_TRANSLATE,
transform: START_TRANSLATE_3D,
...props.innerStyle
}
}, /* @__PURE__ */ React.createElement(ParentContext.Provider, { value: state }, mapChildrenRecursive(children, (child) => !child.props.sticky && child))), /* @__PURE__ */ React.createElement(ParentContext.Provider, { value: state }, mapChildrenRecursive(children, (child) => child.props.sticky && child))));
}));
//#endregion
export { Parallax, ParallaxLayer };