react-transition-group
react-transition-group copied to clipboard
CSSTransition child with dynamic className
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
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%;
}