mirror of https://github.com/buster-so/buster.git
working text area
This commit is contained in:
parent
e3b5f9c991
commit
12cd9c85d0
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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';
|
Loading…
Reference in New Issue