diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4a8cf671..e05c49a11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,12 +10,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- ``
- component for hiding elements in specific media
+- ``
+ - 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`
### Fixed
- ``
- create more whitespace inside `small` tag
- reduce visual impact of border
+- ``
+ - take Markdown rendering into account before testing the maximum preview length
### Changed
@@ -30,6 +36,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- ``
- `` and ``
+### Deprecated
+
+- ``
+ - `firstNonEmptyLineOnly` will be removed, is replaced by `useOnly="firstNonEmptyLine"`
+
## [25.0.0] - 2025-12-01
This is a major release, and it might be not compatible with your current usage of our library. Please read about the necessary changes in the section about how to migrate.
diff --git a/src/cmem/ContentBlobToggler/ContentBlobToggler.tsx b/src/cmem/ContentBlobToggler/ContentBlobToggler.tsx
index 5ba6d1a99..26b214ab1 100644
--- a/src/cmem/ContentBlobToggler/ContentBlobToggler.tsx
+++ b/src/cmem/ContentBlobToggler/ContentBlobToggler.tsx
@@ -58,7 +58,7 @@ export function ContentBlobToggler({
{previewContent}
{enableToggler && (
<>
- …{" "}
+ {" "}…{" "}
{
/**
- The preview content will be cut to this length if it is too long.
+ * The preview content will be cut to this length if it is too long.
*/
previewMaxLength?: number;
/**
- The content string. If it is smaller than previewMaxLength this will be displayed in full, else fullviewContent will be displayed.
+ * The content string.
+ * If it is smaller than `previewMaxLength` this will be displayed in full, else `fullviewContent` will be displayed.
*/
content: string;
- /** If only the first non-empty line should be shown in the preview. This will in addition also be shortened according to previewMaxLength. */
- firstNonEmptyLineOnly?: boolean;
- /** If enabled the preview is rendered as markdown. */
+ /**
+ * Use only parts of `content` in the preview.
+ * `firstMarkdownSection` uses the content until the first double line return.
+ * Currently overwritten by `firstNonEmptyLineOnly`.
+ */
+ useOnly?: "firstNonEmptyLine" | "firstMarkdownSection";
+ /**
+ * If enabled the preview is rendered as Markdown.
+ */
renderPreviewAsMarkdown?: boolean;
- /** White-listing of HTML elements that will be rendered when renderPreviewAsMarkdown is enabled. */
+ /**
+ * White-listing of HTML elements that will be rendered when renderPreviewAsMarkdown is enabled.
+ */
allowedHtmlElementsInPreview?: string[];
- /** Allows to add non-string elements at the end of the content if the full description is shown, i.e. no toggler is necessary.
+ /**
+ * Allows to add non-string elements at the end of the content if the full description is shown, i.e. no toggler is necessary.
* This allows to add non-string elements to both the full-view content and the pure string content.
*/
noTogglerContentSuffix?: JSX.Element;
+ /**
+ * If only the first non-empty line should be shown in the preview.
+ * This will in addition also be shortened according to `previewMaxLength`.
+ * @deprecated (v26) use `useOnly="firstNonEmptyLine"` instead
+ */
+ firstNonEmptyLineOnly?: boolean;
}
-/** Version of the content toggler for text only content. */
+/** Version of the content toggler for text centric content. */
export function StringPreviewContentBlobToggler({
className = "",
previewMaxLength,
@@ -33,14 +49,28 @@ export function StringPreviewContentBlobToggler({
content,
fullviewContent,
startExtended,
- firstNonEmptyLineOnly,
+ useOnly,
renderPreviewAsMarkdown = false,
allowedHtmlElementsInPreview,
noTogglerContentSuffix,
+ firstNonEmptyLineOnly,
}: StringPreviewContentBlobTogglerProps) {
- const previewMaybeFirstLine = firstNonEmptyLineOnly ? firstNonEmptyLine(content) : content;
- const previewString = previewMaxLength ? previewMaybeFirstLine.substr(0, previewMaxLength) : previewMaybeFirstLine;
- const enableToggler = previewString !== content;
+ // need to test `firstNonEmptyLineOnly` until property is removed
+ const useOnlyTest: StringPreviewContentBlobTogglerProps["useOnly"] = firstNonEmptyLineOnly
+ ? "firstNonEmptyLine"
+ : useOnly;
+
+ let previewString = content;
+ switch (useOnlyTest) {
+ case "firstNonEmptyLine":
+ previewString = useOnlyPart(content, regexFirstNonEmptyLine);
+ break;
+ case "firstMarkdownSection":
+ previewString = useOnlyPart(content, regexFirstMarkdownSection);
+ }
+
+ let enableToggler = previewString !== content;
+
let previewContent = renderPreviewAsMarkdown ? (
{previewString}
@@ -48,6 +78,15 @@ export function StringPreviewContentBlobToggler({
) : (
previewString
);
+
+ if (
+ previewMaxLength &&
+ utils.reduceToText(previewContent, { decodeHtmlEntities: true }).length > previewMaxLength
+ ) {
+ previewContent = utils.reduceToText(previewContent, { decodeHtmlEntities: true }).slice(0, previewMaxLength);
+ enableToggler = true;
+ }
+
if (!enableToggler && noTogglerContentSuffix) {
previewContent = (
<>
@@ -60,7 +99,7 @@ export function StringPreviewContentBlobToggler({
return (
{previewContent}}
toggleExtendText={toggleExtendText}
toggleReduceText={toggleReduceText}
fullviewContent={fullviewContent}
@@ -70,17 +109,26 @@ export function StringPreviewContentBlobToggler({
);
}
-const newLineRegex = new RegExp("\r|\n"); // eslint-disable-line
+const regexFirstNonEmptyLine = new RegExp("\r|\n"); // eslint-disable-line
+const regexFirstMarkdownSection = new RegExp("\r\n\r\n|\n\n"); // eslint-disable-line
/**
* Takes the first non-empty line from a preview string.
*/
function firstNonEmptyLine(preview: string) {
+ return useOnlyPart(preview, regexFirstNonEmptyLine);
+}
+
+/**
+ * Returns only the first part from a preview string.
+ * Or the full string as fallback.
+ */
+function useOnlyPart(preview: string, regexTest: RegExp): string {
const previewString = preview.trim();
- const result = newLineRegex.exec(previewString);
- return result !== null ? previewString.substr(0, result.index) : previewString;
+ const result = regexTest.exec(previewString);
+ return result !== null ? result.input.slice(0, result.index) : previewString;
}
export const stringPreviewContentBlobTogglerUtils = {
firstNonEmptyLine,
-};
\ No newline at end of file
+};
diff --git a/src/cmem/ContentBlobToggler/stories/StringPreviewContentBlobToggler.stories.tsx b/src/cmem/ContentBlobToggler/stories/StringPreviewContentBlobToggler.stories.tsx
new file mode 100644
index 000000000..d196bd308
--- /dev/null
+++ b/src/cmem/ContentBlobToggler/stories/StringPreviewContentBlobToggler.stories.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { Meta, StoryFn } from "@storybook/react";
+
+import { Markdown, StringPreviewContentBlobToggler } from "../../../index";
+
+const config = {
+ title: "CMEM/ContentBlobToggler/StringPreview",
+ component: StringPreviewContentBlobToggler,
+} as Meta;
+export default config;
+
+const Template: StoryFn = (args) => (
+
+);
+
+const initialTeststring =
+ "A library for GUI elements.\nIn order to create graphical user interfaces, please have look at the documentation at [Github](https://github.com/eccenca/gui-elements).";
+
+export const Default = Template.bind({});
+Default.args = {
+ content: initialTeststring,
+ fullviewContent: {initialTeststring},
+ previewMaxLength: 64,
+ renderPreviewAsMarkdown: true,
+ toggleExtendText: "show more",
+ toggleReduceText: "show less",
+};
diff --git a/src/cmem/ContentBlobToggler/tests/StringPreviewContentBlobToggler.test.tsx b/src/cmem/ContentBlobToggler/tests/StringPreviewContentBlobToggler.test.tsx
new file mode 100644
index 000000000..f16d9943a
--- /dev/null
+++ b/src/cmem/ContentBlobToggler/tests/StringPreviewContentBlobToggler.test.tsx
@@ -0,0 +1,98 @@
+import React from "react";
+import { render, RenderResult } from "@testing-library/react";
+
+import "@testing-library/jest-dom";
+
+import {
+ StringPreviewContentBlobToggler,
+ StringPreviewContentBlobTogglerProps,
+} from "../StringPreviewContentBlobToggler";
+
+import { Default as StringPreviewContentBlobTogglerStory } from "./../stories/StringPreviewContentBlobToggler.stories";
+
+describe("StringPreviewContentBlobToggler", () => {
+ const textMustExist = (queryByText: RenderResult["queryByText"], text: string) => {
+ expect(queryByText(text, { exact: false })).not.toBeNull();
+ };
+ const textMustNotExist = (queryByText: RenderResult["queryByText"], text: string) => {
+ expect(queryByText(text, { exact: false })).toBeNull();
+ };
+ it("should cut preview and show toggler to extend", () => {
+ const { queryByText } = render(
+
+ );
+ textMustExist(queryByText, "A library for GUI elements.");
+ textMustNotExist(
+ queryByText,
+ "In order to create graphical user interfaces, please have look at the documentation at"
+ );
+ textMustExist(queryByText, "show more");
+ });
+ it("should display full view if `startExtended` is enabled, and show toggler to reduce", () => {
+ const { queryByText } = render(
+
+ );
+ textMustExist(
+ queryByText,
+ "In order to create graphical user interfaces, please have look at the documentation at"
+ );
+ textMustExist(queryByText, "show less");
+ });
+ it('should display only first content line on `useOnly={"firstNonEmptyLine"}`', () => {
+ const { queryByText } = render(
+
+ );
+ textMustExist(queryByText, "A library for GUI elements.");
+ textMustNotExist(queryByText, "In order to create");
+ });
+ it('should use first Markdown paragraph as preview content on `useOnly={"firstMarkdownSection"}` but shorten it', () => {
+ const { queryByText } = render(
+
+ );
+ textMustExist(queryByText, "A library for GUI elements.");
+ textMustExist(queryByText, "In order to create");
+ textMustNotExist(queryByText, "please have look at the documentation at");
+ });
+ it("should display full preview and no toggler if content is short enough", () => {
+ const { queryByText } = render(
+
+ );
+ textMustExist(queryByText, "A library for GUI elements.");
+ textMustExist(
+ queryByText,
+ "In order to create graphical user interfaces, please have look at the documentation at"
+ );
+ textMustNotExist(queryByText, "https://github.com/"); // test if Markdown was rendered
+ textMustNotExist(queryByText, "show more");
+ });
+ it("should not use Markdown rendering on `renderPreviewAsMarkdown={false}`", () => {
+ const { queryByText } = render(
+
+ );
+ textMustExist(queryByText, "A library for GUI elements.");
+ textMustExist(
+ queryByText,
+ "In order to create graphical user interfaces, please have look at the documentation at"
+ );
+ textMustExist(queryByText, "https://github.com/"); // test if Markdown was rendered
+ textMustExist(queryByText, "show more");
+ });
+});
diff --git a/src/components/Typography/InlineText.tsx b/src/components/Typography/InlineText.tsx
new file mode 100644
index 000000000..638ab01fa
--- /dev/null
+++ b/src/components/Typography/InlineText.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+
+import { CLASSPREFIX as eccgui } from "../../configuration/constants";
+import { TestableComponent } from "../interfaces";
+
+export interface InlineTextProps extends React.HTMLAttributes, TestableComponent {
+ /**
+ * Additional CSS class name.
+ */
+ className?: string;
+}
+
+/**
+ * Forces all children to be displayed as inline content.
+ */
+export const InlineText = ({ className = "", children, ...otherProps }: InlineTextProps) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default InlineText;
diff --git a/src/components/Typography/index.ts b/src/components/Typography/index.ts
index fba8b8575..b3687231c 100644
--- a/src/components/Typography/index.ts
+++ b/src/components/Typography/index.ts
@@ -2,3 +2,4 @@ export * from "./Highlighter";
export * from "./HtmlContentBlock";
export * from "./OverflowText";
export * from "./WhiteSpaceContainer";
+export * from "./InlineText";
diff --git a/src/components/Typography/stories/InlineText.stories.tsx b/src/components/Typography/stories/InlineText.stories.tsx
new file mode 100644
index 000000000..4bd6af2b6
--- /dev/null
+++ b/src/components/Typography/stories/InlineText.stories.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { Meta, StoryFn } from "@storybook/react";
+
+import { InlineText } from "../InlineText";
+
+import overflowTextConfig from "./OverflowText.stories";
+
+const config = {
+ title: "Components/Typography/InlineText",
+ component: InlineText,
+ argTypes: {
+ children: overflowTextConfig.argTypes?.children,
+ },
+} as Meta;
+export default config;
+
+const Template: StoryFn = (args) => ;
+
+export const Default = Template.bind({});
+Default.args = {
+ children: (
+