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

Code before:
  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) => {
        // https://stackoverflow.com/questions/588004/is-floating-point-math-broken/51723472#51723472
        const newValue = Math.min(Number((parseFloat(v) + parseFloat(String(step))).toFixed(10)), max)
        wave.args[name] = newValue
        if (trigger) wave.push()
        return String(newValue)
      },
      onDecrement = (v: S) => {
        const newValue = Math.max(Number((parseFloat(v) - parseFloat(String(step))).toFixed(10)), min)
        wave.args[name] = newValue
        if (trigger) wave.push()
        return String(newValue)
      },
      handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        // If user entered "." or 0 as the last number after "." then don't parse
        if (/\.$|\.\d*0+$/.test(e.target.value)) return
        const
          value = parseValue(Number(e.target.value)),
          newValue = value > max
            ? max
            : value < min
              ? min
              : value
        wave.args[name] = newValue
        if (trigger) wave.push()
        setVal({ val: String(newValue) })
      },
      debouncedHandleOnchange = debounce(DEBOUNCE_TIMEOUT, handleOnChange),
      onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        e.persist()
        trigger ? debouncedHandleOnchange(e) : handleOnChange(e)
      }
    // eslint-disable-next-line react-hooks/exhaustive-deps
    React.useEffect(() => { wave.args[name] = (value < min) ? min : ((value > max) ? max : value) }, [])

    return (
      <Fluent.SpinButton
        inputProps={{ 'data-test': name } as React.InputHTMLAttributes<HTMLInputElement>}
        label={label}
        onChange={onChange}
        min={min}

Review comment (suggestion):
We were converting the string to a number for each key pressed including `.`
For example: `11.` -> `11` and the user couldn't type `11.1 .`

To solve this I included a check which uses a regex to skip the string -> number parsing:

Code after:
  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
        if (trigger) wave.push()
        setVal({ val: String(newValue) })
        return String(newValue)
      },
      onDecrement = (v: S) => {
        const newValue = Math.max(parseValue(Number(v) - step), min)
        wave.args[name] = newValue
        if (trigger) wave.push()
        setVal({ val: String(newValue) })
        return String(newValue)
      },
      onBlur = () => setVal({ ...val }), //HACK: Fluent bug in v7, remove after upgrade.
      handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const
          val = e.target.value,
          isLastCharDotOrTraillingZero = /\.$|\.\d*0+$/,
          value = parseValue(Number(val)),
          newValue = value > max
            ? max