Skip to content

Commit 959c9a7

Browse files
authored
Update number input to respect number type (#22)
* Update NumberTextField to NumberField instead of TextField * Create custom numberField with error state * Remove SpectroscopyFormData from NumberField * Change visit input on spectroscopy to VisitInput with validation * rename NumberFieldInput.tsx to NumberInput.tsx * Tidy up * change Modes case in NumberInput.tsx * add tests for NumberInput, add validation for default value * remove unnecessary exports from NumberInput * change Submit to Commit in NumberInput, remove button * remove submit button from visit input on spectroscopy page
1 parent 6de2650 commit 959c9a7

File tree

4 files changed

+314
-85
lines changed

4 files changed

+314
-85
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { render, screen } from "@testing-library/react";
2+
import { NumberInput } from "./NumberInput";
3+
import userEvent from "@testing-library/user-event";
4+
5+
describe("NumberInput", () => {
6+
it("default value is marked invalid", async () => {
7+
render(
8+
<NumberInput label="numberbox" numberMode="natural" defaultValue={-5} />,
9+
);
10+
11+
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
12+
});
13+
14+
it("default value is marked valid", async () => {
15+
render(
16+
<NumberInput label="numberbox" numberMode="natural" defaultValue={5} />,
17+
);
18+
19+
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
20+
});
21+
22+
it("does not accept negative numbers in natural mode", async () => {
23+
render(
24+
<NumberInput label="numberbox" numberMode="natural" defaultValue={1} />,
25+
);
26+
27+
const numberInput = screen.getByLabelText("numberbox");
28+
29+
const user = userEvent.setup();
30+
await user.clear(numberInput);
31+
await user.type(numberInput, "-5");
32+
33+
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
34+
});
35+
36+
it("accepts positive numbers in natural mode", async () => {
37+
render(
38+
<NumberInput label="numberbox" numberMode="natural" defaultValue={1} />,
39+
);
40+
41+
const numberInput = screen.getByLabelText("numberbox");
42+
43+
const user = userEvent.setup();
44+
await user.clear(numberInput);
45+
await user.type(numberInput, "5");
46+
47+
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
48+
});
49+
50+
it("does not accept decimal numbers in integer mode", async () => {
51+
render(
52+
<NumberInput label="numberbox" numberMode="integer" defaultValue={0} />,
53+
);
54+
55+
const numberInput = screen.getByLabelText("numberbox");
56+
57+
const user = userEvent.setup();
58+
await user.clear(numberInput);
59+
await user.type(numberInput, "-5.2");
60+
61+
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
62+
});
63+
64+
it("accepts negative numbers in integer mode", async () => {
65+
render(
66+
<NumberInput label="numberbox" numberMode="integer" defaultValue={0} />,
67+
);
68+
69+
const numberInput = screen.getByLabelText("numberbox");
70+
71+
const user = userEvent.setup();
72+
await user.clear(numberInput);
73+
await user.type(numberInput, "-5");
74+
75+
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
76+
});
77+
78+
it("does not accept scientific numbers in floating mode", async () => {
79+
render(
80+
<NumberInput label="numberbox" numberMode="floating" defaultValue={0} />,
81+
);
82+
83+
const numberInput = screen.getByLabelText("numberbox");
84+
85+
const user = userEvent.setup();
86+
await user.clear(numberInput);
87+
await user.type(numberInput, "-5e5");
88+
89+
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
90+
});
91+
92+
it("accepts decimal numbers in floating mode", async () => {
93+
render(
94+
<NumberInput label="numberbox" numberMode="floating" defaultValue={0} />,
95+
);
96+
97+
const numberInput = screen.getByLabelText("numberbox");
98+
99+
const user = userEvent.setup();
100+
await user.clear(numberInput);
101+
await user.type(numberInput, "-5.2");
102+
103+
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
104+
});
105+
106+
it("does not accept non-number characters in scientific mode", async () => {
107+
render(
108+
<NumberInput
109+
label="numberbox"
110+
numberMode="scientific"
111+
defaultValue={0}
112+
/>,
113+
);
114+
115+
const numberInput = screen.getByLabelText("numberbox");
116+
117+
const user = userEvent.setup();
118+
await user.clear(numberInput);
119+
await user.type(numberInput, "-5e5!");
120+
121+
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
122+
});
123+
124+
it("accepts scientific numbers in scientific mode", async () => {
125+
render(
126+
<NumberInput
127+
label="numberbox"
128+
numberMode="scientific"
129+
defaultValue={0}
130+
/>,
131+
);
132+
133+
const numberInput = screen.getByLabelText("numberbox");
134+
135+
const user = userEvent.setup();
136+
await user.clear(numberInput);
137+
await user.type(numberInput, "5e5");
138+
139+
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
140+
});
141+
});

src/components/NumberInput.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { useState } from "react";
2+
import { TextField } from "@mui/material";
3+
4+
const Modes = {
5+
/** Natural numbers from 0 to inf */
6+
natural: /^([0-9]+)$/,
7+
/** Integers from -inf to inf */
8+
integer: /^[+\\-]?([0-9]+)$/,
9+
/** Floating point numbers from -inf to inf, accepts values such as 1. and .1 as valid*/
10+
floating:
11+
/^[+\\-]?(([0-9]+)|([0-9]+[\\.])|([\\.][0-9]+)|([0-9]+[\\.][0-9]+))$/,
12+
/** Floating point numbers from -inf to inf, accepts values such as 1.e1 and .1e1 as valid*/
13+
scientific:
14+
/^[+\\-]?(([0-9]+)|([0-9]+[\\.])|([\\.][0-9]+)|([0-9]+[\\.][0-9]+))([eE][+\\-]?[0-9]+)?$/,
15+
};
16+
17+
interface NumberInputTextProps {
18+
label: string;
19+
numberMode: keyof typeof Modes;
20+
numberText: string;
21+
setNumberText: (v: string) => void;
22+
isValid: boolean;
23+
setIsValid: (v: boolean) => void;
24+
handleCommit?: () => void;
25+
commitOnReturn?: boolean;
26+
commitOnBlur?: boolean;
27+
}
28+
29+
const NumberInputText: React.FC<NumberInputTextProps> = ({
30+
label,
31+
numberMode,
32+
numberText,
33+
setNumberText,
34+
isValid,
35+
setIsValid,
36+
handleCommit,
37+
commitOnReturn,
38+
commitOnBlur,
39+
}) => {
40+
const numberRegex = Modes[numberMode];
41+
42+
const handleInputChange = (value: string) => {
43+
setIsValid(numberRegex.test(value));
44+
setNumberText(value);
45+
};
46+
47+
const handleKeyDown = (event: { key: string }) => {
48+
if (event.key === "Enter" && commitOnReturn && isValid && handleCommit) {
49+
handleCommit();
50+
}
51+
};
52+
53+
const handleBlur = () => {
54+
if (isValid && commitOnBlur && handleCommit) {
55+
handleCommit();
56+
}
57+
};
58+
59+
return (
60+
<TextField
61+
label={label}
62+
value={numberText}
63+
onChange={e => handleInputChange(e.target.value)}
64+
onKeyDown={handleKeyDown}
65+
onBlur={handleBlur}
66+
error={!isValid}
67+
helperText={!isValid ? "Invalid input" : ""}
68+
variant="outlined"
69+
/>
70+
);
71+
};
72+
73+
interface NumberInputProps {
74+
label: string;
75+
numberMode: keyof typeof Modes;
76+
defaultValue: number | string;
77+
onCommit?: (number: number) => void;
78+
number?: number;
79+
parameters?: object;
80+
commitOnReturn?: boolean;
81+
commitOnBlur?: boolean;
82+
}
83+
84+
const NumberInput: React.FC<NumberInputProps> = ({
85+
label,
86+
numberMode = "floating",
87+
defaultValue,
88+
onCommit,
89+
commitOnReturn = true,
90+
commitOnBlur = true,
91+
}) => {
92+
const [numberText, setNumberText] = useState(defaultValue.toString());
93+
const [isValid, setIsValid] = useState(
94+
Modes[numberMode].test(defaultValue.toString()),
95+
);
96+
97+
const handleCommit = () => {
98+
const parsedValue: number = parseFloat(numberText);
99+
if (onCommit) {
100+
onCommit(parsedValue);
101+
}
102+
};
103+
104+
return (
105+
<>
106+
{
107+
<NumberInputText
108+
label={label}
109+
numberMode={numberMode}
110+
numberText={numberText}
111+
setNumberText={setNumberText}
112+
isValid={isValid}
113+
setIsValid={setIsValid}
114+
handleCommit={handleCommit}
115+
commitOnReturn={commitOnReturn}
116+
commitOnBlur={commitOnBlur}
117+
/>
118+
}
119+
</>
120+
);
121+
};
122+
123+
export { NumberInput };
124+
export type { NumberInputProps };

src/components/spectroscopy/NumberTextField.tsx

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)