️ feat: implement Input Field component

This commit is contained in:
Andre H 2024-02-21 15:43:53 +07:00
parent 636f68d7a4
commit 33b6191539
3 changed files with 170 additions and 0 deletions

View File

@ -0,0 +1,68 @@
import { VariantProps, tv } from 'tailwind-variants';
export const inputTheme = tv(
{
slots: {
container: [
'flex items-center rounded-lg relative',
'placeholder:text-elements-disabled',
'disabled:cursor-not-allowed disabled:bg-controls-disabled',
],
label: ['text-sm', 'text-elements-high-em'],
description: ['text-xs', 'text-elements-low-em'],
input: [
'focus-ring',
'block w-full h-full rounded-lg text-elements-mid-em',
'shadow-sm',
'border border-border-interactive',
'disabled:shadow-none',
'disabled:border-none',
],
icon: ['text-elements-mid-em'],
iconContainer: [
'absolute inset-y-0 flex items-center z-10 cursor-pointer',
],
helperIcon: [],
helperText: ['flex gap-2 items-center text-elements-danger'],
},
variants: {
state: {
default: {
input: '',
},
error: {
input: [
'outline outline-offset-0 outline-border-danger',
'shadow-none',
],
helperText: ['text-elements-danger'],
},
},
size: {
md: {
container: 'h-11',
input: 'text-sm pl-4 pr-4',
icon: 'h-[18px] w-[18px]',
helperText: 'text-sm',
helperIcon: 'h-5 w-5',
},
sm: {
container: 'h-8',
input: 'text-xs pl-3 pr-3',
icon: 'h-4 w-4',
helperText: 'text-xs',
helperIcon: 'h-4 w-4',
},
},
},
defaultVariants: {
size: 'md',
state: 'default',
},
},
{
responsiveVariants: true,
},
);
export type InputTheme = VariantProps<typeof inputTheme>;

View File

@ -0,0 +1,100 @@
import React, { ReactNode, useMemo } from 'react';
import { ComponentPropsWithoutRef } from 'react';
import { InputTheme, inputTheme } from './Input.theme';
import { cloneIcon } from 'utils/cloneIcon';
import { cn } from 'utils/classnames';
import { WarningIcon } from '../CustomIcon';
export interface InputProps
extends InputTheme,
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
label?: string;
description?: string;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
helperText?: string;
}
export const Input = ({
className,
label,
description,
leftIcon,
rightIcon,
helperText,
size,
state,
...props
}: InputProps) => {
const styleProps = (({ size = 'md', state }) => ({
size,
state,
}))({ size, state });
const {
container: containerCls,
label: labelCls,
description: descriptionCls,
input: inputCls,
icon: iconCls,
iconContainer: iconContainerCls,
helperText: helperTextCls,
helperIcon: helperIconCls,
} = inputTheme({ ...styleProps });
const renderLabels = useMemo(
() => (
<div className="space-y-1">
<p className={labelCls()}>{label}</p>
<p className={descriptionCls()}>{description}</p>
</div>
),
[labelCls, descriptionCls, label, description],
);
const renderLeftIcon = useMemo(() => {
return (
<div className={iconContainerCls({ class: 'left-0 pl-4' })}>
{cloneIcon(leftIcon, { className: iconCls(), ariaHidden: true })}
</div>
);
}, [iconCls, leftIcon]);
const renderRightIcon = useMemo(() => {
return (
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
{cloneIcon(rightIcon, { className: iconCls(), ariaHidden: true })}
</div>
);
}, [rightIcon, iconCls]);
const renderHelperText = useMemo(
() => (
<div className={helperTextCls()}>
{state &&
cloneIcon(<WarningIcon className={helperIconCls()} />, {
ariaHidden: true,
})}
<p>{helperText}</p>
</div>
),
[state, helperText, helperTextCls],
);
return (
<div className="space-y-2">
{renderLabels}
<div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon}
<input
className={cn(inputCls({ class: 'w-80' }), {
'pl-10': leftIcon,
})}
{...props}
/>
{rightIcon && renderRightIcon}
</div>
{renderHelperText}
</div>
);
};

View File

@ -0,0 +1,2 @@
export * from './Input';
export * from './Input.theme';