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