UNPKG

@react-spring/parallax

Version:

```bash yarn add @react-spring/parallax ```

285 lines (284 loc) 9.01 kB
// src/index.tsx import * as React from "react"; import { useContext, useState, useRef, useEffect } from "react"; import { useMemoOne, useOnce, raf } from "@react-spring/shared"; import { a, Controller, config as configs } from "@react-spring/web"; var 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) ); } var START_TRANSLATE_3D = "translate3d(0px,0px,0px)"; var START_TRANSLATE = "translate(0px,0px)"; var 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) { const start = sticky.start || 0; translate = start * 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; const end = sticky.end || start + 1; layer.sticky = { start, end }; } }); 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 ref2 = layerRef.current; ref2.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 } } ); } ) ); var Parallax = React.memo( React.forwardRef((props, ref) => { const [ready, setReady] = useState(false); const { pages, innerStyle: _innerStyle, config = configs.slow, enabled = true, horizontal = false, children, ...rest } = props; const containerRef = useRef(void 0); const contentRef = useRef(void 0); const state = useMemoOne( () => ({ config, 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; }, [config]); React.useImperativeHandle(ref, () => state); const update = () => { const container = containerRef.current; if (!container) return; const spaceProp = horizontal ? "clientWidth" : "clientHeight"; state.space = container[spaceProp]; 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, 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 update2 = () => state.update(); raf.onFrame(update2); setTimeout(update2, 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", 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 ))) ); }) ); export { Parallax, ParallaxLayer };