diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3307d44..b93759e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Fixed
- Update `` and `` core components to truncate long strings
+- Fix `` core component to trigger `onChange` callback on + / - button click
## [1.0.50] - 2024-10-23
diff --git a/src/core/components/forms/hooks/useNumberMask.test.ts b/src/core/components/forms/hooks/useNumberMask.test.ts
index 3c1203b5..cb182704 100644
--- a/src/core/components/forms/hooks/useNumberMask.test.ts
+++ b/src/core/components/forms/hooks/useNumberMask.test.ts
@@ -12,7 +12,7 @@ describe('useNumberMask hook', () => {
const formatNumberMock = jest.spyOn(formatterUtils, 'formatNumber');
beforeEach(() => {
- const maskResult = { setValue: jest.fn() } as unknown as IUseNumberMaskResult;
+ const maskResult = { setUnmaskedValue: jest.fn() } as unknown as IUseNumberMaskResult;
maskMock.mockReturnValue(maskResult);
});
@@ -22,7 +22,7 @@ describe('useNumberMask hook', () => {
});
it('returns the result of the useIMask hook', () => {
- const maskResult = { setValue: jest.fn(), setUnmaskedValue: jest.fn() } as unknown as IUseNumberMaskResult;
+ const maskResult = { setUnmaskedValue: jest.fn() } as unknown as IUseNumberMaskResult;
maskMock.mockReturnValue(maskResult);
const { result } = renderHook(() => useNumberMask({}));
expect(result.current).toEqual(maskResult);
@@ -77,18 +77,18 @@ describe('useNumberMask hook', () => {
expect(maskMock).toHaveBeenCalledWith(expect.objectContaining({ mask: `num ${suffix}` }), expect.anything());
});
- it('updates the mask value on value property change for controlled inputs', () => {
+ it('updates the unmasked value on value property change for controlled inputs', () => {
const value = '100';
- const setValue = jest.fn();
- const maskResult = { setValue } as unknown as IUseNumberMaskResult;
+ const setUnmaskedValue = jest.fn();
+ const maskResult = { setUnmaskedValue } as unknown as IUseNumberMaskResult;
maskMock.mockReturnValue(maskResult);
const { rerender } = renderHook((props) => useNumberMask(props), { initialProps: { value } });
- expect(setValue).toHaveBeenCalledWith(value);
+ expect(setUnmaskedValue).toHaveBeenCalledWith(value);
- const newValue = '101';
- rerender({ value: newValue });
- expect(setValue).toHaveBeenCalledWith(newValue);
+ const unmaskedValue = '101';
+ rerender({ value: unmaskedValue });
+ expect(setUnmaskedValue).toHaveBeenCalledWith(unmaskedValue);
});
it('calls the onChange property with the unmasked value when value is valid', () => {
diff --git a/src/core/components/forms/hooks/useNumberMask.ts b/src/core/components/forms/hooks/useNumberMask.ts
index 51dd7188..0f3cf3cf 100644
--- a/src/core/components/forms/hooks/useNumberMask.ts
+++ b/src/core/components/forms/hooks/useNumberMask.ts
@@ -62,13 +62,13 @@ export const useNumberMask = (props: IUseNumberMaskProps): IUseNumberMaskResult
{ onAccept: handleMaskAccept },
);
- const { setValue } = result;
+ const { setUnmaskedValue } = result;
// Update the masked value on value property change
useEffect(() => {
const parsedValue = value?.toString() ?? '';
- setValue(parsedValue);
- }, [setValue, value]);
+ setUnmaskedValue(parsedValue);
+ }, [setUnmaskedValue, value]);
return result;
};
diff --git a/src/core/components/forms/inputNumber/inputNumber.stories.tsx b/src/core/components/forms/inputNumber/inputNumber.stories.tsx
index 4164fcf3..a312a226 100644
--- a/src/core/components/forms/inputNumber/inputNumber.stories.tsx
+++ b/src/core/components/forms/inputNumber/inputNumber.stories.tsx
@@ -35,4 +35,15 @@ export const Controlled: Story = {
},
};
+/**
+ * Usage example with min and max values.
+ */
+export const MinMax: Story = {
+ args: {
+ min: 10,
+ max: 20,
+ placeholder: '0',
+ },
+};
+
export default meta;
diff --git a/src/core/components/forms/inputNumber/inputNumber.test.tsx b/src/core/components/forms/inputNumber/inputNumber.test.tsx
index 608603e6..6fe1c0bd 100644
--- a/src/core/components/forms/inputNumber/inputNumber.test.tsx
+++ b/src/core/components/forms/inputNumber/inputNumber.test.tsx
@@ -30,15 +30,17 @@ describe(' component', () => {
}) => {
const { props, expectedValue, type } = values ?? {};
const user = userEvent.setup();
+
const setUnmaskedValue = jest.fn();
+ const onChange = jest.fn();
const hookResult = {
setUnmaskedValue,
- unmaskedValue: props?.value,
+ unmaskedValue: props?.value ?? '',
} as unknown as InputHooks.IUseNumberMaskResult;
useNumberMaskMock.mockReturnValue(hookResult);
- render(createTestComponent({ ...props }));
+ render(createTestComponent({ ...props, onChange }));
const [decrementButton, incrementButton] = screen.getAllByRole('button');
@@ -49,11 +51,11 @@ describe(' component', () => {
}
expect(setUnmaskedValue).toHaveBeenCalledWith(expectedValue);
+ expect(onChange).toHaveBeenCalledWith(expectedValue);
};
it('renders an input with increment and decrement buttons', () => {
render(createTestComponent());
-
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.getAllByRole('button').length).toEqual(2);
expect(screen.getByTestId(IconType.PLUS)).toBeInTheDocument();
@@ -62,29 +64,28 @@ describe(' component', () => {
it('renders a disabled input with no spin buttons when disabled is set to true', () => {
render(createTestComponent({ disabled: true }));
-
expect(screen.getByRole('textbox')).toBeDisabled();
expect(screen.queryAllByRole('button').length).toEqual(0);
});
- it('should default step to 1 when given value less than zero', () => {
+ it('defaults step to 1 when given value less than zero', () => {
const step = -15;
render(createTestComponent({ step }));
expect(screen.getByRole('textbox')).toHaveAttribute('step', '1');
});
- it('should default step to 1 when given value is zero', () => {
+ it('defaults step to 1 when given value is zero', () => {
const step = 0;
render(createTestComponent({ step }));
expect(screen.getByRole('textbox')).toHaveAttribute('step', '1');
});
describe('increment button', () => {
- it('should increment by one (1) with default parameters', async () => {
+ it('increments by 1 with default parameters', async () => {
await testChangeValueLogic({ type: 'increment', expectedValue: '1' });
});
- it('should return the maximum when the newly generated value exceeds the maximum', async () => {
+ it('returns max value when new value is greater than max value', async () => {
const max = 5;
const step = 2;
const value = '4';
@@ -92,49 +93,60 @@ describe(' component', () => {
await testChangeValueLogic({ type: 'increment', props, expectedValue: max.toString() });
});
- it('should increment by floating point value when the step is a float', async () => {
+ it('increments by floating point value when the step is a float', async () => {
const value = '1';
const step = 0.5;
const props = { step, value };
await testChangeValueLogic({ type: 'increment', props, expectedValue: (Number(value) + step).toString() });
});
- it('should round down to the nearest multiple of the step before incrementing by the step value', async () => {
+ it('increments by provided step', async () => {
+ const value = '10';
+ const step = 2;
+ const props = { value, step };
+ await testChangeValueLogic({ type: 'increment', props, expectedValue: '12' });
+ });
+
+ it('rounds down to the nearest multiple of the step before incrementing by the step value', async () => {
const value = '1';
const step = 0.3;
const props = { value, step };
await testChangeValueLogic({ type: 'increment', props, expectedValue: '1.2' });
});
-
- it('should increment to the minimum when no value is provided', async () => {
- const step = 6;
- const min = 5;
- const max = 10;
- const props = { step, min, max };
- await testChangeValueLogic({ type: 'increment', props, expectedValue: min.toString() });
- });
});
describe('decrement button', () => {
- it('should decrement by step', async () => {
- const value = '10';
- const step = 2;
- const props = { value, step };
- await testChangeValueLogic({ type: 'decrement', props, expectedValue: (10 - 2).toString() });
+ it('decrements by 1 with default parameters', async () => {
+ await testChangeValueLogic({ type: 'decrement', expectedValue: '-1' });
});
- it('should decrement to the minimum when no value provided', async () => {
- const step = 2;
- const min = 1;
- const props = { step, min };
+ it('returns min value when new value is less than min value', async () => {
+ const min = 3;
+ const step = 3;
+ const value = '5';
+ const props = { min, step, value };
await testChangeValueLogic({ type: 'decrement', props, expectedValue: min.toString() });
});
- it('should decrement to the closest multiple of the step smaller than the value', async () => {
+ it('decrements by floating point value when the step is a float', async () => {
+ const value = '1';
+ const step = 0.5;
+ const props = { step, value };
+ await testChangeValueLogic({ type: 'decrement', props, expectedValue: (Number(value) - step).toString() });
+ });
+
+ it('decrements by provided step', async () => {
const value = '10';
- const step = 3;
+ const step = 2;
+ const props = { value, step };
+ await testChangeValueLogic({ type: 'decrement', props, expectedValue: '8' });
+ });
+
+ it('rounds up to the nearest multiple of the step before decrementing by the step value', async () => {
+ const value = '1.3';
+ const step = 0.3;
const props = { value, step };
- await testChangeValueLogic({ type: 'decrement', props, expectedValue: '9' });
+ await testChangeValueLogic({ type: 'decrement', props, expectedValue: '1.2' });
});
});
});
diff --git a/src/core/components/forms/inputNumber/inputNumber.tsx b/src/core/components/forms/inputNumber/inputNumber.tsx
index dbe29de4..2766e01c 100644
--- a/src/core/components/forms/inputNumber/inputNumber.tsx
+++ b/src/core/components/forms/inputNumber/inputNumber.tsx
@@ -70,44 +70,26 @@ export const InputNumber = forwardRef((prop
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowUp') {
- handleIncrement();
+ handleStepChange(1);
} else if (e.key === 'ArrowDown') {
- handleDecrement();
+ handleStepChange(-1);
}
onKeyDown?.(e);
};
- const handleIncrement = () => {
- const parsedValue = Number(unmaskedValue ?? 0);
+ const handleStepChange = (change: number) => {
+ const parsedValue = Number(unmaskedValue);
- // increment directly to the minimum if value is less than the minimum
- if (parsedValue < min) {
- setUnmaskedValue(min.toString());
- return;
- }
-
- // ensure value is multiple of step
- const newValue = (Math.floor(parsedValue / step) + 1) * step;
-
- // ensure the new value is than the max
- setUnmaskedValue(Math.min(max, newValue).toString());
- };
-
- const handleDecrement = () => {
- const parsedValue = Number(unmaskedValue ?? 0);
-
- // decrement directly to the maximum if value is greater than the maximum
- if (parsedValue > max) {
- setUnmaskedValue(max.toString());
- return;
- }
+ // Ensure value is multiple of step
+ const multipleValue = change > 0 ? Math.floor(parsedValue / step) : Math.ceil(parsedValue / step);
+ const stepValue = (multipleValue + change) * step;
- // ensure value is multiple of step
- const newValue = (Math.ceil(parsedValue / step) - 1) * step;
+ // Ensure value between min and max
+ const processedValue = Math.min(max, Math.max(min, stepValue)).toString();
- // ensure the new value is than the max
- setUnmaskedValue(Math.max(min, newValue).toString());
+ setUnmaskedValue(processedValue);
+ onChange?.(processedValue);
};
return (
@@ -116,7 +98,7 @@ export const InputNumber = forwardRef((prop