working text area

This commit is contained in:
Nate Kelley 2025-02-24 18:28:52 -07:00
parent e3b5f9c991
commit 12cd9c85d0
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
3 changed files with 181 additions and 2 deletions

View File

@ -3,7 +3,7 @@ import { cn } from '@/lib/classMerge';
import { cva, type VariantProps } from 'class-variance-authority';
import { useMemoizedFn } from 'ahooks';
const inputVariants = cva(
export const inputVariants = cva(
'flex w-full rounded border px-2.5 text-base transition-all duration-200 disabled:cursor-not-allowed disabled:bg-item-select disabled:text-gray-light ',
{
variants: {
@ -49,7 +49,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
return (
<input
type={type}
type={'type'}
className={cn(inputVariants({ size, variant }), className)}
ref={ref}
onKeyDown={handleKeyDown}

View File

@ -0,0 +1,63 @@
import type { Meta, StoryObj } from '@storybook/react';
import { InputTextArea } from './InputTextArea';
const meta: Meta<typeof InputTextArea> = {
title: 'Base/InputTextArea',
component: InputTextArea,
tags: ['autodocs'],
args: {
autoResize: {
minRows: 1,
maxRows: 4
}
},
argTypes: {
variant: {
control: 'select',
options: ['default', 'ghost']
},
disabled: {
control: 'boolean'
},
placeholder: {
control: 'text'
},
rows: {
control: 'number'
}
}
};
export default meta;
type Story = StoryObj<typeof InputTextArea>;
export const Default: Story = {
args: {
placeholder: 'Enter text here...',
rows: 4
}
};
export const Ghost: Story = {
args: {
variant: 'ghost',
placeholder: 'Ghost textarea...',
rows: 4
}
};
export const Disabled: Story = {
args: {
disabled: true,
placeholder: 'Disabled textarea',
value: 'Cannot edit this text',
rows: 4
}
};
export const LargeRows: Story = {
args: {
placeholder: 'Large textarea...',
rows: 8
}
};

View File

@ -0,0 +1,116 @@
'use client';
import React, { useEffect, useRef } from 'react';
import { cn } from '@/lib/classMerge';
import { cva, type VariantProps } from 'class-variance-authority';
import { inputVariants } from './Input';
const inputTextAreaVariants = inputVariants;
interface AutoResizeOptions {
minRows?: number;
maxRows?: number;
}
export interface InputTextAreaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
VariantProps<typeof inputTextAreaVariants> {
autoResize?: AutoResizeOptions;
}
export const InputTextArea = React.forwardRef<HTMLTextAreaElement, InputTextAreaProps>(
({ className, variant = 'default', autoResize, style, rows = 1, ...props }, ref) => {
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const combinedRef = (node: HTMLTextAreaElement) => {
textareaRef.current = node;
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
ref.current = node;
}
};
const calculateMinHeight = () => {
const textarea = textareaRef.current;
if (!textarea || !autoResize) return null;
const computedStyle = window.getComputedStyle(textarea);
const lineHeight =
parseFloat(computedStyle.lineHeight) || parseFloat(computedStyle.fontSize) * 1.2;
const paddingTop = parseFloat(computedStyle.paddingTop);
const paddingBottom = parseFloat(computedStyle.paddingBottom);
return (autoResize.minRows || rows) * lineHeight + paddingTop + paddingBottom;
};
const adjustHeight = () => {
const textarea = textareaRef.current;
if (!textarea || !autoResize) return;
const minHeight = calculateMinHeight();
if (!minHeight) return;
// Reset the height to auto first to shrink properly
textarea.style.height = 'auto';
const computedStyle = window.getComputedStyle(textarea);
const lineHeight =
parseFloat(computedStyle.lineHeight) || parseFloat(computedStyle.fontSize) * 1.2;
const maxHeight = autoResize.maxRows
? autoResize.maxRows * lineHeight +
parseFloat(computedStyle.paddingTop) +
parseFloat(computedStyle.paddingBottom)
: Infinity;
// Get the scroll height after resetting to auto
const scrollHeight = Math.max(textarea.scrollHeight, minHeight);
const newHeight = Math.min(scrollHeight, maxHeight);
// Apply the new height
textarea.style.height = `${newHeight}px`;
textarea.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden';
};
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea || !autoResize) return;
const minHeight = calculateMinHeight();
if (minHeight) {
textarea.style.minHeight = `${minHeight}px`;
}
// Set initial height
adjustHeight();
// Add event listeners
const handleInput = () => {
requestAnimationFrame(adjustHeight);
};
textarea.addEventListener('input', handleInput);
window.addEventListener('resize', adjustHeight);
return () => {
textarea.removeEventListener('input', handleInput);
window.removeEventListener('resize', adjustHeight);
};
}, [autoResize]);
return (
<textarea
className={cn(inputTextAreaVariants({ variant }), 'px-5 py-4', className)}
ref={combinedRef}
rows={autoResize ? 1 : rows}
style={{
resize: 'none',
...style
}}
{...props}
/>
);
}
);
InputTextArea.displayName = 'InputTextArea';