2022-02-23 10:26:17 +00:00
|
|
|
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]
|
|
|
|
);
|
|
|
|
|
2022-02-26 11:25:01 +00:00
|
|
|
React.useEffect(() => {
|
|
|
|
const maxIndex = slides.length - visibleSlides;
|
|
|
|
|
|
|
|
// Make sure to not scroll too far when screen size changes.
|
|
|
|
if (activeIndex > maxIndex) {
|
|
|
|
setActiveIndex(maxIndex);
|
|
|
|
}
|
|
|
|
}, [slides.length, visibleSlides, activeIndex]);
|
|
|
|
|
2022-02-23 10:26:17 +00:00
|
|
|
return (
|
|
|
|
<Container>
|
|
|
|
<Scroller
|
|
|
|
$visibleSlides={visibleSlides}
|
|
|
|
$allSlides={slides.length}
|
|
|
|
$activeIndex={activeIndex}
|
|
|
|
$scrollable={scrollable}
|
|
|
|
>
|
2022-02-26 11:30:55 +00:00
|
|
|
{slides.map((slide, index) => {
|
|
|
|
const isVisible = index >= activeIndex && index < activeIndex + visibleSlides;
|
2022-02-23 10:26:17 +00:00
|
|
|
|
|
|
|
return (
|
2022-02-26 11:30:55 +00:00
|
|
|
<div key={`slide-${index}`}>
|
2022-02-23 10:26:17 +00:00
|
|
|
<Slide
|
|
|
|
isVisible={isVisible || !scrollable}
|
2022-02-26 11:30:55 +00:00
|
|
|
onClick={scrollable && !isVisible ? () => changeSlide(index) : null}
|
2022-02-23 10:26:17 +00:00
|
|
|
>
|
|
|
|
{slide}
|
|
|
|
</Slide>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</Scroller>
|
2022-02-23 10:54:43 +00:00
|
|
|
{scrollable && (
|
|
|
|
<Bullets
|
|
|
|
activeIndex={activeIndex}
|
|
|
|
allSlides={slides.length}
|
|
|
|
visibleSlides={visibleSlides}
|
|
|
|
changeSlide={changeSlide}
|
|
|
|
/>
|
|
|
|
)}
|
2022-02-23 10:26:17 +00:00
|
|
|
</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;
|