From d1c14e2c92bee5505df2dfacc58bced05d1f95c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ja=C5=9Bkiewicz?= Date: Tue, 10 Sep 2024 08:54:59 +0200 Subject: [PATCH] chore(ui): migrate Checkbox components to Typescript (#310) * chore(ui): migrate Checkbox components to Typescript * release(ui): add changeset * fix(ui): formatting fix --------- Co-authored-by: gjaskiewicz@objectivity.co.uk Co-authored-by: Wowa Barsukov --- .changeset/serious-zebras-decide.md | 5 + .../Checkbox/Checkbox.component.tsx | 352 ++++++++++++++++++ .../components/Checkbox/Checkbox.stories.tsx | 95 +++++ .../src/components/Checkbox/Checkbox.test.tsx | 170 +++++++++ .../Checkbox/{index.js => index.ts} | 0 ...mponent.js => CheckboxGroup.component.tsx} | 93 +++-- ...p.stories.js => CheckboxGroup.stories.tsx} | 9 +- ...oxGroup.test.js => CheckboxGroup.test.tsx} | 38 +- .../CheckboxGroup/{index.js => index.ts} | 0 .../Checkbox.component.js | 128 +++---- .../Checkbox.stories.js | 2 +- .../{Checkbox => CheckboxJs}/Checkbox.test.js | 0 .../CheckboxJs/CheckboxGroups.context.js | 8 + .../src/components/CheckboxJs/index.js | 6 + ...component.js => CheckboxRow.component.tsx} | 49 ++- ...Row.stories.js => CheckboxRow.stories.tsx} | 2 +- ...eckboxRow.test.js => CheckboxRow.test.tsx} | 34 +- .../CheckboxRow/{index.js => index.ts} | 0 .../DataGridCheckboxCell.component.js | 2 +- .../DataListCheckboxCell.component.js | 2 +- .../src/components/Form/Form.stories.js | 6 - 21 files changed, 822 insertions(+), 179 deletions(-) create mode 100644 .changeset/serious-zebras-decide.md create mode 100644 packages/ui-components/src/components/Checkbox/Checkbox.component.tsx create mode 100644 packages/ui-components/src/components/Checkbox/Checkbox.stories.tsx create mode 100644 packages/ui-components/src/components/Checkbox/Checkbox.test.tsx rename packages/ui-components/src/components/Checkbox/{index.js => index.ts} (100%) rename packages/ui-components/src/components/CheckboxGroup/{CheckboxGroup.component.js => CheckboxGroup.component.tsx} (77%) rename packages/ui-components/src/components/CheckboxGroup/{CheckboxGroup.stories.js => CheckboxGroup.stories.tsx} (95%) rename packages/ui-components/src/components/CheckboxGroup/{CheckboxGroup.test.js => CheckboxGroup.test.tsx} (85%) rename packages/ui-components/src/components/CheckboxGroup/{index.js => index.ts} (100%) rename packages/ui-components/src/components/{Checkbox => CheckboxJs}/Checkbox.component.js (83%) rename packages/ui-components/src/components/{Checkbox => CheckboxJs}/Checkbox.stories.js (97%) rename packages/ui-components/src/components/{Checkbox => CheckboxJs}/Checkbox.test.js (100%) create mode 100644 packages/ui-components/src/components/CheckboxJs/CheckboxGroups.context.js create mode 100644 packages/ui-components/src/components/CheckboxJs/index.js rename packages/ui-components/src/components/CheckboxRow/{CheckboxRow.component.js => CheckboxRow.component.tsx} (74%) rename packages/ui-components/src/components/CheckboxRow/{CheckboxRow.stories.js => CheckboxRow.stories.tsx} (98%) rename packages/ui-components/src/components/CheckboxRow/{CheckboxRow.test.js => CheckboxRow.test.tsx} (78%) rename packages/ui-components/src/components/CheckboxRow/{index.js => index.ts} (100%) diff --git a/.changeset/serious-zebras-decide.md b/.changeset/serious-zebras-decide.md new file mode 100644 index 000000000..e38a5757f --- /dev/null +++ b/.changeset/serious-zebras-decide.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-ui-components": patch +--- + +Migrate Checkbox components to Typescript diff --git a/packages/ui-components/src/components/Checkbox/Checkbox.component.tsx b/packages/ui-components/src/components/Checkbox/Checkbox.component.tsx new file mode 100644 index 000000000..05fe48948 --- /dev/null +++ b/packages/ui-components/src/components/Checkbox/Checkbox.component.tsx @@ -0,0 +1,352 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, useMemo, useContext, useId } from "react" +import { CheckboxGroupContext } from "../CheckboxGroup/CheckboxGroup.component" +import { Label } from "../LabelTs/index" +import { Icon } from "../IconTs/index" +import { FormHint } from "../FormHintTs/index" + +const wrapperStyles = ` + jn-inline-flex + jn-items-center +` + +const inputstyles = ` + jn-w-4 + jn-h-4 + jn-opacity-0 + jn-z-50 +` + +const mockcheckboxstyles = ` + jn-relative + jn-w-4 + jn-h-4 + jn-rounded-sm + jn-bg-theme-checkbox + jn-cursor-pointer + focus:jn-outline-none + focus:jn-ring-2 + focus:jn-ring-theme-focus +` + +const mockfocusstyles = ` + jn-ring-2 + jn-ring-theme-focus +` + +const mockcheckmarkstyles = ` + jn-absolute + jn-top-0 + jn-left-0 + jn-text-theme-checkbox-checked + jn-fill-current +` + +const mockindeterminatestyles = ` + jn-absolute + jn-w-2 + jn-h-0.5 + jn-top-1.5 + jn-left-[.2rem] + jn-inline-block + jn-bg-theme-focus +` + +const mockdisabledstyles = ` + jn-pointer-events-none + jn-opacity-50 + jn-cursor-not-allowed +` + +const noBorderStyles = ` + jn-border + jn-border-transparent +` + +const errorstyles = ` + jn-border + jn-border-theme-error +` + +const successstyles = ` + jn-border + jn-border-theme-success +` + +const labelStyles = ` + jn-leading-0 + jn-ml-2 +` + +const iconStyles = ` + jn-ml-1 +` + +const hintStyles = ` + jn-mt-0 + jn-ml-6 +` + +export const Checkbox = ({ + checked = false, + className = "", + disabled = false, + errortext = "", + helptext = "", + id = "", + indeterminate = false, + invalid = false, + label, + name = "", + onChange, + onClick, + required = false, + successtext = "", + valid = false, + value, + ...props +}: CheckboxProps) => { + // Utility + const isNotEmptyString = (str: React.ReactNode | string) => { + return !(typeof str === "string" && str.trim().length === 0) + } + + const uniqueId = () => "juno-checkbox-" + useId() + + // Consume and deconstruct the context so we won't get errors but 'undefined' when trying to access a group context property in case there is none: + const checkboxGroupContext = useContext(CheckboxGroupContext) + const { + selectedOptions: groupSelectedOptions, + name: groupName, + disabled: groupDisabled, + handleCheckboxChange: groupHandleCheckboxChange, + updateSelectedValue: updateGroupSelectedValue, + } = checkboxGroupContext || {} + + // Lazily initialise the Checkbox: + const initialChecked = () => { + if (checkboxGroupContext) { + if (groupSelectedOptions && groupSelectedOptions.includes(value)) { + return true + } else { + return false + } + } else { + return checked ? true : false + } + } + + const [isChecked, setIsChecked] = useState(initialChecked()) + const [isIndeterminate, setIsIndeterminate] = useState(false) + const [hasFocus, setHasFocus] = useState(false) + const [isInvalid, setIsInvalid] = useState(false) + const [isValid, setIsValid] = useState(false) + + // Run once to update the parent state to respect and reflect the checked prop if we are in a group context, but parent has no selected options set via its prop: + useEffect(() => { + if (checked && checkboxGroupContext) { + updateGroupSelectedValue && updateGroupSelectedValue(value) + } + }, []) + + useEffect(() => { + if (!checkboxGroupContext) { + setIsChecked(checked) + } + }, [checked]) + + const invalidated = useMemo( + () => invalid || (errortext && isNotEmptyString(errortext) ? true : false), + [invalid, errortext] + ) + const validated = useMemo( + () => valid || (successtext && isNotEmptyString(successtext) ? true : false), + [valid, successtext] + ) + + useEffect(() => { + setIsIndeterminate(indeterminate) + }, [indeterminate]) + + useEffect(() => { + setIsInvalid(invalidated) + }, [invalidated]) + + useEffect(() => { + setIsValid(validated) + }, [validated]) + + const handleChange = (event: React.ChangeEvent) => { + setIsChecked(!isChecked) + // If we are in a context, update : + if (groupHandleCheckboxChange && typeof groupHandleCheckboxChange === "function") { + groupHandleCheckboxChange(value) + } + + onChange && onChange(event) + } + + const handleClick = (event: React.MouseEvent) => { + onClick && onClick(event) + } + + const handleFocus = () => { + setHasFocus(true) + } + + const handleBlur = () => { + setHasFocus(false) + } + + const determineChecked = () => { + if (checkboxGroupContext) { + return groupSelectedOptions && groupSelectedOptions.includes(value) ? true : false + } else { + return isChecked + } + } + + const theId = id || uniqueId() + + return ( +
+
+
+ {determineChecked() ? ( + + + + ) : ( + "" + )} + + {isIndeterminate && !determineChecked() ?
: ""} +
+ {label && isNotEmptyString(label) ? ( + <> +
+ {errortext && isNotEmptyString(errortext) ? ( + + ) : ( + "" + )} + {successtext && isNotEmptyString(successtext) ? ( + + ) : ( + "" + )} + {helptext && isNotEmptyString(helptext) ? : ""} +
+ ) +} + +export interface CheckboxProps { + /** Whether the Checkbox is checked */ + checked?: boolean + /** Pass a custom className */ + className?: string + /** Whether the Checkbox is disabled */ + disabled?: boolean + /** A text to render when the Checkbox has an error or could not be validated */ + errortext?: React.ReactNode | string + /** A helptext to render to explain meaning and significance of the Checkbox */ + helptext?: React.ReactNode | string + /** The id of the Radio. An id will be automatically generated if not passed. */ + id?: string + /** Whether the Checkbox is indeterminate. Applicable ONLY if the Checkbox represents multiple child Checkboxes with non--identical checked state. */ + indeterminate?: boolean + /** Whether the Checkbox was validated unsuccessfully */ + invalid?: boolean + /** The label of the Checkbox */ + label?: string + /** The name of the Checkbox */ + name?: string + /** handler to be executed when the Checkbox changes. */ + onChange?: React.ChangeEventHandler + /** handler to be executed when the Checkbox is clicked. */ + onClick?: React.MouseEventHandler + /** Whether the Checkbox is required */ + required?: boolean + /** A text to render when the Checkbox was successfully validated */ + successtext?: React.ReactNode | string + /** Whether the Checkbox was successfully validated */ + valid?: boolean + /** The value of the Checkbox */ + value?: string +} diff --git a/packages/ui-components/src/components/Checkbox/Checkbox.stories.tsx b/packages/ui-components/src/components/Checkbox/Checkbox.stories.tsx new file mode 100644 index 000000000..e13f04484 --- /dev/null +++ b/packages/ui-components/src/components/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Checkbox } from "./index" + +export default { + title: "Forms/Checkbox/Checkbox", + component: Checkbox, + argTypes: { + errortext: { + control: false, + }, + helptext: { + control: false, + }, + successtext: { + control: false, + }, + }, +} + +export const Default = { + args: {}, +} + +export const Checked = { + args: { + checked: true, + }, +} + +export const WithLabel = { + args: { + label: "Checkbox with Label", + }, +} + +export const Required = { + args: { + required: true, + label: "Required Checkbox", + }, +} + +export const Disabled = { + args: { + disabled: true, + }, +} + +export const Indeterminate = { + args: { + indeterminate: true, + }, +} + +export const Valid = { + args: { + valid: true, + }, +} + +export const Invalid = { + args: { + invalid: true, + }, +} + +export const ValidWithLabel = { + args: { + valid: true, + label: "Validated checkbox with label and icon", + successtext: "This option is valid.", + helptext: "Validation icons will only show when there is a label on the Checkbox", + }, +} + +export const InvalidWithLabel = { + args: { + invalid: true, + label: "Invalidated checkbox with label and icon", + errortext: "This option is invalid.", + helptext: "Validation icons will only show when there is a label on the Checkbox", + }, +} + +export const IndeterminateWithLabel = { + args: { + indeterminate: true, + label: "Indeterminate checkbox with label", + helptext: "A checkbox can be indeterminate as parent of multiple checkboxes with mixed checked states", + }, +} diff --git a/packages/ui-components/src/components/Checkbox/Checkbox.test.tsx b/packages/ui-components/src/components/Checkbox/Checkbox.test.tsx new file mode 100644 index 000000000..2cf8cabe0 --- /dev/null +++ b/packages/ui-components/src/components/Checkbox/Checkbox.test.tsx @@ -0,0 +1,170 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { act } from "react" +import { render, screen } from "@testing-library/react" +import { Checkbox } from "./index" + +describe("Checkbox", () => { + test("renders a valid html input type checkbox", () => { + render() + expect(screen.getByRole("checkbox")).toBeInTheDocument() + expect(screen.getByRole("checkbox")).toHaveAttribute("type", "checkbox") + }) + + test("renders a checkbox with a name as passed", () => { + render() + expect(screen.getByRole("checkbox")).toBeInTheDocument() + expect(screen.getByRole("checkbox")).toHaveAttribute("name", "My Checkbox") + }) + + test("renders a checkbox with a label", () => { + render() + expect(screen.getByLabelText("My Checkbox")).toBeInTheDocument() + expect(document.querySelector(".juno-label")).toBeInTheDocument() + expect(document.querySelector(".juno-label")).toHaveTextContent("My Checkbox") + }) + + test("renders a checkbox with an id as passed", () => { + render() + expect(screen.getByRole("checkbox")).toBeInTheDocument() + expect(screen.getByRole("checkbox")).toHaveAttribute("id", "my-checkbox") + }) + + test("renders a Checkbox with an auto-generated id if no id was passed", () => { + render() + expect(screen.getByRole("checkbox")).toBeInTheDocument() + expect(screen.getByRole("checkbox")).toHaveAttribute("id") + expect(screen.getByRole("checkbox").getAttribute("id")).toMatch("juno-checkbox") + }) + + test("renders a Checkbox with an associated label with an id as passed", () => { + render() + expect(screen.getByRole("checkbox")).toBeInTheDocument() + expect(screen.getByLabelText("My Checkbox")).toBeInTheDocument() + expect(document.querySelector(".juno-label")).toBeInTheDocument() + expect(document.querySelector(".juno-label")).toHaveTextContent("My Checkbox") + }) + + test("renders a Checkbox with a label associated by an auto-generated id if no id was passed", () => { + render() + expect(screen.getByRole("checkbox")).toBeInTheDocument() + expect(screen.getByLabelText("This is a Checkbox")).toBeInTheDocument() + }) + + test("renders a checkbox with a value as passed", () => { + render() + expect(screen.getByRole("checkbox")).toBeInTheDocument() + expect(screen.getByRole("checkbox")).toHaveAttribute("value", "ValueAsPassed") + }) + + test("renders a checked checkbox as passed", () => { + act(() => { + render() + }) + const checkbox = screen.getByRole("checkbox") + expect(checkbox).toBeInTheDocument() + expect(checkbox).toBeChecked() + }) + + test("renders no checked attribute if false", () => { + act(() => { + render() + }) + const checkbox = screen.getByRole("checkbox") + expect(checkbox).toBeInTheDocument() + expect(checkbox).not.toBeChecked() + }) + + test("renders a disabled checkbox as passed", () => { + render() + const checkbox = screen.getByRole("checkbox") + expect(checkbox).toBeInTheDocument() + expect(checkbox).toBeDisabled() + }) + + test("renders an invalid Checkbox as passed", () => { + render() + expect(screen.getByRole("checkbox")).toBeInTheDocument() + expect(screen.getByRole("checkbox")).toHaveClass("juno-checkbox-invalid") + }) + + test("renders a valid Checkbox as passed", () => { + render() + expect(screen.getByRole("checkbox")).toBeInTheDocument() + expect(screen.getByRole("checkbox")).toHaveClass("juno-checkbox-valid") + }) + + test("renders a helptext as passed", () => { + render() + expect(document.querySelector(".juno-form-hint")).toBeInTheDocument() + expect(document.querySelector(".juno-form-hint")).toHaveClass("juno-form-hint-help") + expect(document.querySelector(".juno-form-hint")).toHaveTextContent("this is a helptext") + }) + + test("renders a successtext as passed and validates the Checkbox", () => { + render() + expect(document.querySelector(".juno-form-hint")).toBeInTheDocument() + expect(document.querySelector(".juno-form-hint")).toHaveClass("juno-form-hint-success") + expect(document.querySelector(".juno-form-hint")).toHaveTextContent("great success!") + expect(screen.getByRole("checkbox")).toHaveClass("juno-checkbox-valid") + }) + + test("renders an errortext as passed and invalidates the Checkbox", () => { + render() + expect(document.querySelector(".juno-form-hint")).toBeInTheDocument() + expect(document.querySelector(".juno-form-hint")).toHaveClass("juno-form-hint-error") + expect(document.querySelector(".juno-form-hint")).toHaveTextContent("this is an error!") + expect(screen.getByRole("checkbox")).toHaveClass("juno-checkbox-invalid") + }) + + test("fires handler on change as passed", () => { + const onChangeSpy = vi.fn() + render() + act(() => { + screen.getByRole("checkbox").click() + }) + expect(onChangeSpy).toHaveBeenCalled() + }) + + test("fires handler on click as passed", () => { + const onClickSpy = vi.fn() + render() + act(() => { + screen.getByRole("checkbox").click() + }) + expect(onClickSpy).toHaveBeenCalled() + }) + + test("does not fire a handler on change when disabled", () => { + const onChangeSpy = vi.fn() + render() + act(() => { + screen.getByRole("checkbox").click() + }) + expect(onChangeSpy).not.toHaveBeenCalled() + }) + + test("does not fire a handler on click when disabled", () => { + const onClickSpy = vi.fn() + render() + act(() => { + screen.getByRole("checkbox").click() + }) + expect(onClickSpy).not.toHaveBeenCalled() + }) + + test("renders a custom className as passed", () => { + render() + expect(screen.getByTestId("23")).toBeInTheDocument() + expect(screen.getByTestId("23")).toHaveClass("my-custom-classname") + }) + + test("renders all props as passed", () => { + render() + expect(screen.getByTestId("23")).toBeInTheDocument() + expect(screen.getByTestId("23")).toHaveAttribute("data-lolol") + }) +}) diff --git a/packages/ui-components/src/components/Checkbox/index.js b/packages/ui-components/src/components/Checkbox/index.ts similarity index 100% rename from packages/ui-components/src/components/Checkbox/index.js rename to packages/ui-components/src/components/Checkbox/index.ts diff --git a/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.component.js b/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.component.tsx similarity index 77% rename from packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.component.js rename to packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.component.tsx index 248f837d4..8696650df 100644 --- a/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.component.js +++ b/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.component.tsx @@ -4,44 +4,55 @@ */ import React, { useState, useEffect, useMemo, createContext, useId } from "react" -import PropTypes from "prop-types" -import { Label } from "../Label/index" -import { Icon } from "../Icon/index" -import { FormHint } from "../FormHint/index" +import { Label } from "../LabelTs/index" +import { Icon } from "../IconTs/index" +import { FormHint } from "../FormHintTs/index" const checkboxgroupstyles = ` - jn-mb-4 - jn-last:mb-0 + jn-mb-4 + jn-last:mb-0 ` const groupstyles = ` - jn-relative - jn-rounded - jn-border - jn-py-1 + jn-relative + jn-rounded + jn-border + jn-py-1 ` const defaultgroupstyles = ` - jn-border-transparent + jn-border-transparent ` const validgroupstyles = ` - jn-border-theme-success - jn-px-2 + jn-border-theme-success + jn-px-2 ` const invalidgroupstyles = ` - jn-border-theme-error - jn-px-2 + jn-border-theme-error + jn-px-2 ` const iconstyles = ` - jn-absolute - jn-right-2 - jn-top-1.5 + jn-absolute + jn-right-2 + jn-top-1.5 ` -export const CheckboxGroupContext = createContext() +type EventUpdateHandler = (_value: string | undefined) => void + +export interface CheckboxGroupContextProps { + selectedOptions?: CheckboxValue[] + handleCheckboxChange?: EventUpdateHandler + name?: string + updateSelectedValue?: EventUpdateHandler + disabled?: boolean +} + +export const CheckboxGroupContext = createContext(undefined) + +export type CheckboxValue = string | undefined export const CheckboxGroup = ({ children = null, @@ -59,9 +70,9 @@ export const CheckboxGroup = ({ successtext = "", valid = false, ...props -}) => { +}: CheckboxGroupProps) => { // Utility - const isNotEmptyString = (str) => { + const isNotEmptyString = (str: React.ReactNode | string) => { return !(typeof str === "string" && str.trim().length === 0) } @@ -72,7 +83,7 @@ export const CheckboxGroup = ({ const groupId = id || uniqueId() // Init state variables: - const [selectedOptions, setSelectedOptions] = useState(selected) // undefined, empty array or array of values + const [selectedOptions, setSelectedOptions] = useState(selected) const [isValid, setIsValid] = useState(false) const [isInvalid, setIsInvalid] = useState(false) @@ -94,7 +105,7 @@ export const CheckboxGroup = ({ }, [invalidated]) // Callback function to be passed via context to individual checkboxes: - const handleCheckboxChange = (value) => { + const handleCheckboxChange = (value: CheckboxValue) => { const changedValue = value if (selectedOptions && selectedOptions.includes(value)) { setSelectedOptions( @@ -103,7 +114,7 @@ export const CheckboxGroup = ({ }) ) } else if (selectedOptions && !selectedOptions.includes(value)) { - setSelectedOptions((selectedOptions) => [...selectedOptions, changedValue]) + setSelectedOptions((selectedOptions) => [...(selectedOptions || []), changedValue]) } else { setSelectedOptions([changedValue]) } @@ -111,7 +122,7 @@ export const CheckboxGroup = ({ } // Callback function to be passed via the context to child Checkboxes so they can add their value to the groups' selectedOptions array in case selected has not been set on the parent (otherwise the parent select will trump whatever is set on the child in a group context). Called ONLY ONCE during initialization of the child Checkbox when we DON't want to execute any additional onChange handlers just yet: - const updateSelectedValue = (value) => { + const updateSelectedValue = (value: CheckboxValue) => { if (!selected) { setSelectedOptions((selectedOptions) => [...(selectedOptions || []), value]) } @@ -166,32 +177,32 @@ export const CheckboxGroup = ({ ) } -CheckboxGroup.propTypes = { +export interface CheckboxGroupProps { /** The Checkbox children of the CheckboxGroup */ - children: PropTypes.node, + children?: React.ReactNode /** Pass a custom className */ - className: PropTypes.string, + className?: string /** Whether all Checkboxes in the group are disabled */ - disabled: PropTypes.bool, + disabled?: boolean /** Text to display in case validation failed or there is an error. Will set the whole group to invalid when passed. */ - errortext: PropTypes.node, + errortext?: string /** A text to render to further explain meaning and significance of the group */ - helptext: PropTypes.node, + helptext?: string /** The id of the group. If not passed, a unique id will be created and used for the group as a whole. */ - id: PropTypes.string, - invalid: PropTypes.bool, - /*+ The label of the whole group. */ - label: PropTypes.string, + id?: string + invalid?: boolean + /** The label of the whole group. */ + label?: string /** The name of all checkboxes in the group. If not passed, a unique name identifier will be created and used for the group as a whole. */ - name: PropTypes.string, + name?: string /** An onChange handler to execute when the selection of options changes */ - onChange: PropTypes.func, + onChange?: EventUpdateHandler /** Whether a selection in the group is required */ - required: PropTypes.bool, + required?: boolean /** Array of values of individual selected options in the group */ - selected: PropTypes.array, + selected?: string[] /** Text to display in case validation is successful. When passed, will set the whole group to valid. */ - successtext: PropTypes.node, + successtext?: string /** Whether the CheckboxGroup was successfully validated */ - valid: PropTypes.bool, + valid?: boolean } diff --git a/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.stories.js b/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.stories.tsx similarity index 95% rename from packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.stories.js rename to packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.stories.tsx index 3b7d3cd18..fb40cc695 100644 --- a/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.stories.js +++ b/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.stories.tsx @@ -5,8 +5,8 @@ import React from "react" import PropTypes from "prop-types" -import { CheckboxGroup } from "./index.js" -import { Checkbox } from "../Checkbox/index.js" +import { CheckboxGroup } from "./index" +import { Checkbox } from "../Checkbox/index" export default { title: "Forms/Checkbox/CheckboxGroup", @@ -35,7 +35,10 @@ export default { }, } -const Template = ({ children, ...args }) => {children} +interface TemplateProps { + children: React.ReactNode +} +const Template = ({ children, ...args }: TemplateProps) => {children} Template.propTypes = { children: PropTypes.node, diff --git a/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.test.js b/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.test.tsx similarity index 85% rename from packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.test.js rename to packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.test.tsx index 4a36bc6be..ff940dfe9 100644 --- a/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.test.js +++ b/packages/ui-components/src/components/CheckboxGroup/CheckboxGroup.test.tsx @@ -9,44 +9,44 @@ import { CheckboxGroup } from "./index" import { Checkbox } from "../Checkbox/index" describe("CheckboxGroup", () => { - test("renders a CheckboxGroup container", async () => { + test("renders a CheckboxGroup container", () => { render() expect(screen.getByTestId("checkbox-group")).toBeInTheDocument() }) - test("renders a CheckboxGroup with an id as passed", async () => { + test("renders a CheckboxGroup with an id as passed", () => { render() expect(screen.getByTestId("group")).toBeInTheDocument() expect(screen.getByTestId("group")).toHaveAttribute("id", "my-checkboxgroup-1") }) - test("renders a CheckboxGroup with an auto-generated id if no id is passed", async () => { + test("renders a CheckboxGroup with an auto-generated id if no id is passed", () => { render() expect(screen.getByTestId("group")).toBeInTheDocument() expect(screen.getByTestId("group")).toHaveAttribute("id") expect(screen.getByTestId("group").getAttribute("id")).toMatch("juno-checkboxgroup") }) - test("renders a CheckboxGroup with an associated label as passed", async () => { + test("renders a CheckboxGroup with an associated label as passed", () => { render() expect(screen.getByRole("group")).toBeInTheDocument() expect(screen.getByText("My Group of Checkboxes")).toBeInTheDocument() }) - test("renders a required label as passed", async () => { + test("renders a required label as passed", () => { render() expect(screen.getByRole("group")).toBeInTheDocument() expect(document.querySelector(".juno-required")).toBeInTheDocument() }) - test("does not render any checkboxes if no children passed", async () => { + test("does not render any checkboxes if no children passed", () => { render() expect(() => { screen.getByRole("checkbox") }).toThrow() }) - test("renders Checkboxes as passed", async () => { + test("renders Checkboxes as passed", () => { render( @@ -57,7 +57,7 @@ describe("CheckboxGroup", () => { expect(screen.getAllByRole("checkbox")).toHaveLength(3) }) - test("renders individually named Checkboxes as passed", async () => { + test("renders individually named Checkboxes as passed", () => { render( @@ -68,7 +68,7 @@ describe("CheckboxGroup", () => { expect(screen.getAllByRole("checkbox")).toHaveLength(3) }) - test("renders Checkboxes with an auto-generated name if no name was passed", async () => { + test("renders Checkboxes with an auto-generated name if no name was passed", () => { render( @@ -79,7 +79,7 @@ describe("CheckboxGroup", () => { checkboxes.forEach((checkbox) => expect(checkbox).toHaveAttribute("name")) }) - test("renders Checkboxes as passed with one item", async () => { + test("renders Checkboxes as passed with one item", () => { render( @@ -88,7 +88,7 @@ describe("CheckboxGroup", () => { expect(screen.getByRole("checkbox")).toHaveAttribute("name", "my-checkboxgroup") }) - test("renders checked Checkboxes as passed", async () => { + test("renders checked Checkboxes as passed", () => { render( @@ -97,7 +97,7 @@ describe("CheckboxGroup", () => { expect(screen.getByRole("checkbox")).toBeChecked() }) - test("renders disabled child Checkboxes as passed", async () => { + test("renders disabled child Checkboxes as passed", () => { render( @@ -108,7 +108,7 @@ describe("CheckboxGroup", () => { expect(document.getElementById("c-2")).toBeDisabled() }) - test("renders a valid CheckboxGroup as passed", async () => { + test("renders a valid CheckboxGroup as passed", () => { render( @@ -119,7 +119,7 @@ describe("CheckboxGroup", () => { expect(screen.getByTitle("CheckCircle")).toBeInTheDocument() }) - test("renders a valid CheckboxGroup when successtext is passed", async () => { + test("renders a valid CheckboxGroup when successtext is passed", () => { render( @@ -131,7 +131,7 @@ describe("CheckboxGroup", () => { expect(screen.getByText("Great Success!")).toBeInTheDocument() }) - test("renders an invalid CheckboxGroup as passed", async () => { + test("renders an invalid CheckboxGroup as passed", () => { render( @@ -142,7 +142,7 @@ describe("CheckboxGroup", () => { expect(screen.getByTitle("Dangerous")).toBeInTheDocument() }) - test("renders an invalid CheckboxGroup when errortext is passed", async () => { + test("renders an invalid CheckboxGroup when errortext is passed", () => { render( @@ -154,7 +154,7 @@ describe("CheckboxGroup", () => { expect(screen.getByText("Big Error!")).toBeInTheDocument() }) - test("renders a helptext as passed", async () => { + test("renders a helptext as passed", () => { render( @@ -166,7 +166,7 @@ describe("CheckboxGroup", () => { expect(document.querySelector(".juno-form-hint")).toHaveTextContent("This is a helpful text") }) - test("renders a custom className", async () => { + test("renders a custom className", () => { render( @@ -176,7 +176,7 @@ describe("CheckboxGroup", () => { expect(screen.getByRole("group")).toHaveClass("my-custom-classname") }) - test("renders all props", async () => { + test("renders all props", () => { render( diff --git a/packages/ui-components/src/components/CheckboxGroup/index.js b/packages/ui-components/src/components/CheckboxGroup/index.ts similarity index 100% rename from packages/ui-components/src/components/CheckboxGroup/index.js rename to packages/ui-components/src/components/CheckboxGroup/index.ts diff --git a/packages/ui-components/src/components/Checkbox/Checkbox.component.js b/packages/ui-components/src/components/CheckboxJs/Checkbox.component.js similarity index 83% rename from packages/ui-components/src/components/Checkbox/Checkbox.component.js rename to packages/ui-components/src/components/CheckboxJs/Checkbox.component.js index b19428540..a3c852b8b 100644 --- a/packages/ui-components/src/components/Checkbox/Checkbox.component.js +++ b/packages/ui-components/src/components/CheckboxJs/Checkbox.component.js @@ -5,91 +5,91 @@ import React, { useState, useEffect, useMemo, useContext, useId } from "react" import PropTypes from "prop-types" -import { CheckboxGroupContext } from "../CheckboxGroup/CheckboxGroup.component" +import { CheckboxGroupContext } from "./CheckboxGroups.context" import { Label } from "../Label/index" import { Icon } from "../Icon/Icon.component" import { FormHint } from "../FormHint/FormHint.component" const wrapperStyles = ` - jn-inline-flex - jn-items-center + jn-inline-flex + jn-items-center ` const inputstyles = ` - jn-w-4 - jn-h-4 - jn-opacity-0 - jn-z-50 + jn-w-4 + jn-h-4 + jn-opacity-0 + jn-z-50 ` const mockcheckboxstyles = ` - jn-relative - jn-w-4 - jn-h-4 - jn-rounded-sm - jn-bg-theme-checkbox - jn-cursor-pointer - focus:jn-outline-none - focus:jn-ring-2 - focus:jn-ring-theme-focus + jn-relative + jn-w-4 + jn-h-4 + jn-rounded-sm + jn-bg-theme-checkbox + jn-cursor-pointer + focus:jn-outline-none + focus:jn-ring-2 + focus:jn-ring-theme-focus ` const mockfocusstyles = ` - jn-ring-2 - jn-ring-theme-focus + jn-ring-2 + jn-ring-theme-focus ` const mockcheckmarkstyles = ` - jn-absolute - jn-top-0 - jn-left-0 - jn-text-theme-checkbox-checked - jn-fill-current + jn-absolute + jn-top-0 + jn-left-0 + jn-text-theme-checkbox-checked + jn-fill-current ` const mockindeterminatestyles = ` - jn-absolute - jn-w-2 - jn-h-0.5 - jn-top-1.5 - jn-left-[.2rem] - jn-inline-block - jn-bg-theme-focus + jn-absolute + jn-w-2 + jn-h-0.5 + jn-top-1.5 + jn-left-[.2rem] + jn-inline-block + jn-bg-theme-focus ` const mockdisabledstyles = ` - jn-pointer-events-none - jn-opacity-50 - jn-cursor-not-allowed + jn-pointer-events-none + jn-opacity-50 + jn-cursor-not-allowed ` const noBorderStyles = ` - jn-border - jn-border-transparent + jn-border + jn-border-transparent ` const errorstyles = ` - jn-border - jn-border-theme-error + jn-border + jn-border-theme-error ` const successstyles = ` - jn-border - jn-border-theme-success + jn-border + jn-border-theme-success ` const labelStyles = ` - jn-leading-0 - jn-ml-2 + jn-leading-0 + jn-ml-2 ` const iconStyles = ` - jn-ml-1 + jn-ml-1 ` const hintStyles = ` - jn-mt-0 - jn-ml-6 + jn-mt-0 + jn-ml-6 ` export const Checkbox = ({ @@ -218,15 +218,15 @@ export const Checkbox = ({
{determineChecked() ? ( @@ -245,11 +245,11 @@ export const Checkbox = ({ ) : ( "" @@ -290,9 +290,9 @@ export const Checkbox = ({ color="jn-text-theme-success" size="1.125rem" className={` - ${iconStyles} - ${disabled ? "jn-opacity-50" : ""} - `} + ${iconStyles} + ${disabled ? "jn-opacity-50" : ""} + `} /> ) : ( "" diff --git a/packages/ui-components/src/components/Checkbox/Checkbox.stories.js b/packages/ui-components/src/components/CheckboxJs/Checkbox.stories.js similarity index 97% rename from packages/ui-components/src/components/Checkbox/Checkbox.stories.js rename to packages/ui-components/src/components/CheckboxJs/Checkbox.stories.js index 3e87e2b8e..e310c30f7 100644 --- a/packages/ui-components/src/components/Checkbox/Checkbox.stories.js +++ b/packages/ui-components/src/components/CheckboxJs/Checkbox.stories.js @@ -6,7 +6,7 @@ import { Checkbox } from "./index.js" export default { - title: "Forms/Checkbox/Checkbox", + title: "Deprecated/Forms/Checkbox/Checkbox", component: Checkbox, argTypes: { errortext: { diff --git a/packages/ui-components/src/components/Checkbox/Checkbox.test.js b/packages/ui-components/src/components/CheckboxJs/Checkbox.test.js similarity index 100% rename from packages/ui-components/src/components/Checkbox/Checkbox.test.js rename to packages/ui-components/src/components/CheckboxJs/Checkbox.test.js diff --git a/packages/ui-components/src/components/CheckboxJs/CheckboxGroups.context.js b/packages/ui-components/src/components/CheckboxJs/CheckboxGroups.context.js new file mode 100644 index 000000000..a65be908f --- /dev/null +++ b/packages/ui-components/src/components/CheckboxJs/CheckboxGroups.context.js @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createContext } from "react" + +export const CheckboxGroupContext = createContext() diff --git a/packages/ui-components/src/components/CheckboxJs/index.js b/packages/ui-components/src/components/CheckboxJs/index.js new file mode 100644 index 000000000..83fa1b37d --- /dev/null +++ b/packages/ui-components/src/components/CheckboxJs/index.js @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { Checkbox } from "./Checkbox.component" diff --git a/packages/ui-components/src/components/CheckboxRow/CheckboxRow.component.js b/packages/ui-components/src/components/CheckboxRow/CheckboxRow.component.tsx similarity index 74% rename from packages/ui-components/src/components/CheckboxRow/CheckboxRow.component.js rename to packages/ui-components/src/components/CheckboxRow/CheckboxRow.component.tsx index e717edd46..15d1000e8 100644 --- a/packages/ui-components/src/components/CheckboxRow/CheckboxRow.component.js +++ b/packages/ui-components/src/components/CheckboxRow/CheckboxRow.component.tsx @@ -4,20 +4,19 @@ */ import React from "react" -import PropTypes from "prop-types" -import { Checkbox } from "../Checkbox/index.js" -import { withDeprecationWarning } from "../withDeprecationWarning/index.js" +import { Checkbox } from "../Checkbox/index" +import { withDeprecationWarning } from "../withDeprecationWarningTs/index" /** DEPRECATED: A single checkbox, associated label, and structural markup. This component is DEPRECATED, use Checkbox instead. */ const CheckboxRow = ({ value = "", checked = false, indeterminate = false, - name = null, - label = null, - id = null, - helptext = null, - required = null, + name = undefined, + label = undefined, + id = undefined, + helptext = undefined, + required = undefined, disabled = false, invalid = false, errortext = "", @@ -26,7 +25,7 @@ const CheckboxRow = ({ className = "", onChange, ...props -}) => { +}: CheckboxRowProps) => { return ( } export default withDeprecationWarning( diff --git a/packages/ui-components/src/components/CheckboxRow/CheckboxRow.stories.js b/packages/ui-components/src/components/CheckboxRow/CheckboxRow.stories.tsx similarity index 98% rename from packages/ui-components/src/components/CheckboxRow/CheckboxRow.stories.js rename to packages/ui-components/src/components/CheckboxRow/CheckboxRow.stories.tsx index 52844e02d..01ddd9d0f 100644 --- a/packages/ui-components/src/components/CheckboxRow/CheckboxRow.stories.js +++ b/packages/ui-components/src/components/CheckboxRow/CheckboxRow.stories.tsx @@ -4,7 +4,7 @@ */ import React from "react" -import { CheckboxRow } from "./index.js" +import { CheckboxRow } from "./index" export default { title: "Deprecated/CheckboxRow", diff --git a/packages/ui-components/src/components/CheckboxRow/CheckboxRow.test.js b/packages/ui-components/src/components/CheckboxRow/CheckboxRow.test.tsx similarity index 78% rename from packages/ui-components/src/components/CheckboxRow/CheckboxRow.test.js rename to packages/ui-components/src/components/CheckboxRow/CheckboxRow.test.tsx index 46ac2207e..f2f4b6b62 100644 --- a/packages/ui-components/src/components/CheckboxRow/CheckboxRow.test.js +++ b/packages/ui-components/src/components/CheckboxRow/CheckboxRow.test.tsx @@ -8,65 +8,65 @@ import { render, screen } from "@testing-library/react" import { CheckboxRow } from "./index" describe("CheckboxRow", () => { - test("renders a checkbox row", async () => { + test("renders a checkbox row", () => { render() expect(screen.getByTestId("checkbox-row")).toBeInTheDocument() }) - test("renders a checked checkbox as passed", async () => { + test("renders a checked checkbox as passed", () => { act(() => { render() }) expect(screen.getByRole("checkbox")).toBeChecked() }) - test("renders a checkbox row with a value as passed", async () => { + test("renders a checkbox row with a value as passed", () => { render() expect(screen.getByRole("checkbox")).toHaveAttribute("value", "my-value") }) - test("renders a checkbox row with a name as passed", async () => { + test("renders a checkbox row with a name as passed", () => { render() expect(screen.getByRole("checkbox")).toHaveAttribute("name", "my-checkbox") }) - test("renders a checkbox row with an id as passed", async () => { + test("renders a checkbox row with an id as passed", () => { render() expect(screen.getByRole("checkbox")).toHaveAttribute("id", "my-checkbox") }) - test("renders a checkbox row with a checkbox and an associated label with an id as passed", async () => { + test("renders a checkbox row with a checkbox and an associated label with an id as passed", () => { render() expect(screen.getByRole("checkbox")).toBeInTheDocument() expect(screen.getByLabelText("My Checkbox Row")).toBeInTheDocument() expect(screen.getByRole("checkbox")).toHaveAttribute("id", "checkbox-row") }) - test("renders a help text as passed", async () => { + test("renders a help text as passed", () => { render() expect(screen.getByText("Helptext goes here")).toBeInTheDocument() }) - test("renders a helpt text with a link as passed", async () => { + test("renders a helpt text with a link as passed", () => { render(Link} />) expect(screen.getByRole("link")).toBeInTheDocument() expect(screen.getByRole("link")).toHaveAttribute("href", "#") expect(screen.getByRole("link")).toHaveTextContent("Link") }) - test("renders a required label as passed", async () => { + test("renders a required label as passed", () => { render() expect(document.querySelector(".juno-required")).toBeInTheDocument() }) - test("renders a disabled Checkbox as passed", async () => { + test("renders a disabled Checkbox as passed", () => { act(() => { render() }) expect(screen.getByRole("checkbox")).toBeDisabled() }) - test("renders an invalid CheckboxRow as passed", async () => { + test("renders an invalid CheckboxRow as passed", () => { act(() => { render() }) @@ -75,7 +75,7 @@ describe("CheckboxRow", () => { expect(screen.getByTitle("Dangerous")).toBeInTheDocument() }) - test("renders an invalid CheckRow with an error text as passed", async () => { + test("renders an invalid CheckRow with an error text as passed", () => { render() expect(screen.getByRole("checkbox")).toBeInTheDocument() expect(screen.getByRole("checkbox")).toHaveClass("juno-checkbox-invalid") @@ -83,7 +83,7 @@ describe("CheckboxRow", () => { expect(screen.getByText("This is an error text")).toBeInTheDocument() }) - test("renders a valid CheckboxRow as passed", async () => { + test("renders a valid CheckboxRow as passed", () => { act(() => { render() }) @@ -92,18 +92,18 @@ describe("CheckboxRow", () => { expect(screen.getByTitle("CheckCircle")).toBeInTheDocument() }) - test("renders a custom className", async () => { + test("renders a custom className", () => { render() expect(screen.getByTestId("my-checkbox-row")).toHaveClass("my-classname") }) - test("renders all props as passed", async () => { + test("renders all props as passed", () => { render() expect(screen.getByTestId("my-checkbox-row")).toHaveAttribute("data-lolol", "some-prop") }) - test("fire handler on change as passed", async () => { - const onChangeSpy = jest.fn() + test("fire handler on change as passed", () => { + const onChangeSpy = vi.fn() render() act(() => { screen.getByRole("checkbox").click() diff --git a/packages/ui-components/src/components/CheckboxRow/index.js b/packages/ui-components/src/components/CheckboxRow/index.ts similarity index 100% rename from packages/ui-components/src/components/CheckboxRow/index.js rename to packages/ui-components/src/components/CheckboxRow/index.ts diff --git a/packages/ui-components/src/components/DataGridCheckboxCell/DataGridCheckboxCell.component.js b/packages/ui-components/src/components/DataGridCheckboxCell/DataGridCheckboxCell.component.js index 98c52c4d0..aaa67834f 100644 --- a/packages/ui-components/src/components/DataGridCheckboxCell/DataGridCheckboxCell.component.js +++ b/packages/ui-components/src/components/DataGridCheckboxCell/DataGridCheckboxCell.component.js @@ -5,7 +5,7 @@ import React from "react" import PropTypes from "prop-types" -import { Checkbox } from "../Checkbox/Checkbox.component.js" +import { Checkbox } from "../CheckboxJs/Checkbox.component.js" import { DataGridCell } from "../DataGridCell/DataGridCell.component.js" export const DataGridCheckboxCell = ({ selected = false, disabled = false, className = "", onChange, ...props }) => { diff --git a/packages/ui-components/src/components/DataListCheckboxCell/DataListCheckboxCell.component.js b/packages/ui-components/src/components/DataListCheckboxCell/DataListCheckboxCell.component.js index 8bd954c4f..a7e21606b 100644 --- a/packages/ui-components/src/components/DataListCheckboxCell/DataListCheckboxCell.component.js +++ b/packages/ui-components/src/components/DataListCheckboxCell/DataListCheckboxCell.component.js @@ -5,7 +5,7 @@ import React from "react" import PropTypes from "prop-types" -import { Checkbox } from "../Checkbox/Checkbox.component.js" +import { Checkbox } from "../CheckboxJs/Checkbox.component.js" import { DataListCell } from "../DataListCell/DataListCell.component.js" const datalistcheckboxcellbasestyles = ` diff --git a/packages/ui-components/src/components/Form/Form.stories.js b/packages/ui-components/src/components/Form/Form.stories.js index a28f2ee08..27514d652 100644 --- a/packages/ui-components/src/components/Form/Form.stories.js +++ b/packages/ui-components/src/components/Form/Form.stories.js @@ -14,8 +14,6 @@ import { Select } from "../Select/index.js" import { SelectOption } from "../SelectOption/index.js" import { Switch } from "../Switch/index.js" import { Textarea } from "../Textarea/index.js" -import { CheckboxGroup } from "../CheckboxGroup/index.js" -import { Checkbox } from "../Checkbox/index.js" import { Button } from "../Button/index.js" import { ButtonRow } from "../ButtonRow/index.js" import { IntroBox } from "../IntroBox/index.ts" @@ -103,10 +101,6 @@ export const ComplexForm = { , - - - -