Merge pull request #85 from snowball-tools/andrehadianto/T-4863-input-field

feat: input field
This commit is contained in:
Andre Hadianto 2024-02-22 10:18:59 +08:00 committed by GitHub
commit 2369f4498a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 330 additions and 1 deletions

View File

@ -16,6 +16,7 @@
"@types/react": "^18.2.42", "@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"assert": "^2.1.0", "assert": "^2.1.0",
"clsx": "^2.1.0",
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"downshift": "^8.2.3", "downshift": "^8.2.3",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CrossIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M5 5L19 19M19 5L5 19"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const SearchIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M20 20L16.05 16.05M18 11C18 14.866 14.866 18 11 18C7.13401 18 4 14.866 4 11C4 7.13401 7.13401 4 11 4C14.866 4 18 7.13401 18 11Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const WarningIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.4902 2.84406C11.1661 1.69 12.8343 1.69 13.5103 2.84406L22.0156 17.3654C22.699 18.5321 21.8576 19.9999 20.5056 19.9999H3.49483C2.14281 19.9999 1.30147 18.5321 1.98479 17.3654L10.4902 2.84406ZM12 9C12.4142 9 12.75 9.33579 12.75 9.75V13.25C12.75 13.6642 12.4142 14 12 14C11.5858 14 11.25 13.6642 11.25 13.25V9.75C11.25 9.33579 11.5858 9 12 9ZM13 15.75C13 16.3023 12.5523 16.75 12 16.75C11.4477 16.75 11 16.3023 11 15.75C11 15.1977 11.4477 14.75 12 14.75C12.5523 14.75 13 15.1977 13 15.75Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -4,4 +4,7 @@ export * from './CheckIcon';
export * from './ChevronGrabberHorizontal'; export * from './ChevronGrabberHorizontal';
export * from './ChevronLeft'; export * from './ChevronLeft';
export * from './ChevronRight'; export * from './ChevronRight';
export * from './WarningIcon';
export * from './SearchIcon';
export * from './CrossIcon';
export * from './GlobeIcon'; export * from './GlobeIcon';

View File

@ -0,0 +1,84 @@
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 { WarningIcon } from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon';
import { cn } from 'utils/classnames';
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>
);
}, [cloneIcon, iconCls, iconContainerCls, leftIcon]);
const renderRightIcon = useMemo(() => {
return (
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
{cloneIcon(rightIcon, { className: iconCls(), ariaHidden: true })}
</div>
);
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
const renderHelperText = useMemo(
() => (
<div className={helperTextCls()}>
{state &&
cloneIcon(<WarningIcon className={helperIconCls()} />, {
ariaHidden: true,
})}
<p>{helperText}</p>
</div>
),
[cloneIcon, state, helperIconCls, 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';

View File

@ -17,6 +17,7 @@ import {
renderTabs, renderTabs,
renderVerticalTabs, renderVerticalTabs,
} from './renders/tabs'; } from './renders/tabs';
import { renderInputs } from './renders/input';
const Page = () => { const Page = () => {
const [singleDate, setSingleDate] = useState<Value>(); const [singleDate, setSingleDate] = useState<Value>();
@ -37,6 +38,11 @@ const Page = () => {
{/* Button */} {/* Button */}
<div className="flex flex-col gap-10 items-center justify-between"> <div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Input</h1>
<div className="flex w-full flex-col gap-10">{renderInputs()}</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
<h1 className="text-2xl font-bold">Button</h1> <h1 className="text-2xl font-bold">Button</h1>
<div className="flex flex-col gap-10"> <div className="flex flex-col gap-10">
{renderButtons()} {renderButtons()}

View File

@ -0,0 +1,57 @@
import React from 'react';
import { Input } from 'components/shared/Input';
import { SearchIcon, CrossIcon } from 'components/shared/CustomIcon';
export const renderInputs = () => {
return (
<>
<div className="flex w-full gap-10">
<Input
label="Label"
description="Additional information or context"
leftIcon={<SearchIcon />}
rightIcon={<CrossIcon />}
placeholder="Placeholder text"
/>
<Input
disabled
label="Label"
description="Additional information or context"
placeholder="Placeholder text"
/>
<Input
state="error"
label="Label"
description="Additional information or context"
placeholder="Placeholder text"
helperText="The error goes here"
/>
</div>
<div className="flex w-full gap-10">
<Input
label="Label"
leftIcon={<SearchIcon />}
rightIcon={<CrossIcon />}
description="Additional information or context"
placeholder="Placeholder text"
size="sm"
/>
<Input
disabled
label="Label"
description="Additional information or context"
placeholder="Placeholder text"
size="sm"
/>
<Input
state="error"
label="Label"
description="Additional information or context"
placeholder="Placeholder text"
size="sm"
helperText="The error goes here"
/>
</div>
</>
);
};

View File

@ -0,0 +1,13 @@
import { clsx } from 'clsx';
import type { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Returns a merged class name string by merging and processing multiple class names and Tailwind CSS styles.
*
* @param {...string[]} args - One or more class names and/or Tailwind CSS styles to be merged.
* @returns {string} - The merged class name string.
*/
export function cn(...args: ClassValue[]): string {
return twMerge(clsx(args));
}

View File

@ -6327,7 +6327,7 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
clsx@^2.0.0: clsx@^2.0.0, clsx@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==