mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-30 21:53:28 +08:00
[ui][text area] implementation
This commit is contained in:
141
packages/ui/src/TextArea/TextArea.tsx
Normal file
141
packages/ui/src/TextArea/TextArea.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import clsx from 'clsx';
|
||||
import type {
|
||||
ChangeEvent,
|
||||
FocusEvent,
|
||||
ForwardedRef,
|
||||
TextareaHTMLAttributes,
|
||||
} from 'react';
|
||||
import React, { forwardRef, useId } from 'react';
|
||||
|
||||
type Attributes = Pick<
|
||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
| 'autoComplete'
|
||||
| 'autoFocus'
|
||||
| 'disabled'
|
||||
| 'maxLength'
|
||||
| 'minLength'
|
||||
| 'name'
|
||||
| 'onBlur'
|
||||
| 'onFocus'
|
||||
| 'placeholder'
|
||||
| 'readOnly'
|
||||
| 'required'
|
||||
| 'rows'
|
||||
>;
|
||||
|
||||
export type TextAreaResize = 'both' | 'horizontal' | 'none' | 'vertical';
|
||||
|
||||
type Props = Readonly<{
|
||||
defaultValue?: string;
|
||||
errorMessage?: React.ReactNode;
|
||||
id?: string;
|
||||
isLabelHidden?: boolean;
|
||||
label: string;
|
||||
onBlur?: (event: FocusEvent<HTMLTextAreaElement>) => void;
|
||||
onChange?: (value: string, event: ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
resize?: TextAreaResize;
|
||||
value?: string;
|
||||
}> &
|
||||
Readonly<Attributes>;
|
||||
|
||||
type State = 'error' | 'normal';
|
||||
|
||||
const stateClasses: Record<
|
||||
State,
|
||||
Readonly<{
|
||||
textArea: string;
|
||||
}>
|
||||
> = {
|
||||
error: {
|
||||
textArea:
|
||||
'border-danger-300 focus:ring-danger-500 focus:border-danger-500 text-danger-900 placeholder-danger-300',
|
||||
},
|
||||
normal: {
|
||||
textArea:
|
||||
'border-slate-300 focus:border-primary-500 focus:ring-primary-500 placeholder:text-slate-400',
|
||||
},
|
||||
};
|
||||
|
||||
const resizeClasses: Record<TextAreaResize, string> = {
|
||||
both: 'resize',
|
||||
horizontal: 'resize-x',
|
||||
none: 'resize-none',
|
||||
vertical: 'resize-y',
|
||||
};
|
||||
|
||||
function TextArea(
|
||||
{
|
||||
defaultValue,
|
||||
disabled,
|
||||
errorMessage,
|
||||
id: idParam,
|
||||
isLabelHidden,
|
||||
label,
|
||||
resize = 'vertical',
|
||||
required,
|
||||
value,
|
||||
onChange,
|
||||
...props
|
||||
}: Props,
|
||||
ref: ForwardedRef<HTMLTextAreaElement>,
|
||||
) {
|
||||
const hasError = errorMessage != null;
|
||||
const generatedId = useId();
|
||||
const id = idParam ?? generatedId;
|
||||
const errorId = useId();
|
||||
const state: State = hasError ? 'error' : 'normal';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
className={clsx(
|
||||
isLabelHidden
|
||||
? 'sr-only'
|
||||
: 'mb-1 block text-sm font-medium text-gray-700',
|
||||
)}
|
||||
htmlFor={id}>
|
||||
{label}
|
||||
{required && (
|
||||
<span aria-hidden="true" className="text-danger-500">
|
||||
{' '}
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div>
|
||||
<textarea
|
||||
ref={ref}
|
||||
aria-describedby={hasError ? errorId : undefined}
|
||||
aria-invalid={hasError ? true : undefined}
|
||||
className={clsx(
|
||||
'block w-full rounded-md sm:text-sm',
|
||||
stateClasses[state].textArea,
|
||||
disabled && 'bg-slate-100',
|
||||
resizeClasses[resize],
|
||||
)}
|
||||
defaultValue={defaultValue}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
name="comment"
|
||||
required={required}
|
||||
value={value != null ? value : undefined}
|
||||
onChange={(event) => {
|
||||
if (!onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(event.target.value, event);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<p className="text-danger-600 mt-2 text-sm" id={errorId}>
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(TextArea);
|
@ -129,7 +129,7 @@ function TextInput(
|
||||
className={clsx(
|
||||
isLabelHidden
|
||||
? 'sr-only'
|
||||
: 'block text-sm font-medium text-slate-700',
|
||||
: 'mb-1 block text-sm font-medium text-slate-700',
|
||||
)}
|
||||
htmlFor={id}>
|
||||
{label}
|
||||
@ -143,7 +143,6 @@ function TextInput(
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full overflow-hidden rounded-md border focus-within:ring-1 sm:text-sm',
|
||||
!isLabelHidden && 'mt-1',
|
||||
disabled && 'pointer-events-none select-none bg-slate-100',
|
||||
containerClass,
|
||||
)}>
|
||||
|
@ -34,6 +34,9 @@ export { default as Spinner } from './Spinner/Spinner';
|
||||
// Tabs
|
||||
export * from './Tabs/Tabs';
|
||||
export { default as Tabs } from './Tabs/Tabs';
|
||||
// TextArea
|
||||
export * from './TextArea/TextArea';
|
||||
export { default as TextArea } from './TextArea/TextArea';
|
||||
// TextInput
|
||||
export * from './TextInput/TextInput';
|
||||
export { default as TextInput } from './TextInput/TextInput';
|
||||
|
Reference in New Issue
Block a user