How to drag carousel on swipe in React - javascript

I'm a junior dev and am struggling with an issue where there is a carousel with about 10 cards on top of the page. All cards do not fit at once on the screen, so there are 2 arrows in the corner to scroll them (arrow left and arrow right). When you click arrow right, the cards scroll to the end and when you click arrow left they move backwards from right to left.
How can I make it that when you click mouse down and hold the images and drag them they move left or right? I need to make them move not only with arrow clicks but with mouse drag also...
Do i need to use some library (i found react-hammerjs, but didn't find any good docs/examples how to use it)?
Thank you in advance for your help
Here's some code to reference:
export const CardsBlock = () => {
const scrollContainer = useRef(null)
const [scrolled, setScrolled] = useState(false)
const dispatch = useDispatch()
useEffect(() => {
const resizeListener = (e) => {
if (e.target.outerWidth <= sizes.mobile) {
setScrolled(null)
} else {
setScrolled(false)
}
}
window.addEventListener('resize', resizeListener)
return () => {
window.removeEventListener('resize', resizeListener)
}
}, [])
useEffect(() => {
if (scrolled) {
scrollTo(scrollContainer.current, scrollContainer.current.offsetWidth)
} else {
scrollTo(scrollContainer.current, 0)
}
}, [scrolled])
return (
<Well>
<Container>
<Wrapper padding={'0 0 16px'} justify={'space-between'} align={'center'}>
<TitleText width={'auto'}>{translator('specilization.title', true)}</TitleText>
<ArrowsContainer>
<Icon
onClick={() => setScrolled(false)}
cursor={'pointer'}
type={'arrow_left'}
color={scrolled ? 'black4' : 'black1'}
/>
<Icon
onClick={() => setScrolled(true)}
cursor={'pointer'}
type={'arrow_right'}
color={scrolled ? 'black1' : 'black4'}
/>
</ArrowsContainer>
</Wrapper>
<SpecializationsInnerContainer ref={scrollContainer}>
{specializations.map((specialization) =>
<SpecializationCard
key={specialization.id}
image={specialization.image}
title={specialization.title}
color={'black1'}
borderColor={'transparent'}
onClick={() => handleOnClick(specialization.id)}
/>
)}
<SpecializationCard borderColor={'black15'} image={'image.png'} isAll onClick={openSpecializationFilter} title={translator('specilization.all', true)} color={'transparent'}/>
</SpecializationsInnerContainer>
</Container>
</Well>
)
}

Since you did not provide any snippet or source code, here is some basic example. It should get you started.
const slider = document.querySelector('.items');
let isDown = false;
let startX;
let scrollLeft;
slider.addEventListener('mousedown', (e) => {
isDown = true;
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
});
slider.addEventListener('mouseleave', () => {
isDown = false;
});
slider.addEventListener('mouseup', () => {
isDown = false;
});
slider.addEventListener('mousemove', (e) => {
if(!isDown) return;
e.preventDefault();
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 3; //scroll-fast
slider.scrollLeft = scrollLeft - walk;
});
https://codepen.io/toddwebdev/pen/yExKoj

During more research found an answer in 'react-swipeable' npm module
https://www.npmjs.com/package/react-swipeable
As the arrow icons onclick logic is already defined here, added the same logic to react swipeable component:
<Swipeable onSwipedLeft={() => setScrolled(true)} onSwipedRight={() => setScrolled(false)} trackMouse={true}>
<SpecializationsInnerContainer ref={scrollContainer}>
{specializations.map((specialization) =>
<SpecializationCard
key={specialization.id}
image={specialization.image}
title={specialization.title}
color={'black1'}
borderColor={'transparent'}
onClick={() => handleOnClick(specialization.id)}
/>
)}
<SpecializationCard borderColor={'black15'} image={'image.png'} isAll onClick={openSpecializationFilter} title={translator('specilization.all', true)} color={'transparent'}/>
</SpecializationsInnerContainer>
</Swipeable>

Related

How to test useRef and functions defined inside of useEffect (using Jest)?

I would like to ask for help to improve my skills about test and code quality.
I read a ton of guides and docs and I was not able to find an example of how to do a better code or test in a clean way.
My component has two useRef and it add some listeners to allow the user resize the div element. it works as expected! But I wish to ensure (by tests) that functions onMouseDownRightResize and onMouseMoveRightResize adjust the component using the correct data.
Could someone please show me:
how to test onMouseDownRightResize or onMouseMoveRightResize once the were defined inside the useEffect?
how to test if ref.current is null once he it is inside of the useEffect
Here is my code and a link to a code pen
https://codepen.io/getjv/pen/LYBZPrp
const { useState, useEffect, useRef } = React;
const App = () => {
const ref = useRef(null);
const refRight = useRef(null);
useEffect(() => {
const resizeableEle = ref.current;
if (!resizeableEle) {
return;
}
const styles = window.getComputedStyle(ref.current);
let width = parseInt(styles.width, 10);
let x = 0;
const onMouseMoveRightResize = (event) => {
const dx = event.clientX - x;
x = event.clientX;
width = width + dx;
resizeableEle.style.width = `${width}px`;
};
const onMouseUpRightResize = (event) => {
document.removeEventListener("mousemove", onMouseMoveRightResize);
};
const onMouseDownRightResize = (event) => {
x = event.clientX;
resizeableEle.style.left = styles.left;
resizeableEle.style.right = "";
document.addEventListener("mousemove", onMouseMoveRightResize);
document.addEventListener("mouseup", onMouseUpRightResize);
};
const resizerRight = refRight.current;
if (!resizerRight) {
return;
}
resizerRight.addEventListener("mousedown", onMouseDownRightResize);
return () => {
resizerRight.removeEventListener(
"mousedown",
onMouseDownRightResize
);
};
}, []);
return (
<div className="App">
<div ref={ref} className="flex w-40 border border-red-500 m-5">
<div className="w-full">move the green -> </div>
<div
ref={refRight}
className="bg-green-500 w-2 cursor-col-resize"
></div>
</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("app"));

Removing an Event Listener if e.target is a link

I'm using a slideshow component from our library of components which gets swipe events from a utility function that's initialized on mount. The problem I'm facing is while testing on phone I found that once you tap on a link in a slide from that slideshow, a swipe event fires along with a click.
this is the util function:
`
function initSwipeEvents(el, deltaMin = 80) {
const swipeData = {
startX: 0,
startY: 0,
endX: 0,
endY: 0,
}
let directionEvents = []
el.addEventListener("touchstart", (e) => {
const t = e.touches[0]
swipeData.startX = t.screenX
swipeData.startY = t.screenY
})
el.addEventListener("touchmove", (e) => {
const t = e.touches[0]
swipeData.endX = t.screenX
swipeData.endY = t.screenY
})
el.addEventListener("touchend", () => {
const deltaX = swipeData.endX - swipeData.startX
const deltaY = swipeData.endY - swipeData.startY
if (Math.abs(deltaX) > deltaMin) {
if (deltaX > 0) directionEvents.push("right")
else directionEvents.push("left")
}
if (Math.abs(deltaY) > deltaMin) {
if (deltaY > 0) directionEvents.push("down")
else directionEvents.push("up")
}
directionEvents.forEach((direction) =>
el.dispatchEvent(new Event(`swipe-${direction}`))
)
directionEvents = []
})
}
export default initSwipeEvents
`
This is the slideshow component:
`
<div
class="slideshow"
tabindex="0"
#swipe-down="goToPrev"
#swipe-up="goToNext"
>
<div
v-show="activeSlideLogic(i, internalIdx)"
class="slide"
>
<slot
:slide="slide"
name="slide"
/>
</div>
</div>
<script>
mounted() {
if (this.swipeEvents) initSwipeEvents(this.$el) **// swipeEvents is just a boolean prop**
},
methods:{
getLoopedIdx(idx) {
if (this.wrap)
return (idx + this.slides.length) % this.slides.length
else return _clamp(idx, 0, this.slides.length - 1)
},
goToNext() {
this.$nextTick(
() =>
(this.internalIdx = this.getLoopedIdx(this.internalIdx + 1))
)
},
goToPrev() {
this.$nextTick(
() =>
(this.internalIdx = this.getLoopedIdx(this.internalIdx - 1))
)
},
}
</script>
`
This is the slide components that is used in the slideshow slot:
<div class="slide-video">
<wp-image **// this is a custom image component, shouldn't be causing the issue**
class="image"
:image="image"
>
<nuxt-link **// this is the link I'm referencing**
class="title-meta"
:to="to"
>
<div class="title-wrapper">
<svg-play class="svg-play" />
<h2
class="title"
v-html="title"
/>
</div>
</nuxt-link>
</wp-image>
</div>
I tried to prevent the default action by doing the folowing:
`
function initSwipeEvents(el, deltaMin = 80) {
const swipeData = {
startX: 0,
startY: 0,
endX: 0,
endY: 0,
}
let directionEvents = []
el.addEventListener("touchstart", (e) => {
const t = e.touches[0]
swipeData.startX = t.screenX
swipeData.startY = t.screenY
// added
console.log(e.target);
if( e.target.matches('.title-meta') ||
e.target.matches('.title-meta *')){
console.log("this is a link");
e.preventDefault();
}
})
el.addEventListener("touchmove", (e) => {
const t = e.touches[0]
swipeData.endX = t.screenX
swipeData.endY = t.screenY
// added
console.log(e.target);
if( e.target.matches('.title-meta') ||
e.target.matches('.title-meta *')){
console.log("this is a link");
e.preventDefault();
}
})
el.addEventListener("touchend", () => {
const deltaX = swipeData.endX - swipeData.startX
const deltaY = swipeData.endY - swipeData.startY
if (Math.abs(deltaX) > deltaMin) {
if (deltaX > 0) directionEvents.push("right")
else directionEvents.push("left")
}
if (Math.abs(deltaY) > deltaMin) {
if (deltaY > 0) directionEvents.push("down")
else directionEvents.push("up")
}
directionEvents.forEach((direction) =>
el.dispatchEvent(new Event(`swipe-${direction}`))
)
directionEvents = []
})
}
export default initSwipeEvents
`
I also tried the css property, touch-action: none; on the link in the slide, to no avail
I expected to be able to click on the link without triggering the swipe event.
I'm not sure where to implement the change, whether on the util or on the slideshow it's self or if the change I implemented is correct.
Edit:
I have tried adding
goToNext(e) {
console.log(e)
this.$nextTick(
() =>
(this.internalIdx = this.getLoopedIdx(this.internalIdx + 1))
)
},
goToPrev(e) {
console.log(e)
this.$nextTick(
() =>
(this.internalIdx = this.getLoopedIdx(this.internalIdx - 1))
)
},
...on the slideshow its self, the console.log is always logging that I'm targeting the div "target: div.slideshow", even when clicking on the link.
While if I log the onWheel method, which listens to the wheel event (it isn't being imported form a utilty), I', getting the expected result.
I think the util might not have the correct scope it needs, as it;s bound to the $el, which is slideshow.
I found out the solution on my own.
Basically what was happening is that the utility function listens for the touchStart, touchMove and touchEnd events.
But if only a tap of a finger happens, there is no touchMove event firing.
TouchEnd event is subtracting touchStart and touchMove data (screenX & screenY).
So result is always bigger then deltaMin and the custom swipe event is triggered.
The solution is to take out the touchMove event and add its logic to touchEnd event.

How to render the actual value of the scroll percentage?

I have this piece of code
import React from 'react'
const App = () => {
function getScrollPercent() {
var h = document.documentElement,
b = document.body,
st = 'scrollTop',
sh = 'scrollHeight';
var scrollPercent = Math.round((h[st]||b[st]) / ((h[sh]||b[sh]) - h.clientHeight) * 100);
//get the percentage of scroll
return (isNaN(scrollPercent) ? '' : scrollPercent)
}
return (
<p>{`${getScrollPercent()}%`}</p>
)
the problem is when I scroll the scrollPercent doesn't refresh in real time but only when the scroll stop, so scrollPercent can pass from 0% to 100% and not display all the numbers between 0 and 100, so the question is how can I modify my piece of code to display the actual value of scrollPercent even when I scroll
You will need to use a useEffect to add an event listener that will listen for the scroll event. Then, you can register a function that will re-calculate the scroll position when the user scrolls up or down the page.
import React, { useEffect, useState } from "react";
function getScrollPercent() {
var h = document.documentElement,
b = document.body,
st = "scrollTop",
sh = "scrollHeight";
var scrollPercent = Math.round(
((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100
);
//get the percentage of scroll
return isNaN(scrollPercent) ? "" : scrollPercent;
}
const App = () => {
// this represents the current calculated scroll percentage
// maybe initialize this to something other than empty string
const [scrollPercentage, setScrollPercentage] = useState("");
useEffect(() => {
function handleScroll() {
const newScrollPercentage = getScrollPercent();
// calculate and set the new scroll percentage
setScrollPercentage(newScrollPercentage);
}
window.addEventListener("scroll", handleScroll, true);
// clean up event listener when component unmounts
return () => {
window.removeEventListener('scroll', handleScroll, true);
}
}, []);
// this contains extra styling to demo, so that you can see the
// scroll value change when scrolling, probably remove these style props
return (
<div style={{ height: "3000px" }}>
<p style={{ position: "fixed" }}>{`${scrollPercentage}%`}</p>
</div>
);
};
export default App;
Codesandbox Link to example: Codesandbox
You need to use window.addEventListener to reach what you want.
import React from "react";
const App = () => {
const [scroll, setScroll] = React.useState("");
function getScrollPercent() {
var h = document.documentElement,
b = document.body,
st = "scrollTop",
sh = "scrollHeight";
var scrollPercent = Math.round(
((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100
);
setScroll(isNaN(scrollPercent) ? "" : scrollPercent);
}
React.useEffect(() => {
window.addEventListener("scroll", getScrollPercent);
return () => {
window.removeEventListener("scroll", getScrollPercent);
};
}, []);
return (
<div style={{ height: "1000px" }}>
<p style={{ position: "fixed" }}>{`${scroll}%`}</p>
</div>
);
};
export default App;
Codesanbox Link: https://codesandbox.io/s/unruffled-field-3l3z8?file=/src/App.js:0-664

"useRefs" variable an initial value of a div reference?

Im trying to build an audio progess bar with react hooks. I was following a tutorial with react class based components but got a little lost with refs.
How can I give my useRef variables an initial value of the div ref when the page loads?
As soon as I start playing then the I get an error saying cant read offsetwidth of null. Obviously timeline ref is null as it doesnt have an initial value. How can I connect it to the div with id of timeline in the useEffect hook?
const AudioPlayer = () => {
const url = "audio file";
const [audio] = useState(new Audio(url));
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0)
let timelineRef = useRef()
let handleRef = useRef()
useEffect(() => {
audio.addEventListener('timeupdate', e => {
setDuration(e.target.duration);
setCurrentTime(e.target.currentTime)
let ratio = audio.currentTime / audio.duration;
let position = timelineRef.offsetWidth * ratio;
positionHandle(position);
})
}, [audio, setCurrentTime, setDuration]);
const mouseMove = (e) => {
positionHandle(e.pageX);
audio.currentTime = (e.pageX / timelineRef.offsetWidth) * audio.duration;
};
const mouseDown = (e) => {
window.addEventListener('mousemove', mouseMove);
window.addEventListener('mouseup', mouseUp);
};
const mouseUp = (e) => {
window.removeEventListener('mousemove', mouseMove);
window.removeEventListener('mouseup', mouseUp);
};
const positionHandle = (position) => {
let timelineWidth = timelineRef.offsetWidth - handleRef.offsetWidth;
let handleLeft = position - timelineRef.offsetLeft;
if (handleLeft >= 0 && handleLeft <= timelineWidth) {
handleRef.style.marginLeft = handleLeft + "px";
}
if (handleLeft < 0) {
handleRef.style.marginLeft = "0px";
}
if (handleLeft > timelineWidth) {
handleRef.style.marginLeft = timelineWidth + "px";
}
};
return (
<div>
<div id="timeline" ref={(timeline) => { timelineRef = timeline }}>
<div id="handle" onMouseDown={mouseDown} ref={(handle) => { handleRef = handle }} />
</div>
</div>
)
}
The useRef() hook returns a reference to an object, with the current property. The current property is the actual value useRef points to.
To use the reference, just set it on the element:
<div id="timeline" ref={timelineRef}>
<div id="handle" onMouseDown={mouseDown} ref={handleRef} />
And then to use it, you need to refer to the current property:
let position = current.timelineRef.offsetWidth * ratio;
And positionHandle - you shouldn't actually set styles on elements in React in this way. Use the setState() hook, and set the style using JSX.
const positionHandle = (position) => {
let timelineWidth = timelineRef.current.offsetWidth - handleRef.current.offsetWidth;
let handleLeft = position - timelineRef.current.offsetLeft;
if (handleLeft >= 0 && handleLeft <= timelineWidth) {
handleRef.current.style.marginLeft = handleLeft + "px";
}
if (handleLeft < 0) {
handleRef.current.style.marginLeft = "0px";
}
if (handleLeft > timelineWidth) {
handleRef.current.style.marginLeft = timelineWidth + "px";
}
};
In addition, the ref can be also used for other values, such as the new Audio(url), and be extracted from the current property:
const { current: audio } = useRef(new Audio(url));

How to correctly wait on state to update/render instead of using a delay/timeout function?

I will attempt to keep this brief, but I am not 100% sure of the correct method of achieving what I am aiming for. I have been thrown in the deep end with React with not much training, so I have most likely been going about most of this component incorrectly, a point in the right direction will definitely help, I don't really expect for someone to completely redo my component for me as it's quite long.
I have a navigation bar SubNav, that finds the currently active item based upon the url/path, this will then move an underline element that inherits the width of the active element. To do this, I find the position of the active item and position accordingly. The same goes for when a user hovers over another navigation item, or when the window resizes it adjusts the position accordingly.
I also have it when at lower resolutions, when the nav gets cut off to have arrows appear to scroll left/right on the navigation to view all navigation items.
Also, if on a lower resolution and the currently active navigation item is off screen, the navigation will scroll to that item and then position the underline correctly.
This, currently works as I have it in my component, this issue is, I don't believe I have done this correctly, I am using a lodash function delay to delay at certain points (I guess to get the correct position of certain navigation items, as it isn't correct at the time of the functions call), which I feel is not the way to go. This is all based on how fast the page loads etc and will not be the same for each user.
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
400,
setArrowStyle(styling)
);
Without using the delay, the values coming back from my state are incorrect as they haven't been set yet.
My question is, how do I go about this correctly? I know my code below is a bit of a read but I have provided a CODESANBOX to play about with.
I have 3 main functions, that all sort of rely on one another:
getPostion()
This function finds the active navigation item, checks if it's within the viewport, if it is not, then it changes the left position of the navigation so it's the leftmost navigation item on the screen, and via setSizes(getSizes()) moves the underline directly underneath.
getSizes()
This is called as an argument within setSizes to update the sizes state, which returns the left and right boundaries of all navigation items
getUnderlineStyle()
This is called as an argument within setUnderLineStyle within the getSizes() function to update the position of the underline object in relation to the position of active navigation item grabbed from the sizes state, but I have to pass the sizesObj as an argument in setSizes as the state has not been set. I think this is where my confusion began, I think I was under the impression, that when I set the state, I could then access it. So, I started using delay to combat.
Below is my whole Component, but can be seen working in CODESANBOX
import React, { useEffect, useState, useRef } from "react";
import _ from "lodash";
import { Link, Route } from "react-router-dom";
import "../../scss/partials/_subnav.scss";
const SubNav = props => {
const subNavLinks = [
{
section: "Link One",
path: "link1"
},
{
section: "Link Two",
path: "link2"
},
{
section: "Link Three",
path: "link3"
},
{
section: "Link Four",
path: "link4"
},
{
section: "Link Five",
path: "link5"
},
{
section: "Link Six",
path: "link6"
},
{
section: "Link Seven",
path: "link7"
},
{
section: "Link Eight",
path: "link8"
}
];
const currentPath =
props.location.pathname === "/"
? "link1"
: props.location.pathname.replace(/\//g, "");
const [useArrows, setUseArrows] = useState(false);
const [rightArrow, updateRightArrow] = useState(false);
const [leftArrow, updateLeftArrow] = useState(false);
const [sizes, setSizes] = useState({});
const [underLineStyle, setUnderLineStyle] = useState({});
const [arrowStyle, setArrowStyle] = useState({});
const [activePath, setActivePath] = useState(currentPath);
const subNavRef = useRef("");
const subNavListRef = useRef("");
const arrowRightRef = useRef("");
const arrowLeftRef = useRef("");
let elsRef = Array.from({ length: subNavLinks.length }, () => useRef(null));
useEffect(
() => {
const reposition = getPosition();
subNavArrows(window.innerWidth);
if (!reposition) {
setSizes(getSizes());
}
window.addEventListener(
"resize",
_.debounce(() => subNavArrows(window.innerWidth))
);
window.addEventListener("resize", () => setSizes(getSizes()));
},
[props]
);
const getPosition = () => {
const activeItem = findActiveItem();
const itemHidden = findItemInView(activeItem);
if (itemHidden) {
const activeItemBounds = elsRef[
activeItem
].current.getBoundingClientRect();
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth =
arrowLeftRef.current !== "" && arrowLeftRef.current !== null
? arrowLeftRef.current.getBoundingClientRect().width
: arrowRightRef.current !== "" && arrowRightRef.current !== null
? arrowRightRef.current.getBoundingClientRect().width
: 30;
const activeItemPos =
activeItemBounds.left * -1 + arrowWidth + currentPos;
const styling = {
left: `${activeItemPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
400,
setArrowStyle(styling)
);
return true;
}
return false;
};
const findActiveItem = () => {
let activeItem;
subNavLinks.map((i, index) => {
const pathname = i.path;
if (pathname === currentPath) {
activeItem = index;
return true;
}
return false;
});
return activeItem;
};
const getSizes = () => {
const rootBounds = subNavRef.current.getBoundingClientRect();
const sizesObj = {};
Object.keys(elsRef).forEach(key => {
const item = subNavLinks[key].path;
const el = elsRef[key];
const bounds = el.current.getBoundingClientRect();
const left = bounds.left - rootBounds.left;
const right = rootBounds.right - bounds.right;
sizesObj[item] = { left, right };
});
setUnderLineStyle(getUnderlineStyle(sizesObj));
return sizesObj;
};
const getUnderlineStyle = (sizesObj, active) => {
sizesObj = sizesObj.length === 0 ? sizes : sizesObj;
active = active ? active : currentPath;
if (active == null || Object.keys(sizesObj).length === 0) {
return { left: "0", right: "100%" };
}
const size = sizesObj[active];
const styling = {
left: `${size.left}px`,
right: `${size.right}px`,
transition: `left 300ms, right 300ms`
};
return styling;
};
const subNavArrows = windowWidth => {
let totalSize = sizeOfList();
_.delay(
() => {
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
300,
setUseArrows(totalSize > windowWidth)
);
};
const sizeOfList = () => {
let totalSize = 0;
Object.keys(elsRef).forEach(key => {
const el = elsRef[key];
const bounds = el.current.getBoundingClientRect();
const width = bounds.width;
totalSize = totalSize + width;
});
return totalSize;
};
const onHover = active => {
setUnderLineStyle(getUnderlineStyle(sizes, active));
setActivePath(active);
};
const onHoverEnd = () => {
setUnderLineStyle(getUnderlineStyle(sizes, currentPath));
setActivePath(currentPath);
};
const scrollRight = () => {
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth = arrowRightRef.current.getBoundingClientRect().width;
const subNavOffsetWidth = subNavRef.current.clientWidth;
let nextElPos;
for (let i = 0; i < elsRef.length; i++) {
const bounds = elsRef[i].current.getBoundingClientRect();
if (bounds.right > subNavOffsetWidth) {
nextElPos = bounds.left * -1 + arrowWidth + currentPos;
break;
}
}
const styling = {
left: `${nextElPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
500,
setArrowStyle(styling)
);
};
const scrollLeft = () => {
const windowWidth = window.innerWidth;
// const lastItemInView = findLastItemInView();
const firstItemInView = findFirstItemInView();
let totalWidth = 0;
const hiddenEls = elsRef
.slice(0)
.reverse()
.filter((el, index) => {
const actualPos = elsRef.length - 1 - index;
if (actualPos >= firstItemInView) return false;
const elWidth = el.current.getBoundingClientRect().width;
const combinedWidth = elWidth + totalWidth;
if (combinedWidth > windowWidth) return false;
totalWidth = combinedWidth;
return true;
});
const targetEl = hiddenEls[hiddenEls.length - 1];
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth = arrowLeftRef.current.getBoundingClientRect().width;
const isFirstEl =
targetEl.current.getBoundingClientRect().left * -1 + currentPos === 0;
const targetElPos = isFirstEl
? targetEl.current.getBoundingClientRect().left * -1 + currentPos
: targetEl.current.getBoundingClientRect().left * -1 +
arrowWidth +
currentPos;
const styling = {
left: `${targetElPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
500,
setArrowStyle(styling)
);
};
const findItemInView = pos => {
const rect = elsRef[pos].current.getBoundingClientRect();
return !(
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
};
const findLastItemInView = () => {
let lastItem;
for (let i = 0; i < elsRef.length; i++) {
const isInView = !findItemInView(i);
if (isInView) {
lastItem = i;
}
}
return lastItem;
};
const findFirstItemInView = () => {
let firstItemInView;
for (let i = 0; i < elsRef.length; i++) {
const isInView = !findItemInView(i);
if (isInView) {
firstItemInView = i;
break;
}
}
return firstItemInView;
};
return (
<div
className={"SubNav" + (useArrows ? " SubNav--scroll" : "")}
ref={subNavRef}
>
<div className="SubNav-content">
<div className="SubNav-menu">
<nav className="SubNav-nav" role="navigation">
<ul ref={subNavListRef} style={arrowStyle}>
{subNavLinks.map((el, i) => (
<Route
key={i}
path="/:section?"
render={() => (
<li
ref={elsRef[i]}
onMouseEnter={() => onHover(el.path)}
onMouseLeave={() => onHoverEnd()}
>
<Link
className={
activePath === el.path
? "SubNav-item SubNav-itemActive"
: "SubNav-item"
}
to={"/" + el.path}
>
{el.section}
</Link>
</li>
)}
/>
))}
</ul>
</nav>
</div>
<div
key={"SubNav-underline"}
className="SubNav-underline"
style={underLineStyle}
/>
</div>
{leftArrow ? (
<div
className="SubNav-arrowLeft"
ref={arrowLeftRef}
onClick={scrollLeft}
/>
) : null}
{rightArrow ? (
<div
className="SubNav-arrowRight"
ref={arrowRightRef}
onClick={scrollRight}
/>
) : null}
</div>
);
};
export default SubNav;
You can make use of useLayoutEffect hook to determine whether the values have been updated and take an action. Since you want to determine whether all the values has been updated, you need to compare old and new values in useEffect. You can refer to the below post to know how to write a usePrevious custom hook
How to compare oldValues and newValues on React Hooks useEffect?
const oldData = usePrevious({ rightArrow, leftArrow, sizes});
useLayoutEffect(() => {
const {rightArrow: oldRightArrow, leftArrow: oldLeftArrow, sizes: oldSizes } = oldData;
if(oldRightArrow !== rightArrow && oldLeftArrow !== leftArrow and oldSizes !== sizes) {
setArrowStyle(styling)
}
}, [rightArrow, leftArrow, sizes])
I think the reason of your delay is necessary here since you calculate based on rectangles of the first and the last element which are affected when you click on button and do animation of scrolling 500ms. So as a result your calculation needs to wait for animation to be done. change the number of animation and delay you will see the relation.
the style I meant.
#include transition(all 500ms ease);
In short, I think what you are using is the right way as long as you have animations related to the calculation.
setState takes an optional second argument which is a callback that executes after the state has been updated and the component has been re-rendered.
Another option is the componentDidUpdate lifecycle method.

Categories

Resources