index.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import type { FC } from 'react'
  2. import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
  3. import Input, { type InputProps } from '../input'
  4. import classNames from '@/utils/classnames'
  5. export type InputNumberProps = {
  6. unit?: string
  7. value?: number
  8. onChange: (value?: number) => void
  9. amount?: number
  10. size?: 'sm' | 'md'
  11. max?: number
  12. min?: number
  13. defaultValue?: number
  14. disabled?: boolean
  15. wrapClassName?: string
  16. controlWrapClassName?: string
  17. controlClassName?: string
  18. } & Omit<InputProps, 'value' | 'onChange' | 'size' | 'min' | 'max' | 'defaultValue'>
  19. export const InputNumber: FC<InputNumberProps> = (props) => {
  20. const { unit, className, onChange, amount = 1, value, size = 'md', max, min, defaultValue, wrapClassName, controlWrapClassName, controlClassName, disabled, ...rest } = props
  21. const isValidValue = (v: number) => {
  22. if (max && v > max)
  23. return false
  24. if (min && v < min)
  25. return false
  26. return true
  27. }
  28. const inc = () => {
  29. if (disabled) return
  30. if (value === undefined) {
  31. onChange(defaultValue)
  32. return
  33. }
  34. const newValue = value + amount
  35. if (!isValidValue(newValue))
  36. return
  37. onChange(newValue)
  38. }
  39. const dec = () => {
  40. if (disabled) return
  41. if (value === undefined) {
  42. onChange(defaultValue)
  43. return
  44. }
  45. const newValue = value - amount
  46. if (!isValidValue(newValue))
  47. return
  48. onChange(newValue)
  49. }
  50. return <div className={classNames('flex', wrapClassName)}>
  51. <Input {...rest}
  52. // disable default controller
  53. type='text'
  54. className={classNames('rounded-r-none', className)}
  55. value={value}
  56. max={max}
  57. min={min}
  58. disabled={disabled}
  59. onChange={(e) => {
  60. if (e.target.value === '')
  61. onChange(undefined)
  62. const parsed = Number(e.target.value)
  63. if (Number.isNaN(parsed))
  64. return
  65. if (!isValidValue(parsed))
  66. return
  67. onChange(parsed)
  68. }}
  69. unit={unit}
  70. />
  71. <div className={classNames(
  72. 'flex flex-col bg-components-input-bg-normal rounded-r-md border-l border-divider-subtle text-text-tertiary focus:shadow-xs',
  73. disabled && 'opacity-50 cursor-not-allowed',
  74. controlWrapClassName)}
  75. >
  76. <button onClick={inc} disabled={disabled} className={classNames(
  77. size === 'sm' ? 'pt-1' : 'pt-1.5',
  78. 'px-1.5 hover:bg-components-input-bg-hover',
  79. disabled && 'cursor-not-allowed hover:bg-transparent',
  80. controlClassName,
  81. )}>
  82. <RiArrowUpSLine className='size-3' />
  83. </button>
  84. <button
  85. onClick={dec}
  86. disabled={disabled}
  87. className={classNames(
  88. size === 'sm' ? 'pb-1' : 'pb-1.5',
  89. 'px-1.5 hover:bg-components-input-bg-hover',
  90. disabled && 'cursor-not-allowed hover:bg-transparent',
  91. controlClassName,
  92. )}>
  93. <RiArrowDownSLine className='size-3' />
  94. </button>
  95. </div>
  96. </div>
  97. }