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 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({
|
2022-03-22 12:46:04 +00:00
|
|
|
className: "slider-scroller grid transition-transform",
|
2022-02-23 10:26:17 +00:00
|
|
|
})`
|
|
|
|
${({ $scrollable }) => ($scrollable ? scrollableStyles : "")}
|
|
|
|
`;
|
|
|
|
|
2022-03-22 12:46:04 +00:00
|
|
|
const Slider = ({ slides, breakpoints, scrollerClassName, className }) => {
|
2022-02-23 10:26:17 +00:00
|
|
|
const { visibleSlides, scrollable } = useActiveBreakpoint(breakpoints);
|
|
|
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
|
|
const changeSlide = React.useCallback(
|
2022-03-02 10:58:35 +00:00
|
|
|
(event, index) => {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
2022-02-23 10:26:17 +00:00
|
|
|
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 (
|
2022-03-22 12:46:04 +00:00
|
|
|
<Container className={className}>
|
2022-02-23 10:26:17 +00:00
|
|
|
<Scroller
|
|
|
|
$visibleSlides={visibleSlides}
|
|
|
|
$allSlides={slides.length}
|
|
|
|
$activeIndex={activeIndex}
|
|
|
|
$scrollable={scrollable}
|
2022-03-22 12:46:04 +00:00
|
|
|
className={scrollerClassName}
|
2022-02-23 10:26:17 +00:00
|
|
|
>
|
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-03-22 12:46:04 +00:00
|
|
|
onClickCapture={
|
|
|
|
scrollable && !isVisible
|
|
|
|
? (event) => changeSlide(event, index > activeIndex ? activeIndex + 1 : activeIndex - 1)
|
|
|
|
: 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({
|
2022-02-26 11:48:24 +00:00
|
|
|
/**
|
|
|
|
* Breakpoint name as defined in Tailwind config. If not defined, config
|
|
|
|
* will be applied to all non-configured screen sizes.
|
|
|
|
*/
|
|
|
|
name: PropTypes.string,
|
|
|
|
/**
|
|
|
|
* Number of slides visible for a given breakpoint.
|
|
|
|
*/
|
2022-02-23 10:26:17 +00:00
|
|
|
visibleSlides: PropTypes.number.isRequired,
|
2022-02-26 11:48:24 +00:00
|
|
|
/**
|
|
|
|
* Whether or not the list should be scrollable horizontally at the given breakpoint.
|
|
|
|
* If set to false, all slides will be visible & rendered in a column.
|
|
|
|
*/
|
|
|
|
scrollable: PropTypes.bool.isRequired,
|
2022-03-22 12:46:04 +00:00
|
|
|
/**
|
|
|
|
* Additional class names to apply to the <Scroller /> element.
|
|
|
|
*/
|
|
|
|
scrollerClassName: PropTypes.string,
|
|
|
|
/**
|
|
|
|
* Additional class names to apply to the <Container /> element.
|
|
|
|
*/
|
|
|
|
className: PropTypes.string,
|
2022-02-23 10:26:17 +00:00
|
|
|
})
|
|
|
|
),
|
|
|
|
};
|
|
|
|
|
|
|
|
Slider.defaultProps = {
|
|
|
|
breakpoints: [
|
|
|
|
{
|
2022-02-26 11:48:24 +00:00
|
|
|
name: "xl",
|
2022-02-23 10:26:17 +00:00
|
|
|
scrollable: true,
|
|
|
|
visibleSlides: 3,
|
|
|
|
},
|
|
|
|
{
|
2022-02-26 11:48:24 +00:00
|
|
|
name: "md",
|
2022-02-23 10:26:17 +00:00
|
|
|
scrollable: true,
|
|
|
|
visibleSlides: 2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// For the smallest screens, we won't scroll but instead stack the slides vertically.
|
|
|
|
scrollable: false,
|
|
|
|
visibleSlides: 1,
|
|
|
|
},
|
|
|
|
],
|
2022-03-22 12:46:04 +00:00
|
|
|
scrollerClassName: "gap-4",
|
|
|
|
className: "",
|
2022-02-23 10:26:17 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export default Slider;
|