Files
pn-new-crm/src/components/shared/currency-input.tsx
Matt 75920a2540 feat(deps): react-number-format replaces hand-rolled CurrencyInput parser
The old CurrencyInput had ~100 LOC of regex-based parsing,
display-state syncing, and caret/focus juggling. react-number-format
ships a 17-LOC equivalent (NumericFormat with customInput pointing
at our shared Input shell) that handles the edge cases the hand-
rolled version missed: paste sanitisation, IME composition,
selection-caret preservation, locale separator switching.

Same external API on CurrencyInput so all 3 call sites
(berth-form, invoice-line-items, expense-form-dialog) keep working
without changes.

Verified: tsc clean, vitest 1315/1315, next build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:53:18 +02:00

70 lines
2.3 KiB
TypeScript

'use client';
import * as React from 'react';
import { NumericFormat, type NumericFormatProps } from 'react-number-format';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { currencySymbol } from '@/lib/utils/currency';
interface CurrencyInputProps extends Omit<
NumericFormatProps,
'value' | 'onChange' | 'customInput'
> {
/** 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 and thousand-separator
* grouping (e.g. `3,528,000.50`). Built on react-number-format which handles
* the inputMode/IME edge cases, decimal/group localisation, paste sanitization,
* and selection caret preservation that hand-rolled parsers miss.
*
* Renders inside the shared `<Input>` shell via `customInput` so border /
* focus-ring / dark-mode styles stay consistent with the rest of the kit.
*/
export const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
({ value, onChange, currency = 'USD', className, ...props }, ref) => {
const symbol = currencySymbol(currency);
// react-number-format wants undefined (not null) to render empty.
const formatValue =
value === null || value === undefined || value === '' ? undefined : Number(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>
<NumericFormat
getInputRef={ref}
customInput={Input}
value={formatValue}
thousandSeparator=","
decimalSeparator="."
decimalScale={2}
allowNegative
inputMode="decimal"
autoComplete="off"
onValueChange={(values) => {
// floatValue is undefined when input is empty.
onChange(values.floatValue ?? null);
}}
className={cn('pl-9 tabular-nums', className)}
{...props}
/>
</div>
);
},
);
CurrencyInput.displayName = 'CurrencyInput';