diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b550f94..ef57df12 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,10 +10,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- ``
- component for hiding elements in specific media
+- ``
+ - `title` property now accepts a React component in addition to a string
- ``
- - force children to get displayed as inline content
+ - force children to get displayed as inline content
- ``
- - `useOnly` property: specify if only parts of the content should be used for the shortened preview, this property replaces `firstNonEmptyLineOnly`
+ - `useOnly` property: specify if only parts of the content should be used for the shortened preview, this property replaces `firstNonEmptyLineOnly`
### Fixed
@@ -21,7 +23,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- create more whitespace inside `small` tag
- reduce visual impact of border
- ``
- - take Markdown rendering into account before testing the maximum preview length
+ - take Markdown rendering into account before testing the maximum preview length
- ``
- header-menu items are vertically centered now
@@ -41,7 +43,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Deprecated
- ``
- - `firstNonEmptyLineOnly` will be removed, is replaced by `useOnly="firstNonEmptyLine"`
+ - `firstNonEmptyLineOnly` will be removed, is replaced by `useOnly="firstNonEmptyLine"`
## [25.0.0] - 2025-12-01
diff --git a/src/components/ContentGroup/ContentGroup.stories.tsx b/src/components/ContentGroup/ContentGroup.stories.tsx
index bf524e9b..edb0df2d 100644
--- a/src/components/ContentGroup/ContentGroup.stories.tsx
+++ b/src/components/ContentGroup/ContentGroup.stories.tsx
@@ -45,3 +45,8 @@ BasicExample.args = {
),
};
+
+export const TitleAsElement = BasicExample.bind({});
+TitleAsElement.args = {
+ title:
Title as element
,
+};
diff --git a/src/components/ContentGroup/ContentGroup.tsx b/src/components/ContentGroup/ContentGroup.tsx
index 4ce4172d..1015bc96 100644
--- a/src/components/ContentGroup/ContentGroup.tsx
+++ b/src/components/ContentGroup/ContentGroup.tsx
@@ -1,6 +1,7 @@
-import React from "react";
+import React, { ReactElement } from "react";
import classNames from "classnames";
import Color from "color";
+import { isString } from "lodash";
import { TestableComponent } from "../../components/interfaces";
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
@@ -19,11 +20,13 @@ import {
Tooltip,
} from "../index";
+const MAX_HEADLINE_LEVEL = 6;
+
export interface ContentGroupProps extends Omit, "title">, TestableComponent {
/**
* Title of the content group.
*/
- title?: string;
+ title?: string | ReactElement;
/**
* Level of the content group.
*/
@@ -138,9 +141,17 @@ export const ContentGroup = ({
}, []);
}
- const contextInfoElements = Array.isArray(contextInfo) ? contextInfo : [contextInfo];
+ const contextInfoElements = React.Children.toArray(contextInfo);
const { className: contentClassName, ...otherContentProps } = contentProps ?? {};
+ const headerLevel = Math.min(Math.max(minimumHeadlineLevel, level + minimumHeadlineLevel), MAX_HEADLINE_LEVEL);
+ const titleComponent = isString(title)
+ ? React.createElement(`h${headerLevel}`, {
+ children: {title},
+ className: `${eccgui}-contentgroup__header__title`,
+ })
+ : title;
+
const headerContent = displayHeader ? (
<>
@@ -158,17 +169,7 @@ export const ContentGroup = ({
)}
{title && (
- {React.createElement(
- "h" +
- Math.min(
- Math.max(minimumHeadlineLevel, level + minimumHeadlineLevel),
- 6
- ).toString(),
- {
- children: {title},
- className: `${eccgui}-contentgroup__header__title`,
- }
- )}
+ {titleComponent}
{description && (
<>
@@ -179,8 +180,8 @@ export const ContentGroup = ({
)}
)}
- {contextInfoElements &&
- contextInfoElements[0]?.props &&
+ {contextInfoElements.length > 0 &&
+ React.isValidElement(contextInfoElements[0]) &&
Object.values(contextInfoElements[0].props).every((v) => v !== undefined) && (
diff --git a/src/components/ContentGroup/tests/ContentGroup.test.tsx b/src/components/ContentGroup/tests/ContentGroup.test.tsx
new file mode 100644
index 00000000..714fdae6
--- /dev/null
+++ b/src/components/ContentGroup/tests/ContentGroup.test.tsx
@@ -0,0 +1,475 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+
+import "@testing-library/jest-dom";
+
+import { ContentGroup } from "../../../../index";
+import { BasicExample } from "../ContentGroup.stories";
+
+describe("ContentGroup", () => {
+ describe("basic rendering", () => {
+ it("should render with string title", () => {
+ const { container } = render(content);
+
+ expect(container.querySelector(".eccgui-contentgroup")).toBeInTheDocument();
+ expect(screen.getByText("test title")).toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__header__title")).toBeInTheDocument();
+ expect(screen.getByText("content")).toBeInTheDocument();
+ });
+
+ it("should render with component title", () => {
+ const { container } = render(
+ test component title
}>
+ content
+
+ );
+
+ expect(container.querySelector(".eccgui-contentgroup")).toBeInTheDocument();
+ expect(screen.getByText("test component title")).toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__header")).toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__header__title")).not.toBeInTheDocument();
+ expect(container.querySelector(".my-custom-classname")).toBeInTheDocument();
+ expect(screen.getByText("content")).toBeInTheDocument();
+ });
+
+ it("should render without title", () => {
+ const { container } = render(content only);
+
+ expect(container.querySelector(".eccgui-contentgroup")).toBeInTheDocument();
+ expect(screen.getByText("content only")).toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__header")).not.toBeInTheDocument();
+ });
+
+ it("should render with BasicExample story args", () => {
+ const { container } = render();
+
+ expect(container.querySelector(".eccgui-contentgroup")).toBeInTheDocument();
+ expect(screen.getByText("Content group title")).toBeInTheDocument();
+ });
+
+ it("should apply custom className", () => {
+ const { container } = render(content);
+
+ const contentGroup = container.querySelector(".eccgui-contentgroup");
+ expect(contentGroup).toHaveClass("custom-class");
+ });
+
+ it("should pass data-testid attribute", () => {
+ const { container } = render(
+
+ content
+
+ );
+
+ expect(container.querySelector('[data-testid="content-group-test"]')).toBeInTheDocument();
+ });
+ });
+
+ describe("collapse functionality", () => {
+ it("should render toggle collapse button when handlerToggleCollapse is provided", () => {
+ const handleToggle = jest.fn();
+ const { container } = render(
+
+ content
+
+ );
+
+ expect(container.querySelector(".eccgui-contentgroup__header__toggler")).toBeInTheDocument();
+ });
+
+ it("should not render toggle button when handlerToggleCollapse is not provided", () => {
+ const { container } = render(Content);
+
+ expect(container.querySelector(".eccgui-contentgroup__header__toggler")).not.toBeInTheDocument();
+ });
+
+ it("should call handlerToggleCollapse when toggle button is clicked", () => {
+ const handleToggle = jest.fn();
+ const { container } = render(
+
+ Content
+
+ );
+
+ const toggleButton = container.querySelector(".eccgui-contentgroup__header__toggler");
+ fireEvent.click(toggleButton!);
+
+ expect(handleToggle).toHaveBeenCalledTimes(1);
+ });
+
+ it("should hide content when isCollapsed is true", () => {
+ const handleToggle = jest.fn();
+ const { container } = render(
+
+ Hidden Content
+
+ );
+
+ expect(container.querySelector(".eccgui-contentgroup__content")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("content")).not.toBeInTheDocument();
+ });
+
+ it("should show content when isCollapsed is false", () => {
+ const handleToggle = jest.fn();
+ const { container } = render(
+
+ Visible Content
+
+ );
+
+ expect(container.querySelector(".eccgui-contentgroup__content")).toBeInTheDocument();
+ expect(screen.getByTestId("content")).toBeInTheDocument();
+ });
+
+ it("should hide action options when collapsed", () => {
+ const handleToggle = jest.fn();
+ const { container } = render(
+ Action}
+ >
+ Content
+
+ );
+
+ expect(screen.queryByTestId("action-btn")).not.toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__header__options")).not.toBeInTheDocument();
+ });
+
+ it("should show action options when not collapsed", () => {
+ const handleToggle = jest.fn();
+ const { container } = render(
+ Action}
+ >
+ Content
+
+ );
+
+ expect(screen.getByTestId("action-btn")).toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__header__options")).toBeInTheDocument();
+ });
+
+ it("should use custom textToggleCollapse", () => {
+ const handleToggle = jest.fn();
+ const { container } = render(
+
+ Content
+
+ );
+
+ const icon = container.querySelector(".eccgui-contentgroup__header__toggler svg");
+ expect(icon).toHaveAttribute("description", "Toggle content");
+ });
+
+ it("should use default 'Show less' text when not collapsed", () => {
+ const handleToggle = jest.fn();
+ const { container } = render(
+
+ Content
+
+ );
+
+ const icon = container.querySelector(".eccgui-contentgroup__header__toggler svg");
+ expect(icon).toHaveAttribute("description", "Show less");
+ });
+
+ it("should use default 'Show more' text when collapsed", () => {
+ const handleToggle = jest.fn();
+ const { container } = render(
+
+ Content
+
+ );
+
+ const icon = container.querySelector(".eccgui-contentgroup__header__toggler svg");
+ expect(icon).toHaveAttribute("description", "Show more");
+ });
+ });
+
+ describe("border styling", () => {
+ it("should apply border-main class when borderMainConnection is true", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector(".eccgui-contentgroup--border-main")).toBeInTheDocument();
+ });
+
+ it("should not apply border-main class when borderMainConnection is false", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector(".eccgui-contentgroup--border-main")).not.toBeInTheDocument();
+ });
+
+ it("should apply border-sub class when borderSubConnection is true", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector(".eccgui-contentgroup--border-sub")).toBeInTheDocument();
+ });
+
+ it("should apply border-sub class when borderSubConnection is an array of colors", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector(".eccgui-contentgroup--border-sub")).toBeInTheDocument();
+ });
+
+ it("should set custom CSS property for gradient border when borderSubConnection is an array", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ const section = container.querySelector(".eccgui-contentgroup");
+ const style = (section as HTMLElement)?.style.getPropertyValue("--eccgui-color-contentgroup-border-sub");
+ expect(style).toBeTruthy();
+ });
+
+ it("should not set gradient border CSS property when borderSubConnection is boolean", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ const section = container.querySelector(".eccgui-contentgroup");
+ const style = (section as HTMLElement)?.style.getPropertyValue("--eccgui-color-contentgroup-border-sub");
+ expect(style).toBeFalsy();
+ });
+ });
+
+ describe("whitespace size", () => {
+ const sizes = ["tiny", "small", "medium", "large", "xlarge"] as const;
+
+ sizes.forEach((size) => {
+ it(`should apply padding class for whitespaceSize="${size}"`, () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector(`.eccgui-contentgroup--padding-${size}`)).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("headline level", () => {
+ it("should render title with correct headline level", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector("h2")).toBeInTheDocument();
+ });
+
+ it("should render title with maximum headline level of 6", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector("h6")).toBeInTheDocument();
+ });
+
+ it("should render title with default minimumHeadlineLevel of 3", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector("h4")).toBeInTheDocument();
+ });
+ });
+
+ describe("context info", () => {
+ it("should render context info in header when title is present", () => {
+ const { container } = render(
+ Context Info}>
+ Content
+
+ );
+
+ expect(screen.getByTestId("context")).toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__header__context")).toBeInTheDocument();
+ });
+
+ it("should render context info in content area when no title", () => {
+ const { container } = render(
+ Context Info}>Content
+ );
+
+ expect(screen.getByTestId("context")).toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__content__context")).toBeInTheDocument();
+ });
+
+ it("should render array of context info elements", () => {
+ render(
+
+ Context 1
+ ,
+
+ Context 2
+ ,
+ ]}
+ >
+ Content
+
+ );
+
+ expect(screen.getByTestId("context-1")).toBeInTheDocument();
+ expect(screen.getByTestId("context-2")).toBeInTheDocument();
+ });
+ });
+
+ describe("action options", () => {
+ it("should render action options in header when title is present", () => {
+ const { container } = render(
+ Action}>
+ Content
+
+ );
+
+ expect(screen.getByTestId("action")).toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__header__options")).toBeInTheDocument();
+ });
+
+ it("should render action options in content area when no title", () => {
+ const { container } = render(
+ Action}>Content
+ );
+
+ expect(screen.getByTestId("action")).toBeInTheDocument();
+ expect(container.querySelector(".eccgui-contentgroup__content__options")).toBeInTheDocument();
+ });
+ });
+
+ describe("annotation", () => {
+ it("should render annotation in content area", () => {
+ render(
+ Annotation}>
+ Content
+
+ );
+
+ expect(screen.getByTestId("annotation")).toBeInTheDocument();
+ });
+ });
+
+ describe("description", () => {
+ it("should render info icon when description is provided", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector(".dmapp--text-info")).toBeInTheDocument();
+ });
+
+ it("should not render info icon when description is not provided", () => {
+ const { container } = render(Content);
+
+ expect(container.querySelector(".dmapp--text-info")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("group divider", () => {
+ it("should render divider by default", () => {
+ const { container } = render(
+ {}}>
+ Content
+
+ );
+
+ expect(container.querySelector(".eccgui-separation__divider-horizontal")).toBeInTheDocument();
+ });
+
+ it("should hide divider when hideGroupDivider is true", () => {
+ const { container } = render(
+ {}} hideGroupDivider={true}>
+ Content
+
+ );
+
+ expect(container.querySelector(".eccgui-separation__divider-horizontal")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("content props", () => {
+ it("should apply contentProps to content container", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ expect(container.querySelector(".custom-content-class")).toBeInTheDocument();
+ });
+ });
+
+ describe("ReactElement as title", () => {
+ it("should render ReactElement title", () => {
+ render(
+ Custom Title Element}>
+ Content
+
+ );
+
+ expect(screen.getByTestId("custom-title")).toBeInTheDocument();
+ expect(screen.getByText("Custom Title Element")).toBeInTheDocument();
+ });
+ });
+
+ describe("custom styles", () => {
+ it("should apply custom style prop", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ const section = container.querySelector(".eccgui-contentgroup") as HTMLElement;
+ expect(section.style.backgroundColor).toBe("red");
+ });
+
+ it("should merge custom style with gradient border style", () => {
+ const { container } = render(
+
+ Content
+
+ );
+
+ const section = container.querySelector(".eccgui-contentgroup") as HTMLElement;
+ expect(section.style.backgroundColor).toBe("red");
+ expect(section.style.getPropertyValue("--eccgui-color-contentgroup-border-sub")).toBeTruthy();
+ });
+ });
+});