init
This commit is contained in:
33
platform/ui/src/components/InputNumber/InputNumber.css
Normal file
33
platform/ui/src/components/InputNumber/InputNumber.css
Normal file
@@ -0,0 +1,33 @@
|
||||
input[type='text'] {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* For most modern browsers */
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* For Firefox */
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.input-number:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.up-arrowsize svg {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.up-arrowsize svg path {
|
||||
fill: #726f7e;
|
||||
}
|
||||
|
||||
.input-small {
|
||||
height: 26px;
|
||||
}
|
||||
185
platform/ui/src/components/InputNumber/InputNumber.tsx
Normal file
185
platform/ui/src/components/InputNumber/InputNumber.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import IconButton from '../IconButton';
|
||||
import Icon from '../Icon';
|
||||
import './InputNumber.css';
|
||||
import Label from '../Label';
|
||||
import getMaxDigits from '../../utils/getMaxDigits';
|
||||
|
||||
const arrowHorizontalClassName =
|
||||
'cursor-pointer text-primary-active active:text-primary-light hover:opacity-70 w-4 flex items-center justify-center';
|
||||
|
||||
/**
|
||||
* React Number Input component'
|
||||
* it has two props, value and onChange
|
||||
* value is a number value
|
||||
* onChange is a function that will be called when the number input is changed
|
||||
* it can get changed by clicking on the up and down buttons
|
||||
* or by typing a number in the input
|
||||
*/
|
||||
|
||||
const sizesClasses = {
|
||||
sm: 'w-[45px] h-[28px]',
|
||||
md: 'w-[58px] h-[28px]',
|
||||
lg: 'w-[206px] h-[35px]',
|
||||
};
|
||||
|
||||
const InputNumber: React.FC<{
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
step?: number;
|
||||
size?: 'sm' | 'lg' | 'md';
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
label?: string;
|
||||
showAdjustmentArrows?: boolean;
|
||||
arrowsDirection: 'vertical' | 'horizontal';
|
||||
labelPosition?: 'left' | 'bottom' | 'right' | 'top';
|
||||
inputClassName?: string;
|
||||
sizeClassName?: string;
|
||||
inputContainerClassName?: string;
|
||||
}> = ({
|
||||
value,
|
||||
onChange,
|
||||
step = 1,
|
||||
className,
|
||||
size = 'sm',
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
labelClassName = 'text-aqua-pale text-[11px] mx-auto',
|
||||
label,
|
||||
showAdjustmentArrows = true,
|
||||
arrowsDirection = 'vertical',
|
||||
labelPosition = 'left',
|
||||
inputClassName = 'text-white bg-primary-dark text-[14px]',
|
||||
sizeClassName,
|
||||
inputContainerClassName = 'bg-primary-dark border-secondary-light border rounded-[4px]',
|
||||
}) => {
|
||||
const [numberValue, setNumberValue] = useState(value);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const maxDigits = getMaxDigits(maxValue, step);
|
||||
const inputWidth = Math.max(maxDigits * 10, showAdjustmentArrows ? 20 : 28);
|
||||
const decimalPlaces = Number.isInteger(step) ? 0 : step.toString().split('.')[1].length;
|
||||
|
||||
const sizeToUse = sizeClassName ? sizeClassName : sizesClasses[size];
|
||||
|
||||
useEffect(() => {
|
||||
setNumberValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleMinMax = useCallback(
|
||||
(val: number) => Math.min(Math.max(val, minValue), maxValue),
|
||||
[maxValue, minValue]
|
||||
);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = e.target.value;
|
||||
|
||||
// Allow negative sign, empty string, or single decimal point for user flexibility
|
||||
if (inputValue === '-' || inputValue === '' || inputValue === '.') {
|
||||
setNumberValue(inputValue);
|
||||
return;
|
||||
}
|
||||
|
||||
const number = Number(inputValue);
|
||||
|
||||
// Filter out invalid inputs like 'NaN'
|
||||
if (!isNaN(number)) {
|
||||
updateValue(number);
|
||||
}
|
||||
};
|
||||
|
||||
const updateValue = (val: number) => {
|
||||
const newValue = handleMinMax(val);
|
||||
setNumberValue(newValue);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsFocused(false);
|
||||
setNumberValue(parseFloat(numberValue).toFixed(decimalPlaces));
|
||||
};
|
||||
|
||||
const increment = () => updateValue(parseFloat(numberValue) + step);
|
||||
const decrement = () => updateValue(parseFloat(numberValue) - step);
|
||||
|
||||
const labelElement = label && (
|
||||
<Label
|
||||
className={labelClassName}
|
||||
text={label}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex overflow-hidden ${className} ${labelPosition === 'top' || labelPosition === 'bottom' ? 'flex-col' : 'flex-row'}`}
|
||||
>
|
||||
{label && labelPosition === 'left' && labelElement}
|
||||
{label && labelPosition === 'top' && labelElement}
|
||||
<div
|
||||
className={`flex flex-grow items-center overflow-hidden ${sizeToUse} ${inputContainerClassName}`}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
{showAdjustmentArrows && arrowsDirection === 'horizontal' && (
|
||||
<div
|
||||
className={arrowHorizontalClassName}
|
||||
onClick={() => decrement()}
|
||||
>
|
||||
<Icon name="arrow-left-small" />
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="number"
|
||||
value={isFocused ? numberValue : parseFloat(numberValue).toFixed(decimalPlaces)}
|
||||
step={step}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
className={`input-number ${inputClassName} w-full flex-grow text-center`}
|
||||
style={{ width: inputWidth }}
|
||||
/>
|
||||
{showAdjustmentArrows && arrowsDirection === 'horizontal' && (
|
||||
<div
|
||||
className={arrowHorizontalClassName}
|
||||
onClick={() => increment()}
|
||||
>
|
||||
<Icon name="arrow-right-small" />
|
||||
</div>
|
||||
)}
|
||||
{showAdjustmentArrows && arrowsDirection === 'vertical' && (
|
||||
<div className="up-arrowsize ml-auto flex flex-shrink-0 flex-col items-center justify-around pr-1">
|
||||
<ArrowButton
|
||||
onClick={increment}
|
||||
rotate
|
||||
/>
|
||||
<ArrowButton onClick={decrement} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{label && labelPosition === 'right' && labelElement}
|
||||
{label && labelPosition === 'bottom' && labelElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ArrowButton = ({ onClick, rotate = false }: { onClick: () => void; rotate?: boolean }) => (
|
||||
<IconButton
|
||||
id="arrow-icon"
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className={`text-[#726f7e] ${rotate ? 'rotate-180 transform' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name="ui-arrow-down" />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
export default InputNumber;
|
||||
@@ -0,0 +1,56 @@
|
||||
import InputNumber from '../InputNumber';
|
||||
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
|
||||
|
||||
export const argTypes = {
|
||||
component: InputNumber,
|
||||
title: 'Components/InputNumber',
|
||||
};
|
||||
|
||||
<Meta
|
||||
title="Components/InputNumber"
|
||||
component={InputNumber}
|
||||
/>
|
||||
|
||||
export const InputNumberTemplate = args => (
|
||||
<div className="w-16">
|
||||
<InputNumber {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
<Heading
|
||||
title="InputNumber"
|
||||
componentRelativePath="InputNumber/InputNumber.tsx"
|
||||
/>
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Props](#props)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
## Overview
|
||||
|
||||
InputNumber is a component that allows you to use as a number value
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Overview"
|
||||
args={{
|
||||
step: 0.5,
|
||||
value: 15,
|
||||
minValue: 0.5,
|
||||
maxValue: 99.5,
|
||||
size: 'sm',
|
||||
showAdjustmentArrows: true,
|
||||
onChange: () => console.log('input range change'),
|
||||
}}
|
||||
>
|
||||
{InputNumberTemplate.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
<ArgsTable of={InputNumber} />
|
||||
|
||||
## Contribute
|
||||
|
||||
<Footer componentRelativePath="InputNumber/__stories__/InputNumber.stories.mdx" />
|
||||
3
platform/ui/src/components/InputNumber/index.js
Normal file
3
platform/ui/src/components/InputNumber/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import InputNumber from './InputNumber';
|
||||
|
||||
export default InputNumber;
|
||||
Reference in New Issue
Block a user