Scroll events unintentionally changing Material UI slider values - javascript

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.

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.

Display overflow indicator in React

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>

Is there a way to listen for 'element resize' and NOT for the entire window.resize?

I'm trying to observe the dimensions of a container - a div (its width to be specific) to hide/show certain tags if the available space cannot contain them.
What I'm doing at the moment is to listen for 'window.resize' and make calculations.
Should I be listening just for the container resize instead of the entire window.resize ? If so, how ?
Taking into account performance, how could you compare them ?
Current state of my custom hook, as you can see, makes calculations based on window.resize
import React from "react";
// To decide on whether or not tags should be hidden...
export const useCalculateAvailableSpace = refParam => {
if (refParam === null) return false;
const [shouldHide, setShouldHide] = React.useState(false),
calculateAvailableSpace = refParam => {
if (!refParam || !refParam.current) return;
const containerWidth = refParam.current.getBoundingClientRect().width,
childrenWidth = Array.from(refParam.current.children).reduce((acc, cur) => acc + cur.getBoundingClientRect().width, 0);
setShouldHide(childrenWidth > containerWidth);
},
handleResize = () => {
calculateAvailableSpace(refParam);
};
React.useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return shouldHide;
};
https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event
In some earlier browsers it was possible to register resize event
handlers on any HTML element. It is still possible to set onresize
attributes or use addEventListener() to set a handler on any element.
However, resize events are only fired on the window object (i.e.
returned by document.defaultView). Only handlers registered on the
window object will receive resize events.
While the resize event fires only for the window nowadays, you can get
resize notifications for other elements using the ResizeObserver API.
https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver

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"));

How do I (correctly) check whether an element is within the viewport in React?

In a functional react component, I'm trying to check whether a call to action button (a different component) is within the viewport. If it's not, I want to display a fixed call to action button at the bottom of the viewport, which shows/hides, depending on whether the other button is visible.
I can do this using a combination of Javascript and react hooks, but although the code works in some components in my app, it doesn't work in others; I'm guessing due to react lifecycles.
I'm also aware that this is NOT the way I should be doing things in react, so would prefer to achieve the same result, but in a proper 'react way'.
I've been looking at using refs, but ideally wanted to avoid having to change my functional component to a class, as I'd like to use react hooks for the show/hide of the fixed cta. However, if this is a requirement in order to get the functionality I want, I could go for that.
Here's what I've got so far - basically, I want to replace document.querySelector with a react method:
useEffect(() => {
const CTA = document.querySelector('#CTANextSteps');
const ApplyStyle = () => (isInViewport(CTA) ? setVisible(false) : setVisible(true));
ApplyStyle();
window.addEventListener('scroll', ApplyStyle);
window.addEventListener('resize', ApplyStyle);
return function cleanup() {
window.removeEventListener('scroll', ApplyStyle);
window.removeEventListener('resize', ApplyStyle);
};
});
const isInViewport = (elem) => {
const bounding = elem.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
As mentioned above, this function works in some areas of the app without issue, but doesn't in others; I get a Cannot read property 'getBoundingClientRect' of null error. I was surprised it worked at all, but rather than tinkering with it to try and get it working everywhere, I want to rewrite it properly.
As always, any assistance would be much appreciated. Thanks.
I was able to do it with the depedency react-visibility-sensor#5.1.1
I followed the tutorial in this link and it worked fine with me.
I don't know if this is the correct way to do it, but it works!
Here is the link https://www.digitalocean.com/community/tutorials/react-components-viewport-react-visibility-sensor
I'll put an example just in case the previous link ever goes out.
import React, { Component } from 'react';
import VisibilitySensor from 'react-visibility-sensor';
class VisibilitySensorImage extends Component {
state = {
visibility: false
}
render() {
return (
<VisibilitySensor
onChange={(isVisible) => {
this.setState({visibility: isVisible})
}}
>
<img
alt={this.props.alt}
src={this.props.src}
style={{
display: 'block',
maxWidth: '100%',
width: '100%',
height: 'auto',
opacity: this.state.visibility ? 1 : 0.25,
transition: 'opacity 500ms linear'
}}
/>
</VisibilitySensor>
);
}
}
export default VisibilitySensorImage;

Categories

Resources