Repository: h2oai/wave
File: ui/src/spinbox.tsx (TypeScript)

Code before:
  /** Text to be displayed alongside the component. */
  label?: S
  /** The minimum value of the spinbox. Defaults to "0". */
  min?: F
  /** The maximum value of the spinbox. Defaults to "100". */
  max?: F
  /** The difference between two adjacent values of the spinbox. Defaults to "1". */
  step?: F
  /** The current value of the spinbox. Defaults to "0". */
  value?: F
  /** True if this field is disabled. */
  disabled?: B
  /** The width of the spinbox, e.g. '100px'. Defaults to '100%'. */
  width?: S
  /** True if the component should be visible. Defaults to true. */
  visible?: B
  /** True if the form should be submitted when the spinbox value changes. */
  trigger?: B
  /** An optional tooltip message displayed when a user clicks the help icon to the right of the component. */
  tooltip?: S
}
const
  DEBOUNCE_TIMEOUT = 500
export const
  XSpinbox = ({ model: { name, trigger, label, disabled, min = 0, max = 100, step = 1, value = 0 } }: { model: Spinbox }) => {
    const
      [val, setVal] = React.useState<{ val?: S }>(), // Use primitive wrapper to always force a React update.
      parseValue = (v: F) => {
        return (!isNaN(v) && isFinite(v)) ? v : value
      },
      onIncrement = (v: S) => {
        const newValue = Math.min(parseValue(Number(v) + step), max)
        wave.args[name] = newValue
        if (trigger) wave.push()
        return String(Math.round(newValue))
      },
      onDecrement = (v: S) => {
        const newValue = Math.max(parseValue(Number(v) - step), min)
        wave.args[name] = newValue
        if (trigger) wave.push()
        return String(Math.round(newValue))
      },
      handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const lastChar = e.target.value.slice(-1)
        if (lastChar === '.') return
        const
          value = parseValue(Number(e.target.value)),
          newValue = value > max
            ? max
            : value < min
              ? min

Review comment (suggestion):
I removed this section because `Math.round` was being called with the `step` as the correction factor.
If we have the step as `10` and the user wants to type `10.1` , `precisionRound` will bring it back to `10`

Code after:
  /** Text to be displayed alongside the component. */
  label?: S
  /** The minimum value of the spinbox. Defaults to "0". */
  min?: F
  /** The maximum value of the spinbox. Defaults to "100". */
  max?: F
  /** The difference between two adjacent values of the spinbox. Defaults to "1". */
  step?: F
  /** The current value of the spinbox. Defaults to "0". */
  value?: F
  /** True if this field is disabled. */
  disabled?: B
  /** The width of the spinbox, e.g. '100px'. Defaults to '100%'. */
  width?: S
  /** True if the component should be visible. Defaults to true. */
  visible?: B
  /** True if the form should be submitted when the spinbox value changes. */
  trigger?: B
  /** An optional tooltip message displayed when a user clicks the help icon to the right of the component. */
  tooltip?: S
}
const
  DEBOUNCE_TIMEOUT = 500,
  precisionRound = (value: F, precision: F) => {
    const exp = Math.pow(10, precision)
    return Math.round(value * exp) / exp
  },
  // Source: https://github.com/microsoft/fluentui/blob/ecb0e9b12665a05353f64f1b69981584c3addbc0/packages/utilities/src/math.ts#L91.
  calculatePrecision = (value: F) => {
    /**
     * Group 1:
     * [1-9]([0]+$) matches trailing zeros
     * Group 2:
     * \.([0-9]*) matches all digits after a decimal point.
     */
    const groups = /[1-9]([0]+$)|\.([0-9]*)/.exec(String(value))
    if (!groups) return 0
    return -groups[1]?.length || groups[2]?.length || 0
  }
export const
  XSpinbox = ({ model: { name, trigger, label, disabled, min = 0, max = 100, step = 1, value = 0 } }: { model: Spinbox }) => {
    const
      [val, setVal] = React.useState<{ val?: S }>(), // Use primitive wrapper to always force a React update.
      precision = Math.max(calculatePrecision(step), 0),
      parseValue = (v: F) => {
        const x = precisionRound(v, precision)
        return (!isNaN(x) && isFinite(x)) ? x : value
      },
      onIncrement = (v: S) => {
        const newValue = Math.min(parseValue(Number(v) + step), max)
        wave.args[name] = newValue