forked from github/plane
[WEB-1024] fix: textarea component auto-resize (#4221)
* chore: updated resize hook logic * fix: page title overflow issue * chore: add length validation to page title
This commit is contained in:
parent
10ed12e589
commit
f0cb48006f
@ -1,6 +1,8 @@
|
||||
import * as React from "react";
|
||||
import React, { useRef } from "react";
|
||||
// helpers
|
||||
import { cn } from "../../helpers";
|
||||
// hooks
|
||||
import { useAutoResizeTextArea } from "../hooks/use-auto-resize-textarea";
|
||||
|
||||
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
mode?: "primary" | "transparent";
|
||||
@ -8,21 +10,6 @@ export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextArea
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Updates the height of a <textarea> when the value changes.
|
||||
const useAutoSizeTextArea = (textAreaRef: HTMLTextAreaElement | null, value: any) => {
|
||||
React.useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
||||
textAreaRef.style.height = "0px";
|
||||
const scrollHeight = textAreaRef.scrollHeight;
|
||||
|
||||
// We then set the height directly, outside of the render loop
|
||||
// Trying to set this with state or a ref will product an incorrect value.
|
||||
textAreaRef.style.height = scrollHeight + "px";
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
|
||||
const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
@ -35,10 +22,10 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>((props, re
|
||||
className = "",
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const textAreaRef = React.useRef<any>(ref);
|
||||
|
||||
useAutoSizeTextArea(textAreaRef?.current, value);
|
||||
// refs
|
||||
const textAreaRef = useRef<any>(ref);
|
||||
// auto re-size
|
||||
useAutoResizeTextArea(textAreaRef);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
|
24
packages/ui/src/hooks/use-auto-resize-textarea.ts
Normal file
24
packages/ui/src/hooks/use-auto-resize-textarea.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useAutoResizeTextArea = (textAreaRef: React.RefObject<HTMLTextAreaElement>) => {
|
||||
useEffect(() => {
|
||||
const textArea = textAreaRef.current;
|
||||
if (!textArea) return;
|
||||
|
||||
const resizeTextArea = () => {
|
||||
textArea.style.height = "auto";
|
||||
const computedHeight = textArea.scrollHeight + "px";
|
||||
textArea.style.height = computedHeight;
|
||||
};
|
||||
|
||||
const handleInput = () => resizeTextArea();
|
||||
|
||||
// resize on mount
|
||||
resizeTextArea();
|
||||
|
||||
textArea.addEventListener("input", handleInput);
|
||||
return () => {
|
||||
textArea.removeEventListener("input", handleInput);
|
||||
};
|
||||
}, [textAreaRef]);
|
||||
};
|
@ -109,7 +109,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
})}
|
||||
>
|
||||
<div className="h-full w-full flex flex-col gap-y-7 overflow-y-auto overflow-x-hidden">
|
||||
<div className="w-full flex-shrink-0 ml-5">
|
||||
<div className="relative w-full flex-shrink-0 pl-5">
|
||||
<PageEditorTitle
|
||||
editorRef={editorRef}
|
||||
title={pageTitle}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// editor
|
||||
import { EditorRefApi } from "@plane/document-editor";
|
||||
// ui
|
||||
import { TextArea } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
@ -13,27 +16,51 @@ type Props = {
|
||||
|
||||
export const PageEditorTitle: React.FC<Props> = observer((props) => {
|
||||
const { editorRef, readOnly, title, updateTitle } = props;
|
||||
// states
|
||||
const [isLengthVisible, setIsLengthVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{readOnly ? (
|
||||
<h6 className="-mt-2 break-words bg-transparent text-4xl font-bold">{title}</h6>
|
||||
) : (
|
||||
<TextArea
|
||||
onChange={(e) => updateTitle(e.target.value)}
|
||||
className="-mt-2 w-full bg-custom-background text-4xl font-bold outline-none p-0 border-none resize-none rounded-none"
|
||||
style={{
|
||||
lineHeight: "1.2",
|
||||
}}
|
||||
placeholder="Untitled Page"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
editorRef.current?.setFocusAtPosition(0);
|
||||
}
|
||||
}}
|
||||
value={title}
|
||||
/>
|
||||
<>
|
||||
<TextArea
|
||||
onChange={(e) => updateTitle(e.target.value)}
|
||||
className="-mt-2 w-full bg-custom-background text-4xl font-bold outline-none p-0 border-none resize-none rounded-none"
|
||||
style={{
|
||||
lineHeight: "1.2",
|
||||
}}
|
||||
placeholder="Untitled Page"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
editorRef.current?.setFocusAtPosition(0);
|
||||
}
|
||||
}}
|
||||
value={title}
|
||||
maxLength={255}
|
||||
onFocus={() => setIsLengthVisible(true)}
|
||||
onBlur={() => setIsLengthVisible(false)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200 opacity-0 transition-opacity",
|
||||
{
|
||||
"opacity-100": isLengthVisible,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn({
|
||||
"text-red-500": title.length === 0 || title.length > 255,
|
||||
})}
|
||||
>
|
||||
{title.length}
|
||||
</span>
|
||||
/255
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user