Stepper | Component Deep Dive #39: Stepper — Long Press Acceleration and Decimal Precision
Click to add 1, hold to auto-increment faster and faster. This interaction is more complex than it looks — timer management, precision handling, and boundary checking are the real challenges.
The Stepper (Number Input) is used in scenarios requiring precise numeric input: shopping quantities, rating settings, time interval configuration. Users can adjust values by clicking +/- buttons or typing directly in the input field.
Core Principle
Three key interactions:
- Single click — Click +/- button, value changes by one step
- Long press acceleration — Hold the button, value continuously increments with increasing speed
- Direct input — Type a value, validated on blur
The challenge is long press acceleration: you need a variable-interval timer. Initial interval ~300ms, decreasing by 20% per tick, minimum 30ms. Release clears the timer. Also handle precision — 0.1+0.2=0.30000000000000004.
Implementation
class Stepper {
constructor(root, options = {}) {
this.input = root.querySelector('.stepper-input');
this.min = options.min ?? -Infinity;
this.max = options.max ?? Infinity;
this.step = options.step ?? 1;
this.precision = this.calcPrecision(this.step);
this.timer = null;
this.interval = 300;
this.minInterval = 30;
this.acceleration = 0.8;
this.bind();
}
calcPrecision(step) {
const str = String(step);
const dot = str.indexOf('.');
return dot === -1 ? 0 : str.length - dot - 1;
}
startRepeat(action) {
const delta = action === 'increase' ? this.step : -this.step;
this.interval = 300;
const tick = () => {
this.adjust(delta);
const val = this.getValue();
if ((delta > 0 && val >= this.max) ||
(delta < 0 && val <= this.min)) {
this.stopRepeat();
return;
}
this.interval = Math.max(this.minInterval,
this.interval * this.acceleration);
this.timer = setTimeout(tick, this.interval);
};
this.timer = setTimeout(tick, 400);
}
stopRepeat() {
if (this.timer) { clearTimeout(this.timer); this.timer = null; }
}
setValue(value) {
const clamped = Math.min(this.max, Math.max(this.min, value));
const fixed = parseFloat(clamped.toFixed(this.precision));
this.input.value = fixed;
}
}
Floating Point Precision
JavaScript uses IEEE 754 double-precision floats. 0.1+0.2 does not equal 0.3:
0.1 + 0.2 // 0.30000000000000004
0.3 - 0.1 // 0.19999999999999998
Each adjustment accumulates error. The solution: round to the step's precision on every setValue:
const fixed = parseFloat(clamped.toFixed(this.precision));
If step=0.01, precision=2, every setValue does toFixed(2) correction, ensuring the display is always clean two-decimal.
Long Press Acceleration Algorithm
| Tick | Interval (ms) | Cumulative (ms) |
|---|---|---|
| 1 | 300 | 300 |
| 2 | 240 | 540 |
| 3 | 192 | 732 |
| 5 | 123 | 1009 |
| 10 | 50 | ~1350 |
| 15+ | 30 | ~1550 |
Initial delay 400ms (simulating "hold" feel), then start at 300ms, multiply by 0.8 each tick, minimum 30ms. After holding for 1.5 seconds, the user has incremented ~15 times.
Common Pitfalls
Using setInterval instead of setTimeout —
setIntervalhas a fixed interval and cannot accelerate. Use recursivesetTimeoutwith recalculated interval each time.Not preventDefault on touchstart — Long press on mobile triggers system menus or text selection.
e.preventDefault()with{ passive: false }prevents this.Forgetting boundary checks — Long press past max/min keeps sending meaningless adjust calls. Every tick must check bounds.
Precision accumulation — Using
value += stepaccumulates float errors. Fix withtoFixedon every setValue, not just at the display layer.
Underlying Logic
A stepper is essentially a "discrete numeric controller": mapping continuous pointer interactions (clicks, long presses, drags) to discrete numeric changes. Single clicks are discrete mappings — one click, one step. Long press is continuous-to-discrete conversion — duration maps to step count, acceleration curve controls the mapping rate.
Precision handling is about bridging floating-point arithmetic and user expectations. Users expect 0.1+0.1=0.2, but hardware gives 0.30000000000000004. toFixed is a "translation layer" — translating the hardware's exact representation into the user's intuitive representation.
本文是「组件深度解析」系列第39篇。每篇拆解一个前端组件的核心原理与实现细节。
Top comments (0)