UNPKG

react-responsive-framer-motion-carousel

Version:

React carousel componet

162 lines (147 loc) 8.08 kB
import React, { useEffect, useState, forwardRef, useImperativeHandle, Children, useMemo } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { wrap } from "./helpers/wrap"; import { verticalVariants, variants } from "./helpers/variants"; import Controls from "./components/Controls"; import Counter from "./components/Counter"; import Navigation from "./components/Navigation"; /** * Carousel - A customizable carousel component for displaying images or content. * * This component allows users to navigate through a series of images or content using swiping gestures or navigation buttons. It provides various configuration options for a tailored user experience. * * @param {Object} props - Props for the carousel component. * @param {React.ReactNode} props.children - The content or images to be displayed within the carousel . * @param {number} [props.range=1000] - An optional number to set how far the children will animate in or out. * @param {string} [props.className] - An optional CSS className for styling. You should style your outer div to stop overflow. * @param {string} [props.motionClassName] - An optional CSS class name used for styling the motion box of each child. Useful with images. * @param {string} [pros.navigation=true] className = carousel-navigation : Clasname = carousel-navigation-button - Optional display showing all indexes that can be clicked to navigate. * @param {boolean} [props.drag=true] - A boolean flag (defaults to `true`) that determines whether dragging/swiping functionality should be enabled. * @param {boolean} [props.controls=true] - A boolean flag (defaults to `true`) that determines whether control buttons should be enabled. *@param {Array} [props.counter] classname = carousel-counter - Optional counter configuration (array with two elements). - Element 0 (boolean): Controls visibility of the counter. Defaults to false. true: Displays the counter. false: Hides the counter. - Element 1 (string, optional): Custom string to display before the counter (if counter[0] is true). If omitted, no string is displayed. * @param {boolean} [props.noExit=false] - A boolean flag (defaults to `false`) useful during issues. If set to `true`, it disables the exit animation, which may be helpful in rendering issues with big components. * @param {number} [props.swipeConfindence=1000] - A optional number for decreasing or increasing the confidence that a user is swiping * @param {Array} [props.interval=[false, 0]] - An optional array that configures automatic pagination using an interval. - The first element (`interval[0]`) is a boolean flag (defaults to `false`) that determines whether automatic pagination is enabled. - The second element (`interval[1]`) is a number (defaults to `0`) that specifies the duration (in seconds) between automatic page transitions. * @param {boolean} [props.intervalActive=true] - A boolean flag (defaults to `true`) that controls whether the configured interval (if any) should be actively used. This provides further control over automatic pagination behavior. * @param {string} [props.type="horizontal"] - Type of carousel. Possible values: "horizontal", "vertical". * @param {number} [props.goToIndex] - Optional number u can pass to set the Index of the `carousel`. Remember child 1 is index 0 * @param {} [props.onChange] - Callback that returns the index when sliding. Useful for knowing when u should display a loading state. * @param {ref} [ref] - Use the paginate function from the `carousel` externally (paginate(1), paginate(-1)) * @returns {JSX.Element} The carousel component. */ const Carousel = forwardRef(({ range = 1000, navigation = true, onChange, controls = true, swipeConfindence, intervalActive = true, children, className, drag = true, counter, interval = [false, 0], noExit = false, type = "horizontal", goToIndex }, ref) => { const [[page, direction], setPage] = useState([0, 0]); const pageCount = useMemo(() => Children.count(children), [children]); const pageIndex = wrap(0, pageCount, page); if (!children || pageCount === 0) { console.error("Carousel: No children provided. Please provide content or images to display within the carousel."); return <div className="carousel-error">No content provided for the carousel.</div>; } if (interval && interval[0] < 0) { console.error("Carousel: interval time is negative") return <div className="carousel-error">Negative interval.</div> } ref && useImperativeHandle(ref, () => ({ paginate })); //Calculations for swipe const swipeConfidenceThreshold = swipeConfindence ? swipeConfindence : 1000; const swipePower = (offset, velocity) => { return Math.abs(offset) * velocity; }; //automatic slider useEffect(() => { const intervalId = intervalActive && interval[0] && setInterval(() => paginate(1), interval[1] * 1000); return () => clearInterval(intervalId); }, [intervalActive, interval]); //goToPage external useEffect(() => { if (goToIndex && goToIndex <= pageCount && goToIndex >= 0) { setPage(prevState => [goToIndex, prevState[1]]) } else { console.error("invalid page") } }, [goToIndex]) //goToPage internal const goToPage = (number) => { if (number <= pageCount && number >= 0) { setPage(prevState => [number, prevState[1]]) } else { console.error("invalid page") } } //paginaton const paginate = (newDirection) => { setPage(prevState => [prevState[0] + newDirection, newDirection]); onChange && onChange(pageIndex); }; return ( <> <AnimatePresence mode="popLayout" initial={false} custom={direction}> <motion.div className={className} style={{ position: 'relative' }} key={page} custom={direction} variants={type === "vertical" ? verticalVariants(range) : variants(range)} initial="enter" animate="center" {...(!noExit ? { exit: "exit" } : {})} transition={{ x: { type: "spring", stiffness: 300, damping: 30 }, opacity: { duration: 0.2 } }} {...(drag ? { drag: type === "vertical" ? "y" : "x", dragConstraints: type === "vertical" ? { top: 0, bottom: 0 } : { left: 0, right: 0 }, dragElastic: 1, onDragEnd: (e, { offset, velocity }) => { const swipe = swipePower( type === "vertical" ? offset.y : offset.x, type === "vertical" ? velocity.y : velocity.x ); if (swipe < -swipeConfidenceThreshold) { paginate(1); } else if (swipe > swipeConfidenceThreshold) { paginate(-1); } } } : {})} > <div style={{ position: 'absolute', top: 0, left: 0 }}> {Children.toArray(children)[pageIndex]} </div> </motion.div> </AnimatePresence> {/* Optional components */} {controls && <Controls paginate={paginate} />} {counter && counter[0] && <Counter counter={counter} pageIndex={pageIndex} pageCount={pageCount} />} {navigation && <Navigation pageCount={pageCount} goToPage={goToPage} pageIndex={pageIndex} />} </> ); }); Carousel.displayName = "Carousel"; export default Carousel;