feat/TanStack-Form (#18346)

This commit is contained in:
Wu Tianwei
2025-04-18 15:54:22 +08:00
committed by GitHub
parent efe5db38ee
commit 1e7418095f
38 changed files with 959 additions and 127 deletions

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>