UNPKG

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