diff --git a/src/components/Controls/NumberField/NumberField.jsx b/src/components/Controls/NumberField/NumberField.jsx
new file mode 100644
index 0000000..d623365
--- /dev/null
+++ b/src/components/Controls/NumberField/NumberField.jsx
@@ -0,0 +1,44 @@
+import {
+ Box,
+ FormControl,
+ FormHelperText,
+ Input,
+ InputLabel,
+} from "@material-ui/core";
+
+const PREFIX = "number-field";
+
+function kebab(str) {
+ return str.replaceAll(" ", "-").toLowerCase();
+}
+
+export function NumberField({
+ name,
+ description,
+ value = 0,
+ setValue = () => { },
+ ...rest
+}) {
+ const inputId = `${PREFIX}-${kebab(name)}`;
+ const describeId = `${PREFIX}-${kebab(name)}-describe`;
+
+ const handleChange = (event) => {
+ setValue(event.target.value);
+ }
+ return (
+
+
+ {name}
+
+ {description}
+
+
+ );
+}
diff --git a/src/components/Controls/NumberField/NumberField.test.jsx b/src/components/Controls/NumberField/NumberField.test.jsx
new file mode 100644
index 0000000..5c274d1
--- /dev/null
+++ b/src/components/Controls/NumberField/NumberField.test.jsx
@@ -0,0 +1,55 @@
+import React from "react";
+import { fireEvent, render } from "@testing-library/react";
+
+import { NumberField } from "./index";
+
+describe("NumberField", () => {
+ it("renders with form label and aria describe", () => {
+ const { getByText } = render(
+
+ );
+
+ const labelEl = getByText("My First Field");
+ const describeEl = getByText("Some helper text");
+
+ expect(labelEl.htmlFor).toEqual("number-field-my-first-field");
+ expect(describeEl.id).toEqual("number-field-my-first-field-describe");
+ });
+
+ it("input is of type number", () => {
+ const { getByLabelText } = render(
+
+ );
+
+ const inputEl = getByLabelText("My First Field", { selector: "input" });
+
+ expect(inputEl.type).toEqual("number");
+ });
+
+ it("updates parent state", () => {
+ let mockState = 0;
+ const mockSetState = jest.fn((value) => mockState = value);
+
+ const { getByLabelText } = render(
+
+ );
+
+ const inputEl = getByLabelText("My First Field", { selector: "input" });
+
+ fireEvent.change(inputEl, { target: { value: "1" } });
+
+ expect(mockState).toEqual("1");
+ expect(mockSetState.mock.calls.length).toEqual(1);
+ });
+})
\ No newline at end of file
diff --git a/src/components/Controls/NumberField/index.jsx b/src/components/Controls/NumberField/index.jsx
new file mode 100644
index 0000000..a7245fa
--- /dev/null
+++ b/src/components/Controls/NumberField/index.jsx
@@ -0,0 +1 @@
+export * from "./NumberField";
\ No newline at end of file
diff --git a/src/components/Manifest/Update/__snapshots__/index.test.jsx.snap b/src/components/Manifest/Update/__snapshots__/index.test.jsx.snap
index 9874fa0..8fce96e 100644
--- a/src/components/Manifest/Update/__snapshots__/index.test.jsx.snap
+++ b/src/components/Manifest/Update/__snapshots__/index.test.jsx.snap
@@ -112,6 +112,43 @@ exports[`Manifest Details Component Render 1`] = `
+
+
+
+
+
+
+
@@ -334,6 +371,68 @@ exports[`Manifest Details Component Render 1`] = `
+
+