Update migrated js to ts and shadcn
This commit is contained in:
201
src/components/ui/form-numeric-input.tsx
Normal file
201
src/components/ui/form-numeric-input.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import * as React from "react"
|
||||
import { Minus, Plus, X } from "lucide-react"
|
||||
import { Button } from "./button"
|
||||
import { Input } from "./input"
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
interface NumericInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
||||
value: string | number
|
||||
onChange: (value: string) => void
|
||||
increment?: number | string
|
||||
min?: number
|
||||
max?: number
|
||||
unit?: string
|
||||
align?: 'left' | 'center' | 'right'
|
||||
allowEmpty?: boolean
|
||||
clearButton?: boolean
|
||||
error?: boolean
|
||||
required?: boolean
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
({
|
||||
value,
|
||||
onChange,
|
||||
increment = 1,
|
||||
min = -Infinity,
|
||||
max = Infinity,
|
||||
unit,
|
||||
align = 'right',
|
||||
allowEmpty = false,
|
||||
clearButton = false,
|
||||
error = false,
|
||||
required = false,
|
||||
errorMessage = 'This field is required',
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const [showError, setShowError] = React.useState(false)
|
||||
const [touched, setTouched] = React.useState(false)
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
// Check if value is invalid (check validity regardless of touch state)
|
||||
const isInvalid = required && !allowEmpty && (value === '' || value === null || value === undefined)
|
||||
const hasError = error || isInvalid
|
||||
|
||||
// Check validity on mount and when value changes
|
||||
React.useEffect(() => {
|
||||
if (isInvalid && touched) {
|
||||
setShowError(true)
|
||||
} else if (!isInvalid) {
|
||||
setShowError(false)
|
||||
}
|
||||
}, [isInvalid, touched])
|
||||
// Determine decimal places based on increment
|
||||
const getDecimalPlaces = () => {
|
||||
const inc = String(increment || '1')
|
||||
const decimalIndex = inc.indexOf('.')
|
||||
if (decimalIndex === -1) return 0
|
||||
return inc.length - decimalIndex - 1
|
||||
}
|
||||
|
||||
const decimalPlaces = getDecimalPlaces()
|
||||
|
||||
// Format value for display
|
||||
const formatValue = (val: string | number): string => {
|
||||
const num = Number(val)
|
||||
if (isNaN(num)) return String(val)
|
||||
return num.toFixed(decimalPlaces)
|
||||
}
|
||||
|
||||
const updateValue = (direction: number) => {
|
||||
const numIncrement = parseFloat(String(increment)) || 1
|
||||
let numValue = Number(value)
|
||||
|
||||
if (isNaN(numValue)) {
|
||||
numValue = 0
|
||||
}
|
||||
|
||||
numValue += direction * numIncrement
|
||||
numValue = Math.max(min, numValue)
|
||||
numValue = Math.min(max, numValue)
|
||||
onChange(formatValue(numValue))
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
updateValue(e.key === 'ArrowUp' ? 1 : -1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value
|
||||
if (val === '' || /^-?\d*\.?\d*$/.test(val)) {
|
||||
onChange(val)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value
|
||||
setTouched(true)
|
||||
setShowError(false)
|
||||
|
||||
if (val === '' && !allowEmpty) {
|
||||
return
|
||||
}
|
||||
|
||||
if (val !== '' && !isNaN(Number(val))) {
|
||||
onChange(formatValue(val))
|
||||
}
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
setShowError(hasError)
|
||||
}
|
||||
|
||||
const getAlignmentClass = () => {
|
||||
switch (align) {
|
||||
case 'left': return 'text-left'
|
||||
case 'center': return 'text-center'
|
||||
case 'right': return 'text-right'
|
||||
default: return 'text-right'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={cn("relative flex items-center gap-2", className)}>
|
||||
<div className="flex items-center flex-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-r-none",
|
||||
hasError && "border-destructive"
|
||||
)}
|
||||
onClick={() => updateValue(-1)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
"rounded-none border-x-0 h-9",
|
||||
getAlignmentClass(),
|
||||
hasError && "border-destructive focus-visible:ring-destructive"
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{clearButton && allowEmpty ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-l-none",
|
||||
hasError && "border-destructive"
|
||||
)}
|
||||
onClick={() => onChange('')}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-l-none",
|
||||
hasError && "border-destructive"
|
||||
)}
|
||||
onClick={() => updateValue(1)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
||||
{hasError && showError && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-64 bg-destructive text-destructive-foreground text-xs p-2 rounded-md shadow-lg">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
FormNumericInput.displayName = "FormNumericInput"
|
||||
|
||||
export { FormNumericInput }
|
||||
Reference in New Issue
Block a user