Skip to content

Commit 6ef765d

Browse files
committed
fix: handle decimal precision correctly in slider and number fields
1 parent 649bb38 commit 6ef765d

File tree

4 files changed

+131
-1
lines changed

4 files changed

+131
-1
lines changed

.changeset/busy-symbols-invent.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@formwerk/core': patch
3+
---
4+
5+
fix: handle decimal precision correctly in slider and number fields

packages/core/src/useNumberField/useNumberField.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,52 @@ test('Applies decimal inputmode if the step contains decimals', async () => {
123123
expect(screen.getByLabelText(label)).toHaveAttribute('inputmode', 'decimal');
124124
});
125125

126+
test('Increments and decrements correctly with decimal steps', async () => {
127+
await render(makeTest({ step: 0.1, value: 0 }));
128+
129+
// Test increment
130+
await fireEvent.keyDown(screen.getByLabelText(label), { code: 'ArrowUp' });
131+
expect(screen.getByLabelText(label)).toHaveDisplayValue('0.1');
132+
expect(screen.getByTestId('value')).toHaveTextContent('0.1');
133+
134+
await fireEvent.keyDown(screen.getByLabelText(label), { code: 'ArrowUp' });
135+
expect(screen.getByLabelText(label)).toHaveDisplayValue('0.2');
136+
expect(screen.getByTestId('value')).toHaveTextContent('0.2');
137+
138+
// Test decrement
139+
await fireEvent.keyDown(screen.getByLabelText(label), { code: 'ArrowDown' });
140+
expect(screen.getByLabelText(label)).toHaveDisplayValue('0.1');
141+
expect(screen.getByTestId('value')).toHaveTextContent('0.1');
142+
143+
// Test with increment button
144+
await fireEvent.mouseDown(screen.getByLabelText('Increment'));
145+
expect(screen.getByLabelText(label)).toHaveDisplayValue('0.2');
146+
expect(screen.getByTestId('value')).toHaveTextContent('0.2');
147+
148+
// Test with decrement button
149+
await fireEvent.mouseDown(screen.getByLabelText('Decrement'));
150+
expect(screen.getByLabelText(label)).toHaveDisplayValue('0.1');
151+
expect(screen.getByTestId('value')).toHaveTextContent('0.1');
152+
});
153+
154+
test('Increments and decrements correctly with step 1.5', async () => {
155+
await render(makeTest({ step: 1.5, value: 0 }));
156+
157+
// Test increment
158+
await fireEvent.keyDown(screen.getByLabelText(label), { code: 'ArrowUp' });
159+
expect(screen.getByLabelText(label)).toHaveDisplayValue('1.5');
160+
expect(screen.getByTestId('value')).toHaveTextContent('1.5');
161+
162+
await fireEvent.keyDown(screen.getByLabelText(label), { code: 'ArrowUp' });
163+
expect(screen.getByLabelText(label)).toHaveDisplayValue('3');
164+
expect(screen.getByTestId('value')).toHaveTextContent('3');
165+
166+
// Test decrement
167+
await fireEvent.keyDown(screen.getByLabelText(label), { code: 'ArrowDown' });
168+
expect(screen.getByLabelText(label)).toHaveDisplayValue('1.5');
169+
expect(screen.getByTestId('value')).toHaveTextContent('1.5');
170+
});
171+
126172
describe('validation', () => {
127173
test('picks up native error messages', async () => {
128174
await render(makeTest({ required: true }));

packages/core/src/useSlider/useSlider.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,73 @@ describe('track behavior', () => {
557557
});
558558
});
559559

560+
describe('decimal steps', () => {
561+
const Thumb = createThumbComponent({});
562+
563+
test('handles decimal step increments correctly', async () => {
564+
const DecimalSlider = createSliderComponent({
565+
label: 'Slider',
566+
min: 0,
567+
max: 1,
568+
step: 0.1,
569+
modelValue: 0,
570+
});
571+
572+
await render({
573+
components: { Thumb, DecimalSlider },
574+
template: `
575+
<DecimalSlider>
576+
<Thumb />
577+
</DecimalSlider>
578+
`,
579+
});
580+
581+
expect(screen.getByTestId('slider-value')).toHaveTextContent('0');
582+
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0');
583+
584+
await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowUp' });
585+
expect(screen.getByTestId('slider-value')).toHaveTextContent('0.1');
586+
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0.1');
587+
588+
await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowUp' });
589+
expect(screen.getByTestId('slider-value')).toHaveTextContent('0.2');
590+
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0.2');
591+
592+
await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowDown' });
593+
expect(screen.getByTestId('slider-value')).toHaveTextContent('0.1');
594+
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '0.1');
595+
});
596+
597+
test('handles step 1.5 increments correctly', async () => {
598+
const DecimalSlider = createSliderComponent({
599+
label: 'Slider',
600+
min: 0,
601+
max: 10,
602+
step: 1.5,
603+
modelValue: 0,
604+
});
605+
606+
await render({
607+
components: { Thumb, DecimalSlider },
608+
template: `
609+
<DecimalSlider>
610+
<Thumb />
611+
</DecimalSlider>
612+
`,
613+
});
614+
615+
expect(screen.getByTestId('slider-value')).toHaveTextContent('0');
616+
617+
await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowUp' });
618+
expect(screen.getByTestId('slider-value')).toHaveTextContent('1.5');
619+
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '1.5');
620+
621+
await fireEvent.keyDown(screen.getByRole('slider'), { code: 'ArrowUp' });
622+
expect(screen.getByTestId('slider-value')).toHaveTextContent('3');
623+
expect(screen.getByRole('slider')).toHaveAttribute('aria-valuenow', '3');
624+
});
625+
});
626+
560627
describe('discrete steps', () => {
561628
const Thumb = createThumbComponent({});
562629

packages/core/src/utils/math.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ export function toNearestMultipleOf(value: number, multiple: number, round?: boo
33
const largerMultiple = smallerMultiple + multiple;
44

55
const result = value - smallerMultiple >= largerMultiple - value ? largerMultiple : smallerMultiple;
6+
67
// Return of closest of two
7-
return round ? Math.round(result) : Math.trunc(result);
8+
if (round) {
9+
return Math.round(result);
10+
}
11+
12+
// For decimal steps, we need to round to the same precision as the step
13+
// to avoid floating point arithmetic issues
14+
const decimalPlaces = (multiple.toString().split('.')[1] || '').length;
15+
if (decimalPlaces > 0) {
16+
return Number(result.toFixed(decimalPlaces));
17+
}
18+
19+
return Math.trunc(result);
820
}

0 commit comments

Comments
 (0)