react-transition-group icon indicating copy to clipboard operation
react-transition-group copied to clipboard

CSSTransition child with dynamic className

Open jesperjohansson opened this issue 6 years ago • 1 comments

Do you want to request a feature or report a bug? Bug, I think.

What is the current behavior? Whenever I update my component with a dynamic className, all the classNames from CSSTransition disappear from the child element.

<Popover.Container
  className={classNames('notificationCenter__popover', {
    '-grayArrow': tabIndex === 0, /* <-- dynamic className */
  })}
>
  /* random child */
</Popover.Container>
export const Container = ({ className, children }) => (
  <PopoverContext.Consumer>
    {context => (
      <CSSTransition
        in={context.isOpen}
        classNames={ANIM_CLASSNAMES}
        timeout={context.isOpen ? ANIM_DURATION.default : ANIM_DURATION.fast}
        mountOnEnter
        unmountOnExit
      >
        <div id={context.id} className={classNames('popover', className)}> /* <-- insert className */
          {children}
        </div>
      </CSSTransition>
    )}
  </PopoverContext.Consumer>
);

What is the expected behavior? The resulting element should include both the "dynamic" className that comes from the className prop as well as the class names from CSSTransition.

Which versions, and which browser / OS are affected by this issue? Did this work in previous versions? react-transition-group@^2.3.0 MacOS High Sierra 10.13.2, Google Chrome 65.0

jesperjohansson avatar Apr 13 '18 08:04 jesperjohansson

I ran into the same problem today, here is a workaround:

Import CSSTransition classNames prop type (if you use typescript): import { CSSTransitionClassNames } from "react-transition-group/CSSTransition";

Create a constant object of class names for CSSTransition:

const TRANSITION_CLASS_NAMES: CSSTransitionClassNames = {
  enter: styles.drawerEnter,
  enterActive: styles.drawerEnterActive,
  enterDone: styles.drawerEnterDone,
  exit: styles.drawerExit,
  exitActive: styles.drawerExitActive,
  exitDone: styles.drawerExitDone,
};

Define this util function to retrieve the current transaction state of CSSTransition component:

function getCurrentTransitionClassName(
  nodeRef: React.RefObject<HTMLDivElement>
) {
  return nodeRef.current?.className.match(
    Object.values(TRANSITION_CLASS_NAMES).join("|")
  )?.[0];
}

Call this function whenever you want after nodeRef init:

 const nodeRef = useRef<HTMLDivElement>(null);
 const currentTransitionClassName = getCurrentTransitionClassName(nodeRef);

Pass currentTransitionClassName along with other class names to the child element.

According to the docs nodeRef should be assigned to the both CSSTransition component and child element (see the complete example below).

Drawer component using CSSTransaction. Complete example (Spoiler)

Drawer.tsx

import classcat from "classcat";
import React, { useRef } from "react";
import ReactDOM from "react-dom";

import styles from "./Drawer.module.css";
import { CSSTransition } from "react-transition-group";
import { CSSTransitionClassNames } from "react-transition-group/CSSTransition";

type DrawerProps = {
  children?: React.ReactNode;
  open: boolean;
  className?: string;
  style?: React.CSSProperties;
  unmountOnExit?: boolean;
  mountOnEnter?: boolean;
  portalContainer?: Element | DocumentFragment;
  onClosed?: () => void;
};

const TRANSITION_CLASS_NAMES: CSSTransitionClassNames = {
  enter: styles.drawerEnter,
  enterActive: styles.drawerEnterActive,
  enterDone: styles.drawerEnterDone,
  exit: styles.drawerExit,
  exitActive: styles.drawerExitActive,
  exitDone: styles.drawerExitDone,
};

function getCurrentTransitionClassName(
  nodeRef: React.RefObject<HTMLDivElement>
) {
  return nodeRef.current?.className.match(
    Object.values(TRANSITION_CLASS_NAMES).join("|")
  )?.[0];
}

function Drawer({
  open,
  className,
  style,
  children,
  unmountOnExit = false,
  mountOnEnter = false,
  portalContainer = document.body,
  onClosed,
}: DrawerProps) {
  const nodeRef = useRef<HTMLDivElement>(null);
  const currentTransitionClassName = getCurrentTransitionClassName(nodeRef);

  return ReactDOM.createPortal(
    <CSSTransition
      nodeRef={nodeRef}
      in={open}
      timeout={300}
      classNames={TRANSITION_CLASS_NAMES}
      unmountOnExit={unmountOnExit}
      mountOnEnter={mountOnEnter}
      onExited={onClosed}
    >
      <div
        ref={nodeRef}
        className={classcat([
          styles.drawer,
          className,
          currentTransitionClassName,
        ])}
        style={style}
      >
        <div className={styles.drawerContent}>{children}</div>
      </div>
    </CSSTransition>,
    portalContainer
  );
}

export default Drawer;

Drawer.module.css

.drawer {
  position: fixed;
  height: 100%;
  width: var(--theme-drawer-width, 376px);
  z-index: 9999;
  top: 0;
  right: calc(var(--theme-drawer-width, 376px) * -1);
  color: var(--theme-drawer-text-color, #000);
  background-color: var(--theme-drawer-background-color, #fff);
  border-left: var(
    --theme-drawer-border-left,
    1px solid var(--theme-drawer-border-color, #e7e7e7)
  );
}

.drawer-enter {
  right: calc(var(--theme-drawer-width, 376px) * -1);
}

.drawer-enter-active {
  right: 0;
  transition: right 300ms ease-out;
}

.drawer-enter-done {
  right: 0;
}

.drawer-exit {
  right: 0;
}

.drawer-exit-active {
  right: calc(var(--theme-drawer-width, 376px) * -1);
  transition: right 300ms ease-out;
}

.drawer-exit-done {
  right: calc(var(--theme-drawer-width, 376px) * -1);
}

.drawer-content {
  display: flex;
  flex-flow: column nowrap;
  width: 100%;
  height: 100%;
}


pripishchik avatar Mar 28 '23 02:03 pripishchik