UNPKG

21.9 kBJavaScriptView Raw
1/*
2 * Copyright 2017 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16import { __assign, __extends } from "tslib";
17import classNames from "classnames";
18import * as React from "react";
19import { ChevronDown, ChevronUp } from "@blueprintjs/icons";
20import { AbstractPureComponent, Classes, DISPLAYNAME_PREFIX, Intent, Position, refHandler, removeNonHTMLProps, setRef, Utils, } from "../../common";
21import * as Errors from "../../common/errors";
22import { ButtonGroup } from "../button/buttonGroup";
23import { Button } from "../button/buttons";
24import { ControlGroup } from "./controlGroup";
25import { InputGroup } from "./inputGroup";
26import { clampValue, getValueOrEmptyValue, isValidNumericKeyboardEvent, isValueNumeric, parseStringToStringNumber, sanitizeNumericInput, toLocaleString, toMaxPrecision, } from "./numericInputUtils";
27var IncrementDirection;
28(function (IncrementDirection) {
29 IncrementDirection[IncrementDirection["DOWN"] = -1] = "DOWN";
30 IncrementDirection[IncrementDirection["UP"] = 1] = "UP";
31})(IncrementDirection || (IncrementDirection = {}));
32var NON_HTML_PROPS = [
33 "allowNumericCharactersOnly",
34 "buttonPosition",
35 "clampValueOnBlur",
36 "className",
37 "defaultValue",
38 "majorStepSize",
39 "minorStepSize",
40 "onButtonClick",
41 "onValueChange",
42 "selectAllOnFocus",
43 "selectAllOnIncrement",
44 "stepSize",
45];
46/**
47 * Numeric input component.
48 *
49 * @see https://blueprintjs.com/docs/#core/components/numeric-input
50 */
51var NumericInput = /** @class */ (function (_super) {
52 __extends(NumericInput, _super);
53 function NumericInput() {
54 var _this = this;
55 var _a;
56 _this = _super.apply(this, arguments) || this;
57 _this.numericInputId = Utils.uniqueId("numericInput");
58 _this.state = {
59 currentImeInputInvalid: false,
60 shouldSelectAfterUpdate: false,
61 stepMaxPrecision: NumericInput.getStepMaxPrecision(_this.props),
62 value: getValueOrEmptyValue((_a = _this.props.value) !== null && _a !== void 0 ? _a : _this.props.defaultValue),
63 };
64 // updating these flags need not trigger re-renders, so don't include them in this.state.
65 _this.didPasteEventJustOccur = false;
66 _this.delta = 0;
67 _this.inputElement = null;
68 _this.inputRef = refHandler(_this, "inputElement", _this.props.inputRef);
69 _this.incrementButtonHandlers = _this.getButtonEventHandlers(IncrementDirection.UP);
70 _this.decrementButtonHandlers = _this.getButtonEventHandlers(IncrementDirection.DOWN);
71 _this.getCurrentValueAsNumber = function () { return Number(parseStringToStringNumber(_this.state.value, _this.props.locale)); };
72 _this.handleButtonClick = function (e, direction) {
73 var _a, _b;
74 var delta = _this.updateDelta(direction, e);
75 var nextValue = _this.incrementValue(delta);
76 (_b = (_a = _this.props).onButtonClick) === null || _b === void 0 ? void 0 : _b.call(_a, Number(parseStringToStringNumber(nextValue, _this.props.locale)), nextValue);
77 };
78 _this.stopContinuousChange = function () {
79 _this.delta = 0;
80 _this.clearTimeouts();
81 clearInterval(_this.intervalId);
82 document.removeEventListener("mouseup", _this.stopContinuousChange);
83 };
84 _this.handleContinuousChange = function () {
85 var _a, _b, _c, _d;
86 // If either min or max prop is set, when reaching the limit
87 // the button will be disabled and stopContinuousChange will be never fired,
88 // hence the need to check on each iteration to properly clear the timeout
89 if (_this.props.min !== undefined || _this.props.max !== undefined) {
90 var min = (_a = _this.props.min) !== null && _a !== void 0 ? _a : -Infinity;
91 var max = (_b = _this.props.max) !== null && _b !== void 0 ? _b : Infinity;
92 var valueAsNumber = _this.getCurrentValueAsNumber();
93 if (valueAsNumber <= min || valueAsNumber >= max) {
94 _this.stopContinuousChange();
95 return;
96 }
97 }
98 var nextValue = _this.incrementValue(_this.delta);
99 (_d = (_c = _this.props).onButtonClick) === null || _d === void 0 ? void 0 : _d.call(_c, Number(parseStringToStringNumber(nextValue, _this.props.locale)), nextValue);
100 };
101 // Callbacks - Input
102 // =================
103 _this.handleInputFocus = function (e) {
104 var _a, _b;
105 // update this state flag to trigger update for input selection (see componentDidUpdate)
106 _this.setState({ shouldSelectAfterUpdate: _this.props.selectAllOnFocus });
107 (_b = (_a = _this.props).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, e);
108 };
109 _this.handleInputBlur = function (e) {
110 var _a, _b;
111 // always disable this flag on blur so it's ready for next time.
112 _this.setState({ shouldSelectAfterUpdate: false });
113 if (_this.props.clampValueOnBlur) {
114 var value = e.target.value;
115 _this.handleNextValue(_this.roundAndClampValue(value));
116 }
117 (_b = (_a = _this.props).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, e);
118 };
119 _this.handleInputKeyDown = function (e) {
120 var _a, _b;
121 if (_this.props.disabled || _this.props.readOnly) {
122 return;
123 }
124 var direction;
125 if (e.key === "ArrowUp") {
126 direction = IncrementDirection.UP;
127 }
128 else if (e.key === "ArrowDown") {
129 direction = IncrementDirection.DOWN;
130 }
131 if (direction !== undefined) {
132 // when the input field has focus, some key combinations will modify
133 // the field's selection range. we'll actually want to select all
134 // text in the field after we modify the value on the following
135 // lines. preventing the default selection behavior lets us do that
136 // without interference.
137 e.preventDefault();
138 var delta = _this.updateDelta(direction, e);
139 _this.incrementValue(delta);
140 }
141 (_b = (_a = _this.props).onKeyDown) === null || _b === void 0 ? void 0 : _b.call(_a, e);
142 };
143 _this.handleCompositionEnd = function (e) {
144 if (_this.props.allowNumericCharactersOnly) {
145 _this.handleNextValue(sanitizeNumericInput(e.data, _this.props.locale));
146 _this.setState({ currentImeInputInvalid: false });
147 }
148 };
149 _this.handleCompositionUpdate = function (e) {
150 if (_this.props.allowNumericCharactersOnly) {
151 var data = e.data;
152 var sanitizedValue = sanitizeNumericInput(data, _this.props.locale);
153 if (sanitizedValue.length === 0 && data.length > 0) {
154 _this.setState({ currentImeInputInvalid: true });
155 }
156 else {
157 _this.setState({ currentImeInputInvalid: false });
158 }
159 }
160 };
161 _this.handleInputKeyPress = function (e) {
162 var _a, _b;
163 // we prohibit keystrokes in onKeyPress instead of onKeyDown, because
164 // e.key is not trustworthy in onKeyDown in all browsers.
165 if (_this.props.allowNumericCharactersOnly && !isValidNumericKeyboardEvent(e, _this.props.locale)) {
166 e.preventDefault();
167 }
168 // eslint-disable-next-line deprecation/deprecation
169 (_b = (_a = _this.props).onKeyPress) === null || _b === void 0 ? void 0 : _b.call(_a, e);
170 };
171 _this.handleInputPaste = function (e) {
172 var _a, _b;
173 _this.didPasteEventJustOccur = true;
174 (_b = (_a = _this.props).onPaste) === null || _b === void 0 ? void 0 : _b.call(_a, e);
175 };
176 _this.handleInputChange = function (value) {
177 var nextValue = value;
178 if (_this.props.allowNumericCharactersOnly && _this.didPasteEventJustOccur) {
179 _this.didPasteEventJustOccur = false;
180 nextValue = sanitizeNumericInput(value, _this.props.locale);
181 }
182 _this.handleNextValue(nextValue);
183 _this.setState({ shouldSelectAfterUpdate: false });
184 };
185 return _this;
186 }
187 NumericInput.getDerivedStateFromProps = function (props, state) {
188 var _a, _b;
189 var nextState = {
190 prevMaxProp: props.max,
191 prevMinProp: props.min,
192 };
193 var didMinChange = props.min !== state.prevMinProp;
194 var didMaxChange = props.max !== state.prevMaxProp;
195 var didBoundsChange = didMinChange || didMaxChange;
196 // in controlled mode, use props.value
197 // in uncontrolled mode, if state.value has not been assigned yet (upon initial mount), use props.defaultValue
198 var value = (_b = (_a = props.value) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : state.value;
199 var stepMaxPrecision = NumericInput.getStepMaxPrecision(props);
200 var sanitizedValue = value !== NumericInput.VALUE_EMPTY
201 ? NumericInput.roundAndClampValue(value, stepMaxPrecision, props.min, props.max, 0, props.locale)
202 : NumericInput.VALUE_EMPTY;
203 // if a new min and max were provided that cause the existing value to fall
204 // outside of the new bounds, then clamp the value to the new valid range.
205 if (didBoundsChange && sanitizedValue !== state.value) {
206 return __assign(__assign({}, nextState), { stepMaxPrecision: stepMaxPrecision, value: sanitizedValue });
207 }
208 return __assign(__assign({}, nextState), { stepMaxPrecision: stepMaxPrecision, value: value });
209 };
210 // Value Helpers
211 // =============
212 NumericInput.getStepMaxPrecision = function (props) {
213 if (props.minorStepSize != null) {
214 return Utils.countDecimalPlaces(props.minorStepSize);
215 }
216 else {
217 return Utils.countDecimalPlaces(props.stepSize);
218 }
219 };
220 NumericInput.roundAndClampValue = function (value, stepMaxPrecision, min, max, delta, locale) {
221 if (delta === void 0) { delta = 0; }
222 if (!isValueNumeric(value, locale)) {
223 return NumericInput.VALUE_EMPTY;
224 }
225 var currentValue = parseStringToStringNumber(value, locale);
226 var nextValue = toMaxPrecision(Number(currentValue) + delta, stepMaxPrecision);
227 var clampedValue = clampValue(nextValue, min, max);
228 return toLocaleString(clampedValue, locale);
229 };
230 NumericInput.prototype.render = function () {
231 var _a;
232 var _b = this.props, buttonPosition = _b.buttonPosition, className = _b.className, fill = _b.fill, large = _b.large, small = _b.small;
233 var containerClasses = classNames(Classes.NUMERIC_INPUT, (_a = {}, _a[Classes.LARGE] = large, _a[Classes.SMALL] = small, _a), className);
234 var buttons = this.renderButtons();
235 return (React.createElement(ControlGroup, { className: containerClasses, fill: fill },
236 buttonPosition === Position.LEFT && buttons,
237 this.renderInput(),
238 buttonPosition === Position.RIGHT && buttons));
239 };
240 NumericInput.prototype.componentDidUpdate = function (prevProps, prevState) {
241 var _a, _b, _c;
242 _super.prototype.componentDidUpdate.call(this, prevProps, prevState);
243 if (prevProps.inputRef !== this.props.inputRef) {
244 setRef(prevProps.inputRef, null);
245 this.inputRef = refHandler(this, "inputElement", this.props.inputRef);
246 setRef(this.props.inputRef, this.inputElement);
247 }
248 if (this.state.shouldSelectAfterUpdate) {
249 (_a = this.inputElement) === null || _a === void 0 ? void 0 : _a.setSelectionRange(0, this.state.value.length);
250 }
251 var didMinChange = this.props.min !== prevProps.min;
252 var didMaxChange = this.props.max !== prevProps.max;
253 var didBoundsChange = didMinChange || didMaxChange;
254 var didLocaleChange = this.props.locale !== prevProps.locale;
255 var didValueChange = this.state.value !== prevState.value;
256 if ((didBoundsChange && didValueChange) || (didLocaleChange && prevState.value !== NumericInput.VALUE_EMPTY)) {
257 // we clamped the value due to a bounds change, so we should fire the change callback
258 var valueToParse = didLocaleChange ? prevState.value : this.state.value;
259 var valueAsString = parseStringToStringNumber(valueToParse, prevProps.locale);
260 var localizedValue = toLocaleString(+valueAsString, this.props.locale);
261 (_c = (_b = this.props).onValueChange) === null || _c === void 0 ? void 0 : _c.call(_b, +valueAsString, localizedValue, this.inputElement);
262 }
263 };
264 NumericInput.prototype.validateProps = function (nextProps) {
265 var majorStepSize = nextProps.majorStepSize, max = nextProps.max, min = nextProps.min, minorStepSize = nextProps.minorStepSize, stepSize = nextProps.stepSize, value = nextProps.value;
266 if (min != null && max != null && min > max) {
267 console.error(Errors.NUMERIC_INPUT_MIN_MAX);
268 }
269 if (stepSize <= 0) {
270 console.error(Errors.NUMERIC_INPUT_STEP_SIZE_NON_POSITIVE);
271 }
272 if (minorStepSize && minorStepSize <= 0) {
273 console.error(Errors.NUMERIC_INPUT_MINOR_STEP_SIZE_NON_POSITIVE);
274 }
275 if (majorStepSize && majorStepSize <= 0) {
276 console.error(Errors.NUMERIC_INPUT_MAJOR_STEP_SIZE_NON_POSITIVE);
277 }
278 if (minorStepSize && minorStepSize > stepSize) {
279 console.error(Errors.NUMERIC_INPUT_MINOR_STEP_SIZE_BOUND);
280 }
281 if (majorStepSize && majorStepSize < stepSize) {
282 console.error(Errors.NUMERIC_INPUT_MAJOR_STEP_SIZE_BOUND);
283 }
284 // controlled mode
285 if (value != null) {
286 var stepMaxPrecision = NumericInput.getStepMaxPrecision(nextProps);
287 var sanitizedValue = NumericInput.roundAndClampValue(value.toString(), stepMaxPrecision, min, max, 0, this.props.locale);
288 var valueDoesNotMatch = sanitizedValue !== value.toString();
289 var localizedValue = toLocaleString(Number(parseStringToStringNumber(value, this.props.locale)), this.props.locale);
290 var isNotLocalized = sanitizedValue !== localizedValue;
291 if (valueDoesNotMatch && isNotLocalized) {
292 console.warn(Errors.NUMERIC_INPUT_CONTROLLED_VALUE_INVALID);
293 }
294 }
295 };
296 // Render Helpers
297 // ==============
298 NumericInput.prototype.renderButtons = function () {
299 var _a = this.props, intent = _a.intent, max = _a.max, min = _a.min, locale = _a.locale;
300 var value = parseStringToStringNumber(this.state.value, locale);
301 var disabled = this.props.disabled || this.props.readOnly;
302 var isIncrementDisabled = max !== undefined && value !== "" && +value >= max;
303 var isDecrementDisabled = min !== undefined && value !== "" && +value <= min;
304 return (React.createElement(ButtonGroup, { className: Classes.FIXED, key: "button-group", vertical: true },
305 React.createElement(Button, __assign({ "aria-label": "increment", "aria-controls": this.numericInputId, disabled: disabled || isIncrementDisabled, icon: React.createElement(ChevronUp, null), intent: intent }, this.incrementButtonHandlers)),
306 React.createElement(Button, __assign({ "aria-label": "decrement", "aria-controls": this.numericInputId, disabled: disabled || isDecrementDisabled, icon: React.createElement(ChevronDown, null), intent: intent }, this.decrementButtonHandlers))));
307 };
308 NumericInput.prototype.renderInput = function () {
309 var inputGroupHtmlProps = removeNonHTMLProps(this.props, NON_HTML_PROPS, true);
310 var valueAsNumber = this.getCurrentValueAsNumber();
311 return (React.createElement(InputGroup, __assign({ asyncControl: this.props.asyncControl, autoComplete: "off", id: this.numericInputId, role: this.props.allowNumericCharactersOnly ? "spinbutton" : undefined }, inputGroupHtmlProps, { "aria-valuemax": this.props.max, "aria-valuemin": this.props.min, "aria-valuenow": valueAsNumber, intent: this.state.currentImeInputInvalid ? Intent.DANGER : this.props.intent, inputClassName: this.props.inputClassName, inputRef: this.inputRef, large: this.props.large, leftElement: this.props.leftElement, leftIcon: this.props.leftIcon, onFocus: this.handleInputFocus, onBlur: this.handleInputBlur, onCompositionEnd: this.handleCompositionEnd, onCompositionUpdate: this.handleCompositionUpdate, onKeyDown: this.handleInputKeyDown, onKeyPress: this.handleInputKeyPress, onPaste: this.handleInputPaste, onValueChange: this.handleInputChange, rightElement: this.props.rightElement, small: this.props.small, value: this.state.value })));
312 };
313 // Callbacks - Buttons
314 // ===================
315 NumericInput.prototype.getButtonEventHandlers = function (direction) {
316 var _this = this;
317 return {
318 // keydown is fired repeatedly when held so it's implicitly continuous
319 onKeyDown: function (evt) {
320 if (!_this.props.disabled && Utils.isKeyboardClick(evt)) {
321 _this.handleButtonClick(evt, direction);
322 }
323 },
324 onMouseDown: function (evt) {
325 if (!_this.props.disabled) {
326 _this.handleButtonClick(evt, direction);
327 _this.startContinuousChange();
328 }
329 },
330 };
331 };
332 NumericInput.prototype.startContinuousChange = function () {
333 var _this = this;
334 // The button's onMouseUp event handler doesn't fire if the user
335 // releases outside of the button, so we need to watch all the way
336 // from the top.
337 document.addEventListener("mouseup", this.stopContinuousChange);
338 // Initial delay is slightly longer to prevent the user from
339 // accidentally triggering the continuous increment/decrement.
340 this.setTimeout(function () {
341 _this.intervalId = window.setInterval(_this.handleContinuousChange, NumericInput.CONTINUOUS_CHANGE_INTERVAL);
342 }, NumericInput.CONTINUOUS_CHANGE_DELAY);
343 };
344 // Data logic
345 // ==========
346 NumericInput.prototype.handleNextValue = function (valueAsString) {
347 var _a, _b;
348 if (this.props.value == null) {
349 this.setState({ value: valueAsString });
350 }
351 (_b = (_a = this.props).onValueChange) === null || _b === void 0 ? void 0 : _b.call(_a, Number(parseStringToStringNumber(valueAsString, this.props.locale)), valueAsString, this.inputElement);
352 };
353 NumericInput.prototype.incrementValue = function (delta) {
354 // pretend we're incrementing from 0 if currValue is empty
355 var currValue = this.state.value === NumericInput.VALUE_EMPTY ? NumericInput.VALUE_ZERO : this.state.value;
356 var nextValue = this.roundAndClampValue(currValue, delta);
357 if (nextValue !== this.state.value) {
358 this.handleNextValue(nextValue);
359 this.setState({ shouldSelectAfterUpdate: this.props.selectAllOnIncrement });
360 }
361 // return value used in continuous change updates
362 return nextValue;
363 };
364 NumericInput.prototype.getIncrementDelta = function (direction, isShiftKeyPressed, isAltKeyPressed) {
365 var _a = this.props, majorStepSize = _a.majorStepSize, minorStepSize = _a.minorStepSize, stepSize = _a.stepSize;
366 if (isShiftKeyPressed && majorStepSize != null) {
367 return direction * majorStepSize;
368 }
369 else if (isAltKeyPressed && minorStepSize != null) {
370 return direction * minorStepSize;
371 }
372 else {
373 return direction * stepSize;
374 }
375 };
376 NumericInput.prototype.roundAndClampValue = function (value, delta) {
377 if (delta === void 0) { delta = 0; }
378 return NumericInput.roundAndClampValue(value, this.state.stepMaxPrecision, this.props.min, this.props.max, delta, this.props.locale);
379 };
380 NumericInput.prototype.updateDelta = function (direction, e) {
381 this.delta = this.getIncrementDelta(direction, e.shiftKey, e.altKey);
382 return this.delta;
383 };
384 NumericInput.displayName = "".concat(DISPLAYNAME_PREFIX, ".NumericInput");
385 NumericInput.VALUE_EMPTY = "";
386 NumericInput.VALUE_ZERO = "0";
387 NumericInput.defaultProps = {
388 allowNumericCharactersOnly: true,
389 buttonPosition: Position.RIGHT,
390 clampValueOnBlur: false,
391 defaultValue: NumericInput.VALUE_EMPTY,
392 large: false,
393 majorStepSize: 10,
394 minorStepSize: 0.1,
395 selectAllOnFocus: false,
396 selectAllOnIncrement: false,
397 small: false,
398 stepSize: 1,
399 };
400 NumericInput.CONTINUOUS_CHANGE_DELAY = 300;
401 NumericInput.CONTINUOUS_CHANGE_INTERVAL = 100;
402 return NumericInput;
403}(AbstractPureComponent));
404export { NumericInput };
405//# sourceMappingURL=numericInput.js.map
\No newline at end of file