Skip to content

Commit

Permalink
refactor increment/decrement logic; review changes
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabricevladimir committed Jan 30, 2024
1 parent 7907fa4 commit 6341777
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 119 deletions.
44 changes: 14 additions & 30 deletions src/components/input/inputNumber/inputNumber.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,24 +72,16 @@ describe('<InputNumber /> component', () => {
});

it('should return the maximum when the newly generated value exceeds the maximum', () => {
const max = '5';
const step = '2';
const max = 5;
const step = 2;
const value = '4';
testIncrementLogic({ max, step, value, expectedValue: max });
});

it('should increment to the minimum when the newly generated value is less than the minimum', () => {
const max = '5';
const min = '0';
const step = '2';
const value = '-7';
testIncrementLogic({ max, min, step, value, expectedValue: min });
testIncrementLogic({ max, step, value, expectedValue: max.toString() });
});

it('should increment by floating point value when the step is a float', () => {
const value = 1;
const value = '1';
const step = 0.5;
testIncrementLogic({ step, value, expectedValue: (value + step).toString() });
testIncrementLogic({ step, value, expectedValue: (Number(value) + step).toString() });
});

it('should round down to the nearest multiple of the step before incrementing by the step value', () => {
Expand All @@ -98,18 +90,11 @@ describe('<InputNumber /> component', () => {
testIncrementLogic({ step, value, expectedValue: '1.2' });
});

it('should increment to the minimum when the newly generated increment value is less than the minimum', () => {
const value = '0';
const step = '1';
const min = '5';
testIncrementLogic({ step, value, min, expectedValue: min });
});

it('should increment to the minimum when no value is provided', () => {
const step = 6;
const min = '5';
const min = 5;
const max = 10;
testIncrementLogic({ step, min, max, expectedValue: min });
testIncrementLogic({ step, min, max, expectedValue: min.toString() });
});
});

Expand Down Expand Up @@ -141,29 +126,28 @@ describe('<InputNumber /> component', () => {
};

it('should decrement by step', () => {
const value = 10;
const value = '10';
const step = 2;
const expectedValue = (10 - 2).toString();
testDecrementLogic({ value, step, expectedValue });
});

it('should not decrement when the value is less than the minimum', () => {
const value = '-1';
it('should decrement to the minimum when no value provided', () => {
const step = 2;
const min = 1;
testDecrementLogic({ value, step, min, expectedValue: value });
testDecrementLogic({ step, min, expectedValue: min.toString() });
});

it('should decrement to maximum if the initial value minus the step is greater than the maximum', () => {
const value = 100;
const value = '100';
const step = 2;
const min = 1;
const max = '50';
testDecrementLogic({ value, step, min, max, expectedValue: max });
const max = 50;
testDecrementLogic({ value, step, min, max, expectedValue: max.toString() });
});

it('should decrement to the closest multiple of the step smaller than the value', () => {
const value = 10;
const value = '10';
const step = 3;
testDecrementLogic({ value, step, expectedValue: '9' });
});
Expand Down
115 changes: 26 additions & 89 deletions src/components/input/inputNumber/inputNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import { IconType } from '../../icon';
import { useInputProps, useNumberMask, type IUseNumberMaskProps } from '../hooks';
import { InputContainer, type IInputComponentProps } from '../inputContainer';

export interface IInputNumberProps extends Omit<IInputComponentProps, 'onChange'> {
export interface IInputNumberProps extends Omit<IInputComponentProps, 'onChange' | 'value' | 'step' | 'min' | 'max'> {
min?: number;
max?: number;
step?: number;
/**
* The value of the number input.
*/
value?: string | number;
/**
* Optional string appended to the input value.
*/
Expand All @@ -18,23 +25,21 @@ export interface IInputNumberProps extends Omit<IInputComponentProps, 'onChange'
}

export const InputNumber: React.FC<IInputNumberProps> = (props) => {
const { suffix, onChange, ...otherProps } = props;
const {
suffix,
onChange,
max = Number.MAX_SAFE_INTEGER,
min = Number.MIN_SAFE_INTEGER,
step: inputStep = 1,
value,
...otherProps
} = props;

const step = inputStep <= 0 ? 1 : inputStep;
const { containerProps, inputProps } = useInputProps(otherProps);

const { className: containerClassName, isDisabled, ...otherContainerProps } = containerProps;
const {
max: inputMax,
min: inputMin,
step: inputStep,
value: inputValue,
onBlur,
onFocus,
onKeyDown,
className: inputClassName,
...otherInputProps
} = inputProps;

const { step, min, max, value } = parseInputs(inputValue, inputStep, inputMin, inputMax);
const { onBlur, onFocus, onKeyDown, className: inputClassName, ...otherInputProps } = inputProps;

const [isFocused, setIsFocused] = useState(false);
const {
Expand Down Expand Up @@ -74,63 +79,32 @@ export const InputNumber: React.FC<IInputNumberProps> = (props) => {
const handleIncrement = () => {
const parsedValue = Number(unmaskedValue ?? 0);

// return the input value if it's bigger than the max
if (parsedValue > max) {
setValue(parsedValue.toString());
return;
}

// increment directly to the minimum if value is less than the minimum
if (parsedValue < min) {
setValue(min.toString());
return;
}

// round down to the nearest multiple of the step when the value
// is not already a multiple of the step
let newValue = parsedValue + step;
if (parsedValue % step !== 0) {
newValue = Math.floor(parsedValue / step) * step;
}
// ensure value is multiple of step
const newValue = (Math.floor(parsedValue / step) + 1) * step;

// increment value with step if it's smaller than the initial value
if (newValue < parsedValue) {
newValue += step;
}

// ensure the new value is within the min and max range
newValue = Math.max(min, Math.min(max, newValue));
setValue(newValue.toString());
// ensure the new value is than the
setValue(Math.min(max, newValue).toString());
};

const handleDecrement = () => {
const parsedValue = Number(unmaskedValue ?? 0);

// if the current value is less than the min, don't decrement
if (parsedValue < min) {
setValue(parsedValue.toString());
}

// decrement value by the step
let newValue = parsedValue - step;

// if the value is not a multiple of the step,
// decrement to the biggest multiple of the step that is less than the value
if (parsedValue % step !== 0) {
newValue = Math.floor(parsedValue / step) * step;
}
let newValue = parsedValue % step !== 0 ? Math.floor(parsedValue / step) * step : parsedValue - step;

// ensure the new value is within the min and max range
newValue = Math.max(min, Math.min(max, newValue));
setValue(newValue.toString());
};

return (
<InputContainer
className={classNames('relative', containerClassName)}
{...otherContainerProps}
isDisabled={isDisabled}
>
<InputContainer className={containerClassName} {...otherContainerProps} isDisabled={isDisabled}>
{!isDisabled && (
<Button
size="sm"
Expand All @@ -148,7 +122,6 @@ export const InputNumber: React.FC<IInputNumberProps> = (props) => {
value={isFocused ? maskedValue : suffixedValue}
onBlur={handleBlur}
onFocus={handleFocus}
disabled={isDisabled}
inputMode="numeric"
onKeyDown={handleKeyDown}
className={classNames('text-center spin-buttons:appearance-none', inputClassName)}
Expand All @@ -166,39 +139,3 @@ export const InputNumber: React.FC<IInputNumberProps> = (props) => {
</InputContainer>
);
};

/**
* Parses the input values and returns an object with the parsed values.
*
* @param value - The value of the number input. Defaults to '0'.
* @param min - The minimum value of the number input. Defaults to Number.MIN_SAFE_INTEGER.
* @param max - The maximum value of the number input. Defaults to Number.MAX_SAFE_INTEGER.
* @returns An object with the parsed values.
*/
function parseInputs(
value: IInputNumberProps['value'] = '',
step: IInputNumberProps['step'] = 1,
min: IInputNumberProps['min'] = Number.MIN_SAFE_INTEGER,
max: IInputNumberProps['max'] = Number.MAX_SAFE_INTEGER,
) {
let parsedMax = Number(max);
if (isNaN(parsedMax)) {
parsedMax = Number.MAX_SAFE_INTEGER;
}

let parsedMin = Number(min);
if (isNaN(parsedMin)) {
parsedMin = Number.MIN_SAFE_INTEGER;
}

// ignore all values less zero one and use the default step value of one
let parsedStep = Number(step);
if (isNaN(parsedStep) || parsedStep <= 0) {
parsedStep = 1;
}

let parsedValue: IInputNumberProps['value'] = Number(value);
parsedValue = isNaN(parsedValue) ? '' : value;

return { step: parsedStep, min: parsedMin, max: parsedMax, value: parsedValue };
}

0 comments on commit 6341777

Please sign in to comment.