Reputation: 148
I've implemented my own responsive accordion in React, and I can't get it to animate the opening of a fold.
This is especially odd because I can get the icon before the title to animate up and down, and, other than the icon being a pseudo-element, I can't seem to see the difference between the two.
JS:
class Accordion extends React.Component {
constructor(props) {
super(props);
this.state = {
active: -1
};
}
/***
* Selects the given fold, and deselects if it has been clicked again by setting "active to -1"
* */
selectFold = foldNum => {
const current = this.state.active === foldNum ? -1 : foldNum;
this.setState(() => ({ active: current }));
};
render() {
return (
<div className="accordion">
{this.props.contents.map((content, i) => {
return (
<Fold
key={`${i}-${content.title}`}
content={content}
handle={() => this.selectFold(i)}
active={i === this.state.active}
/>
);
})}
</div>
);
}
}
class Fold extends React.Component {
render() {
return (
<div className="fold">
<button
className={`fold_trigger ${this.props.active ? "open" : ""}`}
onClick={this.props.handle}
>
{this.props.content.title}
</button>
<div
key="content"
className={`fold_content ${this.props.active ? "open" : ""}`}
>
{this.props.active ? this.props.content.inner : null}
</div>
</div>
);
}
}
CSS:
$line-color: rgba(34, 36, 38, 0.35);
.accordion {
width: 100%;
padding: 1rem 2rem;
display: flex;
flex-direction: column;
border-radius: 10%;
overflow-y: auto;
}
.fold {
.fold_trigger {
&:before {
font-family: FontAwesome;
content: "\f107";
display: block;
float: left;
padding-right: 1rem;
transition: transform 400ms;
transform-origin: 20%;
color: $line-color;
}
text-align: start;
width: 100%;
padding: 1rem;
border: none;
outline: none;
background: none;
cursor: pointer;
border-bottom: 1px solid $line-color;
&.open {
&:before {
transform: rotateZ(-180deg);
}
}
}
.fold_content {
display: none;
max-height: 0;
opacity: 0;
transition: max-height 400ms linear;
&.open {
display: block;
max-height: 400px;
opacity: 1;
}
}
border-bottom: 1px solid $line-color;
}
Here's the CodePen: https://codepen.io/renzyq19/pen/bovZKj
Upvotes: 5
Views: 4296
Reputation: 5516
I wouldn't conditionally render the content if you want a smooth transition. It will make animating a slide-up especially tricky.
I would change this:
{this.props.active ? this.props.content.inner : null}
to this:
{this.props.content.inner}
and use this scss
:
.fold_content {
max-height: 0;
overflow: hidden;
transition: max-height 400ms ease;
&.open {
max-height: 400px;
}
}
Try the snippet below or see the forked CodePen Demo.
class Accordion extends React.Component {
constructor(props) {
super(props);
this.state = {
active: -1
};
}
/***
* Selects the given fold, and deselects if it has been clicked again by setting "active to -1"
* */
selectFold = foldNum => {
const current = this.state.active === foldNum ? -1 : foldNum;
this.setState(() => ({ active: current }));
};
render() {
return (
<div className="accordion">
{this.props.contents.map((content, i) => {
return (
<Fold
key={`${i}-${content.title}`}
content={content}
handle={() => this.selectFold(i)}
active={i === this.state.active}
/>
);
})}
</div>
);
}
}
class Fold extends React.Component {
render() {
return (
<div className="fold">
<button
className={`fold_trigger ${this.props.active ? "open" : ""}`}
onClick={this.props.handle}
>
{this.props.content.title}
</button>
<div
key="content"
className={`fold_content ${this.props.active ? "open" : ""}`}
>
{this.props.content.inner}
</div>
</div>
);
}
}
const pictures = [
"http://unsplash.it/200",
"http://unsplash.it/200",
"http://unsplash.it/200",
];
var test = (title, text, imageURLs) => {
const images=
<div className='test-images' >
{imageURLs.map((url,i) => <img key={i} src={url} />)}
</div>;
const inner =
<div className='test-content' >
<p>{text} </p>
{images}
</div>;
return {title, inner};
};
const testData = [
test('Title', 'Content',pictures ),
test('Title', 'Content',pictures ),
test('Title', 'Content',pictures ),
test('Title', 'Content',pictures ),
test('Title', 'Content',pictures ),
];
ReactDOM.render(<Accordion contents={testData} />, document.getElementById('root'));
.accordion {
width: 100%;
padding: 1rem 2rem;
display: flex;
flex-direction: column;
border-radius: 10%;
overflow-y: auto;
}
.fold {
border-bottom: 1px solid rgba(34, 36, 38, 0.35);
}
.fold .fold_trigger {
text-align: start;
width: 100%;
padding: 1rem;
border: none;
outline: none;
background: none;
cursor: pointer;
border-bottom: 1px solid rgba(34, 36, 38, 0.35);
}
.fold .fold_trigger:before {
font-family: FontAwesome;
content: "\f107";
display: block;
float: left;
padding-right: 1rem;
transition: transform 400ms;
transform-origin: 20%;
color: rgba(34, 36, 38, 0.35);
}
.fold .fold_trigger.open:before {
transform: rotateZ(-180deg);
}
.fold .fold_content {
max-height: 0;
overflow: hidden;
transition: max-height 400ms ease;
}
.fold .fold_content.open {
max-height: 400px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet" />
<div id='root'></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
Note:
I used ease
instead of linear
on the transition because I think it's a nicer effect. But that's just personal taste. linear
will work as well.
Also, you can continue to conditionally render the content. A slide-down animation is possible, but a slide-up can't be easily achieved. There are some transition libraries that you could explore as well.
However, I think it's easiest to use the state just for conditional classes (as you are doing with the open
class). I think conditionally rendering content to the DOM makes your life difficult if you're trying to do CSS animations.
Upvotes: 5