mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[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
|
// helpers
|
||||||
import { cn } from "../../helpers";
|
import { cn } from "../../helpers";
|
||||||
|
// hooks
|
||||||
|
import { useAutoResizeTextArea } from "../hooks/use-auto-resize-textarea";
|
||||||
|
|
||||||
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||||
mode?: "primary" | "transparent";
|
mode?: "primary" | "transparent";
|
||||||
@ -8,21 +10,6 @@ export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextArea
|
|||||||
className?: string;
|
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 TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>((props, ref) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@ -35,10 +22,10 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>((props, re
|
|||||||
className = "",
|
className = "",
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
// refs
|
||||||
const textAreaRef = React.useRef<any>(ref);
|
const textAreaRef = useRef<any>(ref);
|
||||||
|
// auto re-size
|
||||||
useAutoSizeTextArea(textAreaRef?.current, value);
|
useAutoResizeTextArea(textAreaRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<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="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
|
<PageEditorTitle
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
title={pageTitle}
|
title={pageTitle}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// editor
|
// editor
|
||||||
import { EditorRefApi } from "@plane/document-editor";
|
import { EditorRefApi } from "@plane/document-editor";
|
||||||
// ui
|
// ui
|
||||||
import { TextArea } from "@plane/ui";
|
import { TextArea } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editorRef: React.RefObject<EditorRefApi>;
|
editorRef: React.RefObject<EditorRefApi>;
|
||||||
@ -13,12 +16,15 @@ type Props = {
|
|||||||
|
|
||||||
export const PageEditorTitle: React.FC<Props> = observer((props) => {
|
export const PageEditorTitle: React.FC<Props> = observer((props) => {
|
||||||
const { editorRef, readOnly, title, updateTitle } = props;
|
const { editorRef, readOnly, title, updateTitle } = props;
|
||||||
|
// states
|
||||||
|
const [isLengthVisible, setIsLengthVisible] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{readOnly ? (
|
{readOnly ? (
|
||||||
<h6 className="-mt-2 break-words bg-transparent text-4xl font-bold">{title}</h6>
|
<h6 className="-mt-2 break-words bg-transparent text-4xl font-bold">{title}</h6>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<TextArea
|
<TextArea
|
||||||
onChange={(e) => updateTitle(e.target.value)}
|
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"
|
className="-mt-2 w-full bg-custom-background text-4xl font-bold outline-none p-0 border-none resize-none rounded-none"
|
||||||
@ -33,7 +39,28 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value={title}
|
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