Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/components/ui/input/InputNumber.tsx
T

114 lines
3.5 KiB
TypeScript

import clsx from "clsx";
import { forwardRef, ReactNode, Ref, RefObject, useId, useImperativeHandle, useRef } from "react";
import { useTranslation } from "react-i18next";
import HintTooltip from "~/components/ui/tooltips/HintTooltip";
import { NumberInput } from "~/ui/components/NumberInput";
export interface RefInputNumber {
input: RefObject<HTMLInputElement | null>;
}
// type WithoutValue = {};
type WithDefaultValue = { defaultValue: number | undefined };
type WithValueAndSetValue = { value: number | undefined; setValue: React.Dispatch<React.SetStateAction<number | undefined>> };
type Props = (WithDefaultValue | WithValueAndSetValue) & {
name?: string;
title?: string;
withLabel?: boolean;
className?: string;
help?: string;
min?: number;
max?: number;
disabled?: boolean;
readOnly?: boolean;
required?: boolean;
hint?: ReactNode;
step?: string;
placeholder?: string;
icon?: string;
borderless?: boolean;
autoFocus?: boolean;
canUnset?: boolean;
};
const InputNumber = (props: Props, ref: Ref<RefInputNumber>) => {
const { t } = useTranslation();
const {
name,
title,
withLabel = true,
className,
hint,
help,
disabled = false,
readOnly = false,
required = false,
min = 0,
max,
step,
placeholder,
// icon — Subframe NumberInput has no left-icon slot; see note below.
borderless,
autoFocus,
canUnset,
} = props;
const value = "value" in props ? props.value : undefined;
const setValue = "setValue" in props ? props.setValue : undefined;
const defaultValue = "defaultValue" in props ? props.defaultValue : undefined;
useImperativeHandle(ref, () => ({ input }));
const input = useRef<HTMLInputElement>(null);
// Fall back to a generated id so the visible <label> always associates with the
// input even when no name is supplied. Mirrors the InputText shim.
const generatedId = useId();
const inputId = name ?? generatedId;
const showLabel = Boolean(withLabel && title);
// Rich label content rendered into Subframe's label slot (inline-safe — spans only).
const labelContent: ReactNode = showLabel ? (
<span className="flex w-full items-center justify-between gap-2">
<span className="flex shrink-0 items-center space-x-1">
<span className="truncate">
{title}
{required && <span className="ml-1 text-red-600">*</span>}
</span>
{help && <HintTooltip text={help} />}
</span>
{canUnset && !required && !disabled && !readOnly && value !== undefined && setValue && (
<button type="button" onClick={() => (setValue ? setValue(undefined) : null)} className="text-muted-foreground text-xs">
{t("shared.clear")}
</button>
)}
{hint}
</span>
) : null;
return (
<div className={clsx(className, "")}>
<NumberInput className="w-full max-w-none" showLabel={showLabel} label={labelContent} disabled={disabled} borderless={borderless}>
<NumberInput.Input
ref={input}
id={inputId}
name={name}
required={required}
min={min}
max={max}
value={value}
defaultValue={defaultValue}
onChange={(e) => (setValue ? setValue(Number(e.currentTarget.value)) : null)}
step={step}
placeholder={placeholder}
disabled={disabled}
readOnly={readOnly}
autoFocus={autoFocus}
aria-label={!showLabel ? title ?? placeholder : undefined}
/>
</NumberInput>
</div>
);
};
export default forwardRef(InputNumber);