mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-04 18:59:08 +01:00
multiline input
This commit is contained in:
parent
26a2ce250f
commit
719831d254
@ -1,64 +0,0 @@
|
|||||||
.chat-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
border-radius: 6px;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
border: 2px solid var(--form-element-border-color);
|
|
||||||
background: var(--form-element-bg-color);
|
|
||||||
padding: 5px;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
&.focused {
|
|
||||||
border-color: var(--form-element-primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
border-color: var(--form-element-error-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-left-element,
|
|
||||||
.input-right-element {
|
|
||||||
padding: 0 5px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-textarea {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: transparent;
|
|
||||||
resize: none;
|
|
||||||
overflow-y: auto;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: var(--form-element-text-color);
|
|
||||||
vertical-align: top;
|
|
||||||
height: auto;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-palette-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 3px 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
import clsx from "clsx";
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { EmojiPalette } from "./emojipalette";
|
|
||||||
|
|
||||||
import "./chatinput.less";
|
|
||||||
|
|
||||||
interface ChatInputProps {
|
|
||||||
value?: string;
|
|
||||||
className?: string;
|
|
||||||
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
||||||
onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
||||||
onFocus?: () => void;
|
|
||||||
onBlur?: () => void;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
maxLength?: number;
|
|
||||||
autoFocus?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
rows?: number;
|
|
||||||
maxRows?: number;
|
|
||||||
inputRef?: React.MutableRefObject<HTMLTextAreaElement>;
|
|
||||||
manageFocus?: (isFocused: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChatInput = ({
|
|
||||||
value,
|
|
||||||
className,
|
|
||||||
onChange,
|
|
||||||
onKeyDown,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
placeholder,
|
|
||||||
defaultValue = "",
|
|
||||||
maxLength,
|
|
||||||
autoFocus,
|
|
||||||
disabled,
|
|
||||||
rows = 1,
|
|
||||||
maxRows = 5,
|
|
||||||
inputRef,
|
|
||||||
manageFocus,
|
|
||||||
}: ChatInputProps) => {
|
|
||||||
const textareaRef = inputRef || useRef<HTMLTextAreaElement>(null);
|
|
||||||
const actionWrapperRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
||||||
const [lineHeight, setLineHeight] = useState(24); // Default line height fallback of 24px
|
|
||||||
|
|
||||||
// Function to count the number of lines in the textarea value
|
|
||||||
const countLines = (text: string) => {
|
|
||||||
return text.split("\n").length;
|
|
||||||
};
|
|
||||||
|
|
||||||
const adjustTextareaHeight = () => {
|
|
||||||
if (textareaRef.current) {
|
|
||||||
textareaRef.current.style.height = "auto"; // Reset height to auto first
|
|
||||||
|
|
||||||
const maxHeight = maxRows * lineHeight; // Max height based on maxRows
|
|
||||||
const currentLines = countLines(textareaRef.current.value); // Count the number of lines
|
|
||||||
const newHeight = Math.min(textareaRef.current.scrollHeight, maxHeight); // Calculate new height
|
|
||||||
|
|
||||||
// If the number of lines is less than or equal to maxRows, set height accordingly
|
|
||||||
const calculatedHeight = currentLines <= maxRows ? `${lineHeight * currentLines}px` : `${newHeight}px`;
|
|
||||||
|
|
||||||
textareaRef.current.style.height = calculatedHeight; // Set new height based on lines or scrollHeight
|
|
||||||
if (actionWrapperRef.current) {
|
|
||||||
actionWrapperRef.current.style.height = calculatedHeight; // Adjust emoji palette wrapper height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
setInternalValue(e.target.value);
|
|
||||||
onChange?.(e);
|
|
||||||
|
|
||||||
// Adjust the height of the textarea after text change
|
|
||||||
adjustTextareaHeight();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFocus = () => {
|
|
||||||
manageFocus?.(true);
|
|
||||||
onFocus?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur = () => {
|
|
||||||
manageFocus?.(false);
|
|
||||||
onBlur?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (textareaRef.current) {
|
|
||||||
const computedStyle = window.getComputedStyle(textareaRef.current);
|
|
||||||
let lineHeightValue = computedStyle.lineHeight;
|
|
||||||
const detectedLineHeight = parseFloat(lineHeightValue);
|
|
||||||
setLineHeight(detectedLineHeight);
|
|
||||||
}
|
|
||||||
}, [textareaRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
adjustTextareaHeight(); // Adjust the height when the component mounts or value changes
|
|
||||||
}, [value, maxRows, lineHeight]);
|
|
||||||
|
|
||||||
const inputValue = value ?? internalValue;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={clsx("chat-group", className)}>
|
|
||||||
<textarea
|
|
||||||
className="chat-textarea"
|
|
||||||
ref={textareaRef}
|
|
||||||
value={inputValue}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
placeholder={placeholder}
|
|
||||||
maxLength={maxLength}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
disabled={disabled}
|
|
||||||
rows={rows}
|
|
||||||
style={{
|
|
||||||
overflowY:
|
|
||||||
textareaRef.current && textareaRef.current.scrollHeight > maxRows * lineHeight
|
|
||||||
? "auto"
|
|
||||||
: "hidden",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div ref={actionWrapperRef} className="emoji-palette-wrapper">
|
|
||||||
<EmojiPalette placement="top-end" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { ChatInput };
|
|
@ -18,10 +18,6 @@
|
|||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.focused {
|
|
||||||
border-color: var(--form-element-primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
@ -58,8 +54,8 @@
|
|||||||
line-height: 24;
|
line-height: 24;
|
||||||
padding: 5px 7px;
|
padding: 5px 7px;
|
||||||
|
|
||||||
&:placeholder-shown {
|
&:focus {
|
||||||
user-select: none;
|
border-color: var(--form-element-primary-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,17 +100,17 @@
|
|||||||
|
|
||||||
/* Ensure the input elements inside the group behave correctly */
|
/* Ensure the input elements inside the group behave correctly */
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
border: none; /* Remove the individual input border */
|
border: none;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
&.focused {
|
&.focused {
|
||||||
border-color: transparent; /* Make sure individual input focus doesn't override group focus */
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
border-color: transparent; /* Make sure individual input error doesn't override group error */
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-wrapper-inner {
|
.input-wrapper-inner {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React, { forwardRef, useState } from "react";
|
import React, { forwardRef, memo, useImperativeHandle, useRef, useState } from "react";
|
||||||
|
|
||||||
import "./input.less";
|
import "./input.less";
|
||||||
|
|
||||||
@ -71,87 +71,91 @@ interface InputProps {
|
|||||||
manageFocus?: (isFocused: boolean) => void;
|
manageFocus?: (isFocused: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Input = forwardRef<HTMLDivElement, InputProps>(
|
const Input = memo(
|
||||||
(
|
forwardRef<HTMLInputElement, InputProps>(
|
||||||
{
|
(
|
||||||
label,
|
{
|
||||||
value,
|
label,
|
||||||
className,
|
value,
|
||||||
onChange,
|
className,
|
||||||
onKeyDown,
|
onChange,
|
||||||
onFocus,
|
onKeyDown,
|
||||||
onBlur,
|
onFocus,
|
||||||
placeholder,
|
onBlur,
|
||||||
defaultValue = "",
|
placeholder,
|
||||||
required,
|
defaultValue = "",
|
||||||
maxLength,
|
required,
|
||||||
autoFocus,
|
maxLength,
|
||||||
disabled,
|
autoFocus,
|
||||||
isNumber,
|
disabled,
|
||||||
inputRef,
|
isNumber,
|
||||||
manageFocus,
|
manageFocus,
|
||||||
}: InputProps,
|
}: InputProps,
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const [internalValue, setInternalValue] = useState(defaultValue);
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<any>) => {
|
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
|
||||||
const inputValue = e.target.value;
|
|
||||||
|
|
||||||
if (isNumber && inputValue !== "" && !/^\d*$/.test(inputValue)) {
|
const handleInputChange = (e: React.ChangeEvent<any>) => {
|
||||||
return;
|
const inputValue = e.target.value;
|
||||||
}
|
|
||||||
|
|
||||||
if (value === undefined) {
|
if (isNumber && inputValue !== "" && !/^\d*$/.test(inputValue)) {
|
||||||
setInternalValue(inputValue);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange && onChange(inputValue);
|
if (value === undefined) {
|
||||||
};
|
setInternalValue(inputValue);
|
||||||
|
}
|
||||||
|
|
||||||
const handleFocus = () => {
|
onChange && onChange(inputValue);
|
||||||
manageFocus?.(true);
|
};
|
||||||
onFocus?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleFocus = () => {
|
||||||
manageFocus?.(false);
|
manageFocus?.(true);
|
||||||
onBlur?.();
|
onFocus?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputValue = value ?? internalValue;
|
const handleBlur = () => {
|
||||||
|
manageFocus?.(false);
|
||||||
|
onBlur?.();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const inputValue = value ?? internalValue;
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={clsx("input-wrapper", className, {
|
|
||||||
disabled: disabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="input-wrapper-inner">
|
|
||||||
{label && (
|
|
||||||
<label className={clsx("label")} htmlFor={label}>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input
|
return (
|
||||||
className={clsx("input")}
|
<div
|
||||||
ref={inputRef}
|
ref={ref}
|
||||||
value={inputValue}
|
className={clsx("input-wrapper", className, {
|
||||||
onChange={handleInputChange}
|
disabled: disabled,
|
||||||
onKeyDown={onKeyDown}
|
})}
|
||||||
onFocus={handleFocus}
|
>
|
||||||
onBlur={handleBlur}
|
<div className="input-wrapper-inner">
|
||||||
placeholder={placeholder}
|
{label && (
|
||||||
maxLength={maxLength}
|
<label className={clsx("label")} htmlFor={label}>
|
||||||
autoFocus={autoFocus}
|
{label}
|
||||||
disabled={disabled}
|
</label>
|
||||||
/>
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
className={clsx("input")}
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
placeholder={placeholder}
|
||||||
|
maxLength={maxLength}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
export { Input, InputGroup, InputLeftElement, InputRightElement };
|
export { Input, InputGroup, InputLeftElement, InputRightElement };
|
||||||
|
24
frontend/app/element/multilineinput.less
Normal file
24
frontend/app/element/multilineinput.less
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
.multiline-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
box-shadow: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: transparent;
|
||||||
|
resize: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--form-element-text-color);
|
||||||
|
vertical-align: top;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--form-element-border-color);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 26px;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ChatInput } from "./chatinput";
|
import { MultiLineInput } from "./multilineinput";
|
||||||
|
|
||||||
const meta: Meta<typeof ChatInput> = {
|
const meta: Meta<typeof MultiLineInput> = {
|
||||||
title: "Elements/ChatInput",
|
title: "Elements/MultiLineInput",
|
||||||
component: ChatInput,
|
component: MultiLineInput,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
value: {
|
value: {
|
||||||
description: "The value of the textarea.",
|
description: "The value of the textarea.",
|
||||||
@ -47,10 +47,10 @@ const meta: Meta<typeof ChatInput> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ChatInput>;
|
type Story = StoryObj<typeof MultiLineInput>;
|
||||||
|
|
||||||
// Default ChatInput Story
|
// Default MultiLineInput Story
|
||||||
export const DefaultChatInput: Story = {
|
export const DefaultMultiLineInput: Story = {
|
||||||
render: (args) => {
|
render: (args) => {
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
@ -65,11 +65,11 @@ export const DefaultChatInput: Story = {
|
|||||||
height: "600px",
|
height: "600px",
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-end",
|
alignItems: "flex-start",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChatInput {...args} value={message} onChange={handleChange} />
|
<MultiLineInput {...args} value={message} onChange={handleChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -80,8 +80,8 @@ export const DefaultChatInput: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ChatInput with long text
|
// MultiLineInput with long text
|
||||||
export const ChatInputWithLongText: Story = {
|
export const MultiLineInputWithLongText: Story = {
|
||||||
render: (args) => {
|
render: (args) => {
|
||||||
const [message, setMessage] = useState("This is a long message that will expand the textarea.");
|
const [message, setMessage] = useState("This is a long message that will expand the textarea.");
|
||||||
|
|
||||||
@ -96,11 +96,11 @@ export const ChatInputWithLongText: Story = {
|
|||||||
height: "600px",
|
height: "600px",
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-end",
|
alignItems: "flex-start",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChatInput {...args} value={message} onChange={handleChange} />
|
<MultiLineInput {...args} value={message} onChange={handleChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
144
frontend/app/element/multilineinput.tsx
Normal file
144
frontend/app/element/multilineinput.tsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React, { forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import "./multilineinput.less";
|
||||||
|
|
||||||
|
interface MultiLineInputProps {
|
||||||
|
value?: string;
|
||||||
|
className?: string;
|
||||||
|
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
|
onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
placeholder?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
maxLength?: number;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
rows?: number;
|
||||||
|
maxRows?: number;
|
||||||
|
inputRef?: React.MutableRefObject<HTMLTextAreaElement>;
|
||||||
|
manageFocus?: (isFocused: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiLineInput = memo(
|
||||||
|
forwardRef<HTMLTextAreaElement, MultiLineInputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
onChange,
|
||||||
|
onKeyDown,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
placeholder,
|
||||||
|
defaultValue = "",
|
||||||
|
maxLength,
|
||||||
|
autoFocus,
|
||||||
|
disabled,
|
||||||
|
rows = 1,
|
||||||
|
maxRows = 5,
|
||||||
|
manageFocus,
|
||||||
|
}: MultiLineInputProps,
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
||||||
|
const [lineHeight, setLineHeight] = useState(24); // Default line height fallback of 24px
|
||||||
|
const [paddingTop, setPaddingTop] = useState(0);
|
||||||
|
const [paddingBottom, setPaddingBottom] = useState(0);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => textareaRef.current as HTMLTextAreaElement);
|
||||||
|
|
||||||
|
// Function to count the number of lines in the textarea value
|
||||||
|
const countLines = (text: string) => {
|
||||||
|
return text.split("\n").length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjustTextareaHeight = () => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.style.height = "auto"; // Reset height to auto first
|
||||||
|
|
||||||
|
const maxHeight = maxRows * lineHeight + paddingTop + paddingBottom; // Max height based on maxRows
|
||||||
|
const currentLines = countLines(textareaRef.current.value); // Count the number of lines
|
||||||
|
const newHeight = Math.min(textareaRef.current.scrollHeight, maxHeight); // Calculate new height
|
||||||
|
|
||||||
|
// If the number of lines is less than or equal to maxRows, set height accordingly
|
||||||
|
const calculatedHeight =
|
||||||
|
currentLines <= maxRows
|
||||||
|
? `${lineHeight * currentLines + paddingTop + paddingBottom}px`
|
||||||
|
: `${newHeight}px`;
|
||||||
|
|
||||||
|
textareaRef.current.style.height = calculatedHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setInternalValue(e.target.value);
|
||||||
|
onChange?.(e);
|
||||||
|
|
||||||
|
// Adjust the height of the textarea after text change
|
||||||
|
adjustTextareaHeight();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
manageFocus?.(true);
|
||||||
|
onFocus?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
manageFocus?.(false);
|
||||||
|
onBlur?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
const computedStyle = window.getComputedStyle(textareaRef.current);
|
||||||
|
const detectedLineHeight = parseFloat(computedStyle.lineHeight);
|
||||||
|
const detectedPaddingTop = parseFloat(computedStyle.paddingTop);
|
||||||
|
const detectedPaddingBottom = parseFloat(computedStyle.paddingBottom);
|
||||||
|
|
||||||
|
setLineHeight(detectedLineHeight);
|
||||||
|
setPaddingTop(detectedPaddingTop);
|
||||||
|
setPaddingBottom(detectedPaddingBottom);
|
||||||
|
}
|
||||||
|
}, [textareaRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
adjustTextareaHeight();
|
||||||
|
}, [value, maxRows, lineHeight, paddingTop, paddingBottom]);
|
||||||
|
|
||||||
|
const inputValue = value ?? internalValue;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={clsx("multiline-input", className)}
|
||||||
|
ref={textareaRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
placeholder={placeholder}
|
||||||
|
maxLength={maxLength}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
disabled={disabled}
|
||||||
|
rows={rows}
|
||||||
|
style={{
|
||||||
|
overflowY:
|
||||||
|
textareaRef.current &&
|
||||||
|
textareaRef.current.scrollHeight > maxRows * lineHeight + paddingTop + paddingBottom
|
||||||
|
? "auto"
|
||||||
|
: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export { MultiLineInput };
|
||||||
|
export type { MultiLineInputProps };
|
@ -102,7 +102,7 @@ const TypeAheadModal = ({
|
|||||||
const width = domRect?.width ?? 0;
|
const width = domRect?.width ?? 0;
|
||||||
const height = domRect?.height ?? 0;
|
const height = domRect?.height ?? 0;
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const realInputRef = useRef<HTMLInputElement>(null);
|
const realInputRef = useRef<HTMLInputElement>(null);
|
||||||
const suggestionsWrapperRef = useRef<HTMLDivElement>(null);
|
const suggestionsWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const suggestionsRef = useRef<HTMLDivElement>(null);
|
const suggestionsRef = useRef<HTMLDivElement>(null);
|
||||||
|
Loading…
Reference in New Issue
Block a user