diff --git a/packages/core/src/common/keys.ts b/packages/core/src/common/keys.ts index 5e17ce2b4f..041bdb0606 100644 --- a/packages/core/src/common/keys.ts +++ b/packages/core/src/common/keys.ts @@ -15,3 +15,8 @@ export const ARROW_UP = 38; export const ARROW_RIGHT = 39; export const ARROW_DOWN = 40; export const DELETE = 46; + +/** Returns whether the key code is `enter` or `space`, the two keys that can click a button. */ +export function isKeyboardClick(keyCode: number) { + return keyCode === ENTER || keyCode === SPACE; +} diff --git a/packages/core/src/components/button/abstractButton.tsx b/packages/core/src/components/button/abstractButton.tsx index 088353d02f..e86561c0d3 100644 --- a/packages/core/src/components/button/abstractButton.tsx +++ b/packages/core/src/components/button/abstractButton.tsx @@ -125,7 +125,7 @@ export abstract class AbstractButton> extend // argument because it is not a supertype of candidate // 'KeyboardEvent'." protected handleKeyDown = (e: React.KeyboardEvent) => { - if (isKeyboardClick(e.which)) { + if (Keys.isKeyboardClick(e.which)) { e.preventDefault(); if (e.which !== this.currentKeyDown) { this.setState({ isActive: true }); @@ -136,7 +136,7 @@ export abstract class AbstractButton> extend }; protected handleKeyUp = (e: React.KeyboardEvent) => { - if (isKeyboardClick(e.which)) { + if (Keys.isKeyboardClick(e.which)) { this.setState({ isActive: false }); this.buttonRef.click(); } @@ -159,7 +159,3 @@ export abstract class AbstractButton> extend ]; } } - -function isKeyboardClick(keyCode: number) { - return keyCode === Keys.ENTER || keyCode === Keys.SPACE; -} diff --git a/packages/core/src/components/forms/numericInput.tsx b/packages/core/src/components/forms/numericInput.tsx index e1a487416e..f9bab4c3d6 100644 --- a/packages/core/src/components/forms/numericInput.tsx +++ b/packages/core/src/components/forms/numericInput.tsx @@ -25,7 +25,16 @@ import * as Errors from "../../common/errors"; 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 { /** @@ -128,11 +137,9 @@ export interface INumericInputProps extends IIntentProps, IProps { } export interface INumericInputState { - isInputGroupFocused?: boolean; - isButtonGroupFocused?: boolean; - shouldSelectAfterUpdate?: boolean; - stepMaxPrecision?: number; - value?: string; + shouldSelectAfterUpdate: boolean; + stepMaxPrecision: number; + value: string; } enum IncrementDirection { @@ -140,6 +147,23 @@ enum IncrementDirection { UP = +1, } +const NON_HTML_PROPS = [ + "allowNumericCharactersOnly", + "buttonPosition", + "clampValueOnBlur", + "className", + "large", + "majorStepSize", + "minorStepSize", + "onButtonClick", + "onValueChange", + "selectAllOnFocus", + "selectAllOnIncrement", + "stepSize", +]; + +type ButtonEventHandlers = Required, "onKeyDown" | "onMouseDown">>; + export class NumericInput extends AbstractPureComponent { public static displayName = `${DISPLAYNAME_PREFIX}.NumericInput`; @@ -159,48 +183,28 @@ export class NumericInput extends AbstractPureComponent + const containerClasses = classNames(Classes.NUMERIC_INPUT, { [Classes.LARGE]: large }, className); + const buttons = this.renderButtons(); + return ( + + {buttonPosition === Position.LEFT && buttons} + {this.renderInput()} + {buttonPosition === Position.RIGHT && buttons} + ); - - // the strict null check here is intentional; an undefined value should - // fall back to the default button position on the right side. - if (buttonPosition === "none" || buttonPosition === null) { - // If there are no buttons, then the control group will render the - // text field with squared border-radii on the left side, causing it - // to look weird. This problem goes away if we simply don't nest within - // a control group. - return
{inputGroup}
; - } else { - const incrementButton = this.renderButton( - NumericInput.INCREMENT_KEY, - NumericInput.INCREMENT_ICON_NAME, - this.handleIncrementButtonMouseDown, - this.handleIncrementButtonKeyDown, - this.handleIncrementButtonKeyUp, - ); - const decrementButton = this.renderButton( - NumericInput.DECREMENT_KEY, - NumericInput.DECREMENT_ICON_NAME, - this.handleDecrementButtonMouseDown, - this.handleDecrementButtonKeyDown, - this.handleDecrementButtonKeyUp, - ); - - const buttonGroup = ( - - {incrementButton} - {decrementButton} - - ); - - const inputElems = buttonPosition === Position.LEFT ? [buttonGroup, inputGroup] : [inputGroup, buttonGroup]; - - const classes = classNames( - Classes.NUMERIC_INPUT, - Classes.CONTROL_GROUP, - { - [Classes.FILL]: fill, - [Classes.LARGE]: large, - }, - className, - ); - - return
{inputElems}
; - } } public componentDidUpdate() { - if (this.shouldSelectAfterUpdate) { + if (this.state.shouldSelectAfterUpdate) { this.inputElement.setSelectionRange(0, this.state.value.length); } } @@ -345,29 +274,39 @@ export class NumericInput extends AbstractPureComponent, - onKeyDown: React.KeyboardEventHandler, - onKeyUp: React.KeyboardEventHandler, - ) { + private renderButtons() { + const { intent } = this.props; + const disabled = this.props.disabled || this.props.readOnly; + return ( + +