Simple Carousel in React - How to register Scrolling to next Slide? - javascript

This is a relatively simple problem, but I haven't been able to solve it. I have built the following carousel / slider:
const BlogPostCardSlider = ({ children }) => {
const [activeSlide, setActiveSlide] = useState(0);
const activeSlideRef = useRef(null);
const wrapperRef = useRef(null);
const firstRenderRef = useRef(true);
useEffect(() => {
if (firstRenderRef.current) {
//this is checking whether its the first render of the component. If it is, we dont want useEffect to run.
firstRenderRef.current = false;
} else if (activeSlideRef.current) {
activeSlideRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}
}, [activeSlide]);
const moveRight = () => {
if (activeSlide + 1 >= children.length) {
return children.length - 1;
}
return activeSlide + 1;
};
const moveLeft = () => {
if (activeSlide - 1 <= 0) {
return 0;
}
return activeSlide - 1;
};
return (
<div id="trap" tabIndex="0">
<button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
{children.map((child, i) => {
return (
<SlideLink
key={`slideLink-${i}`}
isActive={activeSlide === i}
onClick={e => {
setActiveSlide(i);
}}
>
{i + 1}
</SlideLink>
);
})}
<Wrapper
onScroll={e => {
let { width } = wrapperRef.current.getBoundingClientRect();
let { scrollLeft } = wrapperRef.current;
if ((scrollLeft / width) % 1 === 0) {
setActiveSlide(scrollLeft / width);
}
}}
ref={wrapperRef}
>
{children.map((child, i) => {
return (
<Slide
key={`slide-${i}`}
ref={i === activeSlide ? activeSlideRef : null}
>
{child}
</Slide>
);
})}
</Wrapper>
<button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
</div>
);
};
export default BlogPostCardSlider;
It displays its children as Slides in a carousel. You can navigate the carousel by pressing the 'NEXT' or 'PREVIOUS' buttons. There is also a component that shows you what slide you're on (e.g.: 1 2 *3* 4 etc.). I call these the SlideLink(s). Whenever the activeSlide updates, the SlideLink will update and highlight you the new current Slide.
Lastly, you can also navigate by scrolling. And here's the problem:
Whenever somebody scrolls, I am checking what slide they're on by doing some calculations:
onScroll={e => {
let { width } = wrapperRef.current.getBoundingClientRect();
let { scrollLeft } = wrapperRef.current;
if ((scrollLeft / width) % 1 === 0) {
setActiveSlide(scrollLeft / width);
}
}}
...the result of this is that setActiveSlide is only called once the slide completely in view. This results in a laggy experience : the user has scrolled to the next slide, the next slide is in view, but the calculation is not completed yet (since it is only completed once the Slide is 100% in view), so the active SlideLink gets updated very late in the process.
How would I solve this? Is there some way to optimistically update this?

You can modify the calculations little bit so that when the slide is more than 50% in the view, set the active property to that one.
onScroll={e => {
let { width } = wrapperRef.current.getBoundingClientRect();
let { scrollLeft } = wrapperRef.current;
setActiveSlide(Math.round(scrollLeft / width) + 1);
}}
Example: Carousel with width: 800px, Total slides: 3. So, scrollLeft will vary from 0 to 1600
From 0-400: Active slide = Math.round(scrollLeft / width) = 0th index
(or first slide)
From 400-1200: Active slide = 1st index (or second
slide) since it is more in the view
From 1200-1600: Active slide =
2nd index (or third slide) since it is more in the view
Hope it helps. Revert for any doubts.

At first sight I thought this should be a quick fix. Turned out a bit more was necessary. I create a working example here:
https://codesandbox.io/s/dark-field-g78zb?fontsize=14&hidenavigation=1&theme=dark
The main point is that setActiveSide should not be called during the onScroll event, but rather when the user is done with scrolling. This is achieved in the example using the onIdle react hook.

Related

When I click on the button, if the value of the button is 0, then the text will not show in that space. I mean, how can it be done without showing 10?

When I click on the button, if the value of the button is 0, then the text will not show in that space. I mean, how can it be done without showing -10?
console.log(variable)
The this is the error I'm getting ..
const Mobile = () => {
const [charge, setCharge] = useState(20)
const setMobileCharge = () => setCharge(charge - 10)
if (setMobileCharge === 0) {
return 0;
}
return (
<div>
<h3>Charge : {charge}</h3>
<button onClick={setMobileCharge}>Battery down</button>
</div>
);
};
export default Mobile;
this happens because the react "ticks" your render is not fully synced with the state ... so to prevent that you can set the new state like that
setCharge(oldCharge => oldCharge -10 )
you can add a condition like below,
const Mobile = () => {
const [charge, setCharge] = useState(20)
const setMobileCharge = () => {
if(charge > 0) setCharge(charge - 10)} //here
if (setMobileCharge === 0) {
return 0;
}
return (
<div>
<h3>Charge : {charge === 0 ? "" : charge}</h3> //here
<button onClick={setMobileCharge}>Battery down</button>
</div>
);
};
I'm don't quite understand your question.
But if you want when 0 not to decrease, you can :
const setMobileCharge = () => {
if (charge > 0) {
setCharge(charge - 10)
}
}
If you want when 0 Mobile component disappear you can:
if (charge === 0) {
return 0;
}

Anchor link not working when manipulating scroll via JS

I'm creating a landing page and it has a bar to navigate through the website sections. But, it's not working and I gathered some resources that made me believe it's because of the Infinite Slide animation I did on the website.
The Anchor tags only navigate when window.clientWidth >= 1920.
I have two Infinite section sliders that load the animation, adding +.5 to scrollLeft.
So when the view goes below the enable boundary (<1920px), the anchor links stop working. Is it possible to make them work again without removing the animations?
export const SliderContainer: React.FC<SliderProps> = ({
children,
initialOffsetX,
className,
contentWidth,
contentClassName,
}) => {
const { innerWidth } = useSize();
const refScrollX = useRef<number>(initialOffsetX);
const refContainer = useRef<HTMLDivElement>(null);
const refContent = useRef<HTMLDivElement>(null);
const enabled = innerWidth < contentWidth;
useAnimationFrame(
enabled,
useCallback(() => {
const { current: elContainer } = refContainer;
const { current: elContent } = refContent;
if (elContainer && elContent) {
refScrollX.current += 0.5;
elContainer.scrollLeft = refScrollX.current;
if (elContainer.scrollLeft >= elContent.clientWidth) {
refScrollX.current = 0;
elContainer.scrollLeft = 0;
}
}
}, [])
);
const cn = "inline-block".concat(" ", contentClassName || "");
return (
<div ref={refContainer} className={`overflow-x-hidden whitespace-nowrap max-w-full pointer-events-none ${className}`}>
<div ref={refContent} className={cn}>
{children}
</div>
<div className={enabled ? cn : "hidden"}>{children}</div>
</div>
);
};

Interactive carousel with react

I've been trying to create a component similar to instagram stories for a few hours. That is nothing more than an interactive carousel, where you can move forward and backward. The thing is, my strategy with setTimeOut fires every time I interact and does not cancel the state, so if the user is in the first photo and clicks 5x in a row to go to the sixth photo, in a few seconds the settimeout will be like a tsunami and the component advances 5x stories in a row starting from 6. I wanted somehow to reset my timer every time the user clicks on a new image. Seems crazy or is it possible?
I don't know if there is anyone with enough patience to see the code, if not, if someone at least knows a new approach. Thank you very much, I have been trying to resolve this for some time.
function Stories(){
const files = [ 'image1.jpg', 'image2.jpg' , 'video3.mp4' ]
const videoRef = useRef()
const imageRef = useRef()
const [ index , setIndex ] = useState(0); // the index starts at 0... first file...
const [ focus, setFocus ] = useState(null);
// Every time the index of the storie is changed. This effect happens.
useEffect(() => {
const video = videoRef.current;
const image = imageRef.current;
if( files[index].includes('mp4')){
video.style.display = "inline"
video.src = files[index];
// when i put it i put something in the "setFocus"
// triggers a useEffect for the "focus"
// that makes the setTimeOut which in the end runs
// this same script again after a certain time.
setFocus(files[index]);
// if there is any image in the DOM, hide it
if( image ) {
image.style.display = "none";
}
//In case the files [index] is an image.
} else {
image.style.display = "inline"
image.src = files[index];
setFocus(files[index])
// if there is any video in the DOM, hide it
if ( video ){
video.style.display = 'none';
video.muted = true
}
}
},[index])
function back(){
if( index <= 0 ) return
setIndex( index - 1);
}
function next(){
if ( index < files.length - 1) setIndex( index + 1 );
}
// This useeffect fires every time a file comes into focus.
useEffect(() => {
const timelapse = 5000;
// I wait for the video to finish to advance to the next index
if( files[index].includes('mp4')){
videoRef.current.addEventListener('ended',() => setIndex( index + 1 ));
}
// If it's an image, so..
else {
setTimeout(() => {
if( focus !== null && index < files.length - 1){
setIndex(index + 1);
}
}, timelapse )
}
},[focus])
// HTML
return <>
<video ref={videoRef}/>
<img ref={imageRef} />
<button onClick={back}> back </button>
<button onClick={next}> next </button>
</>
}
You can capture the ID of the timeout and cancel it like this:
const timeout = setTimeout(() => { console.log("woo") }, 5000)
window.clearTimeout(timeout)
So one approach would be to store that and cancel the timeout when your back or next functions are invoked.
Another approach would be to use state to record the remaining time and use a timer (setInterval) that counts down every second and advances the slide if it reaches zero. Then onClick you can just re-set the time remaining to 5 seconds.
const CarouselExample = () => {
const [time, setTime] = useState(5)
const [slide, setSlide] = setState(1)
useEffect(() => {
const countdown = window.setInterval(() => {
setTime(timeRemaining => {
if (timeRemaining - 1 < 0) {
setSlide(prevSlide => prevSlide + 1)
return 5
} else {
return timeRemaining - 1
}
})
}, 1000)
return () => {
window.clearInterval(countdown)
}
}, [])
return (
<div>
<div>Current Slide: {slide}</div>
<button
onClick={() => {
setTime(5)
setSlide(prev => prev + 1)
}}
>
Next
</button>
</div>
)
}

Adding a simple left/right swipe gesture

I need to add a simple left/right swipe gesture so that the 'selected' image cycles when swiped on mobile, similar to clicking the buttons in the hero component, also similar to pressing the left/right arrow keys on a keyboard
I don't have the most experience with JavaScript so if anyone could tell me what exactly to write and where so that I can completely wrap up this project.
Here is a demo: http://nufaith.ca/justinatkins/
Code:
Vue.component('hero-bg', {
template: `
<div class="hero-bg">
<div class="hero">
<img id="pushed" :src="selected"/>
</div>
</div>
`,
props: ['selected']
});
Vue.component('hero-bg-empty', {
template: `
<div class="hero-bg">
<div class="hero">
<span style="display:block;height:100px;"></span>
</div>
</div>
`
});
Vue.component('hero', {
template: `
<div>
<topbar v-if="!gridEnabled"></topbar>
<topbar2 v-if="gridEnabled"></topbar2>
<hero-bg :selected="selectedItem.img" v-if="!gridEnabled"></hero-bg>
<hero-bg-empty v-if="gridEnabled"></hero-bg-empty>
<div class="hero-container" v-if="!gridEnabled">
<div class="hero">
<img :src="selectedItem.img" v-if="thing" alt=""/>
</div>
<div class="hero-desc">
<button class="control left" #click="previous">
<i class="zmdi zmdi-chevron-left"></i>
</button>
<span class="hero-desc-title" v-html="title"></span>
<button class="control right" #click="next">
<i class="zmdi zmdi-chevron-right"></i>
</button>
<br/>
<button class="view-all-button" #click="enableGrid">OVERVIEW</button>
</div>
</div>
</div>
`,
data() {
return {
gridEnabled: false,
selected: 0,
thing: true
};
},
computed: {
selectedItem() {
return info[this.selected];
},
title() {
const comma = this.selectedItem.title.indexOf(',');
const len = this.selectedItem.title.length;
const strBeginning = this.selectedItem.title.substring(comma, 0);
const strEnd = this.selectedItem.title.substring(comma, len);
if (this.selectedItem.title.includes(',')) {
return `<span>${strBeginning}<span class="font-regular font-muted">${strEnd}</span></span>`;
}
return this.selectedItem.title;
},
maxImages() {
return info.length - 1;
}
},
created() {
window.addEventListener('keydown', e => {
if (e.keyCode === 37) {
this.previous();
return;
}
if (e.keyCode === 39) {
this.next();
return;
}
});
Event.$on('updateImg', index => {
this.selected = index;
this.gridEnabled = !this.gridEnabled;
});
},
methods: {
next() {
this.selected === this.maxImages ? (this.selected = 0) : (this.selected += 1);
},
previous() {
this.selected === 0 ? (this.selected = this.maxImages) : (this.selected -= 1);
},
enableGrid() {
this.gridEnabled = !this.gridEnabled;
window.scroll(0, 0);
Event.$emit('enableGrid');
}
}
});
This is how I implemented a simple swipe gesture in one of my projects. You may check this out.
Code:
touchableElement.addEventListener('touchstart', function (event) {
touchstartX = event.changedTouches[0].screenX;
touchstartY = event.changedTouches[0].screenY;
}, false);
touchableElement.addEventListener('touchend', function (event) {
touchendX = event.changedTouches[0].screenX;
touchendY = event.changedTouches[0].screenY;
handleGesture();
}, false);
function handleGesture() {
if (touchendX < touchstartX) {
console.log('Swiped Left');
}
if (touchendX > touchstartX) {
console.log('Swiped Right');
}
if (touchendY < touchstartY) {
console.log('Swiped Up');
}
if (touchendY > touchstartY) {
console.log('Swiped Down');
}
if (touchendY === touchstartY) {
console.log('Tap');
}
}
Basically, touchableElement mentioned here, refers to the DOM Element that will receive the touch event. If you want to activate swipe options on your entire screen, then you may use your body tag as the touchable element. Or you may configure any specific div element as the touchable element, in case you just want the swipe gesture on that specific div.
On that touchableElement, we are adding 2 event-listeners here:
touchstart:
this is when user starts swiping. We take that initial coordinates (x,y) and
store them into touchstartX, touchstartY respectively.
touchend: this is when user stops swiping. We take that final coordinates (x, y) and store them into touchendX, touchendY respectively.
Keep in mind that, the origin of these coordinates is the top left corner of the screen. x-coordinate increases as you go from left to right and y-coordinate increases as you go from top to bottom.
Then, in handleGesture(), we just compare those 2 pair of coordinates (touchstartX, touchstartY) and (touchendX, touchendY), to detect different types of swipe gesture (up, down, left, right):
touchendX < touchstartX: says that, user started swiping at a higher X value & stopped swiping at a lower X value. That means, swiped from right to left (Swiped Left).
touchendX > touchstartX: says that, user started swiping at a lower X value & stopped swiping at a higher X value. That means, swiped from left to right (Swiped Right).
touchendY < touchstartY: says that, user started swiping at a higher Y value & stopped swiping at a lower Y value. That means, swiped from bottom to top (Swiped Up).
touchendY > touchstartY: says that, user started swiping at a lower Y value & stopped swiping at a higher Y value. That means, swiped from top to bottom (Swiped Down).
You may add the code for these 4 different events (Swipe Up/Down/Left/Right), on the corresponding if blocks, as shown on the code.
I took smmehrab’s answer, added some thresholds to avoid accidental swipes, and turned it into a little library. Might come in handy, so here it is:
export default class TouchEvent
{
static SWPIE_THRESHOLD = 50 // Minumum difference in pixels at which a swipe gesture is detected
static SWIPE_LEFT = 1
static SWIPE_RIGHT = 2
static SWIPE_UP = 3
static SWIPE_DOWN = 4
constructor(startEvent, endEvent)
{
this.startEvent = startEvent
this.endEvent = endEvent || null
}
isSwipeLeft()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_LEFT
}
isSwipeRight()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_RIGHT
}
isSwipeUp()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_UP
}
isSwipeDown()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_DOWN
}
getSwipeDirection()
{
let start = this.startEvent.changedTouches[0]
let end = this.endEvent.changedTouches[0]
if (!start || !end) {
return null
}
let horizontalDifference = start.screenX - end.screenX
let verticalDifference = start.screenY - end.screenY
// Horizontal difference dominates
if (Math.abs(horizontalDifference) > Math.abs(verticalDifference)) {
if (horizontalDifference >= TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_LEFT
} else if (horizontalDifference <= -TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_RIGHT
}
// Verical or no difference dominates
} else {
if (verticalDifference >= TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_UP
} else if (verticalDifference <= -TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_DOWN
}
}
return null
}
setEndEvent(endEvent)
{
this.endEvent = endEvent
}
}
How to use
Simply feed it the events from touchstart and touchend:
import TouchEvent from '#/TouchEvent'
let touchEvent = null;
document.addEventListener('touchstart', (event) => {
touchEvent = new TouchEvent(event);
});
document.addEventListener('touchend', handleSwipe);
function handleSwipe(event)
{
if (!touchEvent) {
return;
}
touchEvent.setEndEvent(event);
if (touchEvent.isSwipeRight()) {
// Do something
} else if (touchEvent.isSwipeLeft()) {
// Do something different
}
// Reset event for next touch
touchEvent = null;
}
This sounds like a job for Hammer.JS, unless you're trying to avoid dependencies. They have good documentation and examples for getting started
My Vue knowledge is next to nothing, so I'm wary of this becoming a blind leading the blind scenario, but the first thing you'll have to do is add the dependency using either npm or yarn - then add it to the top of your file using
import Hammer from 'hammerjs'
Try adding the below code right above this line: Event.$on('updateImg', index => {
const swipeableEl = document.getElementsByClassName('.hero')[0];
this.hammer = Hammer(swipeableEl)
this.hammer.on('swipeleft', () => this.next())
this.hammer.on('swiperight', () => this.previous())
If it doesn't work you'll have to check your developer tools / console log to see if it's logged any useful errors.
This codepen might be a useful resource too:
Good luck.
Using #smmehrab answer, I created a Vue 3 composable that also works for SSR builds.
import { onMounted, Ref } from 'vue'
export type SwipeCallback = (event: TouchEvent) => void;
export type SwipeOptions = {
directinoal_threshold?: number; // Pixels offset to trigger swipe
};
export const useSwipe = (touchableElement: HTMLElement = null, options: Ref<SwipeOptions> = ref({
directinoal_threshold: 10
})) => {
const touchStartX = ref(0);
const touchEndX = ref(0);
const touchStartY = ref(0);
const touchEndY = ref(0);
onMounted(() => {
if (!touchableElement)
touchableElement = document.body;
touchableElement.addEventListener('touchstart', (event) => {
touchStartX.value = event.changedTouches[0].screenX;
touchStartY.value = event.changedTouches[0].screenY;
}, false);
touchableElement.addEventListener('touchend', (event) => {
touchEndX.value = event.changedTouches[0].screenX;
touchEndY.value = event.changedTouches[0].screenY;
handleGesture(event);
}, false);
});
const onSwipeLeft: Array<SwipeCallback> = [];
const onSwipeRight: Array<SwipeCallback> = [];
const onSwipeUp: Array<SwipeCallback> = [];
const onSwipeDown: Array<SwipeCallback> = [];
const onTap: Array<SwipeCallback> = [];
const addEventListener = (arr: Array<SwipeCallback>, callback: SwipeCallback) => {
arr.push(callback);
};
const handleGesture = (event: TouchEvent) => {
if (touchEndX.value < touchStartX.value && (Math.max(touchStartY.value, touchEndY.value) - Math.min(touchStartY.value, touchEndY.value)) < options.value.directinoal_threshold) {
onSwipeLeft.forEach(callback => callback(event));
}
if (touchEndX.value > touchStartX.value && (Math.max(touchStartY.value, touchEndY.value) - Math.min(touchStartY.value, touchEndY.value)) < options.value.directinoal_threshold) {
onSwipeRight.forEach(callback => callback(event));
}
if (touchEndY.value < touchStartY.value && (Math.max(touchStartX.value, touchEndX.value) - Math.min(touchStartX.value, touchEndX.value)) < options.value.directinoal_threshold) {
onSwipeUp.forEach(callback => callback(event));
}
if (touchEndY.value > touchStartY.value && (Math.max(touchStartX.value, touchEndX.value) - Math.min(touchStartX.value, touchEndX.value)) < options.value.directinoal_threshold) {
onSwipeDown.forEach(callback => callback(event));
}
if (touchEndY.value === touchStartY.value) {
onTap.forEach(callback => callback(event));
}
}
return {
onSwipeLeft: (callback: SwipeCallback) => addEventListener(onSwipeLeft, callback),
onSwipeRight: (callback: SwipeCallback) => addEventListener(onSwipeRight, callback),
onSwipeUp: (callback: SwipeCallback) => addEventListener(onSwipeUp, callback),
onSwipeDown: (callback: SwipeCallback) => addEventListener(onSwipeDown, callback),
onTap: (callback: SwipeCallback) => addEventListener(onTap, callback)
}
}
Example usage:
const { onSwipeLeft, onSwipeRight } = useSwipe(document.body);
onSwipeLeft((e:TouchEvent) => {
//logic
});

Logic for rotating a square

The following code is using React.
I have a square positioned at 45deg, that has four elements. Upon click on each element, I want the square to rotate the relevant degrees so that the clicked element is on top. (In the image, 3 is currently selected).
Please see the notes within the code. I hope it explains what my logic is.
Here is the component code:
class CentreCtrls extends React.Component {
constructor(props) {
super(props);
this.state = {
aboutUs: {
activeCtrl: 0
}
}
}
// animate the square to rotate the relevant degrees
// so that the clicked element is at the top.
// this logic is flawed and not working correctly
componentDidUpdate() {
const activeHeading = this.state.aboutUs.activeCtrl;
const { centreCtrl } = this.refs;
const style = centreCtrl.style;
const currentDeg = parseInt(style.transform.slice(7, -4), 10);
const position = this.getPosition(activeHeading, 0);
const degRotation = position * 90;
console.log('pos:', position, 'deg:', currentDeg, '-', degRotation);
anime({
targets: this.refs.centreCtrl,
rotate: `-${currentDeg + degRotation}deg`,
duration: 150,
easing: 'linear',
});
}
onClickHandler = e =>
const ele = e.target;
const i = ele.parentNode.getAttribute('data-i');
this.setState({
aboutUs: {
activeCtrl: parseInt(i, 10),
},
});
};
// the purpose of this function is to find the current value
// to be assigned to the 'data-i' property on element, this 'data-i'
// is read in the above 'componentDidUpdate' function to animate
// the square the relevant degrees so that the clicked square is at
// top (where the purple 3 is in the image).
getPosition = (i, activeHeading) => {
const total = 3; // start: 0, ttl: 4
let correctSeq = i;
if (i === activeHeading) {
correctSeq = 0;
// if i == 2, then for our rotation purpose, its at position 3 in DOM
// because of the way, DOM renders elements from left to right within
// square
} else if (i === 2) {
correctSeq = 3;
} else if (i === 3) {
correctSeq = 2;
}
let pos = total - activeHeading + (correctSeq + 0);
if (pos > 3) {
pos = pos - 3;
}
return pos;
};
render() {
const { data } = this.props;
const activeHeading = this.state.aboutUs.activeCtrl;
return (
// using the react v15 style ref
<div
className="centreCtrl"
ref="centreCtrl"
style={{ transform: 'rotate(45deg)' }}
>
{data.map((entry, i) => {
const j = this.getPosition(i, activeHeading);
return (
<div
className="ctrl"
key={i}
data-i={j}
id={`${entry.narrative.heading}`}
onClick={this.onClickHandler}
>
<div
className="textContainer"
id={entry.narrative.heading}
ref={`textRef${i}`}
>
{j}
</div>
</div>
);
})}
</div>
);
}
}
Edit: How can I get the rotation logic to work correctly. More to the point, I am looking for input into what is the best logic for rotation calculation of this square. It looks far easier than I estimated.
Assuming you want to rotate clockwise, something like this might help:
import React from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
import { Container, Square, Block, Top, Bottom } from './components';
const rotateRight = items => {
const last = items.pop();
items.unshift(last);
};
class App extends React.Component {
state = {
items: ["a", "b", "c", "d"],
rotation: 0,
};
handleClick = i => {
let start = i;
let count = 0;
let items = [...this.state.items];
const end = items.length - 1;
while (start <= end) {
rotateRight(items);
start += 1;
count += 1;
}
this.setState(state => ({
rotation: ((count * 90) % 360),
}));
};
render() {
const { items, rotation } = this.state;
return (
<Container>
<Square rotation={rotation}>
<Top>
<Block onClick={() => this.handleClick(0)}>
{items[0]}
</Block>
<Block onClick={() => this.handleClick(1)}>
{items[1]}
</Block>
</Top>
<Bottom>
<Block onClick={() => this.handleClick(2)}>{
items[2]}
</Block>
<Block onClick={() => this.handleClick(3)}>
{items[3]}
</Block>
</Bottom>
</Square>
</Container>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
StackBlitz link here.

Categories