index.tsx 2.7 KB

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