Midday / packages /ui /src /components /quantity-input.tsx
Jules
Final deployment with all fixes and verified content
c09f67c
import { Minus, Plus } from "lucide-react";
import * as React from "react";
import { cn } from "../utils";
type Props = {
value?: number;
min?: number;
max?: number;
onChange?: (value: number) => void;
onBlur?: () => void;
onFocus?: () => void;
className?: string;
step?: number;
placeholder?: string;
};
export function QuantityInput({
value = 0,
min = Number.NEGATIVE_INFINITY,
max = Number.POSITIVE_INFINITY,
onChange,
onBlur,
onFocus,
className,
step = 0.1,
placeholder = "0",
}: Props) {
const inputRef = React.useRef<HTMLInputElement>(null);
const [rawValue, setRawValue] = React.useState(String(value));
const handleInput = ({
currentTarget: el,
}: {
currentTarget: HTMLInputElement;
}) => {
const input = el.value;
setRawValue(input);
// Check if input can be parsed as a valid number
const num = Number.parseFloat(input);
if (!Number.isNaN(num) && min <= num && num <= max) {
onChange?.(num);
}
};
const handlePointerDown =
(diff: number) => (event: React.PointerEvent<HTMLButtonElement>) => {
if (event.pointerType === "mouse") {
event.preventDefault();
inputRef.current?.focus();
}
const newVal = Math.min(Math.max(value + diff, min), max);
onChange?.(newVal);
setRawValue(String(newVal));
};
return (
<div
className={cn(
"group flex items-stretch transition-[box-shadow] font-mono",
className,
)}
>
<button
aria-label="Decrease"
className="flex items-center pr-[.325em]"
disabled={value <= min}
onPointerDown={handlePointerDown(-1)}
type="button"
tabIndex={-1}
>
<Minus
className="size-2"
absoluteStrokeWidth
strokeWidth={3.5}
tabIndex={-1}
/>
</button>
<div className="relative grid items-center justify-items-center text-center">
<input
ref={inputRef}
className="flex w-full max-w-full text-center transition-colors file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 p-0 border-0 h-6 text-xs !bg-transparent border-b border-transparent focus:border-border [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [-moz-appearance:textfield]"
style={{ fontKerning: "none" }}
type="number"
min={min}
max={max}
autoComplete="off"
step={step}
value={rawValue === "0" ? "" : rawValue}
placeholder={placeholder}
onInput={handleInput}
onBlur={onBlur}
onFocus={onFocus}
inputMode="decimal"
/>
</div>
<button
aria-label="Increase"
className="flex items-center pl-[.325em]"
disabled={value >= max}
onPointerDown={handlePointerDown(1)}
type="button"
tabIndex={-1}
>
<Plus
className="size-2"
absoluteStrokeWidth
strokeWidth={3.5}
tabIndex={-1}
/>
</button>
</div>
);
}