Problem when applying css transitions on a carousel component

44 Views Asked by At

I'm trying to build a carousel with a cover flow effect. I'm rendering three items in the screen, looping through a list of items until the restart. The problem I'm getting is that the transition is not occurring as expected. When the user wants to check the next image, the middle carousel item should transition to the first position, the first one to the last, and the last to the middle. But the last element is not transitioning, it looks like that it's only being rendered at the screen without transitions.

Carousel result

Carousel.tsx:


type CarouselProps = {
  children:
    | React.ReactElement<CarouselItemProps>
    | React.ReactElement<CarouselItemProps>[];
  className?: React.HTMLAttributes<HTMLDivElement>["className"];
};

const Carousel = ({ children, className }: CarouselProps) => {
  const carouselItems = Children.toArray(children);

  const [currentIndex, setCurrentIndex] = useState<number>(0);

  const prevIndex =
    (currentIndex - 1 + carouselItems.length) % carouselItems.length;
  const nextIndex = (currentIndex + 1) % carouselItems.length;

  const getCarouselItemsToShow = () => {
    if (carouselItems.length < 3) return carouselItems;

    const indexes = [prevIndex, currentIndex, nextIndex];

    return indexes.map((index) => carouselItems[index]);
  };

  const handleClickPrev = () => {
    setCurrentIndex(
      (prevState) =>
        (prevState - 1 + carouselItems.length) % carouselItems.length
    );
  };

  const handleClickNext = () => {
    setCurrentIndex((prevState) => (prevState + 1) % carouselItems.length);
  };

return <div className="relative flex align-items h-[279px] mb-[20px] border border-white *:absolute *:top-[50%] *:-translate-y-1/2  *:-translate-x-1/2 *:transition-all *:duration-1000">
        {getCarouselItemsToShow().map((item, index) => {
          return React.cloneElement(
            item as React.ReactElement<CarouselItemProps>,
            {
              className:
                index === 1
                  ? "w-[419px] left-[50%] z-10"
                  : "w-[297px] opacity-80 first:left-[30%] last:left-[70%]",
            }
          );
        })}
      </div>
[...]
}

CarouselItem.tsx:

export type CarouselItemProps = {
  imgSrc: string;
  description: string;
  className?: React.HTMLAttributes<HTMLDivElement>["className"];
};

const CarouselItem = ({ imgSrc, className }: CarouselItemProps) => {
  return (
    <Image
      src={imgSrc}
      alt=""
      width={419}
      height={275}
      className={"rounded-[40px] " + className}
    />
  );
};

export default CarouselItem;

App.tsx:

[...]
    <Carousel>
      <CarouselItem
        imgSrc={data.jobs[0].projects[0].imageSrc}
        description={data.jobs[0].projects[0].description}
      />
      <CarouselItem
        imgSrc={data.jobs[0].projects[1].imageSrc}
        description={data.jobs[0].projects[1].description}
      />
      <CarouselItem
        imgSrc={data.jobs[0].projects[2].imageSrc}
        description={data.jobs[0].projects[2].description}
      />
    </Carousel>
[...]
1

There are 1 best solutions below

0
Felipe Eduardo On BEST ANSWER

The issue with this code is, even with the same key being used on the component, React will rerender the components (I think that they will be removed and added again in the DOM) because they are being created in different positions of the DOM.

So, to solve this issue, I kept the keys as the items indexes, but, for showing them on the UI, I ordered them, so I can render them all at the same position in the DOM, keeping the transition.

Updated Carousel component:

export type CarouselItemProps = {
 imageSrc: string;
 description: string;
 index: number;
};

type CarouselProps = {
  className?: React.HTMLAttributes<HTMLDivElement>["className"];
  carouselItems: CarouselItemProps[];
};

const Carousel = ({ carouselItems, className }: CarouselProps) => {
const [currentIndex, setCurrentIndex] = useState<number>(0);

const getPrevIndex = (currentIndex: number) =>
(currentIndex - 1 + carouselItems.length) % carouselItems.length;

const getNextIndex = (currentIndex: number) =>
(currentIndex + 1) % carouselItems.length;

const getCarouselItemsToShow = () => {
if (carouselItems.length < 3) return carouselItems;

const prevIndex = getPrevIndex(currentIndex);
const nextIndex = getNextIndex(currentIndex);

const indexes = [prevIndex, currentIndex, nextIndex].sort();

return indexes.map((index) => carouselItems[index]);
};

const handleClickPrev = () =>
setCurrentIndex((prevState) => getPrevIndex(prevState));

const handleClickNext = () =>
setCurrentIndex((prevState) => getNextIndex(prevState));

return (
<div className={className}>
  <div className="relative flex align-items h-[279px] mb-[20px] *:absolute *:top-[50%] *:transition-all *:-translate-y-1/2  *:-translate-x-1/2 *:duration-1000 ">
    {getCarouselItemsToShow().map((item, index) => {
      let className = "left-[30%] z-0 w-[297px] opacity-80";

      if (index === currentIndex)
        className = "left-[50%] z-10 opacity-100 w-[419px]";

      if (index === getNextIndex(currentIndex))
        className = "left-[70%] z-0 w-[297px] opacity-80";

      return (
        <CarouselItem
          key={item.index}
          description={item.description}
          imgSrc={item.imageSrc}
          className={className}
        />
      );
    })}
  </div>
  <div className="flex items-center gap-[10px] justify-center h-[46px]">
    <Arrow onClick={handleClickPrev} />
    {carouselItems.map((carouselItem, index) => (
      <DotIndicator
        key={carouselItem.index}
        isActive={index === currentIndex}
      />
    ))}
    <Arrow rotate onClick={handleClickNext} />
  </div>
  <p className="text-neutral-500 mt-1">
    {carouselItems[currentIndex].description}
  </p>
</div>
);
};

export default Carousel;