react-responsive-framer-motion-carousel
Version:
React carousel componet
162 lines (147 loc) • 8.08 kB
JSX
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;