Skip to content

Commit

Permalink
Merge pull request #20 from Jaspersoft/number-input-control
Browse files Browse the repository at this point in the history
Number input control
  • Loading branch information
grantbacon-jaspersoft authored Jul 16, 2024
2 parents 33acdc3 + 4981e3f commit e8befb4
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/jv-input-controls/src/InputControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BoolICType } from "./controls/BooleanInputControl";
import { createRoot } from "react-dom/client";
import { DateICType } from "./controls/DatePickerInputControl";
import { DateTimeICType } from "./controls/DateTimePickerInputControl";
import { NumberICType } from "./controls/SingleValueNumberInputControl";
import { TextFieldICType } from "./controls/SingleValueTextInputControl";
import { TimeICType } from "./controls/TimePickerInputControl";
import BasePanel from "./panels/BasePanel";
Expand All @@ -23,7 +24,7 @@ export interface InputControlUserConfig {
type: TextFieldICType;
};
singleValueNumber?: {
type: "number";
type: NumberICType;
};
singleValueDate?: {
type: DateICType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { TextField as JVTextField } from "@jaspersoft/jv-ui-components/material-ui/TextField/TextField";
import { parseNumber } from "../utils/NumberUtils";
import { BaseInputControlProps } from "./BaseInputControl";
import { useControlClasses } from "./hooks/useControlClasses";
import { useLiveState } from "./hooks/useLiveState";

export type NumberICType = "number";

export interface NumberICProps extends BaseInputControlProps {
defaultValue?: string;
value?: string;
variant?: "standard" | "filled" | "outlined" | undefined;
className?: string;
}

const checkIfNumber = (value: string) => {
const result = parseNumber(value);
return result !== null;
};

/**
* Number Input Control Component
*
* Will handle the numbers as a text based input
* @param props
* @constructor
*/
export const SingleValueNumberInputControl = (props: NumberICProps) => {
const {
value: theValue,
className,
defaultValue,
mandatory,
readOnly,
visible,
...remainingProps
} = props;
const liveState = useLiveState(
props.state?.value || theValue || defaultValue || "0",
);
const controlClasses = useControlClasses([], props);
// inputProps is needed to handle readOnly by TextField from MUI natively:
const inputProps: any = {};
if (readOnly) {
inputProps.readOnly = true;
}
const theInputProps = { ...inputProps, ...liveState };
const isError = !checkIfNumber(liveState.value);
// TODO: in the future, this message need to be considered for i18n:
const helperText = isError ? "Specify a valid value for type number." : "";
return (
<JVTextField
{...remainingProps}
variant={props.variant || "outlined"}
className={`${controlClasses.join(" ")} ${className || ""}`}
InputProps={theInputProps}
type="text"
error={isError}
helperText={helperText}
/>
);
};
16 changes: 16 additions & 0 deletions packages/jv-input-controls/src/panels/BasePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from "react";
import BooleanInputControl from "../controls/BooleanInputControl";
import { DatePickerInputControl } from "../controls/DatePickerInputControl";
import { DateTimePickerInputControl } from "../controls/DateTimePickerInputControl";
import { SingleValueNumberInputControl } from "../controls/SingleValueNumberInputControl";
import { SingleValueTextInputControl } from "../controls/SingleValueTextInputControl";
import { TimePickerInputControl } from "../controls/TimePickerInputControl";
import { InputControlUserConfig } from "../InputControls";
Expand Down Expand Up @@ -47,6 +48,21 @@ export default function BasePanel(props: BasePanelProps): React.JSX.Element {
/>
);
}
if (control.type === "singleValueNumber") {
return (
<SingleValueNumberInputControl
key={control.id}
id={control.id}
label={control.label}
value={control.state.value}
type={control.type}
readOnly={control.readOnly}
visible={control.visible}
mandatory={control.mandatory}
/>
);
}

if (control.type === "singleValueDate") {
return (
<DatePickerInputControl
Expand Down
59 changes: 59 additions & 0 deletions packages/jv-input-controls/src/utils/NumberUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright © 2005-2024. Cloud Software Group, Inc. All rights reserved. Confidential & Proprietary.
* Licensed pursuant to commercial Cloud Software Group, Inc End User License Agreement.
*/

const DECIMAL_SEPARATOR = "\\.";
const GROUPING_SEPARATOR = ",";
const SPACE_SEPARATOR = "\\s";
const MAX_INT = Number.MAX_SAFE_INTEGER
? Number.MAX_SAFE_INTEGER + 1
: 9007199254740992;
const MIN_INT = Number.MIN_SAFE_INTEGER
? Number.MIN_SAFE_INTEGER - 1
: -9007199254740992;

const DECIMAL_NUMBER_PATTERN = new RegExp(
"^-?([1-9]{1}[0-9]{0,2}(" +
GROUPING_SEPARATOR +
"[0-9]{3})*(" +
DECIMAL_SEPARATOR +
"[0-9]+)?|[1-9]{1}[0-9]{0,}(" +
DECIMAL_SEPARATOR +
"[0-9]+)?|0(" +
DECIMAL_SEPARATOR +
"[0-9]+)?)$",
);
const INTEGER_NUMBER_PATTERN = new RegExp(
"^-?([1-9]{1}[0-9]{0,2}(" +
GROUPING_SEPARATOR +
"[0-9]{3})*|[1-9]{1}[0-9]{0,}|0)$",
);

export const parseNumber = (value: string) => {
DECIMAL_NUMBER_PATTERN.lastIndex = 0; // reset the regex.
if (!DECIMAL_NUMBER_PATTERN.test(value)) {
// not valid.
return null;
}
value = value
.replace(new RegExp(GROUPING_SEPARATOR, "g"), "")
.replace(new RegExp(DECIMAL_SEPARATOR, "g"), ".");
const result = +value;
if (result > MIN_INT && result < MAX_INT) {
return result;
}
if (window.console) {
window.console.warn(
value +
" is out of the [" +
(MIN_INT + 1) +
", " +
(MAX_INT - 1) +
"] bounds. " +
"Parsing results may be corrupted. Use string representation instead. " +
"For more details see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number.",
);
}
return null;
};
144 changes: 144 additions & 0 deletions packages/jv-input-controls/test/SingleValueNumberInputControl.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { SizeToClass } from "@jaspersoft/jv-ui-components/material-ui/types/InputTypes";
import { fireEvent, render, screen } from "@testing-library/react";
import { JSX } from "react";
import { SingleValueNumberInputControl } from "../src/controls/SingleValueNumberInputControl";
import "@testing-library/jest-dom";

const LARGE_CSS_CLASS = SizeToClass.large;
const requiredProps = {
id: "column_float_1",
label: "column_float",
mandatory: false,
readOnly: false,
visible: true,
type: "singleValueNumber",
};

const getNumberIC = (options?: object): JSX.Element => {
return (
<SingleValueNumberInputControl {...{ ...requiredProps, ...options }} />
);
};

describe("SingleValueNumberInputControls tests", () => {
test("SingleValueNumberInputControls is rendered correctly", () => {
render(getNumberIC());
const buttonElement = screen.getByRole("textbox");
expect(buttonElement).toBeInTheDocument();
});

// Test for label prop
test("displays the label when provided", () => {
const testLabel = "Test Label";
render(getNumberIC({ label: testLabel }));
const labelElement = screen.queryByLabelText(testLabel);
expect(labelElement).toBeInTheDocument();
});

// Test for value prop
test("uses value as the initial input value", () => {
const defaultValue = "1,786";
render(getNumberIC({ defaultValue }));
const inputElement = screen.getByRole("textbox") as HTMLInputElement;
expect(inputElement.value).toBe(defaultValue);
});

test("a string is an invalid value for this input", () => {
const defaultValue = "this is a string";
render(getNumberIC({ defaultValue }));
const element = screen.getByText("Specify a valid value for type number.");
expect(element).toBeVisible();
});

test("a combination of numbers and strings is an invalid value", () => {
const defaultValue = "1.23e-10";
render(getNumberIC({ defaultValue }));
const element = screen.getByText("Specify a valid value for type number.");
expect(element).toBeVisible();
});

// Test for onChange event
test("updates value on change", () => {
const { getByRole } = render(getNumberIC({}));
const inputElement = getByRole("textbox") as HTMLInputElement;
const newValue = "3,926";
fireEvent.change(inputElement, { target: { value: newValue } });
expect(inputElement.value).toBe(newValue);
});

// Test for variant prop
test("changes style based on variant prop", () => {
const { rerender } = render(getNumberIC({ variant: "outlined" }));
let inputElement = screen.getByRole("textbox");
expect(inputElement).toHaveClass("MuiOutlinedInput-input");

rerender(getNumberIC({ variant: "filled" }));
inputElement = screen.getByRole("textbox");
expect(inputElement).toHaveClass("MuiFilledInput-input");
});

// test for default size.
test("check the default size is large if it is not provided", () => {
// Render the component
const { container } = render(getNumberIC({}));

// Use querySelector to get the first div with the class "jv-mInputLarge"
const divElement = container.querySelector(`div.${LARGE_CSS_CLASS}`);

// Assert that the element is found and has the expected class
expect(divElement).toBeInTheDocument();
expect(divElement).toHaveClass(LARGE_CSS_CLASS);
});

// test className prop
test("check wrapping CSS class", () => {
const cssClass = "ANY_CLASS";
// Render the component
const { container } = render(getNumberIC({ className: cssClass }));

// Use querySelector to get the first div with the class "jv-mInputLarge"
const divElement = container.querySelector(`div.${cssClass}`);

// Assert that the element is found and has the expected class
expect(divElement).toBeInTheDocument();
expect(divElement).toHaveClass(cssClass);
});

// test readOnly prop
test("check the component is read-only", () => {
// Render the component
const { rerender } = render(getNumberIC({ readOnly: true }));
let inputElement = screen.getByRole("textbox") as HTMLInputElement;

// Assert that the element is found and has the expected attribute
expect(inputElement).toBeInTheDocument();
expect(inputElement).toHaveAttribute("readonly");

rerender(getNumberIC({}));
inputElement = screen.getByRole("textbox") as HTMLInputElement;
expect(inputElement).not.toHaveAttribute("readonly");
});

// test visible prop
test("check the component is visible or not", () => {
const HIDDEN_CLASS_NAME = "jv-uVisibility-hide";
// Render the component
const { container } = render(getNumberIC({ visible: false }));
// Use querySelector to get the first div with the class "jv-mInputLarge"
const divElement = container.querySelector(`div.${HIDDEN_CLASS_NAME}`);

// Assert that the element is found and has the expected class
expect(divElement).toBeInTheDocument();
expect(divElement).toHaveClass(HIDDEN_CLASS_NAME);
});

// Test for mandatory field
test("verify the field shows error when mandatory prop is set", () => {
const CSS_ERROR_CLASS = "jv-uMandatory";
const { container } = render(getNumberIC({ mandatory: true }));
let wrapperDiv = container.querySelector(
`div.${CSS_ERROR_CLASS}`,
) as HTMLInputElement;
expect(wrapperDiv).toBeInTheDocument();
});
});

0 comments on commit e8befb4

Please sign in to comment.