Is it possible to register onScroll event on a react element which does not have scrollbar / overflow ?
I wish to create zoom on a picture with mouse wheel scrolling.
I tried
<div style={{position: "relative", width: "100%", height:"100%"}} onScroll={ () => console.log("TEST")}>
this wont fire event, neither will onWheel. If I change event to onClick everything works.
I know it's possbile, because there is a React components which make it work, I tried react wheel handler and it works. But how do they make it register mouse wheel events when there is no overflow or scrollbars?
EDIT:
I got it to work when I manually added eventlistener to componentDidMount, this is my current component. Is there a way I could make synthetic event onWheel work? I tried to add onWheel to div's and img but it wont fire.
class Zoom extends Component {
state = {
size: 20,
}
componentDidMount() {
window.addEventListener('wheel', this.handleScroll, true);
}
handleScroll = e => {
e.preventDefault();
console.log(e)
if (e.deltaY > 0 && this.state.size < 50) {
this.setState({size: this.state.size + 1})
} else if (e.deltaY < 0 && this.state.size > 20) {
this.setState({size: this.state.size - 1})
}
}
render = () =>
<div className="livetods-product-image-container" style={{
top: openAsModal() ? '25%' : '20%',
left: openAsModal() ? '30%' : '',
right: openAsModal() ? '30%' : '',
width: this.state.size + "%"
}}>
<div className="livetods-product-image-button-container" >
<div></div>
<h6 className="livetods-product-image-name">{this.props.name}</h6>
<span className="livetods-modal-header-close-button" onClick={e => this.props.hideImage()}>×</span>
</div>
<div style={{position: "relative", width: "100%", height:"100%"}}>
<div style={{position: "absolute", padding: "5px"}}>
<FontAwesomeIcon icon={faSearchPlus} color={'red'} style={{ width: '20px', height: '20px', pointerEvents:"none"}}/>
</div>
<img src={this.props.src} style={{borderRadius: "5px"}}/>
</div>
</div>
}
Use the onWheel event and its properties instead of onScroll to achieve the same result.
onScroll event no longer bubbles up in react. This question is pretty similar.
onWheel works for both trackpad scrolling as well as mouse wheel scrolling.
Here is a simple example that shows onWheel working with your provided code sample.
UPDATE:
I have changed the codesanbox example to reflect your class component. onWheel works in it too now. The issue you could be facing earlier was a warning like this one:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the method `isPropagationStopped` on a released/nullified synthetic event. This is a no-op function. If you must keep the original synthetic event around, use event.persist(). See https://reactjs.org/docs/legacy-event-pooling.html for more information.
The workaround to which is adding e.persist() to your onScroll function, as shown in my example.
Related
I want to have a carousel that each item is a zoomable image, the carousel should be all over the screen so I use Portal for that. To support zoom I use ImageZoom component from react-native-image-pan-zoom, and the carousel is from react-native-reanimated-carousel, in the following way:
<Portal>
<Carousel
loop
windowSize={1}
width={SCREEN_WIDTH}
height={SCREEN_HEIGHT}
data={images}
style={{
flex: 1,
backgroundColor: "black",
}}
defaultIndex={imageIndexToDisplay}
onSnapToItem={handleImageChange}
renderItem={({ item: { url, width, height } }) => (
<ImageZoom
cropWidth={SCREEN_WIDTH}
cropHeight={SCREEN_HEIGHT}
imageWidth={width}
imageHeight={height}
enableSwipeDown
onSwipeDown={handleClose}
onClick={handlePress}
useNativeDriver
>
<Image
imageSource={url}
defaultImage={defaultImage}
imageStyle={{ height, width }}
/>
</ImageZoom>
)}
/>
</Portal>
What happens is that the carousel barely let me scroll left or right since it seems like the ImageZoom responds first to the scrolls. I tried to set onStartShouldSetPanResponder={() => false} on the ImageView which solves the Carousel scrolling but doesn't let me use the ImageZoom to zoom since it appears like the Carousel now responds first to gestures.
I would like to know if there is any way to make them both work together.
Thanks ahead!
import React, {useState, useEffect} from "react";
// The tooltip component is used for the Tooltip feature that is utilized
const Tooltip = (couponProps) => {
const { couponTheme, verticalMousePosition, data, showTooltip, isSwiper } = couponProps;
const { TooltipText } = data;
const [tooltipStyles, setTooltipStyles] = useState({display: 'none'});
const [tooltipArrowStyles, setTooltipArrowStyles] = useState({display: 'none'});
const { TooltipTextColor, TooltipBackgroundColor, TooltipFontSize } = couponTheme;
// The useEffect hook will first define all the tooltip styling as objects. If the showTooltip variable defined in the parent element is active, render the tooltip.
useEffect(() => {
const topTooltipStyles = {
bottom: '130%',
left: '20%',
right: '20%',
width: '60%'
}
const topTooltipArrowStyles = {
top: '100%',
left: '50%',
marginLeft: '-5px',
borderColor: (TooltipBackgroundColor || 'black') + ' transparent transparent transparent',
marginTop: 'unset'
}
const bottomTooltipStyles = {
top: '125%',
left: '20%',
right: '20%',
width: '60%'
}
const bottomTooltipArrowStyles = {
bottom: '100%',
left: '50%',
marginLeft: '-5px',
borderColor: 'transparent transparent ' + (TooltipBackgroundColor || 'black') + ' transparent'
}
if (showTooltip) {
// The tooltip is configured to either render at the top of the interval bar or at the bottom of the interval bar. That is pased on where the mouse is located on the screen.
let tooltipPositionStyles = {};
let tooltipArrowPositionStyles = {};
// If the vertical mouse position is less than 250px to the top, render the tooltip at the bottom under the parent componet. This means the user is at the top of the screen, so use the bottom styling.
if (verticalMousePosition < 250 || isSwiper) {
// This way, the tooltip will not be cut of from the top of the screen.
tooltipPositionStyles = bottomTooltipStyles;
tooltipArrowPositionStyles = bottomTooltipArrowStyles;
// Else, that means the user is not at the top of the screen
} else {
tooltipPositionStyles = topTooltipStyles;
tooltipArrowPositionStyles = topTooltipArrowStyles;
}
const tooltipArrowStylesObj = {
content: ' ',
position: 'absolute',
borderWidth: '5px',
borderStyle: 'solid',
...tooltipArrowPositionStyles
}
const tooltipStylesObj = {
position: 'absolute',
color: TooltipTextColor || 'white',
background: TooltipBackgroundColor || 'black',
padding: '10px',
borderRadius: '10px',
zIndex: '5000',
textAlign: 'center',
...tooltipPositionStyles
}
// Set all of the arrow styles after determining if the tooltip is on top or on bottom.
setTooltipArrowStyles(tooltipArrowStylesObj);
setTooltipStyles(tooltipStylesObj);
} else {
// If the showTooltip variable is false, hide the tooltip.
setTooltipArrowStyles({});
setTooltipStyles({display: 'none'});
}
}, [showTooltip, verticalMousePosition, TooltipBackgroundColor, TooltipTextColor, isSwiper])
return (
<>{TooltipText !== undefined && TooltipText.trim() !== '' && TooltipText !== 'None' && TooltipText !== 'Inset Tooltip Text Here' && showTooltip ?
<div className='TooltipDiv' style={tooltipStyles}>
<span className="ToolTipText" style={{fontSize: TooltipFontSize || '12px'}}>{TooltipText}</span>
<span className="ToolTipArrow" style={tooltipArrowStyles}></span>
</div>
: null}</>
);
}
export default Tooltip;
So the code is passed the vertical mouse position as a prop and if the mouse is within 250 from the top of the page the tooltip is rendered below the component hovered. It all works fine if the component renders above and the mouse is lower down the page because there is nothing above to obstruct the tooltip, but when the component renders below, with bottomTooltipStyles, it is absolutely positioned but the issue is it is rendering behind another component. My gut said it was obviously the z-index, but this tooltip has a zIndex of 5000 which is significantly larger than the next largest z-index on the page at 60. Checking google chrome dev tools, the styles are all appropriate, the component renders, but it is still somehow behind/hidden behind another component no matter what I do. Any ideas would be greatly appreciated!! Thanks in advance!
So my question can also be asked like, "Is there a css property that would cause an element to render 'above' (over) another element that has an arbitrarily high z-index?"
Most of the gotchas with z-index have to do with "stacking context".
Elements are stacked on the z-axis within their stacking context.
Elements without position or explicit z-index values all share the same stacking context and are rendered in order of appearance in the rendered HTML.
Here are some specific z-index gotchas related to stacking context that may be affecting you:
1. z-index only applies to positioned elements
That is, position: absolute, position: relative, position: fixed, or position: sticky) and flex items. [1]
So first, make sure the elements you want to position on the z-axis are all explicitly positioned.
2. Some css properties can move an element into a new stacking context.
Some common ones are opacity and transform. Here is a list of CSS properties that can affect the stacking context.
And here is a detailed explanation on how opacity values affect stacking context:
Since an element with opacity less than 1 is composited from a single
offscreen image, content outside of it cannot be layered in z-order
between pieces of content inside of it. For the same reason,
implementations must create a new stacking context for any element
with opacity less than 1. If an element with opacity less than 1 is
not positioned, then it is painted on the same layer, within its
parent stacking context, as positioned elements with stack level 0. If
an element with opacity less than 1 is positioned, the ‘z-index’
property applies as described in [CSS21], except that if the used
value is ‘auto’ then the element behaves exactly as if it were ‘0’. [2]
To fix these, explicitly set the position and z-index so that they will be evaluated relative to the other positioned elements.
3. If an element's parent z-index (and position) is set, then that element's z-index will only apply within the parent.
In other words, the parent element is the stacking context.
To fix this, you can either modify the HTML hierarchy, or remove the position of the parent, or modify its z-index.
There are some good visuals and code examples for these situations here: https://www.freecodecamp.org/news/4-reasons-your-z-index-isnt-working-and-how-to-fix-it-coder-coder-6bc05f103e6c/
I had the same issue when rendering multiple components in one parent component. My popup div didn't appear on top of other components even though it had a higher z-index.
After a little bit of research, I found this answer helpful.
I had to move the popup component above everything else.
Also, please fix your code snippet so we can find out exactly what's wrong.
Here I have a timer minute state and a scroll container, where the state value changes based on the scroll position and vice versa:
const Container = ({ minutes, setMinutes}) => {
draggableElement = useRef(null);
const handleScroll = () => {
const scrollableWidth =
draggableElement.current.container.current.scrollWidth -
draggableElement.current.container.current.offsetWidth;
const amountScrolled = draggableElement.current.scrollLeft;
const rangeValue = (amountScrolled / scrollableWidth) * 30;
setMinutes(Math.round(rangeValue));
}
useEffect(() => {
draggableElement.current.container.current.scrollLeft = minutes * 20;
}, [minutes]);
return (
<ScrollContainer
className="container"
ref={draggableElement}
onScroll={handleScroll}
id="scrolling-container"
vertical={false}
style={{
display: "grid",
gridTemplateColumns: "repeat(100, 0.25em)",
//border: "1px solid yellow",
width: "4em",
height: "50%",
alignItems: "center",
cursor: "grab",
marginBottom: "1rem",
}}
>
<Tick >1</Tick>
<Tick >2</Tick>
<Tick >3</Tick>
<Tick >4</Tick>
<Tick >5</Tick>
//more ticks here
</ScrollContainer>
);
};
on top of that, I also have counter buttons where you can increase/decrease the minute state value:
<AddButton onClick={()=> setMinutes(minutes + 1)}></AddButton>
<SubtractButton onClick={()=> setMinutes(minutes - 1)}></SubtractButton>
Now, everything seemed to be working just fine, until I tried to rapidly click the AddButton/SubtractButton. I have observed the following problems:
If I rapidly click it without using the scroll container yet, the state becomes NaN.
If I rapidly click it after scrolling to a specific position, the state value resets to the value based on the scroll position. Example: I scroll to position value 3, state value becomes 3. I click AddButton rapidly, state becomes 4 for a split second then resets to 3.
As I mentioned before, it works just fine if I click at a slow pace. Also, scrolling rapidly doesn't cause any problems.
How can I fix this?
A simple use-case is to allow a user to either click buttons to paginate in a slider, or drag. Both events call the same paginate function with a param to either go forward or back--simple stuff.
However, the trigger from drag seems to cause bizarre behavior where the slider wants to start the animation from several slides back as if it ignores the updated props. This doesn't happen when using the buttons and both use the same simple paginate call.
Any tips appreciated.
Minimal example:
export default function App() {
const [position, setPosition] = useState<number>(0);
const paginate = (direction: Direction) => {
setPosition((prev) => {
return direction === Direction.Forward
? Math.max(-800, prev - 200)
: Math.min(0, prev + 200);
});
};
return (
<div className="App">
<Slider>
<Wrapper
animate={{ x: position }}
transition={{
x: { duration: 1, type: "tween" }
}}
drag="x"
dragConstraints={{
top: 0,
left: 0,
right: 0,
bottom: 0
}}
onDragEnd={(e, { offset, velocity }) => {
const swipe = swipePower(offset.x, velocity.x);
if (swipe < -swipeConfidenceThreshold) {
paginate(Direction.Forward);
} else if (swipe > swipeConfidenceThreshold) {
paginate(Direction.Back);
}
}}
>
<Slide>1</Slide>
<Slide className="alt">2</Slide>
<Slide>3</Slide>
<Slide className="alt">4</Slide>
<Slide>5</Slide>
</Wrapper>
</Slider>
<button onClick={() => paginate(Direction.Back)}>prev</button>
<button onClick={() => paginate(Direction.Forward)}>next</button>
</div>
);
}
Codesandbox Demo
I have to say, this problem is quite interesting. However, I think I figured out a way for you to handle this. One thing I noticed is that if you comment out
onDragEnd={(e, { offset, velocity }) => {
// const swipe = swipePower(offset.x, velocity.x);
// if (swipe < -swipeConfidenceThreshold) {
// paginate(Direction.Forward);
// } else if (swipe > swipeConfidenceThreshold) {
// paginate(Direction.Back);
// }
}}
the entire onDragEnd prop function, this example still doesn't work, since by the looks of things, the draggable component is not respecting your offset.
I realized that at this point, the problem is the internal state of the component is out of sync with your state. And would you look at that, the Framer Motion API actually provides a way to inspect this.
https://www.framer.com/api/motion/motionvalue/#usemotionvalue
It's the hook useMotionValue() which allows us to see what's actually happening. Turns out, our value is being set wrong when the user starts dragging:
useEffect(
() =>
motionX.onChange((latest) => {
console.log("LATEST: ", latest);
}),
[]
);
We can see this, because the state "jumps" to 200 as soon as we start dragging.
So fixing in theory is easy, we just need to make sure to let that value "know" about our offset, and that way it's gonna start with the proper offset in mind!
Anyway, that was my thought process, here's the solution, all you need to do is set the left constraint to make it work:
dragConstraints={{
top: 0,
left: position,
right: 0,
bottom: 0
}}
And tada! This makes it work. Here's my working solution: https://codesandbox.io/s/lingering-waterfall-2tsfi?file=/src/App.tsx
I want to get the current y-offset of my scrollView by using a PanResponder. this is what i did so far, but i couldn't find a way to get the current y-offset
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
console.log(this.scrollView) //this logs my scrollView
}
});
}
<ScrollView ref={(view) => this._scrollView = view} {...this._panResponder.panHandlers}>
Assuming that by touch release you mean the moment when one touch stops existing, you can do that with ScrollVIew's onTouchEnd prop.
I would do that by using onScroll to save the offset somewhere and onTouchEnd to do something with that offset.
<ScrollView
onScroll={(event) => {
this.offsetY = event.nativeEvent.contentOffset.y;
}}
onTouchEnd={(event) => {
console.log('offsetY:', this.offsetY);
console.log('touch info:', event.nativeEvent);
}}
>
{content of the scroll view}
</ScrollView>
If by touch release you meant that there should be zero touches, you can check that with the event that is passed to onTouchEnd.
Not sure why you wanna access y offset in panResponder response. But there is a way to do it. First you need to define a local variable in your component like:
this.currentScrollViewX=0;
this.currentScrollViewY=0;
Normally, you can trace the scrollView offset when scroll event happening. You can add props in ScrollView like
<ScrollView onScroll = {e => this._onScroll(e)} ...>
...
</ScrollView>
then define _onScroll method in your component, update the currentScrollViewX and currentScrollViewY when scrolling happens
_onScroll(e){
this.currentScrollViewX = e.nativeEvent.contentOffset.x;
this.currentScrollViewY = e.nativeEvent.contentOffset.x;
}
Then in your onPanResponderRelease handler, you can always get the latest y offset by referring this.currentScrollViewY
If you wanna do drag and drop animation in ScrollView using PanResponder, it will have some issues when PanResponder drag and scrollView scroll.
More detailed is here https://github.com/facebook/react-native/issues/1046