Custom, Draggable component too slow in ReactJS when Using onMouseMove() - javascript

I am trying to create a draggable preact component. My current implementation breaks if the cursor moves to fast. Here is the code.
export { Draggable };
import { h } from "preact";
import { useState } from "preact/hooks";
const Draggable = (props: any) => {
const [styles, setStyles] = useState({});
const [diffPos, setDiffPos] = useState({ diffX: 0, diffY: 0 });
const [isDragging, setIsDragging] = useState(false);
const dragStart = (e: MouseEvent): void => {
const boundingRect =
(e.currentTarget as HTMLElement).getBoundingClientRect();
setDiffPos({
diffX: e.screenX - boundingRect.left,
diffY: e.screenY - boundingRect.top,
});
setIsDragging(true);
}
const dragging = (e: MouseEvent): void => {
if (isDragging === true) {
const left = e.screenX - diffPos.diffX;
const top = e.screenY - diffPos.diffY;
setStyles({ left: left, top: top });
}
}
const dragEnd = (): void => {
setIsDragging(false);
}
return (
<div
class="draggable"
style={{ ...styles, position: "absolute" }}
onMouseDown={dragStart}
onMouseMove={dragging}
onMouseUp={dragEnd}
>
{props.children}
</div>
);
}
I tried to fix it by creating a mouseup event listener but the element stops dragging if I move the mouse to fast.
Here is my attempted fix:
export { Draggable };
import { h } from "preact";
import { useState } from "preact/hooks";
const Draggable = (props: any) => {
const [styles, setStyles] = useState({});
const [diffPos, setDiffPos] = useState({ diffX: 0, diffY: 0 });
const [isDragging, setIsDragging] = useState(false);
const dragStart = (e: MouseEvent): void => {
const boundingRect =
(e.currentTarget as HTMLElement).getBoundingClientRect();
setDiffPos({
diffX: e.screenX - boundingRect.left,
diffY: e.screenY - boundingRect.top,
});
setIsDragging(true);
// ------------------------------------------------------------ Added an event listener
document.addEventListener("mouseup", dragEnd, { once: true });
}
const dragging = (e: MouseEvent): void => {
if (isDragging === true) {
const left = e.screenX - diffPos.diffX;
const top = e.screenY - diffPos.diffY;
setStyles({ left: left, top: top });
}
}
const dragEnd = (): void => {
setIsDragging(false);
}
return (
<div
class="draggable"
style={{ ...styles, position: "absolute" }}
onMouseDown={dragStart}
onMouseMove={dragging}
// -------------------------------------------------------- Removed onMouseUp
>
{props.children}
</div>
);
}

The problem is that onMouseMove() triggers every time you move the mouse, so, if you move over 200 pixels very slowly, that's 200 iterations. Try instead using onDragStart and onDragEnd. Full working demo.
Ultimately, your change will be here in render()...
return (
<div
class="draggable"
style={{ ...styles, position: "absolute" }}
onDragStart={(e) => dragStart(e)}
onDragEnd={(e) => dragging(e)}
draggable={true}
>
{props.children}
</div>
);
I use dragEnd(), so only two events actually fire with dragging: start and end. MouseMove was being fired every time there was movement, which could be hundreds of times during dragging.
Also, by giving it that extra draggable={true} param, the browser will treat it as a naturally-draggable item (i.e., a semi-opaque version of the dragged item will visually appear at the position of the cursor, as the user drags the element around).
Finally, to speed up things just a smidge, I removed the eventListener you had for dragEnd in dragStart().

Related

Sticky header with hide and show on scroll

I have a navbar with two part (navbarTop and navbarBottom); navbarTop will shown when the scrollY is bigger than 110 and the navbarbottom will shown when the user scroll up, but my problem is that each component will be re-called after each scroll, even scrolling down, or when the navbottom already exist and the user scroll up and it's still keep calling the component.
here is the code:
const Navbar: FC = () => {
const [stickyHeader, setStickyHeader] = useState(false);
const [stickyMenu, setStickyMenu] = useState(false);
console.log('navbar')
const [y, setY] = useState(null);
const handleNavigation = useCallback(
(e) => {
const window = e.currentTarget;
if (!stickyMenu) {
if (y > window.scrollY) {
setStickyMenu(true)
// console.log("scrolling up");
}
}
if (!stickyHeader) {
if (window.scrollY >= 110) {
setStickyHeader(true);
}
}
if (stickyHeader) {
if (window.scrollY <= 110) {
setStickyHeader(false);
}
}
if (stickyMenu) {
if (y < window.scrollY) {
setStickyMenu(false)
// console.log("scrolling down");
}
}
setY(window.scrollY);
},
[y, stickyMenu,stickyHeader]
);
useEffect(() => {
window.addEventListener("scroll", handleNavigation);
return () => {
window.removeEventListener("scroll", handleNavigation);
};
}, [handleNavigation, stickyMenu]);
return (
<>
<div className={s.root}>
{stickyHeader === false ? (
<div className='hidden xl:block md:w-auto'>
<NavbarTop sticky={false}/>
<NavbarBottom
sticky={false}/>
</div>
) : (
<div className='absolute hidden xl:block md:w-auto'>
<NavbarTop sticky={true}/>
{stickyMenu &&
<NavbarBottom sticky={true}/>
}
</div>
)}
<SmallNav/>
</div>
</>
)
}
export default Navbar
In fact, I did not quite understand your question. You said that each component is re-called after each scroll. Of course it will be re-called, right?
Whenever you scroll, you change the stickyHeader or stickyMenu. React listens for the state change and re-renders the component. This is how React works.
I see that you're using tailwindcss.
You can use a useMemo to listen to the state and then return the appropriate class based on the different state states. This way you avoid having to create DOM nodes over and over again. I don't know if this would help you or not. I hope this will help you.
const Navbar: FC = () => {
const [stickyHeader, setStickyHeader] = useState(false);
const [stickyMenu, setStickyMenu] = useState(false);
// console.log('navbar');
const [y, setY] = useState(null);
const handleNavigation = useCallback(
(e) => {
const window = e.currentTarget;
if (!stickyMenu) {
if (y > window.scrollY) {
setStickyMenu(true);
// console.log("scrolling up");
}
}
if (!stickyHeader) {
if (window.scrollY >= 110) {
setStickyHeader(true);
}
}
if (stickyHeader) {
if (window.scrollY <= 110) {
setStickyHeader(false);
}
}
if (stickyMenu) {
if (y < window.scrollY) {
setStickyMenu(false);
// console.log("scrolling down");
}
}
setY(window.scrollY);
},
[y, stickyMenu, stickyHeader],
);
useEffect(() => {
window.addEventListener('scroll', handleNavigation);
return () => {
window.removeEventListener('scroll', handleNavigation);
};
}, [handleNavigation, stickyMenu]);
const stickyClassName = useMemo(() => {
if (stickyHeader) {
return 'sticky top-0 xl:block md:w-auto';
}
return 'xl:block md:w-auto';
}, [stickyHeader]);
return (
<div className="test" style={{ height: 2000 }}>
<div className={stickyClassName}>
<NavbarTop sticky={stickyHeader} />
{stickyMenu && <NavbarBottom sticky={stickyHeader} />}
</div>
<div>SmallNav</div>
</div>
);
};
export default Navbar;

handling fast move of mouse on mousemove while resizinng an element

I want to have a resizeable modal only on height so I did write some code but while trying to grow it to the bottom because it's going fast and out of the element it doesn't have any impact, also I have seen codes like this but they work properly like this I don't know what I'm missing.
also, I want to ask; is it the right way of doing resizeable components in react? I did try to write it with states but I faced some problems like it was growing unexpectedly.
import React, { FC, useCallback, useMemo, useRef } from "react";
import { PrimitivesT } from "../Table/Table";
interface ModalProps {
children: JSX.Element | PrimitivesT;
display: boolean;
width: string;
height: string;
x?: number;
y?: number;
boxShadow?: boolean;
}
const Modal: FC<ModalProps> = ({
children,
display = false,
// initial height
height = "0",
width = "0",
x,
y,
boxShadow = true,
}) => {
const ref = useRef<HTMLDivElement>(null);
const styles = useMemo<React.CSSProperties>(
() => ({
display: display ? "block" : "none",
height: height,
width,
minHeight: "15px",
position: "absolute",
left: x,
top: y,
boxShadow: boxShadow ? "1px 1px 10px 5px var(--gray)" : undefined,
borderRadius: "5px",
backgroundColor: "white",
zIndex: 900,
}),
[display, height, width, x, y, boxShadow]
);
const bottomStyle = useMemo<React.CSSProperties>(
() => ({
cursor: "row-resize",
width: "100%",
position: "absolute",
bottom: "0",
left: "0",
height: "5px",
}),
[]
);
const onMouseDown =
useCallback((): React.MouseEventHandler<HTMLDivElement> => {
let y = 0;
let h = 60;
const onMouseMove = (e: MouseEvent) => {
const YDir = e.clientY - y;
if (ref.current) ref.current.style.height = `${h + YDir}px`;
};
const onMouseUp = () => {
try {
ref.current?.removeEventListener("mousemove", onMouseMove);
ref.current?.removeEventListener("mouseup", onMouseUp);
} catch (err) {
console.error(err);
}
};
return e => {
e.stopPropagation();
const bounding = ref.current?.getBoundingClientRect();
if (bounding?.height) h = bounding?.height;
y = e.clientY;
ref.current?.addEventListener("mousemove", onMouseMove);
ref.current?.addEventListener("mouseup", onMouseUp);
};
}, []);
return (
<div
ref={ref}
style={styles}
data-testid="Modal"
onMouseDown={e => e.stopPropagation()}>
{children}
<div style={bottomStyle} onMouseDown={onMouseDown()}></div>
</div>
);
};
export default Modal;
I think it didn't work that way because it's modal and it has to be fixed or absolute so I change the element that I was attaching event listeners instead of the resizeable target I used document object.
const onMouseDown =
useCallback((): React.MouseEventHandler<HTMLDivElement> => {
let y = 0;
let h = 60;
const onMouseMove = (e: MouseEvent) => {
const YDir = e.clientY - y;
if (ref.current) ref.current.style.height = `${h + YDir}px`;
};
const onMouseUp = () => {
try {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
} catch (err) {
console.error(err);
}
};
return e => {
e.stopPropagation();
const bounding = ref.current?.getBoundingClientRect();
if (bounding?.height) h = bounding?.height;
y = e.clientY;
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
}, []);

selected element move up and down, but scroll behavior is not working with

I make a list of div tags for listing filename.
and After selecting a div, then I can change focus up and down using Arrow keys
Because I have a long file list, I add overflow: scroll to the container
but Scroll does not move along with my focus(so active div disappear from the viewport),
How can I make scroll behavior move down along with active div?
I create an example in codesandbox
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [selectedItem, setSelectedItem] = useState(0);
useEffect(() => {
const keyPress = (e) => {
if (e.key === "ArrowLeft") {
setSelectedItem((prev) => Number(prev) - 1);
}
if (e.key === "ArrowRight") {
setSelectedItem((prev) => Number(prev) + 1);
}
};
window.addEventListener("keydown", keyPress);
return () => {
window.removeEventListener("keydown", keyPress);
};
}, [selectedItem]);
const onClickDiv = (e) => {
setSelectedItem(e.target.id);
};
const renderList = () => {
let items = [];
console.log(selectedItem);
for (let i = 0; i < 60; i++) {
items.push(
<div
key={i}
className={`item ${Number(selectedItem) === i ? "active" : ""}`}
id={i}
onClick={onClickDiv}
>
Item{i}.png
</div>
);
}
return items;
};
return (
<div className="App">
<div className="list-container">{renderList()}</div>
</div>
);
}
.list-container {
height: 300px;
overflow: scroll;
}
.active {
background-color: orangered;
}
------------------ EDIT -----------------------
I finally complete this example, I sincerely thank you guys for answering my question.
Here is code sandbox final code
Here's my take on it.
I am using refs as well along with scrollIntoView.This way we don't have to scroll by a fixed amount and also we only are scrolling when we are at the end of the viewport.
Here's the demo
I am storing refs of each element.
ref={(ref) => {
elementRefs.current = { ...elementRefs.current, [i]: ref };
}}
And then we will use scrollIntoView when focus changes.
const prevItem = elementRefs.current[selectedItem - 1];
prevItem && prevItem.scrollIntoView({ block: "end" });
Notice the {block:"end"} argument here. It makes sure we only scroll if the element is not in the viewport.
You can learn more about scrollIntoView here.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import { useEffect, useState, useRef } from "react";
export default function App() {
const [selectedItem, setSelectedItem] = useState(0);
const elementRefs = useRef({});
useEffect(() => {
const keyPress = (e) => {
if (e.key === "ArrowLeft") {
setSelectedItem((prev) => Number(prev) - 1);
const prevItem = elementRefs.current[selectedItem - 1];
prevItem && prevItem.scrollIntoView({ block: "end" });
}
if (e.key === "ArrowRight") {
console.log(elementRefs.current[selectedItem]);
// if (selectedItem < elementRefs.current.length)
const nextItem = elementRefs.current[selectedItem + 1];
nextItem && nextItem.scrollIntoView({ block: "end" });
setSelectedItem((prev) => Number(prev) + 1);
}
};
window.addEventListener("keydown", keyPress);
return () => {
window.removeEventListener("keydown", keyPress);
};
}, [selectedItem]);
const onClickDiv = (e) => {
setSelectedItem(e.target.id);
};
const renderList = () => {
let items = [];
console.log(selectedItem);
for (let i = 0; i < 60; i++) {
items.push(
<div
key={i}
className={`item ${Number(selectedItem) === i ? "active" : ""}`}
id={i}
onClick={onClickDiv}
ref={(ref) => {
elementRefs.current = { ...elementRefs.current, [i]: ref };
}}
>
Item{i}.png
</div>
);
}
return items;
};
return (
<div className="App">
<div className="list-container">{renderList()}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("container"));
One way to approach this problem is to use ref in React.
First define a ref using
const scrollRef = useRef(null);
Then, assign it to your scrolling element like this,
<div className="list-container" ref={scrollRef}>
What this does is, gives you a reference to the html element inside your code.
ref.current is the HTML Div element now.
Now, you can use scrollBy method on HTML element to scroll up or down.
Like this,
useEffect(() => {
const keyPress = (e) => {
if (e.key === "ArrowLeft") {
setSelectedItem((prev) => Number(prev) - 1);
scrollRef.current.scrollBy(0, -18); // <-- Scrolls the div 18px to the top
}
if (e.key === "ArrowRight") {
setSelectedItem((prev) => Number(prev) + 1);
scrollRef.current.scrollBy(0, 18); // <-- Scrolls the div 18px to the bottom
}
};
window.addEventListener("keydown", keyPress);
return () => {
window.removeEventListener("keydown", keyPress);
};
}, [selectedItem]);
I have given 18 because I know the height of my list item.
I have updated your Sandbox. Check it out.

How to Drag & Drop Multiple Elements In React?

This is my first question on StackOverflow. I want to build a little game with React, where users can drag and drop tetrominos onto a grid and also reposition or rotating them to their liking. The tetrominos are represented by a matrix and then every block gets rendered in a li element.
Example for the z-tetromino:
[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0]
Unfortunately I cannot post images yet, that would make things easier.
The grid is too respresented by a matrix.
Now what I want to do is basically drag and drop these block matrices onto the grid, so that the values in the grid change accordingly (0 → 1 etc.).
The problem is, I have no clue how to drag multiple li elements at once with the standard HTML5 DnD API or with React DnD. When the user clicks on one li element of a certain tetromino, the whole piece should move. Maybe I could solve this using jQuery UI, but since in React no direct DOM manipulation is allowed, I'm left wondering how to do it.
I tried to drag one block onto the grid which worked semi optimally, because one block took the place of an entire row of grid blocks, even with display: inline-block set in CSS.
Here is some simple code from the first experiment.
onDragStart = e => {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text', e.target.id);
// e.dataTransfer.setDragImage(e.target.parentNode, 20, 20);
};
handleDrop = e => {
const pieceOrder = e.dataTransfer.getData('text');
// console.log(document.getElementById(pieceOrder));
// e.target.appendChild(document.getElementById(pieceOrder));
// console.log(pieceOrder);
e.target.replaceWith(document.getElementById(pieceOrder));
e.target.remove();
};
renderEmptyBoardCell(i) {
return (
<li key={i} className="emptyBoardCell" onDragOver={(e) => e.preventDefault()} onDrop={(e) => this.handleDrop(e)}>></li>
);
}
renderTemplateBoardCell(i) {
return (
<li key={i} className="templateBoardCell" onDragOver={(e) => e.preventDefault()} onDrop={(e) => this.handleDrop(e)}>></li>
);
}
renderEmptyCell(i) {
return (
<li key={i} className="emptyCell"></li>
);
}
renderFilledCell(piece_config, i) {
return (
<li key={i} id={i} className={`filledCell ${piece_config}`} draggable onDragStart={this.onDragStart}></li>
);
}
So the question is, would that be theoretically possible with React DnD or any other library? If yes, what would be the approximate solution to DnD multiple elements at once.
Thanks for your time!
In case anyone is looking for a solution in 2020. Here is my current solution with react-dnd and react hooks. You can try the live demo here.
Here is another simpler example, you can check out the codesandbox here.
You can only drag one item at a time using react-dnd. Either use a different library, or somehow group together the different pieces into one item first, and then drag and drop that one item.
I know its a bit late but have you looked into: panResponder. I am looking into multiple d'n'd elements and panResponder is the most likely fit
A nice choice for your need could be react-beautiful-dnd.
Try this out It will surely work in your case!
react-beautiful-dnd multi drag pattern
https://github.com/atlassian/react-beautiful-dnd/tree/master/stories/src/multi-drag
demo: https://react-beautiful-dnd.netlify.app/?path=/story/multi-drag--pattern
import React, { Component } from 'react';
import styled from '#emotion/styled';
import { DragDropContext } from 'react-beautiful-dnd';
import initial from './data';
import Column from './column';
import type { Result as ReorderResult } from './utils';
import { mutliDragAwareReorder, multiSelectTo as multiSelect } from './utils';
import type { DragStart, DropResult, DraggableLocation } from 'react-beautiful-dnd';
import type { Task, Id } from '../types';
import type { Entities } from './types';
const Container = styled.div`
display: flex;
user-select: none;
justify-content: center;
`;
type State = {
entities: Entities,
selectedTaskIds: Id[],
columnFlag: false,
// sad times
draggingTaskId: Id,
};
const getTasks = (entities: Entities, columnId: Id): Task[] =>
entities.columns[columnId].taskIds.map(
(taskId: Id): Task => entities.tasks[taskId],
);
export default class TaskApp extends Component<any, State> {
state: State = {
entities: initial,
selectedTaskIds: [],
draggingTaskId: '',
};
componentDidMount() {
window.addEventListener('click', this.onWindowClick);
window.addEventListener('keydown', this.onWindowKeyDown);
window.addEventListener('touchend', this.onWindowTouchEnd);
}
componentWillUnmount() {
window.removeEventListener('click', this.onWindowClick);
window.removeEventListener('keydown', this.onWindowKeyDown);
window.removeEventListener('touchend', this.onWindowTouchEnd);
}
onDragStart = (start: DragStart) => {
const id: string = start.draggableId;
const selected: Id = this.state.selectedTaskIds.find(
(taskId: Id): boolean => taskId === id,
);
// if dragging an item that is not selected - unselect all items
if (!selected) {
this.unselectAll();
}
this.setState({
draggingTaskId: start.draggableId,
});
};
onDragEnd = (result: DropResult) => {
const destination = result.destination;
const source = result.source;
const draggableId = result.draggableId;
const combine = result.combine;
console.log('combine',combine);
console.log('destination',destination);
console.log('source',source);
console.log('draggableId',draggableId);
// nothing to do
if (!destination || result.reason === 'CANCEL') {
this.setState({
draggingTaskId: '',
});
return;
}
const processed: ReorderResult = mutliDragAwareReorder({
entities: this.state.entities,
selectedTaskIds: this.state.selectedTaskIds,
source,
destination,
});
this.setState({
...processed,
draggingTaskId: null,
});
};
onWindowKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented) {
return;
}
if (event.key === 'Escape') {
this.unselectAll();
}
};
onWindowClick = (event: KeyboardEvent) => {
if (event.defaultPrevented) {
return;
}
this.unselectAll();
};
onWindowTouchEnd = (event: TouchEvent) => {
if (event.defaultPrevented) {
return;
}
this.unselectAll();
};
toggleSelection = (taskId: Id) => {
const selectedTaskIds: Id[] = this.state.selectedTaskIds;
const wasSelected: boolean = selectedTaskIds.includes(taskId);
console.log('hwwo',this.state.entities.columns);
console.log('hwwo',this.state.entities.columns.done.taskIds);
// if there is change in entities - update the state
const newTaskIds: Id[] = (() => {
// Task was not previously selected
// now will be the only selected item
if (!wasSelected) {
return [taskId];
}
// Task was part of a selected group
// will now become the only selected item
if (selectedTaskIds.length > 1) {
return [taskId];
}
// task was previously selected but not in a group
// we will now clear the selection
return [];
})();
this.setState({
selectedTaskIds: newTaskIds,
});
};
toggleSelectionInGroup = (taskId: Id) => {
const selectedTaskIds: Id[] = this.state.selectedTaskIds;
const index: number = selectedTaskIds.indexOf(taskId);
// if not selected - add it to the selected items
if (index === -1) {
this.setState({
selectedTaskIds: [...selectedTaskIds, taskId],
});
return;
}
// it was previously selected and now needs to be removed from the group
const shallow: Id[] = [...selectedTaskIds];
shallow.splice(index, 1);
this.setState({
selectedTaskIds: shallow,
});
};
// This behaviour matches the MacOSX finder selection
multiSelectTo = (newTaskId: Id) => {
const updated: string[] | null | undefined = multiSelect(
this.state.entities,
this.state.selectedTaskIds,
newTaskId,
);
if (updated == null) {
return;
}
this.setState({
selectedTaskIds: updated,
});
};
unselect = () => {
this.unselectAll();
};
unselectAll = () => {
this.setState({
selectedTaskIds: [],
});
};
render() {
const entities = this.state.entities;
const selected = this.state.selectedTaskIds;
console.log('entities', entities);
console.log('selected', selected);
return (
<DragDropContext
onDragStart={this.onDragStart}
onDragEnd={this.onDragEnd}
>
<Container>
{entities.columnOrder.map((columnId: Id) => (
<Column
column={entities.columns[columnId]}
tasks={getTasks(entities, columnId)}
selectedTaskIds={selected}
key={columnId}
draggingTaskId={this.state.draggingTaskId}
toggleSelection={this.toggleSelection}
toggleSelectionInGroup={this.toggleSelectionInGroup}
multiSelectTo={this.multiSelectTo}
entities={entities}
/>
))}
</Container>
</DragDropContext>
);
}
}

javascript/react dynamic height textarea (stop at a max)

What I'm trying to achieve is a textarea that starts out as a single line but will grow up to 4 lines and at that point start to scroll if the user continues to type. I have a partial solution kinda working, it grows and then stops when it hits the max, but if you delete text it doesn't shrink like I want it to.
This is what I have so far.
export class foo extends React.Component {
constructor(props) {
super(props);
this.state = {
textareaHeight: 38
};
}
handleKeyUp(evt) {
// Max: 75px Min: 38px
let newHeight = Math.max(Math.min(evt.target.scrollHeight + 2, 75), 38);
if (newHeight !== this.state.textareaHeight) {
this.setState({
textareaHeight: newHeight
});
}
}
render() {
let textareaStyle = { height: this.state.textareaHeight };
return (
<div>
<textarea onKeyUp={this.handleKeyUp.bind(this)} style={textareaStyle}/>
</div>
);
}
}
Obviously the problem is scrollHeight doesn't shrink back down when height is set to something larger. Any suggestion for how I might be able to fix this so it will also shrink back down if text is deleted?
ANOTHER SIMPLE APPROACH (without an additional package)
export class foo extends React.Component {
handleKeyDown(e) {
e.target.style.height = 'inherit';
e.target.style.height = `${e.target.scrollHeight}px`;
// In case you have a limitation
// e.target.style.height = `${Math.min(e.target.scrollHeight, limit)}px`;
}
render() {
return <textarea onKeyDown={this.handleKeyDown} />;
}
}
The problem when you delete the text and textarea doesn't shrink back is because you forget to set this line
e.target.style.height = 'inherit';
Consider using onKeyDown because it works for all keys while others may not (w3schools)
In case you have padding or border of top or bottom. (reference)
handleKeyDown(e) {
// Reset field height
e.target.style.height = 'inherit';
// Get the computed styles for the element
const computed = window.getComputedStyle(e.target);
// Calculate the height
const height = parseInt(computed.getPropertyValue('border-top-width'), 10)
+ parseInt(computed.getPropertyValue('padding-top'), 10)
+ e.target.scrollHeight
+ parseInt(computed.getPropertyValue('padding-bottom'), 10)
+ parseInt(computed.getPropertyValue('border-bottom-width'), 10);
e.target.style.height = `${height}px`;
}
I hope this may help.
you can use autosize for that
LIVE DEMO
import React, { Component } from 'react';
import autosize from 'autosize';
class App extends Component {
componentDidMount(){
this.textarea.focus();
autosize(this.textarea);
}
render(){
const style = {
maxHeight:'75px',
minHeight:'38px',
resize:'none',
padding:'9px',
boxSizing:'border-box',
fontSize:'15px'};
return (
<div>Textarea autosize <br/><br/>
<textarea
style={style}
ref={c=>this.textarea=c}
placeholder="type some text"
rows={1} defaultValue=""/>
</div>
);
}
}
or if you prefer react modules https://github.com/andreypopp/react-textarea-autosize
Just use useEffect hook which will pick up the height during the renderer:
import React, { useEffect, useRef, useState} from "react";
const defaultStyle = {
display: "block",
overflow: "hidden",
resize: "none",
width: "100%",
backgroundColor: "mediumSpringGreen"
};
const AutoHeightTextarea = ({ style = defaultStyle, ...etc }) => {
const textareaRef = useRef(null);
const [currentValue, setCurrentValue ] = useState("");// you can manage data with it
useEffect(() => {
textareaRef.current.style.height = "0px";
const scrollHeight = textareaRef.current.scrollHeight;
textareaRef.current.style.height = scrollHeight + "px";
}, [currentValue]);
return (
<textarea
ref={textareaRef}
style={style}
{...etc}
value={currentValue}
onChange={e=>{
setCurrentValue(e.target.value);
//to do something with value, maybe callback?
}}
/>
);
};
export default AutoHeightTextarea;
Really simple if you use hooks "useRef()".
css:
.text-area {
resize: none;
overflow: hidden;
min-height: 30px;
}
react componet:
export default () => {
const textRef = useRef<any>();
const onChangeHandler = function(e: SyntheticEvent) {
const target = e.target as HTMLTextAreaElement;
textRef.current.style.height = "30px";
textRef.current.style.height = `${target.scrollHeight}px`;
};
return (
<div>
<textarea
ref={textRef}
onChange={onChangeHandler}
className="text-area"
/>
</div>
);
};
you can even do it with react refs. as setting ref to element
<textarea ref={this.textAreaRef}></textarea> // after react 16.3
<textarea ref={textAreaRef=>this.textAreaRef = textAreaRef}></textarea> // before react 16.3
and update the height on componentDidMount or componentDidUpdate as your need. with,
if (this.textAreaRef) this.textAreaRef.style.height = this.textAreaRef.scrollHeight + "px";
actually you can get out of this with useState and useEffect
function CustomTextarea({minRows}) {
const [rows, setRows] = React.useState(minRows);
const [value, setValue] = React.useState("");
React.useEffect(() => {
const rowlen = value.split("\n");
if (rowlen.length > minRows) {
setRows(rowlen.length);
}
}, [value]);
return (
<textarea rows={rows} onChange={(text) => setValue(text.target.value)} />
);
}
Uses
<CustomTextarea minRows={10} />
I like using this.yourRef.current.offsetHeight. Since this is a textarea, it wont respond to height:min-content like a <div style={{height:"min-content"}}>{this.state.message}</div> would. Therefore I don't use
uponResize = () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(
this.getHeightOfText.current &&
this.setState({
heightOfText: this.getHeightOfText.current.offsetHeight
}),
20
);
};
componentDidMount = () => {
window.addEventListener('resize', this.uponResize, /*true*/)
}
componentWillUnmount = () => {
window.removeEventListener('resize', this.uponResize)
}
but instead use
componentDidUpdate = () => {
if(this.state.lastMessage!==this.state.message){
this.setState({
lastMessage:this.state.message,
height:this.yourRef.current.offsetHeight
})
}
}
on a hidden div
<div
ref={this.yourRef}
style={{
height:this.state.height,
width:"100%",
opacity:0,
zIndex:-1,
whiteSpace: "pre-line"
})
>
{this.state.message}
</div>
Using hooks + typescript :
import { useEffect, useRef } from 'react';
import type { DetailedHTMLProps, TextareaHTMLAttributes } from 'react';
// inspired from : https://stackoverflow.com/a/5346855/14223224
export const AutogrowTextarea = (props: DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>) => {
const ref = useRef<HTMLTextAreaElement>(null);
let topPadding = 0;
let bottomPadding = 0;
const resize = () => {
ref.current.style.height = 'auto';
ref.current.style.height = ref.current.scrollHeight - topPadding - bottomPadding + 'px';
};
const delayedResize = () => {
window.setTimeout(resize, 0);
};
const getPropertyValue = (it: string) => {
return Number.parseFloat(window.getComputedStyle(ref.current).getPropertyValue(it));
};
useEffect(() => {
[topPadding, bottomPadding] = ['padding-top', 'padding-bottom'].map(getPropertyValue);
ref.current.focus();
ref.current.select();
resize();
}, []);
return <textarea ref={ref} onChange={resize} onCut={delayedResize} onPaste={delayedResize} onDrop={delayedResize} onKeyDown={delayedResize} rows={1} {...props} />;
};
import { useRef, useState } from "react"
const TextAreaComponent = () => {
const [inputVal, setInputVal] =useState("")
const inputRef = useRef(null)
const handleInputHeight = () => {
const scrollHeight = inputRef.current.scrollHeight;
inputRef.current.style.height = scrollHeight + "px";
};
const handleInputChange = () => {
setInputVal(inputRef.current.value)
handleInputHeight()
}
return (
<textarea
ref={inputRef}
value={inputVal}
onChange={handleInputChange}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSubmit(e);
inputRef.current.style.height = "40px";
}
}}
/>
)}
Extremely simple solution:
function allowTextareasToDynamicallyResize() {
let textareas = document.getElementsByTagName('textarea');
for (let i = 0; i < textareas.length; i++) {
textareas[i].style.height = textareas[i].scrollHeight + 'px';
textareas[i].addEventListener('input', (e) => {
e.target.style.height = textareas[i].scrollHeight + 'px';
});
}
}
// Call this function in the componentDidMount() method of App,
// or whatever class you want that contains all the textareas
// you want to dynamically resize.
This works by setting an event listener to all textareas. For any given textarea, new input will trigger a function that resizes it. This function looks at any scrollHeight, i.e. the height that is overflowing out of your existing container. It then increments the textarea height by that exact height. Simple!
As mentioned in the comment, you have to call this function in some method, but the important part is that you call it AFTER everything is mounted in React / populated in JS. So the componentDidMount() in App is a good place for this.

Categories

Resources