Files
pn-new-crm/src/components/shared/currency-input.tsx

68 lines
2.1 KiB
TypeScript
Raw Normal View History

'use client';
import * as React from 'react';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { currencySymbol } from '@/lib/utils/currency';
interface CurrencyInputProps extends Omit<
React.ComponentProps<'input'>,
'value' | 'onChange' | 'type'
> {
/** Controlled raw numeric value. `null` / `undefined` render empty. */
value: number | string | null | undefined;
/** Fires with a raw number (or `null` if cleared). */
onChange: (value: number | null) => void;
/** ISO currency code; renders as a leading symbol prefix. */
currency?: string;
className?: string;
}
/**
* Numeric input pre-decorated with a currency symbol. The display
* value is the raw number the user typed (we don't fight the keystroke
* cadence by re-formatting on every key) formatted display lives in
* read-only contexts via `formatCurrency()`. This keeps form behaviour
* predictable while still scoping the input to a money field via the
* symbol prefix and the `decimal` inputMode.
*/
export const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
({ value, onChange, currency = 'USD', className, ...props }, ref) => {
const symbol = currencySymbol(currency);
const display = value === null || value === undefined || value === '' ? '' : String(value);
return (
<div className="relative">
<span
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground"
aria-hidden
>
{symbol}
</span>
<Input
ref={ref}
type="number"
inputMode="decimal"
step="0.01"
min="0"
value={display}
onChange={(e) => {
const raw = e.target.value;
if (raw === '') {
onChange(null);
return;
}
const n = Number(raw);
onChange(Number.isFinite(n) ? n : null);
}}
className={cn('pl-9 tabular-nums', className)}
{...props}
/>
</div>
);
},
);
CurrencyInput.displayName = 'CurrencyInput';