Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
65723cb
fix story re storybook changes
haschek Aug 26, 2025
dad204f
add component to display single chat contents
haschek Aug 26, 2025
a338cac
prevent hight smaller than right element button
haschek Aug 27, 2025
a907b71
add ChatField component to enter texts
haschek Aug 27, 2025
da459a1
add property to limit bubble height easily
haschek Aug 27, 2025
97d9685
add shadow border to bubble
haschek Aug 27, 2025
94c18a8
improve indentation settings and display
haschek Aug 27, 2025
d444b2b
fix display without avatar
haschek Aug 27, 2025
a1ec630
use new send button for chat input
haschek Aug 27, 2025
9b93f0a
add component for chat area
haschek Aug 27, 2025
d4ae875
update changelog
haschek Aug 27, 2025
e3904af
move scrollbar to parent container border
haschek Aug 27, 2025
109a177
prevent cutted bubble borders
haschek Aug 28, 2025
d9cffd3
option to scroll to the most recent chat content
haschek Aug 28, 2025
8ea9346
add expand/shrink option to chat content
haschek Aug 28, 2025
4a2d8e9
add simple display to chat area itself
haschek Aug 28, 2025
d434ca4
open up ChatField for a more complex usages but still supporting a si…
haschek Sep 1, 2025
1077361
move autoshrink element creation from chat into own reusable component
haschek Sep 1, 2025
ca6937f
remove line breaks to prevent multiple markdown paragraphs
haschek Sep 1, 2025
c3ee50b
remove any markup from ContentShrinker children
haschek Sep 1, 2025
7cc81e6
fix property value type
haschek Sep 1, 2025
7a9f947
allow action button in handler if there is no collapse handler
haschek Sep 1, 2025
fcd529a
Merge branch 'develop' into feature/chatComponents-CMEM-6775
haschek Sep 3, 2025
95ab596
make usage of Overflowtext optional
haschek Sep 4, 2025
2bd1e09
provide properties to control the reduced content a bit more
haschek Sep 4, 2025
1e06762
rename element
haschek Sep 4, 2025
b98a6cb
fix array rendering
haschek Sep 4, 2025
4450973
move shrinking functionality to own component
haschek Sep 4, 2025
43a4bbd
extend chat area content examples
haschek Sep 4, 2025
4b92f4b
forward events when text submit handler is given
haschek Sep 4, 2025
128a002
Merge remote-tracking branch 'origin/feature/chatComponents-CMEM-6775…
haschek Sep 4, 2025
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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p

### Added

- `<ChatContent />`
- displays single chat contents in a bubble, including options to add status line and avatar
- `<ChatContentCollapsed />`
- can collapse (and expand) `<ChatContent />` automatically for convenience
- `<ChatField />`
- let the user input texts, calls `onSubmit` handler on enter key and submit button
- `<ChatArea />`
- combine a list of chat contents and user input box
- `<TextReducer />`
- reduces HTML to simple text and can display it as one ellipsed line
- `<Tooltip />`
- prove useage of `usePlaceholder` by jest test coverage

Expand All @@ -20,7 +30,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- `<Tooltip />`
- re-check hover state after swapping the placeholder before triggering the event bubbling

## Changed
### Changed

- `<IconButton/>`
- increase the default delay before swapping the tooltip placeholder of the icon, reducing unwanted swaps because of mouseovers that were not intended
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@
"n3": "^1.25.1",
"re-resizable": "^6.10.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-dom": "^16.14.0",
"react-flow-renderer": "9.7.4",
"react-flow-renderer-lts": "npm:react-flow-renderer@^10.3.17",
"react-inlinesvg": "^3.0.3",
"react-is": "^16.13.1",
"react-markdown": "^10.1.0",
"react-markdown-deprecated": "npm:react-markdown@^8.0.7",
"react-syntax-highlighter": "^15.6.1",
Expand Down Expand Up @@ -138,6 +139,7 @@
"@types/jshint": "^2.12.4",
"@types/lodash": "^4.17.16",
"@types/n3": "^1.24.2",
"@types/react-is": "^19.0.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^8.30.1",
Expand Down Expand Up @@ -174,8 +176,7 @@
},
"peerDependencies": {
"@blueprintjs/core": ">=5",
"react": ">=16",
"react-dom": ">=16"
"react": ">=16"
},
"resolutions": {
"**/@types/react": "^17.0.85",
Expand Down
114 changes: 114 additions & 0 deletions src/components/Chat/ChatArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from "react";

import { TestableComponent } from "../../components/interfaces";
import { CLASSPREFIX as eccgui } from "../../configuration/constants";

import { FlexibleLayoutContainer, FlexibleLayoutContainerProps, FlexibleLayoutItem } from "./../FlexibleLayout";
import { Spacing, SpacingProps } from "./../Separation/Spacing";
import { ChatFieldProps } from "./ChatField";

export interface ChatAreaProps
extends Omit<FlexibleLayoutContainerProps, "vertical" | "noEqualItemSpace">,
TestableComponent {
/**
* The inut field for the chat.
*/
chatField?: React.ReactElement<ChatFieldProps>;
/**
* Set the position of the chat field.
*/
chatFieldPosition?: "top" | "bottom";
/**
* Sets the maximum width for chat contents and input.
*/
contentWidth?: "small" | "medium" | "large" | "full";
/**
* Put chat content in a list and add spacings automatically.
* Works best if each `ChatArea` child represents one chat content item.
*/
autoSpacingSize?: SpacingProps["size"];
/**
* Scrolls content to the first or last child automatically.
* The correct value depends on the place where you insert the most recent chat item.
*/
autoScrollTo?: "first" | "last";
}

/**
* Component to display a full chat, containing chat content bubbles and text input.
*/
export const ChatArea = ({
children,
className,
chatField,
chatFieldPosition = "bottom",
contentWidth = "medium",
autoSpacingSize,
gapSize = "medium",
autoScrollTo,
...otherFlexibleLayoutContainerProps
}: ChatAreaProps) => {
const chatcontents = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
if (chatcontents.current && children && autoScrollTo) {
const chatitems = chatcontents.current.getElementsByClassName(`${eccgui}-chat__content`);
if (chatitems.length > 0) {
chatitems[autoScrollTo === "first" ? 0 : chatitems.length - 1].scrollIntoView({
behavior: "instant",
block: autoScrollTo === "first" ? "start" : "end",
});
}
}
}, [chatcontents, children, autoScrollTo]);

return (
<FlexibleLayoutContainer
className={
`${eccgui}-chat__area` + ` ${eccgui}-chat__area--${contentWidth}` + (className ? ` ${className}` : "")
}
vertical
noEqualItemSpace
gapSize={gapSize}
{...otherFlexibleLayoutContainerProps}
>
{chatField && (
<FlexibleLayoutItem
growFactor={0}
shrinkFactor={0}
style={chatFieldPosition === "bottom" ? { order: 1 } : undefined}
>
<div className={`${eccgui}-chat__area-contentwidth`}>{chatField}</div>
</FlexibleLayoutItem>
)}
<FlexibleLayoutItem
style={
otherFlexibleLayoutContainerProps.useAbsoluteSpace
? {
overflow: "auto",
minHeight: 0,
padding: "2px 0",
}
: undefined
}
>
<div className={`${eccgui}-chat__area-contentwidth`} ref={chatcontents}>
{autoSpacingSize && children ? (
<ul>
{React.Children.toArray(children).map((child, index) => (
<li key={index}>
{child}
<Spacing size={autoSpacingSize} />
</li>
))}
</ul>
) : (
children
)}
</div>
</FlexibleLayoutItem>
</FlexibleLayoutContainer>
);
};

export default ChatArea;
132 changes: 132 additions & 0 deletions src/components/Chat/ChatContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React from "react";

import { TestableComponent } from "../../components/interfaces";
import { CLASSPREFIX as eccgui } from "../../configuration/constants";

import { Markdown, MarkdownProps } from "./../../cmem/markdown/Markdown";
import { ContextMenuProps } from "./../ContextOverlay/ContextMenu";
import { DepictionProps } from "./../Depiction/Depiction";
import { FlexibleLayoutContainer, FlexibleLayoutItem } from "./../FlexibleLayout";
import { IconButtonProps } from "./../Icon/IconButton";
import { Spacing } from "./../Separation/Spacing";
import { HtmlContentBlock, OverflowTextProps } from "./../Typography";

export interface ChatContentProps extends React.HTMLAttributes<HTMLDivElement>, TestableComponent {
/**
* Should be a line of text, e.g. username, timestamp, ...
*/
statusLine?: React.ReactElement<OverflowTextProps>;
/**
* How the chat content box is displayed.
*/
displayType?: "free" | "simple" | "bubble";
/**
* A depiction used as avatar next to the content box.
*/
avatar?: React.ReactElement<DepictionProps>;
/**
* If indented then the content box has some white space on the opposite side to the alignment
*/
indentationSize?: "small" | "medium" | "large";
/**
* How the content box and avatar is aligned.
* If `left` is set then the avatar is on the left side, and the indentation on the right side.
*/
alignment?: "left" | "right";
/**
* If set then the chat bubble only grows to a height of 50% of the viewport.
* In case you need to set other maximum heights then use the `style` property directly.
*/
limitHeight?: boolean;
/**
* If given then the content is automatically parsed and displayed by our `<Markdown />` component.
* `children` need to a `string` then, otherwise it cannot be parsed.
*/
markdownProps?: Omit<MarkdownProps, "children">;
/**
* Could be used to add some type of toggle button or to include a context menu.
*/
actionButton?: React.ReactElement<IconButtonProps> | React.ReactElement<ContextMenuProps>;
}

/**
* Component to display single chat contents, including avatar and status line.
*/
export const ChatContent = ({
className,
children,
statusLine,
avatar,
displayType = "bubble",
indentationSize,
alignment = "left",
limitHeight,
markdownProps,
actionButton,
...otherDivProps
}: ChatContentProps) => {
const content =
markdownProps && typeof children === "string" ? <Markdown {...markdownProps}>{children}</Markdown> : children;

const chatitem = (
<div
className={
`${eccgui}-chat__content` +
` ${eccgui}-chat__content--display-${displayType}` +
` ${eccgui}-chat__content--align-${alignment}` +
(limitHeight ? ` ${eccgui}-chat__content--limitheight` : "") +
(className ? ` ${className}` : "")
}
{...otherDivProps}
>
{statusLine && (
<HtmlContentBlock small>
{statusLine}
<Spacing size="tiny" />
</HtmlContentBlock>
)}
{content}
</div>
);

const indentationSizes = {
small: "8%",
medium: "21%",
large: "34%",
};

return (
<div
style={{
marginLeft: alignment === "right" && indentationSize ? indentationSizes[indentationSize] : undefined,
marginRight: alignment === "left" && indentationSize ? indentationSizes[indentationSize] : undefined,
}}
>
<FlexibleLayoutContainer noEqualItemSpace gapSize="tiny">
{avatar && (
<FlexibleLayoutItem
className={`${eccgui}-chat__content-avatar`}
growFactor={0}
shrinkFactor={0}
style={alignment === "right" ? { order: 1 } : undefined}
>
{React.cloneElement(avatar, { size: "small", ratio: "1:1", rounded: true, resizing: "cover" })}
</FlexibleLayoutItem>
)}
<FlexibleLayoutItem className={`${eccgui}-chat__content-wrapper`}>{chatitem}</FlexibleLayoutItem>
{actionButton && (
<FlexibleLayoutItem
className={`${eccgui}-chat__content-sizetoggle`}
growFactor={0}
shrinkFactor={0}
style={alignment === "right" ? { order: -1 } : undefined}
>
{actionButton}
</FlexibleLayoutItem>
)}
</FlexibleLayoutContainer>
</div>
);
};

export default ChatContent;
64 changes: 64 additions & 0 deletions src/components/Chat/ChatContentCollapsed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react";

import { Markdown } from "../../cmem/markdown/Markdown";
import { IconButton } from "../Icon/IconButton";
import { TextReducer, TextReducerProps } from "../TextReducer/TextReducer";

import { ChatContentProps } from "./ChatContent";

export interface ChatContentCollapsedProps {
children: React.ReactElement<ChatContentProps>;
/**
* Set this to `false` if the compoment should initally start in an expanded state.
*/
collapsed?: boolean;
/**
* Use this to set extra `TextReducer` properties.
* This is used to create the collapsed variant of the given content.
*/
textReducerProps?: Omit<TextReducerProps, "children">;
/**
* Text for collapse button.
*/
textCollapse?: string;
/**
* Text for expand button.
*/
textExpand?: string;
}

/**
* Adds an auto collapsing feature for convenience to `ChatContent`.
*/
export const ChatContentCollapsed = ({
children,
collapsed = true,
textReducerProps = {},
textCollapse = "Collapse",
textExpand = "Expand",
}: ChatContentCollapsedProps) => {
const [displayCollapsed, setDispayCollapsed] = React.useState<boolean>(collapsed);

const childrenAsTextline = (
<TextReducer useOverflowTextWrapper {...textReducerProps}>
{typeof children.props.children === "string" ? (
<Markdown>{children.props.children}</Markdown>
) : (
children.props.children
)}
</TextReducer>
);

return React.cloneElement(children, {
children: displayCollapsed ? childrenAsTextline : children.props.children,
actionButton: (
<IconButton
text={displayCollapsed ? textExpand : textCollapse}
name={displayCollapsed ? "toggler-showmore" : "toggler-showless"}
onClick={() => setDispayCollapsed(!displayCollapsed)}
/>
),
});
};

export default ChatContentCollapsed;
Loading