diff --git a/CHANGELOG.md b/CHANGELOG.md index 543792ca3..1a3bc4d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added +- `` + - displays single chat contents in a bubble, including options to add status line and avatar +- `` + - can collapse (and expand) `` automatically for convenience +- `` + - let the user input texts, calls `onSubmit` handler on enter key and submit button +- `` + - combine a list of chat contents and user input box +- `` + - reduces HTML to simple text and can display it as one ellipsed line - `` - prove useage of `usePlaceholder` by jest test coverage @@ -20,7 +30,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `` - re-check hover state after swapping the placeholder before triggering the event bubbling -## Changed +### Changed - `` - increase the default delay before swapping the tooltip placeholder of the icon, reducing unwanted swaps because of mouseovers that were not intended diff --git a/package.json b/package.json index afaac9666..a93daa1b5 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -174,8 +176,7 @@ }, "peerDependencies": { "@blueprintjs/core": ">=5", - "react": ">=16", - "react-dom": ">=16" + "react": ">=16" }, "resolutions": { "**/@types/react": "^17.0.85", diff --git a/src/components/Chat/ChatArea.tsx b/src/components/Chat/ChatArea.tsx new file mode 100644 index 000000000..d854d022a --- /dev/null +++ b/src/components/Chat/ChatArea.tsx @@ -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, + TestableComponent { + /** + * The inut field for the chat. + */ + chatField?: React.ReactElement; + /** + * 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(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 ( + + {chatField && ( + +
{chatField}
+
+ )} + +
+ {autoSpacingSize && children ? ( +
    + {React.Children.toArray(children).map((child, index) => ( +
  • + {child} + +
  • + ))} +
+ ) : ( + children + )} +
+
+
+ ); +}; + +export default ChatArea; diff --git a/src/components/Chat/ChatContent.tsx b/src/components/Chat/ChatContent.tsx new file mode 100644 index 000000000..ad60c14db --- /dev/null +++ b/src/components/Chat/ChatContent.tsx @@ -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, 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 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 `` component. + * `children` need to a `string` then, otherwise it cannot be parsed. + */ + markdownProps?: Omit; + /** + * Could be used to add some type of toggle button or to include a context menu. + */ + actionButton?: React.ReactElement | React.ReactElement; +} + +/** + * 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" ? {children} : children; + + const chatitem = ( +
+ {statusLine && ( + + {statusLine} + + + )} + {content} +
+ ); + + const indentationSizes = { + small: "8%", + medium: "21%", + large: "34%", + }; + + return ( +
+ + {avatar && ( + + {React.cloneElement(avatar, { size: "small", ratio: "1:1", rounded: true, resizing: "cover" })} + + )} + {chatitem} + {actionButton && ( + + {actionButton} + + )} + +
+ ); +}; + +export default ChatContent; diff --git a/src/components/Chat/ChatContentCollapsed.tsx b/src/components/Chat/ChatContentCollapsed.tsx new file mode 100644 index 000000000..4b10f937e --- /dev/null +++ b/src/components/Chat/ChatContentCollapsed.tsx @@ -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; + /** + * 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; + /** + * 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(collapsed); + + const childrenAsTextline = ( + + {typeof children.props.children === "string" ? ( + {children.props.children} + ) : ( + children.props.children + )} + + ); + + return React.cloneElement(children, { + children: displayCollapsed ? childrenAsTextline : children.props.children, + actionButton: ( + setDispayCollapsed(!displayCollapsed)} + /> + ), + }); +}; + +export default ChatContentCollapsed; diff --git a/src/components/Chat/ChatField.tsx b/src/components/Chat/ChatField.tsx new file mode 100644 index 000000000..e024efd1c --- /dev/null +++ b/src/components/Chat/ChatField.tsx @@ -0,0 +1,75 @@ +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 TextAreaProps, 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. + * If you use it together with your own handlers for `onChange` and `onKeyDown` it won't work properly. + */ + onTextSubmit?: (value: string) => void; +} + +/** + * Component to input chat text. + * Based on `TextArea` component. + */ +export const ChatField = ({ + className, + onTextSubmit, + onChange, + onKeyDown, + rightElement, + ...otherTextAreaProps +}: ChatFieldProps) => { + const chatvalue = React.useRef(otherTextAreaProps.children ?? ""); + + const onContentChange = (value: string) => { + chatvalue.current = value; + }; + + const onEnter = (e: React.KeyboardEvent) => { + if (onKeyDown) onKeyDown(e); + if (e.keyCode === 13 && e.shiftKey === false && onTextSubmit) { + e.preventDefault(); + onTextSubmit(chatvalue.current); + } + }; + + return ( +