Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add NumericSliderControl #25

Merged
merged 29 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
14d70f5
Add NumericControl, InputNumber, tests, and stories
NathanFarmer Mar 8, 2024
97aa738
Add NumericSliderControl, SingleSlider, tests, and stories
NathanFarmer Mar 8, 2024
3126a3b
Remove deprecated NumberControl tests
NathanFarmer Mar 8, 2024
7245b92
Revert tsconfig changes
NathanFarmer Mar 8, 2024
469a3e6
Unused imports
NathanFarmer Mar 8, 2024
690707f
Improve variable name
NathanFarmer Mar 8, 2024
2a2cd8e
Improve variable name
NathanFarmer Mar 8, 2024
715aafe
Rename story
NathanFarmer Mar 8, 2024
bda4779
Merge branch 'main' into f/FE-59/add-numeric-control
NathanFarmer Mar 11, 2024
920726a
Merge branch 'f/FE-59/add-numeric-control' of github.com:great-expect…
NathanFarmer Mar 11, 2024
c357b0b
Delete src/controls/NumberControl.test.tsx
NathanFarmer Mar 11, 2024
8c5243d
Cleanup merge
NathanFarmer Mar 11, 2024
0f1a596
Merge branch 'f/FE-59/add-numeric-slider-control' of github.com:great…
NathanFarmer Mar 11, 2024
08aefa7
Add type InputNumberOptions
NathanFarmer Mar 13, 2024
98dcc50
Make linter happy
NathanFarmer Mar 13, 2024
2d95230
Merge branch 'main' into f/FE-59/add-numeric-control
NathanFarmer Mar 13, 2024
69f3dc9
Add NumericControl to registry
NathanFarmer Mar 13, 2024
c386cc5
Merge branch 'f/FE-59/add-numeric-control' of github.com:great-expect…
NathanFarmer Mar 13, 2024
8ed9805
One more linter error
NathanFarmer Mar 13, 2024
ac3dcd3
Add new test for integer rounding
NathanFarmer Mar 13, 2024
a3f12c9
Move parser cast to higher level
NathanFarmer Mar 13, 2024
f47f841
Import types
NathanFarmer Mar 13, 2024
d767581
Merge branch 'f/FE-59/add-numeric-control' into f/FE-59/add-numeric-s…
NathanFarmer Mar 13, 2024
6b35ea5
Make linter happy
NathanFarmer Mar 13, 2024
da644e2
Rename SliderSingle to Slider
NathanFarmer Mar 13, 2024
2fb0441
Merge branch 'main' into f/FE-59/add-numeric-slider-control
NathanFarmer Mar 13, 2024
63b3b3a
Rename SliderSingle to Slider
NathanFarmer Mar 13, 2024
07c0247
Merge branch 'f/FE-59/add-numeric-slider-control' of github.com:great…
NathanFarmer Mar 13, 2024
946ef26
Merge branch 'main' into f/FE-59/add-numeric-slider-control
NathanFarmer Mar 13, 2024
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
51 changes: 51 additions & 0 deletions src/antd/Slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ReactElement } from "react"
import { ControlProps, RendererProps } from "@jsonforms/core"
import { Slider as AntdSlider, SliderSingleProps as AntdSliderSingleProps } from "antd"
import { coerceToInteger, coerceToNumber, decimalToPercentage } from "../controls/utils"

type SliderProps = AntdSliderSingleProps & RendererProps & ControlProps

export const Slider = (props: SliderProps): ReactElement<typeof AntdSlider> => {
const isDataNonNullObject = typeof props.data === "object" && props.data !== undefined && props.data !== null
const isDataEmptyObj = isDataNonNullObject ? Object.keys(props.data as object).length === 0 : false
const defaultValue = typeof props.schema?.default === "number" ? props.schema.default : props.schema.minimum
const value = props.data === undefined || isDataEmptyObj ? defaultValue : props.data as number

const addonAfter = props.uischema.options?.addonAfter as string | undefined
const addonBefore = props.uischema.options?.addonBefore as string | undefined
const isPercentage = addonAfter?.trim() === "%"

const min = props.schema.minimum
const max = props.schema.maximum
const step = props.schema.multipleOf

const numberType = props.schema.type
const isInteger = (typeof numberType === "string" && numberType === "integer") || (Array.isArray(numberType) && numberType.includes("integer"))
const handleChange = (value: number) => {
if ((min !== undefined && value >= min) && (max !== undefined && value <= max)) {
if (isInteger) {
props.handleChange(props.path, value !== null ? coerceToInteger(value) : value)
} else {
props.handleChange(props.path, value !== null ? coerceToNumber(value) : value)
}
}
}

const tooltip = {
formatter: (value?: number) => {
const tooltipValue = value !== undefined ? value : defaultValue
const formattedTooltipValue = isPercentage ? decimalToPercentage(tooltipValue) : tooltipValue
return `${addonBefore ? addonBefore : ""}${formattedTooltipValue}${addonAfter ? addonAfter : ""}`
}
}

return <AntdSlider
defaultValue={defaultValue}
value={value}
onChange={(value) => handleChange(value)}
min={min}
max={max}
step={step}
tooltip={tooltip}
/>
}
98 changes: 98 additions & 0 deletions src/controls/NumericControls/NumericSliderControl.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, test, it } from "vitest"
import { screen, waitFor } from "@testing-library/react"
import { userEvent } from "@testing-library/user-event"
import { render } from "../../common/test-render"
import { JSONSchema } from "json-schema-to-ts"
import {
numericSliderBasisPointsSchema,
numericSliderUISchema,
numericSliderFinalGradeSchema,
numericSliderPercentageUISchema,
numericSliderUISchemaWithRule,
} from "../../testSchemas/numericSchema/numericSliderSchema"

describe("NumericSliderControl", () => {
test("renders a slider and number input with no UISchema provided", () => {
render({
schema: numericSliderBasisPointsSchema,
})

screen.getByRole("slider")
screen.getByText("Basis Points")
})

it("Follows the hide rule", () => {
const data = { numericRangeValue: 1000 }
render({
data: data,
schema: numericSliderBasisPointsSchema,
uischema: numericSliderUISchemaWithRule,
})
expect(screen.queryByText("Basis Points")).toBeNull()
})

it("renders default value when no data is provided", () => {
render({
schema: numericSliderFinalGradeSchema,
uischema: numericSliderUISchema,
})
expect(screen.getByRole("spinbutton")).toHaveValue("0.5")
})

it("changes its value when users type", async () => {
let data: JSONSchema
render({
schema: numericSliderBasisPointsSchema,
uischema: numericSliderUISchema,
onChange: (state: { data: JSONSchema }) => {
data = state.data
},
})

await userEvent.clear(screen.getByRole("spinbutton"))
await userEvent.type(screen.getByRole("spinbutton"), "123")

await waitFor(() => {
expect(data).toEqual({ numericRangeValue: 123 })
})
})

it("does not fall back to default if value is empty", () => {
render({
schema: numericSliderBasisPointsSchema,
data: {},
})

expect(screen.getByRole("spinbutton")).toHaveValue("")
})

it("calls onChange with number values", async () => {
let data = { numericRangeValue: 42.00 }
render({
schema: numericSliderBasisPointsSchema,
data,
onChange: (state) => {
data = state.data
},
})

await userEvent.clear(screen.getByRole("spinbutton"))
await userEvent.type(screen.getByRole("spinbutton"), "42.00")

await waitFor(() => {
expect(data).toEqual({ numericRangeValue: 42.00 })
})
})

it ("shows units in tooltip if set in UI schema", async () => {
render({
schema: numericSliderFinalGradeSchema,
uischema: numericSliderPercentageUISchema,
})
const slider = screen.getByRole("slider")
expect(screen.queryByText("50%")).toBeNull()
await userEvent.hover(slider)

screen.getByText("50%")
})
})
32 changes: 32 additions & 0 deletions src/controls/NumericControls/NumericSliderControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ControlProps, RendererProps } from "@jsonforms/core"
import { Col, Form, Row } from "antd"
import { Rule } from "antd/lib/form"
import { InputNumber } from "../../antd/InputNumber"
import { Slider } from "../../antd/Slider"


export const NumericSliderControl = (props: ControlProps & RendererProps) => {
if (!props.visible) return null

const initialValue = typeof props.schema.default === "number" ? props.schema.default : props.schema.minimum

const rules: Rule[] = [
{ required: props.required, message: `${props.label} is required` },
]

return (
<Form.Item
label={props.label}
id={props.id}
name={props.path}
required={props.required}
initialValue={initialValue}
rules={rules}
validateTrigger={["onBlur"]}
>
<Row>
<Col span={8}>{Slider({...props})}</Col><Col span={7}>{InputNumber({...props})}</Col>
</Row>
</Form.Item>
)
}
9 changes: 8 additions & 1 deletion src/controls/NumericControls/testers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { Tester, isNumberControl, isIntegerControl, or } from "@jsonforms/core"
import { JsonSchema, Tester, isNumberControl, isIntegerControl, and, or, schemaMatches } from "@jsonforms/core"

export const isNumericControl: Tester = or(isNumberControl, isIntegerControl)

export const isNumericSliderControl: Tester = and(
isNumericControl,
schemaMatches((schema: JsonSchema) => {
return schema.minimum !== undefined && schema.maximum !== undefined
}),
)
15 changes: 14 additions & 1 deletion src/renderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import { VerticalLayoutRenderer } from "./layouts/VerticalLayoutRenderer";
import { ObjectControl } from "./controls/ObjectControl";
import { GroupLayoutRenderer } from "./layouts/GroupLayoutRenderer";
import { NumericControl } from "./controls/NumericControls/NumericControl";
import { NumericSliderControl } from "./controls/NumericControls/NumericSliderControl";
import React from "react";

import { isNumericControl } from "./controls/NumericControls/testers";
import { isNumericControl, isNumericSliderControl } from "./controls/NumericControls/testers";

// Ordered from lowest rank to highest rank. Higher rank renderers will be preferred over lower rank renderers.
export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [
Expand Down Expand Up @@ -56,10 +57,22 @@ export const rendererRegistryEntries: JsonFormsRendererRegistryEntry[] = [
tester: rankWith(2, uiTypeIs("Label")),
renderer: withJsonFormsLabelProps(AlertControl),
},
{
tester: rankWith(2, isNumericControl),
renderer: withJsonFormsControlProps(NumericControl)
},
{
tester: rankWith(2, isNumericControl),
renderer: withJsonFormsControlProps(NumericControl),
},
{
tester: rankWith(2, isNumericControl),
renderer: withJsonFormsControlProps(NumericControl),
},
{
tester: rankWith(3, isNumericSliderControl),
renderer: withJsonFormsControlProps(NumericSliderControl)
},
{
tester: rankWith(10, and(isObjectControl, not(isLayout))),
renderer: withJsonFormsDetailProps(ObjectControl),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Meta, StoryObj } from "@storybook/react"
import { StorybookAntDJsonForm } from "../../../common/StorybookAntDJsonForm";

import {
numericSliderBasisPointsSchema,
numericSliderUISchema,
numericSliderTemperatureSchema,
numericSliderTemperatureUISchema,
numericSliderFinalGradeSchema,
numericSliderPercentageUISchema,
numericSliderDonateNowSchema,
numericSliderUSDUISchema,
} from "../../../testSchemas/numericSchema/numericSliderSchema";


const meta: Meta<typeof StorybookAntDJsonForm> = {
title: "Control/Numeric Slider",
component: StorybookAntDJsonForm,
tags: ["autodocs"],
args: {
uiSchema: numericSliderUISchema,
},
argTypes: {
uiSchema: {
control: "object",
},
}
}

export default meta
type Story = StoryObj<typeof StorybookAntDJsonForm>

export const RequiredInteger: Story = {
tags: ["autodocs"],
args: {
jsonSchema: numericSliderBasisPointsSchema,
uiSchema: numericSliderUISchema,
},
}

export const RequiredFloatingPointWithUnits: Story = {
tags: ["autodocs"],
args: {
jsonSchema: numericSliderTemperatureSchema,
uiSchema: numericSliderTemperatureUISchema,
},
}

export const RequiredPercentageWithDefault: Story = {
tags: ["autodocs"],
args: {
jsonSchema: numericSliderFinalGradeSchema,
uiSchema: numericSliderPercentageUISchema,
},
}

export const OptionalUSDWithDefault: Story = {
tags: ["autodocs"],
args: {
jsonSchema: numericSliderDonateNowSchema,
uiSchema: numericSliderUSDUISchema,
},
}
Loading
Loading