diff --git a/tgui/packages/tgui/components/NumberInput.jsx b/tgui/packages/tgui/components/NumberInput.jsx
deleted file mode 100644
index f5579731c0b..00000000000
--- a/tgui/packages/tgui/components/NumberInput.jsx
+++ /dev/null
@@ -1,288 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { clamp } from 'common/math';
-import { classes } from 'common/react';
-import { Component, createRef } from 'react';
-
-import { AnimatedNumber } from './AnimatedNumber';
-import { Box } from './Box';
-
-const DEFAULT_UPDATE_RATE = 400;
-
-export class NumberInput extends Component {
- constructor(props) {
- super(props);
- const { value } = props;
- this.inputRef = createRef();
- this.state = {
- value,
- dragging: false,
- editing: false,
- internalValue: null,
- origin: null,
- suppressingFlicker: false,
- };
-
- // Suppresses flickering while the value propagates through the backend
- this.flickerTimer = null;
- this.suppressFlicker = () => {
- const { suppressFlicker } = this.props;
- if (suppressFlicker > 0) {
- this.setState({
- suppressingFlicker: true,
- });
- clearTimeout(this.flickerTimer);
- this.flickerTimer = setTimeout(
- () =>
- this.setState({
- suppressingFlicker: false,
- }),
- suppressFlicker,
- );
- }
- };
-
- this.handleDragStart = (e) => {
- const { value } = this.props;
- const { editing } = this.state;
- if (editing) {
- return;
- }
- document.body.style['pointer-events'] = 'none';
- this.ref = e.target;
- this.setState({
- dragging: false,
- origin: e.screenY,
- value,
- internalValue: value,
- });
- this.timer = setTimeout(() => {
- this.setState({
- dragging: true,
- });
- }, 250);
- this.dragInterval = setInterval(() => {
- const { dragging, value } = this.state;
- const { onDrag } = this.props;
- if (dragging && onDrag) {
- onDrag(e, value);
- }
- }, this.props.updateRate || DEFAULT_UPDATE_RATE);
- document.addEventListener('mousemove', this.handleDragMove);
- document.addEventListener('mouseup', this.handleDragEnd);
- };
-
- this.handleDragMove = (e) => {
- const { minValue, maxValue, step, stepPixelSize } = this.props;
- this.setState((prevState) => {
- const state = { ...prevState };
- const offset = state.origin - e.screenY;
- if (prevState.dragging) {
- const stepOffset = Number.isFinite(minValue) ? minValue % step : 0;
- // Translate mouse movement to value
- // Give it some headroom (by increasing clamp range by 1 step)
- state.internalValue = clamp(
- state.internalValue + (offset * step) / stepPixelSize,
- minValue - step,
- maxValue + step,
- );
- // Clamp the final value
- state.value = clamp(
- state.internalValue - (state.internalValue % step) + stepOffset,
- minValue,
- maxValue,
- );
- state.origin = e.screenY;
- } else if (Math.abs(offset) > 4) {
- state.dragging = true;
- }
- return state;
- });
- };
-
- this.handleDragEnd = (e) => {
- const { onChange, onDrag } = this.props;
- const { dragging, value, internalValue } = this.state;
- document.body.style['pointer-events'] = 'auto';
- clearTimeout(this.timer);
- clearInterval(this.dragInterval);
- this.setState({
- dragging: false,
- editing: !dragging,
- origin: null,
- });
- document.removeEventListener('mousemove', this.handleDragMove);
- document.removeEventListener('mouseup', this.handleDragEnd);
- if (dragging) {
- this.suppressFlicker();
- if (onChange) {
- onChange(e, value);
- }
- if (onDrag) {
- onDrag(e, value);
- }
- } else if (this.inputRef) {
- const input = this.inputRef.current;
- input.value = internalValue;
- // IE8: Dies when trying to focus a hidden element
- // (Error: Object does not support this action)
- try {
- input.focus();
- input.select();
- } catch {}
- }
- };
- }
-
- render() {
- const {
- dragging,
- editing,
- value: intermediateValue,
- suppressingFlicker,
- } = this.state;
- const {
- className,
- fluid,
- animated,
- value,
- unit,
- minValue,
- maxValue,
- height,
- width,
- lineHeight,
- fontSize,
- format,
- onChange,
- onDrag,
- } = this.props;
- let displayValue = value;
- if (dragging || suppressingFlicker) {
- displayValue = intermediateValue;
- }
-
- const contentElement = (
-
- {animated && !dragging && !suppressingFlicker ? (
-
- ) : format ? (
- format(displayValue)
- ) : (
- displayValue
- )}
-
- {unit ? ' ' + unit : ''}
-
- );
-
- return (
-
-
- {contentElement}
- {
- if (!editing) {
- return;
- }
- const value = clamp(parseFloat(e.target.value), minValue, maxValue);
- if (Number.isNaN(value)) {
- this.setState({
- editing: false,
- });
- return;
- }
- this.setState({
- editing: false,
- value,
- });
- this.suppressFlicker();
- if (onChange) {
- onChange(e, value);
- }
- if (onDrag) {
- onDrag(e, value);
- }
- }}
- onKeyDown={(e) => {
- if (e.keyCode === 13) {
- // prettier-ignore
- const value = clamp(
- parseFloat(e.target.value),
- minValue,
- maxValue
- );
- if (Number.isNaN(value)) {
- this.setState({
- editing: false,
- });
- return;
- }
- this.setState({
- editing: false,
- value,
- });
- this.suppressFlicker();
- if (onChange) {
- onChange(e, value);
- }
- if (onDrag) {
- onDrag(e, value);
- }
- return;
- }
- if (e.keyCode === 27) {
- this.setState({
- editing: false,
- });
- return;
- }
- }}
- />
-
- );
- }
-}
-
-NumberInput.defaultProps = {
- minValue: -Infinity,
- maxValue: +Infinity,
- step: 1,
- stepPixelSize: 1,
- suppressFlicker: 50,
-};
diff --git a/tgui/packages/tgui/components/NumberInput.tsx b/tgui/packages/tgui/components/NumberInput.tsx
new file mode 100644
index 00000000000..dbbcfb62dac
--- /dev/null
+++ b/tgui/packages/tgui/components/NumberInput.tsx
@@ -0,0 +1,337 @@
+import { KEY } from 'common/keys';
+import { clamp } from 'common/math';
+import { BooleanLike, classes } from 'common/react';
+import {
+ Component,
+ createRef,
+ FocusEventHandler,
+ KeyboardEventHandler,
+ MouseEventHandler,
+ RefObject,
+} from 'react';
+
+import { AnimatedNumber } from './AnimatedNumber';
+import { Box } from './Box';
+
+const DEFAULT_UPDATE_RATE = 400;
+
+type Props = Required<{
+ value: number | string;
+ minValue: number;
+ maxValue: number;
+}> &
+ Partial<{
+ step: number;
+ stepPixelSize: number;
+ suppressFlicker: number;
+ disabled: BooleanLike;
+
+ className: string;
+ fluid: BooleanLike;
+ animated: BooleanLike;
+ unit: string;
+ height: string;
+ width: string;
+ lineHeight: string;
+ fontSize: string;
+ updateRate: number;
+ format: (value: number) => string;
+ onChange: (e: any, value: number) => void;
+ onDrag: (e: any, value: number) => void;
+ }>;
+
+type State = {
+ value: number;
+ dragging: BooleanLike;
+ editing: BooleanLike;
+ internalValue: number;
+ origin: number;
+ suppressingFlicker: BooleanLike;
+};
+
+export class NumberInput extends Component {
+ // Ref to the input field to set focus & highlight
+ inputRef: RefObject = createRef();
+
+ // Timer id for the flicker id
+ flickerTimer: NodeJS.Timeout;
+
+ // After this time has elapsed we are in drag mode so no editing when dragging ends
+ dragTimeout: NodeJS.Timeout;
+
+ // Call onDrag at this interval
+ dragInterval: NodeJS.Timeout;
+
+ // default values for the number input state
+ state: State = {
+ value: 0,
+ dragging: false,
+ editing: false,
+ internalValue: 0,
+ origin: 0,
+ suppressingFlicker: false,
+ };
+
+ // default values for the number input props
+ static defaultProps = {
+ step: 1,
+ stepPixelSize: 1,
+ suppressFlicker: 50,
+ };
+
+ constructor(props: Props) {
+ super(props);
+ }
+
+ suppressFlicker = () => {
+ const { suppressFlicker } = this.props;
+ if (suppressFlicker) {
+ this.setState({
+ suppressingFlicker: true,
+ });
+ clearTimeout(this.flickerTimer);
+ this.flickerTimer = setTimeout(
+ () =>
+ this.setState({
+ suppressingFlicker: false,
+ }),
+ suppressFlicker,
+ );
+ }
+ };
+
+ handleDragStart: MouseEventHandler = (event) => {
+ const { value, updateRate, disabled } = this.props;
+ const { editing } = this.state;
+ if (disabled || editing) {
+ return;
+ }
+ this.setState({
+ dragging: false,
+ origin: event.screenY,
+ internalValue: parseFloat(value.toString()),
+ });
+ this.dragTimeout = setTimeout(() => {
+ this.setState({
+ dragging: true,
+ });
+ }, 250);
+
+ this.dragInterval = setInterval(() => {
+ const { dragging, value } = this.state;
+ const { onDrag } = this.props;
+ if (dragging) {
+ onDrag?.(event, value);
+ }
+ }, updateRate || DEFAULT_UPDATE_RATE);
+ };
+
+ handleDragMove: MouseEventHandler = (event) => {
+ const { minValue, maxValue, step, stepPixelSize, disabled } = this.props;
+ const { dragging } = this.state;
+ if (disabled || !dragging) {
+ return;
+ }
+
+ this.setState((prevState) => {
+ const state = { ...prevState };
+ const offset = state.origin - event.screenY;
+ if (prevState.dragging && step) {
+ const stepOffset = isFinite(minValue) ? minValue % step : 0;
+ // Translate mouse movement to value
+ // Give it some headroom (by increasing clamp range by 1 step)
+ state.internalValue = clamp(
+ state.internalValue + (offset * step) / (stepPixelSize || 1),
+ minValue - step,
+ maxValue + step,
+ );
+ // Clamp the final value
+ state.value = clamp(
+ state.internalValue - (state.internalValue % step) + stepOffset,
+ minValue,
+ maxValue,
+ );
+ state.origin = event.screenY;
+ } else if (Math.abs(offset) > 4) {
+ state.dragging = true;
+ }
+ return state;
+ });
+ };
+
+ handleDragEnd: MouseEventHandler = (event) => {
+ const { value, dragging, internalValue } = this.state;
+ const { onDrag, onChange, disabled } = this.props;
+ if (disabled) {
+ return;
+ }
+
+ clearInterval(this.dragInterval);
+ clearTimeout(this.dragTimeout);
+ this.setState({
+ dragging: false,
+ editing: !dragging,
+ });
+
+ if (dragging) {
+ this.suppressFlicker();
+ onChange?.(event, value);
+ onDrag?.(event, value);
+ } else if (this.inputRef) {
+ const input = this.inputRef.current;
+ if (input) {
+ input.value = `${internalValue}`;
+ setTimeout(() => {
+ input.focus();
+ input.select();
+ }, 1);
+ }
+ }
+ };
+
+ handleBlur: FocusEventHandler = (event) => {
+ const { editing } = this.state;
+ const { minValue, maxValue, onChange, onDrag, disabled } = this.props;
+
+ if (disabled || !editing) {
+ return;
+ }
+
+ const targetValue = clamp(
+ parseFloat(event.target.value),
+ minValue,
+ maxValue,
+ );
+ if (isNaN(targetValue)) {
+ this.setState({
+ editing: false,
+ });
+ return;
+ }
+ this.setState({
+ editing: false,
+ });
+ this.suppressFlicker();
+ onChange?.(event, targetValue);
+ onDrag?.(event, targetValue);
+ };
+
+ handleKeyDown: KeyboardEventHandler = (event) => {
+ const { minValue, maxValue, onChange, onDrag, disabled } = this.props;
+ if (disabled) {
+ return;
+ }
+
+ if (event.key === KEY.Enter) {
+ const targetValue = clamp(
+ parseFloat(event.currentTarget.value),
+ minValue,
+ maxValue,
+ );
+ if (isNaN(targetValue)) {
+ this.setState({
+ editing: false,
+ });
+ return;
+ }
+ this.setState({
+ editing: false,
+ value: targetValue,
+ });
+ this.suppressFlicker();
+ onChange?.(event, targetValue);
+ onDrag?.(event, targetValue);
+ } else if (event.key === KEY.Escape) {
+ this.setState({
+ editing: false,
+ });
+ }
+ };
+
+ render() {
+ const {
+ dragging,
+ editing,
+ value: intermediateValue,
+ suppressingFlicker,
+ } = this.state;
+
+ const {
+ className,
+ fluid,
+ animated,
+ value,
+ unit,
+ minValue,
+ maxValue,
+ height,
+ width,
+ lineHeight,
+ fontSize,
+ format,
+ } = this.props;
+
+ let displayValue = parseFloat(value.toString());
+ if (dragging || suppressingFlicker) {
+ displayValue = intermediateValue;
+ }
+
+ const contentElement = (
+
+ {animated && !dragging && !suppressingFlicker ? (
+
+ ) : format ? (
+ format(displayValue)
+ ) : (
+ displayValue
+ )}
+
+ {unit ? ' ' + unit : ''}
+
+ );
+
+ return (
+
+
+ {contentElement}
+
+
+ );
+ }
+}
diff --git a/tgui/packages/tgui/interfaces/BountyBoard.tsx b/tgui/packages/tgui/interfaces/BountyBoard.tsx
index fcaa3556a70..f0e0eecd86c 100644
--- a/tgui/packages/tgui/interfaces/BountyBoard.tsx
+++ b/tgui/packages/tgui/interfaces/BountyBoard.tsx
@@ -173,7 +173,7 @@ export const BountyBoardContent = (props) => {
/>
Hue:
setHue(value)}
diff --git a/tgui/packages/tgui/interfaces/ChemPress.tsx b/tgui/packages/tgui/interfaces/ChemPress.tsx
index f3881061696..6d3c0ead35e 100644
--- a/tgui/packages/tgui/interfaces/ChemPress.tsx
+++ b/tgui/packages/tgui/interfaces/ChemPress.tsx
@@ -23,10 +23,10 @@ type Category = {
};
type Data = {
- current_volume: Number;
+ current_volume: number;
product_name: string;
- min_volume: Number;
- max_volume: Number;
+ min_volume: number;
+ max_volume: number;
packaging_category: string;
packaging_types: Category[];
packaging_type: string;
diff --git a/tgui/packages/tgui/interfaces/ColorMatrixEditor.tsx b/tgui/packages/tgui/interfaces/ColorMatrixEditor.tsx
index 2b3ae692915..c3b9529fd9f 100644
--- a/tgui/packages/tgui/interfaces/ColorMatrixEditor.tsx
+++ b/tgui/packages/tgui/interfaces/ColorMatrixEditor.tsx
@@ -41,14 +41,15 @@ export const ColorMatrixEditor = (props) => {
{`${PREFIXES[row]}${PREFIXES[col]}:`}
toFixed(value, 2)}
onDrag={(_, value) => {
let retColor = currentColor;
- retColor[row * 4 + col] = value;
+ retColor[row * 4 + col] = `${value}`;
act('transition_color', {
color: retColor,
});
diff --git a/tgui/packages/tgui/interfaces/MatrixMathTester.tsx b/tgui/packages/tgui/interfaces/MatrixMathTester.tsx
index 1f67fd3f3eb..6ba7bce6377 100644
--- a/tgui/packages/tgui/interfaces/MatrixMathTester.tsx
+++ b/tgui/packages/tgui/interfaces/MatrixMathTester.tsx
@@ -9,6 +9,8 @@ const MatrixMathTesterInput = (props: { value: number; varName: string }) => {
const { act } = useBackend();
return (
toFixed(value, 3)}
@@ -111,6 +113,8 @@ export const MatrixMathTester = (props) => {
toFixed(value, 2)}
@@ -120,6 +124,8 @@ export const MatrixMathTester = (props) => {
toFixed(value, 2)}
@@ -142,6 +148,8 @@ export const MatrixMathTester = (props) => {
toFixed(value, 0)}
@@ -151,6 +159,8 @@ export const MatrixMathTester = (props) => {
toFixed(value, 0)}
@@ -171,6 +181,8 @@ export const MatrixMathTester = (props) => {
toFixed(value, 3)}
@@ -180,6 +192,8 @@ export const MatrixMathTester = (props) => {
toFixed(value, 3)}
diff --git a/tgui/packages/tgui/interfaces/Mecha/ModulesPane.tsx b/tgui/packages/tgui/interfaces/Mecha/ModulesPane.tsx
index 99b0fd4b7a7..f8f6af31f3e 100644
--- a/tgui/packages/tgui/interfaces/Mecha/ModulesPane.tsx
+++ b/tgui/packages/tgui/interfaces/Mecha/ModulesPane.tsx
@@ -547,7 +547,7 @@ const SnowflakeRadio = (props) => {
{
minValue={tank_pump_pressure_min}
maxValue={tank_pump_pressure_max}
step={10}
- format={(value) => Math.round(value)}
+ format={(value) => `${Math.round(value)}`}
onChange={(e, value) =>
act('equip_act', {
ref: ref,
diff --git a/tgui/packages/tgui/interfaces/ParticleEdit/EntriesBasic.tsx b/tgui/packages/tgui/interfaces/ParticleEdit/EntriesBasic.tsx
index 3509c6d1851..7e48a4f46fb 100644
--- a/tgui/packages/tgui/interfaces/ParticleEdit/EntriesBasic.tsx
+++ b/tgui/packages/tgui/interfaces/ParticleEdit/EntriesBasic.tsx
@@ -40,6 +40,7 @@ export const EntryFloat = (props: EntryFloatProps) => {
animated
value={float}
minValue={0}
+ maxValue={Infinity}
onDrag={(e, value) =>
act('edit', {
var: var_name,
@@ -64,7 +65,9 @@ export const EntryCoord = (props: EntryCoordProps) => {
/>
act('edit', {
var: var_name,
@@ -74,7 +77,9 @@ export const EntryCoord = (props: EntryCoordProps) => {
/>
act('edit', {
var: var_name,
@@ -84,7 +89,9 @@ export const EntryCoord = (props: EntryCoordProps) => {
/>
act('edit', {
var: var_name,
@@ -274,8 +281,9 @@ export const EntryIcon = (props: EntryIconStateProps) => {
act('edit', {
var: var_name,
@@ -363,8 +371,9 @@ export const EntryIconState = (props: EntryIconStateProps) => {
act('edit', {
var: var_name,
diff --git a/tgui/packages/tgui/interfaces/ParticleEdit/EntriesGenerators.tsx b/tgui/packages/tgui/interfaces/ParticleEdit/EntriesGenerators.tsx
index 3365b55a61c..9299228f4ae 100644
--- a/tgui/packages/tgui/interfaces/ParticleEdit/EntriesGenerators.tsx
+++ b/tgui/packages/tgui/interfaces/ParticleEdit/EntriesGenerators.tsx
@@ -52,7 +52,9 @@ export const FloatGenerator = (props: FloatGeneratorProps) => {
act('edit', {
var: var_name,
@@ -177,7 +179,9 @@ export const EntryGeneratorNumbersList = (
act('edit', {
var: var_name,
@@ -196,6 +200,8 @@ export const EntryGeneratorNumbersList = (
act('edit', {
@@ -206,6 +212,8 @@ export const EntryGeneratorNumbersList = (
/>
act('edit', {
@@ -217,6 +225,8 @@ export const EntryGeneratorNumbersList = (
{allow_z ? (
act('edit', {
diff --git a/tgui/packages/tgui/interfaces/ParticleEdit/Generators.tsx b/tgui/packages/tgui/interfaces/ParticleEdit/Generators.tsx
index c92e4b5be07..367493acc42 100644
--- a/tgui/packages/tgui/interfaces/ParticleEdit/Generators.tsx
+++ b/tgui/packages/tgui/interfaces/ParticleEdit/Generators.tsx
@@ -74,6 +74,7 @@ export const GeneratorListEntry = (props: GeneratorProps) => {
new_value: [type, value, B, RandToNumber[rand_type]],
})
}
+ maxValue={Infinity}
/>
) : (
<>
@@ -92,6 +93,8 @@ export const GeneratorListEntry = (props: GeneratorProps) => {
],
})
}
+ minValue={-Infinity}
+ maxValue={Infinity}
/>
{
new_value: [type, [A[0], value, A[2]], B, rand_type],
})
}
+ minValue={-Infinity}
+ maxValue={Infinity}
/>
{
new_value: [type, [A[0], A[1], value], B, rand_type],
})
}
+ minValue={-Infinity}
+ maxValue={Infinity}
/>
>
)}
@@ -131,6 +138,8 @@ export const GeneratorListEntry = (props: GeneratorProps) => {
new_value: [type, A, value, RandToNumber[rand_type]],
})
}
+ minValue={-Infinity}
+ maxValue={Infinity}
/>
) : (
<>
@@ -149,6 +158,8 @@ export const GeneratorListEntry = (props: GeneratorProps) => {
],
})
}
+ minValue={-Infinity}
+ maxValue={Infinity}
/>
{
],
})
}
+ minValue={-Infinity}
+ maxValue={Infinity}
/>
{
],
})
}
+ minValue={-Infinity}
+ maxValue={Infinity}
/>
>
)}
diff --git a/tgui/packages/tgui/interfaces/Signaler.tsx b/tgui/packages/tgui/interfaces/Signaler.tsx
index 15a486a62df..a50ecae7002 100644
--- a/tgui/packages/tgui/interfaces/Signaler.tsx
+++ b/tgui/packages/tgui/interfaces/Signaler.tsx
@@ -35,7 +35,7 @@ export const SignalerContent = (props) => {
Frequency:
{