feat/TanStack-Form (#18346)

This commit is contained in:
Wu Tianwei 2025-04-18 15:54:22 +08:00 • committed by GitHub
commit 1e7418095f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 959 additions and 127 deletions

View file

@ -0,0 +1,11 @@
const IndeterminateIcon = () => {
return (
<div data-testid='indeterminate-icon'>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.5 6H9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</div>
)
}
export default IndeterminateIcon

View file

@ -1,5 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="check">
<path id="Vector 1" d="M2.5 6H9.5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 217 B

View file

@ -1,10 +0,0 @@
.mixed {
background: var(--color-components-checkbox-bg) url(./assets/mixed.svg) center center no-repeat;
background-size: 12px 12px;
border: none;
}
.checked.disabled {
background-color: #d0d5dd;
border-color: #d0d5dd;
}

View file

@ -0,0 +1,67 @@
import { fireEvent, render, screen } from '@testing-library/react'
import Checkbox from './index'
describe('Checkbox Component', () => {
const mockProps = {
id: 'test',
}
it('renders unchecked checkbox by default', () => {
render(<Checkbox {...mockProps} />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toBeInTheDocument()
expect(checkbox).not.toHaveClass('bg-components-checkbox-bg')
})
it('renders checked checkbox when checked prop is true', () => {
render(<Checkbox {...mockProps} checked />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toHaveClass('bg-components-checkbox-bg')
expect(screen.getByTestId('check-icon-test')).toBeInTheDocument()
})
it('renders indeterminate state correctly', () => {
render(<Checkbox {...mockProps} indeterminate />)
expect(screen.getByTestId('indeterminate-icon')).toBeInTheDocument()
})
it('handles click events when not disabled', () => {
const onCheck = jest.fn()
render(<Checkbox {...mockProps} onCheck={onCheck} />)
const checkbox = screen.getByTestId('checkbox-test')
fireEvent.click(checkbox)
expect(onCheck).toHaveBeenCalledTimes(1)
})
it('does not handle click events when disabled', () => {
const onCheck = jest.fn()
render(<Checkbox {...mockProps} disabled onCheck={onCheck} />)
const checkbox = screen.getByTestId('checkbox-test')
fireEvent.click(checkbox)
expect(onCheck).not.toHaveBeenCalled()
expect(checkbox).toHaveClass('cursor-not-allowed')
})
it('applies custom className when provided', () => {
const customClass = 'custom-class'
render(<Checkbox {...mockProps} className={customClass} />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toHaveClass(customClass)
})
it('applies correct styles for disabled checked state', () => {
render(<Checkbox {...mockProps} checked disabled />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toHaveClass('bg-components-checkbox-bg-disabled-checked')
expect(checkbox).toHaveClass('cursor-not-allowed')
})
it('applies correct styles for disabled unchecked state', () => {
render(<Checkbox {...mockProps} disabled />)
const checkbox = screen.getByTestId('checkbox-test')
expect(checkbox).toHaveClass('bg-components-checkbox-bg-disabled')
expect(checkbox).toHaveClass('cursor-not-allowed')
})
})

View file

@ -1,48 +1,49 @@
import { RiCheckLine } from '@remixicon/react'
import s from './index.module.css'
import cn from '@/utils/classnames'
import IndeterminateIcon from './assets/indeterminate-icon'
type CheckboxProps = {
id?: string
checked?: boolean
onCheck?: () => void
className?: string
disabled?: boolean
mixed?: boolean
indeterminate?: boolean
}
const Checkbox = ({ checked, onCheck, className, disabled, mixed }: CheckboxProps) => {
if (!checked) {
return (
<div
className={cn(
'h-4 w-4 cursor-pointer rounded-[4px] border border-components-checkbox-border bg-components-checkbox-bg-unchecked shadow-xs hover:border-components-checkbox-border-hover',
mixed ? s.mixed : 'hover:bg-components-checkbox-bg-unchecked-hover',
disabled && 'cursor-not-allowed border-components-checkbox-border-disabled bg-components-checkbox-bg-disabled hover:border-components-checkbox-border-disabled hover:bg-components-checkbox-bg-disabled',
className,
)}
onClick={() => {
if (disabled)
return
onCheck?.()
}}
></div>
)
}
const Checkbox = ({
id,
checked,
onCheck,
className,
disabled,
indeterminate,
}: CheckboxProps) => {
const checkClassName = (checked || indeterminate)
? 'bg-components-checkbox-bg text-components-checkbox-icon hover:bg-components-checkbox-bg-hover'
: 'border border-components-checkbox-border bg-components-checkbox-bg-unchecked hover:bg-components-checkbox-bg-unchecked-hover hover:border-components-checkbox-border-hover'
const disabledClassName = (checked || indeterminate)
? 'cursor-not-allowed bg-components-checkbox-bg-disabled-checked text-components-checkbox-icon-disabled hover:bg-components-checkbox-bg-disabled-checked'
: 'cursor-not-allowed border-components-checkbox-border-disabled bg-components-checkbox-bg-disabled hover:border-components-checkbox-border-disabled hover:bg-components-checkbox-bg-disabled'
return (
<div
id={id}
className={cn(
'flex h-4 w-4 cursor-pointer items-center justify-center rounded-[4px] bg-components-checkbox-bg text-components-checkbox-icon shadow-xs hover:bg-components-checkbox-bg-hover',
disabled && 'cursor-not-allowed bg-components-checkbox-bg-disabled-checked text-components-checkbox-icon-disabled hover:bg-components-checkbox-bg-disabled-checked',
'flex h-4 w-4 cursor-pointer items-center justify-center rounded-[4px] shadow-xs shadow-shadow-shadow-3',
checkClassName,
disabled && disabledClassName,
className,
)}
onClick={() => {
if (disabled)
return
onCheck?.()
}}
data-testid={`checkbox-${id}`}
>
<RiCheckLine className={cn('h-3 w-3')} />
{!checked && indeterminate && <IndeterminateIcon />}
{checked && <RiCheckLine className='h-3 w-3' data-testid={`check-icon-${id}`} />}
</div>
)
}

View file

@ -0,0 +1,43 @@
import cn from '@/utils/classnames'
import { useFieldContext } from '../..'
import Checkbox from '../../../checkbox'
type CheckboxFieldProps = {
label: string;
labelClassName?: string;
}
const CheckboxField = ({
label,
labelClassName,
}: CheckboxFieldProps) => {
const field = useFieldContext<boolean>()
return (
<div className='flex gap-2'>
<div className='flex h-6 shrink-0 items-center'>
<Checkbox
id={field.name}
checked={field.state.value}
onCheck={() => {
field.handleChange(!field.state.value)
}}
/>
</div>
<label
htmlFor={field.name}
className={cn(
'system-sm-medium grow cursor-pointer pt-1 text-text-secondary',
labelClassName,
)}
onClick={() => {
field.handleChange(!field.state.value)
}}
>
{label}
</label>
</div>
)
}
export default CheckboxField

View file

@ -0,0 +1,49 @@
import React from 'react'
import { useFieldContext } from '../..'
import Label from '../label'
import cn from '@/utils/classnames'
import type { InputNumberProps } from '../../../input-number'
import { InputNumber } from '../../../input-number'
type TextFieldProps = {
label: string
isRequired?: boolean
showOptional?: boolean
tooltip?: string
className?: string
labelClassName?: string
} & Omit<InputNumberProps, 'id' | 'value' | 'onChange' | 'onBlur'>
const NumberInputField = ({
label,
isRequired,
showOptional,
tooltip,
className,
labelClassName,
...inputProps
}: TextFieldProps) => {
const field = useFieldContext<number | undefined>()
return (
<div className={cn('flex flex-col gap-y-0.5', className)}>
<Label
htmlFor={field.name}
label={label}
isRequired={isRequired}
showOptional={showOptional}
tooltip={tooltip}
className={labelClassName}
/>
<InputNumber
id={field.name}
value={field.state.value}
onChange={value => field.handleChange(value)}
onBlur={field.handleBlur}
{...inputProps}
/>
</div>
)
}
export default NumberInputField

View file

@ -0,0 +1,34 @@
import cn from '@/utils/classnames'
import { useFieldContext } from '../..'
import Label from '../label'
import ConfigSelect from '@/app/components/app/configuration/config-var/config-select'
type OptionsFieldProps = {
label: string;
className?: string;
labelClassName?: string;
}
const OptionsField = ({
label,
className,
labelClassName,
}: OptionsFieldProps) => {
const field = useFieldContext<string[]>()
return (
<div className={cn('flex flex-col gap-y-0.5', className)}>
<Label
htmlFor={field.name}
label={label}
className={labelClassName}
/>
<ConfigSelect
options={field.state.value}
onChange={value => field.handleChange(value)}
/>
</div>
)
}
export default OptionsField

View file

@ -0,0 +1,51 @@
import cn from '@/utils/classnames'
import { useFieldContext } from '../..'
import PureSelect from '../../../select/pure'
import Label from '../label'
type SelectOption = {
value: string
label: string
}
type SelectFieldProps = {
label: string
options: SelectOption[]
isRequired?: boolean
showOptional?: boolean
tooltip?: string
className?: string
labelClassName?: string
}
const SelectField = ({
label,
options,
isRequired,
showOptional,
tooltip,
className,
labelClassName,
}: SelectFieldProps) => {
const field = useFieldContext<string>()
return (
<div className={cn('flex flex-col gap-y-0.5', className)}>
<Label
htmlFor={field.name}
label={label}
isRequired={isRequired}
showOptional={showOptional}
tooltip={tooltip}
className={labelClassName}
/>
<PureSelect
value={field.state.value}
options={options}
onChange={value => field.handleChange(value)}
/>
</div>
)
}
export default SelectField

View file

@ -0,0 +1,48 @@
import React from 'react'
import { useFieldContext } from '../..'
import Input, { type InputProps } from '../../../input'
import Label from '../label'
import cn from '@/utils/classnames'
type TextFieldProps = {
label: string
isRequired?: boolean
showOptional?: boolean
tooltip?: string
className?: string
labelClassName?: string
} & Omit<InputProps, 'className' | 'onChange' | 'onBlur' | 'value' | 'id'>
const TextField = ({
label,
isRequired,
showOptional,
tooltip,
className,
labelClassName,
...inputProps
}: TextFieldProps) => {
const field = useFieldContext<string>()
return (
<div className={cn('flex flex-col gap-y-0.5', className)}>
<Label
htmlFor={field.name}
label={label}
isRequired={isRequired}
showOptional={showOptional}
tooltip={tooltip}
className={labelClassName}
/>
<Input
id={field.name}
value={field.state.value}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
{...inputProps}
/>
</div>
)
}
export default TextField

View file

@ -0,0 +1,25 @@
import { useStore } from '@tanstack/react-form'
import { useFormContext } from '../..'
import Button, { type ButtonProps } from '../../../button'
type SubmitButtonProps = Omit<ButtonProps, 'disabled' | 'loading' | 'onClick'>
const SubmitButton = ({ ...buttonProps }: SubmitButtonProps) => {
const form = useFormContext()
const [isSubmitting, canSubmit] = useStore(form.store, state => [
state.isSubmitting,
state.canSubmit,
])
return (
<Button
disabled={isSubmitting || !canSubmit}
loading={isSubmitting}
onClick={() => form.handleSubmit()}
{...buttonProps}
/>
)
}
export default SubmitButton

View file

@ -0,0 +1,53 @@
import { fireEvent, render, screen } from '@testing-library/react'
import Label from './label'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
describe('Label Component', () => {
const defaultProps = {
htmlFor: 'test-input',
label: 'Test Label',
}
it('renders basic label correctly', () => {
render(<Label {...defaultProps} />)
const label = screen.getByTestId('label')
expect(label).toBeInTheDocument()
expect(label).toHaveAttribute('for', 'test-input')
})
it('shows optional text when showOptional is true', () => {
render(<Label {...defaultProps} showOptional />)
expect(screen.getByText('common.label.optional')).toBeInTheDocument()
})
it('shows required asterisk when isRequired is true', () => {
render(<Label {...defaultProps} isRequired />)
expect(screen.getByText('*')).toBeInTheDocument()
})
it('renders tooltip when tooltip prop is provided', () => {
const tooltipText = 'Test Tooltip'
render(<Label {...defaultProps} tooltip={tooltipText} />)
const trigger = screen.getByTestId('test-input-tooltip')
fireEvent.mouseEnter(trigger)
expect(screen.getByText(tooltipText)).toBeInTheDocument()
})
it('applies custom className when provided', () => {
const customClass = 'custom-label'
render(<Label {...defaultProps} className={customClass} />)
const label = screen.getByTestId('label')
expect(label).toHaveClass(customClass)
})
it('does not show optional text and required asterisk simultaneously', () => {
render(<Label {...defaultProps} isRequired showOptional />)
expect(screen.queryByText('common.label.optional')).not.toBeInTheDocument()
expect(screen.getByText('*')).toBeInTheDocument()
})
})

View file

@ -0,0 +1,48 @@
import cn from '@/utils/classnames'
import Tooltip from '../../tooltip'
import { useTranslation } from 'react-i18next'
export type LabelProps = {
htmlFor: string
label: string
isRequired?: boolean
showOptional?: boolean
tooltip?: string
className?: string
}
const Label = ({
htmlFor,
label,
isRequired,
showOptional,
tooltip,
className,
}: LabelProps) => {
const { t } = useTranslation()
return (
<div className='flex h-6 items-center'>
<label
data-testid='label'
htmlFor={htmlFor}
className={cn('system-sm-medium text-text-secondary', className)}
>
{label}
</label>
{!isRequired && showOptional && <div className='system-xs-regular ml-1 text-text-tertiary'>{t('common.label.optional')}</div>}
{isRequired && <div className='system-xs-regular ml-1 text-text-destructive-secondary'>*</div>}
{tooltip && (
<Tooltip
popupContent={
<div className='w-[200px]'>{tooltip}</div>
}
triggerClassName='ml-0.5 w-4 h-4'
triggerTestId={`${htmlFor}-tooltip`}
/>
)}
</div>
)
}
export default Label

View file

@ -0,0 +1,35 @@
import { withForm } from '../..'
import { demoFormOpts } from './shared-options'
import { ContactMethods } from './types'
const ContactFields = withForm({
...demoFormOpts,
render: ({ form }) => {
return (
<div className='my-2'>
<h3 className='title-lg-bold text-text-primary'>Contacts</h3>
<div className='flex flex-col gap-4'>
<form.AppField
name='contact.email'
children={field => <field.TextField label='Email' />}
/>
<form.AppField
name='contact.phone'
children={field => <field.TextField label='Phone' />}
/>
<form.AppField
name='contact.preferredContactMethod'
children={field => (
<field.SelectField
label='Preferred Contact Method'
options={ContactMethods}
/>
)}
/>
</div>
</div>
)
},
})
export default ContactFields

View file

@ -0,0 +1,68 @@
import { useStore } from '@tanstack/react-form'
import { useAppForm } from '../..'
import ContactFields from './contact-fields'
import { demoFormOpts } from './shared-options'
import { UserSchema } from './types'
const DemoForm = () => {
const form = useAppForm({
...demoFormOpts,
validators: {
onSubmit: ({ value }) => {
// Validate the entire form
const result = UserSchema.safeParse(value)
if (!result.success) {
const issues = result.error.issues
console.log('Validation errors:', issues)
return issues[0].message
}
return undefined
},
},
onSubmit: ({ value }) => {
console.log('Form submitted:', value)
},
})
const name = useStore(form.store, state => state.values.name)
return (
<form
className='flex w-[400px] flex-col gap-4'
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<form.AppField
name='name'
children={field => (
<field.TextField label='Name' />
)}
/>
<form.AppField
name='surname'
children={field => (
<field.TextField label='Surname' />
)}
/>
<form.AppField
name='isAcceptingTerms'
children={field => (
<field.CheckboxField label='I accept the terms and conditions.' />
)}
/>
{
!!name && (
<ContactFields form={form} />
)
}
<form.AppForm>
<form.SubmitButton>Submit</form.SubmitButton>
</form.AppForm>
</form>
)
}
export default DemoForm

View file

@ -0,0 +1,14 @@
import { formOptions } from '@tanstack/react-form'
export const demoFormOpts = formOptions({
defaultValues: {
name: '',
surname: '',
isAcceptingTerms: false,
contact: {
email: '',
phone: '',
preferredContactMethod: 'email',
},
},
})

View file

@ -0,0 +1,34 @@
import { z } from 'zod'
const ContactMethod = z.union([
z.literal('email'),
z.literal('phone'),
z.literal('whatsapp'),
z.literal('sms'),
])
export const ContactMethods = ContactMethod.options.map(({ value }) => ({
value,
label: value.charAt(0).toUpperCase() + value.slice(1),
}))
export const UserSchema = z.object({
name: z
.string()
.regex(/^[A-Z]/, 'Name must start with a capital letter')
.min(3, 'Name must be at least 3 characters long'),
surname: z
.string()
.min(3, 'Surname must be at least 3 characters long')
.regex(/^[A-Z]/, 'Surname must start with a capital letter'),
isAcceptingTerms: z.boolean().refine(val => val, {
message: 'You must accept the terms and conditions',
}),
contact: z.object({
email: z.string().email('Invalid email address'),
phone: z.string().optional(),
preferredContactMethod: ContactMethod,
}),
})
export type User = z.infer<typeof UserSchema>

View file

@ -0,0 +1,25 @@
import { createFormHook, createFormHookContexts } from '@tanstack/react-form'
import TextField from './components/field/text'
import NumberInputField from './components/field/number-input'
import CheckboxField from './components/field/checkbox'
import SelectField from './components/field/select'
import OptionsField from './components/field/options'
import SubmitButton from './components/form/submit-button'
export const { fieldContext, useFieldContext, formContext, useFormContext }
= createFormHookContexts()
export const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
NumberInputField,
CheckboxField,
SelectField,
OptionsField,
},
formComponents: {
SubmitButton,
},
fieldContext,
formContext,
})

View file

@ -0,0 +1,97 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { InputNumber } from './index'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
describe('InputNumber Component', () => {
const defaultProps = {
onChange: jest.fn(),
}
afterEach(() => {
jest.clearAllMocks()
})
it('renders input with default values', () => {
render(<InputNumber {...defaultProps} />)
const input = screen.getByRole('textbox')
expect(input).toBeInTheDocument()
})
it('handles increment button click', () => {
render(<InputNumber {...defaultProps} value={5} />)
const incrementBtn = screen.getByRole('button', { name: /increment/i })
fireEvent.click(incrementBtn)
expect(defaultProps.onChange).toHaveBeenCalledWith(6)
})
it('handles decrement button click', () => {
render(<InputNumber {...defaultProps} value={5} />)
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
fireEvent.click(decrementBtn)
expect(defaultProps.onChange).toHaveBeenCalledWith(4)
})
it('respects max value constraint', () => {
render(<InputNumber {...defaultProps} value={10} max={10} />)
const incrementBtn = screen.getByRole('button', { name: /increment/i })
fireEvent.click(incrementBtn)
expect(defaultProps.onChange).not.toHaveBeenCalled()
})
it('respects min value constraint', () => {
render(<InputNumber {...defaultProps} value={0} min={0} />)
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
fireEvent.click(decrementBtn)
expect(defaultProps.onChange).not.toHaveBeenCalled()
})
it('handles direct input changes', () => {
render(<InputNumber {...defaultProps} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: '42' } })
expect(defaultProps.onChange).toHaveBeenCalledWith(42)
})
it('handles empty input', () => {
render(<InputNumber {...defaultProps} value={0} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: '' } })
expect(defaultProps.onChange).toHaveBeenCalledWith(undefined)
})
it('handles invalid input', () => {
render(<InputNumber {...defaultProps} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'abc' } })
expect(defaultProps.onChange).not.toHaveBeenCalled()
})
it('displays unit when provided', () => {
const unit = 'px'
render(<InputNumber {...defaultProps} unit={unit} />)
expect(screen.getByText(unit)).toBeInTheDocument()
})
it('disables controls when disabled prop is true', () => {
render(<InputNumber {...defaultProps} disabled />)
const input = screen.getByRole('textbox')
const incrementBtn = screen.getByRole('button', { name: /increment/i })
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
expect(input).toBeDisabled()
expect(incrementBtn).toBeDisabled()
expect(decrementBtn).toBeDisabled()
})
})

View file

@ -8,7 +8,7 @@ export type InputNumberProps = {
value?: number
onChange: (value?: number) => void
amount?: number
size?: 'sm' | 'md'
size?: 'regular' | 'large'
max?: number
min?: number
defaultValue?: number
@ -19,14 +19,12 @@ export type InputNumberProps = {
} & Omit<InputProps, 'value' | 'onChange' | 'size' | 'min' | 'max' | 'defaultValue'>
export const InputNumber: FC<InputNumberProps> = (props) => {
const { unit, className, onChange, amount = 1, value, size = 'md', max, min, defaultValue, wrapClassName, controlWrapClassName, controlClassName, disabled, ...rest } = props
const { unit, className, onChange, amount = 1, value, size = 'regular', max, min, defaultValue, wrapClassName, controlWrapClassName, controlClassName, disabled, ...rest } = props
const isValidValue = (v: number) => {
if (max && v > max)
if (typeof max === 'number' && v > max)
return false
if (min && v < min)
return false
return true
return !(typeof min === 'number' && v < min)
}
const inc = () => {
@ -76,29 +74,39 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
onChange(parsed)
}}
unit={unit}
size={size}
/>
<div className={classNames(
'flex flex-col bg-components-input-bg-normal rounded-r-md border-l border-divider-subtle text-text-tertiary focus:shadow-xs',
disabled && 'opacity-50 cursor-not-allowed',
controlWrapClassName)}
>
<button onClick={inc} disabled={disabled} className={classNames(
size === 'sm' ? 'pt-1' : 'pt-1.5',
'px-1.5 hover:bg-components-input-bg-hover',
disabled && 'cursor-not-allowed hover:bg-transparent',
controlClassName,
)}>
<RiArrowUpSLine className='size-3' />
</button>
<button
onClick={dec}
type='button'
onClick={inc}
disabled={disabled}
aria-label='increment'
className={classNames(
size === 'sm' ? 'pb-1' : 'pb-1.5',
size === 'regular' ? 'pt-1' : 'pt-1.5',
'px-1.5 hover:bg-components-input-bg-hover',
disabled && 'cursor-not-allowed hover:bg-transparent',
controlClassName,
)}>
)}
>
<RiArrowUpSLine className='size-3' />
</button>
<button
type='button'
onClick={dec}
disabled={disabled}
aria-label='decrement'
className={classNames(
size === 'regular' ? 'pb-1' : 'pb-1.5',
'px-1.5 hover:bg-components-input-bg-hover',
disabled && 'cursor-not-allowed hover:bg-transparent',
controlClassName,
)}
>
<RiArrowDownSLine className='size-3' />
</button>
</div>

View file

@ -30,7 +30,7 @@ export type InputProps = {
wrapperClassName?: string
styleCss?: CSSProperties
unit?: string
} & React.InputHTMLAttributes<HTMLInputElement> & VariantProps<typeof inputVariants>
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> & VariantProps<typeof inputVariants>
const Input = ({
size,

View file

@ -54,7 +54,7 @@ const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1,
max={max}
step={step}
amount={step}
size='sm'
size='regular'
value={value}
onChange={(value) => {
onChange(id, value)

View file

@ -10,6 +10,7 @@ export type TooltipProps = {
position?: Placement
triggerMethod?: 'hover' | 'click'
triggerClassName?: string
triggerTestId?: string
disabled?: boolean
popupContent?: React.ReactNode
children?: React.ReactNode
@ -24,6 +25,7 @@ const Tooltip: FC<TooltipProps> = ({
position = 'top',
triggerMethod = 'hover',
triggerClassName,
triggerTestId,
disabled = false,
popupContent,
children,
@ -91,7 +93,7 @@ const Tooltip: FC<TooltipProps> = ({
onMouseLeave={() => triggerMethod === 'hover' && handleLeave(true)}
asChild={asChild}
>
{children || <div className={triggerClassName || 'h-3.5 w-3.5 shrink-0 p-[1px]'}><RiQuestionLine className='h-full w-full text-text-quaternary hover:text-text-tertiary' /></div>}
{children || <div data-testid={triggerTestId} className={triggerClassName || 'h-3.5 w-3.5 shrink-0 p-[1px]'}><RiQuestionLine className='h-full w-full text-text-quaternary hover:text-text-tertiary' /></div>}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent
className="z-[9999]"