Created
March 27, 2024 12:59
-
-
Save sgb-io/800d8f99d434b7ca5a89bf8b6fe3ffcf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect } from "react"; | |
type NativeEventInputType = | |
| "insertText" | |
| "deleteContentBackward" | |
| "insertFromPaste" | |
| "formatBold" | |
| "deleteByCut"; | |
function isNativeEventTyping(inputType: NativeEventInputType) { | |
switch (inputType) { | |
case "deleteContentBackward": | |
case "insertFromPaste": | |
case "insertText": | |
case "formatBold": | |
case "deleteByCut": | |
return true; | |
default: | |
return false; | |
} | |
} | |
const countDecimalPlaces = (value: number) => { | |
if (!isNaN(value)) { | |
const decimalPlaces = String(value) | |
.match(/\d*\.\d*/)?.[0] | |
?.split(".")[1]; | |
if (decimalPlaces) { | |
return decimalPlaces.length; | |
} | |
} | |
return 0; | |
}; | |
function sanitizeIncrementedValue(value: number, numDecimals: number) { | |
return parseFloat(value.toFixed(numDecimals)); | |
} | |
export const SteppedNumberInput: React.FC<{ | |
step: string; | |
initialValue?: number; | |
onChange?: (value: number) => void; | |
}> = ({ step, initialValue, onChange }) => { | |
const [arrowChange, setArrowChange] = useState<number | undefined>(); | |
const [value, setValue] = useState<number | string>(initialValue ?? ""); | |
const stepAmt = parseFloat(step); | |
const minDecimals = countDecimalPlaces(parseFloat(step)); | |
// Manually apply a stepper change | |
const applyStepChange = () => { | |
if (typeof arrowChange !== "undefined") { | |
// If there is non-empty content, parse to a number | |
// If there is empty content, the input is blank, treat as 0 | |
const valueNum = | |
typeof value === "string" | |
? value === "" | |
? 0 | |
: parseFloat(value) | |
: value; | |
const wasUpwards = arrowChange > valueNum; | |
const wasDownwards = arrowChange < valueNum; | |
if (wasUpwards || wasDownwards) { | |
const change = wasUpwards ? stepAmt : -stepAmt; | |
const valueDecimals = countDecimalPlaces(valueNum); | |
const numDecimals = | |
valueDecimals > minDecimals ? valueDecimals : minDecimals; | |
const newValue = sanitizeIncrementedValue( | |
valueNum + change, | |
numDecimals | |
); | |
if (!isNaN(newValue)) { | |
setValue(newValue); | |
} | |
} | |
setArrowChange(undefined); | |
} | |
}; | |
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |
// Do not change the input value if we detect a stepper was clicked | |
// Instead, track what the value would have changed to | |
const isTypingChange = isNativeEventTyping( | |
(event.nativeEvent as any).inputType | |
); | |
if (!isTypingChange) { | |
setArrowChange(parseFloat(event.currentTarget.value)); | |
event.preventDefault(); | |
return; | |
} | |
// Update the input value as normal | |
const newValue = parseFloat(event.target.value); | |
// Allow for blank value where the user types a dash (when entering a negative value) | |
if (event.target.value === "") { | |
setValue(""); | |
return; | |
} | |
if (!isNaN(newValue)) { | |
setValue(newValue); | |
} | |
}; | |
// Trigger step changes when arrow buttons are clicked | |
const onMouseDown = () => { | |
applyStepChange(); | |
}; | |
// Trigger stepper changes when stepper click values change | |
useEffect(() => { | |
applyStepChange(); | |
}, [arrowChange, applyStepChange]); | |
// Notify parent when value changes to a valid number | |
useEffect(() => { | |
if (!onChange) { | |
return; | |
} | |
if (typeof value === "string") { | |
const parsed = parseFloat(value); | |
if (!isNaN(parsed)) { | |
onChange(parsed); | |
} | |
} | |
if (typeof value === "number") { | |
onChange(value); | |
} | |
}, [value, onChange]); | |
const onBlur = () => { | |
if (value === "" && typeof initialValue === "number") { | |
setValue(initialValue); | |
} | |
}; | |
return ( | |
<input | |
type="number" | |
step="any" | |
value={value} | |
onChange={handleInputChange} | |
onMouseDown={onMouseDown} | |
onBlur={onBlur} | |
/> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment