From fdfe92b12291520c167f900405507966ec39d7d7 Mon Sep 17 00:00:00 2001 From: Manan Vaghasiya Date: Mon, 16 Jan 2023 11:36:26 +0000 Subject: [PATCH 1/2] use cva for input component variant classes --- .../packages/ui-components/package.json | 1 + .../src/components/input/HelperText.tsx | 42 ++- .../src/components/input/TextInput.tsx | 277 ++++++++++-------- .../input/TextInputArea.stories.tsx | 7 - .../components/input/TextInputArea.test.tsx | 5 +- .../src/components/input/TextInputArea.tsx | 121 +++++--- .../packages/ui-components/src/types/utils.ts | 3 + deepfence_frontend/pnpm-lock.yaml | 14 +- 8 files changed, 270 insertions(+), 200 deletions(-) create mode 100644 deepfence_frontend/packages/ui-components/src/types/utils.ts diff --git a/deepfence_frontend/packages/ui-components/package.json b/deepfence_frontend/packages/ui-components/package.json index 7816fd0810..6046485c78 100644 --- a/deepfence_frontend/packages/ui-components/package.json +++ b/deepfence_frontend/packages/ui-components/package.json @@ -39,6 +39,7 @@ "@tanstack/react-table": "^8.5.13", "ariakit": "2.0.0-next.41", "classnames": "^2.3.2", + "cva": "npm:class-variance-authority@^0.4.0", "lodash-es": "^4.17.21", "tailwind-merge": "^1.6.0", "tailwindcss-radix": "^2.6.0" diff --git a/deepfence_frontend/packages/ui-components/src/components/input/HelperText.tsx b/deepfence_frontend/packages/ui-components/src/components/input/HelperText.tsx index 9f0ba8cda9..8a5cf19e5f 100644 --- a/deepfence_frontend/packages/ui-components/src/components/input/HelperText.tsx +++ b/deepfence_frontend/packages/ui-components/src/components/input/HelperText.tsx @@ -1,34 +1,30 @@ -import cx from 'classnames'; +import { cva, VariantProps } from 'cva'; import { FC } from 'react'; import { twMerge } from 'tailwind-merge'; -export type ColorType = 'default' | 'error' | 'success'; +import { ObjectWithNonNullableValues } from '@/types/utils'; -type Props = { +const helperTextClasses = cva('leading-tight text-sm fornt-normal', { + variants: { + color: { + default: 'text-gray-500 dark:text-gray-400', + error: 'text-red-600 dark:text-red-600', + success: 'text-green-600 dark:text-green-600', + }, + }, + defaultVariants: { + color: 'default', + }, +}); + +interface Props + extends ObjectWithNonNullableValues> { text: string; - color: ColorType; className?: string; -}; - -export const classes = { - color: { - default: 'text-gray-500 dark:text-gray-400', - error: 'text-red-600 dark:text-red-600', - success: 'text-green-600 dark:text-green-600', - }, -}; +} export const HelperText: FC = ({ text, color, className }) => { - return ( -

- {text} -

- ); + return

{text}

; }; export default HelperText; diff --git a/deepfence_frontend/packages/ui-components/src/components/input/TextInput.tsx b/deepfence_frontend/packages/ui-components/src/components/input/TextInput.tsx index 5d9e6d32e0..bfc8ebb30a 100644 --- a/deepfence_frontend/packages/ui-components/src/components/input/TextInput.tsx +++ b/deepfence_frontend/packages/ui-components/src/components/input/TextInput.tsx @@ -1,118 +1,142 @@ import * as LabelPrimitive from '@radix-ui/react-label'; import cx from 'classnames'; +import { cva, VariantProps } from 'cva'; import React, { ComponentProps, forwardRef, useId } from 'react'; import { IconContext } from 'react-icons'; import { twMerge } from 'tailwind-merge'; import HelperText from '@/components/input/HelperText'; +import { ObjectWithNonNullableValues } from '@/types/utils'; export type SizeType = 'sm' | 'md' | 'lg'; export type ColorType = 'default' | 'error' | 'success'; -export interface TextInputProps - extends Omit, 'ref' | 'color' | 'className' | 'size'> { - sizing?: SizeType; - startIcon?: React.ReactNode; - endIcon?: React.ReactNode; - color?: ColorType; - label?: string; - helperText?: string; - className?: string; - required?: boolean; -} - -type IconProps = { - icon: React.ReactNode; - id?: string; - color?: ColorType; - sizing?: SizeType; - disabled?: boolean; -}; - -export const inputClasses = { - color: { - default: cx( - // ring styles - 'ring-gray-300 focus:ring-blue-600', - 'dark:ring-gray-600 dark:focus:ring-blue-600', - // bg styles - 'bg-gray-50', - 'dark:bg-gray-700', - // placeholder styles - 'placeholder-gray-500 disabled:placeholder-gray-400', - 'dark:placeholder-gray-400 dark:disabled:placeholder-gray-500', - // text styles - 'text-gray-900 disabled:text-gray-700', - 'dark:text-white dark:disabled:text-gray-200', - ), - error: cx( - // ring styles - 'ring-red-200 focus:ring-red-500', - 'dark:ring-red-800 dark:focus:ring-red-500', - // bg styles - 'bg-red-50', - 'dark:bg-gray-700', - // placeholder styles - 'placeholder-red-400 disabled:placeholder-red-300', - 'dark:placeholder-red-700 dark:disabled:placeholder-red-800', - // text styles - 'text-red-700 disabled:text-red-500', - 'dark:text-red-500 dark:disabled:text-red-700', - ), - success: cx( - // ring styles - 'ring-green-300 focus:ring-green-500', - 'dark:ring-green-800 dark:focus:ring-green-500', - // bg styles - 'bg-green-50', - 'dark:bg-gray-700', - // placeholder styles - 'placeholder-green-400 disabled:placeholder-green-300', - 'dark:placeholder-green-700 dark:disabled:placeholder-green-800', - // text styles - 'text-green-700 disabled:text-green-500', - 'dark:text-green-500 dark:disabled:text-green-700', - ), - }, - size: { - sm: `text-sm px-4 py-2`, - md: `text-sm leading-tight px-4 py-3`, - lg: `text-base px-4 py-3.5`, +const inputElementClassnames = cva( + [ + 'block w-full ring-1 rounded-lg', + 'font-normal', + 'focus:outline-none', + 'disabled:cursor-not-allowed', + ], + { + variants: { + color: { + default: [ + // ring styles + 'ring-gray-300 focus:ring-blue-600', + 'dark:ring-gray-600 dark:focus:ring-blue-600', + // bg styles + 'bg-gray-50', + 'dark:bg-gray-700', + // placeholder styles + 'placeholder-gray-500 disabled:placeholder-gray-400', + 'dark:placeholder-gray-400 dark:disabled:placeholder-gray-500', + // text styles + 'text-gray-900 disabled:text-gray-700', + 'dark:text-white dark:disabled:text-gray-200', + ], + error: [ + // ring styles + 'ring-red-200 focus:ring-red-500', + 'dark:ring-red-800 dark:focus:ring-red-500', + // bg styles + 'bg-red-50', + 'dark:bg-gray-700', + // placeholder styles + 'placeholder-red-400 disabled:placeholder-red-300', + 'dark:placeholder-red-700 dark:disabled:placeholder-red-800', + // text styles + 'text-red-700 disabled:text-red-500', + 'dark:text-red-500 dark:disabled:text-red-700', + ], + success: [ + // ring styles + 'ring-green-300 focus:ring-green-500', + 'dark:ring-green-800 dark:focus:ring-green-500', + // bg styles + 'bg-green-50', + 'dark:bg-gray-700', + // placeholder styles + 'placeholder-green-400 disabled:placeholder-green-300', + 'dark:placeholder-green-700 dark:disabled:placeholder-green-800', + // text styles + 'text-green-700 disabled:text-green-500', + 'dark:text-green-500 dark:disabled:text-green-700', + ], + }, + sizing: { + sm: `text-sm px-4 py-2`, + md: `text-sm leading-tight px-4 py-3`, + lg: `text-base px-4 py-3.5`, + }, + withStartIcon: { + true: 'pl-[42px]', + }, + withEndIcon: { + true: 'pr-[38px]', + }, + }, + compoundVariants: [ + { + sizing: 'lg', + withStartIcon: true, + className: 'pl-[48px]', + }, + ], + defaultVariants: { + color: 'default', + sizing: 'md', + withStartIcon: false, + withEndIcon: false, + }, }, -}; +); -const iconClasses = { - color: { - default: { - enabled: cx('text-gray-500', 'dark:text-gray-400'), - disabled: cx('text-gray-400', 'dark:text-gray-500'), - }, - error: { - enabled: cx('text-red-400', 'dark:text-red-700'), - disabled: cx('text-red-300', 'dark:text-red-800'), +const iconContextClasses = cva('', { + variants: { + color: { + default: ['text-gray-500', 'dark:text-gray-400'], + error: ['text-red-400', 'dark:text-red-700'], + success: ['text-green-400', 'dark:text-green-700'], }, - success: { - enabled: cx('text-green-400', 'dark:text-green-700'), - disabled: cx('text-green-300', 'dark:text-green-800'), + sizing: { + sm: `w-4 h-4`, + md: `w-4 h-4`, + lg: `w-5 h-5`, }, + disabled: { true: '' }, }, - size: { - sm: `w-4 h-4`, - md: `w-4 h-4`, - lg: `w-5 h-5`, + compoundVariants: [ + { + color: 'default', + disabled: true, + className: ['text-gray-400', 'dark:text-gray-500'], + }, + { + color: 'error', + disabled: true, + className: ['text-red-300', 'dark:text-red-800'], + }, + { + color: 'success', + disabled: true, + className: ['text-green-300', 'dark:text-green-800'], + }, + ], + defaultVariants: { + color: 'default', + sizing: 'md', + disabled: false, }, -}; +}); -const COLOR_DEFAULT = 'default'; -const SIZE_DEFAULT = 'md'; +interface IconProps + extends ObjectWithNonNullableValues> { + icon: React.ReactNode; + id?: string; +} -export const LeftIcon = ({ - icon, - id, - color = COLOR_DEFAULT, - sizing = SIZE_DEFAULT, - disabled, -}: IconProps) => { +export const LeftIcon = ({ icon, id, color, sizing, disabled }: IconProps) => { return ( {icon} @@ -134,13 +159,7 @@ export const LeftIcon = ({ ); }; -export const RightIcon = ({ - icon, - id, - color = COLOR_DEFAULT, - sizing = SIZE_DEFAULT, - disabled, -}: IconProps) => { +export const RightIcon = ({ icon, id, color, sizing, disabled }: IconProps) => { return ( {icon} @@ -162,11 +182,24 @@ export const RightIcon = ({ ); }; +export interface TextInputProps + extends Omit, 'ref' | 'color' | 'className' | 'size'>, + ObjectWithNonNullableValues< + Omit, 'withStartIcon' | 'withEndIcon'> + > { + startIcon?: React.ReactNode; + endIcon?: React.ReactNode; + label?: string; + helperText?: string; + className?: string; + required?: boolean; +} + export const TextInput = forwardRef( ( { - sizing = SIZE_DEFAULT, - color = COLOR_DEFAULT, + sizing, + color, label, disabled, startIcon, @@ -212,18 +245,12 @@ export const TextInput = forwardRef( /> )} { - it(`render with placehoder, label, onChange, startIcon, endIcon, helperText, style width`, () => { + it(`render with placehoder, label, onChange, startIcon, endIcon, helperText`, () => { const onChange = vi.fn(); const { getByTestId, getByPlaceholderText, getByText, getByLabelText } = renderUI( { onChange={onChange} label="Comments" helperText="Put your comments" - width="w-4/12" />, ); expect(getByPlaceholderText('test@email.com')).toBeInTheDocument(); @@ -27,8 +26,6 @@ describe(`Component TextInputArea`, () => { const textInputArea = getByTestId('textinputarea-id'); - expect(textInputArea).toHaveClass('w-4/12'); - // action fireEvent.change(textInputArea, { target: { value: 'I am very satisfied' } }); expect(onChange).toHaveBeenCalledTimes(1); diff --git a/deepfence_frontend/packages/ui-components/src/components/input/TextInputArea.tsx b/deepfence_frontend/packages/ui-components/src/components/input/TextInputArea.tsx index 0199f7b825..9dc140be75 100644 --- a/deepfence_frontend/packages/ui-components/src/components/input/TextInputArea.tsx +++ b/deepfence_frontend/packages/ui-components/src/components/input/TextInputArea.tsx @@ -1,46 +1,96 @@ import * as LabelPrimitive from '@radix-ui/react-label'; import cx from 'classnames'; +import { cva, VariantProps } from 'cva'; +import { isNil } from 'lodash-es'; import { ComponentProps, forwardRef, useId } from 'react'; import HelperText from '@/components/input/HelperText'; import { Typography } from '@/components/typography/Typography'; +import { ObjectWithNonNullableValues } from '@/types/utils'; -export type SizeType = 'sm' | 'md'; -export type ColorType = 'default' | 'error' | 'success'; export interface TextInputAreaProps - extends Omit, 'ref' | 'color' | 'className'> { + extends Omit, 'ref' | 'color' | 'className'>, + ObjectWithNonNullableValues< + Omit, 'isFullWidth'> + > { label?: string; - width?: string; helperText?: string; - sizing?: SizeType; - color?: ColorType; } -// TODO: use the same color classes as input -const classes = { - size: { - sm: `${Typography.size.sm} p-3`, - md: `${Typography.size.base} py-3.5 px-4`, +const inputElementClassnames = cva( + [ + 'block ring-1 rounded-lg', + 'font-normal', + 'focus:outline-none', + 'disabled:cursor-not-allowed', + ], + { + variants: { + color: { + default: [ + // ring styles + 'ring-gray-300 focus:ring-blue-600', + 'dark:ring-gray-600 dark:focus:ring-blue-600', + // bg styles + 'bg-gray-50', + 'dark:bg-gray-700', + // placeholder styles + 'placeholder-gray-500 disabled:placeholder-gray-400', + 'dark:placeholder-gray-400 dark:disabled:placeholder-gray-500', + // text styles + 'text-gray-900 disabled:text-gray-700', + 'dark:text-white dark:disabled:text-gray-200', + ], + error: [ + // ring styles + 'ring-red-200 focus:ring-red-500', + 'dark:ring-red-800 dark:focus:ring-red-500', + // bg styles + 'bg-red-50', + 'dark:bg-gray-700', + // placeholder styles + 'placeholder-red-400 disabled:placeholder-red-300', + 'dark:placeholder-red-700 dark:disabled:placeholder-red-800', + // text styles + 'text-red-700 disabled:text-red-500', + 'dark:text-red-500 dark:disabled:text-red-700', + ], + success: [ + // ring styles + 'ring-green-300 focus:ring-green-500', + 'dark:ring-green-800 dark:focus:ring-green-500', + // bg styles + 'bg-green-50', + 'dark:bg-gray-700', + // placeholder styles + 'placeholder-green-400 disabled:placeholder-green-300', + 'dark:placeholder-green-700 dark:disabled:placeholder-green-800', + // text styles + 'text-green-700 disabled:text-green-500', + 'dark:text-green-500 dark:disabled:text-green-700', + ], + }, + sizing: { + sm: `text-sm px-4 py-2`, + md: `text-sm leading-tight px-4 py-3`, + lg: `text-base px-4 py-3.5`, + }, + isFullWidth: { + true: 'w-full', + }, + }, + defaultVariants: { + color: 'default', + sizing: 'md', + isFullWidth: false, + }, }, -}; +); const COLOR_DEFAULT = 'default'; export const TextInputArea = forwardRef( - ( - { - label, - id, - sizing = 'sm', - cols, - disabled, - helperText, - color = COLOR_DEFAULT, - width = '', - ...rest - }, - ref, - ) => { + ({ label, id, sizing, cols, helperText, color = COLOR_DEFAULT, ...rest }, ref) => { const internalId = useId(); const _id = id ? id : internalId; @@ -56,20 +106,11 @@ export const TextInputArea = forwardRef )}