ui(components): Adds InputNumber component and 4d panel updates (#4874)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
1 parent
c4045251fb
commit
b7419fb6a1
15 files changed
+1620
-304
No files matched your search
@@ -5,9 +5,23 @@ import { cn } from '../../lib/utils';
|
||||
import { Input } from '../Input/Input';
|
||||
import { Slider } from '../Slider/Slider';
|
||||
import { DoubleSlider } from '../DoubleSlider/DoubleSlider';
|
||||
import { Button } from '../Button/Button';
|
||||
import { ChevronUp, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
// Calculate decimal places based on step
|
||||
const getDecimalPlaces = (step: number): number => {
|
||||
if (Number.isInteger(step)) {
|
||||
return 0;
|
||||
}
|
||||
const stepStr = step.toString();
|
||||
if (stepStr.includes('.')) {
|
||||
return stepStr.split('.')[1].length;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
interface NumericMetaContextValue {
|
||||
mode: 'number' | 'singleRange' | 'doubleRange';
|
||||
mode: 'number' | 'singleRange' | 'doubleRange' | 'stepper';
|
||||
singleValue: number;
|
||||
doubleValue: [number, number];
|
||||
setSingleValue: (val: number) => void;
|
||||
@@ -23,7 +37,7 @@ const NumericMetaContext = createContext<NumericMetaContextValue | null>(null);
|
||||
1) Container
|
||||
---------------------------------------------------------------------------*/
|
||||
interface NumericMetaContainerProps {
|
||||
mode: 'number' | 'singleRange' | 'doubleRange';
|
||||
mode: 'number' | 'singleRange' | 'doubleRange' | 'stepper';
|
||||
value?: number; // for controlled single-value usage from parent
|
||||
defaultValue?: number; // for uncontrolled single-value usage
|
||||
values?: [number, number]; // for controlled double-range usage from parent
|
||||
@@ -57,10 +71,10 @@ function NumericMetaContainer({
|
||||
|
||||
// Use useControllableState for both single and double values
|
||||
const [internalSingleValue, setInternalSingleValue] = useControllableState({
|
||||
prop: mode === 'number' || mode === 'singleRange' ? value : undefined,
|
||||
prop: mode === 'number' || mode === 'singleRange' || mode === 'stepper' ? value : undefined,
|
||||
defaultProp: calculatedDefaultValue,
|
||||
onChange: newVal => {
|
||||
if (mode === 'number' || mode === 'singleRange') {
|
||||
if (mode === 'number' || mode === 'singleRange' || mode === 'stepper') {
|
||||
onChange?.(newVal);
|
||||
}
|
||||
},
|
||||
@@ -127,7 +141,7 @@ function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelPr
|
||||
|
||||
let displayedValue = '';
|
||||
let valueClasses = '';
|
||||
if (mode === 'number' || mode === 'singleRange') {
|
||||
if (mode === 'number' || mode === 'singleRange' || mode === 'stepper') {
|
||||
displayedValue = singleValue.toString();
|
||||
valueClasses = 'w-10';
|
||||
} else if (mode === 'doubleRange') {
|
||||
@@ -291,12 +305,177 @@ function NumberInput({ className }: NumberInputProps) {
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
6) NumberStepper sub-component
|
||||
---------------------------------------------------------------------------*/
|
||||
interface NumberStepperProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
// Modified NumberStepper component to properly position left/right controls
|
||||
function NumberStepper({ className, children, direction }: NumberStepperProps) {
|
||||
const ctx = useContext(NumericMetaContext);
|
||||
if (!ctx) {
|
||||
throw new Error('NumberStepper must be used inside <Numeric.Container>.');
|
||||
}
|
||||
|
||||
const { mode, singleValue, setSingleValue, min, max, step } = ctx;
|
||||
if (mode !== 'stepper') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate decimal places based on step
|
||||
const decimalPlaces = getDecimalPlaces(step);
|
||||
|
||||
// Format displayed value with proper decimal places
|
||||
const displayValue = React.useMemo(() => {
|
||||
return decimalPlaces > 0 ? singleValue.toFixed(decimalPlaces) : singleValue.toString();
|
||||
}, [singleValue, decimalPlaces]);
|
||||
|
||||
const handleInputChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = evt.target.value;
|
||||
|
||||
// Allow empty string, minus sign, or decimal point for flexibility
|
||||
if (val === '' || val === '-' || val === '.') {
|
||||
return;
|
||||
}
|
||||
|
||||
const numValue = Number(val);
|
||||
if (!isNaN(numValue)) {
|
||||
setSingleValue(Math.max(min, Math.min(numValue, max)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
// Ensure value is within constraints when input loses focus
|
||||
const boundedValue = Math.max(min, Math.min(singleValue, max));
|
||||
if (boundedValue !== singleValue) {
|
||||
setSingleValue(boundedValue);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if children is HorizontalControls component
|
||||
const hasHorizontalControls = direction === 'horizontal';
|
||||
|
||||
if (hasHorizontalControls) {
|
||||
// We'll handle the control positioning ourselves
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-background border-input flex h-[28px] items-center overflow-hidden rounded-md border',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<LeftControl
|
||||
min={min}
|
||||
step={step}
|
||||
value={singleValue}
|
||||
setValue={setSingleValue}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={displayValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
className="h-6 flex-1 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none"
|
||||
/>
|
||||
{children}
|
||||
<RightControl
|
||||
max={max}
|
||||
step={step}
|
||||
value={singleValue}
|
||||
setValue={setSingleValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-background border-input flex items-center overflow-hidden rounded-md border',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={displayValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
className={cn(
|
||||
'h-6 flex-1 appearance-none border-none p-0 text-center shadow-none focus:border-none focus:outline-none'
|
||||
)}
|
||||
/>
|
||||
<div className="ml-1 flex flex-col">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSingleValue(singleValue + step)}
|
||||
disabled={singleValue >= max}
|
||||
className="text-primary h-3 w-5 pr-px"
|
||||
>
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSingleValue(singleValue - step)}
|
||||
disabled={singleValue <= min}
|
||||
className="text-primary h-3 w-5 pr-px"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// New components for left and right controls
|
||||
function LeftControl({ min, step, value, setValue }) {
|
||||
const decrement = useCallback(() => {
|
||||
const newValue = Math.max(value - step, min);
|
||||
setValue(newValue);
|
||||
}, [value, min, step, setValue]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={decrement}
|
||||
className="text-primary h-full w-4 cursor-pointer p-0"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function RightControl({ max, step, value, setValue }) {
|
||||
const increment = useCallback(() => {
|
||||
const newValue = Math.min(value + step, max);
|
||||
setValue(newValue);
|
||||
}, [value, max, step, setValue]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={increment}
|
||||
className="text-primary h-full w-4 cursor-pointer p-0"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export const Numeric = {
|
||||
Container: NumericMetaContainer,
|
||||
Label: NumericMetaLabel,
|
||||
SingleRange,
|
||||
DoubleRange,
|
||||
NumberInput,
|
||||
NumberStepper,
|
||||
};
|
||||
|
||||
export default Numeric;
|
||||
export default Numeric;
|
||||
Reference in new issue
Block a user