Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

- `<ApplicationViewability />`
- component for hiding elements in specific media
- `<InlineText />`
- force children to get displayed as inline content
- `<StringPreviewContentBlobToggler />`
- `useOnly` property: specify if only parts of the content should be used for the shortened preview, this property replaces `firstNonEmptyLineOnly`

### Fixed

- `<Tag />`
- create more whitespace inside `small` tag
- reduce visual impact of border
- `<StringPreviewContentBlobToggler />`
- take Markdown rendering into account before testing the maximum preview length

### Changed

Expand All @@ -30,6 +36,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- `<GridColumn />`
- `<PropertyName />` and `<PropertyValue />`

### Deprecated

- `<StringPreviewContentBlobToggler />`
- `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.
Expand Down
2 changes: 1 addition & 1 deletion src/cmem/ContentBlobToggler/ContentBlobToggler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function ContentBlobToggler({
{previewContent}
{enableToggler && (
<>
&hellip;{" "}
{" "}&hellip;{" "}
<Link
href="#more"
data-test-id={"content-blob-toggler-more-link"}
Expand Down
84 changes: 66 additions & 18 deletions src/cmem/ContentBlobToggler/StringPreviewContentBlobToggler.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,46 @@
import React from "react";

import { ContentBlobToggler, ContentBlobTogglerProps, Markdown } from "./..";
import { ContentBlobToggler, ContentBlobTogglerProps, InlineText, Markdown, utils } from "./../../index";

export interface StringPreviewContentBlobTogglerProps
extends Omit<ContentBlobTogglerProps, "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,
Expand All @@ -33,21 +49,44 @@ 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 ? (
<Markdown key="markdown-content" allowedElements={allowedHtmlElementsInPreview}>
{previewString}
</Markdown>
) : (
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 = (
<>
Expand All @@ -60,7 +99,7 @@ export function StringPreviewContentBlobToggler({
return (
<ContentBlobToggler
className={className}
previewContent={previewContent}
previewContent={<InlineText>{previewContent}</InlineText>}
toggleExtendText={toggleExtendText}
toggleReduceText={toggleReduceText}
fullviewContent={fullviewContent}
Expand All @@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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<typeof StringPreviewContentBlobToggler>;
export default config;

const Template: StoryFn<typeof StringPreviewContentBlobToggler> = (args) => (
<StringPreviewContentBlobToggler {...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: <Markdown htmlContentBlockProps={{ large: true }}>{initialTeststring}</Markdown>,
previewMaxLength: 64,
renderPreviewAsMarkdown: true,
toggleExtendText: "show more",
toggleReduceText: "show less",
};
Original file line number Diff line number Diff line change
@@ -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(
<StringPreviewContentBlobToggler
{...(StringPreviewContentBlobTogglerStory.args as StringPreviewContentBlobTogglerProps)}
/>
);
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(
<StringPreviewContentBlobToggler
{...(StringPreviewContentBlobTogglerStory.args as StringPreviewContentBlobTogglerProps)}
startExtended
/>
);
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(
<StringPreviewContentBlobToggler
{...(StringPreviewContentBlobTogglerStory.args as StringPreviewContentBlobTogglerProps)}
useOnly={"firstNonEmptyLine"}
/>
);
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(
<StringPreviewContentBlobToggler
{...(StringPreviewContentBlobTogglerStory.args as StringPreviewContentBlobTogglerProps)}
useOnly={"firstMarkdownSection"}
/>
);
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(
<StringPreviewContentBlobToggler
{...(StringPreviewContentBlobTogglerStory.args as StringPreviewContentBlobTogglerProps)}
previewMaxLength={144}
/>
);
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(
<StringPreviewContentBlobToggler
{...(StringPreviewContentBlobTogglerStory.args as StringPreviewContentBlobTogglerProps)}
previewMaxLength={144}
renderPreviewAsMarkdown={false}
/>
);
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");
});
});
24 changes: 24 additions & 0 deletions src/components/Typography/InlineText.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>, TestableComponent {
/**
* Additional CSS class name.
*/
className?: string;
}

/**
* Forces all children to be displayed as inline content.
*/
export const InlineText = ({ className = "", children, ...otherProps }: InlineTextProps) => {
return (
<div {...otherProps} className={`${eccgui}-typography__inlinetext` + (className ? " " + className : "")}>
{children}
</div>
);
};

export default InlineText;
1 change: 1 addition & 0 deletions src/components/Typography/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./Highlighter";
export * from "./HtmlContentBlock";
export * from "./OverflowText";
export * from "./WhiteSpaceContainer";
export * from "./InlineText";
27 changes: 27 additions & 0 deletions src/components/Typography/stories/InlineText.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof InlineText>;
export default config;

const Template: StoryFn<typeof InlineText> = (args) => <InlineText {...args} />;

export const Default = Template.bind({});
Default.args = {
children: (
<div>
<div>Block line 1.</div>
<div>Block line 2.</div>
</div>
),
};
Loading