@onevetka

Как сделать раскрывающийся список с неопределённой высотой дочернего элемента с анимацией?

Пытаюсь сделать раскрывающийся список, но упираюсь в то, что если элемент с неопределенной высотой, то это нельзя анимировать. Есть предположение, что нужно использовать ref.current. clientHeight. Но я не знаю, как это засинхронить с анимацией.

Знаю, что это можно легко решить дочитав документацию, но время ограничено.

Ссылка на sandbox: https://codesandbox.io/s/pensive-sutherland-1h9zf?...

import React, { useRef, useState, useEffect } from 'react'
import { useSpring, useTransition, animated } from '@react-spring/web'

const items = [
  { label: 'Hello' },
  { label: 'World', items: [{ label: 'Hello 2 level' }, { label: 'World 2 level' }] },
  { label: 'Man' },
]

const Item: React.FC<any> = ({ label, items }) => {
  const [isExpanded, setIsExpanded] = useState(false);
  const listWrapper = useRef(null);

  const onClick = () => {
    setIsExpanded(!isExpanded)
  }

  const transitions = useTransition(isExpanded, {
    from: { opacity: 0, height: '0px' },
    enter: { opacity: 1, height: '200px' },
    leave: { opacity: 0, height: '0px' },
  })

  return (
    <li>
      <a onClick={onClick}>{label}</a>
      {transitions(
        (styles, isReady) =>
          isReady && (
            <animated.ul style={styles} ref={listWrapper}>
              {items.map(item => (
                <li key={item.label}>{item.label}</li>
              ))}
            </animated.ul>
          )
      )}
    </li>
  )
}

export default function App() {
  return (
    <div>
      {items.map(item => (
        <Item key={item.label} label={item.label} items={item.items} />
      ))}
    </div>
  )
}
  • Вопрос задан
  • 437 просмотров
Пригласить эксперта
Ответы на вопрос 1
@onevetka Автор вопроса
Нашёл решение, через создание специального компонента, который принимает чайлд

index.tsx
// Base
import React from 'react';
import { CSSTransition } from 'react-transition-group';
import useExpandableWrapper from './useExpandableWrapper';

// Assets
import styles from './style.module.scss';
import animation from './animation.module.scss';

interface ExpandableWrapperProps {
  isExpanded: boolean;
}

const ExpandableWrapper: React.FC<ExpandableWrapperProps> = ({
  children,
  isExpanded,
}) => {
  const { onEnter, onEntered, onExit, onExiting, state } = useExpandableWrapper();
  const { childHeight } = state;

  return (
    <div style={{ height: childHeight }} className={styles.wrapper}>
      <CSSTransition
        in={isExpanded}
        timeout={600}
        classNames={animation}
        onEnter={onEnter}
        onEntered={onEntered}
        onExit={onExit}
        onExiting={onExiting}
        unmountOnExit
      >
        {children}
      </CSSTransition>
    </div>
  );
};

export default ExpandableWrapper;


style.module.scss
.wrapper {
  overflow: hidden;
  transition: height 300ms ease;
  height: auto;
}


useExpandableWrapper.ts (Хук вьюхи)
import { useState } from 'react';

const useExpandableWrapper = () => {
  const [childHeight, setChildHeight] = useState<string | number>(0);

  const onEnter = (element: HTMLElement) => {
    const height = element.offsetHeight;
    setChildHeight(height);
  };

  const onEntered = () => {
    setChildHeight('auto');
  };

  const onExit = (element: any) => {
    const height = element.offsetHeight;
    setChildHeight(height);
  };

  const onExiting = () => {
    const height = 0;
    setChildHeight(height);
  };

  const state = {
    childHeight,
  };

  return {
    onEnter,
    onEntered,
    onExit,
    onExiting,
    state,
  };
};

export default useExpandableWrapper;


animation.style.scss (Файл с анимацией)
.enter {
  opacity: 0;
  transition: all 600ms cubic-bezier(0.15, 0.84, 0.21, 0.96);
}

.enterActive {
  opacity: 1;
}

.exit {
  opacity: 1;
  transition: all 300ms cubic-bezier(0.15, 0.84, 0.21, 0.96);
}

.exitActive {
  opacity: 0;
}
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы