Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

index.tsx 3.3KB

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