Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Responsive carousel component #1045

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
838c760
feat(Carousel): add infinite loop
juliansotuyo Apr 29, 2021
bb9de39
feat(Carousel): add responsive layout
juliansotuyo Apr 29, 2021
29d1dc5
refactor(Carousel): add hooks for handling resize and touch
juliansotuyo May 3, 2021
8e6ab41
test(Carousel): add test cases
juliansotuyo May 5, 2021
6789e0a
test(storyshots): update
juliansotuyo May 5, 2021
f4d8d9e
feat(Carousel): add full responsive carousel component
juliansotuyo May 20, 2021
d595425
refactor(Carousel): change the basic structure and remove unnecessary…
juliansotuyo May 31, 2021
aa44660
refactor(Carousel): adjust aligments and rearrange slide logic into s…
juliansotuyo Jun 1, 2021
e8d8ecb
test(Carousel): update tests
juliansotuyo Jun 1, 2021
81bfa09
test(storyshots): update
alcferreira May 20, 2021
b7f86f5
test(storyshots): update carousel storyshots
juliansotuyo Jun 1, 2021
874e00b
feat(Carousel/Slide): add prop to customize border radius
juliansotuyo Jun 7, 2021
dadb0c0
feat(Carousel): add prop to set carousel breakpoints
juliansotuyo Jun 7, 2021
9bde097
fix(Carousel): add delay to ensure additional slides are renderer
juliansotuyo Jun 8, 2021
d27f5f6
feat(Carousel): add option to customize arrow buttons
juliansotuyo Jun 9, 2021
a2d8759
WIP(Carousel): update storyshots and skip tests with error
juliansotuyo Jun 9, 2021
e91bfc0
perf(Carousel/useResizeWindow): add debounce on window resizing
juliansotuyo Jun 14, 2021
bc9da99
perf(Carousel): add debounce in the next and previous slide actions
juliansotuyo Jun 14, 2021
44cea1d
feat(Carousel/useResizeWindow): add breakpoints logic only on resize…
juliansotuyo Jun 14, 2021
5685e41
refactor(Carousel/useTouch): change variable names to more descriptiv…
juliansotuyo Jun 14, 2021
9148bc1
fix(Carousel/useResizeWindow): adjust breakpoints resizing
juliansotuyo Jun 14, 2021
c3ace3b
test(Carousel): update storyshots
juliansotuyo Jun 14, 2021
39c49e4
test(storyshots): fix story using hook
alcferreira Jun 15, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/carousel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@
"react"
],
"peerDependencies": {
"@crave/farmblocks-icon": "^1.0.0",
"@crave/farmblocks-button": "^10.0.5",
"prop-types": "^15.7.2",
"react": "^16.13.0",
"styled-components": "^5.2.2"
},
"dependencies": {
"@crave/farmblocks-image": "^3.2.6",
"@crave/farmblocks-text": "^3.6.5",
"@crave/farmblocks-theme": "^1.10.1"
"@crave/farmblocks-theme": "^1.10.1",
"lodash.debounce": "^4.0.8"
}
}
289 changes: 170 additions & 119 deletions packages/carousel/src/Carousel.js
Original file line number Diff line number Diff line change
@@ -1,139 +1,190 @@
import * as React from "react";
import React, { useState, useEffect, useMemo } from "react";
import PropTypes from "prop-types";
import Image from "@crave/farmblocks-image";
import Text from "@crave/farmblocks-text";

import Container from "./styledComponents/Carousel";

const defaultConfig = {
width: 656,
height: 328,
margin: 20,
fontSize: 88,
displayTime: 4,
transitionTime: 2,
border: "4px solid rgba(255, 255, 255, 0.56)",
borderRadius: "16px",
};
import debounce from "lodash.debounce";
import { SmChevronRight, SmChevronLeft } from "@crave/farmblocks-icon";
import {
Container,
Wrapper,
Content,
ButtonContainer,
ArrowButton,
} from "./styledComponents/Carousel";

import Dots from "./components/Dots";
import useTouch from "./hooks/useTouch";

function Carousel(
{
qtyOfSlidesPerSet,
infiniteLoop,
children,
leftButtonProps,
rightButtonProps,
},
...props
) {
const CAROUSEL_DELAY = 300;

const displayNumber =
qtyOfSlidesPerSet < children.length ? qtyOfSlidesPerSet : children.length;

const [dotIndex, setDotIndex] = useState(0);
const [currentIndex, setCurrentIndex] = useState(
infiniteLoop && displayNumber < children.length ? displayNumber : 0,
);

useEffect(() => {
if (infiniteLoop) setCurrentIndex(dotIndex + displayNumber);
else if (currentIndex > children.length - qtyOfSlidesPerSet)
setCurrentIndex(children.length - qtyOfSlidesPerSet);
}, [displayNumber]);

const totalOfCards = children.length;
const isRepeating = useMemo(
() => infiniteLoop && children.length > displayNumber,
[children, infiniteLoop, displayNumber],
);

const [transitionEnabled, setTransitionEnabled] = useState(true);

useEffect(() => {
if (isRepeating) {
if (currentIndex === displayNumber || currentIndex === totalOfCards) {
setTransitionEnabled(true);
}
}
}, [currentIndex, isRepeating, displayNumber, totalOfCards]);

class Carousel extends React.Component {
constructor(props) {
super(props);
this.state = {
activeItem: 0,
};
function handleDotClick(index) {
setDotIndex(index);
setCurrentIndex(index + displayNumber);
}

componentDidMount = () => {
this.setInterval();
};
const nextSlide = debounce(() => {
if (dotIndex < totalOfCards - 1) setDotIndex((prevState) => prevState + 1);
else if (isRepeating && dotIndex === totalOfCards - 1) {
setDotIndex(0);
}
const result = totalOfCards - displayNumber;

componentDidUpdate(prevProps) {
if (prevProps.imageSet !== this.props.imageSet) {
this.setState({ activeItem: 0 });
this.setInterval();
if (isRepeating || currentIndex < result) {
setCurrentIndex((prevState) => prevState + 1);
}
}
}, CAROUSEL_DELAY);

componentWillUnmount = () => {
this.clearInterval();
};
const prevSlide = debounce(() => {
if (dotIndex === 0) setDotIndex(totalOfCards - 1);
else setDotIndex((prevState) => prevState - 1);

nextItem = () => {
const activeItem = this.state.activeItem + 1; // eslint-disable-line
if (activeItem === this.props.imageSet.length) {
this.clearInterval();
this.props.onEnd();
return;
if (isRepeating || currentIndex > 0) {
setCurrentIndex((prevState) => prevState - 1);
}
this.props.onChange(activeItem);
this.setState({ activeItem });
};

setInterval = () => {
if (this.transitionId) {
return;
}, CAROUSEL_DELAY);

const { handleTouchStart, handleTouchMove } = useTouch({
nextSlide,
prevSlide,
});

function handleTransitionEnd() {
if (isRepeating) {
if (currentIndex === 0) {
setTransitionEnabled(false);
setCurrentIndex(totalOfCards);
} else if (currentIndex === totalOfCards + displayNumber) {
setTransitionEnabled(false);
setCurrentIndex(displayNumber);
}
}
}

const { displayTime } = { ...defaultConfig, ...this.props.itemConfig };

this.transitionId = window.setInterval(this.nextItem, displayTime * 1000);
};

clearInterval = () => {
if (this.transitionId) {
window.clearInterval(this.transitionId);
delete this.transitionId;
const renderExtraPrev = useMemo(() => {
const output = [];
for (let index = 0; index < displayNumber; index += 1) {
output.push(children[totalOfCards - 1 - index]);
}
};

render() {
const { imageSet, itemConfig } = this.props;
const configProps = { ...defaultConfig, ...itemConfig };

return (
<Container
activeItem={this.state.activeItem}
itemConfig={configProps}
shouldScale={this.props.scale}
className={this.props.className}
>
<ul>
{imageSet.map((item, index) => {
const isActive = index === this.state.activeItem;
return (
<li key={index} className={isActive ? "active" : ""}>
<Image
className="image"
src={item.image}
width="100%"
height="100%"
borderRadius={configProps.borderRadius}
css={{
border: configProps.border,
}}
/>
<Text
className="itemLabel"
size={configProps.fontSize}
align="center"
fontWeight="title"
>
{item.name}
</Text>
</li>
);
})}
</ul>
</Container>
);
}
output.reverse();
return output;
}, [children, totalOfCards, displayNumber]);

const renderExtraNext = useMemo(() => {
const output = [];
for (let index = 0; index < displayNumber; index += 1) {
output.push(children[index]);
}
return output;
}, [children, displayNumber]);

const showLeftArrow = isRepeating || currentIndex > 0;
const showRightArrow =
isRepeating || currentIndex < totalOfCards - displayNumber;
const renderExtras = totalOfCards > displayNumber && isRepeating;

return (
<Container {...props}>
<Wrapper>
<ButtonContainer direction="left">
{showLeftArrow && (
<ArrowButton
data-testid="left-arrow"
icon={<SmChevronLeft size={24} />}
{...leftButtonProps}
onClick={(event) => {
prevSlide();
leftButtonProps?.onClick?.(event);
}}
/>
)}
</ButtonContainer>
<div style={{ overflow: "hidden" }}>
<Content
currentIndex={currentIndex}
displayNumber={displayNumber}
transitionEnabled={transitionEnabled}
onTransitionEnd={handleTransitionEnd}
onTouchStart={(event) => handleTouchStart(event)}
onTouchMove={(event) => handleTouchMove(event)}
>
{renderExtras && renderExtraPrev}
{children}
{renderExtras && renderExtraNext}
</Content>
</div>
<ButtonContainer direction="right">
{showRightArrow && (
<ArrowButton
data-testid="right-arrow"
icon={<SmChevronRight size={24} />}
{...rightButtonProps}
onClick={(event) => {
nextSlide();
rightButtonProps?.onClick?.(event);
}}
/>
)}
</ButtonContainer>
</Wrapper>
{isRepeating && (
<Dots
slides={children}
handleClick={handleDotClick}
selectedDot={dotIndex}
/>
)}
</Container>
);
}

Carousel.propTypes = {
imageSet: PropTypes.arrayOf(
PropTypes.shape({ image: PropTypes.string, name: PropTypes.string }),
),
onChange: PropTypes.func,
onEnd: PropTypes.func,
scale: PropTypes.bool,
itemConfig: PropTypes.shape({
width: PropTypes.number,
height: PropTypes.number,
margin: PropTypes.number,
fontSize: PropTypes.number,
displayTime: PropTypes.number,
transitionTime: PropTypes.number,
border: PropTypes.string,
}),
className: PropTypes.string,
children: PropTypes.node.isRequired,
qtyOfSlidesPerSet: PropTypes.number,
infiniteLoop: PropTypes.bool,
leftButtonProps: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
rightButtonProps: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
};

Carousel.defaultProps = {
itemConfig: defaultConfig,
scale: true,
onChange: () => null,
onEnd: () => null,
infiniteLoop: false,
qtyOfSlidesPerSet: 1,
};

export default Carousel;
Loading