自定义轮播组件 - 动画未按预期工作

问题描述 投票:0回答:1

我有一个自定义轮播组件(使用 React、Typescript 和 Tailwind)-

import React, { useState } from 'react'

interface Slide {
  content: React.ReactNode
  title: string
  description: string
}

interface CarouselProps {
  slides: Slide[]
}

const Carousel: React.FC<CarouselProps> = ({ slides }) => {
  const [currentIndex, setCurrentIndex] = useState(0)
  const [nextIndex, setNextIndex] = useState<null | number>(null)
  const [animationClass, setAnimationClass] = useState('')

  const goToPrevious = () => {
    setNextIndex(currentIndex === 0 ? slides.length - 1 : currentIndex - 1)
    setAnimationClass('slide-out-right') // Current slide exits to the right
  }

  const goToNext = () => {
    setNextIndex(currentIndex === slides.length - 1 ? 0 : currentIndex + 1)
    setAnimationClass('slide-out-left') // Current slide exits to the left
  }

  const goToSlide = (index: number) => {
    if (index > currentIndex) {
      setAnimationClass('slide-out-left')
      setTimeout(() => setNextIndex(index), 5000)
    } else if (index < currentIndex) {
      setAnimationClass('slide-out-right')
      setTimeout(() => setNextIndex(index), 5000)
    }
  }

  const handleAnimationEnd = () => {
    if (nextIndex !== null) {
      setCurrentIndex(nextIndex)
      setNextIndex(null)
      setAnimationClass(
        nextIndex > currentIndex ? 'slide-in-left' : 'slide-in-right'
      )
    }
  }

  return (
    <div className="carousel-container flex flex-col items-center">
      <div className="relative flex h-[250px] w-[400px] items-center justify-center">
        <button
          onClick={goToPrevious}
          className="absolute left-0 -translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ‹
        </button>
        <div className="carousel-content flex size-full items-center justify-center overflow-hidden rounded-lg bg-gray-100">
          <div
            className={`${animationClass} flex size-full items-center justify-center`}
            onAnimationEnd={handleAnimationEnd}
          >
            {slides[nextIndex !== null ? nextIndex : currentIndex].content}
          </div>
        </div>
        <button
          onClick={goToNext}
          className="absolute right-0 translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ›
        </button>
      </div>
      <div className="carousel-text mt-4 w-[400px] text-center">
        <h3 className="text-lg font-semibold">{slides[currentIndex].title}</h3>
        <p className="text-gray-600">{slides[currentIndex].description}</p>
      </div>
      <div className="carousel-indicators mt-2 flex space-x-1">
        {slides.map((_, index) => (
          <span
            key={index}
            onClick={() => goToSlide(index)}
            className={`block size-2 cursor-pointer rounded-full ${
              index === currentIndex ? 'bg-black' : 'bg-gray-300'
            }`}
          />
        ))}
      </div>
    </div>
  )
}

export default Carousel

我在我的 globals.css 中定义了这些动画 -

@keyframes slide-in-left {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes slide-in-right {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes slide-out-left {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-100%);
  }
}

@keyframes slide-out-right {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(100%);
  }
}

.slide-in-left {
  animation: slide-in-left 0.5s ease-in-out forwards;
}

.slide-in-right {
  animation: slide-in-right 0.5s ease-in-out forwards;
}

.slide-out-left {
  animation: slide-out-left 0.5s ease-in-out forwards;
}

.slide-out-right {
  animation: slide-out-right 0.5s ease-in-out forwards;
}

目前,逻辑表现得很奇怪,我似乎无法弄清楚我做错了什么。当用户向右导航时,内容应向左滑出,新内容应从右侧滑入。反之亦然。如果我向左导航,内容会向右滑出,新内容会从左侧滑入。

javascript css reactjs typescript tailwind-css
1个回答
0
投票

考虑使用 CSS 过渡而不是 CSS 动画来为幻灯片添加动画效果。这意味着您不需要

setTimeout()
调用或需要管理下一张或上一张幻灯片。

首先,使用绝对定位将每张幻灯片彼此叠放:

const { useState } = React;

const Carousel = ({ slides }) => {
  const [currentIndex, setCurrentIndex] = useState(0);

  const goToPrevious = () => {};

  const goToNext = () => {};
  
  const goToSlide = () => {};

  return (
    <div className="carousel-container flex flex-col items-center">
      <div className="relative flex h-[250px] w-[400px] items-center justify-center">
        <button
          onClick={goToPrevious}
          className="absolute left-0 -translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ‹
        </button>
        <div className="relative size-full overflow-hidden rounded-lg bg-gray-100">
          {slides.map(({ content }, i) => (
            <div
              key={i}
              className="flex size-full items-center justify-center absolute inset-0"
            >
              {content}
            </div>
          ))}
        </div>
        <button
          onClick={goToNext}
          className="absolute right-0 translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ›
        </button>
      </div>
      <div className="carousel-text mt-4 w-[400px] text-center">
        <h3 className="text-lg font-semibold">{slides[currentIndex].title}</h3>
        <p className="text-gray-600">{slides[currentIndex].description}</p>
      </div>
      <div className="carousel-indicators mt-2 flex space-x-1">
        {slides.map((_, index) => (
          <span
            key={index}
            onClick={() => goToSlide(index)}
            className={`block size-2 cursor-pointer rounded-full ${
              index === currentIndex ? 'bg-black' : 'bg-gray-300'
            }`}
          />
        ))}
      </div>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('app')).render(
  <Carousel
    slides={[
      { content: 'Foo' },
      { content: 'Bar' },
      { content: 'Baz' },
      { content: 'Qux' },
    ]}
  />
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.5"></script>

<div id="app"></div>

然后让每张幻灯片位于滑块的左侧或右侧,具体取决于它相对于活动幻灯片的位置:

currentIndex = 0:
      ┏━━━━━┓–––––┐
      ┃  0  ┃1,2,3│
      ┗━━━━━┛–––––┘

currentIndex = 1:
┌–––––┏━━━━━┓–––––┐
│  0  ┃  1  ┃ 2,3 │
└–––––┗━━━━━┛–––––┘

currentIndex = 2:
┌–––––┏━━━━━┓–––––┐
│ 0,1 ┃  2  ┃  3  │
└–––––┗━━━━━┛–––––┘

currentIndex = 3:
┌–––––┏━━━━━┓
│0,1,2┃  3  ┃
└–––––┗━━━━━┛

这给出了幻灯片的效果:

const { useState } = React;

const Carousel = ({ slides }) => {
  const [currentIndex, setCurrentIndex] = useState(0);

  const goToPrevious = () => {
    setCurrentIndex((i) => (i === 0 ? slides.length - 1 : i - 1));
  };

  const goToNext = () => {
    setCurrentIndex((i) => (i === slides.length - 1 ? 0 : i + 1));
  };

  return (
    <div className="carousel-container flex flex-col items-center">
      <div className="relative flex h-[250px] w-[400px] items-center justify-center">
        <button
          onClick={goToPrevious}
          className="absolute left-0 -translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ‹
        </button>
        <div className="relative size-full overflow-hidden rounded-lg bg-gray-100">
          {slides.map(({ content }, i) => {
            const animateClass =
              i === currentIndex
                ? ''
                : i < currentIndex
                  ? '-translate-x-full'
                  : 'translate-x-full';

            return (
              <div
                key={i}
                className={`${animateClass} duration-500 flex size-full items-center justify-center absolute inset-0`}
              >
                {content}
              </div>
            );
          })}
        </div>
        <button
          onClick={goToNext}
          className="absolute right-0 translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ›
        </button>
      </div>
      <div className="carousel-text mt-4 w-[400px] text-center">
        <h3 className="text-lg font-semibold">{slides[currentIndex].title}</h3>
        <p className="text-gray-600">{slides[currentIndex].description}</p>
      </div>
      <div className="carousel-indicators mt-2 flex space-x-1">
        {slides.map((_, index) => (
          <span
            key={index}
            onClick={() => setCurrentIndex(index)}
            className={`block size-2 cursor-pointer rounded-full ${
              index === currentIndex ? 'bg-black' : 'bg-gray-300'
            }`}
          />
        ))}
      </div>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('app')).render(
  <Carousel
    slides={[
      { content: 'Foo' },
      { content: 'Bar' },
      { content: 'Baz' },
      { content: 'Qux' },
    ]}
  />
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.5"></script>

<div id="app"></div>

但是,当使用分页控件在非连续幻灯片之间导航时,我们看到它们之间的幻灯片也会移到另一侧。这可能不是我们所希望的,因此为了解决这个问题,我们可以隐藏非活动幻灯片。这使得它们可以在不被发现的情况下移动。我们可以通过在非活动幻灯片上应用

visibility: hidden
来做到这一点:

const { useState } = React;

const Carousel = ({ slides }) => {
  const [currentIndex, setCurrentIndex] = useState(0);

  const goToPrevious = () => {
    setCurrentIndex((i) => (i === 0 ? slides.length - 1 : i - 1));
  };

  const goToNext = () => {
    setCurrentIndex((i) => (i === slides.length - 1 ? 0 : i + 1));
  };

  return (
    <div className="carousel-container flex flex-col items-center">
      <div className="relative flex h-[250px] w-[400px] items-center justify-center">
        <button
          onClick={goToPrevious}
          className="absolute left-0 -translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ‹
        </button>
        <div className="relative size-full overflow-hidden rounded-lg bg-gray-100">
          {slides.map(({ content }, i) => {
            let animateClass = '';
            if (i < currentIndex) {
              animateClass = '-translate-x-full invisible';
            } else if (i > currentIndex) {
              animateClass = 'translate-x-full invisible';
            }

            return (
              <div
                key={i}
                className={`${animateClass} duration-500 flex size-full items-center justify-center absolute inset-0`}
              >
                {content}
              </div>
            );
          })}
        </div>
        <button
          onClick={goToNext}
          className="absolute right-0 translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ›
        </button>
      </div>
      <div className="carousel-text mt-4 w-[400px] text-center">
        <h3 className="text-lg font-semibold">{slides[currentIndex].title}</h3>
        <p className="text-gray-600">{slides[currentIndex].description}</p>
      </div>
      <div className="carousel-indicators mt-2 flex space-x-1">
        {slides.map((_, index) => (
          <span
            key={index}
            onClick={() => setCurrentIndex(index)}
            className={`block size-2 cursor-pointer rounded-full ${
              index === currentIndex ? 'bg-black' : 'bg-gray-300'
            }`}
          />
        ))}
      </div>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('app')).render(
  <Carousel
    slides={[
      { content: 'Foo' },
      { content: 'Bar' },
      { content: 'Baz' },
      { content: 'Qux' },
    ]}
  />
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.5"></script>

<div id="app"></div>

最后,当您从最后一个→第一个或第一个→最后一个移动时,它看起来不像一个无缝循环。为此,我们需要针对每种情况将第一张和最后一张幻灯片分别放置在右侧和左侧:

currentIndex = 0:
┌–––––┏━━━━━┓–––––┐
│  3  ┃  0  ┃ 1,2 │
└–––––┗━━━━━┛–––––┘

currentIndex = 3:
┌–––––┏━━━━━┓–––––┐
│ 1,2 ┃  3  ┃  0  │
└–––––┗━━━━━┛–––––┘

const { useState } = React;

const Carousel = ({ slides }) => {
  const [currentIndex, setCurrentIndex] = useState(0);

  const goToPrevious = () => {
    setCurrentIndex((i) => (i === 0 ? slides.length - 1 : i - 1));
  };

  const goToNext = () => {
    setCurrentIndex((i) => (i === slides.length - 1 ? 0 : i + 1));
  };

  return (
    <div className="carousel-container flex flex-col items-center">
      <div className="relative flex h-[250px] w-[400px] items-center justify-center">
        <button
          onClick={goToPrevious}
          className="absolute left-0 -translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ‹
        </button>
        <div className="relative size-full overflow-hidden rounded-lg bg-gray-100">
          {slides.map(({ content }, i) => {
            let animateClass = '';
            if (currentIndex === 0 && slides.length - 1 === i) {
              animateClass = '-translate-x-full invisible';
            } else if (currentIndex === slides.length - 1 && i === 0) {
              animateClass = 'translate-x-full invisible';
            } else if (i < currentIndex) {
              animateClass = '-translate-x-full invisible';
            } else if (i > currentIndex) {
              animateClass = 'translate-x-full invisible';
            }

            return (
              <div
                key={i}
                className={`${animateClass} duration-500 flex size-full items-center justify-center absolute inset-0`}
              >
                {content}
              </div>
            );
          })}
        </div>
        <button
          onClick={goToNext}
          className="absolute right-0 translate-x-full px-4 py-2 text-lg font-bold text-gray-600"
        >
          ›
        </button>
      </div>
      <div className="carousel-text mt-4 w-[400px] text-center">
        <h3 className="text-lg font-semibold">{slides[currentIndex].title}</h3>
        <p className="text-gray-600">{slides[currentIndex].description}</p>
      </div>
      <div className="carousel-indicators mt-2 flex space-x-1">
        {slides.map((_, index) => (
          <span
            key={index}
            onClick={() => setCurrentIndex(index)}
            className={`block size-2 cursor-pointer rounded-full ${
              index === currentIndex ? 'bg-black' : 'bg-gray-300'
            }`}
          />
        ))}
      </div>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('app')).render(
  <Carousel
    slides={[
      { content: 'Foo' },
      { content: 'Bar' },
      { content: 'Baz' },
      { content: 'Qux' },
    ]}
  />
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.tailwindcss.com/3.4.5"></script>

<div id="app"></div>

© www.soinside.com 2019 - 2024. All rights reserved.