diff --git a/packages/core/src/components/forms/numericInput.tsx b/packages/core/src/components/forms/numericInput.tsx index be859615f1..cc3d1eddd6 100644 --- a/packages/core/src/components/forms/numericInput.tsx +++ b/packages/core/src/components/forms/numericInput.tsx @@ -27,6 +27,14 @@ import { ButtonGroup } from "../button/buttonGroup"; import { Button } from "../button/buttons"; import { ControlGroup } from "./controlGroup"; import { InputGroup } from "./inputGroup"; +import { + clampValue, + getValueOrEmptyValue, + isFloatingPointNumericCharacter, + isValidNumericKeyboardEvent, + isValueNumeric, + toMaxPrecision, +} from "./numericInputUtils"; export interface INumericInputProps extends IIntentProps, IProps { /** @@ -175,19 +183,6 @@ export class NumericInput extends AbstractPureComponent { // we prohibit keystrokes in onKeyPress instead of onKeyDown, because // e.key is not trustworthy in onKeyDown in all browsers. - if (this.props.allowNumericCharactersOnly && this.isKeyboardEventDisabledForBasicNumericEntry(e)) { + if (this.props.allowNumericCharactersOnly && !isValidNumericKeyboardEvent(e)) { e.preventDefault(); } @@ -447,7 +442,7 @@ export class NumericInput extends AbstractPureComponent= 0; - } - - private isKeyboardEventDisabledForBasicNumericEntry(e: React.KeyboardEvent) { - // unit tests may not include e.key. don't bother disabling those events. - if (e.key == null) { - return false; - } - - // allow modified key strokes that may involve letters and other - // non-numeric/invalid characters (Cmd + A, Cmd + C, Cmd + V, Cmd + X). - if (e.ctrlKey || e.altKey || e.metaKey) { - return false; - } - - // keys that print a single character when pressed have a `key` name of - // length 1. every other key has a longer `key` name (e.g. "Backspace", - // "ArrowUp", "Shift"). since none of those keys can print a character - // to the field--and since they may have important native behaviors - // beyond printing a character--we don't want to disable their effects. - const isSingleCharKey = e.key.length === 1; - if (!isSingleCharKey) { - return false; - } - - // now we can simply check that the single character that wants to be printed - // is a floating-point number character that we're allowed to print. - return !this.isFloatingPointNumericCharacter(e.key); - } - - private isFloatingPointNumericCharacter(character: string) { - return NumericInput.FLOATING_POINT_NUMBER_CHARACTER_REGEX.test(character); + const nextValue = toMaxPrecision(parseFloat(value) + delta, this.state.stepMaxPrecision); + return clampValue(nextValue, min, max).toString(); } private getStepMaxPrecision(props: HTMLInputProps & INumericInputProps) { @@ -556,14 +498,6 @@ export class NumericInput extends AbstractPureComponent= 0; +} + +export function isValidNumericKeyboardEvent(e: React.KeyboardEvent) { + // unit tests may not include e.key. don't bother disabling those events. + if (e.key == null) { + return true; + } + + // allow modified key strokes that may involve letters and other + // non-numeric/invalid characters (Cmd + A, Cmd + C, Cmd + V, Cmd + X). + if (e.ctrlKey || e.altKey || e.metaKey) { + return true; + } + + // keys that print a single character when pressed have a `key` name of + // length 1. every other key has a longer `key` name (e.g. "Backspace", + // "ArrowUp", "Shift"). since none of those keys can print a character + // to the field--and since they may have important native behaviors + // beyond printing a character--we don't want to disable their effects. + const isSingleCharKey = e.key.length === 1; + if (!isSingleCharKey) { + return true; + } + + // now we can simply check that the single character that wants to be printed + // is a floating-point number character that we're allowed to print. + return isFloatingPointNumericCharacter(e.key); +} + +/** + * A regex that matches a string of length 1 (i.e. a standalone character) + * if and only if it is a floating-point number character as defined by W3C: + * https://www.w3.org/TR/2012/WD-html-markup-20120329/datatypes.html#common.data.float + * + * Floating-point number characters are the only characters that can be + * printed within a default input[type="number"]. This component should + * behave the same way when this.props.allowNumericCharactersOnly = true. + * See here for the input[type="number"].value spec: + * https://www.w3.org/TR/2012/WD-html-markup-20120329/input.number.html#input.number.attrs.value + */ +const FLOATING_POINT_NUMBER_CHARACTER_REGEX = /^[Ee0-9\+\-\.]$/; +export function isFloatingPointNumericCharacter(character: string) { + return FLOATING_POINT_NUMBER_CHARACTER_REGEX.test(character); +} + +/** + * Round the value to have _up to_ the specified maximum precision. + * + * This differs from `toFixed(5)` in that trailing zeroes are not added on + * more precise values, resulting in shorter strings. + */ +export function toMaxPrecision(value: number, maxPrecision: number) { + // round the value to have the specified maximum precision (toFixed is the wrong choice, + // because it would show trailing zeros in the decimal part out to the specified precision) + // source: http://stackoverflow.com/a/18358056/5199574 + const scaleFactor = Math.pow(10, maxPrecision); + return Math.round(value * scaleFactor) / scaleFactor; +}