Textareas should expand with content (vibe-kanban) (#194)
* Perfect! I've successfully implemented auto-expanding textareas for both the description field and the follow-up question textarea. Here's what was implemented: ## Changes Made: 1. **Created `AutoExpandingTextarea` component** - A new component that automatically adjusts height based on content 2. **Updated `FileSearchTextarea`** - Modified to use the new auto-expanding component with configurable `maxRows` 3. **Updated task creation form** - Description field now expands up to 8 rows maximum 4. **Updated follow-up question textarea** - Expands up to 6 rows maximum ## Key Features: - **Auto-expansion**: Textareas grow vertically as users type more content - **Maximum height limits**: Prevents infinite growth with configurable row limits - **Smooth transitions**: Height adjusts seamlessly without flickering - **Accessibility**: Maintains all existing functionality including keyboard navigation and file search The implementation ensures both textareas now provide a better user experience by expanding to show content without requiring manual resizing, while maintaining reasonable maximum heights to prevent layout issues. * prettier
This commit is contained in:
committed by
GitHub
parent
69956760e5
commit
fc0ede7538
@@ -102,6 +102,7 @@ export function TaskFollowUpSection() {
|
||||
disabled={!canSendFollowUp}
|
||||
projectId={projectId}
|
||||
rows={1}
|
||||
maxRows={6}
|
||||
/>
|
||||
<Button
|
||||
onClick={onSendFollowUp}
|
||||
|
||||
@@ -220,6 +220,7 @@ export function TaskFormDialog({
|
||||
onChange={setDescription}
|
||||
placeholder="Enter task description (optional). Type @ to search files."
|
||||
rows={3}
|
||||
maxRows={8}
|
||||
disabled={isSubmitting || isSubmittingAndStart}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
70
frontend/src/components/ui/auto-expanding-textarea.tsx
Normal file
70
frontend/src/components/ui/auto-expanding-textarea.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface AutoExpandingTextareaProps extends React.ComponentProps<'textarea'> {
|
||||
maxRows?: number;
|
||||
}
|
||||
|
||||
const AutoExpandingTextarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
AutoExpandingTextareaProps
|
||||
>(({ className, maxRows = 10, ...props }, ref) => {
|
||||
const internalRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Get the actual ref to use
|
||||
const textareaRef = ref || internalRef;
|
||||
|
||||
const adjustHeight = React.useCallback(() => {
|
||||
const textarea = (textareaRef as React.RefObject<HTMLTextAreaElement>)
|
||||
.current;
|
||||
if (!textarea) return;
|
||||
|
||||
// Reset height to auto to get the natural height
|
||||
textarea.style.height = 'auto';
|
||||
|
||||
// Calculate line height
|
||||
const style = window.getComputedStyle(textarea);
|
||||
const lineHeight = parseInt(style.lineHeight) || 20;
|
||||
const paddingTop = parseInt(style.paddingTop) || 0;
|
||||
const paddingBottom = parseInt(style.paddingBottom) || 0;
|
||||
|
||||
// Calculate max height based on maxRows
|
||||
const maxHeight = lineHeight * maxRows + paddingTop + paddingBottom;
|
||||
|
||||
// Set the height to scrollHeight, but cap at maxHeight
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
}, [maxRows]);
|
||||
|
||||
// Adjust height on mount and when content changes
|
||||
React.useEffect(() => {
|
||||
adjustHeight();
|
||||
}, [adjustHeight, props.value]);
|
||||
|
||||
// Adjust height on input
|
||||
const handleInput = React.useCallback(
|
||||
(e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
adjustHeight();
|
||||
if (props.onInput) {
|
||||
props.onInput(e);
|
||||
}
|
||||
},
|
||||
[adjustHeight, props.onInput]
|
||||
);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm resize-none overflow-hidden',
|
||||
className
|
||||
)}
|
||||
ref={textareaRef}
|
||||
onInput={handleInput}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
AutoExpandingTextarea.displayName = 'AutoExpandingTextarea';
|
||||
|
||||
export { AutoExpandingTextarea };
|
||||
@@ -1,6 +1,6 @@
|
||||
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea';
|
||||
import { projectsApi } from '@/lib/api';
|
||||
|
||||
interface FileSearchResult {
|
||||
@@ -17,6 +17,7 @@ interface FileSearchTextareaProps {
|
||||
className?: string;
|
||||
projectId?: string;
|
||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||
maxRows?: number;
|
||||
}
|
||||
|
||||
export function FileSearchTextarea({
|
||||
@@ -28,6 +29,7 @@ export function FileSearchTextarea({
|
||||
className,
|
||||
projectId,
|
||||
onKeyDown,
|
||||
maxRows = 10,
|
||||
}: FileSearchTextareaProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<FileSearchResult[]>([]);
|
||||
@@ -230,7 +232,7 @@ export function FileSearchTextarea({
|
||||
<div
|
||||
className={`relative ${className?.includes('flex-1') ? 'flex-1' : ''}`}
|
||||
>
|
||||
<Textarea
|
||||
<AutoExpandingTextarea
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
@@ -239,6 +241,7 @@ export function FileSearchTextarea({
|
||||
rows={rows}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
maxRows={maxRows}
|
||||
/>
|
||||
|
||||
{showDropdown &&
|
||||
|
||||
Reference in New Issue
Block a user