We will create a React "down slider" component. When we click a button, the slide content will appear/disappear (fully or partially) by sliding down/up. The slider will adjust to the content height using the triggers we specify. We can place the button inside or outside the slider content.
We will use: React, typescript, styled-components.
I used vite to create the app.
First we create a new component in DownSlider.tsx:
import React from "react";
import styled from "styled-components";
type DownSliderProps = {
expanded: boolean;
collapsedHeight: number;
expandedHeight: number;
};
// height doesn't animate, but max-height does
const Container = styled.div<DownSliderProps>`
position: relative;
background-color: beige;
overflow: hidden; // hide any overflowing content
${({ expanded, collapsedHeight, expandedHeight }) =>
expanded
? `max-height: ${expandedHeight}px; transition: max-height 0.5s;`
: `max-height: ${collapsedHeight}px; transition: max-height 0.5s;`}
`;
// ------------------------------------
// You can place the button inside or outside the content.
type Props = {
expanded: boolean;
collapsedHeight: number; // height of the button, if inside content
expandedHeight: number; // height of expanded content
children: React.ReactNode;
};
// ------------------------------------
const DownSlider = ({
expanded,
collapsedHeight,
expandedHeight,
children,
}: Props) => {
return (
<Container
expanded={expanded}
collapsedHeight={collapsedHeight}
expandedHeight={expandedHeight}
>
{children}
</Container>
);
};
export default DownSlider;
Then in TestPage.tsx, we add the slider, a button, some content, and we simulate some delayed data access:
import React, { useEffect, useRef, useState } from "react";
import styled from "styled-components";
import { DownSliderButton, DownSlider, Page } from "components";
const DownSliderContainer = styled.div`
width: 700px;
padding: 10px;
border-radius: 10px;
border: 1px solid forestgreen;
`;
const SlideTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
background-color: #eee;
font-weight: bold;
`;
const SlideContent = styled.div`
padding: 10px;
`;
// ------------------------------------
const TestPage = () => {
const ref: any = useRef(null); // to measure content height
const initialHeight = 0; // 0: slides at start, ~1000: no sliding
const [updatedHeight, setUpdatedHeight] = useState(initialHeight);
const [isExpanded, setExpanded] = useState(true);
const [delayedData, setDelayedData] = useState("");
useEffect(() => {
setUpdatedHeight(ref.current?.clientHeight);
}, [ref.current?.clientHeight, delayedData]); // update triggers
useEffect(() => {
setTimeout(() => {
setDelayedData("This data arrives ~2s later.");
}, 2000);
}, []);
// ------------------------------------
return (
...
<DownSliderContainer>
<SlideTitle>
<div>Slide title</div>
<DownSliderButton isExpanded={isExpanded} setExpanded={setExpanded} />
</SlideTitle>
<DownSlider
expanded={isExpanded}
collapsedHeight={0}
expandedHeight={updatedHeight}
>
<SlideContent ref={ref}>
<div>Slide content</div>
<div>
<img
src={"/src/assets/cairn-terrier.jpg"}
alt="dog"
height={454}
/>
</div>
<div>More data arriving soon.</div>
<div>{delayedData}</div>
</SlideContent>
</DownSlider>
</DownSliderContainer>
...
);
};
export default TestPage;
For completeness, here's DownSliderButton.tsx:
import React from "react";
import styled from "styled-components";
// @ts-ignore
import { Rotator } from "components";
// @ts-ignore
import { CircleArrowRightIcon } from "svg";
const Container = styled.div`
cursor: pointer;
color: var(--color-primary-lighter);
& > div {
width: 32px;
height: 32px;
}
`;
// ------------------------------------
type Props = {
isExpanded: boolean;
setExpanded: (isExpanded: boolean) => void;
};
// ------------------------------------
const DownSliderButton = ({ isExpanded, setExpanded }: Props) => {
return (
<Container role="button" onClick={() => setExpanded(!isExpanded)}>
<Rotator rotated={isExpanded ? 90 : 180}>
<CircleArrowRightIcon />
</Rotator>
</Container>
);
};
export default DownSliderButton;
And the Rotator rotates anything you place inside it:
import styled, { css } from "styled-components";
type Props = {
rotated?: number;
};
const Rotator = styled.div<Props>`
transform: rotate(0deg);
transition: transform 0.5s ease-out;
${({ rotated }) =>
rotated !== undefined &&
css`
transform: rotate(${rotated}deg);
transition: transform 0.5s ease-out;
`};
`;
export default Rotator;
I declare this button component inside my "svg/index.js" file:
export { ReactComponent as CircleArrowRightIcon } from "svg/circle-arrow-right.svg";
.. and it imports this svg:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentcolor">
<path d="M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM406.6 278.6l-103.1 103.1c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25L306.8 288H128C110.3 288 96 273.7 96 256s14.31-32 32-32h178.8l-49.38-49.38c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0l103.1 103.1C414.6 241.3 416 251.1 416 256C416 260.9 414.6 270.7 406.6 278.6z"/>
</svg>
Question/comments, let me know.
Top comments (0)