UNPKG

@react-spring/parallax

Version:

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

229 lines (227 loc) 7.38 kB
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 };