dify/web/app/components/base/input-number/index.tsx

194 lines
4.6 KiB
TypeScript
Raw Normal View History

import type { NumberFieldRoot as BaseNumberFieldRoot } from '@base-ui/react/number-field'
import type { CSSProperties, FC, InputHTMLAttributes } from 'react'
import { useCallback } from 'react'
import {
NumberField,
NumberFieldControls,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldUnit,
} from '@/app/components/base/ui/number-field'
import { cn } from '@/utils/classnames'
type InputNumberInputProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'defaultValue' | 'max' | 'min' | 'onChange' | 'size' | 'type' | 'value'
>
export type InputNumberProps = InputNumberInputProps & {
unit?: string
value?: number
onChange: (value: number) => void
amount?: number
2025-04-18 07:54:22 +00:00
size?: 'regular' | 'large'
max?: number
min?: number
step?: number | 'any'
defaultValue?: number
disabled?: boolean
wrapClassName?: string
wrapperClassName?: string
styleCss?: CSSProperties
controlWrapClassName?: string
controlClassName?: string
type?: 'number'
}
const STEPPER_REASONS = new Set<BaseNumberFieldRoot.ChangeEventDetails['reason']>([
'increment-press',
'decrement-press',
])
const isValueWithinBounds = (value: number, min?: number, max?: number) => {
if (typeof min === 'number' && value < min)
return false
if (typeof max === 'number' && value > max)
return false
return true
}
const resolveStep = (amount?: number, step?: InputNumberProps['step']) => (
amount ?? (step === 'any' || typeof step === 'number' ? step : undefined) ?? 1
)
const exceedsStepBounds = ({
value,
reason,
stepAmount,
min,
max,
}: {
value?: number
reason: BaseNumberFieldRoot.ChangeEventDetails['reason']
stepAmount: number
min?: number
max?: number
}) => {
if (typeof value !== 'number')
return false
if (reason === 'increment-press' && typeof max === 'number')
return value + stepAmount > max
if (reason === 'decrement-press' && typeof min === 'number')
return value - stepAmount < min
return false
}
export const InputNumber: FC<InputNumberProps> = (props) => {
const {
unit,
className,
wrapperClassName,
styleCss,
onChange,
amount,
value,
size = 'regular',
max,
min,
defaultValue,
wrapClassName,
controlWrapClassName,
controlClassName,
disabled,
step,
id,
name,
readOnly,
required,
type: _type,
...rest
} = props
const resolvedStep = resolveStep(amount, step)
const stepAmount = typeof resolvedStep === 'number' ? resolvedStep : 1
const handleValueChange = useCallback((
nextValue: number | null,
eventDetails: BaseNumberFieldRoot.ChangeEventDetails,
) => {
if (value === undefined && STEPPER_REASONS.has(eventDetails.reason)) {
onChange(defaultValue ?? 0)
return
}
if (nextValue === null) {
onChange(0)
return
}
if (exceedsStepBounds({
value,
reason: eventDetails.reason,
stepAmount,
min,
max,
})) {
return
}
if (!isValueWithinBounds(nextValue, min, max))
return
onChange(nextValue)
}, [defaultValue, max, min, onChange, stepAmount, value])
return (
<div data-testid="input-number-wrapper" className={cn('flex w-full min-w-0', wrapClassName, wrapperClassName)}>
<NumberField
className="min-w-0 grow"
value={value ?? null}
min={min}
max={max}
step={resolvedStep}
disabled={disabled}
readOnly={readOnly}
required={required}
id={id}
name={name}
allowOutOfRange
onValueChange={handleValueChange}
test: improve coverage for some test files (#32916) Signed-off-by: edvatar <88481784+toroleapinc@users.noreply.github.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: Poojan <poojan@infocusp.com> Co-authored-by: sahil-infocusp <73810410+sahil-infocusp@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Pandaaaa906 <ye.pandaaaa906@gmail.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: heyszt <270985384@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Ijas <ijas.ahmd.ap@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: 木之本澪 <kinomotomiovo@gmail.com> Co-authored-by: KinomotoMio <200703522+KinomotoMio@users.noreply.github.com> Co-authored-by: 不做了睡大觉 <64798754+stakeswky@users.noreply.github.com> Co-authored-by: User <user@example.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: edvatar <88481784+toroleapinc@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Leilei <138381132+Inlei@users.noreply.github.com> Co-authored-by: HaKu <104669497+haku-ink@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: wangxiaolei <fatelei@gmail.com> Co-authored-by: Varun Chawla <34209028+veeceey@users.noreply.github.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: tda <95275462+tda1017@users.noreply.github.com> Co-authored-by: root <root@DESKTOP-KQLO90N> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: Niels Kaspers <153818647+nielskaspers@users.noreply.github.com> Co-authored-by: hj24 <mambahj24@gmail.com> Co-authored-by: Tyson Cung <45380903+tysoncung@users.noreply.github.com> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com> Co-authored-by: slegarraga <64795732+slegarraga@users.noreply.github.com> Co-authored-by: 99 <wh2099@pm.me> Co-authored-by: Br1an <932039080@qq.com> Co-authored-by: L1nSn0w <l1nsn0w@qq.com> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: akkoaya <151345394+akkoaya@users.noreply.github.com> Co-authored-by: 盐粒 Yanli <yanli@dify.ai> Co-authored-by: lif <1835304752@qq.com> Co-authored-by: weiguang li <codingpunk@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: HanWenbo <124024253+hwb96@users.noreply.github.com> Co-authored-by: Coding On Star <447357187@qq.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: Stable Genius <stablegenius043@gmail.com> Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com> Co-authored-by: ふるい <46769295+Echo0ff@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
2026-03-06 10:59:16 +00:00
>
<NumberFieldGroup size={size}>
<NumberFieldInput
{...rest}
size={size}
style={styleCss}
className={className}
/>
{unit && (
<NumberFieldUnit size={size}>
{unit}
</NumberFieldUnit>
)}
<NumberFieldControls
data-testid="input-number-controls"
className={controlWrapClassName}
>
<NumberFieldIncrement
aria-label="increment"
size={size}
className={controlClassName}
>
<span aria-hidden="true" className="i-ri-arrow-up-s-line size-3" />
</NumberFieldIncrement>
<NumberFieldDecrement
aria-label="decrement"
size={size}
className={controlClassName}
>
<span aria-hidden="true" className="i-ri-arrow-down-s-line size-3" />
</NumberFieldDecrement>
</NumberFieldControls>
</NumberFieldGroup>
</NumberField>
</div>
)
}