Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions src/components/controls/NumberInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Meta, StoryObj } from "@storybook/react";
import { NumberInput } from "./NumberInput";

const meta: Meta<typeof NumberInput> = {
title: "Components/Controls/NumberInput",
component: NumberInput,
tags: ["autodocs"],
parameters: {
docs: {
description: {
component:
"A number input field, which validates by number mode and limits.",
},
},
},
};

export default meta;

type Story = StoryObj<typeof meta>;

const handleCommit = (number: number) => {
alert(JSON.stringify({ number }));
};

export const Input: Story = {
parameters: {
docs: {
description: {
story: "Default number input field.",
},
},
},
};

export const NumberWithLabel: Story = {
args: { label: "A floating point number" },
parameters: {
docs: {
description: {
story: "Number input field with a label.",
},
},
},
};

export const InvalidDefaultNumber: Story = {
args: {
label: "An invalid default number",
defaultValue: "15.2e5",
},
parameters: {
docs: {
description: {
story:
"Number input field with a label and an invalid default value given.",
},
},
},
};

export const NaturalNumberWithLimits: Story = {
args: {
label: "A natural number",
numberMode: "natural",
defaultValue: 1,
minValue: 0,
maxValue: 15,
},
parameters: {
docs: {
description: {
story: "Number input field with natural number mode and given limits.",
},
},
},
};

export const IntegerNumber: Story = {
args: {
label: "An integer number",
numberMode: "integer",
defaultValue: -1,
},
parameters: {
docs: {
description: {
story: "Number input field with integer number mode.",
},
},
},
};

export const FloatingNumberWithLimits: Story = {
args: {
label: "A floating point number",
numberMode: "floating",
defaultValue: 1.1,
minValue: -50,
maxValue: 50,
},
parameters: {
docs: {
description: {
story:
"Number input field with floating point number mode and given limits.",
},
},
},
};

export const ScientificNumber: Story = {
args: {
label: "A scientific number",
numberMode: "scientific",
defaultValue: "1e5",
},
parameters: {
docs: {
description: {
story: "Number input field with scientific number mode.",
},
},
},
};

export const NumberWithOnlyReturnKeyCommit: Story = {
args: {
label: "A floating point number",
onCommit: handleCommit,
commitOnBlur: false,
},
parameters: {
docs: {
description: {
story: "Number input field with commit on return.",
},
},
},
};

export const NumberWithOnlyBlurCommit: Story = {
args: {
label: "A floating point number",
onCommit: handleCommit,
commitOnReturn: false,
},
parameters: {
docs: {
description: {
story: "Number input field with commit on blur.",
},
},
},
};
142 changes: 142 additions & 0 deletions src/components/controls/NumberInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { NumberInput } from "./NumberInput";

describe("NumberInput", () => {
it("default value is marked invalid", async () => {
render(
<NumberInput label="numberbox" numberMode="natural" defaultValue={-5} />,
);
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
});

it("default value is marked valid", async () => {
render(
<NumberInput label="numberbox" numberMode="natural" defaultValue={5} />,
);
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
});

it("is marked valid when limits are present", async () => {
render(
<NumberInput
label="numberbox"
numberMode="natural"
defaultValue={1}
minValue={-2}
maxValue={2}
/>,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "0" } });
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
});

it("is marked invalid when a lower limit is exceeded", async () => {
render(
<NumberInput
label="numberbox"
numberMode="natural"
defaultValue={1}
minValue={0}
/>,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "-1" } });
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
});

it("is marked invalid when an upper limit is exceeded", async () => {
render(
<NumberInput
label="numberbox"
numberMode="natural"
defaultValue={1}
maxValue={10}
/>,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "15" } });
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
});

it("does not accept negative numbers in natural mode", async () => {
render(
<NumberInput label="numberbox" numberMode="natural" defaultValue={1} />,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "-5" } });
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
});

it("accepts positive numbers in natural mode", async () => {
render(
<NumberInput label="numberbox" numberMode="natural" defaultValue={1} />,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "5" } });
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
});

it("does not accept decimal numbers in integer mode", async () => {
render(
<NumberInput label="numberbox" numberMode="integer" defaultValue={0} />,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "-5.2" } });
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
});

it("accepts negative numbers in integer mode", async () => {
render(
<NumberInput label="numberbox" numberMode="integer" defaultValue={0} />,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "-5" } });
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
});

it("does not accept scientific numbers in floating mode", async () => {
render(
<NumberInput label="numberbox" numberMode="floating" defaultValue={0} />,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "-5e5" } });
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
});

it("accepts decimal numbers in floating mode", async () => {
render(
<NumberInput label="numberbox" numberMode="floating" defaultValue={0} />,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "-5.2" } });
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
});

it("does not accept non-number characters in scientific mode", async () => {
render(
<NumberInput
label="numberbox"
numberMode="scientific"
defaultValue={0}
/>,
);
const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "-5e5!" } });
expect(screen.queryByText("Invalid input")).toBeInTheDocument();
});

it("accepts scientific numbers in scientific mode", async () => {
render(
<NumberInput
label="numberbox"
numberMode="scientific"
defaultValue={0}
/>,
);

const numberInput = screen.getByLabelText("numberbox");
fireEvent.change(numberInput, { target: { value: "5e5" } });
expect(screen.queryByText("Invalid input")).not.toBeInTheDocument();
});
});
Loading