feat: textarea auto resize (#3695)
* feat: improve textarea user experience with autoresize * chore: remove log * chore: update test * chore: update test and cleanup logic useEffect
This commit is contained in:
parent
c62b6e9842
commit
ba3c07eba8
@ -1,9 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen, act } from '@testing-library/react'
|
||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
import { TextArea } from './index'
|
import { TextArea } from './index'
|
||||||
|
|
||||||
// Mock the styles import
|
|
||||||
jest.mock('./styles.scss', () => ({}))
|
jest.mock('./styles.scss', () => ({}))
|
||||||
|
|
||||||
describe('@joi/core/TextArea', () => {
|
describe('@joi/core/TextArea', () => {
|
||||||
@ -31,4 +30,40 @@ describe('@joi/core/TextArea', () => {
|
|||||||
const textareaElement = screen.getByTestId('custom-textarea')
|
const textareaElement = screen.getByTestId('custom-textarea')
|
||||||
expect(textareaElement).toHaveAttribute('rows', '5')
|
expect(textareaElement).toHaveAttribute('rows', '5')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should auto resize the textarea based on minResize', () => {
|
||||||
|
render(<TextArea autoResize minResize={10} />)
|
||||||
|
|
||||||
|
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
|
||||||
|
|
||||||
|
Object.defineProperty(textarea, 'scrollHeight', {
|
||||||
|
value: 20,
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
textarea.value = 'Short text'
|
||||||
|
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(textarea.style.height).toBe('10px')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should auto resize the textarea based on maxResize', () => {
|
||||||
|
render(<TextArea autoResize maxResize={40} />)
|
||||||
|
|
||||||
|
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
|
||||||
|
|
||||||
|
Object.defineProperty(textarea, 'scrollHeight', {
|
||||||
|
value: 100,
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
textarea.value = 'A very long text that should exceed max height'
|
||||||
|
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(textarea.style.height).toBe('40px')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,19 +1,41 @@
|
|||||||
import React, { ReactNode, forwardRef } from 'react'
|
import React, { forwardRef, useRef, useEffect } from 'react'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import './styles.scss'
|
import './styles.scss'
|
||||||
import { ScrollArea } from '../ScrollArea'
|
|
||||||
|
type ResizeProps = {
|
||||||
|
autoResize?: boolean
|
||||||
|
minResize?: number
|
||||||
|
maxResize?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface TextAreaProps
|
export interface TextAreaProps
|
||||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
extends ResizeProps,
|
||||||
|
React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||||
({ className, ...props }, ref) => {
|
(
|
||||||
|
{ autoResize, minResize = 80, maxResize = 250, className, ...props },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoResize && textareaRef.current) {
|
||||||
|
const textarea = textareaRef.current
|
||||||
|
textarea.style.height = 'auto'
|
||||||
|
const scrollHeight = textarea.scrollHeight
|
||||||
|
const newHeight = Math.min(maxResize, Math.max(minResize, scrollHeight))
|
||||||
|
textarea.style.height = `${newHeight}px`
|
||||||
|
textarea.style.overflow = newHeight >= maxResize ? 'auto' : 'hidden'
|
||||||
|
}
|
||||||
|
}, [props.value, autoResize, minResize, maxResize])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="textarea__wrapper">
|
<div className="textarea__wrapper">
|
||||||
<textarea
|
<textarea
|
||||||
className={twMerge('textarea', className)}
|
className={twMerge('textarea', className)}
|
||||||
ref={ref}
|
ref={autoResize ? textareaRef : ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -36,7 +36,7 @@ const ModelConfigInput = ({
|
|||||||
<TextArea
|
<TextArea
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(e) => onValueChanged?.(e.target.value)}
|
onChange={(e) => onValueChanged?.(e.target.value)}
|
||||||
cols={50}
|
autoResize
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -247,7 +247,7 @@ const ThreadRightPanel = () => {
|
|||||||
id="assistant-instructions"
|
id="assistant-instructions"
|
||||||
placeholder="Eg. You are a helpful assistant."
|
placeholder="Eg. You are a helpful assistant."
|
||||||
value={activeThread?.assistants[0].instructions ?? ''}
|
value={activeThread?.assistants[0].instructions ?? ''}
|
||||||
rows={8}
|
autoResize
|
||||||
onChange={onAssistantInstructionChanged}
|
onChange={onAssistantInstructionChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user