Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

index.tsx 3.1KB

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