Files
vibe-kanban/frontend/src/components/ui/auto-expanding-textarea.tsx
hayato iida 567b4a0411 Fix: Prevent form submission during IME composition (#934)
This fix addresses issue #919 where pressing Enter during IME (Input Method Editor)
composition incorrectly triggers form submission in task title and description fields.

Changes:
- Add isComposing check to Input component's Enter key handler
- Add isComposing check to AutoExpandingTextarea component's Enter key handler

This ensures that Enter key during IME composition (e.g., Japanese, Chinese, Korean input)
only confirms the text conversion without accidentally submitting the form.

Fixes #919
2025-10-04 14:30:46 +01:00

94 lines
2.9 KiB
TypeScript

import * as React from 'react';
import { cn } from '@/lib/utils';
interface AutoExpandingTextareaProps extends React.ComponentProps<'textarea'> {
maxRows?: number;
onCommandEnter?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onCommandShiftEnter?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
}
const AutoExpandingTextarea = React.forwardRef<
HTMLTextAreaElement,
AutoExpandingTextareaProps
>(
(
{ className, maxRows = 10, onCommandEnter, onCommandShiftEnter, ...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]);
// Handle keyboard shortcuts
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
if (e.metaKey && e.shiftKey) {
onCommandShiftEnter?.(e);
} else if (e.metaKey) {
onCommandEnter?.(e);
}
}
props.onKeyDown?.(e);
},
[onCommandEnter, onCommandShiftEnter, props.onKeyDown]
);
// 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(
'bg-muted p-0 min-h-[80px] w-full text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 resize-none overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words',
className
)}
ref={textareaRef}
onInput={handleInput}
{...props}
onKeyDown={handleKeyDown}
/>
);
}
);
AutoExpandingTextarea.displayName = 'AutoExpandingTextarea';
export { AutoExpandingTextarea };