diff --git a/.changeset/lucky-weeks-dance.md b/.changeset/lucky-weeks-dance.md new file mode 100644 index 0000000000..c1dc5160c7 --- /dev/null +++ b/.changeset/lucky-weeks-dance.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-ui-components": minor +--- + +Add `DescriptionList`, `DescriptionTerm` and `DescriptionDefinition` components and export them as `DL`, `DT`, `DD` shorthand. diff --git a/packages/ui-components/src/components/DescriptionDefinition/DescriptionDefinition.component.tsx b/packages/ui-components/src/components/DescriptionDefinition/DescriptionDefinition.component.tsx new file mode 100644 index 0000000000..df26658bbb --- /dev/null +++ b/packages/ui-components/src/components/DescriptionDefinition/DescriptionDefinition.component.tsx @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ReactNode } from "react" + +const baseStyles = ` + jn:grid + jn:items-start + jn:border-b + jn:border-theme-default + jn:bg-dd-background + jn:text-dd-text + jn:p-2 + jn:col-span-3 +` + +export interface DescriptionDefinitionProps { + /** + * Content to be displayed as the description, accommodating text or more complex nodes to explain or define the associated term. + */ + children: ReactNode + /** + * Additional class names for applying custom styles or overriding default styles on the
element. + */ + className?: string +} + +/** + * Represents the definition or description in a description list, rendering as an HTML
element. + * Pairs with DescriptionTerm to complete the term-description association, offering flexible content styling. + */ +export const DescriptionDefinition: React.FC = ({ children, className = "" }) => ( +
{children}
+) + +export const DD = DescriptionDefinition diff --git a/packages/ui-components/src/components/DescriptionDefinition/DescriptionDefinition.test.tsx b/packages/ui-components/src/components/DescriptionDefinition/DescriptionDefinition.test.tsx new file mode 100644 index 0000000000..df97314fb0 --- /dev/null +++ b/packages/ui-components/src/components/DescriptionDefinition/DescriptionDefinition.test.tsx @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { render, screen } from "@testing-library/react" +import { describe, it, expect } from "vitest" +import { DescriptionDefinition } from "../DescriptionDefinition" + +describe("DescriptionDefinition", () => { + it("renders the children correctly", () => { + render(Test Description) + expect(screen.getByText("Test Description")).toBeInTheDocument() + }) + + it("applies custom className", () => { + const customClass = "custom-class" + render(Test Description) + const ddElement = screen.getByText("Test Description") + expect(ddElement).toHaveClass(customClass) + }) + + it("renders within a
element", () => { + render(Test Description) + const ddElement = screen.getByText("Test Description") + expect(ddElement.tagName).toBe("DD") + }) + + it("can render complex children", () => { + render( + + Complex Content + + ) + expect(screen.getByText("Complex")).toBeInTheDocument() + expect(screen.getByText("Content")).toBeInTheDocument() + }) +}) diff --git a/packages/ui-components/src/components/DescriptionDefinition/index.ts b/packages/ui-components/src/components/DescriptionDefinition/index.ts new file mode 100644 index 0000000000..26f02a17ec --- /dev/null +++ b/packages/ui-components/src/components/DescriptionDefinition/index.ts @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { DescriptionDefinition, type DescriptionDefinitionProps } from "./DescriptionDefinition.component" diff --git a/packages/ui-components/src/components/DescriptionList/DescriptionList.component.tsx b/packages/ui-components/src/components/DescriptionList/DescriptionList.component.tsx new file mode 100644 index 0000000000..20b7383ae8 --- /dev/null +++ b/packages/ui-components/src/components/DescriptionList/DescriptionList.component.tsx @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ReactElement } from "react" +import { DescriptionTermProps } from "../DescriptionTerm" +import { DescriptionDefinitionProps } from "../DescriptionDefinition" + +export interface DescriptionListProps { + /** + * Child components must be either DescriptionTerm or DescriptionDefinition to maintain semantic structure. + * Supports multiple instances to create a detailed list of terms and definitions. + */ + children: + | ReactElement + | Array> + | ReactElement<"div"> + /** + * Determines the alignment of terms within the list. Align terms to the left or right based on preference for display style. + */ + alignTerms?: "left" | "right" + /** + * Additional custom class names to apply styles to the
element or to extend styling from the design system. + */ + className?: string +} + +/** + * A wrapper component that semantically represents a list of terms and their corresponding descriptions. + * This component enforces structure by expecting child elements of DescriptionTerm or DescriptionDefinition, + * aligning them according to the specified terms alignment. + */ +export const DescriptionList: React.FC = ({ children, alignTerms = "right", className = "" }) => ( +
+ {children} +
+) + +export const DL = DescriptionList diff --git a/packages/ui-components/src/components/DescriptionList/DescriptionList.stories.tsx b/packages/ui-components/src/components/DescriptionList/DescriptionList.stories.tsx new file mode 100644 index 0000000000..1646dd8a78 --- /dev/null +++ b/packages/ui-components/src/components/DescriptionList/DescriptionList.stories.tsx @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import type { Meta, StoryObj } from "@storybook/react-vite" +import { DL } from "./DescriptionList.component" +import { DT } from "../DescriptionTerm/DescriptionTerm.component" +import { DD } from "../DescriptionDefinition/DescriptionDefinition.component" + +const meta: Meta = { + title: "Components/DescriptionList", + component: DL, + parameters: { + docs: { + description: { + component: ` +A wrapper component that semantically represents a list of terms and their corresponding descriptions. + +This component enforces structure by expecting child elements of \`DescriptionTerm\` or \`DescriptionDefinition\`. + +### Grid Layout +- By default, the component uses a 4-column grid layout where each \`DescriptionTerm\` spans 1 column and each \`DescriptionDefinition\` spans 3 columns. +- Customize the grid template by passing other Tailwind CSS grid classes via the \`className\` prop to override the defaults. + +#### Example +\`\`\`jsx +
+
Shipping Method
+
Standard shipping: 5-7 business days.
+
+\`\`\` + `, + }, + }, + }, + argTypes: { + children: { + control: false, + }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: (args) => ( +
+
Shipping
+
Standard shipping: 5-7 business days.
+
Payment Options
+
+ Credit/Debit cards, PayPal, and bank transfer. Lots and lots and lots of options. Oh so many, many options. +
+
Delivery Time
+
1 day, 2 days, 3 days.
+
+ ), +} + +export const LeftAligned: Story = { + render: (args) => ( +
+
Shipping Method
+
Standard shipping: 5-7 business days.
+
Payment Options
+
+ Credit/Debit cards, PayPal, and bank transfer. Lots and lots of options available for a seamless transaction + experience. +
+
Delivery Time
+
Estimated delivery between 1 to 3 business days after shipping.
+
Return Policy
+
+ Returns are accepted within 30 days of purchase. Items must be returned in their original packaging and + condition. +
+
Customer Support
+
+ Available via phone, email, and live chat from 9 AM to 6 PM, Monday to Friday. Our support team is ready to + assist with any inquiries. +
+
+ ), +} diff --git a/packages/ui-components/src/components/DescriptionList/DescriptionList.test.tsx b/packages/ui-components/src/components/DescriptionList/DescriptionList.test.tsx new file mode 100644 index 0000000000..d952a5af4d --- /dev/null +++ b/packages/ui-components/src/components/DescriptionList/DescriptionList.test.tsx @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { render, screen } from "@testing-library/react" +import { describe, it, expect } from "vitest" +import { DescriptionList } from "./DescriptionList.component" +import { DescriptionTerm } from "../DescriptionTerm" +import { DescriptionDefinition } from "../DescriptionDefinition" + +describe("DescriptionList", () => { + it("renders child DescriptionTerm and DescriptionDefinition components correctly", () => { + render( + + Term 1 + Definition 1 + + ) + expect(screen.getByText("Term 1")).toBeInTheDocument() + expect(screen.getByText("Definition 1")).toBeInTheDocument() + }) + + it("applies custom className to the
element", () => { + const customClass = "custom-class" + render( + + Term 2 + Definition 2 + + ) + + const dlElement = screen.getByTestId("description-list") + expect(dlElement).toHaveClass(customClass) + }) + + it("aligns terms to the right by default", () => { + render( + + Term 3 + Definition 3 + + ) + }) + + it("aligns terms to the left when specified", () => { + render( + + Left Term + Definition for Left Term + + ) + }) + + it("renders multiple terms and definitions in a single list", () => { + render( + + Term 4 + Definition 4 + Term 5 + Definition 5 + + ) + expect(screen.getByText("Term 4")).toBeInTheDocument() + expect(screen.getByText("Definition 4")).toBeInTheDocument() + expect(screen.getByText("Term 5")).toBeInTheDocument() + expect(screen.getByText("Definition 5")).toBeInTheDocument() + }) +}) diff --git a/packages/ui-components/src/components/DescriptionList/index.ts b/packages/ui-components/src/components/DescriptionList/index.ts new file mode 100644 index 0000000000..f91a5ee2a4 --- /dev/null +++ b/packages/ui-components/src/components/DescriptionList/index.ts @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { DescriptionList, type DescriptionListProps } from "./DescriptionList.component" diff --git a/packages/ui-components/src/components/DescriptionTerm/DescriptionTerm.component.tsx b/packages/ui-components/src/components/DescriptionTerm/DescriptionTerm.component.tsx new file mode 100644 index 0000000000..582f884044 --- /dev/null +++ b/packages/ui-components/src/components/DescriptionTerm/DescriptionTerm.component.tsx @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ReactNode } from "react" + +const baseStyles = ` +jn:grid +jn:items-start +jn:border-b +jn:border-theme-default +jn:bg-dt-background +jn:text-dt-text +jn:font-bold +jn:gap-y-[0.25rem] +jn:whitespace-nowrap +jn:p-2 +jn:col-span-1 +jn:group-[.align-right]:text-right +jn:group-[.align-left]:text-left +` + +export interface DescriptionTermProps { + /** + * Content to be displayed as the term, which could be simple text or any ReactNode, providing semantic meaning to the associated description. + */ + children: ReactNode + /** + * Custom class names to apply additional styling to the
element, useful for overrides or custom styles. + */ + className?: string +} + +/** + * Represents a term in a description list, rendering an HTML
element. + * Used to denote terms, headers, or keys in a semantic way, allowing for flexible styling. + */ +export const DescriptionTerm: React.FC = ({ children, className = "" }) => ( +
{children}
+) + +export const DT = DescriptionTerm diff --git a/packages/ui-components/src/components/DescriptionTerm/DescriptionTerm.test.tsx b/packages/ui-components/src/components/DescriptionTerm/DescriptionTerm.test.tsx new file mode 100644 index 0000000000..b20bb2e922 --- /dev/null +++ b/packages/ui-components/src/components/DescriptionTerm/DescriptionTerm.test.tsx @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { render, screen } from "@testing-library/react" +import { describe, it, expect } from "vitest" +import { DescriptionTerm } from "./DescriptionTerm.component" + +describe("DescriptionTerm", () => { + it("renders the children properly", () => { + render(Test Term) + expect(screen.getByText("Test Term")).toBeInTheDocument() + }) + + it("applies custom className to the
element", () => { + const customClass = "custom-class" + render(Styled Term) + const dtElement = screen.getByText("Styled Term") + expect(dtElement).toHaveClass(customClass) + }) + + it("renders within a
element", () => { + render(Term Element) + const dtElement = screen.getByText("Term Element") + expect(dtElement.tagName.toLowerCase()).toBe("dt") + }) + + it("can render complex children", () => { + render( + + Complex Term Content + + ) + expect(screen.getByText("Complex Term")).toBeInTheDocument() + expect(screen.getByText("Content")).toBeInTheDocument() + }) +}) diff --git a/packages/ui-components/src/components/DescriptionTerm/index.ts b/packages/ui-components/src/components/DescriptionTerm/index.ts new file mode 100644 index 0000000000..99c3cb2656 --- /dev/null +++ b/packages/ui-components/src/components/DescriptionTerm/index.ts @@ -0,0 +1,6 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { DescriptionTerm, type DescriptionTermProps } from "./DescriptionTerm.component" diff --git a/packages/ui-components/src/global.css b/packages/ui-components/src/global.css index d8254d192e..fec9640e7d 100644 --- a/packages/ui-components/src/global.css +++ b/packages/ui-components/src/global.css @@ -412,7 +412,7 @@ --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active); /* Component Border Colors: */ - --border-color-theme-default: var(--color-default-border); + --border-color-theme-default: var(--color-border-default); --border-color-theme-box-default: var(--color-box-border); @@ -489,6 +489,12 @@ --shadow-theme-default: var(--box-shadow-default); --shadow-theme-default-hover: var(--box-shadow-hover); + + /* Description - DescriptionList, DescriptionTerm, DescriptionDefinition */ + --color-dt-background: var(--color-background-lvl-1); + --color-dd-background: var(--color-transparent); + --color-dt-text: var(--color-text-high); + --color-dd-text: var(--color-text-high); } /* JUNO THEME: LIGHT MODE */ diff --git a/packages/ui-components/src/index.ts b/packages/ui-components/src/index.ts index 781d39de78..1f9d0f34d8 100644 --- a/packages/ui-components/src/index.ts +++ b/packages/ui-components/src/index.ts @@ -36,6 +36,9 @@ export { DataGridHeadCell } from "./components/DataGridHeadCell/DataGridHeadCell export { DataGridRow } from "./components/DataGridRow/DataGridRow.component" export { DataGridToolbar } from "./components/DataGridToolbar/DataGridToolbar.component" export { DateTimePicker } from "./components/DateTimePicker/DateTimePicker.component" +export { DescriptionList, DL } from "./components/DescriptionList/DescriptionList.component" +export { DescriptionTerm, DT } from "./components/DescriptionTerm/DescriptionTerm.component" +export { DescriptionDefinition, DD } from "./components/DescriptionDefinition/DescriptionDefinition.component" export { Form } from "./components/Form/Form.component" export { FormattedText } from "./components/FormattedText/FormattedText.component" export { FormRow } from "./components/FormRow/FormRow.component" @@ -148,6 +151,9 @@ export type { CustomLocale, DateChangeHandler, } from "./components/DateTimePicker/DateTimePicker.types" +export type { DescriptionListProps } from "./components/DescriptionList/DescriptionList.component" +export type { DescriptionTermProps } from "./components/DescriptionTerm/DescriptionTerm.component" +export type { DescriptionDefinitionProps } from "./components/DescriptionDefinition/DescriptionDefinition.component" export type { FormProps } from "./components/Form/Form.component" export type { FormattedTextProps } from "./components/FormattedText/FormattedText.component" export type { FormRowProps } from "./components/FormRow/FormRow.component" diff --git a/packages/ui-components/src/theme.css b/packages/ui-components/src/theme.css index a09ee55fc5..37790aa797 100644 --- a/packages/ui-components/src/theme.css +++ b/packages/ui-components/src/theme.css @@ -402,7 +402,7 @@ --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active); /* Component Border Colors: */ - --border-color-theme-default: var(--color-default-border); + --border-color-theme-default: var(--color-border-default); --border-color-theme-box-default: var(--color-box-border); @@ -479,6 +479,12 @@ --shadow-theme-default: var(--box-shadow-default); --shadow-theme-default-hover: var(--box-shadow-hover); + + /* Description - DescriptionList, DescriptionTerm, DescriptionDefinition */ + --color-dt-background: var(--color-background-lvl-1); + --color-dd-background: var(--color-transparent); + --color-dt-text: var(--color-text-high); + --color-dd-text: var(--color-text-high); } /* JUNO THEME: LIGHT MODE */