[ui][text area] implementation

pull/326/head
Yangshun Tay 2 years ago
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>
);
}

@ -167,7 +167,7 @@ export function Error() {
errorMessage={
value.length < 6 ? 'Password must be at least 6 characters' : undefined
}
label="Email"
label="Password"
startAddOn={KeyIcon}
startAddOnType="icon"
type="password"

@ -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';

Loading…
Cancel
Save