Skip to content

Commit dad204f

Browse files
committed
add component to display single chat contents
1 parent 65723cb commit dad204f

7 files changed

Lines changed: 194 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- `<ChatContent />`
12+
- displays single chat contents in a bubble, including options to add status line and avatar
13+
914
## [24.4.1] - 2025-08-25
1015

1116
### Fixed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React from "react";
2+
3+
import { TestableComponent } from "../../components/interfaces";
4+
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
5+
6+
import { Markdown, MarkdownProps } from "./../../cmem/markdown/Markdown";
7+
import { DepictionProps } from "./../Depiction/Depiction";
8+
import { FlexibleLayoutContainer, FlexibleLayoutItem } from "./../FlexibleLayout";
9+
import { Spacing } from "./../Separation/Spacing";
10+
import { HtmlContentBlock, OverflowTextProps, WhiteSpaceContainer } from "./../Typography";
11+
12+
export interface ChatContentProps extends React.HTMLAttributes<HTMLDivElement>, TestableComponent {
13+
/**
14+
* Should be a line of text, e.g. username, timestamp, ...
15+
*/
16+
statusLine?: React.ReactElement<OverflowTextProps>;
17+
/**
18+
* How the chat content box is displayed.
19+
*/
20+
displayType?: "free" | "simple" | "bubble";
21+
/**
22+
* A depiction used as avatar next to the content box.
23+
*/
24+
avatar?: React.ReactElement<DepictionProps>;
25+
/**
26+
* If indented then the content box has some white space on one side.
27+
*/
28+
indentation?: boolean;
29+
/**
30+
* How the content box and avatar is aligned.
31+
* If `left` is set then the avatar is on the left side, and the indentation on the right side.
32+
*/
33+
alignment?: "left" | "right" | "block";
34+
/**
35+
* If given then the content is automatically parsed and displayed by our `<Markdown />` component.
36+
* `children` need to a `string` then, otherwise it cannot be parsed.
37+
*/
38+
markdownProps?: Omit<MarkdownProps, "children">;
39+
}
40+
41+
/**
42+
* Component to display singe chat contents, including avatar and status line.
43+
*/
44+
export const ChatContent = ({
45+
className,
46+
children,
47+
statusLine,
48+
avatar,
49+
displayType = "bubble",
50+
indentation = true,
51+
alignment = "left",
52+
markdownProps,
53+
...otherDivProps
54+
}: ChatContentProps) => {
55+
const content = (
56+
<div
57+
className={
58+
`${eccgui}-chat__content` +
59+
` ${eccgui}-chat__content--display-${displayType}` +
60+
` ${eccgui}-chat__content--align-${alignment}` +
61+
(className ? ` ${className}` : "")
62+
}
63+
{...otherDivProps}
64+
>
65+
{statusLine && (
66+
<HtmlContentBlock small>
67+
{statusLine}
68+
<Spacing size="small" />
69+
</HtmlContentBlock>
70+
)}
71+
{markdownProps && typeof children === "string" ? (
72+
<Markdown {...markdownProps}>{children}</Markdown>
73+
) : (
74+
children
75+
)}
76+
</div>
77+
);
78+
79+
return (
80+
<WhiteSpaceContainer
81+
marginLeft={alignment === "right" && indentation ? "xlarge" : undefined}
82+
marginRight={alignment === "left" && indentation ? "xlarge" : undefined}
83+
>
84+
{avatar ? (
85+
<FlexibleLayoutContainer noEqualItemSpace gapSize="tiny">
86+
<FlexibleLayoutItem
87+
growFactor={0}
88+
shrinkFactor={0}
89+
style={alignment === "right" ? { order: 1 } : undefined}
90+
>
91+
{React.cloneElement(avatar, { size: "small", ratio: "1:1", rounded: true, resizing: "cover" })}
92+
</FlexibleLayoutItem>
93+
<FlexibleLayoutItem>{content}</FlexibleLayoutItem>
94+
</FlexibleLayoutContainer>
95+
) : (
96+
content
97+
)}
98+
</WhiteSpaceContainer>
99+
);
100+
};
101+
102+
export default ChatContent;

src/components/Chat/_chat.scss

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.#{$eccgui}-chat__content {
2+
--#{$eccgui}-chat__content-background: #{$light-gray5};
3+
4+
position: relative;
5+
min-height: $button-height;
6+
padding: $eccgui-size-inline-whitespace;
7+
background-color: var(--#{$eccgui}-chat__content-background);
8+
border-radius: $pt-border-radius;
9+
}
10+
11+
.#{$eccgui}-chat__content--display-free {
12+
padding: 0;
13+
14+
--#{$eccgui}-chat__content-background: transparent;
15+
}
16+
17+
.#{$eccgui}-chat__content--display-bubble {
18+
margin-left: 0.5 * $eccgui-size-block-whitespace;
19+
20+
&::before {
21+
position: absolute;
22+
top: calc(#{mini-units(3)} - #{0.5 * $eccgui-size-block-whitespace});
23+
left: calc(-0.5 * #{$eccgui-size-block-whitespace} + 1px);
24+
width: $eccgui-size-block-whitespace;
25+
height: $eccgui-size-block-whitespace;
26+
content: " ";
27+
background-color: var(--#{$eccgui}-chat__content-background);
28+
clip-path: polygon(0% 50%, 50% 0%, 50% 100%);
29+
}
30+
31+
&.#{$eccgui}-chat__content--align-right {
32+
margin-right: 0.5 * $eccgui-size-block-whitespace;
33+
margin-left: none;
34+
35+
&::before {
36+
right: calc(-0.5 * #{$eccgui-size-block-whitespace} + 1px);
37+
left: auto;
38+
clip-path: polygon(100% 50%, 50% 0%, 50% 100%);
39+
}
40+
}
41+
}

src/components/Chat/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./ChatContent";
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from "react";
2+
import { LoremIpsum } from "react-lorem-ipsum";
3+
import { Meta, StoryFn } from "@storybook/react";
4+
5+
import { ChatContent, Depiction, Icon, OverflowText } from "../../../index";
6+
7+
import canonicalIcons from "./../../Icon/canonicalIconNames";
8+
9+
const allIcons = new Map([
10+
...Object.keys(canonicalIcons).map((keyId) => {
11+
return [`Icon: ${keyId}`, <Depiction image={<Icon name={keyId} />} />];
12+
}),
13+
]);
14+
15+
const exampleImages = {
16+
None: undefined,
17+
...Object.fromEntries(allIcons),
18+
};
19+
20+
export default {
21+
title: "Components/Chat",
22+
component: ChatContent,
23+
argTypes: {
24+
avatar: {
25+
control: "select",
26+
options: Object.keys(exampleImages),
27+
mapping: exampleImages,
28+
},
29+
},
30+
} as Meta<typeof ChatContent>;
31+
32+
const TemplateFull: StoryFn<typeof ChatContent> = (args) => <ChatContent {...args} />;
33+
34+
export const Default = TemplateFull.bind({});
35+
Default.args = {
36+
children: <LoremIpsum p={2} avgSentencesPerParagraph={4} random={false} />,
37+
avatar: <Depiction image={<Icon name={"application-useraccount"} />} />,
38+
statusLine: (
39+
<OverflowText>
40+
<strong>Username</strong> 25 minutes ago
41+
</OverflowText>
42+
),
43+
};

src/components/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@import "./Breadcrumb/breadcrumb";
44
@import "./Button/button";
55
@import "./Card/card";
6+
@import "./Chat/chat";
67
@import "./Checkbox/checkbox";
78
@import "./Depiction/depiction";
89
@import "./Dialog/dialog";

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from "./Badge/Badge";
88
export * from "./Breadcrumb";
99
export * from "./Button/Button";
1010
export * from "./Card";
11+
export * from "./Chat";
1112
export * from "./Checkbox/Checkbox";
1213
export * from "./ContextOverlay";
1314
export * from "./Depiction/Depiction";

0 commit comments

Comments
 (0)