From fc8f32b00c518b8de31103daa52c7d344e0d5cf2 Mon Sep 17 00:00:00 2001 From: NovaBot <154629622+NovaBot13@users.noreply.github.com> Date: Sun, 24 Mar 2024 10:48:56 -0400 Subject: [PATCH] [MIRROR] Typescript refactor for Number Input (#1429) * Typescript refactor for Number Input (#81913) ## About The Pull Request - Fixes #80971 - Fixes #81379 You now only click once to highlight & edit the value. Converted NumberInput to typescript ## Changelog :cl: fix: NumberInput(used in chem heater, plumbing reaction chamber etc) highlighting & editing requires only single click like before refactor: Typescript conversion for NumberInput TGUI Component /:cl: --------- Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> * Typescript refactor for Number Input --------- Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> --- tgui/packages/tgui/components/NumberInput.jsx | 288 --------------- tgui/packages/tgui/components/NumberInput.tsx | 337 ++++++++++++++++++ tgui/packages/tgui/interfaces/BountyBoard.tsx | 2 +- .../CentcomPodLauncher/PresetsPage.tsx | 1 - tgui/packages/tgui/interfaces/ChemPress.tsx | 6 +- .../tgui/interfaces/ColorMatrixEditor.tsx | 5 +- .../tgui/interfaces/MatrixMathTester.tsx | 14 + .../tgui/interfaces/Mecha/ModulesPane.tsx | 4 +- .../interfaces/ParticleEdit/EntriesBasic.tsx | 19 +- .../ParticleEdit/EntriesGenerators.tsx | 14 +- .../interfaces/ParticleEdit/Generators.tsx | 15 + tgui/packages/tgui/interfaces/Signaler.tsx | 4 +- 12 files changed, 403 insertions(+), 306 deletions(-) delete mode 100644 tgui/packages/tgui/components/NumberInput.jsx create mode 100644 tgui/packages/tgui/components/NumberInput.tsx 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: {