O
O
onevetka2021-08-06 18:03:35
Animation
onevetka, 2021-08-06 18:03:35

How to make dropdown list with undefined height of child element with animation?

I'm trying to make a drop-down list, but I run into the fact that if the element has an indefinite height, then this cannot be animated. There is a suggestion to use ref.current. clientHeight. But I don't know how to sync it with animation.

I know that this can be easily solved after reading the documentation, but time is limited.

Link to 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>
  )
}

Answer the question

In order to leave comments, you need to log in

1 answer(s)
O
onevetka, 2021-11-08
@onevetka

I found a solution by creating a special component that accepts child
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 (View Hook)
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 (Animation file)
.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;
}

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question