feat(web): make stepper values tappable for direct number input

Tap the number to type a value directly instead of clicking +/- repeatedly.
Input auto-selects, commits on Enter/blur, cancels on Escape, and clamps
to min/max constraints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bas van Rossem
2026-02-19 16:41:35 +01:00
parent 79ea720d18
commit 275137cdbb
2 changed files with 67 additions and 1 deletions

View File

@@ -1,3 +1,5 @@
import { useState, useRef, useEffect } from 'react';
interface Props {
label: string;
value: number;
@@ -8,6 +10,17 @@ interface Props {
}
export default function NumericStepper({ label, value, onChange, min = 0, max, step = 1 }: Props) {
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (editing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editing]);
const decrement = () => {
const next = value - step;
onChange(min !== undefined ? Math.max(min, next) : next);
@@ -18,6 +31,26 @@ export default function NumericStepper({ label, value, onChange, min = 0, max, s
onChange(max !== undefined ? Math.min(max, next) : next);
};
const startEditing = () => {
setEditValue(String(value));
setEditing(true);
};
const commitEdit = () => {
setEditing(false);
const parsed = parseInt(editValue, 10);
if (isNaN(parsed)) return; // discard invalid input
let clamped = parsed;
if (min !== undefined) clamped = Math.max(min, clamped);
if (max !== undefined) clamped = Math.min(max, clamped);
if (clamped !== value) onChange(clamped);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') commitEdit();
if (e.key === 'Escape') setEditing(false);
};
return (
<div className="stepper">
<span className="stepper-label">{label}</span>
@@ -30,7 +63,23 @@ export default function NumericStepper({ label, value, onChange, min = 0, max, s
>
-
</button>
<span className="stepper-value">{value}</span>
{editing ? (
<input
ref={inputRef}
type="number"
className="stepper-input"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={commitEdit}
onKeyDown={handleKeyDown}
min={min}
max={max}
/>
) : (
<span className="stepper-value" onClick={startEditing}>
{value}
</span>
)}
<button
type="button"
className="stepper-btn"

View File

@@ -362,6 +362,23 @@ button:disabled {
text-align: center;
font-weight: 600;
font-size: 1.1rem;
cursor: pointer;
border-bottom: 1px dashed var(--color-text-muted);
padding: 2px 4px;
border-radius: 2px;
}
.stepper-value:hover {
background: var(--color-surface-hover);
}
.stepper-input {
width: 60px;
text-align: center;
font-weight: 600;
font-size: 1.1rem;
padding: 2px 4px;
height: 30px;
}
/* Dropdown */