From 65723cb6f3382d98f765f3df79c9defbaec1f922 Mon Sep 17 00:00:00 2001 From: Michael Haschke Date: Tue, 26 Aug 2025 16:47:33 +0200 Subject: [PATCH 01/29] fix story re storybook changes --- src/components/Tooltip/Tooltip.stories.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Tooltip/Tooltip.stories.tsx b/src/components/Tooltip/Tooltip.stories.tsx index 7d0787537..671d7cf5b 100644 --- a/src/components/Tooltip/Tooltip.stories.tsx +++ b/src/components/Tooltip/Tooltip.stories.tsx @@ -2,6 +2,7 @@ import React from "react"; import { loremIpsum } from "react-lorem-ipsum"; import { OverlaysProvider } from "@blueprintjs/core"; import { Meta, StoryFn } from "@storybook/react"; +import { fn } from "@storybook/test"; import { Tooltip } from "../../index"; @@ -34,6 +35,7 @@ Default.args = { children: "hover me", content: testContent, addIndicator: true, + onOpening: fn(), }; export const MarkdownSupport = Template.bind({}); From dad204f9fdc6b4a060d5c54d7f86d5b42d11fdb0 Mon Sep 17 00:00:00 2001 From: Michael Haschke Date: Tue, 26 Aug 2025 16:48:16 +0200 Subject: [PATCH 02/29] add component to display single chat contents --- CHANGELOG.md | 5 + src/components/Chat/ChatContent.tsx | 102 ++++++++++++++++++ src/components/Chat/_chat.scss | 41 +++++++ src/components/Chat/index.ts | 1 + .../Chat/stories/ChatContent.stories.tsx | 43 ++++++++ src/components/index.scss | 1 + src/components/index.ts | 1 + 7 files changed, 194 insertions(+) create mode 100644 src/components/Chat/ChatContent.tsx create mode 100644 src/components/Chat/_chat.scss create mode 100644 src/components/Chat/index.ts create mode 100644 src/components/Chat/stories/ChatContent.stories.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a219613..ff1d4e884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] +### Added + +- `` + - displays single chat contents in a bubble, including options to add status line and avatar + ## [24.4.1] - 2025-08-25 ### Fixed diff --git a/src/components/Chat/ChatContent.tsx b/src/components/Chat/ChatContent.tsx new file mode 100644 index 000000000..90f37e7f7 --- /dev/null +++ b/src/components/Chat/ChatContent.tsx @@ -0,0 +1,102 @@ +import React from "react"; + +import { TestableComponent } from "../../components/interfaces"; +import { CLASSPREFIX as eccgui } from "../../configuration/constants"; + +import { Markdown, MarkdownProps } from "./../../cmem/markdown/Markdown"; +import { DepictionProps } from "./../Depiction/Depiction"; +import { FlexibleLayoutContainer, FlexibleLayoutItem } from "./../FlexibleLayout"; +import { Spacing } from "./../Separation/Spacing"; +import { HtmlContentBlock, OverflowTextProps, WhiteSpaceContainer } from "./../Typography"; + +export interface ChatContentProps extends React.HTMLAttributes, TestableComponent { + /** + * Should be a line of text, e.g. username, timestamp, ... + */ + statusLine?: React.ReactElement; + /** + * How the chat content box is displayed. + */ + displayType?: "free" | "simple" | "bubble"; + /** + * A depiction used as avatar next to the content box. + */ + avatar?: React.ReactElement; + /** + * If indented then the content box has some white space on one side. + */ + indentation?: boolean; + /** + * 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" | "block"; + /** + * If given then the content is automatically parsed and displayed by our `` component. + * `children` need to a `string` then, otherwise it cannot be parsed. + */ + markdownProps?: Omit; +} + +/** + * Component to display singe chat contents, including avatar and status line. + */ +export const ChatContent = ({ + className, + children, + statusLine, + avatar, + displayType = "bubble", + indentation = true, + alignment = "left", + markdownProps, + ...otherDivProps +}: ChatContentProps) => { + const content = ( +
+ {statusLine && ( + + {statusLine} + + + )} + {markdownProps && typeof children === "string" ? ( + {children} + ) : ( + children + )} +
+ ); + + return ( + + {avatar ? ( + + + {React.cloneElement(avatar, { size: "small", ratio: "1:1", rounded: true, resizing: "cover" })} + + {content} + + ) : ( + content + )} + + ); +}; + +export default ChatContent; diff --git a/src/components/Chat/_chat.scss b/src/components/Chat/_chat.scss new file mode 100644 index 000000000..433e75f60 --- /dev/null +++ b/src/components/Chat/_chat.scss @@ -0,0 +1,41 @@ +.#{$eccgui}-chat__content { + --#{$eccgui}-chat__content-background: #{$light-gray5}; + + position: relative; + min-height: $button-height; + padding: $eccgui-size-inline-whitespace; + background-color: var(--#{$eccgui}-chat__content-background); + border-radius: $pt-border-radius; +} + +.#{$eccgui}-chat__content--display-free { + padding: 0; + + --#{$eccgui}-chat__content-background: transparent; +} + +.#{$eccgui}-chat__content--display-bubble { + margin-left: 0.5 * $eccgui-size-block-whitespace; + + &::before { + position: absolute; + top: calc(#{mini-units(3)} - #{0.5 * $eccgui-size-block-whitespace}); + left: calc(-0.5 * #{$eccgui-size-block-whitespace} + 1px); + width: $eccgui-size-block-whitespace; + height: $eccgui-size-block-whitespace; + content: " "; + background-color: var(--#{$eccgui}-chat__content-background); + clip-path: polygon(0% 50%, 50% 0%, 50% 100%); + } + + &.#{$eccgui}-chat__content--align-right { + margin-right: 0.5 * $eccgui-size-block-whitespace; + margin-left: none; + + &::before { + right: calc(-0.5 * #{$eccgui-size-block-whitespace} + 1px); + left: auto; + clip-path: polygon(100% 50%, 50% 0%, 50% 100%); + } + } +} diff --git a/src/components/Chat/index.ts b/src/components/Chat/index.ts new file mode 100644 index 000000000..884f4c4d5 --- /dev/null +++ b/src/components/Chat/index.ts @@ -0,0 +1 @@ +export * from "./ChatContent"; diff --git a/src/components/Chat/stories/ChatContent.stories.tsx b/src/components/Chat/stories/ChatContent.stories.tsx new file mode 100644 index 000000000..5ca7b837e --- /dev/null +++ b/src/components/Chat/stories/ChatContent.stories.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { LoremIpsum } from "react-lorem-ipsum"; +import { Meta, StoryFn } from "@storybook/react"; + +import { ChatContent, Depiction, Icon, OverflowText } from "../../../index"; + +import canonicalIcons from "./../../Icon/canonicalIconNames"; + +const allIcons = new Map([ + ...Object.keys(canonicalIcons).map((keyId) => { + return [`Icon: ${keyId}`, } />]; + }), +]); + +const exampleImages = { + None: undefined, + ...Object.fromEntries(allIcons), +}; + +export default { + title: "Components/Chat", + component: ChatContent, + argTypes: { + avatar: { + control: "select", + options: Object.keys(exampleImages), + mapping: exampleImages, + }, + }, +} as Meta; + +const TemplateFull: StoryFn = (args) => ; + +export const Default = TemplateFull.bind({}); +Default.args = { + children: , + avatar: } />, + statusLine: ( + + Username 25 minutes ago + + ), +}; diff --git a/src/components/index.scss b/src/components/index.scss index 2455a2bac..3e244f890 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -3,6 +3,7 @@ @import "./Breadcrumb/breadcrumb"; @import "./Button/button"; @import "./Card/card"; +@import "./Chat/chat"; @import "./Checkbox/checkbox"; @import "./Depiction/depiction"; @import "./Dialog/dialog"; diff --git a/src/components/index.ts b/src/components/index.ts index 9538a98de..9dcb7b042 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,6 +8,7 @@ export * from "./Badge/Badge"; export * from "./Breadcrumb"; export * from "./Button/Button"; export * from "./Card"; +export * from "./Chat"; export * from "./Checkbox/Checkbox"; export * from "./ContextOverlay"; export * from "./Depiction/Depiction"; From a338caccf4a3d1ac316ee612b949698d4e6d76c9 Mon Sep 17 00:00:00 2001 From: Michael Haschke Date: Wed, 27 Aug 2025 10:17:04 +0200 Subject: [PATCH 03/29] prevent hight smaller than right element button --- src/components/TextField/TextArea.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TextField/TextArea.tsx b/src/components/TextField/TextArea.tsx index 842e80bac..52c463ca0 100644 --- a/src/components/TextField/TextArea.tsx +++ b/src/components/TextField/TextArea.tsx @@ -91,7 +91,7 @@ export const TextArea = ({ leftIconElementRect.width ?? 0 }px)` ); - leftIconElement.addEventListener("click", (_event: MouseEvent) => { + leftIconElement.addEventListener("click", () => { textAreaElement.focus(); }); } @@ -157,7 +157,7 @@ export const TextArea = ({ rows={ rows && !otherBlueprintTextAreaProps.autoResize && !otherBlueprintTextAreaProps.growVertically ? rows - : 1 + : 2 } {...otherBlueprintTextAreaProps} dir={"auto"} From a907b714107d71013435311af8b58e515d247099 Mon Sep 17 00:00:00 2001 From: Michael Haschke Date: Wed, 27 Aug 2025 10:18:06 +0200 Subject: [PATCH 04/29] add ChatField component to enter texts --- CHANGELOG.md | 2 + src/components/Chat/ChatContent.tsx | 2 +- src/components/Chat/ChatField.tsx | 52 +++++++++++++++++++ src/components/Chat/_chat.scss | 6 +++ src/components/Chat/index.ts | 1 + .../Chat/stories/ChatContent.stories.tsx | 2 +- .../Chat/stories/ChatField.stories.tsx | 18 +++++++ 7 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/components/Chat/ChatField.tsx create mode 100644 src/components/Chat/stories/ChatField.stories.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1d4e884..2ecfbfece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `` - displays single chat contents in a bubble, including options to add status line and avatar +- `` + - let the user input texts, calls `onSubmit` handler on enter key and submit button ## [24.4.1] - 2025-08-25 diff --git a/src/components/Chat/ChatContent.tsx b/src/components/Chat/ChatContent.tsx index 90f37e7f7..79954ca50 100644 --- a/src/components/Chat/ChatContent.tsx +++ b/src/components/Chat/ChatContent.tsx @@ -65,7 +65,7 @@ export const ChatContent = ({ {statusLine && ( {statusLine} - + )} {markdownProps && typeof children === "string" ? ( diff --git a/src/components/Chat/ChatField.tsx b/src/components/Chat/ChatField.tsx new file mode 100644 index 000000000..f921982da --- /dev/null +++ b/src/components/Chat/ChatField.tsx @@ -0,0 +1,52 @@ +import React from "react"; + +import { TestableComponent } from "../../components/interfaces"; +import { CLASSPREFIX as eccgui } from "../../configuration/constants"; +import { IconButton } from "../Icon/IconButton"; +import { TextArea, TextAreaProps } from "../TextField/TextArea"; + +export interface ChatFieldProps extends Pick, TestableComponent { + /** + * Default input to start with. + */ + children?: string; + /** + * Callback handler to process the input of the field when `Enter` is pressed or the submit button is clicked. + */ + onSubmit: (value: string) => void; +} + +/** + * Component to input chat text. + * Based on `TextArea` component. + */ +export const ChatField = ({ className, onSubmit, ...otherTextAreaProps }: ChatFieldProps) => { + const chatvalue = React.useRef(otherTextAreaProps.children ?? ""); + + const onContentChange = (value: string) => { + chatvalue.current = value; + }; + + const onEnter = (e: React.KeyboardEvent) => { + if (e.keyCode === 13 && e.shiftKey === false) { + e.preventDefault(); + onSubmit(chatvalue.current); + } + }; + + return ( +