feat(dashboard-v2): add Slider component

This commit is contained in:
Michał Leszczyk 2022-02-23 11:26:17 +01:00
parent fad846ee7a
commit f6496f0358
No known key found for this signature in database
GPG Key ID: FA123CA8BAA2FBF4
5 changed files with 169 additions and 0 deletions

View File

@ -0,0 +1,28 @@
import PropTypes from "prop-types";
export default function Bullets({ visibleSlides, activeIndex, allSlides, changeSlide }) {
if (allSlides <= visibleSlides) {
return null;
}
return (
<div className="flex gap-3 pt-6">
{Array(allSlides - visibleSlides + 1)
.fill(null)
.map((_, i) => (
<span
key={`slider-bullets-${i}`}
className={`rounded-full w-3 h-3 ${activeIndex === i ? "bg-primary" : "border-2 cursor-pointer"}`}
onClick={() => changeSlide(i)}
/>
))}
</div>
);
}
Bullets.propTypes = {
allSlides: PropTypes.number.isRequired,
activeIndex: PropTypes.number.isRequired,
visibleSlides: PropTypes.number.isRequired,
changeSlide: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,12 @@
import styled from "styled-components";
import PropTypes from "prop-types";
const Slide = styled.div.attrs(({ isVisible }) => ({
className: `slider-slide transition-opacity ${isVisible ? "" : "opacity-50 cursor-pointer"}`,
}))``;
Slide.propTypes = {
isVisible: PropTypes.bool.isRequired,
};
export default Slide;

View File

@ -0,0 +1,105 @@
import * as React from "react";
import PropTypes from "prop-types";
import styled, { css } from "styled-components";
import theme from "../../lib/theme";
import useActiveBreakpoint from "./useActiveBreakpoint";
import Bullets from "./Bullets";
import Slide from "./Slide";
const Container = styled.div.attrs({
className: "slider w-full",
})``;
/**
* Styles applied to the movable element when the number of slide elements
* exceeds the number of visible slides for the current breakpoint
* */
const scrollableStyles = css`
${({ $allSlides, $visibleSlides, $activeIndex }) => `
transform: translateX(calc(-1 * ${$activeIndex} * ((100% + 1rem) / ${$visibleSlides})));
grid-template-columns: repeat(${$allSlides}, calc((100% - ${$visibleSlides - 1}rem) / ${$visibleSlides}));
`}
`;
const Scroller = styled.div.attrs({
className: "slider-scroller grid gap-4 transition-transform",
})`
${({ $scrollable }) => ($scrollable ? scrollableStyles : "")}
`;
const Slider = ({ slides, breakpoints }) => {
const { visibleSlides, scrollable } = useActiveBreakpoint(breakpoints);
const [activeIndex, setActiveIndex] = React.useState(0);
const changeSlide = React.useCallback(
(index) => {
setActiveIndex(Math.min(index, slides.length - visibleSlides)); // Don't let it scroll too far
},
[slides, visibleSlides, setActiveIndex]
);
return (
<Container>
<Scroller
$visibleSlides={visibleSlides}
$allSlides={slides.length}
$activeIndex={activeIndex}
$scrollable={scrollable}
>
{slides.map((slide, i) => {
const isVisible = i >= activeIndex && i < activeIndex + visibleSlides;
return (
<div key={`slide-${i}`}>
<Slide
isVisible={isVisible || !scrollable}
onClick={scrollable && !isVisible ? () => changeSlide(i) : null}
>
{slide}
</Slide>
</div>
);
})}
</Scroller>
<Bullets
activeIndex={activeIndex}
allSlides={slides.length}
visibleSlides={visibleSlides}
changeSlide={changeSlide}
/>
</Container>
);
};
Slider.propTypes = {
slides: PropTypes.arrayOf(PropTypes.node.isRequired),
breakpoints: PropTypes.arrayOf(
PropTypes.shape({
minWidth: PropTypes.number.isRequired,
visibleSlides: PropTypes.number.isRequired,
})
),
};
Slider.defaultProps = {
breakpoints: [
{
minWidth: parseInt(theme.screens.xl),
scrollable: true,
visibleSlides: 3,
},
{
minWidth: parseInt(theme.screens.md, 10),
scrollable: true,
visibleSlides: 2,
},
{
// For the smallest screens, we won't scroll but instead stack the slides vertically.
minWidth: -Infinity,
scrollable: false,
visibleSlides: 1,
},
],
};
export default Slider;

View File

@ -0,0 +1 @@
export * from "./Slider";

View File

@ -0,0 +1,23 @@
import { useEffect, useMemo, useCallback, useState } from "react";
import { useWindowSize } from "react-use";
export default function useActiveBreakpoint(breakpoints) {
const { width: windowWidth } = useWindowSize();
// Since our breakpoints are setup with min-width rule, we need to sort them from largest to smallest
const monitoredBreakpoints = useMemo(
() => breakpoints.slice().sort(({ minWidth: widthA }, { minWidth: widthB }) => widthB - widthA),
[breakpoints]
);
const findActiveBreakpoint = useCallback(
() => monitoredBreakpoints.find((breakpoint) => windowWidth >= breakpoint.minWidth),
[monitoredBreakpoints, windowWidth]
);
const [activeBreakpoint, setActiveBreakpoint] = useState(findActiveBreakpoint());
useEffect(() => {
setActiveBreakpoint(findActiveBreakpoint());
}, [findActiveBreakpoint]);
return activeBreakpoint;
}