From 3e225b95f63648d35dec53a70c442580f1d9291f Mon Sep 17 00:00:00 2001
From: Guoda <121792659+guoda-puidokaite@users.noreply.github.com>
Date: Mon, 4 Nov 2024 09:23:15 +0100
Subject: [PATCH] chore(ui): migrate InputGroup to Typescript (#569)
* chore(ui): convert inputgroup to ts
* chore(ui): remove native select from tests and stories, add deprecated code for .js files
* chore(ui): revert Button
* chore(ui): fix deprecated tests
* chore(ui): remove deprecated component and make suggested changes
* chore(ui): make imports more explicit
* chore(ui): suggested class name changes
---
.changeset/shiny-rivers-fetch.md | 5 +
.../InputGroup/InputGroup.component.js | 43 ------
.../InputGroup/InputGroup.component.tsx | 85 ++++++++++++
...roup.stories.js => InputGroup.stories.tsx} | 62 +--------
.../components/InputGroup/InputGroup.test.js | 102 --------------
.../components/InputGroup/InputGroup.test.tsx | 129 ++++++++++++++++++
.../InputGroup/{index.js => index.ts} | 0
.../components/InputGroup/input-group.scss | 20 +--
packages/ui-components/src/index.js | 2 +-
9 files changed, 236 insertions(+), 212 deletions(-)
create mode 100644 .changeset/shiny-rivers-fetch.md
delete mode 100644 packages/ui-components/src/components/InputGroup/InputGroup.component.js
create mode 100644 packages/ui-components/src/components/InputGroup/InputGroup.component.tsx
rename packages/ui-components/src/components/InputGroup/{InputGroup.stories.js => InputGroup.stories.tsx} (54%)
delete mode 100644 packages/ui-components/src/components/InputGroup/InputGroup.test.js
create mode 100644 packages/ui-components/src/components/InputGroup/InputGroup.test.tsx
rename packages/ui-components/src/components/InputGroup/{index.js => index.ts} (100%)
diff --git a/.changeset/shiny-rivers-fetch.md b/.changeset/shiny-rivers-fetch.md
new file mode 100644
index 000000000..2bb3cc183
--- /dev/null
+++ b/.changeset/shiny-rivers-fetch.md
@@ -0,0 +1,5 @@
+---
+"@cloudoperators/juno-ui-components": minor
+---
+
+Migrate the InputGroup component to TypeScript
diff --git a/packages/ui-components/src/components/InputGroup/InputGroup.component.js b/packages/ui-components/src/components/InputGroup/InputGroup.component.js
deleted file mode 100644
index 4d4dafa65..000000000
--- a/packages/ui-components/src/components/InputGroup/InputGroup.component.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import React from "react"
-import { Stack } from "../../deprecated_js/Stack/index"
-import "./input-group.scss"
-import PropTypes from "prop-types"
-
-/** A component to visually group Buttons, TextInput, and Select elements. */
-export const InputGroup = ({ children = null, className, variant = "default", disabled = false, ...props }) => {
- const modifiedChildren = () => {
- return React.Children.map(children, (child) => {
- const ownVariant = child.props.variant || variant
- const ownDisabled = child.props.disabled || disabled
- return React.cloneElement(child, {
- variant: ownVariant,
- disabled: ownDisabled,
- })
- })
- }
-
- return (
-
- {modifiedChildren()}
-
- )
-}
-
-InputGroup.propTypes = {
- /** The children to render */
- children: PropTypes.node,
- /** Pass a className to the group */
- className: PropTypes.string,
- /** Passing a variant prop to the group will set all child Buttons and Select elements to use that variant, unless specified otherwise on the individual child component */
- variant: PropTypes.oneOf(["default", "primary", "primary-danger", "subdued"]),
- /** Disable all elements in the InputGroup */
- disabled: PropTypes.bool,
-}
diff --git a/packages/ui-components/src/components/InputGroup/InputGroup.component.tsx b/packages/ui-components/src/components/InputGroup/InputGroup.component.tsx
new file mode 100644
index 000000000..2107bab50
--- /dev/null
+++ b/packages/ui-components/src/components/InputGroup/InputGroup.component.tsx
@@ -0,0 +1,85 @@
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { ReactNode, isValidElement } from "react"
+import { Stack } from "../Stack/Stack.component"
+import "./input-group.scss"
+
+type VariantTypes = "default" | "primary" | "primary-danger" | "subdued"
+
+export interface InputGroupProps {
+ /**
+ * The children to render within the InputGroup.
+ * This can be any React node or a collection of React nodes such as Buttons, TextInput, and Select elements.
+ */
+ children?: React.ReactNode
+ /**
+ * Additional CSS class name(s) to apply to the InputGroup for custom styling.
+ */
+ className?: string
+ /**
+ * The variant style to apply to the group and its children.
+ */
+ variant?: VariantTypes
+ /**
+ * If true, all elements within the InputGroup will be disabled.
+ * Individual elements can override this setting if needed.
+ */
+ disabled?: boolean
+}
+
+type InheritedProps = { variant?: VariantTypes; disabled?: boolean }
+
+/**
+ * Clones a child element with inherited variant and disabled props.
+ * Individual child props will override inherited ones if specified.
+ *
+ * @returns A cloned child element with the variant and disabled props applied.
+ */
+const cloneElementWithInheritedProps = (
+ child: ReactNode,
+ parentVariant: VariantTypes,
+ parentDisabled: boolean
+): ReactNode => {
+ if (!isValidElement(child)) return child
+
+ const combinedProps = {
+ variant: child.props.variant || parentVariant,
+ disabled: child.props.disabled || parentDisabled,
+ }
+ return React.cloneElement(child, combinedProps)
+}
+
+const getClassNames = (baseClassName: string, variant: VariantTypes, disabled: boolean): string => {
+ return `
+ ${baseClassName}-${variant}
+ ${disabled ? `${baseClassName}-disabled` : ""}
+ `
+}
+
+/**
+ * InputGroup is a component used to visually group related elements such as
+ * Buttons, TextInput, and Select elements, providing a cohesive styling approach.
+ */
+export const InputGroup: React.FC = ({
+ children = null,
+ className = "",
+ variant = "default",
+ disabled = false,
+ ...props
+}) => {
+ // Clone each child element with inherited variant and disabled props
+ const modifiedChildren = React.Children.map(children, (child) =>
+ cloneElementWithInheritedProps(child, variant, disabled)
+ )
+
+ const inputGroupClassName = getClassNames("juno-input-group", variant, disabled)
+
+ return (
+
+ {modifiedChildren}
+
+ )
+}
diff --git a/packages/ui-components/src/components/InputGroup/InputGroup.stories.js b/packages/ui-components/src/components/InputGroup/InputGroup.stories.tsx
similarity index 54%
rename from packages/ui-components/src/components/InputGroup/InputGroup.stories.js
rename to packages/ui-components/src/components/InputGroup/InputGroup.stories.tsx
index c3cd59b7e..a01955bc6 100644
--- a/packages/ui-components/src/components/InputGroup/InputGroup.stories.js
+++ b/packages/ui-components/src/components/InputGroup/InputGroup.stories.tsx
@@ -4,11 +4,11 @@
*/
import React from "react"
-import { InputGroup } from "./index.js"
-import { Button } from "../../deprecated_js/Button/index"
-import { NativeSelect } from "../NativeSelect/index"
-import { NativeSelectOption } from "../NativeSelectOption/index"
-import { TextInput } from "../../deprecated_js/TextInput/index"
+
+import { InputGroup } from "./InputGroup.component"
+
+import { Button } from "../Button/Button.component"
+import { TextInput } from "../TextInput/TextInput.component"
export default {
title: "WiP/InputGroup",
@@ -91,55 +91,3 @@ export const MultipleTextInputsWithButton = {
],
},
}
-
-export const ButtonWithOptions = {
- args: {
- children: [
- ,
-
-
-
- ,
- ],
- },
-}
-
-export const SelectWithTextInput = {
- args: {
- children: [
-
-
-
- ,
- ,
- ],
- },
-}
-
-export const TextInputWithButtonAndOptions = {
- args: {
- children: [
- ,
- ,
-
-
-
- ,
- ],
- },
-}
-
-export const SelectWithSelect = {
- args: {
- children: [
-
-
-
- ,
-
-
-
- ,
- ],
- },
-}
diff --git a/packages/ui-components/src/components/InputGroup/InputGroup.test.js b/packages/ui-components/src/components/InputGroup/InputGroup.test.js
deleted file mode 100644
index 71a47c213..000000000
--- a/packages/ui-components/src/components/InputGroup/InputGroup.test.js
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as React from "react"
-import { render, screen } from "@testing-library/react"
-import { InputGroup } from "./index"
-import { Button } from "../../deprecated_js/Button/index"
-import { TextInput } from "../../deprecated_js/TextInput/index"
-import { NativeSelect } from "../NativeSelect/index"
-import { NativeSelectOption } from "../NativeSelectOption/index"
-
-describe("InputGroup", () => {
- test("renders an InputGroup", async () => {
- render()
- expect(document.querySelector(".juno-input-group")).toBeInTheDocument()
- })
-
- test("renders children as passed", async () => {
- render(
-
-
-
-
-
-
-
-
- )
- expect(document.querySelector(".juno-input-group")).toBeInTheDocument()
- expect(screen.getByRole("button")).toBeInTheDocument()
- expect(screen.getByRole("button")).toHaveTextContent("A Button")
- expect(screen.getByRole("textbox")).toBeInTheDocument()
- expect(screen.getByRole("textbox")).toHaveValue("some value")
- expect(screen.getByRole("combobox")).toBeInTheDocument() // use listbox for radix-based select
- })
-
- test("renders child button variants as passed to parent", async () => {
- render(
-
-
-
-
-
- )
- expect(screen.getByRole("button", { name: "first" })).toBeInTheDocument()
- expect(screen.getByRole("button", { name: "first" })).toHaveClass("juno-button-primary-danger")
- expect(screen.getByRole("button", { name: "second" })).toBeInTheDocument()
- expect(screen.getByRole("button", { name: "second" })).toHaveClass("juno-button-primary-danger")
- expect(screen.getByRole("button", { name: "third" })).toBeInTheDocument()
- expect(screen.getByRole("button", { name: "third" })).toHaveClass("juno-button-primary-danger")
- })
-
- test("allows child button variant to override variant passed to parent", async () => {
- render(
-
-
-
-
-
- )
- expect(screen.getByRole("button", { name: "first" })).toBeInTheDocument()
- expect(screen.getByRole("button", { name: "first" })).toHaveClass("juno-button-primary-danger")
- expect(screen.getByRole("button", { name: "second" })).toBeInTheDocument()
- expect(screen.getByRole("button", { name: "second" })).not.toHaveClass("juno-button-primary-danger")
- expect(screen.getByRole("button", { name: "second" })).toHaveClass("juno-button-primary")
- expect(screen.getByRole("button", { name: "third" })).toBeInTheDocument()
- expect(screen.getByRole("button", { name: "third" })).toHaveClass("juno-button-primary-danger")
- })
-
- test("disables all child elements as passed to parent", async () => {
- render(
-
-
-
-
-
-
-
-
- )
- expect(screen.getByRole("button")).toBeInTheDocument()
- expect(screen.getByRole("button")).toBeDisabled()
- expect(screen.getByRole("textbox")).toBeInTheDocument()
- expect(screen.getByRole("textbox")).toBeDisabled()
- expect(screen.getByRole("combobox")).toBeInTheDocument()
- expect(screen.getByRole("combobox")).toBeDisabled()
- })
-
- test("renders a className a spassed", async () => {
- render()
- expect(document.querySelector(".juno-input-group")).toBeInTheDocument()
- expect(document.querySelector(".juno-input-group")).toHaveClass("my-class")
- })
-
- test("renders all props as passed", async () => {
- render()
- expect(document.querySelector(".juno-input-group")).toBeInTheDocument()
- expect(document.querySelector(".juno-input-group")).toHaveAttribute("data-test", "my-prop")
- })
-})
diff --git a/packages/ui-components/src/components/InputGroup/InputGroup.test.tsx b/packages/ui-components/src/components/InputGroup/InputGroup.test.tsx
new file mode 100644
index 000000000..c228d05af
--- /dev/null
+++ b/packages/ui-components/src/components/InputGroup/InputGroup.test.tsx
@@ -0,0 +1,129 @@
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as React from "react"
+import { render, screen } from "@testing-library/react"
+import { describe, expect, test } from "vitest"
+
+import { InputGroup } from "./InputGroup.component"
+import { Button } from "../Button/Button.component"
+import { TextInput } from "../TextInput/TextInput.component"
+
+describe("InputGroup", () => {
+ describe("Basic Rendering", () => {
+ test("renders an InputGroup", () => {
+ render()
+ expect(document.querySelector(".juno-input-group")).toBeInTheDocument()
+ })
+
+ test("renders without children", () => {
+ render()
+ expect(document.querySelector(".juno-input-group")).toBeInTheDocument()
+ expect(screen.queryByRole("button")).not.toBeInTheDocument()
+ expect(screen.queryByRole("textbox")).not.toBeInTheDocument()
+ })
+
+ test("renders children as passed", () => {
+ render(
+
+
+
+
+ )
+ expect(document.querySelector(".juno-input-group")).toBeInTheDocument()
+ expect(screen.getByRole("button")).toBeInTheDocument()
+ expect(screen.getByRole("button")).toHaveTextContent("A Button")
+ expect(screen.getByRole("textbox")).toBeInTheDocument()
+ expect(screen.getByRole("textbox")).toHaveValue("some value")
+ })
+ })
+
+ describe("Variant Handling", () => {
+ test("renders child button variants as passed to parent", () => {
+ render(
+
+
+
+
+
+ )
+ expect(screen.getByRole("button", { name: "first" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "first" })).toHaveClass("juno-button-primary-danger")
+ expect(screen.getByRole("button", { name: "second" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "second" })).toHaveClass("juno-button-primary-danger")
+ expect(screen.getByRole("button", { name: "third" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "third" })).toHaveClass("juno-button-primary-danger")
+ })
+
+ test("allows child button variant to override variant passed to parent", () => {
+ render(
+
+
+
+
+
+ )
+ expect(screen.getByRole("button", { name: "first" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "first" })).toHaveClass("juno-button-primary-danger")
+ expect(screen.getByRole("button", { name: "second" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "second" })).not.toHaveClass("juno-button-primary-danger")
+ expect(screen.getByRole("button", { name: "second" })).toHaveClass("juno-button-primary")
+ expect(screen.getByRole("button", { name: "third" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "third" })).toHaveClass("juno-button-primary-danger")
+ })
+ })
+
+ describe("Disabled State", () => {
+ test("disables all child elements as passed to parent", () => {
+ render(
+
+
+
+
+ )
+ expect(screen.getByRole("button")).toBeInTheDocument()
+ expect(screen.getByRole("button")).toBeDisabled()
+ expect(screen.getByRole("textbox")).toBeInTheDocument()
+ expect(screen.getByRole("textbox")).toBeDisabled()
+ })
+
+ test("renders with a mix of enabled and disabled children", () => {
+ render(
+
+
+
+
+ )
+ expect(screen.getByRole("button", { name: "Enabled" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "Enabled" })).not.toBeDisabled()
+ expect(screen.getByRole("button", { name: "Disabled" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "Disabled" })).toBeDisabled()
+ })
+ })
+
+ describe("Class and Props Handling", () => {
+ test("renders a className as passed", () => {
+ render()
+ expect(document.querySelector(".juno-input-group")).toBeInTheDocument()
+ expect(document.querySelector(".juno-input-group")).toHaveClass("my-class")
+ })
+
+ test("renders all props as passed", () => {
+ render()
+ expect(document.querySelector(".juno-input-group")).toBeInTheDocument()
+ expect(document.querySelector(".juno-input-group")).toHaveAttribute("data-test", "my-prop")
+ })
+
+ test("allows custom attributes to be passed to children", () => {
+ render(
+
+
+
+ )
+ expect(screen.getByRole("button", { name: "Custom" })).toBeInTheDocument()
+ expect(screen.getByRole("button", { name: "Custom" })).toHaveAttribute("data-custom", "custom-attribute")
+ })
+ })
+})
diff --git a/packages/ui-components/src/components/InputGroup/index.js b/packages/ui-components/src/components/InputGroup/index.ts
similarity index 100%
rename from packages/ui-components/src/components/InputGroup/index.js
rename to packages/ui-components/src/components/InputGroup/index.ts
diff --git a/packages/ui-components/src/components/InputGroup/input-group.scss b/packages/ui-components/src/components/InputGroup/input-group.scss
index d62f2f93a..75f7cb849 100644
--- a/packages/ui-components/src/components/InputGroup/input-group.scss
+++ b/packages/ui-components/src/components/InputGroup/input-group.scss
@@ -1,32 +1,34 @@
-// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
-// SPDX-License-Identifier: Apache-2.0
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
.juno-input-group {
-
- // remove border-radius where necessary
+ // Remove border-radius for specific elements within the input group
.juno-button,
.juno-textinput,
- // this is weird for selects as is since these are inside a wrapper each and thus all qualify as first child, and potentially harder for radix-ui based selects, as we would need to select the Select button child?:
.juno-select {
+ // Remove border-radius for elements that are neither the first nor the last child
&:not(:first-child, :last-child) {
border-radius: 0;
}
+ // Adjust border-radius for the first child element
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
+ // Adjust border-radius for the last child element
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
-
- // prevent double-borders for identical buttons next to each other:
+
+ // Prevent double borders for identical button variants placed next to each other
.juno-button-default + .juno-button-default,
.juno-button-primary + .juno-button-primary,
.juno-button-default-primary-danger + .juno-button-primary-danger,
.juno-button-subdued + .juno-button-subdued {
border-left: 0;
}
-
-}
\ No newline at end of file
+}
diff --git a/packages/ui-components/src/index.js b/packages/ui-components/src/index.js
index fd8369036..764c804d6 100644
--- a/packages/ui-components/src/index.js
+++ b/packages/ui-components/src/index.js
@@ -49,7 +49,7 @@ export { Grid } from "./components/Grid"
export { GridRow } from "./components/GridRow"
export { GridColumn } from "./components/GridColumn"
export { Icon } from "./components/Icon/index"
-export { InputGroup } from "./components/InputGroup/index.js"
+export { InputGroup } from "./components/InputGroup/index"
export { IntroBox } from "./components/IntroBox/index.js"
export { JsonViewer } from "./components/JsonViewer/index"
export { Label } from "./components/Label/index"