1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import classNames from "classnames";
|
18 | import * as React from "react";
|
19 | import { polyfill } from "react-lifecycles-compat";
|
20 |
|
21 | import { IconName } from "@blueprintjs/icons";
|
22 |
|
23 | import {
|
24 | AbstractPureComponent2,
|
25 | Classes,
|
26 | DISPLAYNAME_PREFIX,
|
27 | HTMLInputProps,
|
28 | IntentProps,
|
29 | Intent,
|
30 | Props,
|
31 | IRef,
|
32 | Keys,
|
33 | MaybeElement,
|
34 | Position,
|
35 | refHandler,
|
36 | removeNonHTMLProps,
|
37 | setRef,
|
38 | Utils,
|
39 | } from "../../common";
|
40 | import * as Errors from "../../common/errors";
|
41 | import { ButtonGroup } from "../button/buttonGroup";
|
42 | import { Button } from "../button/buttons";
|
43 | import { ControlGroup } from "./controlGroup";
|
44 | import { InputGroup } from "./inputGroup";
|
45 | import {
|
46 | clampValue,
|
47 | getValueOrEmptyValue,
|
48 | isValidNumericKeyboardEvent,
|
49 | isValueNumeric,
|
50 | parseStringToStringNumber,
|
51 | sanitizeNumericInput,
|
52 | toLocaleString,
|
53 | toMaxPrecision,
|
54 | } from "./numericInputUtils";
|
55 |
|
56 |
|
57 | export type NumericInputProps = INumericInputProps;
|
58 |
|
59 | export interface INumericInputProps extends IntentProps, Props {
|
60 | |
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 | allowNumericCharactersOnly?: boolean;
|
67 |
|
68 | |
69 |
|
70 |
|
71 |
|
72 |
|
73 | asyncControl?: boolean;
|
74 |
|
75 | |
76 |
|
77 |
|
78 |
|
79 |
|
80 | buttonPosition?: typeof Position.LEFT | typeof Position.RIGHT | "none";
|
81 |
|
82 | |
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 | clampValueOnBlur?: boolean;
|
90 |
|
91 | |
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 | defaultValue?: number | string;
|
99 |
|
100 | |
101 |
|
102 |
|
103 |
|
104 |
|
105 | disabled?: boolean;
|
106 |
|
107 |
|
108 | fill?: boolean;
|
109 |
|
110 | |
111 |
|
112 |
|
113 | inputRef?: IRef<HTMLInputElement>;
|
114 |
|
115 | |
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 | large?: boolean;
|
123 |
|
124 | |
125 |
|
126 |
|
127 | leftIcon?: IconName | MaybeElement;
|
128 |
|
129 | |
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 | locale?: string;
|
136 |
|
137 | |
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | majorStepSize?: number | null;
|
144 |
|
145 |
|
146 | max?: number;
|
147 |
|
148 |
|
149 | min?: number;
|
150 |
|
151 | |
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | minorStepSize?: number | null;
|
158 |
|
159 |
|
160 | placeholder?: string;
|
161 |
|
162 | |
163 |
|
164 |
|
165 |
|
166 | rightElement?: JSX.Element;
|
167 |
|
168 | |
169 |
|
170 |
|
171 |
|
172 |
|
173 | selectAllOnFocus?: boolean;
|
174 |
|
175 | |
176 |
|
177 |
|
178 |
|
179 |
|
180 | selectAllOnIncrement?: boolean;
|
181 |
|
182 | |
183 |
|
184 |
|
185 |
|
186 |
|
187 | stepSize?: number;
|
188 |
|
189 | |
190 |
|
191 |
|
192 | value?: number | string;
|
193 |
|
194 |
|
195 | onButtonClick?(valueAsNumber: number, valueAsString: string): void;
|
196 |
|
197 |
|
198 | onValueChange?(valueAsNumber: number, valueAsString: string, inputElement: HTMLInputElement | null): void;
|
199 | }
|
200 |
|
201 | export interface INumericInputState {
|
202 | currentImeInputInvalid: boolean;
|
203 | prevMinProp?: number;
|
204 | prevMaxProp?: number;
|
205 | shouldSelectAfterUpdate: boolean;
|
206 | stepMaxPrecision: number;
|
207 | value: string;
|
208 | }
|
209 |
|
210 | enum IncrementDirection {
|
211 | DOWN = -1,
|
212 | UP = +1,
|
213 | }
|
214 |
|
215 | const NON_HTML_PROPS: Array<keyof NumericInputProps> = [
|
216 | "allowNumericCharactersOnly",
|
217 | "buttonPosition",
|
218 | "clampValueOnBlur",
|
219 | "className",
|
220 | "defaultValue",
|
221 | "majorStepSize",
|
222 | "minorStepSize",
|
223 | "onButtonClick",
|
224 | "onValueChange",
|
225 | "selectAllOnFocus",
|
226 | "selectAllOnIncrement",
|
227 | "stepSize",
|
228 | ];
|
229 |
|
230 | type ButtonEventHandlers = Required<Pick<React.HTMLAttributes<Element>, "onKeyDown" | "onMouseDown">>;
|
231 |
|
232 | @polyfill
|
233 | export class NumericInput extends AbstractPureComponent2<HTMLInputProps & NumericInputProps, INumericInputState> {
|
234 | public static displayName = `${DISPLAYNAME_PREFIX}.NumericInput`;
|
235 |
|
236 | public static VALUE_EMPTY = "";
|
237 |
|
238 | public static VALUE_ZERO = "0";
|
239 |
|
240 | public static defaultProps: NumericInputProps = {
|
241 | allowNumericCharactersOnly: true,
|
242 | buttonPosition: Position.RIGHT,
|
243 | clampValueOnBlur: false,
|
244 | defaultValue: NumericInput.VALUE_EMPTY,
|
245 | large: false,
|
246 | majorStepSize: 10,
|
247 | minorStepSize: 0.1,
|
248 | selectAllOnFocus: false,
|
249 | selectAllOnIncrement: false,
|
250 | stepSize: 1,
|
251 | };
|
252 |
|
253 | public static getDerivedStateFromProps(props: NumericInputProps, state: INumericInputState) {
|
254 | const nextState = {
|
255 | prevMaxProp: props.max,
|
256 | prevMinProp: props.min,
|
257 | };
|
258 |
|
259 | const didMinChange = props.min !== state.prevMinProp;
|
260 | const didMaxChange = props.max !== state.prevMaxProp;
|
261 | const didBoundsChange = didMinChange || didMaxChange;
|
262 |
|
263 |
|
264 |
|
265 | const value = props.value?.toString() ?? state.value;
|
266 | const stepMaxPrecision = NumericInput.getStepMaxPrecision(props);
|
267 |
|
268 | const sanitizedValue =
|
269 | value !== NumericInput.VALUE_EMPTY
|
270 | ? NumericInput.roundAndClampValue(value, stepMaxPrecision, props.min, props.max, 0, props.locale)
|
271 | : NumericInput.VALUE_EMPTY;
|
272 |
|
273 |
|
274 |
|
275 | if (didBoundsChange && sanitizedValue !== state.value) {
|
276 | return { ...nextState, stepMaxPrecision, value: sanitizedValue };
|
277 | }
|
278 | return { ...nextState, stepMaxPrecision, value };
|
279 | }
|
280 |
|
281 | private static CONTINUOUS_CHANGE_DELAY = 300;
|
282 |
|
283 | private static CONTINUOUS_CHANGE_INTERVAL = 100;
|
284 |
|
285 |
|
286 |
|
287 | private static getStepMaxPrecision(props: HTMLInputProps & NumericInputProps) {
|
288 | if (props.minorStepSize != null) {
|
289 | return Utils.countDecimalPlaces(props.minorStepSize);
|
290 | } else {
|
291 | return Utils.countDecimalPlaces(props.stepSize!);
|
292 | }
|
293 | }
|
294 |
|
295 | private static roundAndClampValue(
|
296 | value: string,
|
297 | stepMaxPrecision: number,
|
298 | min: number | undefined,
|
299 | max: number | undefined,
|
300 | delta = 0,
|
301 | locale: string | undefined,
|
302 | ) {
|
303 | if (!isValueNumeric(value, locale)) {
|
304 | return NumericInput.VALUE_EMPTY;
|
305 | }
|
306 | const currentValue = parseStringToStringNumber(value, locale);
|
307 | const nextValue = toMaxPrecision(Number(currentValue) + delta, stepMaxPrecision);
|
308 | const clampedValue = clampValue(nextValue, min, max);
|
309 | return toLocaleString(clampedValue, locale);
|
310 | }
|
311 |
|
312 | public state: INumericInputState = {
|
313 | currentImeInputInvalid: false,
|
314 | shouldSelectAfterUpdate: false,
|
315 | stepMaxPrecision: NumericInput.getStepMaxPrecision(this.props),
|
316 | value: getValueOrEmptyValue(this.props.value ?? this.props.defaultValue),
|
317 | };
|
318 |
|
319 |
|
320 | private didPasteEventJustOccur = false;
|
321 |
|
322 | private delta = 0;
|
323 |
|
324 | public inputElement: HTMLInputElement | null = null;
|
325 |
|
326 | private inputRef: IRef<HTMLInputElement> = refHandler(this, "inputElement", this.props.inputRef);
|
327 |
|
328 | private intervalId?: number;
|
329 |
|
330 | private incrementButtonHandlers = this.getButtonEventHandlers(IncrementDirection.UP);
|
331 |
|
332 | private decrementButtonHandlers = this.getButtonEventHandlers(IncrementDirection.DOWN);
|
333 |
|
334 | public render() {
|
335 | const { buttonPosition, className, fill, large } = this.props;
|
336 | const containerClasses = classNames(Classes.NUMERIC_INPUT, { [Classes.LARGE]: large }, className);
|
337 | const buttons = this.renderButtons();
|
338 | return (
|
339 | <ControlGroup className={containerClasses} fill={fill}>
|
340 | {buttonPosition === Position.LEFT && buttons}
|
341 | {this.renderInput()}
|
342 | {buttonPosition === Position.RIGHT && buttons}
|
343 | </ControlGroup>
|
344 | );
|
345 | }
|
346 |
|
347 | public componentDidUpdate(prevProps: NumericInputProps, prevState: INumericInputState) {
|
348 | super.componentDidUpdate(prevProps, prevState);
|
349 |
|
350 | if (prevProps.inputRef !== this.props.inputRef) {
|
351 | setRef(prevProps.inputRef, null);
|
352 | this.inputRef = refHandler(this, "inputElement", this.props.inputRef);
|
353 | setRef(this.props.inputRef, this.inputElement);
|
354 | }
|
355 |
|
356 | if (this.state.shouldSelectAfterUpdate) {
|
357 | this.inputElement?.setSelectionRange(0, this.state.value.length);
|
358 | }
|
359 |
|
360 | const didMinChange = this.props.min !== prevProps.min;
|
361 | const didMaxChange = this.props.max !== prevProps.max;
|
362 | const didBoundsChange = didMinChange || didMaxChange;
|
363 | const didLocaleChange = this.props.locale !== prevProps.locale;
|
364 | const didValueChange = this.state.value !== prevState.value;
|
365 |
|
366 | if ((didBoundsChange && didValueChange) || (didLocaleChange && prevState.value !== NumericInput.VALUE_EMPTY)) {
|
367 |
|
368 | const valueToParse = didLocaleChange ? prevState.value : this.state.value;
|
369 | const valueAsString = parseStringToStringNumber(valueToParse, prevProps.locale);
|
370 | const localizedValue = toLocaleString(+valueAsString, this.props.locale);
|
371 |
|
372 | this.props.onValueChange?.(+valueAsString, localizedValue, this.inputElement);
|
373 | }
|
374 | }
|
375 |
|
376 | protected validateProps(nextProps: HTMLInputProps & NumericInputProps) {
|
377 | const { majorStepSize, max, min, minorStepSize, stepSize, value } = nextProps;
|
378 | if (min != null && max != null && min > max) {
|
379 | console.error(Errors.NUMERIC_INPUT_MIN_MAX);
|
380 | }
|
381 | if (stepSize! <= 0) {
|
382 | console.error(Errors.NUMERIC_INPUT_STEP_SIZE_NON_POSITIVE);
|
383 | }
|
384 | if (minorStepSize && minorStepSize <= 0) {
|
385 | console.error(Errors.NUMERIC_INPUT_MINOR_STEP_SIZE_NON_POSITIVE);
|
386 | }
|
387 | if (majorStepSize && majorStepSize <= 0) {
|
388 | console.error(Errors.NUMERIC_INPUT_MAJOR_STEP_SIZE_NON_POSITIVE);
|
389 | }
|
390 | if (minorStepSize && minorStepSize > stepSize!) {
|
391 | console.error(Errors.NUMERIC_INPUT_MINOR_STEP_SIZE_BOUND);
|
392 | }
|
393 | if (majorStepSize && majorStepSize < stepSize!) {
|
394 | console.error(Errors.NUMERIC_INPUT_MAJOR_STEP_SIZE_BOUND);
|
395 | }
|
396 |
|
397 |
|
398 | if (value != null) {
|
399 | const stepMaxPrecision = NumericInput.getStepMaxPrecision(nextProps);
|
400 | const sanitizedValue = NumericInput.roundAndClampValue(
|
401 | value.toString(),
|
402 | stepMaxPrecision,
|
403 | min,
|
404 | max,
|
405 | 0,
|
406 | this.props.locale,
|
407 | );
|
408 | const valueDoesNotMatch = sanitizedValue !== value.toString();
|
409 | const localizedValue = toLocaleString(
|
410 | Number(parseStringToStringNumber(value, this.props.locale)),
|
411 | this.props.locale,
|
412 | );
|
413 | const isNotLocalized = sanitizedValue !== localizedValue;
|
414 |
|
415 | if (valueDoesNotMatch && isNotLocalized) {
|
416 | console.warn(Errors.NUMERIC_INPUT_CONTROLLED_VALUE_INVALID);
|
417 | }
|
418 | }
|
419 | }
|
420 |
|
421 |
|
422 |
|
423 |
|
424 | private renderButtons() {
|
425 | const { intent, max, min, locale } = this.props;
|
426 | const value = parseStringToStringNumber(this.state.value, locale);
|
427 | const disabled = this.props.disabled || this.props.readOnly;
|
428 | const isIncrementDisabled = max !== undefined && value !== "" && +value >= max;
|
429 | const isDecrementDisabled = min !== undefined && value !== "" && +value <= min;
|
430 |
|
431 | return (
|
432 | <ButtonGroup className={Classes.FIXED} key="button-group" vertical={true}>
|
433 | <Button
|
434 | aria-label="increment"
|
435 | disabled={disabled || isIncrementDisabled}
|
436 | icon="chevron-up"
|
437 | intent={intent}
|
438 | {...this.incrementButtonHandlers}
|
439 | />
|
440 | <Button
|
441 | aria-label="decrement"
|
442 | disabled={disabled || isDecrementDisabled}
|
443 | icon="chevron-down"
|
444 | intent={intent}
|
445 | {...this.decrementButtonHandlers}
|
446 | />
|
447 | </ButtonGroup>
|
448 | );
|
449 | }
|
450 |
|
451 | private renderInput() {
|
452 | const inputGroupHtmlProps = removeNonHTMLProps(this.props, NON_HTML_PROPS, true);
|
453 | return (
|
454 | <InputGroup
|
455 | asyncControl={this.props.asyncControl}
|
456 | autoComplete="off"
|
457 | {...inputGroupHtmlProps}
|
458 | intent={this.state.currentImeInputInvalid ? Intent.DANGER : this.props.intent}
|
459 | inputRef={this.inputRef}
|
460 | large={this.props.large}
|
461 | leftIcon={this.props.leftIcon}
|
462 | onFocus={this.handleInputFocus}
|
463 | onBlur={this.handleInputBlur}
|
464 | onChange={this.handleInputChange}
|
465 | onCompositionEnd={this.handleCompositionEnd}
|
466 | onCompositionUpdate={this.handleCompositionUpdate}
|
467 | onKeyDown={this.handleInputKeyDown}
|
468 | onKeyPress={this.handleInputKeyPress}
|
469 | onPaste={this.handleInputPaste}
|
470 | rightElement={this.props.rightElement}
|
471 | value={this.state.value}
|
472 | />
|
473 | );
|
474 | }
|
475 |
|
476 |
|
477 |
|
478 |
|
479 | private getButtonEventHandlers(direction: IncrementDirection): ButtonEventHandlers {
|
480 | return {
|
481 |
|
482 | onKeyDown: evt => {
|
483 |
|
484 | if (!this.props.disabled && Keys.isKeyboardClick(evt.keyCode)) {
|
485 | this.handleButtonClick(evt, direction);
|
486 | }
|
487 | },
|
488 | onMouseDown: evt => {
|
489 | if (!this.props.disabled) {
|
490 | this.handleButtonClick(evt, direction);
|
491 | this.startContinuousChange();
|
492 | }
|
493 | },
|
494 | };
|
495 | }
|
496 |
|
497 | private handleButtonClick = (e: React.MouseEvent | React.KeyboardEvent, direction: IncrementDirection) => {
|
498 | const delta = this.updateDelta(direction, e);
|
499 | const nextValue = this.incrementValue(delta);
|
500 | this.props.onButtonClick?.(Number(parseStringToStringNumber(nextValue, this.props.locale)), nextValue);
|
501 | };
|
502 |
|
503 | private startContinuousChange() {
|
504 |
|
505 |
|
506 |
|
507 | document.addEventListener("mouseup", this.stopContinuousChange);
|
508 |
|
509 |
|
510 |
|
511 | this.setTimeout(() => {
|
512 | this.intervalId = window.setInterval(this.handleContinuousChange, NumericInput.CONTINUOUS_CHANGE_INTERVAL);
|
513 | }, NumericInput.CONTINUOUS_CHANGE_DELAY);
|
514 | }
|
515 |
|
516 | private stopContinuousChange = () => {
|
517 | this.delta = 0;
|
518 | this.clearTimeouts();
|
519 | clearInterval(this.intervalId);
|
520 | document.removeEventListener("mouseup", this.stopContinuousChange);
|
521 | };
|
522 |
|
523 | private handleContinuousChange = () => {
|
524 |
|
525 |
|
526 |
|
527 | if (this.props.min !== undefined || this.props.max !== undefined) {
|
528 | const min = this.props.min ?? -Infinity;
|
529 | const max = this.props.max ?? Infinity;
|
530 | const valueAsNumber = Number(parseStringToStringNumber(this.state.value, this.props.locale));
|
531 | if (valueAsNumber <= min || valueAsNumber >= max) {
|
532 | this.stopContinuousChange();
|
533 | return;
|
534 | }
|
535 | }
|
536 | const nextValue = this.incrementValue(this.delta);
|
537 | this.props.onButtonClick?.(Number(parseStringToStringNumber(nextValue, this.props.locale)), nextValue);
|
538 | };
|
539 |
|
540 |
|
541 |
|
542 |
|
543 | private handleInputFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
544 |
|
545 | this.setState({ shouldSelectAfterUpdate: this.props.selectAllOnFocus! });
|
546 | this.props.onFocus?.(e);
|
547 | };
|
548 |
|
549 | private handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
550 |
|
551 | this.setState({ shouldSelectAfterUpdate: false });
|
552 |
|
553 | if (this.props.clampValueOnBlur) {
|
554 | const { value } = e.target as HTMLInputElement;
|
555 | this.handleNextValue(this.roundAndClampValue(value));
|
556 | }
|
557 |
|
558 | this.props.onBlur?.(e);
|
559 | };
|
560 |
|
561 | private handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
562 | if (this.props.disabled || this.props.readOnly) {
|
563 | return;
|
564 | }
|
565 |
|
566 |
|
567 | const { keyCode } = e;
|
568 |
|
569 | let direction: IncrementDirection | undefined;
|
570 |
|
571 | if (keyCode === Keys.ARROW_UP) {
|
572 | direction = IncrementDirection.UP;
|
573 | } else if (keyCode === Keys.ARROW_DOWN) {
|
574 | direction = IncrementDirection.DOWN;
|
575 | }
|
576 |
|
577 | if (direction !== undefined) {
|
578 |
|
579 |
|
580 |
|
581 |
|
582 |
|
583 | e.preventDefault();
|
584 |
|
585 | const delta = this.updateDelta(direction, e);
|
586 | this.incrementValue(delta);
|
587 | }
|
588 |
|
589 | this.props.onKeyDown?.(e);
|
590 | };
|
591 |
|
592 | private handleCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>) => {
|
593 | if (this.props.allowNumericCharactersOnly) {
|
594 | this.handleNextValue(sanitizeNumericInput(e.data, this.props.locale));
|
595 | this.setState({ currentImeInputInvalid: false });
|
596 | }
|
597 | };
|
598 |
|
599 | private handleCompositionUpdate = (e: React.CompositionEvent<HTMLInputElement>) => {
|
600 | if (this.props.allowNumericCharactersOnly) {
|
601 | const { data } = e;
|
602 | const sanitizedValue = sanitizeNumericInput(data, this.props.locale);
|
603 | if (sanitizedValue.length === 0 && data.length > 0) {
|
604 | this.setState({ currentImeInputInvalid: true });
|
605 | } else {
|
606 | this.setState({ currentImeInputInvalid: false });
|
607 | }
|
608 | }
|
609 | };
|
610 |
|
611 | private handleInputKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
612 |
|
613 |
|
614 | if (this.props.allowNumericCharactersOnly && !isValidNumericKeyboardEvent(e, this.props.locale)) {
|
615 | e.preventDefault();
|
616 | }
|
617 |
|
618 | this.props.onKeyPress?.(e);
|
619 | };
|
620 |
|
621 | private handleInputPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
622 | this.didPasteEventJustOccur = true;
|
623 | this.props.onPaste?.(e);
|
624 | };
|
625 |
|
626 | private handleInputChange = (e: React.FormEvent) => {
|
627 | const { value } = e.target as HTMLInputElement;
|
628 | let nextValue = value;
|
629 | if (this.props.allowNumericCharactersOnly && this.didPasteEventJustOccur) {
|
630 | this.didPasteEventJustOccur = false;
|
631 | nextValue = sanitizeNumericInput(value, this.props.locale);
|
632 | }
|
633 |
|
634 | this.handleNextValue(nextValue);
|
635 | this.setState({ shouldSelectAfterUpdate: false });
|
636 | };
|
637 |
|
638 |
|
639 |
|
640 |
|
641 | private handleNextValue(valueAsString: string) {
|
642 | if (this.props.value == null) {
|
643 | this.setState({ value: valueAsString });
|
644 | }
|
645 |
|
646 | this.props.onValueChange?.(
|
647 | Number(parseStringToStringNumber(valueAsString, this.props.locale)),
|
648 | valueAsString,
|
649 | this.inputElement,
|
650 | );
|
651 | }
|
652 |
|
653 | private incrementValue(delta: number) {
|
654 |
|
655 | const currValue = this.state.value === NumericInput.VALUE_EMPTY ? NumericInput.VALUE_ZERO : this.state.value;
|
656 | const nextValue = this.roundAndClampValue(currValue, delta);
|
657 |
|
658 | if (nextValue !== this.state.value) {
|
659 | this.handleNextValue(nextValue);
|
660 | this.setState({ shouldSelectAfterUpdate: this.props.selectAllOnIncrement! });
|
661 | }
|
662 |
|
663 |
|
664 | return nextValue;
|
665 | }
|
666 |
|
667 | private getIncrementDelta(direction: IncrementDirection, isShiftKeyPressed: boolean, isAltKeyPressed: boolean) {
|
668 | const { majorStepSize, minorStepSize, stepSize } = this.props;
|
669 |
|
670 | if (isShiftKeyPressed && majorStepSize != null) {
|
671 | return direction * majorStepSize;
|
672 | } else if (isAltKeyPressed && minorStepSize != null) {
|
673 | return direction * minorStepSize;
|
674 | } else {
|
675 | return direction * stepSize!;
|
676 | }
|
677 | }
|
678 |
|
679 | private roundAndClampValue(value: string, delta = 0) {
|
680 | return NumericInput.roundAndClampValue(
|
681 | value,
|
682 | this.state.stepMaxPrecision,
|
683 | this.props.min,
|
684 | this.props.max,
|
685 | delta,
|
686 | this.props.locale,
|
687 | );
|
688 | }
|
689 |
|
690 | private updateDelta(direction: IncrementDirection, e: React.MouseEvent | React.KeyboardEvent) {
|
691 | this.delta = this.getIncrementDelta(direction, e.shiftKey, e.altKey);
|
692 | return this.delta;
|
693 | }
|
694 | }
|