193 lines
5.3 KiB
TypeScript
193 lines
5.3 KiB
TypeScript
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
import { LabelInfo } from '@gpustack/core-ui';
|
|
import { useIntl } from '@umijs/max';
|
|
import { Flex, InputNumber, Tooltip } from 'antd';
|
|
import classNames from 'classnames';
|
|
import React, { useEffect, useState } from 'react';
|
|
import styles from './styles.less';
|
|
|
|
interface NumberSelectionProps {
|
|
id?: string;
|
|
step?: number;
|
|
min?: number;
|
|
max?: number;
|
|
value?: number;
|
|
disabled?: boolean;
|
|
label?: React.ReactNode;
|
|
required?: boolean;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
styles?: {
|
|
input?: React.CSSProperties;
|
|
};
|
|
labelExtra?: React.ReactNode;
|
|
maxCount?: number;
|
|
tips?: string;
|
|
onChange?: (value: number) => void;
|
|
}
|
|
|
|
const NumberSelection: React.FC<NumberSelectionProps> = ({
|
|
id,
|
|
step = 1,
|
|
min = 1,
|
|
max = 16,
|
|
value,
|
|
disabled,
|
|
label,
|
|
required,
|
|
labelExtra,
|
|
className,
|
|
maxCount = 8,
|
|
tips,
|
|
style,
|
|
onChange
|
|
}) => {
|
|
const intl = useIntl();
|
|
|
|
const showCustomInput = max > maxCount;
|
|
const presetItems = Array.from(
|
|
{ length: Math.max(0, maxCount) },
|
|
(_, i) => i + 1
|
|
);
|
|
if (min <= 0) {
|
|
presetItems.unshift(0);
|
|
}
|
|
const items = presetItems;
|
|
const isItemDisabled = (num: number) => !!disabled || num > max || num < min;
|
|
const [inputValue, setInputValue] = useState<number | null>(() =>
|
|
value !== undefined && value !== null && !presetItems.includes(value)
|
|
? value
|
|
: null
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (value === undefined || value === null) {
|
|
setInputValue(null);
|
|
} else if (presetItems.includes(value)) {
|
|
setInputValue(null);
|
|
} else {
|
|
setInputValue(value);
|
|
}
|
|
}, [value]);
|
|
|
|
const handleSelect = (num: number) => {
|
|
if (isItemDisabled(num) || num === value) {
|
|
return;
|
|
}
|
|
setInputValue(null);
|
|
onChange?.(num);
|
|
};
|
|
|
|
const handleInputChange = (num: number | null) => {
|
|
setInputValue(num);
|
|
};
|
|
|
|
const commitInput = () => {
|
|
if (disabled || inputValue === null || inputValue === value) {
|
|
return;
|
|
}
|
|
onChange?.(inputValue);
|
|
};
|
|
return (
|
|
<div
|
|
id={id}
|
|
className={classNames(styles.wrapper, className, {
|
|
[styles.disabled]: disabled
|
|
})}
|
|
style={style}
|
|
role="radiogroup"
|
|
>
|
|
{label !== undefined && label !== null && (
|
|
<div className={styles.label}>
|
|
<LabelInfo
|
|
label={label}
|
|
required={required}
|
|
labelExtra={labelExtra}
|
|
></LabelInfo>
|
|
</div>
|
|
)}
|
|
<Flex className={styles.contentWrapper} align="center">
|
|
<div className={styles.content}>
|
|
{items.map((num) => {
|
|
const itemDisabled = isItemDisabled(num);
|
|
return (
|
|
<div key={num} style={{ flex: 1 }}>
|
|
<Tooltip title={num === 0 ? tips : false}>
|
|
<div
|
|
key={num}
|
|
role="radio"
|
|
aria-checked={num === value}
|
|
aria-disabled={itemDisabled}
|
|
tabIndex={itemDisabled ? -1 : 0}
|
|
className={classNames(styles.numberItem, {
|
|
[styles.active]: num === value && value != null,
|
|
[styles.itemDisabled]: itemDisabled && !disabled
|
|
})}
|
|
onClick={() => handleSelect(num)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
handleSelect(num);
|
|
}
|
|
}}
|
|
>
|
|
{num}
|
|
{num === 0 && (
|
|
<QuestionCircleOutlined
|
|
style={{
|
|
marginLeft: 4,
|
|
fontSize: 11,
|
|
color: 'var(--ant-color-text-tertiary)'
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
{showCustomInput && (
|
|
<div className={styles.inputWrapper}>
|
|
<div className={styles.line}></div>
|
|
<div
|
|
className={classNames(styles.inputContainer, {
|
|
[styles.hasValue]: inputValue != null
|
|
})}
|
|
>
|
|
<InputNumber
|
|
controls={false}
|
|
variant="borderless"
|
|
min={min}
|
|
max={max}
|
|
step={step}
|
|
disabled={disabled}
|
|
value={inputValue ?? undefined}
|
|
style={{
|
|
fontSize: 14,
|
|
width: 60,
|
|
height: 32,
|
|
backgroundColor: 'var(--ant-color-bg-container)',
|
|
...style
|
|
}}
|
|
styles={{
|
|
input: {
|
|
fontWeight: inputValue != null ? 500 : 400,
|
|
...(styles?.input as React.CSSProperties)
|
|
}
|
|
}}
|
|
placeholder={intl.formatMessage({ id: 'common.option.other' })}
|
|
onChange={handleInputChange}
|
|
onBlur={commitInput}
|
|
onPressEnter={commitInput}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Flex>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default NumberSelection;
|