Number Input
Champ numerique avec controles d'incrementation et de decrementation. Installez Number Input depuis le registre VLLNT UI avec la CLI shadcn.
Champ numerique avec controles d'incrementation et de decrementation. Fait partie de la famille form dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.
Installation
Ajoutez Number Input a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/number-input.jsonSource
"use client";
import * as React from "react";
import { Minus, Plus } from "lucide-react";
import { cn } from "../../lib/utils";
import { Button } from "../button/button";
export type NumberInputProps = Omit<
React.ComponentPropsWithoutRef<"input">,
"defaultValue" | "onChange" | "type" | "value"
> & {
defaultValue?: number;
onValueChange?: (value?: number) => void;
step?: number;
value?: number;
};
function getNumericBound(bound: number | string | undefined) {
if (bound === undefined) {
return;
}
const parsedBound = Number(bound);
return Number.isNaN(parsedBound) ? undefined : parsedBound;
}
function useNumberInputState(
controlledValue: number | undefined,
defaultValue: number | undefined,
onValueChange?: (value?: number) => void,
) {
const [internalValue, setInternalValue] = React.useState<number | undefined>(
defaultValue,
);
const resolvedValue = controlledValue ?? internalValue;
const commitValue = (nextValue?: number) => {
if (controlledValue === undefined) {
setInternalValue(nextValue);
}
onValueChange?.(nextValue);
};
return { commitValue, resolvedValue };
}
function clampNumber(
nextValue: number,
min: number | undefined,
max: number | undefined,
) {
let result = nextValue;
if (min !== undefined) {
result = Math.max(min, result);
}
if (max !== undefined) {
result = Math.min(max, result);
}
return result;
}
function StepButton({
direction,
disabled,
onClick,
}: {
direction: "decrement" | "increment";
disabled?: boolean;
onClick: () => void;
}) {
return (
<Button
className={cn(
"h-full px-3",
direction === "decrement"
? "rounded-r-none border-r"
: "rounded-l-none border-l",
)}
disabled={disabled}
onClick={onClick}
tabIndex={-1}
type="button"
variant="ghost"
>
{direction === "decrement" ? (
<Minus className="size-4" />
) : (
<Plus className="size-4" />
)}
</Button>
);
}
function NumberInputField({
disabled,
onValueChange,
placeholder,
reference,
resolvedValue,
...props
}: React.ComponentPropsWithoutRef<"input"> & {
onValueChange: (value?: number) => void;
reference?: React.Ref<HTMLInputElement>;
resolvedValue?: number;
}) {
return (
<input
{...props}
className="h-full w-full border-0 bg-transparent px-3 text-center text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
disabled={disabled}
inputMode="decimal"
onChange={(event) => {
if (event.target.value === "") {
onValueChange();
return;
}
const parsedValue = Number(event.target.value);
if (!Number.isNaN(parsedValue)) {
onValueChange(parsedValue);
}
}}
placeholder={placeholder}
ref={reference}
type="number"
value={resolvedValue ?? ""}
/>
);
}
function NumberInputComponent({
className,
defaultValue,
disabled,
max,
min,
onValueChange,
placeholder,
ref: reference,
step = 1,
value,
...props
}: NumberInputProps & { ref?: React.Ref<HTMLInputElement> }) {
const { commitValue, resolvedValue } = useNumberInputState(
value,
defaultValue,
onValueChange,
);
const parsedMin = getNumericBound(min);
const parsedMax = getNumericBound(max);
const handleStepChange = (direction: number) => {
const baseValue = resolvedValue ?? parsedMin ?? 0;
commitValue(
clampNumber(baseValue + direction * step, parsedMin, parsedMax),
);
};
return (
<div
className={cn(
"flex h-10 w-full items-center rounded-md border border-input bg-background ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
disabled && "cursor-not-allowed opacity-50",
className,
)}
>
<StepButton
direction="decrement"
disabled={disabled}
onClick={() => {
handleStepChange(-1);
}}
/>
<NumberInputField
{...props}
disabled={disabled}
onValueChange={(nextValue) => {
commitValue(
nextValue === undefined
? undefined
: clampNumber(nextValue, parsedMin, parsedMax),
);
}}
placeholder={placeholder}
reference={reference}
resolvedValue={resolvedValue}
/>
<StepButton
direction="increment"
disabled={disabled}
onClick={() => {
handleStepChange(1);
}}
/>
</div>
);
}
NumberInputComponent.displayName = "NumberInput";
const NumberInput = NumberInputComponent;
export { NumberInput };
Stories
Explorez chaque variante et etat dans le Storybook interactif :
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.