import type { HTMLAttributes, KeyboardEvent, ReactNode } from "react"; import { useContext, useId, useState } from "react"; import { cx } from "../../utils/cx.utils"; import { FormContext } from "../form/form.context"; import { Icon } from "../icons/icon"; import { Label } from "../label/label.component"; import "./number-input.component.css"; function roundStep(value: number, useFloat: boolean): number { return useFloat ? Math.round(value * 100) / 100 : Math.round(value); } function clamp(value: number, min: number | undefined, max: number | undefined): number { let result = value; if (min !== undefined) result = Math.max(min, result); if (max !== undefined) result = Math.min(max, result); return result; } /** * Formats a value the way {@link NumberInput} displays it: plain digits when * `useFloat` is `false`, otherwise always two decimal places with a comma as * the decimal separator (`5,00`, `5,01`) — except `0`, which is shown bare (`0`). */ function formatValue(value: number, useFloat: boolean): string { if (!useFloat || value === 0) return String(value); return value.toFixed(2).replace(".", ","); } /** Props accepted by {@link NumberInput}. */ export interface NumberInputProps extends Omit, "onChange"> { /** Label rendered above the control, via {@link Label.Form}. */ label: ReactNode; /** * Field name to bind to the enclosing `
`. When provided, `NumberInput` * reads/writes its value through the form context and the * `value`/`onChange` props are ignored. */ name?: string; /** Current value for standalone use, outside a ``. */ value?: number; /** Initial value for uncontrolled standalone use. Defaults to `0`. */ defaultValue?: number; /** Called with the next value whenever it changes. Ignored when `name` is set. */ onChange?: (value: number) => void; /** Lower bound. Unbounded when omitted. */ min?: number; /** Upper bound. Unbounded when omitted. */ max?: number; /** Amount added or subtracted per step. Defaults to `0.1` when `useFloat`, otherwise `1`. */ step?: number; /** * Treats the value as a float rather than an integer: the display always * shows two decimal places with a comma separator (e.g. `5,00`, `5,01`), * except `0` which is shown bare. Defaults to `false`. */ useFloat?: boolean; /** Disables both step buttons. Defaults to `false`. */ disabled?: boolean; } /** * Numeric stepper for e-ink displays: a labeled decrease/increase button pair * around a value display, for quantities or other small bounded numbers. * Renders as a native `role="spinbutton"` (`aria-valuemin`/`aria-valuemax`/ * `aria-valuenow`/`aria-valuetext`) flanked by two `
{formatted}
); }