I use the throttle from lodash in this way on mouseMove event to track the mouse position in the div square, but the throttle function that I wrote seems not working. where I am wrong?
import React, { useState } from "react";
import "./index.css";
import _throttle from "lodash/throttle";
function Throttle() {
const [state, setState] = useState({
clientX: 0,
clientY: 0
});
const update = ({ clientX, clientY }) => {
setState({ clientX, clientY });
};
const throtteFn = _throttle(update, 1000);
const handleMouseMove = (event) => {
event.persist();
throtteFn(event);
};
return (
<>
<div
style={{
width: "10em",
backgroundColor: "green",
height: "10em"
}}
onMouseMove={handleMouseMove}
/>
<div>
<p>clientX: {state.clientX}</p>
<p>clientY: {state.clientY}</p>
</div>
</>
);
}
export default Throttle;
You need to consider the position of the rectangle in the viewport. Keep a ref to get the left and top positions of the rectangle.
Try like this
const ref = useRef();
const update = ({ clientX, clientY }) => {
const { top, left } = ref.current.getBoundingClientRect();
// deduct the left and top offset values before setting the values
setState({
clientX: clientX - left >= 0 ? clientX - left : 0,
clientY: clientY - top >= 0 ? clientY - top : 0
});
};
...
...
<>
<div
style={{
width: "10em",
backgroundColor: "green",
height: "10em"
}}
onMouseMove={handleMouseMove}
ref={ref}
/>
...
...
</>
Code Sandbox
You must use usecallback in the throttle function.
For now, throttleFn is constantly being created so it doesn't work the way you want it to.
const throtteFn = useCallback(_throttle(update, 1000), []);
sandbox live
Related
I am working with the MUI library, specifically with the Draggable Dialog component seen here.
My question is how do I stop the dialog from being dragged off the screen?
I have tried this inside the Paper Component, but its not working:
import * as React from 'react';
import Paper, { PaperProps } from '#mui/material/Paper';
import Draggable, { DraggableEvent, DraggableData } from 'react-draggable';
export const PaperComponent = (props: PaperProps) => {
const widgetContainerRef = React.useRef<HTMLInputElement>(null);
const [widgetState, setWidgetState] = React.useState({
visible: false,
disabled: true,
bounds: { left: 0, top: 0, bottom: 0, right: 0 },
});
// const draggleRef = useRef<HTMLInputElement>(null);
const onStart = (event: DraggableEvent, uiData: DraggableData) => {
const { clientWidth, clientHeight } = window?.document?.documentElement;
const targetRect = widgetContainerRef?.current?.getBoundingClientRect();
if (targetRect) {
setWidgetState((prevState) => ({
...prevState,
bounds: {
left: -targetRect?.left + uiData?.x,
right: clientWidth - (targetRect?.right - uiData?.x),
top: -targetRect?.top + uiData?.y,
bottom: clientHeight - (targetRect?.bottom - uiData?.y),
},
}));
}
};
return (
<Draggable
handle="#draggable-dialog-title"
cancel={'[class*="MuiDialogContent-root"]'}
nodeRef={widgetContainerRef}
onStart={(event, uiData) => onStart(event, uiData)}
>
{/* <Resizable height={scaler.height} width={scaler.width} onResize={onResize}> */}
<div ref={widgetContainerRef}>
<Paper {...props} />
</div>
</Draggable>
);
};
Would anyone know how to prevent my user from dragging the dialog off the screen?
Looks like I overlooked a little thing here. It works if you add :
<Draggable
...
bounds={widgetState.bounds}
>
<div ref={widgetContainerRef}>
<Paper {...props} />
</div>
</Draggable>
Use onDrag Prop to prevent from being dragged, just return false on a specific scenario
onDrag={(e: any) => {
const from = e.relatedTarget || e.toElement;
if (!from || from.nodeName === EVENT_NODE_NAME.HTML) {
// stop your drag event here
return false;
}
}}
handle='#event-max-dialog'
cancel={'[class*="MuiDialogContent-root"]'}>```
I have a simple list of options in a menu like so:
Option 1
Option 2
Option 3
When the user clicks on an option, there should be a highlighted bar that shows which one they selected. And when the user clicks on different options, the highlighted bar should slide up and down depending on what they chose. I'm trying to use react-spring, but I can't seem to get the animation and clicking behavior to happen properly.
With my current code, the highlighted bar does not slide up and down; it just shows and hides upon user selection. And clicking on an option once does not put the highlighted bar on it, instead, I have to click twice for it to show up correctly on the selected option.
Help is appreciated! This is my first time using react-spring so I'm a bit lost on this.
Below is the code snippet for the animations and rendering the component:
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [previousIndex, setPreviousIndex] = useState<number>(0);
const onClick = (name: string, index: number) => {
setPreviousIndex(currentIndex);
setCurrentIndex(index);
setSpring(fn());
};
// Spring animation code
const fn = () => (
{
transform: `translateY(${currentIndex * 52}px)`,
from: {
transform: `translateY(${previousIndex * 52}px)`,
},
}
);
const [spring, setSpring] = useState<any>(useSpring(fn()));
// Rendering component
return (
<div>
{options.map((option, index) => (
<>
{currentIndex === index && <animated.div style={{...spring, ...{ background: 'orange', height: 52, width: '100%', position: 'absolute', left: 0, zIndex: 1}}}></animated.div>}
<div onClick={() => onClick(option.name, index)}>
<TextWithIcon icon={currentIndex === index ? option.filledIcon : option.outlineIcon} text={option.name} />
</div>
</>
))}
</div>
);
And here is the custom component, TextWithIcon:
// Interfaces
interface TextWithIconProps {
containerStyle?: Record<any, any>;
icon: ReactElement;
text: string;
textStyle?: Record<any, any>;
}
// TextWithIcon component
const TextWithIcon: React.FC<TextWithIconProps> = ({ containerStyle, icon, text, textStyle}) => {
return (
<div id='menu-items' style={{...styles.container, ...containerStyle}}>
{icon}
<Text style={{...styles.text, ...textStyle}}>{text}</Text>
</div>
)
};
You have to set the duration property of the config property inside the useSpring's parameter to a value. Try the following:
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [previousIndex, setPreviousIndex] = useState<number>(0);
const onClick = (name: string, index: number) => {
setPreviousIndex(currentIndex);
setCurrentIndex(index);
setSpring(fn());
};
// Spring animation code
const fn = () => (
{
transform: `translateY(${currentIndex * 52}px)`,
from: {
transform: `translateY(${previousIndex * 52}px)`,
},
config: {
duration: 1250 //this can be a different number
}
}
);
const [spring, setSpring] = useState<any>(useSpring(fn()));
// Rendering component
return (
<div>
{options.map((option, index) => (
<>
{currentIndex === index && <animated.div style={{...spring, ...{ background: 'orange', height: 52, width: '100%', position: 'absolute', left: 0, zIndex: 1}}}></animated.div>}
<div onClick={() => onClick(option.name, index)}>
<TextWithIcon icon={currentIndex === index ? option.filledIcon : option.outlineIcon} text={option.name} />
</div>
</>
))}
</div>
);
References:
StackOverflow. Animation duration in React Spring.https://stackoverflow.com/a/54076843/8121551. (Accessed August 23, 2021).
I am attempting to create a button which pops up a react-color SketchPicker component(akin to the first example listed in react-color's "More Examples"). However, when I open my popup and drag the selector in SketchPicker, the color in the UI does not update, and the selector does not move.
I have perused my component, and I cannot find a single aspect of it in error; I have merely updated the first component in More Examples to utilize useState.
Why is this?
CodeSandbox
You need to pass a color prop to SketchPicker component and also add an onChange handler so whenever you move the cursor this handler triggers and it has access to selected color which you can update the state variable and pass that state variable to color prop.
import React, { useState } from "react";
import "./ColorSelector.css";
import { SketchPicker } from "react-color";
const ColorSelector = () => {
const [display, setDisplay] = useState(false);
const [color, setColor] = useState({
r: "241",
g: "112",
b: "19",
a: "1"
});
const onClickMethod = () => {
setDisplay(!display);
};
const onCloseMethod = () => {
setDisplay(false);
};
const onChangeMethod = (color) => {
setColor({ ...color.rgb });
};
const popover = {
position: "absolute",
zIndex: "3"
};
const cover = {
position: "fixed",
top: "0px",
right: "0px",
bottom: "0px",
left: "0px"
};
return (
<div>
<a className="colorSelector" onClick={onClickMethod}>
select color
</a>
{display ? (
<div style={popover}>
<div style={cover} onClick={onCloseMethod} />
<SketchPicker color={color} onChange={onChangeMethod} />
</div>
) : null}
</div>
);
};
export default ColorSelector;
I have a useRef attached to a div. I need to update my UI when the div's width changes. I can access this using ref.current.innerWidth, however, when its width changes, it doesn't update other elements that depend on ref.current.innerWidth.
How can I do this?
CODE:
let ref = useRef();
return (
<>
<Box resizable ref={ref}>
This is a resizable div
</Box>
<Box width={ref.current.innerWidth}>
This box needs the same with as the resizable div
</Box>
</>
);
You could use a ResizeObserver. Implemented like so, it will set the width everytime the size of the ref changes:
let ref = useRef()
const [width, setwidth] = useState(0)
useEffect(() => {
const observer = new ResizeObserver(entries => {
setwidth(entries[0].contentRect.width)
})
observer.observe(ref.current)
return () => ref.current && observer.unobserve(ref.current)
}, [])
return (
<>
<Box ref={ref}>
This is a resizable div
</Box>
<Box width={width}>
This box needs the same with as the resizable div
</Box>
</>
)
You should make a lifecycle using useEffect and useState and event listener on window to listen the data change then re-render your component based on that.
CodeSandBox
const [size, setSize] = useState(null);
let ref = useRef();
const updateDimensions = () => {
console.log(ref.current.clientWidth);
if (ref.current) setSize(ref.current.clientWidth);
};
useEffect(() => {
window.addEventListener("resize", updateDimensions);
setSize(ref.current.clientWidth);
return () => {
console.log("dismount");
window.removeEventListener("resize", updateDimensions);
};
}, []);
return (
<>
<div ref={ref}>This is a resizable div</div>
<div
style={{
width: size,
border: "1px solid"
}}
>
This div needs the same with as the resizable div
</div>
</>
);
For anyone looking for a reusable logic and a Typescript support, I created the below custom hook based on #fredy's awesome answer, and also fixed some issues I've found in his answer:
import { useState, useRef, useEffect } from "react";
export const useObserveElementWidth = <T extends HTMLElement>() => {
const [width, setWidth] = useState(0);
const ref = useRef<T>(null);
useEffect(() => {
const observer = new ResizeObserver((entries) => {
setWidth(entries[0].contentRect.width);
});
if (ref.current) {
observer.observe(ref.current);
}
return () => {
ref.current && observer.unobserve(ref.current);
};
}, []);
return {
width,
ref
};
};
Then, import useObserveElementWidth, and use it like this:
const YourComponent = () => {
const { width, ref } = useObserveElementWidth<HTMLDivElement>();
return (
<>
<Box resizable ref={ref}>
This is a resizable div
</Box>
<Box width={width}>
This box needs the same with as the resizable div
</Box>
</>
);
};
I've created an example codesandbox for it.
I would like to provide a way to make an antd Drawer resizable ?
I read a popular answer specifically for material-ui/Drawer but I am looking to do something very similar with antd.
Does anyone have a similar antd example - or have a better idea how to handle info getting chopped off at side of the drawer.
You can extend the width of Drawer by specifying it on the width props. If you don't want to extend it but you want the content to be still fit, you can set the width on bodyStyle prop and use overflow: "auto":
<Drawer
title="Basic Drawer"
placement="right"
closable={false}
visible={isDrawerVisible}
bodyStyle={{
width: 400,
overflow: "auto"
}}
onClose={toggleDrawerVisible}
>
I also made a resizable drawer based on the link that you provide in antd version (react hooks version answer).
ResizableDrawer.jsx
import React, { useState, useEffect } from "react";
import { Drawer } from "antd";
let isResizing = null;
const ResizableDrawer = ({ children, ...props }) => {
const [drawerWidth, setDrawerWidth] = useState(undefined);
const cbHandleMouseMove = React.useCallback(handleMousemove, []);
const cbHandleMouseUp = React.useCallback(handleMouseup, []);
useEffect(() => {
setDrawerWidth(props.width);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.visible]);
function handleMouseup(e) {
if (!isResizing) {
return;
}
isResizing = false;
document.removeEventListener("mousemove", cbHandleMouseMove);
document.removeEventListener("mouseup", cbHandleMouseUp);
}
function handleMousedown(e) {
e.stopPropagation();
e.preventDefault();
// we will only add listeners when needed, and remove them afterward
document.addEventListener("mousemove", cbHandleMouseMove);
document.addEventListener("mouseup", cbHandleMouseUp);
isResizing = true;
}
function handleMousemove(e) {
let offsetRight =
document.body.offsetWidth - (e.clientX - document.body.offsetLeft);
let minWidth = 256;
let maxWidth = 600;
if (offsetRight > minWidth && offsetRight < maxWidth) {
setDrawerWidth(offsetRight);
}
}
return (
<Drawer {...props} width={drawerWidth}>
<div className="sidebar-dragger" onMouseDown={handleMousedown} />
{children}
</Drawer>
);
};
export default ResizableDrawer;
and to use it:
import ResizableDrawer from "./ResizableDrawer";
<ResizableDrawer
title="Resizable Drawer"
placement="right"
closable={false}
visible={isResizableDrawerVisible}
onClose={toggleResizableDrawerVisible}
>
...
</ResizableDrawer>
See working demo here:
Have two states for tracking the width of the drawer and whether or not the drawer is being resized (isResizing).
Add two event listeners on the global document where it will listen for mousemove and mouseup. The mousemove event will resize the drawer, only if isResizing is true. And the mouseup event will set isResizing to false.
Add a div in your drawer that acts as the draggable border for making the drawer resizable. This div will listen for a mousedown event, which will set the state of isResizing to true.
Here's the code that has been improved upon from the basic drawer demo from antd's website.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Drawer, Button } from "antd";
const App = () => {
const [visible, setVisible] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [width, setWidth] = useState(256);
const showDrawer = () => {
setVisible(true);
};
const onClose = () => {
setVisible(false);
};
const onMouseDown = e => {
setIsResizing(true);
};
const onMouseUp = e => {
setIsResizing(false);
};
const onMouseMove = e => {
if (isResizing) {
let offsetRight =
document.body.offsetWidth - (e.clientX - document.body.offsetLeft);
const minWidth = 50;
const maxWidth = 600;
if (offsetRight > minWidth && offsetRight < maxWidth) {
setWidth(offsetRight);
}
}
};
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
});
return (
<>
<Button type="primary" onClick={showDrawer}>
Open
</Button>
<Drawer
title="Basic Drawer"
placement="right"
closable={false}
onClose={onClose}
visible={visible}
width={width}
>
<div
style={{
position: "absolute",
width: "5px",
padding: "4px 0 0",
top: 0,
left: 0,
bottom: 0,
zIndex: 100,
cursor: "ew-resize",
backgroundColor: "#f4f7f9"
}}
onMouseDown={onMouseDown}
/>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Drawer>
</>
);
};
ReactDOM.render(<App />, document.getElementById("container"));
And here's the demo of the code:
DEMO