parent
d9880dbff1
commit
2f13d5f009
@ -0,0 +1,144 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import type { ComponentMeta } from '@storybook/react';
|
||||||
|
import type { TextAreaResize } from '@tih/ui';
|
||||||
|
import { TextArea } from '@tih/ui';
|
||||||
|
|
||||||
|
const textAreaResize: ReadonlyArray<TextAreaResize> = [
|
||||||
|
'vertical',
|
||||||
|
'horizontal',
|
||||||
|
'none',
|
||||||
|
'both',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
argTypes: {
|
||||||
|
autoComplete: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
isLabelHidden: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
resize: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: textAreaResize,
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
control: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
component: TextArea,
|
||||||
|
title: 'TextArea',
|
||||||
|
} as ComponentMeta<typeof TextArea>;
|
||||||
|
|
||||||
|
export const Basic = {
|
||||||
|
args: {
|
||||||
|
label: 'Comment',
|
||||||
|
placeholder: 'Type your comment here',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function HiddenLabel() {
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextArea
|
||||||
|
isLabelHidden={true}
|
||||||
|
label="Name"
|
||||||
|
placeholder="John Doe"
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Disabled() {
|
||||||
|
return (
|
||||||
|
<TextArea
|
||||||
|
disabled={true}
|
||||||
|
label="Comment"
|
||||||
|
placeholder="You can't type here, it's disabled."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Required() {
|
||||||
|
return (
|
||||||
|
<TextArea label="Required input" placeholder="John Doe" required={true} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Error() {
|
||||||
|
const [value, setValue] = useState('1234');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextArea
|
||||||
|
errorMessage={value.length < 6 ? 'Your comment is too short' : undefined}
|
||||||
|
label="Leave a reply"
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReadOnly() {
|
||||||
|
return (
|
||||||
|
<TextArea
|
||||||
|
label="Leave a reply"
|
||||||
|
readOnly={true}
|
||||||
|
value="But you can't change this"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Rows() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<TextArea label="Reply" placeholder="Leave a reply" rows={4} />
|
||||||
|
<TextArea label="Reply" placeholder="Leave a reply" rows={10} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Resize() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<TextArea
|
||||||
|
label="Vertical resizing"
|
||||||
|
placeholder="Leave a reply"
|
||||||
|
resize="vertical"
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
label="Horizontal resizing"
|
||||||
|
placeholder="Leave a reply"
|
||||||
|
resize="horizontal"
|
||||||
|
/>
|
||||||
|
<TextArea label="No resizing" placeholder="Leave a reply" resize="none" />
|
||||||
|
<TextArea
|
||||||
|
label="Both resizing"
|
||||||
|
placeholder="Leave a reply"
|
||||||
|
resize="both"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -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);
|
Loading…
Reference in new issue