I'm mainly a backend engineer and have been trying to implement, and failing, a simple drag and drop for a slider I am making in React.
First I will show you the behavior without using debounce:
no-debounce
And here is the behavior with debounce:
with-debounce
The debounce I took from here with a little modification.
I think I have two problems, one is fast flickering, which debounce should solve, and the other is the incorrect left which I cannot figure how to fix. For some reason, onDrag, rect.left has all the left margins of the parents (100 + 10) added to it as well. This happens on Chrome and Safari.
My question is, how do I make this drag and drop work? What am I doing wrong? My code is below:
import React, {
Dispatch,
MouseEvent,
RefObject,
SetStateAction,
useEffect,
useRef,
useState
} from "react";
const useDebounce = (callback: any, delay: number) => {
const latestCallback = useRef(callback);
const latestTimeout = useRef(1);
useEffect(() => {
latestCallback.current = callback;
}, [callback]);
return (obj: any) => {
const { event, args } = obj;
event.persist();
if (latestTimeout.current) {
clearTimeout(latestTimeout.current);
}
latestTimeout.current = window.setTimeout(
() => latestCallback.current(event, ...args),
delay
);
};
};
const setPosition = (
event: any,
setter: any
) => {
const rect = event.target.getBoundingClientRect();
const clientX: number = event.pageX;
console.log('clientX: ', clientX)
// console.log(rect.left)
setter(clientX - rect.left)
};
const Slider: React.FC = () => {
const [x, setX] = useState(null);
const handleOnDrag = useDebounce(setPosition, 100)
return (
<div style={{ position: "absolute", margin: '10px' }}>
<div
style={{ position: "absolute", left: "0", top: "0" }}
onDragOver={e => e.preventDefault()}
>
<svg width="300" height="10">
<rect
y="5"
width="300"
height="2"
rx="10"
ry="10"
style={{ fill: "rgb(96,125,139)" }}
/>
</svg>
</div>
<div
draggable={true}
onDrag={event => handleOnDrag({event, args: [setX]})}
// onDrag={event => setPosition(event, setX)}
style={{
position: "absolute",
left: (x || 0).toString() + "px",
top: "5px",
width: "10px",
height: "10px",
padding: "0px",
}}
>
<svg width="10" height="10" style={{display:"block"}}>
<circle cx="5" cy="5" r="4" />
</svg>
</div>
</div>
);
};
Thank you.
Draggable and onDrag hast its own woes. Tried my hand with simple mouse events.
You can find a working code in the following sandbox
The source for it is as follows
import React, { useRef, useState, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const isDragging = useRef(false);
const dragHeadRef = useRef();
const [position, setPosition] = useState(0);
const onMouseDown = useCallback(e => {
if (dragHeadRef.current && dragHeadRef.current.contains(e.target)) {
isDragging.current = true;
}
}, []);
const onMouseUp = useCallback(() => {
if (isDragging.current) {
isDragging.current = false;
}
}, []);
const onMouseMove = useCallback(e => {
if (isDragging.current) {
setPosition(position => position + e.movementX);
}
}, []);
useEffect(() => {
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("mousedown", onMouseDown);
document.addEventListener("mousemove", onMouseMove);
return () => {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mousemove", onMouseMove);
};
}, [onMouseMove, onMouseDown, onMouseUp]);
return (
<div
style={{
flex: "1",
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100vh"
}}
>
<div
style={{
height: "5px",
width: "500px",
background: "black",
position: "absolute"
}}
>
<div
ref={dragHeadRef}
style={{
left: `${position}px`,
transition: 'left 0.1s ease-out',
top: "-12.5px",
position: "relative",
height: "30px",
width: "30px",
background: "black",
borderRadius: "50%"
}}
/>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The crux of the logic is e.movementX which returns the amount of distance moved along x axis by the mouse since the last occurrence of that event. Use that to set the left position of the dragHeader
Related
I have created an app which will have a button in the start, and on click, it will create a parent div for existing div. Child div should be draggable inside its parent div. When I try to drag child component its parent component are also getting dragged.
Expected solution - video
My code -
import React, { useState } from 'react';
import Draggable from 'react-draggable';
function ParentDraggable(props) {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const handleMouseDown = (e) => {
let startX = e.clientX;
let startY = e.clientY;
const handleMouseMove = (e) => {
setX(x + e.clientX - startX);
setY(y + e.clientY - startY);
startX = e.clientX;
startY = e.clientY;
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', handleMouseMove);
});
};
return (
<Draggable bounds='parent' handle='.handle' onDrag={e=>e.stopPropagation()}>
<div
style={{
position: 'relative',
top: y,
left: x,
border: '1px solid black',
width: `${props.w}px`,
height: `${props.h}px`,
}}
>
<div
style={{
height: 20,
backgroundColor: 'gray',
}}
onMouseDown={handleMouseDown}
className='handle'
>
Title Bar
</div>
{props.children}
</div>
</Draggable>
);
}
function App() {
const [parents, setParents] = useState([]);
const [w, setW] = useState(200)
const [h, setH] = useState(200)
const handleAddParent = () => {
setParents([ <ParentDraggable w = {w} h={h}>
{parents}
</ParentDraggable>]);
setW(prev => prev + 100)
setH(prev => prev + 100)
};
return (
<>
<button onClick={handleAddParent}>AddParent</button>
<div style={{height: '97vh'}}>
{parents}
</div>
</>
);
}
export default App;
Please help me achieve my goal.
Your additions to Draggable doesn't seem to really do anything. Instead of DIY, what you are missing is nodeRef(). The React-draggable docs are not the easiest to read, but there's extra something there if you are interested.
Try this out, you'll want to use something like useId() to target the handle and you really need a combo if useRef() and nodeRef(). You will need to make your "parents" stack again, I hardcoded this one for simplicity.
import React, { useRef } from "react"
import Draggable from "react-draggable"
function App() {
const r1 = useRef()
const r2 = useRef()
return (
<div style={{ height: "97vh" }}>
<Draggable bounds="parent" handle=".handlea" nodeRef={r1.current}>
<div
ref={r1}
style={{
border: "1px solid black",
width: `300px`,
height: `300px`,
}}
>
<div
style={{
height: 20,
backgroundColor: "gray",
}}
className="handlea"
>
Title Bar
</div>
<Draggable bounds="parent" handle=".handleb" nodeRef={r2.current}>
<div
ref={r2}
style={{
border: "1px solid black",
width: `100px`,
height: `100px`,
}}
>
<div
style={{
height: 20,
backgroundColor: "gray",
}}
className="handleb"
>
Title Bar
</div>
</div>
</Draggable>
</div>
</Draggable>
</div>
)
}
export default App
My onScroll event is not firing in react js.
I am trying to set infinite scrolling in react js , but my onScroll event is not firing away.
it fetches posts from api and send it to the Post component. and i am rendering the post in the Post component.
Feed.js
import { Box } from "#mui/material";
import Post from "./Post";
import About from "./About";
import { useEffect, useState } from "react";
import { getInfiniteScroll } from "../apis/posts";
const Feed = () => {
const [data, setData] = useState([]);
const [skip, setSkip] = useState(0);
useEffect(() => {
fetchPosts();
document
.getElementById("scroll_div")
.addEventListener("scroll", handleScroll, true);
// Remove the event listener
return () => {
document
.getElementById("scroll_div")
.removeEventListener("scroll", handleScroll, true);
};
}, [skip]);
const fetchPosts = async () => {
try {
const newPosts = await getInfiniteScroll(skip);
setData(newPosts);
} catch (error) {
console.log(error.message);
}
};
const handleScroll = (e) => {
const { offsetHeight, scrollTop, scrollHeight } = e.target;
console.log(e.target);
if (offsetHeight + scrollTop >= scrollHeight) {
setSkip(data?.length);
}
console.log(skip);
};
return (
<Box
flex={4}
sx={{ padding: { xs: "0", sm: "0px 20px " }, overflow: "auto" }}
onScroll={handleScroll}>
<Post post={data} />
<Box
sx={{
display: { sm: "none", xs: "block" },
justifyContent: "center",
alignItems: "center",
paddingBottom: "50px",
}}>
<About />
</Box>
</Box>
);
};
export default Feed;
Please help !!
I don't know if onScroll is supported by material's Box component. What you can instead do is use refs
function App() {
const ref = useRef()
useEffect(() => {
const handleScroll = (e) => {
// Do your stuff here
};
ref.current?.addEventListener("scroll", handleScroll);
return () => ref.current?.removeEventListener("scroll", handleScroll);
}, []);
return (
<Box ref={ref} />
);
}
I have piece of code:
`function App() {
const myRef = useRef(null);
const onWheel = e => {
e.preventDefault();
const containerScrollPosition = myRef.current.scrollLeft;
inputEl.current.scrollTo({
top: 0,
left: containerScrollPosition + e.deltaY * 0.35,
behaviour: "smooth"
});
};
return (
<div
className="App"
style={{
height: 440,
width: "100%"
}}
onWheel={onWheel}
>
<AutoSizer>
{({ height, width }) => (
<List
ref={myRef}
height={height}
itemCount={30}
itemSize={600}
layout="horizontal"
width={width}
>
{Grid}
</List>
)}
</AutoSizer>
</div>
);
}
When i use myRef for List component, myRef.current.scrollLeft/myRef.current.clientHeight returns undefined(myRef.current returns correct component node). If i use myRef for div.App all goes right.
What could be the problem?
The List ref is not a dom node and does not have scrollLeft property. You could use the scrollOffset of the list which is stored in it's state.
const handleScrollLeft = () => {
let scrollPosition = listRef.state.scrollOffset - 50
listRef.scrollTo(scrollPosition)
}
As per Material Design guidelines:
Upon scrolling, the top app bar can […] transform in the following ways:
- Scrolling upward hides the top app bar
- Scrolling downward reveals the top app bar
When the top app bar scrolls, its elevation above other elements becomes apparent.
Is there any built-in approach to do this in material-ui-next or should it be considered as a new feature? Can you give a hint on how to achieve the animation of the AppBar component as described in the guidelines?
To my knowledge, there's no out-of-the-box solution for this at the moment. It's quite easy to implement though. Here is a snippet that subscribes to scroll events and hides or shows the AppBar accordingly:
const styles = {
root: {
flexGrow: 1,
},
show: {
transform: 'translateY(0)',
transition: 'transform .5s',
},
hide: {
transform: 'translateY(-110%)',
transition: 'transform .5s',
},
};
class CollapsibleAppBar extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
shouldShow: null,
};
this.lastScroll = null;
this.handleScroll = this.handleScroll.bind(this);
// Alternatively, you can throttle scroll events to avoid
// updating the state too often. Here using lodash.
// this.handleScroll = _.throttle(this.handleScroll.bind(this), 100);
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll, { passive: true });
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(evt) {
const lastScroll = window.scrollY;
if (lastScroll === this.lastScroll) {
return;
}
const shouldShow = (this.lastScroll !== null) ? (lastScroll < this.lastScroll) : null;
if (shouldShow !== this.state.shouldShow) {
this.setState((prevState, props) => ({
...prevState,
shouldShow,
}));
}
this.lastScroll = lastScroll;
}
render() {
const { classes } = this.props;
return (
<AppBar
position="fixed"
color="default"
className={
`${classes.root} ${
this.state.shouldShow === null ? '' : (
this.state.shouldShow ? classes.show : classes.hide
)
}`
}
>
<Toolbar>
<Typography variant="title" color="inherit">
Title
</Typography>
</Toolbar>
</AppBar>
);
}
}
CollapsibleAppBar.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(CollapsibleAppBar);
For those who are still looking for built-in feature, Hide appbar on scroll is available in material-ui.
in the current version of Material-ui, you can simply use the following
import clsx from "clsx";
import useScrollTrigger from "#material-ui/core/useScrollTrigger";
const trigger = useScrollTrigger();
<AppBar className={trigger ? classes.show : classes.hide}>
</AppBar>
https://material-ui.com/components/app-bar/#usescrolltrigger-options-trigger
this seem to work for me
import {
useScrollTrigger,
Fab,
Zoom,
} from '#mui/material';
...
function ElevationScroll(props) {
const { children } = props;
const theme = useTheme();
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 0,
});
return React.cloneElement(children, {
sx: trigger
? {
bgcolor: theme.palette.primary.dark,
'transition-duration': '500ms',
'transition-property':
'padding-top, padding-bottom, background-color',
'transition-timing-function': 'ease-in-out',
}
: {
pt: 2,
pb: 2,
bgcolor: theme.palette.primary.main,
},
elevation: trigger ? 5 : 0,
});
}
function ScrollTop(props) {
const { children } = props;
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 200,
});
const handleClick = (event) => {
const anchor = (event.target.ownerDocument || document).querySelector(
'#back-to-top-anchor'
);
if (anchor) {
anchor.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};
return (
<Zoom in={trigger}>
<Box
onClick={handleClick}
role="presentation"
sx={{ position: 'fixed', bottom: 16, right: 16, zIndex: 1 }}
>
{children}
</Box>
</Zoom>
);
}
...
return (
<React.Fragment>
<ElevationScroll {...props}>
<AppBar position="sticky">
...
</AppBar>
</ElevationScroll>
<Toolbar
id="back-to-top-anchor"
className="_Toolbar"
sx={{
minHeight: '0 !important',
}}
/>
<ScrollTop {...props}>
<Fab color="secondary" size="small" aria-label="scroll back to top">
<KeyboardArrowUpIcon />
</Fab>
</ScrollTop>
</React.Fragment>
this seem to work for me
import {
useScrollTrigger,
Fab,
Zoom,
} from '#mui/material';
...
function ElevationScroll(props) {
const { children } = props;
const theme = useTheme();
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 0,
});
return React.cloneElement(children, {
sx: trigger
? {
bgcolor: theme.palette.primary.dark,
'transition-duration': '500ms',
'transition-property':
'padding-top, padding-bottom, background-color',
'transition-timing-function': 'ease-in-out',
}
: {
pt: 2,
pb: 2,
bgcolor: theme.palette.primary.main,
},
elevation: trigger ? 5 : 0,
});
}
function ScrollTop(props) {
const { children } = props;
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 200,
});
const handleClick = (event) => {
const anchor = (event.target.ownerDocument || document).querySelector(
'#back-to-top-anchor'
);
if (anchor) {
anchor.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};
return (
<Zoom in={trigger}>
<Box
onClick={handleClick}
role="presentation"
sx={{ position: 'fixed', bottom: 16, right: 16, zIndex: 1 }}
>
{children}
</Box>
</Zoom>
);
}
...
return (
<React.Fragment>
<ElevationScroll {...props}>
<AppBar position="sticky">
...
</AppBar>
</ElevationScroll>
<Toolbar
id="back-to-top-anchor"
className="_Toolbar"
sx={{
minHeight: '0 !important',
}}
/>
<ScrollTop {...props}>
<Fab color="secondary" size="small" aria-label="scroll back to top">
<KeyboardArrowUpIcon />
</Fab>
</ScrollTop>
</React.Fragment>
https://mui.com/components/app-bar/#usescrolltrigger-options-trigger
Let's say I have a view that is positioned absolute at the bottom of the screen. This view contains a text input. When the text input is focused, I want the bottom of the view to touch the top of the keyboard.
I've been messing around with KeyboardAvoidingView, but the keyboard keeps going over my view. Is it not possible to make this work with position absolute?
What other method can I try? Thanks!
Few days ago I have the same problem (although I have a complex view with TextInput as a child) and wanted not only the TextInput to be focused but the whole view to be "attached" to the keyboard. What's finally is working for me is the following code:
constructor(props) {
super(props);
this.paddingInput = new Animated.Value(0);
}
componentWillMount() {
this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
}
componentWillUnmount() {
this.keyboardWillShowSub.remove();
this.keyboardWillHideSub.remove();
}
keyboardWillShow = (event) => {
Animated.timing(this.paddingInput, {
duration: event.duration,
toValue: 60,
}).start();
};
keyboardWillHide = (event) => {
Animated.timing(this.paddingInput, {
duration: event.duration,
toValue: 0,
}).start();
};
render() {
return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
[...]
<Animated.View style={{ marginBottom: this.paddingInput }}>
<TextTranslateInput />
</Animated.View>
</KeyboardAvoidingView>
);
}
where [..] you have other views.
Custom hook:
import { useRef, useEffect } from 'react';
import { Animated, Keyboard, KeyboardEvent } from 'react-native';
export const useKeyboardHeight = () => {
const keyboardHeight = useRef(new Animated.Value(0)).current;
useEffect(() => {
const keyboardWillShow = (e: KeyboardEvent) => {
Animated.timing(keyboardHeight, {
duration: e.duration,
toValue: e.endCoordinates.height,
useNativeDriver: true,
}).start();
};
const keyboardWillHide = (e: KeyboardEvent) => {
Animated.timing(keyboardHeight, {
duration: e.duration,
toValue: 0,
useNativeDriver: true,
}).start();
};
const keyboardWillShowSub = Keyboard.addListener(
'keyboardWillShow',
keyboardWillShow
);
const keyboardWillHideSub = Keyboard.addListener(
'keyboardWillHide',
keyboardWillHide
);
return () => {
keyboardWillHideSub.remove();
keyboardWillShowSub.remove();
};
}, [keyboardHeight]);
return keyboardHeight;
};
#jazzdle example works great! Thank you for that!
Just one addition - in keyboardWillShow method, one can add event.endCoordinates.height so paddingBottom is exact height as keyboard.
keyboardWillShow = (event) => {
Animated.timing(this.paddingInput, {
duration: event.duration,
toValue: event.endCoordinates.height,
}).start();
}
Using Functional Component. This works for both iOS and Android
useEffect(() => {
const keyboardVisibleListener = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
handleKeyboardVisible
);
const keyboardHiddenListener = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
handleKeyboardHidden
);
return () => {
keyboardHiddenListener.remove();
keyboardVisibleListener.remove();
};}, []);
const handleKeyboardVisible = (event) => {
Animated.timing(paddingInput, {
duration: event.duration,
toValue: 60,
useNativeDriver: false,
});};
const handleKeyboardHidden = (event: any) => {
Animated.timing(paddingInput, {
duration: event.duration,
toValue: 0,
useNativeDriver: false,
});};
React Native now supports an InputAccessoryView which can be used for exactly this purpose - even for anchored TextInputs.
Here's a specific example: https://github.com/facebook/react-native/blob/main/packages/rn-tester/js/examples/InputAccessoryView/InputAccessoryViewExample.js
You can use flexbox to bottom position the element. Here's simple example -
render() {
return (
<View style={styles.container}>
<View style={styles.top}/>
<View style={styles.bottom}>
<View style={styles.input}>
<TextInput/>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
top: {
flex: .8,
},
bottom: {
flex: .2,
},
input: {
width: 200,
},
});