diff --git a/frontend/src/components/NumberComponent.tsx b/frontend/src/components/NumberComponent.tsx
new file mode 100644
index 0000000..5aa031c
--- /dev/null
+++ b/frontend/src/components/NumberComponent.tsx
@@ -0,0 +1,243 @@
+import React, { useEffect, useRef, useState } from 'react';
+import {
+ OverlayTrigger,
+ Badge,
+ Tooltip,
+ Form,
+ InputGroup,
+ Button
+} from 'react-bootstrap';
+import { socket } from '../socket';
+import './NumberComponent.css';
+
+interface ButtonComponentProps {
+ name: string;
+ parent_path?: string;
+ value: number;
+ readOnly: boolean;
+ docString: string;
+}
+
+const handleNumericKey = (key: string, value: string, selectionStart: number) => {
+ // Check if a number key or a decimal point key is pressed
+ if (key === '.' && value.includes('.')) {
+ // Check if value already contains a decimal. If so, ignore input.
+ // eslint-disable-next-line no-console
+ console.warn('Number already contains decimal! Ignoring...');
+ return { value, selectionStart };
+ }
+ // Add the new key at the cursor's position
+ const newValue = value.slice(0, selectionStart) + key + value.slice(selectionStart);
+ return { value: newValue, selectionStart: selectionStart + 1 };
+};
+
+// TODO: highlight the digit that is being changed by setting both selectionStart and
+// selectionEnd
+const handleArrowKey = (
+ key: string,
+ value: string,
+ selectionStart: number,
+ selectionEnd: number
+) => {
+ // Split the input value into the integer part and decimal part
+ const parts = value.split('.');
+ const beforeDecimalCount = parts[0].length; // Count digits before the decimal
+ const afterDecimalCount = parts[1] ? parts[1].length : 0; // Count digits after the decimal
+
+ const isCursorAfterDecimal = selectionStart > beforeDecimalCount;
+
+ // Calculate the increment/decrement value based on the cursor position
+ let increment = 0;
+ if (isCursorAfterDecimal) {
+ increment = Math.pow(10, beforeDecimalCount + 1 - selectionStart);
+ } else {
+ increment = Math.pow(10, beforeDecimalCount - selectionStart);
+ }
+
+ // Convert the input value to a number, increment or decrement it based on the
+ // arrow key
+ const numValue = parseFloat(value) + (key === 'ArrowUp' ? increment : -increment);
+
+ // Convert the resulting number to a string, maintaining the same number of digits
+ // after the decimal
+ const newValue = numValue.toFixed(afterDecimalCount);
+
+ // Check if the length of the integer part of the number string has in-/decreased
+ const newBeforeDecimalCount = newValue.split('.')[0].length;
+ if (newBeforeDecimalCount > beforeDecimalCount) {
+ // Move the cursor one position to the right
+ selectionStart += 1;
+ } else if (newBeforeDecimalCount < beforeDecimalCount) {
+ // Move the cursor one position to the left
+ selectionStart -= 1;
+ }
+ return { value: newValue, selectionStart };
+};
+
+const handleBackspaceKey = (
+ value: string,
+ selectionStart: number,
+ selectionEnd: number
+) => {
+ if (selectionEnd > selectionStart) {
+ // If there is a selection, delete all characters in the selection
+ return {
+ value: value.slice(0, selectionStart) + value.slice(selectionEnd),
+ selectionStart
+ };
+ } else if (selectionStart > 0) {
+ return {
+ value: value.slice(0, selectionStart - 1) + value.slice(selectionStart),
+ selectionStart: selectionStart - 1
+ };
+ }
+ return { value, selectionStart };
+};
+
+const handleDeleteKey = (
+ value: string,
+ selectionStart: number,
+ selectionEnd: number
+) => {
+ if (selectionEnd > selectionStart) {
+ // If there is a selection, delete all characters in the selection
+ return {
+ value: value.slice(0, selectionStart) + value.slice(selectionEnd),
+ selectionStart
+ };
+ } else if (selectionStart < value.length) {
+ return {
+ value: value.slice(0, selectionStart) + value.slice(selectionStart + 1),
+ selectionStart
+ };
+ }
+ return { value, selectionStart };
+};
+
+export const NumberComponent = React.memo((props: ButtonComponentProps) => {
+ const renderCount = useRef(0);
+ // Create a state for the cursor position
+ const [cursorPosition, setCursorPosition] = useState(null);
+ // Create a state for the input string
+ const [inputString, setInputString] = useState(props.value.toString());
+
+ useEffect(() => {
+ renderCount.current++;
+
+ // Set the cursor position and input string after the component re-renders
+ const inputElement = document.getElementsByName(name)[0] as HTMLInputElement;
+ if (inputElement && cursorPosition !== null) {
+ // Setting input string as trailing zeros after the decimal will be stripped
+ // otherwise
+ inputElement.value = inputString;
+
+ inputElement.setSelectionRange(cursorPosition, cursorPosition);
+ }
+ });
+
+ const { name, parent_path, value, readOnly, docString } = props;
+
+ const tooltip =
Render count: {renderCount.current}
+