dify
This commit is contained in:
124
dify/web/app/components/base/input/index.spec.tsx
Normal file
124
dify/web/app/components/base/input/index.spec.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import Input, { inputVariants } from './index'
|
||||
|
||||
// Mock the i18n hook
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'common.operation.search': 'Search',
|
||||
'common.placeholder.input': 'Please input',
|
||||
}
|
||||
return translations[key] || ''
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Input component', () => {
|
||||
describe('Variants', () => {
|
||||
it('should return correct classes for regular size', () => {
|
||||
const result = inputVariants({ size: 'regular' })
|
||||
expect(result).toContain('px-3')
|
||||
expect(result).toContain('radius-md')
|
||||
expect(result).toContain('system-sm-regular')
|
||||
})
|
||||
|
||||
it('should return correct classes for large size', () => {
|
||||
const result = inputVariants({ size: 'large' })
|
||||
expect(result).toContain('px-4')
|
||||
expect(result).toContain('radius-lg')
|
||||
expect(result).toContain('system-md-regular')
|
||||
})
|
||||
|
||||
it('should use regular size as default', () => {
|
||||
const result = inputVariants({})
|
||||
expect(result).toContain('px-3')
|
||||
expect(result).toContain('radius-md')
|
||||
expect(result).toContain('system-sm-regular')
|
||||
})
|
||||
})
|
||||
|
||||
it('renders correctly with default props', () => {
|
||||
render(<Input />)
|
||||
const input = screen.getByPlaceholderText('Please input')
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).not.toBeDisabled()
|
||||
expect(input).not.toHaveClass('cursor-not-allowed')
|
||||
})
|
||||
|
||||
it('shows left icon when showLeftIcon is true', () => {
|
||||
render(<Input showLeftIcon />)
|
||||
const searchIcon = document.querySelector('svg')
|
||||
expect(searchIcon).toBeInTheDocument()
|
||||
const input = screen.getByPlaceholderText('Search')
|
||||
expect(input).toHaveClass('pl-[26px]')
|
||||
})
|
||||
|
||||
it('shows clear icon when showClearIcon is true and has value', () => {
|
||||
render(<Input showClearIcon value="test" />)
|
||||
const clearIcon = document.querySelector('.group svg')
|
||||
expect(clearIcon).toBeInTheDocument()
|
||||
const input = screen.getByDisplayValue('test')
|
||||
expect(input).toHaveClass('pr-[26px]')
|
||||
})
|
||||
|
||||
it('does not show clear icon when disabled, even with value', () => {
|
||||
render(<Input showClearIcon value="test" disabled />)
|
||||
const clearIcon = document.querySelector('.group svg')
|
||||
expect(clearIcon).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onClear when clear icon is clicked', () => {
|
||||
const onClear = jest.fn()
|
||||
render(<Input showClearIcon value="test" onClear={onClear} />)
|
||||
const clearIconContainer = document.querySelector('.group')
|
||||
fireEvent.click(clearIconContainer!)
|
||||
expect(onClear).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('shows warning icon when destructive is true', () => {
|
||||
render(<Input destructive />)
|
||||
const warningIcon = document.querySelector('svg')
|
||||
expect(warningIcon).toBeInTheDocument()
|
||||
const input = screen.getByPlaceholderText('Please input')
|
||||
expect(input).toHaveClass('border-components-input-border-destructive')
|
||||
})
|
||||
|
||||
it('applies disabled styles when disabled', () => {
|
||||
render(<Input disabled />)
|
||||
const input = screen.getByPlaceholderText('Please input')
|
||||
expect(input).toBeDisabled()
|
||||
expect(input).toHaveClass('cursor-not-allowed')
|
||||
expect(input).toHaveClass('bg-components-input-bg-disabled')
|
||||
})
|
||||
|
||||
it('displays custom unit when provided', () => {
|
||||
render(<Input unit="km" />)
|
||||
const unitElement = screen.getByText('km')
|
||||
expect(unitElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies custom className and style', () => {
|
||||
const customClass = 'test-class'
|
||||
const customStyle = { color: 'red' }
|
||||
render(<Input className={customClass} styleCss={customStyle} />)
|
||||
const input = screen.getByPlaceholderText('Please input')
|
||||
expect(input).toHaveClass(customClass)
|
||||
expect(input).toHaveStyle('color: red')
|
||||
})
|
||||
|
||||
it('applies large size variant correctly', () => {
|
||||
render(<Input size={'large' as any} />)
|
||||
const input = screen.getByPlaceholderText('Please input')
|
||||
expect(input.className).toContain(inputVariants({ size: 'large' }))
|
||||
})
|
||||
|
||||
it('uses custom placeholder when provided', () => {
|
||||
const placeholder = 'Custom placeholder'
|
||||
render(<Input placeholder={placeholder} />)
|
||||
const input = screen.getByPlaceholderText(placeholder)
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
424
dify/web/app/components/base/input/index.stories.tsx
Normal file
424
dify/web/app/components/base/input/index.stories.tsx
Normal file
@@ -0,0 +1,424 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import { useState } from 'react'
|
||||
import Input from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Data Entry/Input',
|
||||
component: Input,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Input component with support for icons, clear button, validation states, and units. Includes automatic leading zero removal for number inputs.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['regular', 'large'],
|
||||
description: 'Input size',
|
||||
},
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['text', 'number', 'email', 'password', 'url', 'tel'],
|
||||
description: 'Input type',
|
||||
},
|
||||
placeholder: {
|
||||
control: 'text',
|
||||
description: 'Placeholder text',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Disabled state',
|
||||
},
|
||||
destructive: {
|
||||
control: 'boolean',
|
||||
description: 'Error/destructive state',
|
||||
},
|
||||
showLeftIcon: {
|
||||
control: 'boolean',
|
||||
description: 'Show search icon on left',
|
||||
},
|
||||
showClearIcon: {
|
||||
control: 'boolean',
|
||||
description: 'Show clear button when input has value',
|
||||
},
|
||||
unit: {
|
||||
control: 'text',
|
||||
description: 'Unit text displayed on right (e.g., "px", "ms")',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Input>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Interactive demo wrapper
|
||||
const InputDemo = (args: any) => {
|
||||
const [value, setValue] = useState(args.value || '')
|
||||
|
||||
return (
|
||||
<div style={{ width: '400px' }}>
|
||||
<Input
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
console.log('Input changed:', e.target.value)
|
||||
}}
|
||||
onClear={() => {
|
||||
setValue('')
|
||||
console.log('Input cleared')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Default state
|
||||
export const Default: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
placeholder: 'Enter text...',
|
||||
type: 'text',
|
||||
},
|
||||
}
|
||||
|
||||
// Large size
|
||||
export const LargeSize: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'large',
|
||||
placeholder: 'Enter text...',
|
||||
type: 'text',
|
||||
},
|
||||
}
|
||||
|
||||
// With search icon
|
||||
export const WithSearchIcon: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
showLeftIcon: true,
|
||||
placeholder: 'Search...',
|
||||
type: 'text',
|
||||
},
|
||||
}
|
||||
|
||||
// With clear button
|
||||
export const WithClearButton: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
showClearIcon: true,
|
||||
value: 'Some text to clear',
|
||||
placeholder: 'Type something...',
|
||||
type: 'text',
|
||||
},
|
||||
}
|
||||
|
||||
// Search input (icon + clear)
|
||||
export const SearchInput: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
showLeftIcon: true,
|
||||
showClearIcon: true,
|
||||
value: '',
|
||||
placeholder: 'Search...',
|
||||
type: 'text',
|
||||
},
|
||||
}
|
||||
|
||||
// Disabled state
|
||||
export const Disabled: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
value: 'Disabled input',
|
||||
disabled: true,
|
||||
type: 'text',
|
||||
},
|
||||
}
|
||||
|
||||
// Destructive/error state
|
||||
export const DestructiveState: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
value: 'invalid@email',
|
||||
destructive: true,
|
||||
placeholder: 'Enter email...',
|
||||
type: 'email',
|
||||
},
|
||||
}
|
||||
|
||||
// Number input
|
||||
export const NumberInput: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
type: 'number',
|
||||
placeholder: 'Enter a number...',
|
||||
value: '0',
|
||||
},
|
||||
}
|
||||
|
||||
// With unit
|
||||
export const WithUnit: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
type: 'number',
|
||||
value: '100',
|
||||
unit: 'px',
|
||||
placeholder: 'Enter value...',
|
||||
},
|
||||
}
|
||||
|
||||
// Email input
|
||||
export const EmailInput: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
type: 'email',
|
||||
placeholder: 'Enter your email...',
|
||||
showClearIcon: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Password input
|
||||
export const PasswordInput: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
type: 'password',
|
||||
placeholder: 'Enter password...',
|
||||
value: 'secret123',
|
||||
},
|
||||
}
|
||||
|
||||
// Size comparison
|
||||
const SizeComparisonDemo = () => {
|
||||
const [regularValue, setRegularValue] = useState('')
|
||||
const [largeValue, setLargeValue] = useState('')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6" style={{ width: '400px' }}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Regular Size</label>
|
||||
<Input
|
||||
size="regular"
|
||||
value={regularValue}
|
||||
onChange={e => setRegularValue(e.target.value)}
|
||||
placeholder="Regular input..."
|
||||
showClearIcon
|
||||
onClear={() => setRegularValue('')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Large Size</label>
|
||||
<Input
|
||||
size="large"
|
||||
value={largeValue}
|
||||
onChange={e => setLargeValue(e.target.value)}
|
||||
placeholder="Large input..."
|
||||
showClearIcon
|
||||
onClear={() => setLargeValue('')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SizeComparison: Story = {
|
||||
render: () => <SizeComparisonDemo />,
|
||||
}
|
||||
|
||||
// State comparison
|
||||
const StateComparisonDemo = () => {
|
||||
const [normalValue, setNormalValue] = useState('Normal state')
|
||||
const [errorValue, setErrorValue] = useState('Error state')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6" style={{ width: '400px' }}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Normal</label>
|
||||
<Input
|
||||
value={normalValue}
|
||||
onChange={e => setNormalValue(e.target.value)}
|
||||
showClearIcon
|
||||
onClear={() => setNormalValue('')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Destructive</label>
|
||||
<Input
|
||||
value={errorValue}
|
||||
onChange={e => setErrorValue(e.target.value)}
|
||||
destructive
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Disabled</label>
|
||||
<Input
|
||||
value="Disabled input"
|
||||
onChange={() => undefined}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StateComparison: Story = {
|
||||
render: () => <StateComparisonDemo />,
|
||||
}
|
||||
|
||||
// Form example
|
||||
const FormExampleDemo = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
age: '',
|
||||
website: '',
|
||||
})
|
||||
const [errors, setErrors] = useState({
|
||||
email: false,
|
||||
age: false,
|
||||
})
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: '500px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold">User Profile</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Name</label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Enter your name..."
|
||||
showClearIcon
|
||||
onClear={() => setFormData({ ...formData, name: '' })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Email</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, email: e.target.value })
|
||||
setErrors({ ...errors, email: e.target.value ? !validateEmail(e.target.value) : false })
|
||||
}}
|
||||
placeholder="Enter your email..."
|
||||
destructive={errors.email}
|
||||
showClearIcon
|
||||
onClear={() => {
|
||||
setFormData({ ...formData, email: '' })
|
||||
setErrors({ ...errors, email: false })
|
||||
}}
|
||||
/>
|
||||
{errors.email && (
|
||||
<span className="text-xs text-red-600">Please enter a valid email address</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Age</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.age}
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, age: e.target.value })
|
||||
setErrors({ ...errors, age: e.target.value ? Number(e.target.value) < 18 : false })
|
||||
}}
|
||||
placeholder="Enter your age..."
|
||||
destructive={errors.age}
|
||||
unit="years"
|
||||
/>
|
||||
{errors.age && (
|
||||
<span className="text-xs text-red-600">Must be 18 or older</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Website</label>
|
||||
<Input
|
||||
type="url"
|
||||
value={formData.website}
|
||||
onChange={e => setFormData({ ...formData, website: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
showClearIcon
|
||||
onClear={() => setFormData({ ...formData, website: '' })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FormExample: Story = {
|
||||
render: () => <FormExampleDemo />,
|
||||
}
|
||||
|
||||
// Search example
|
||||
const SearchExampleDemo = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape']
|
||||
const filteredItems = items.filter(item =>
|
||||
item.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ width: '400px' }} className="flex flex-col gap-4">
|
||||
<Input
|
||||
size="large"
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
onClear={() => setSearchQuery('')}
|
||||
placeholder="Search fruits..."
|
||||
/>
|
||||
{searchQuery && (
|
||||
<div className="rounded-lg bg-gray-50 p-4">
|
||||
<div className="mb-2 text-xs text-gray-500">
|
||||
{filteredItems.length} result{filteredItems.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{filteredItems.map(item => (
|
||||
<div key={item} className="text-sm text-gray-700">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SearchExample: Story = {
|
||||
render: () => <SearchExampleDemo />,
|
||||
}
|
||||
|
||||
// Interactive playground
|
||||
export const Playground: Story = {
|
||||
render: args => <InputDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
type: 'text',
|
||||
placeholder: 'Type something...',
|
||||
disabled: false,
|
||||
destructive: false,
|
||||
showLeftIcon: false,
|
||||
showClearIcon: true,
|
||||
unit: '',
|
||||
},
|
||||
}
|
||||
140
dify/web/app/components/base/input/index.tsx
Normal file
140
dify/web/app/components/base/input/index.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiCloseCircleFill, RiErrorWarningLine, RiSearchLine } from '@remixicon/react'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import { noop } from 'lodash-es'
|
||||
import type { CSSProperties, ChangeEventHandler, FocusEventHandler } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CopyFeedbackNew } from '../copy-feedback'
|
||||
|
||||
export const inputVariants = cva(
|
||||
'',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
regular: 'px-3 radius-md system-sm-regular',
|
||||
large: 'px-4 radius-lg system-md-regular',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'regular',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type InputProps = {
|
||||
showLeftIcon?: boolean
|
||||
showClearIcon?: boolean
|
||||
showCopyIcon?: boolean
|
||||
onClear?: () => void
|
||||
disabled?: boolean
|
||||
destructive?: boolean
|
||||
wrapperClassName?: string
|
||||
styleCss?: CSSProperties
|
||||
unit?: string
|
||||
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> & VariantProps<typeof inputVariants>
|
||||
|
||||
const removeLeadingZeros = (value: string) => value.replace(/^(-?)0+(?=\d)/, '$1')
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(({
|
||||
size,
|
||||
disabled,
|
||||
destructive,
|
||||
showLeftIcon,
|
||||
showClearIcon,
|
||||
showCopyIcon,
|
||||
onClear,
|
||||
wrapperClassName,
|
||||
className,
|
||||
styleCss,
|
||||
value,
|
||||
placeholder,
|
||||
onChange = noop,
|
||||
onBlur = noop,
|
||||
unit,
|
||||
...props
|
||||
}, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const handleNumberChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (value === 0) {
|
||||
// remove leading zeros
|
||||
const formattedValue = removeLeadingZeros(e.target.value)
|
||||
if (e.target.value !== formattedValue)
|
||||
e.target.value = formattedValue
|
||||
}
|
||||
onChange(e)
|
||||
}
|
||||
const handleNumberBlur: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
// remove leading zeros
|
||||
const formattedValue = removeLeadingZeros(e.target.value)
|
||||
if (e.target.value !== formattedValue) {
|
||||
e.target.value = formattedValue
|
||||
onChange({
|
||||
...e,
|
||||
type: 'change',
|
||||
target: {
|
||||
...e.target,
|
||||
value: formattedValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
onBlur(e)
|
||||
}
|
||||
return (
|
||||
<div className={cn('relative w-full', wrapperClassName)}>
|
||||
{showLeftIcon && <RiSearchLine className={cn('absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-components-input-text-placeholder')} />}
|
||||
<input
|
||||
ref={ref}
|
||||
style={styleCss}
|
||||
className={cn(
|
||||
'w-full appearance-none border border-transparent bg-components-input-bg-normal py-[7px] text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs',
|
||||
inputVariants({ size }),
|
||||
showLeftIcon && 'pl-[26px]',
|
||||
showLeftIcon && size === 'large' && 'pl-7',
|
||||
showClearIcon && value && 'pr-[26px]',
|
||||
showClearIcon && value && size === 'large' && 'pr-7',
|
||||
(destructive || showCopyIcon) && 'pr-[26px]',
|
||||
(destructive || showCopyIcon) && size === 'large' && 'pr-7',
|
||||
disabled && 'cursor-not-allowed border-transparent bg-components-input-bg-disabled text-components-input-text-filled-disabled hover:border-transparent hover:bg-components-input-bg-disabled',
|
||||
destructive && 'border-components-input-border-destructive bg-components-input-bg-destructive text-components-input-text-filled hover:border-components-input-border-destructive hover:bg-components-input-bg-destructive focus:border-components-input-border-destructive focus:bg-components-input-bg-destructive',
|
||||
className,
|
||||
)}
|
||||
placeholder={placeholder ?? (showLeftIcon
|
||||
? (t('common.operation.search') || '')
|
||||
: (t('common.placeholder.input') || ''))}
|
||||
value={value}
|
||||
onChange={props.type === 'number' ? handleNumberChange : onChange}
|
||||
onBlur={props.type === 'number' ? handleNumberBlur : onBlur}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
{showClearIcon && value && !disabled && !destructive && (
|
||||
<div className={cn('group absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer p-[1px]')} onClick={onClear}>
|
||||
<RiCloseCircleFill className='h-3.5 w-3.5 cursor-pointer text-text-quaternary group-hover:text-text-tertiary' />
|
||||
</div>
|
||||
)}
|
||||
{destructive && (
|
||||
<RiErrorWarningLine className='absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-text-destructive-secondary' />
|
||||
)}
|
||||
{showCopyIcon && (
|
||||
<div className={cn('group absolute right-0 top-1/2 -translate-y-1/2 cursor-pointer')}>
|
||||
<CopyFeedbackNew
|
||||
content={String(value ?? '')}
|
||||
className='!h-7 !w-7 hover:bg-transparent'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
unit && (
|
||||
<div className='system-sm-regular absolute right-2 top-1/2 -translate-y-1/2 text-text-tertiary'>
|
||||
{unit}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export default Input
|
||||
Reference in New Issue
Block a user