Reputation: 1106
I have a component where when o clicking the button, the div with more info will slideUp and slideDown. Below the code and css style
import { CSSTransition } from "react-transition-group";
const Card = () => {
const [showMoreInfo, setShowMoreInfo] = useState(false);
return (
<div className="Card">
<ButtonShowMore isOpen={showMoreInfo} click={() => setShowMoreInfo(!showMoreInfo)} />
<CSSTransition in={showMoreInfo} classNames="Card-Details" timeout={1000}>
<div>
{showMoreInfo && (
<>
<p>details</p>
<p>details</p>
</>
)}
</div>
</CSSTransition>
</div>
);
};
.Card-Details-enter {
height: 0px;
}
.Card-Details-enter-active {
height: 100%;
-webkit-transition: height 1s ease;
-moz-transition: height 1s ease;
-o-transition: height 1s ease;
transition: height 1s ease;
}
.Card-Details-enter-done {
height: 100%;
}
.Card-Details-exit {
height: 100%;
}
.Card-Details-exit-active {
height: 0px;
-webkit-transition: height 1s ease;
-moz-transition: height 1s ease;
-o-transition: height 1s ease;
transition: height 1s ease;
}
.Card-Details-exit-done {
height: 0px;
}
But it doesnt work, I don't know why. I tred to put the transition to the parent element like here and add transition to the class *-exit-done like here, and nothing helped.
Upvotes: 0
Views: 597
Reputation: 11027
The reason why it doesn't work is because transitioning on percentage height isn't quite what you would expect.
Percentage height and width in CSS refer to their parent's height and width rather than their own height.
The CSS data type represents a percentage value. It is often used to define a size as relative to an element's parent object. Numerous properties can use percentages, such as
width
,height
,margin
,padding
, andfont-size
.
Example with the same element transitioning using a percentage height/width. One has a container element with a height/width of 50px, and the other doesn't.
div.container {
width: 50px;
height: 50px;
}
div.transition {
width: 100%;
height: 100%;
background: red;
transition: width 2s, height 4s;
}
div.transition:hover {
width: 300%;
height: 500%;
}
Transition div with a 50px container
<div class="container">
<div class="transition">
<p>test</p>
</div>
</div>
Transition div without a container:
<div class="transition">
<p>test</p>
</div>
What we'd actually want is to transition from 0px to auto height. Unfortunately browsers don't support transitioning on auto height.
A good write up of this is in Using CSS Transitions Auto Dimensions include some approaches to get what you want and their downsides.
Why hasn’t this problem been fixed at the browser level?
According to the Mozilla Developer Network docs, auto values have been intentionally excluded from the CSS transitions spec. It looks like it’s been requested by a few people, but when you think about it, it makes at least a little sense that it hasn’t been included. The browser process that re-calculates the sizes and positions of all elements based on their content and the way they interact with each other (known as “reflow”) is expensive. If you were to transition an element into a height of auto, the browser would have to perform a reflow for every stage of that animation, to determine how all the other elements should move. This couldn’t be cached or calculated in a simple way, since it doesn’t know the starting and/or ending values until the moment the transition happens. This would significantly complicate the math that has to be done under the hood and probably degrade performance in a way that might not be obvious to the developer.
How can I transition height: 0; to height: auto; using CSS? also has some good workarounds, though there really is no magic bullet for this.
It is definitely a very well known issue, and there's a request for the spec to change to allow transitions on auto, though I don't think it's gone anywhere yet.
As for support for the type of transition you are working on in React Transition Group:
Slide Down Animation and Trying to fade out element then slide up both have the same answer overall: pointing at how React Bootstrap's Collapse component does it.
You need to rely on finding the dom node's actual height and using that as part of the transition:
getDimension() {
return typeof this.props.dimension === 'function'
? this.props.dimension()
: this.props.dimension;
}
// for testing
_getScrollDimensionValue(elem, dimension) {
return `${elem[`scroll${capitalize(dimension)}`]}px`;
}
/* -- Expanding -- */
handleEnter = (elem) => {
elem.style[this.getDimension()] = '0';
}
handleEntering = (elem) => {
const dimension = this.getDimension();
elem.style[dimension] = this._getScrollDimensionValue(elem, dimension);
}
handleEntered = (elem) => {
elem.style[this.getDimension()] = null;
}
/* -- Collapsing -- */
handleExit = (elem) => {
const dimension = this.getDimension();
elem.style[dimension] = `${this.props.getDimensionValue(dimension, elem)}px`;
triggerBrowserReflow(elem);
}
handleExiting = (elem) => {
elem.style[this.getDimension()] = '0';
}
A quick and dirty example of using the functionality from the Collapse class for a working example of the code using a less fully featured solution (note, based heavily on the Collapse.js code linked above):
const { Transition } = ReactTransitionGroup;
const { EXITED, ENTERED, ENTERING, EXITING } = Transition;
const { useState } = React;
// Quick and dirty classNames functionality
const classNames = (...names) => names.filter((name) => name).join(' ');
const ButtonShowMore = ({ isOpen, click }) => {
return <button onClick={click}>{isOpen ? 'Close' : 'Open'}</button>;
};
// Heavily based on https://github.com/react-bootstrap/react-bootstrap/blob/next/src/Collapse.js#L150
// for the purpose of demonstration without just pulling in the module:
function triggerBrowserReflow(node) {
node.offsetHeight; // eslint-disable-line no-unused-expressions
}
const collapseStyles = {
[EXITED]: 'collapse',
[EXITING]: 'collapsing',
[ENTERING]: 'collapsing',
[ENTERED]: 'collapse in',
};
const Collapse = ({ children, ...props }) => {
const handleEnter = (elem) => (elem.style.height = '0');
const handleEntering = (elem) =>
(elem.style.height = `${elem.scrollHeight}px`);
const handleEntered = (elem) => (elem.style.height = null);
const handleExit = (elem) => {
elem.style.height = `${elem.scrollHeight}px`;
triggerBrowserReflow(elem);
};
const handleExiting = (elem) => (elem.style.height = '0');
return (
<Transition
{...props}
onEnter={handleEnter}
onEntering={handleEntering}
onEntered={handleEntered}
onExit={handleExit}
onExiting={handleExiting}
>
{(state, innerProps) =>
React.cloneElement(children, {
...innerProps,
className: classNames(
props.className,
children.props.className,
collapseStyles[state]
),
})
}
</Transition>
);
};
const Card = () => {
const [showMoreInfo, setShowMoreInfo] = useState(false);
return (
<div className="Card">
<ButtonShowMore
isOpen={showMoreInfo}
click={() => setShowMoreInfo(!showMoreInfo)}
/>
<Collapse in={showMoreInfo} className="Card-Details" timeout={1000}>
<div style={{ height: 0 }}>
<p>details</p>
<p>details</p>
</div>
</Collapse>
</div>
);
};
ReactDOM.render(<Card />, document.querySelector('#root'));
.collapsing {
-webkit-transition: height 1s ease;
-moz-transition: height 1s ease;
-o-transition: height 1s ease;
transition: height 1s ease;
overflow: hidden;
}
.collapse {
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-transition-group/4.4.1/react-transition-group.min.js"></script>
<div id="root" />
Upvotes: 1