-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from Jaspersoft/number-input-control
Number input control
- Loading branch information
Showing
5 changed files
with
283 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
packages/jv-input-controls/src/controls/SingleValueNumberInputControl.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
144
packages/jv-input-controls/test/SingleValueNumberInputControl.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |