A resizable `antd` Drawer? - reactjs

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

Related

React: Prevent MUI Draggable Dialog From Going Off Screen

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"]'}>```

How to update state for device width using Hooks in react

I am working on a React project, according to my scenario, a have button in my project and I have written two functions to change background color. First function will call if device width is less than or equal to 320px. Second function will call if device width is === 768px. but here the problem is when my device width is 320px when I click the button at that time the background color is changing to red here the problem comes now when I go to 768px screen then initially my button background color has to be in blue color, but it is showing red. to show button background color blue I have to update state for device size.
So someone please help me to achieve this.
This is my code
This is App.js
import React, { useState } from 'react';
import './App.css';
const App = () => {
const [backGroundColor, setBackGroundColor] = useState(null)
const [deviceSize, changeDeviceSize] = useState(window.innerWidth);
const changeBackGroundColorForMobile = () => {
if(deviceSize <= 320) {
setBackGroundColor({
backgroundColor: 'red'
})
}
}
const changeBackGroundColorForTab = () => {
if(deviceSize === 768) {
setBackGroundColor({
backgroundColor: 'green'
})
}
}
return (
<div className='container'>
<div className='row'>
<div className='col-12'>
<div className='first'>
<button onClick={() => {changeBackGroundColorForMobile(); changeBackGroundColorForTab() }} style={backGroundColor} className='btn btn-primary'>Click here</button>
</div>
</div>
</div>
</div>
)
}
export default App
If you have any questions please let me know thank you.
You're always running two functions. Don’t need that.
You’re updating the deviceSize only on the initial render. You have to update that in orientation change also.
Set the default colour always to blue.
import React, { useEffect, useState } from "react";
import "./App.css";
const App = () => {
const [backGroundColor, setBackGroundColor] = useState({
backgroundColor: "blue"
}); // Initialize bgColor with "blue"
const [deviceSize, changeDeviceSize] = useState(window.innerWidth);
useEffect(() => {
const resizeW = () => changeDeviceSize(window.innerWidth);
window.addEventListener("resize", resizeW); // Update the width on resize
return () => window.removeEventListener("resize", resizeW);
});
const changeBgColor = () => {
let bgColor = "blue";
if (deviceSize === 768) {
bgColor = "green";
} else if (deviceSize <= 320) {
bgColor = "red";
}
setBackGroundColor({
backgroundColor: bgColor
});
}; // Update the bgColor by considering the deviceSize
return (
<div className="container">
<div className="row">
<div className="col-12">
<div className="first">
<button
onClick={changeBgColor}
style={backGroundColor}
className="btn btn-primary"
>
Click here
</button>
</div>
</div>
</div>
</div>
);
};
export default App;
I would follow the previous advice to get the width and if you have lots of child components that rely on the width then I would suggest using the useContext hook so you don't have to keep passing the window data as a prop.
You can use useWindowSize() hook to get window width. And whenever width changes you can change background color by calling the functions in useEffect()
import { useState, useEffect } from "react";
// Usage
function App() {
const [backGroundColor, setBackGroundColor] = useState(null)
const { width } = useWindowSize();
useEffect(()=>{
if(width <= 320) {
changeBackGroundColorForMobile();
}
if(width === 768) {
changeBackGroundColorForTab()
}
}, [width])
const changeBackGroundColorForMobile = () => {
setBackGroundColor({
backgroundColor: 'red'
})
}
const changeBackGroundColorForTab = () => {
setBackGroundColor({
backgroundColor: 'green'
})
}
return (
<div className='container'>
<div className='row'>
<div className='col-12'>
<div className='first'>
<button style={backGroundColor} className='btn btn-primary'>Click here</button>
</div>
</div>
</div>
</div>
)
}
// Hook
function useWindowSize() {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
You can use useEffect hook to add an event listener to window resize.
export default function App() {
const [bgClassName, setBgClassName] = useState("btn-primary");
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function updateWidth() {
setWidth(window.innerWidth);
if(window.innerWidth === 768){
setBgClassName('btn-primary')
}
}
window.addEventListener("resize", updateWidth);
return () => window.removeEventListener("resize", updateWidth);
}, []);
const changeColor = () => {
if (window.innerWidth < 320) {
setBgClassName("btn-danger");
} else if (window.innerWidth === 768) {
setBgClassName("btn-success");
}
};
console.log(width);
return (
<div className="container">
<div className="row">
<div className="col-12">
<div className="first">
<button
onClick={() => changeColor()}
className={`btn ${bgClassName}`}
>
Click here
</button>
</div>
</div>
</div>
</div>
);
}

Why is my React Color popup not updating?

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;

Update UI when useRef Div Width Changes

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.

How to move the cursor to the end of the line?

I am using draft.js to develop a Rich text editor. I want the user to be able to keep typing once the Italic button is clicked. And inline styling should be applied until the user disable the italic button. Clicking on the button make the cursor to focus out of the editor. I created a ref and called the focus() function on the current ref and then called moveFocusToEnd on on edotorState. This does not work as expected. How do I achieve this behavior?
ReactJS
import React from 'react';
import { Editor, EditorState, RichUtils } from 'draft-js';
import { Button, Icon } from 'antd';
function MyEditor() {
const ref = React.useRef(null);
const [editorState, setEditorState] = React.useState(
EditorState.createEmpty()
);
const handleKeyCommand = command => {
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
setEditorState(newState)
return "handled"
}
return "not-handled";
}
const onItalicClick = event => {
ref.current.focus()
EditorState.moveFocusToEnd(editorState)
setEditorState(RichUtils.toggleInlineStyle(editorState, 'ITALIC'))
}
const onUnderLinkClick = event => {
event.preventDefault()
setEditorState(RichUtils.toggleInlineStyle(editorState, "UNDERLINE"))
}
const onBoldClick = event => {
event.preventDefault()
console.log(event)
setEditorState(RichUtils.toggleInlineStyle(editorState, "BOLD"))
}
return <div>
<div>
<Button
onClick={onItalicClick}
>
<Icon type="italic" />
</Button>
<Button
onClick={onUnderLinkClick}
>
<Icon type="underline" />
</Button>
<Button
onClick={onBoldClick}
>
<Icon type="bold" />
</Button>
</div>
<Editor
editorState={editorState}
onChange={editorState => setEditorState(editorState)}
handleKeyCommand={handleKeyCommand}
ref={ref}
/>
</div>;
}
export default MyEditor;
SCSS
.wrapper {
border: 1px solid #e2e2e2;
padding: 10px;
margin-bottom: 20px;
}
selectionState = this.state.editorState.getSelection()
selectionState=selectionState.merge({'forceKey':xxxx, focusOffset:5})
here you can set focusOffset to be the text length of that block.

Resources