FormNumberInput: add number|undefined input

This commit is contained in:
Enrico Ros
2026-03-02 19:23:28 -08:00
parent 69063bb544
commit 2eb77f532a
@@ -0,0 +1,94 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, FormControl, IconButton, Input, Tooltip } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import { FormLabelStart } from './FormLabelStart';
const _style = {
control: {
flexWrap: 'wrap',
justifyContent: 'space-between',
alignItems: 'center',
},
inputRow: {
display: 'flex',
alignItems: 'center',
gap: 0.5,
},
} as const satisfies Record<string, SxProps>;
/**
* Compact numeric input field with label and optional description.
* Supports undefined (unset/default) state with a toggle button:
* - undefined: input disabled, X button pressed (soft) — input shows empty
* - number: input active, X button flat (plain) — pressing X clears to undefined
*/
export function FormNumberInput(props: {
title: string,
description?: string,
tooltip?: string,
disabled?: boolean,
size?: 'sm' | 'md' | 'lg',
min?: number,
max?: number,
initialValue: number,
value: undefined | number,
onChange: (value: number | undefined) => void,
inputSx?: SxProps,
}) {
const { onChange, min, max, initialValue } = props;
const isSet = props.value !== undefined;
const handleInputChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const num = parseInt(event.target.value, 10);
if (!isNaN(num)) {
const clamped = Math.max(min ?? -Infinity, Math.min(max ?? Infinity, num));
onChange(clamped);
}
}, [max, min, onChange]);
const handleToggle = React.useCallback(() => {
if (isSet)
onChange(undefined);
else
onChange(initialValue);
}, [isSet, onChange, initialValue]);
return (
<FormControl size={props.size} disabled={props.disabled} orientation='horizontal' sx={_style.control}>
<FormLabelStart title={props.title} description={props.description} tooltip={props.tooltip} />
<Box sx={_style.inputRow}>
<Input
type='number'
size={props.size}
variant={isSet ? 'outlined' : 'plain'}
disabled={!isSet}
placeholder='unset'
slotProps={{
input: {
...(min !== undefined ? { min } : {}),
...(max !== undefined ? { max } : {}),
},
}}
value={isSet ? props.value : ''}
onChange={handleInputChange}
sx={props.inputSx}
/>
<Tooltip arrow disableInteractive title={isSet ? 'Reset to default' : 'Customize'}>
<IconButton
size={props.size}
variant={isSet ? 'plain' : 'soft'}
onClick={handleToggle}
>
<ClearIcon sx={{ fontSize: props.size === 'sm' ? 'md' : 'lg' }} />
</IconButton>
</Tooltip>
</Box>
</FormControl>
);
}