Skip to content

Commit

Permalink
Merge pull request #2473 from anuradha9712/feat-chat-bubble-component
Browse files Browse the repository at this point in the history
feat(chatBubble): add new chat bubble component
  • Loading branch information
veekays authored Jan 29, 2025
2 parents 30070b3 + 25bf60e commit 53a07c5
Show file tree
Hide file tree
Showing 26 changed files with 1,792 additions and 25 deletions.
2 changes: 2 additions & 0 deletions core/components/molecules/chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import DateSeparator from './dateSeparator';
import UnreadMessage from './unreadMessage';
import NewMessage from './newMessage';
import TypingIndicator from './typingIndicator';
import ChatBubble from './chatBubble';
import { BaseProps } from '@/utils/types';

export interface ChatProps extends BaseProps {
Expand All @@ -25,5 +26,6 @@ Chat.DateSeparator = DateSeparator;
Chat.UnreadMessage = UnreadMessage;
Chat.NewMessage = NewMessage;
Chat.TypingIndicator = TypingIndicator;
Chat.ChatBubble = ChatBubble;

export default Chat;
83 changes: 83 additions & 0 deletions core/components/molecules/chat/chatBubble/ChatBubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as React from 'react';
import { IncomingBubble, IncomingOptionProps } from './IncomingBubble';
import { OutgoingBubble, OutgoingOptionProps } from './OutgoingBubble';
import { BaseProps } from '@/utils/types';

export type ChatBubbleType = 'incoming' | 'outgoing';

export interface ChatBubbleProps extends BaseProps {
/**
* Type of ChatBubble
*/
type: ChatBubbleType;
/**
* Props for Incoming Chat Bubble Type
*
* **`incomingOptions` are only applicable when `type` is `incoming`
*
* <pre style="font-family: monospace; font-size: 13px; background: #f8f8f8">
* IncomingOptionProps: {
* children?: React.ReactNode;
* time?: string | React.ReactText;
* metaData?: string;
* actionBar?: () => JSX.Element;
* urgentMessage?: () => JSX.Element;
* avatarData?: ChatAvatarProps;
* showAvatar?: boolean;
* }
*
* ChatAvatarProps: {
* appearance?: AvatarAppearance;
* firstName?: string;
* lastName?: string;
* role?: string;
* tabIndex?: number;
* icon?: React.ReactNode;
* image?: React.ReactNode;
* }
* </pre>
*/
incomingOptions?: IncomingOptionProps;
/**
* Props for Outgoing Chat Bubble Type
* **`outgoingOptions` are only applicable when `type` is `outgoing`
* <pre style="font-family: monospace; font-size: 13px; background: #f8f8f8">
* OutgoingOptionProps: {
* metaData?: string;
* status?: boolean;
* failed?: boolean;
* children?: React.ReactNode;
* time?: string | React.ReactText;
* actionBar?: () => JSX.Element;
* urgentMessage?: () => JSX.Element;
* failedMessage?: () => JSX.Element;
* }
* </pre>
*/
outgoingOptions?: OutgoingOptionProps;
/**
* Elements to be rendered inside ChatBubble
*/
children?: React.ReactNode;
}

export const ChatBubble = (props: ChatBubbleProps) => {
const { type, incomingOptions, outgoingOptions, children, ...rest } = props;

if (type === 'incoming') {
return (
<IncomingBubble {...incomingOptions} {...rest}>
{children}
</IncomingBubble>
);
}

return (
<OutgoingBubble {...outgoingOptions} {...rest}>
{children}
</OutgoingBubble>
);
};

ChatBubble.displayName = 'ChatBubble';
export default ChatBubble;
144 changes: 144 additions & 0 deletions core/components/molecules/chat/chatBubble/IncomingBubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import * as React from 'react';
import { AccentAppearance } from '@/common.type';
import styles from '@css/components/chatBubble.module.css';
import { Text, Avatar, Row, Column } from '@/index';
import classNames from 'classnames';
import { BaseProps } from '@/utils/types';

export interface ChatAvatarProps {
appearance?: AccentAppearance;
firstName?: string;
lastName?: string;
role?: string;
tabIndex?: number;
icon?: React.ReactNode;
image?: React.ReactNode;
}

export interface IncomingOptionProps extends BaseProps {
children?: React.ReactNode;
time?: string | React.ReactText;
metaData?: string;
actionBar?: () => JSX.Element;
urgentMessage?: () => JSX.Element;
avatarData?: ChatAvatarProps;
showAvatar?: boolean;
}

const MetaSeparator = () => (
<span className={styles['ChatBubble-separator']} data-test="DesignSystem-IncomingChatBubble-Separator" />
);

export const IncomingBubble = (props: IncomingOptionProps) => {
const { time, metaData, urgentMessage, avatarData = {}, showAvatar, children, actionBar, ...rest } = props;
const { image, icon, firstName, lastName } = avatarData;
const fullName = `${firstName ? firstName : ''} ${lastName ? lastName : ''}`;

const [showActionBar, setShowActionBar] = React.useState(false);

const showMetaRow = firstName || lastName || time || metaData || urgentMessage;

const metaDataClass = classNames({
['d-flex align-items-center mb-3']: true,
[styles['ChatBubble-metaData--incoming']]: showAvatar,
});

return (
<div
data-test="DesignSystem-ChatBubble-IncomingWrapper"
{...rest}
role="group"
aria-labelledby="chat-bubble-header"
>
{showMetaRow && (
<Row className={metaDataClass} data-test="DesignSystem-IncomingChatBubble-MetaDataWrapper">
{[
fullName && (firstName || lastName) && (
<Text key="fullName" weight="medium" size="small" data-test="DesignSystem-IncomingChatBubble-Name">
{fullName}
</Text>
),
time && (
<Text
key="time"
appearance="subtle"
weight="medium"
size="small"
data-test="DesignSystem-IncomingChatBubble-Time"
aria-label={`Time: ${time}`}
>
{time}
</Text>
),
metaData && (
<Text
key="metaData"
appearance="subtle"
weight="medium"
size="small"
data-test="DesignSystem-IncomingChatBubble-MetaData"
aria-label={metaData}
>
{metaData}
</Text>
),
urgentMessage && (
<div
key="urgentMessage"
data-test="DesignSystem-IncomingChatBubble-UrgentMessage"
role="alert"
aria-live="assertive"
>
{urgentMessage()}
</div>
),
]
.filter(Boolean)
.map((element, index, array) => (
<React.Fragment key={index}>
{element}
{index < array.length - 1 && <MetaSeparator key={`separator-${index}`} />}
</React.Fragment>
))}
</Row>
)}
<Row
onMouseEnter={() => setShowActionBar(true)}
onMouseLeave={() => setShowActionBar(false)}
data-test="DesignSystem-IncomingChatBubble-Wrapper"
>
<Column className={styles['ChatBubble-boxWrapper']} data-test="DesignSystem-IncomingChatBubble-ChatBoxWrapper">
{showAvatar && (
<Avatar
data-test="DesignSystem-IncomingChatBubble-Avatar"
className="mr-4"
{...avatarData}
withTooltip={false}
aria-label={`Avatar of ${fullName}`}
>
{image || icon}
</Avatar>
)}
<div className={styles['ChatBubble-box--incoming']} data-test="DesignSystem-IncomingChatBubble-ChatBox">
{children}
</div>
</Column>
<Column className={styles['ChatBubble-actionBarWrapper']}>
{actionBar && showActionBar && (
<div
data-test="DesignSystem-IncomingChatBubble-ActionBar"
className={styles['ChatBubble-actionBar--incoming']}
role="toolbar"
aria-label="Action bar"
>
{actionBar()}
</div>
)}
</Column>
</Row>
</div>
);
};

IncomingBubble.displayName = 'IncomingBubble';
export default IncomingBubble;
Loading

0 comments on commit 53a07c5

Please sign in to comment.