Display overflow indicator in React - javascript

I'm working on a commercial project in React where customer wants to display used and unused devices on one screen of the website. On the left, there is one div with used devices which takes up max 70% of the screen width and on the right is div with unused devices, which takes up min 30% of the screen width (or rather 550px). The number of used devices on the right may vary, so if there is not enough to fill the 70% of the screen, unused devices on the right will expand to fill the gap (display: flex). However, if there is more used devices than can fit in the 70% of the screen width, I want to display a custom overflow indicator on the left side of unused devices. The reason for this, is that the used devices will go under the used devices (z-index 0 and 1) and I want to show that there is overflow of used devices, that there are more than user can see. For this overflow indicator I wanted to use a gradient on the left side of unused devices. I did manage to achieve this, but with some problems.
I used a state boolean variable showMore to indicate when should I display the overflow indicator. The value change of this variable is happening in custom function checkOverflow:
function checkOverflow() {
if (ref.current && refUnused.current) {
if (
window.innerWidth - refUnused.current?.clientWidth <
ref.current?.clientWidth
) {
setShowMore(true);
} else {
setShowMore(false);
}
}
}
ref is representing used devices and refUnused unused devices. I use this function in useEffect and useLayoutEffect as I need to check this on load and on window resize.
useLayoutEffect(
() => {
if (reload()) {
checkOverflow();
}
window.addEventListener("resize", checkOverflow);
return function cleanupListener() {
window.removeEventListener("resize", checkOverflow);
};
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[ref]
);
useEffect(
() => {
checkOverflow();
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[ref]
);
However, the problem I ran into is flickering in the situation where the values differ just slightly. It's flickering between showing the overflow indicator and not showing it. I also get console error message
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Does anyone know how to prevent this flickering and React error/warning? Or any idea on how to achieve this differently?
Here is the code:
<div className="vehicle-main-panel">
<div className="vehicle-standorten" ref={ref}> //used devices
{(() => {
const array = [];
items.stos.map((item, index) => {
return array.push(
<Veh
standort={item}
vehicles={item.vehicles}
key={index}
isVehicle={true}
></Veh>
);
});
return array;
})()}
</div>
<div className="vehicle-right-panel"> //unused vehicles and gradient
<div className="vehicle-overflow-hor-1">
<div className="vehicle-card-overflow">
<div className="vehicle-card-overflow-box gradient-90">
<p style={{ fontWeight: "bold", fontSize: "40px" }}>
... {/* {t("vehicles.more-items")} */}
</p>
</div>
</div>
</div>
<div
className={
showMore
? "vehicle-nonfunctional-absolute"
: "vehicle-nonfunctional-flex"
}
ref={refUnused}
>
<Veh value={items.notReady} isVehicle={false}></Veh>
</div>
</div>
</div>

Related

React, Expanding box inside wrapper causes wrapper to stretch rather than auto adjust

I've been trying to create a wrapper function so that i could load a sprite map into it and have an auto adjusting box that maintains the correct dimensions, rather than stretch the inital image that the sprites were made from. The problem occurs when the children of the wrapper are expanded, it causes the container box to expand but doesnt allow the wrapper to auto adjust its dimensions. This causes one of the borders, either top and bottom, or left and right to be larger/smaller than the other pair, which is not the desired effect.
The wrapper:
const AdaptBox = ({patternID, borderWidth, children}) =>{
const parentRef = useRef(null);
const [pixelHSize, setPixelHSize] = useState(borderWidth);
const [pixelWSize, setPixelWSize] = useState(borderWidth);
const imgs = SPRITE_PATTERNS[patternID];
useEffect ( () => {
console.log("use effect");
if(parentRef.current){
//useEffect hook to get the parents dimensions and then adjust ratio of wrapper
let parentHeight = parentRef.current.scrollHeight;
let parentWidth = parentRef.current.scrollWidth;
console.log(parentHeight);
console.log(parentWidth);
if(parentHeight<parentWidth){
console.log("wide scale");
setPixelHSize(((parentWidth/parentHeight)*borderWidth)/(parentWidth/parentHeight));
setPixelWSize(((parentHeight/parentWidth)*borderWidth));
}
else if(parentHeight>parentWidth){
console.log("tall scale");
setPixelHSize(((parentWidth/parentHeight)*borderWidth));
setPixelWSize(((parentHeight/parentWidth)*borderWidth)*(parentHeight/parentWidth));
}
else{
setPixelHSize(borderWidth);
setPixelWSize(borderWidth);
}
}
},[parentRef, borderWidth]);
return (
<Box img={imgs[4]} ref={parentRef}> //all styled comps
<BoxRow h={pixelHSize} w={100}>
<RowStuff.../>
</BoxRow>
<BoxRow h={100-(2*pixelHSize)} w={100}>
<VSide img={imgs[3]} h={100} w={pixelWSize}/>
<Back >
{children}
</Back>
<VSide img={imgs[5]} h={100} w={pixelWSize}/>
</BoxRow>
<BoxRow h={pixelHSize} w={100}>
<RowStuff.../>
</BoxRow>
</Box>
);
}
Implimentation of the wrapper:
...
<MainBodyContainer> //styled comp
<MainBody img={BGimg}> //styled comp
<AdaptBox patternID={0} borderWidth={1}> //Wrapper
<CoeffDisplay ></CoeffDisplay> //Child comp
</AdaptBox>
</MainBody>
</MainBodyContainer>
...
The "CoeffDisplay" contains a number of boxes that can be clicked on to expand.
Im open to another method of achieving this however i'm a fan of this wrapper style.
I've triple checked the simple maths but i think the issue is related to the use effect not being called on rerender.
Thanks for the help.

React intersection observer does not work when responsive

I'm using react-intersection-observer in order to make some animations when the component is in view.
It works cool, but, when i try to go to a mobile screen, like, 633px width and 400px height, it does not work, and i really don't know why.
Let me show you the code
So, what i'm doing here, is that, i want to know when the user is seeing 45% of the component, but, it gives false always, is it because this component is quite big, probably its height is 750px or even more and the height of the screen of the phone is only 400px ? i don't know... I'm just trying to understand why it doens't work
const DifferentWorks = () => {
const [scrollY, setScrollY] = useState(0);
const { inView, ref } = useInView({ threshold: 0.45 });
useEffect(() => {
scrollYProgress.onChange(n => setScrollY(n));
}, [inView, scrollYProgress, scrollY]);
console.log(inView)
return (
<div ref={ref}>
<DifferentWorksHero>
</DifferentWorksHero>
</div>
);
};
export default DifferentWorks;
And let me show you what is the output of the console.log(inView)
I've scrolled all the component, from bottom to top, and this is the output
Always false..
What could be generating that error ? why it doesn't work when i change the size of the screen ?, specifally when the width is 663px and the height is under 500px

Reactjs, How to make scroll stay at current position when scrollable div extends

I'm trying to build a react Chat app and I was going to use the infinite scroll but it doesn't work the way I wanted it to. So I built one myself. I used useref to determine the height and if it reaches to the top it will add more chats. kinda like fb messenger's chat. The problem now is that when I add more chats, once it reaches the top, the scroll bar will continue to go up and not stick in place unlike how react-infinite scroll works.
May I know how I can go through this? Here is my code.
for the scroll:
const onScroll = () => {
if (topDiv.current) {
const { scrollTop, scrollHeight, clientHeight } = topDiv.current;
if(scrollTop === 0){
props.fecthMoreChat()
}
}
};
return(
<div id='scrollableDiv' ref={topDiv} onScroll={()=> onScroll()} style={{height:'100%', padding:'20px', display:'flex', flexDirection:'column', backgroundColor:'#efefef', overflowY:'scroll'}}>
{/* <div style={{visibility:'hidden'}} ref={topDiv}></div> */}
{currentChat.map((chat, index)=>{
return(
<div key={index} style={{textAlign:chat.sender===username?'right':'left'}}>
{chat.message}
</div>
)
})}
<div style={{visibility:'hidden'}} ref={botMsg}></div>
</div>
)
So the answer was actually pretty simple. I just took the current scroll height, saved it in a variable, then waited for the re-render and took the new scroll height. Basically, just subtract the old scroll height with the new scroll height and use that number to set scroll by using scrollTo(). Here is the code:
const onScroll = async () => {
if (chatDiv.current) {
const { scrollTop, scrollHeight, clientHeight } = chatDiv.current;
if(scrollTop === 0){
const pastScroll = scrollHeight
await props.fecthMoreChat()
const currentScroll = (await chatDiv.current.scrollHeight-pastScroll)
await chatDiv.current.scrollTo(0, currentScroll)
}
}
};

React scroll event doesn't stop firing

At work we're having some performance issues with rendering data tables so we've decided to try to virtualize a list "window". Essentially following the same idea as react-window, whereby you only render the sublist - the one showing on your viewport - of your data list.
For a myriad of reasons, we tried to implement the technique ourselves. In doing so, we learnt this is mostly done using position: absolute on each list item, which didn't really suitable for us. So we came up with the idea of just having two "wrapper" divs around the sublist we want to render.
Essentially box1 would have the height equal to the combined height of all the list items before our window and box2 would have the height of all the items after our window. Every time the user scrolls, we figure out which indices to render and adjust the box heights.
Unfortunately we ran into an issue where, when the user scrolls down, the scroll event keeps firing even after the user has stopped scrolling. This scrolls the list all the way to the end. It seems to work fine when scrolling up though, so we're really at a loss here. We couldn't figure out why it keeps firing.
Here's a link to an example. I just replaced all the list item logic with a fixed box for simplicity. I've also added a timeout to the scroll handler so the scrolling up behaviour is more noticeable, otherwise it's too fast and the red upper box is not noticeable.
Any help is much appreciated. Thanks in advance!
EDIT: We're actually using this in a <table> element, which means solutions based on css position property will not work, given that that property has undefined behaviour for table elements, and it breaks the standard table layout.
The problem is most likely caused by using the scrollTop value to change the height of the items which cause the scrollTop value to change and so on (maybe).
Here is the right way to do it https://codesandbox.io/s/react-hooks-playground-forked-97vsq
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
const ROW_HEIGHT = 25;
const App = (props) => {
const [items, innerHeight, onScroll] = useVirtualizedList({
numItems: 500,
itemHeight: ROW_HEIGHT,
windowHeight: ROW_HEIGHT * 5,
windowExtension: 0
});
return (
<div onScroll={onScroll} style={{ height: "500px", overflowY: "scroll" }}>
<div style={{
// position should be calculated depanding on the parent element position
position: 'fixed'
}}>{items}</div>
<div className="forceOverflow" style={{height: 500 * ROW_HEIGHT}}></div>
</div>
);
};
const useVirtualizedList = ({ numItems, itemHeight, windowHeight }) => {
const [scrollTop, setScrollTop] = useState(0);
const innerHeight = numItems * itemHeight;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
numItems - 1,
startIndex + Math.floor( windowHeight / itemHeight)
);
const onScroll = useCallback((e) => {
const currentScroll = e.currentTarget.scrollTop;
setScrollTop(currentScroll);
}, []);
const items = `${startIndex} --- ${endIndex}`
return [items, innerHeight, onScroll];
};
ReactDOM.render(<App />, document.getElementById("root"));

Scroll events unintentionally changing Material UI slider values

I have a React application that is utilizing Material UI. The application has slider components implemented in a lot of places (https://material.io/components/sliders). When using a touch screen device, I am unintentionally impacting slider components (the value is getting changed) while trying to scroll up or down the page. This does not happen with other page components, I suspect this is because sliders do (and should) respond to swipe events.
Material UI has documentation that implies a way to discern between a "scroll" and a "swipe" (https://material.io/design/interaction/gestures.html#types-of-gestures). Is there a way for me to indicate to my slider components that a "scroll" should be ignored. Or, can I discern between a vertical or horizontal swipe, telling the slider to ignore vertical swipes?
I have come up with a fairly elegant solution, I believe, which allows the user to scroll if their scroll position begins on the track but not on the thumbs. This replicates the native HTML range input so I feel that this is the best solution.
There's two parts to this
Step 1, allow touch-action on the slider root element as it is disabled by default and prevents the user from starting a scroll on the slider
const useStyles = makeStyles({
sliderRoot: {
touchAction: "auto"
}
});
return (
<Slider
classes={{
root: classes.sliderRoot
}}
...
/>
Step 2, stop propagation on the root element with a ref
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
ref.current.addEventListener(
"touchstart",
(e) => {
const isThumb = e.target?.dataset.index;
if (!isThumb) {
e.stopPropagation();
}
},
{ capture: true }
);
}
});
return (
<Slider
ref={ref}
...
/>
And here is a fully working demo on Codesandbox.
There is not going to be a completely clean solution to this other than telling your users to watch their finger placement when scrolling on mobile devices.
Using a controlled Slider, one approach would be to listen to the touchstart event and record the current pageY on the first changedTouches object. Then compare that coordinate to the pageY on the onChangeCommited event handler for the corresponding touchmove event. If the difference between the two coordinates is larger than some predefined range, then do not update the Slider value.
Inside your component using the Slider:
const delta = 50
const sliderRef = useRef(null)
const [value, setValue] = useState(0) // Or from some prop
const [touchStart, setTouchStart] = useState(0)
const debouncedHandler = useMemo(() => {
// Using lodash.debounce
return debounce((evt, value) => {
// If it is a mouse event then just update value as usual
if (evt instanceof MouseEvent) {
setValue(value)
}
}, 25)
}, [])
useLayoutEffect(() => {
if (sliderRef.current) {
sliderRef.current.addEventListener('touchstart', evt => {
setTouchStart(evt.changedTouches[0].pageY)
})
}
}, [])
return (
<Slider
value={value}
ref={sliderRef}
onChange={debouncedHandler}
onChangeCommitted={(evt, value) => {
if (evt instanceof TouchEvent) {
if (Math.abs(touchStart - evt.changedTouches[0].pageY) < delta) {
setValue(value)
}
} else {
setValue(value)
}
}}
/>
)
This will prevent the Slider from changing value on TouchEvent when the difference between the starting y-coordinate and the ending y-coordinate is larger than delta. Adjust delta to whatever value you like. The tradeoff is that you will not get as smooth of a transition when adjusting the Slider with a normal MouseEvent (or TouchEvent within the predefined range).
See the jsFiddle.
Or npm i mui-scrollable-slider-hook and use it like
import Slider from '#mui/material/Slider'
import { useMuiScrollableSlider } from 'mui-scrollable-slider-hook'
const { ref, value, onChange, onChangeCommitted } = useMuiScrollableSlider()
return <Slider ref={ref} value={value} onChange={onChange} onChangeCommitted={onChangeCommitted} />
An example of using mui-scrollable-slider-hook on codesandbox.

Categories

Resources