I'm trying to add a scale animation to an SVG. Its working fine in Webkit based browsers, but on iOS Chrome and Safari the animation is super slow. Here is the page I am trying to animate. Here is the relevant code...
const HomeHeading = styled.svg`
margin: 0;
color: rgba(255, 255, 255, 1);
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
backface-visibility: hidden;
perspective: 1000;
transform: scale(
${props =>
props.scrollPosition / props.scale < 1
? 1
: props.scrollPosition / props.scale}
)
translateZ(0);
transform-origin: 42% 56%;
#media screen and (max-width: 480px) {
transform-origin: 43% 38% !important;
}
rect {
-webkit-mask: url(#mask);
mask: url(#mask);
fill: #f00;
}
defs {
mask {
rect {
fill: white;
}
text {
transform: translateY(10%);
font-size: 8vw;
#media screen and (max-width: 480px) {
transform: translateY(0);
}
&:last-child {
#media screen and (max-width: 480px) {
transform: translateY(0);
}
transform: translateY(20%);
}
}
}
}
`;
const HomeSubHomeSectionHeading = styled.section`
width: 100vw;
height: 100vh;
top: 0;
left: 0;
background: #1ecbe1;
position: fixed !important;
display: flex;
align-content: center;
justify-content: center;
flex-wrap: wrap;
`;
const ColorChanger = styled.div`
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: 3;
background-color: rgb(255, 255, 255);
opacity: ${props => 3000 / props.scrollPosition / 20};
`;
const ImageStamp = styled.div`
width: 280px;
height: auto;
display: inline-block;
padding: 10px;
background: white;
position: relative;
-webkit-filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.5));
background: radial-gradient(
transparent 0px,
transparent 4px,
white 4px,
white
);
background-size: 20px 20px;
background-position: -10px -10px;
&:after {
content: "";
position: absolute;
left: 5px;
top: 5px;
right: 5px;
bottom: 5px;
z-index: -1;
}
#media screen and (max-width: 768px) {
width: 200px;
margin-bottom: 30px;
}
`;
const MeSection = styled.section`
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
`;
const MePhoto = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex: 1 0 33.333%;
`;
const MeBio = styled.div`
flex: 1 0 66.666%;
color: #ffffff;
padding: 0 30px;
`;
const MeSocials = styled.div`
svg {
margin: 0 10px;
}
`;
const SocialLink = styled.a`
color: #fff;
&:hover {
color: #f1f1f1;
}
`;
const IndexPage = props => {
const [scrollPosition, setScrollPosition] = useState(0);
const [scale, setScale] = useState(0);
useEffect(() => {
window.addEventListener("scroll", handleScroll, { passive: true });
});
const handleScroll = () => {
const position = window.scrollY;
setScale(document && document.width > 500 ? 20 : 5);
setScrollPosition(position);
};
return (
<Layout>
<HomeSubHomeSectionHeading>
<ColorChanger scrollPosition={scrollPosition}></ColorChanger>
<MeSection>
<div className="container">
<MePhoto>
<ImageStamp>
<Img fluid={props.data.mattImage.childImageSharp.fluid} />
</ImageStamp>
</MePhoto>
<MeBio>
<h3>Hi, I'm Matt!</h3>
<hr />
<p>
I'm a Lead Frontend Developer currently based at Oliver Wyman
Digital. I have experience in a range of frontend technologies
and practices; more recently dabbling with AB testing, VueCLI
and Typescript.
</p>
<p>
Outside of the web world, I like to run, travel and like to
watch movies. Apart from Toy Story 1, I cried when I found out
Buzz Lightyear couldn't fly.
</p>
<h4>Find out more</h4>
<hr />
<MeSocials>
<SocialLink
href="https://uk.linkedin.com/in/mattmaclennan"
target="_blank"
>
<FontAwesomeIcon icon={faLinkedin} />
</SocialLink>
<SocialLink href="https://github.com/mmaclenn" target="_blank">
<FontAwesomeIcon icon={faGithub} />
</SocialLink>
</MeSocials>
</MeBio>
</div>
</MeSection>
</HomeSubHomeSectionHeading>
<HomeHeading
preserveAspectRatio="xMinYMin meet"
scale={scale}
scrollPosition={scrollPosition}
>
<defs>
<mask id="mask" x="0" y="0" width="100%" height="100%">
<rect x="0" y="0" width="100%" height="100%" fill="#fff"></rect>
<text x="50%" y="40%" textAnchor="middle">
Matt Maclennan
</text>
<text id="editText" x="50%" y="45%" textAnchor="middle">
Web Developer
</text>
</mask>
</defs>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="#E1341E"
id="mask"
></rect>
</HomeHeading>
</Layout>
);
};
export const fluidImage = graphql`
fragment fluidImage on File {
childImageSharp {
fluid(maxWidth: 1000) {
...GatsbyImageSharpFluid
}
}
}
`;
export const pageQuery = graphql`
query {
mattImage: file(relativePath: { eq: "me.jpg" }) {
...fluidImage
}
}
`;
export default IndexPage;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
I'm aware I can only animate transforms and opacity, also I've added translateZ to use hardware rendering and backface-visibility to the CSS with no luck.
Any ideas where I'm going wrong?
Edit: As per the comments, I have tried to throttle the scroll callback by using this package. Here is the code I'm using based on this package...
useScrollPosition(
({ prevPos, currPos }) => {
setScale(document && document.width > 500 ? 20 : 5);
const shouldBeStyle = {
transform: `scale(${
Math.abs(currPos.y) < 9 ? 1 : Math.abs(currPos.y) / scale
}) translateZ(0)`,
pointerEvents: `${Math.abs(currPos.y) > 1000 ? "none" : "auto"}`,
};
const opacityStyle = {
opacity: 1000 / Math.abs(currPos.y) / 20,
};
if (JSON.stringify(shouldBeStyle) === JSON.stringify(scrollStyling))
return;
setOpactiyStyling(opacityStyle);
setScrollStyling(shouldBeStyle);
},
[scrollStyling, opacityStyling]
);
useEffect(() => {
window.addEventListener("scroll", handleScroll, { passive: true });
});
This adds an event listener every time props change. You probably only want to do this once, e.g.:
useEffect(() => {
window.addEventListener("scroll", handleScroll, { passive: true });
}, []);
or at the very least put some dependencies in those brackets. See: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
You can use this, also you need to return a cleanup after the component is unmounted. I hope it helps
useEffect(() => {
window.addEventListener("scroll", handleScroll, { passive: true });
return () => {
window.removeEventListener("scroll", handleScroll, { passive: true });
};
}, []);
Related
Problem
In my app I have card with projects and sometimes images of those projects doesn't load on mobile i have to reload browser and to it is loaded correctly I have no idea how.I must also point out that on the computer viewer everything works correctly.
Stack
I`m using Astro.js with Preact.
Here is demonstration video of my problem
https://streamable.com/okquzk
My code
The way i use it. In Astro framework you can create collections with you mdx or md data here i pass route to imgs like this:
Note: in Astro you dont have to write public/something it ok to write
/projects/bhn.webp
---
title: "Black Hat News"
heroImage: /projects/bhn.webp
___
After some step I am using it inside Card.tsx
import "./styles/card.css";
type Props = {
title: string;
heroImage: string;
slug: string;
};
export const Card = ({ title, heroImage, slug }: Props) => (
<a href={slug}>
<div class="card">
<img src={heroImage} alt={title} />
<div class="info">
<h1>{title}</h1>
</div>
</div>
</a>
);
and there is css
/* Set padding top to make a trick with aspect ratio 16:9 */
.card {
padding: 1rem;
width: 100%;
padding-top: 56.25%;
position: relative;
display: flex;
align-items: flex-end;
transition: 0.4s ease-out;
box-shadow: 0px 7px 10px rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.card:before {
content: "";
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
height: 100%;
/* background: rgba(0, 0, 0, 0.6); */
z-index: 2;
}
.card img {
width: 100%;
height: 100%;
-o-object-fit: cover;
object-fit: cover;
object-position: center top;
position: absolute;
top: 0;
left: 0;
}
.card .info {
position: relative;
z-index: 3;
color: var(--color-text);
background: var(--color-dark);
padding: 0.4rem;
border-radius: 5px;
}
.card .info h1 {
font-size: var(--font-lg);
}
/* for desktop add nice effects */
#media (min-width: 1024px) {
.card:hover {
transform: translateY(10px);
}
.card:hover:before {
opacity: 1;
background: rgba(0, 0, 0, 0.6);
}
.card:hover .info {
opacity: 1;
transform: translateY(0px);
}
.card:before {
transition: 0.3s;
opacity: 0;
}
.card .info {
opacity: 0;
transform: translateY(30px);
transition: 0.3s;
}
.card .info h1 {
margin: 0px;
}
}
Edit
Added usage
import "./styles/projectsGrid.css";
import { useStore } from "#nanostores/preact";
import { getProjectsByTag } from "#utils/*";
import type { CollectionEntry } from "astro:content";
import { tagValue } from "src/tagStore";
import { Card } from "./Card";
type Props = {
projects: CollectionEntry<"projects">[];
};
export const ProjectsGrid = ({ projects }: Props) => {
const $tagValue = useStore(tagValue);
const filteredProjects = getProjectsByTag(projects, $tagValue);
return (
<div class="projects_wrapper">
{filteredProjects.map(({ data: { title, heroImage }, slug }) => (
<Card title={title} heroImage={heroImage} slug={slug} />
))}
</div>
);
};
previous
<svg
class="get-carousel__prev-svg"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 512 512"
>
Chevron Back
<button
class="gel-carousel__next get-carousel__buttons_back"
ref="next"
aria-label="next "
#click="nextShift"
type="button"
>
next
Chevron Forward
<div
class="gel-carousel__scrollable"
ref="gallery"
#scroll="carouselScroll"
>
Mohtasim Hasan
Mohtasim Hasan
Mohtasim Hasan
Mohtasim Hasan
Mohtasim Hasan
Mohtasim Hasan
Mohtasim Hasan
Mohtasim Hasan
import { onMounted, ref } from "vue";
let debounced = ref();
const gallery = ref();
const lists = ref();
const previous = ref();
const next = ref();
const buttons = ref();
onMounted(() => {
const slides = document.querySelectorAll("img[data-src]");
buttons.value?.removeAttribute("hidden");
disableEnable();
let observer = new IntersectionObserver(imgSrcSet, {
root: null,
threshold: 0.6,
});
slides.forEach((slide) => {
observer.observe(slide);
});
});
const carouselScroll = () => {
window.clearTimeout(debounced.value);
debounced.value = setTimeout(disableEnable, 250);
};
const disableEnable = function () {
previous.value.disabled = gallery.value?.scrollLeft {
if (entry.intersectionRatio > 0.5) {
entry.target.closest("li").removeAttribute("inert");
console.log(entry);
entry.target.setAttribute("src", entry.target.dataset.src);
entry.target.src = entry.target.dataset.src;
}
if (!entry.isIntersecting) {
entry.target.closest("li").setAttribute("inert", "");
entry.target.closest("li").setAttribute("tabindex", -1);
}
if (entry.isIntersecting) {
entry.target.closest("li").setAttribute("tabindex", 0);
}
});
};
const previousShift = function () {
const scrollPos = ref(gallery.value?.scrollWidth / 4);
gallery.value?.scrollTo(gallery.value?.scrollLeft - scrollPos.value, 0);
};
const nextShift = function () {
const scrollPos = ref(gallery.value?.scrollWidth / 4);
gallery.value?.scrollTo(
gallery.value?.scrollLeft + 400,
gallery.value.scrollTop
);
};
.gel-carousel__buttons {
button {
cursor: pointer;
}
button[disabled] {
cursor: no-drop;
}
}
.gel-carousel__list {
display: flex;
list-style: none;
padding: 0;
margin: 0;
white-space: nowrap;
}
.gel-carousel__list > li {
display: flex;
flex-shrink: 0;
white-space: normal;
width: var(--size-fluid-9); /* standard Promo width */
transition: all 0.5s linear;
position: relative;
figure {
display: flex;
justify-items: space-between;
flex-direction: column;
margin: 0;
padding: 0;
}
figcaption {
margin-block: 2rem;
}
img {
display: block;
width: 100%;
height: var(--size-fluid-8);
background-size: cover;
transition: all 0.3s linear;
}
}
.gel-carousel__list > li[inert] {
opacity: 0.6;
}
ul {
padding: 0;
margin: 0;
}
.gel-carousel__list > li + li {
margin-left: 1rem;
&:not(:last-child) {
margin-left: 0;
}
}
.gel-carousel__scrollable {
width: 50rem;
overflow: auto;
scroll-behavior: smooth;
}
#media (prefers-reduced-motion: reduce) {
.gel-carousel__scrollable {
scroll-behavior: auto;
}
}
.gel-carousel__scrollable::-webkit-scrollbar {
height: 0.8rem;
}
.gel-carousel__scrollable::-webkit-scrollbar-track {
background-color: lightblue;
}
.gel-carousel__scrollable::-webkit-scrollbar-thumb {
background-color: lightpink;
}
.gel-sr:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
I created a vue js carousel app with typescript. But in it shows red squiggly line on both fuction.
I'm trying to create a TextField component that takes an optional left adornment component, which renders a container with an SVG icon within it, where the SVG icon is centered within the parent.
The problem here is that the parent of the SVG element is, for some reason, setting an (arbitrary?) height for itself. Not sure why this is happening, maybe it's got something to do with the SVG props themselves.
If I set the Container height to 3rem, for example, then the adornments' parent containers height does change, but the SVG itself keeps extending outside of the bounds.
Here's how it looks like now:
And here is how I'd like it to look like:
Here's the code:
// TextField
import * as React from "react";
import clsx from "clsx";
import styled from "#emotion/styled";
const Container = styled.div`
--input-color: #99a3ba;
--input-border: #cdd9ed;
--input-border-error: #a30000;
--input-background: #fff;
--input-placeholder: #cbd1dc;
--input-border-focus: #275efe;
--group-color: var(--input-color);
--group-border: var(--input-border);
--group-background: #eef4ff;
--group-color-focus: #fff;
--group-border-focus: var(--input-border-focus);
--group-background-focus: #678efe;
position: relative;
display: flex;
width: 100%;
& > .adornment_container,
.form-field {
white-space: nowrap;
display: block;
&:not(:first-child):not(:last-child) {
border-radius: 0;
}
&:first-child {
border-radius: 0.5rem 0 0 0.5rem;
}
&:last-child {
border-radius: 0 0.5rem 0.5rem 0;
}
&:not(:first-child) {
margin-left: 1px;
}
}
.form-field {
position: relative;
z-index: 1;
flex: 1 1 auto;
width: 1%;
margin-top: 0;
margin-bottom: 0;
}
& > .adornment_container {
width: 5rem;
height: 100%;
text-align: center;
color: var(--group-color);
background: var(--group-background);
border: 1px solid var(--group-border);
transition: background 0.3s ease, border 0.3s ease, color 0.3s ease;
}
&:focus-within {
& > .adornment_container {
color: var(--group-color-focus);
background: var(--group-background-focus);
border-color: var(--group-border-focus);
path {
fill: #ffffff;
}
}
}
`;
const Input = styled.input`
display: block;
width: 100%;
height: 100%;
padding: 0.5rem 1rem;
font-weight: 500;
font-family: inherit;
border-radius: 6px;
-webkit-appearance: none;
color: var(--input-color);
border: 1px solid var(--input-border);
background: var(--input-background);
transition: border 0.3s ease;
&::placeholder {
color: var(--input-placeholder);
}
&:focus {
outline: none;
border-color: var(--input-border-focus);
}
`;
const AdornmentContent = styled.div`
height: 100%;
width: 100%;
`;
type TextFieldProps = {
adornmentContent?: React.ReactNode;
value: string;
disabled?: boolean;
} & Omit<React.HTMLAttributes<HTMLInputElement>, "name">;
const TextField = React.memo(
({
className,
adornmentContent = null,
disabled,
...props
}: TextFieldProps) => {
const adornment = adornmentContent ? (
<AdornmentContent className="adornment_container">
{adornmentContent}
</AdornmentContent>
) : null;
return (
<Container>
{adornment}
<Input className={clsx("form-field", className)} {...props} />
</Container>
);
}
);
export default TextField;
// EmailIcon
import * as React from "react";
const EmailIcon = React.memo(() => {
return (
<svg
aria-hidden="true"
viewBox="0 0 100% 100%"
preserveAspectRatio="xMidYMid meet"
style={{
width: "100%",
height: "100%"
}}
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10h5v-2h-5c-4.34 0-8-3.66-8-8s3.66-8 8-8 8 3.66 8 8v1.43c0 .79-.71 1.57-1.5 1.57s-1.5-.78-1.5-1.57V12c0-2.76-2.24-5-5-5s-5 2.24-5 5 2.24 5 5 5c1.38 0 2.64-.56 3.54-1.47.65.89 1.77 1.47 2.96 1.47 1.97 0 3.5-1.6 3.5-3.57V12c0-5.52-4.48-10-10-10zm0 13c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"
fill={"#232323"}
/>
</svg>
);
});
export default EmailIcon;
What am I doing wrong here? Here is how I would like it to look like:
I have a navigation bar that has a logo and a burgerbar that changes from a burger bar to an "x" when opened.
The problem is that the dropdown should go down when you click on the burger and go back up when you press the "x". This functions smoothly and perfectly so far, but the problem I noticed is that when I resize the browser window from >700px to <700px, the dropdown menu animation going up occurs.
import React, { useState, useRef, useEffect } from "react";
import { BurgerBar } from "../../styles/Navbar.style";
import Navlinks from "./Navlinks";
function Burger() {
const [open, setOpen] = useState(false);
const [width, setWidth] = useState(window.innerWidth);
const prevWidth = useRef();
useEffect(() => {
prevWidth.current = width;
}, [width]);
function checkResize() {
setWidth(window.innerWidth);
if (prevWidth.current <= 700 && width > 700) {
setOpen(false);
};
};
window.addEventListener("resize", checkResize);
return (
<>
<BurgerBar open={open} onClick={() => {setOpen(!open);}}>
<span />
<span />
<span />
</BurgerBar>
<Navlinks open={open} />
</>
)
};
export default Burger;
This is my styled-components file with the relevant styles:
export const BurgerBar = styled.span`
z-index: 1;
width: 2rem;
position: fixed;
display: flex;
justify-content: space-around;
flex-flow: column;
top: ${({open}) => open ? 22 : 24}px;
height: ${({open}) => open ? 2.3 : 2}rem;
right: ${({open}) => open ? 17 : 20}px;
span {
width: 2rem;
height: 0.25rem;
background-color: rgb(46, 203, 64);
border-radius: 10px;
transform-origin: 1px;
&:nth-child(1) {
transform: ${({open}) => open ? 'rotate(45deg)' : 'rotate(0)'};
width: ${({open}) => open ? 2.3 : 2}rem;
}
&:nth-child(2) {
transform: ${({open}) => open ? 'translateX(100%)' : 'translateX(0)'};
opacity: ${({open}) => open ? 0 : 1};
}
&:nth-child(3) {
transform: ${({open}) => open ? 'rotate(-45deg)' : 'rotate(0)'};
width: ${({open}) => open ? 2.3 : 2}rem;
}
}
&:hover {
opacity: 0.7;
cursor: pointer;
}
#media (min-width: 701px) {
visibility: hidden;
}
`;
export const Dropdown = styled.ul`
z-index: 1;
position: fixed;
list-style: none;
display: flex;
flex-flow: row nowrap;
padding: 0;
margin: 0;
top: 0;
li {
padding: 20px 20px;
&:hover {
opacity: 0.7;
cursor: pointer;
}
}
#media (max-width: 700px) {
position: fixed;
z-index: 0;
flex-flow: column nowrap;
background-color: rgb(32, 80, 36);
transform: ${({ open }) => (open) ? 'translateY(0)' : 'translateY(-100%)'};
vertical-align: middle;
transition: transform 0.3s ease-in-out;
top: 85px;
align-items: center;
border-bottom-left-radius: 4%;
li {
padding-top: 10px;
padding-bottom: 4px;
}
}
`;
I recognize that the problem lies in these two lines:
transform: ${({ open }) => (open) ? 'translateY(0)' : 'translateY(-100%)'};
and
transition: transform 0.3s ease-in-out;
I'm new to Javascript and React though and I cannot think of an efficient way to block the transition animation from going off upon browser resizing.
I can link my repo if more code/reference is necessary.
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
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.