Files
Genarrative/src/components/common/AutoGrowTextArea.tsx
T
k88936 99d239c8bf
Project CI / Repository checks (push) Successful in 50s
Project CI / Backend tests (push) Successful in 3m30s
Project CI / Frontend tests (push) Successful in 3m42s
Project CI / Native shell tests (push) Successful in 11m34s
style: 画布编辑器多行输入框样式调整 (#130)
复用了画布agent的输入框:

* 高度自适应
* 滑动条样式变为浅色和位置不再出界

before
![shotmd-1785745933-compressed.webp](/attachments/95727f6d-63e1-48f7-b806-8203cd73d1eb)
after
![shotmd-1785745957-compressed.webp](/attachments/b5af077d-d3b9-4572-9135-39e288291e24)

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/130
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-08-04 17:07:30 +08:00

131 lines
3.6 KiB
TypeScript

import {
type InputEventHandler,
type TextareaHTMLAttributes,
useLayoutEffect,
useRef,
} from 'react';
const DEFAULT_MIN_HEIGHT_PX = 40;
const DEFAULT_MAX_HEIGHT_PX = 128;
function supportsNativeFieldSizing() {
return (
typeof CSS !== 'undefined' &&
typeof CSS.supports === 'function' &&
CSS.supports('field-sizing', 'content')
);
}
function parseComputedPixelValue(value: string, fallback: number) {
const parsedValue = Number.parseFloat(value);
return Number.isFinite(parsedValue) ? parsedValue : fallback;
}
function resizeFallbackTextArea(textarea: HTMLTextAreaElement) {
textarea.style.height = 'auto';
const computedStyle = window.getComputedStyle(textarea);
const minHeight = parseComputedPixelValue(
computedStyle.minHeight,
DEFAULT_MIN_HEIGHT_PX,
);
const maxHeight = Math.max(
minHeight,
parseComputedPixelValue(computedStyle.maxHeight, DEFAULT_MAX_HEIGHT_PX),
);
const contentHeight = textarea.scrollHeight;
const borderHeight = Math.max(
0,
textarea.offsetHeight - textarea.clientHeight,
);
const isBorderBox = computedStyle.boxSizing === 'border-box';
const paddingHeight =
parseComputedPixelValue(computedStyle.paddingTop, 0) +
parseComputedPixelValue(computedStyle.paddingBottom, 0);
const measuredHeight = isBorderBox
? contentHeight + borderHeight
: Math.max(0, contentHeight - paddingHeight);
const nextHeight = Math.min(
Math.max(measuredHeight, minHeight),
maxHeight,
);
textarea.style.height = `${nextHeight}px`;
textarea.style.overflowY = measuredHeight > maxHeight ? 'auto' : 'hidden';
}
export function AutoGrowTextArea({
className,
onInput,
rows = 1,
value,
...textareaProps
}: TextareaHTMLAttributes<HTMLTextAreaElement>) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
useLayoutEffect(() => {
const textarea = textareaRef.current;
if (!textarea || supportsNativeFieldSizing()) {
return;
}
resizeFallbackTextArea(textarea);
}, [value]);
useLayoutEffect(() => {
const textarea = textareaRef.current;
if (!textarea || supportsNativeFieldSizing()) {
return undefined;
}
let previousWidth = textarea.getBoundingClientRect().width;
const resizeWhenWidthChanges = (nextWidth: number) => {
if (Math.abs(nextWidth - previousWidth) < 0.5) {
return;
}
previousWidth = nextWidth;
resizeFallbackTextArea(textarea);
};
if (typeof ResizeObserver !== 'undefined') {
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry) {
resizeWhenWidthChanges(entry.contentRect.width);
}
});
observer.observe(textarea);
return () => observer.disconnect();
}
const handleWindowResize = () => {
resizeWhenWidthChanges(textarea.getBoundingClientRect().width);
};
window.addEventListener('resize', handleWindowResize);
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
const resolvedClassName = [
'auto-grow-text-area max-h-32 min-h-10 resize-none overflow-x-hidden overflow-y-auto overscroll-contain [field-sizing:content] disabled:cursor-not-allowed disabled:opacity-60',
className,
]
.filter(Boolean)
.join(' ');
const handleInput: InputEventHandler<HTMLTextAreaElement> = (event) => {
if (!supportsNativeFieldSizing()) {
resizeFallbackTextArea(event.currentTarget);
}
onInput?.(event);
};
return (
<textarea
{...textareaProps}
ref={textareaRef}
className={resolvedClassName}
onInput={handleInput}
rows={rows}
value={value}
/>
);
}