Files
gravl/frontend/src/components/StepperInput.jsx
T
clawd 0a8c44b5a1 feat(01-input-ux-01): create StepperInput controlled component
- Reusable stepper with +/- buttons flanking a number input
- Handles min clamping, max constraint, decimal steps
- Controlled component (no internal state): value/onChange props
- 44px touch targets, 16px font, aria-labels present
- Rejects non-numeric input silently
2026-02-16 08:03:04 +01:00

90 lines
2.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function StepperInput({
value = '',
onChange,
step = 1,
min = 0,
max = null,
label = 'Value',
suffix = '',
disabled = false,
}) {
const numValue = parseFloat(value) || 0;
function handleInputChange(e) {
const raw = e.target.value;
if (raw === '') {
onChange('');
return;
}
const parsed = parseFloat(raw);
if (isNaN(parsed)) return;
if (parsed < min) {
onChange(String(min));
} else if (max !== null && parsed > max) {
onChange(String(max));
} else {
onChange(String(parsed));
}
}
function handleDecrement() {
if (disabled) return;
const newVal = Math.max(min, numValue - step);
onChange(String(newVal));
}
function handleIncrement() {
if (disabled) return;
const newVal = numValue + step;
if (max === null || newVal <= max) {
onChange(String(newVal));
}
}
const canDecrement = numValue > min;
const canIncrement = max === null || numValue < max;
return (
<div className="stepper-wrapper" role="group" aria-labelledby={`stepper-label-${label}`}>
<label id={`stepper-label-${label}`} className="stepper-label">{label}</label>
<div className="stepper-container">
<button
type="button"
className="stepper-btn stepper-minus"
onClick={handleDecrement}
disabled={!canDecrement || disabled}
aria-label={`Decrease ${label}`}
>
</button>
<div className="stepper-input-wrapper">
<input
type="number"
value={value}
onChange={handleInputChange}
min={min}
max={max ?? undefined}
step={step}
inputMode={step % 1 === 0 ? 'numeric' : 'decimal'}
className="stepper-input"
aria-label={label}
disabled={disabled}
/>
{suffix && <span className="input-suffix">{suffix}</span>}
</div>
<button
type="button"
className="stepper-btn stepper-plus"
onClick={handleIncrement}
disabled={!canIncrement || disabled}
aria-label={`Increase ${label}`}
>
+
</button>
</div>
</div>
);
}
export default StepperInput;