@@ -41,14 +41,19 @@ export type CompositeNumberInputProps = Omit<NumberInputProps, 'onChange' | 'min
4141 * An optional callback to constrain the value. For example, to round it to the nearest multiple of 8.
4242 */
4343 constrainValue ?: ( v : number ) => number ;
44+ /**
45+ * Whether to allow math expressions (e.g. "1+2/3*4"). Defaults to false.
46+ * Expressions are evaluated using the math-expression-evaluator library. Trigonometric functions use degrees.
47+ */
48+ allowMath ?: boolean ;
4449} ;
4550
4651const roundToMultiple = ( value : number , multiple : number ) : number => {
4752 return Math . round ( value / multiple ) * multiple ;
4853} ;
4954
5055const mexp = new Mexp ( ) ;
51- const isValidCharacter = ( _ : string ) => true ;
56+ const isValidCharacterAllowMath = ( _ : string ) => true ;
5257
5358export const CompositeNumberInput : ComponentWithAs <
5459 ComponentWithAs < 'div' , NumberInputProps > ,
@@ -64,6 +69,7 @@ export const CompositeNumberInput: ComponentWithAs<
6469 onChange : _onChange ,
6570 defaultValue,
6671 constrainValue,
72+ allowMath,
6773 ...rest
6874 } = props ;
6975 const [ localValue , setLocalValue ] = useState ( String ( value ) ) ;
@@ -84,27 +90,44 @@ export const CompositeNumberInput: ComponentWithAs<
8490 } , [ ] ) ;
8591
8692 const pushLocalValue = useCallback ( ( ) => {
87- let localValueAsNumber = Number ( localValue ) ;
88-
89- if ( isNaN ( localValueAsNumber ) ) {
90- localValueAsNumber = mexp . eval ( localValue ) ;
91-
92- if ( isNaN ( localValueAsNumber ) ) {
93- setLocalValue ( String ( isNumber ( defaultValue ) ? defaultValue : min ) ) ;
94- return ;
93+ let nextValue ;
94+ if ( allowMath ) {
95+ try {
96+ nextValue = mexp . eval ( localValue ) ;
97+ } catch {
98+ nextValue = NaN ;
9599 }
100+ } else {
101+ nextValue = Number ( localValue ) ;
102+ }
103+
104+ if ( isNaN ( nextValue ) ) {
105+ setLocalValue ( String ( isNumber ( defaultValue ) ? defaultValue : min ) ) ;
106+ return ;
96107 }
97108
98109 // Otherwise, we round the value to the nearest multiple if integer, else 3 decimals
99110 const roundedValue = isInteger
100- ? roundToMultiple ( localValueAsNumber , _fineStep ?? _step )
101- : Number ( localValueAsNumber . toFixed ( precision ) ) ;
111+ ? roundToMultiple ( nextValue , _fineStep ?? _step )
112+ : Number ( nextValue . toFixed ( precision ) ) ;
102113 // Clamp to min/max
103114 const clampedValue = clamp ( roundedValue , min , max ) ;
104115 const constrainedValue = constrainValue ? constrainValue ( clampedValue ) : clampedValue ;
105116 _onChange ( constrainedValue ) ;
106117 setLocalValue ( String ( constrainedValue ) ) ;
107- } , [ _fineStep , _onChange , _step , defaultValue , isInteger , localValue , max , min , precision , constrainValue ] ) ;
118+ } , [
119+ allowMath ,
120+ isInteger ,
121+ _fineStep ,
122+ _step ,
123+ precision ,
124+ min ,
125+ max ,
126+ constrainValue ,
127+ _onChange ,
128+ localValue ,
129+ defaultValue ,
130+ ] ) ;
108131
109132 const onClickStepper = useCallback ( ( ) => {
110133 pushLocalValue ( ) ;
@@ -137,7 +160,7 @@ export const CompositeNumberInput: ComponentWithAs<
137160 precision = { precision }
138161 variant = "filled"
139162 onKeyDown = { onKeyDown }
140- isValidCharacter = { isValidCharacter }
163+ isValidCharacter = { allowMath ? isValidCharacterAllowMath : undefined }
141164 { ...rest }
142165 >
143166 < NumberInputField onBlur = { pushLocalValue } />
0 commit comments