Wersja wideo tego bloga

Framer Motion to prawdopodobnie jedna z najpotężniejszych bibliotek do animacji interfejsu użytkownika w React. Niedawno miałem okazję dogłębnie pobawić się tą biblioteką w przypadku niektórych rzeczy, nad którymi pracuję w Razorpay. W tym wpisie na blogu wideo zamierzam podzielić się niektórymi zdobytymi doświadczeniami, tworząc rozwijalny komponent tagów, którego treść zmienia się, gdy użytkownik najedzie na niego kursorem. Omówię różne przypadki Edge w takim komponencie, a także to, jak Framer Motion sprawia, że ​​animacje złożonego układu stają się dziecinnie proste!

Zmiana zawartości elementu po najechaniu na niego myszką

Stylizacja związana z najechaniem kursorem jest bardzo prosta w CSS ze względu na pseudoselektor :hover, ale nie jest zbyt jasne, w jaki sposób można zmienić zawartość elementu po najechaniu myszką. Na szczęście JavaScript udostępnia dwa zdarzenia - onMouseEnter i onMouseLeave, które są uruchamiane, gdy mysz odpowiednio wejdzie (najedzie) na element lub opuści go (odsunie). Zdarzeń tych można użyć do aktualizacji zmiennej stanu w React, która jest następnie używana do warunkowego renderowania zawartości elementu.

function ExpandableTag({ unhoveredText, hoveredText }) {
  const [isHovering, setIsHovering] = useState(false);
  return (
    <div
      className="inline-block rounded-lg relative text-gray-300 bg-gray-900 ring-1 ring-gray-800 px-4 py-1.5 tracking-wider text-sm font-medium whitespace-nowrap"
      onMouseEnter={() => setIsHovering(true)} 
      onMouseLeave={() => setIsHovering(false)} 
    >
      {isHovering ? hoveredText : unhoveredText}
    </div>
  );
}

Mamy gotową podstawową implementację komponentu, ale są dwa problemy:

  • Brak animacji. Treść zmienia się natychmiast, co w tym scenariuszu może nie być dobrym UX, a subtelna animacja może jeszcze bardziej dopracować ten komponent.
  • Przerażający przebłysk zagłady. Trudno to uchwycić, ale w przypadku, gdy tekst najechany kursorem jest mniejszy niż tekst nie najechany, użytkownik może spowodować, że komponent przejdzie w stan migotania, w którym zawartość składnika zmienia się w sposób ciągły.

Dodawanie animacji pojawiania się/zanikania do treści

Aby rozwiązać pierwszy problem, zacznijmy od dodania prostego pojawiania się i zanikania zmieniającej się zawartości tagu. Kiedy użytkownik najedzie kursorem na tag, nienajechany tekst zanika, a najechany tekst pojawia się. Gdy użytkownik najedzie kursorem, działanie będzie odwrotne. Można to bardzo łatwo zrobić w Framer Motion za pomocą komponentów motion i AnimatePresence. Komponentu AnimatePresence należy używać zawsze, gdy dla komponentu motion zdefiniowano animację wyjścia. W naszym przypadku będzie to animacja zanikania, gdy treść wychodzi z interfejsu użytkownika.

function ExpandableTag({ unhoveredText, hoveredText }) {
  const [isHovering, setIsHovering] = useState(false);
  return (
    <div
      className="inline-block rounded-lg relative text-gray-300 bg-gray-900 ring-1 ring-gray-800 px-4 py-1.5 tracking-wider text-sm font-medium whitespace-nowrap"
      onMouseEnter={() => setIsHovering(true)}
      onMouseLeave={() => setIsHovering(false)}
    >
      <AnimatePresence>
        <motion.span initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
          {isHovering ? hoveredText : unhoveredText}
        </motion.span>
      </AnimatePresence>
    </div>
  );
}

Samo to nie rozwiązuje problemu. Framer Motion nie wie, kiedy powinny zostać uruchomione animacje, więc domyślnie uruchamia się tylko po pierwszym zamontowaniu komponentu. Chcemy, aby animacja uruchamiała się, gdy użytkownik najedzie i odsunie element (ponieważ wtedy zmienia się zawartość), co jest już śledzone przez zmienną stanu isHovering. Aby nakazać Framer Motion uruchomienie animacji, możemy po prostu ustawić właściwość key tak, aby zmieniała się, gdy chcemy, aby animacje się uruchamiały. W naszym przypadku byłoby to ustawienie key na wartość opartą na zmiennej stanu isHovering.

Ponadto, ponieważ nie chcemy, aby animacje wejścia były uruchamiane po pierwszym zamontowaniu komponentu, możemy zrezygnować z tego zachowania, ustawiając właściwość initial w komponencie AnimatePresence na false

function ExpandableTag({ unhoveredText, hoveredText }) {
  const [isHovering, setIsHovering] = useState(false);
  return (
    <div
      className="inline-block rounded-lg relative text-gray-300 bg-gray-900 ring-1 ring-gray-800 px-4 py-1.5 tracking-wider text-sm font-medium whitespace-nowrap"
      onMouseEnter={() => setIsHovering(true)}
      onMouseLeave={() => setIsHovering(false)}
    >
      <AnimatePresence
        initial={false} 
      >
        <motion.span
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          key={isHovering ? 'hovering' : 'unhovering'} 
        >
          {isHovering ? hoveredText : unhoveredText}
        </motion.span>
      </AnimatePresence>
    </div>
  );
}