Skip to content

Commit 2b67fa7

Browse files
authored
Merge pull request #419 from anujsingla/418-extraContent
Enhance Message Component to Support ReactNode in Content via extraContent Prop
2 parents bf2adeb + 8cf158d commit 2b67fa7

File tree

4 files changed

+234
-35
lines changed

4 files changed

+234
-35
lines changed

packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ propComponents:
1919
'FileDropZone',
2020
'PreviewAttachment',
2121
'Message',
22+
'MessageExtraContent',
2223
'PreviewAttachment',
2324
'ActionProps',
2425
'SourcesCardProps',
@@ -165,6 +166,16 @@ Messages from users have a different background color to differentiate them from
165166

166167
```
167168

169+
### Custom message content
170+
171+
**Caution:** Take care when using this feature. It can cause you to stray from accessibility and design best practice standards. If you frequently need add the same component via custom message content, reach out to the PatternFly team. If there's a consistent need for a certain component, we can look into adding native support for additional features.
172+
173+
You can add custom content to specific parts of a `<Message>` via the `extraContent` prop, including additional components (like timestamps, badges, or custom elements). This prop allows you to create dynamic and reusable elements for various use cases, without changing the default message layout.
174+
175+
```js file="./UserMessageWithExtraContent.tsx"
176+
177+
```
178+
168179
## File attachments
169180

170181
### Messages with attachments
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
3+
import Message from '@patternfly/chatbot/dist/dynamic/Message';
4+
import userAvatar from './user_avatar.svg';
5+
import { Alert, Badge, Button, Card, CardBody, CardFooter, CardTitle } from '@patternfly/react-core';
6+
7+
const UserActionEndContent = () => {
8+
// eslint-disable-next-line no-console
9+
const onClick = () => console.log('custom button click');
10+
return (
11+
<React.Fragment>
12+
<Button variant="secondary" ouiaId="Secondary" onClick={onClick}>
13+
End content button
14+
</Button>
15+
<Alert variant="danger" title="Danger alert title" ouiaId="DangerAlert" />
16+
</React.Fragment>
17+
);
18+
};
19+
20+
const CardInformationAfterMainContent = () => (
21+
<Card ouiaId="BasicCard">
22+
<CardTitle>This is content card after main content</CardTitle>
23+
<CardBody>Body</CardBody>
24+
<CardFooter>Footer</CardFooter>
25+
</Card>
26+
);
27+
28+
const BeforeMainContent = () => (
29+
<div>
30+
<Badge key={1} isRead>
31+
7
32+
</Badge>
33+
<Badge key={2} isRead>
34+
24
35+
</Badge>
36+
</div>
37+
);
38+
39+
export const UserMessageWithExtraContent: React.FunctionComponent = () => (
40+
<>
41+
<Message
42+
avatar={userAvatar}
43+
name="User"
44+
role="user"
45+
content="This is a main message."
46+
timestamp="1 hour ago"
47+
extraContent={{
48+
beforeMainContent: <BeforeMainContent />,
49+
afterMainContent: <CardInformationAfterMainContent />,
50+
endContent: <UserActionEndContent />
51+
}}
52+
/>
53+
</>
54+
);

packages/module/src/Message/Message.test.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,4 +627,118 @@ describe('Message', () => {
627627
render(<Message avatar="./img" role="user" name="User" content={TABLE} tableProps={{ 'aria-label': 'Test' }} />);
628628
expect(screen.getByRole('grid', { name: /Test/i })).toBeTruthy();
629629
});
630+
it('should render beforeMainContent with main content', () => {
631+
const mainContent = 'Main message content';
632+
const beforeMainContentText = 'Before main content';
633+
const beforeMainContent = <div>{beforeMainContentText}</div>;
634+
635+
render(
636+
<Message avatar="./img" role="user" name="User" content={mainContent} extraContent={{ beforeMainContent }} />
637+
);
638+
639+
expect(screen.getByText(beforeMainContentText)).toBeTruthy();
640+
expect(screen.getByText(mainContent)).toBeTruthy();
641+
});
642+
it('should render afterMainContent with main content', () => {
643+
const mainContent = 'Main message content';
644+
const afterMainContentText = 'After main content';
645+
const afterMainContent = <div>{afterMainContentText}</div>;
646+
647+
render(
648+
<Message avatar="./img" role="user" name="User" content={mainContent} extraContent={{ afterMainContent }} />
649+
);
650+
651+
expect(screen.getByText(afterMainContentText)).toBeTruthy();
652+
expect(screen.getByText(mainContent)).toBeTruthy();
653+
});
654+
655+
it('should render endContent with main content', () => {
656+
const mainContent = 'Main message content';
657+
const endMainContentText = 'End content';
658+
const endContent = <div>{endMainContentText}</div>;
659+
660+
render(<Message avatar="./img" role="user" name="User" content={mainContent} extraContent={{ endContent }} />);
661+
662+
expect(screen.getByText(endMainContentText)).toBeTruthy();
663+
expect(screen.getByText(mainContent)).toBeTruthy();
664+
});
665+
it('should render all parts of extraContent with main content', () => {
666+
const beforeMainContent = <div>Before main content</div>;
667+
const afterMainContent = <div>After main content</div>;
668+
const endContent = <div>End content</div>;
669+
670+
render(
671+
<Message
672+
avatar="./img"
673+
role="user"
674+
name="User"
675+
content="Main message content"
676+
extraContent={{ beforeMainContent, afterMainContent, endContent }}
677+
/>
678+
);
679+
680+
expect(screen.getByText('Before main content')).toBeTruthy();
681+
expect(screen.getByText('Main message content')).toBeTruthy();
682+
expect(screen.getByText('After main content')).toBeTruthy();
683+
expect(screen.getByText('End content')).toBeTruthy();
684+
});
685+
686+
it('should not render extraContent when not provided', () => {
687+
render(<Message avatar="./img" role="user" name="User" content="Main message content" />);
688+
689+
// Ensure no extraContent is rendered
690+
expect(screen.getByText('Main message content')).toBeTruthy();
691+
expect(screen.queryByText('Before main content')).toBeFalsy();
692+
expect(screen.queryByText('After main content')).toBeFalsy();
693+
expect(screen.queryByText('end message content')).toBeFalsy();
694+
});
695+
696+
it('should handle undefined or null values in extraContent gracefully', () => {
697+
render(
698+
<Message
699+
avatar="./img"
700+
role="user"
701+
name="User"
702+
content="Main message content"
703+
extraContent={{ beforeMainContent: null, afterMainContent: undefined, endContent: null }}
704+
/>
705+
);
706+
707+
// Ensure that no extraContent is rendered if they are null or undefined
708+
expect(screen.getByText('Main message content')).toBeTruthy();
709+
expect(screen.queryByText('Before main content')).toBeFalsy();
710+
expect(screen.queryByText('After main content')).toBeFalsy();
711+
expect(screen.queryByText('end message content')).toBeFalsy();
712+
});
713+
it('should render JSX in extraContent correctly', () => {
714+
const beforeMainContent = (
715+
<div data-testid="before-main-content">
716+
<strong>Bold before content</strong>
717+
</div>
718+
);
719+
const afterMainContent = (
720+
<div data-testid="after-main-content">
721+
<strong>Bold after content</strong>
722+
</div>
723+
);
724+
const endContent = (
725+
<div data-testid="end-main-content">
726+
<strong>Bold end content</strong>
727+
</div>
728+
);
729+
render(
730+
<Message
731+
avatar="./img"
732+
role="user"
733+
name="User"
734+
content="Main message content"
735+
extraContent={{ beforeMainContent, afterMainContent, endContent }}
736+
/>
737+
);
738+
739+
// Check that the JSX is correctly rendered
740+
expect(screen.getByTestId('before-main-content')).toContainHTML('<strong>Bold before content</strong>');
741+
expect(screen.getByTestId('after-main-content')).toContainHTML('<strong>Bold after content</strong>');
742+
expect(screen.getByTestId('end-main-content')).toContainHTML('<strong>Bold end content</strong>');
743+
});
630744
});

packages/module/src/Message/Message.tsx

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Chatbot Main - Message
33
// ============================================================================
44

5-
import React from 'react';
5+
import React, { ReactNode } from 'react';
66

77
import Markdown from 'react-markdown';
88
import remarkGfm from 'remark-gfm';
@@ -56,13 +56,26 @@ export interface MessageAttachment {
5656
spinnerTestId?: string;
5757
}
5858

59+
export interface MessageExtraContent {
60+
/** Content to display before the main content */
61+
beforeMainContent?: ReactNode;
62+
63+
/** Content to display after the main content */
64+
afterMainContent?: ReactNode;
65+
66+
/** Content to display at the end */
67+
endContent?: ReactNode;
68+
}
69+
5970
export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'role'> {
6071
/** Unique id for message */
6172
id?: string;
6273
/** Role of the user sending the message */
6374
role: 'user' | 'bot';
6475
/** Message content */
6576
content?: string;
77+
/** Extra Message content */
78+
extraContent?: MessageExtraContent;
6679
/** Name of the user */
6780
name?: string;
6881
/** Avatar src for the user */
@@ -123,6 +136,7 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
123136
export const MessageBase: React.FunctionComponent<MessageProps> = ({
124137
role,
125138
content,
139+
extraContent,
126140
name,
127141
avatar,
128142
timestamp,
@@ -145,6 +159,7 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
145159
tableProps,
146160
...props
147161
}: MessageProps) => {
162+
const { beforeMainContent, afterMainContent, endContent } = extraContent || {};
148163
let avatarClassName;
149164
if (avatarProps && 'className' in avatarProps) {
150165
const { className, ...rest } = avatarProps;
@@ -189,40 +204,44 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
189204
{isLoading ? (
190205
<MessageLoading loadingWord={loadingWord} />
191206
) : (
192-
<Markdown
193-
components={{
194-
p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
195-
code: ({ children, ...props }) => (
196-
<CodeBlockMessage {...props} {...codeBlockProps}>
197-
{children}
198-
</CodeBlockMessage>
199-
),
200-
h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
201-
h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
202-
h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
203-
h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
204-
h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
205-
h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
206-
blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
207-
ul: (props) => <UnorderedListMessage {...props} />,
208-
ol: (props) => <OrderedListMessage {...props} />,
209-
li: (props) => <ListItemMessage {...props} />,
210-
table: (props) => <TableMessage {...props} {...tableProps} />,
211-
tbody: (props) => <TbodyMessage {...props} />,
212-
thead: (props) => <TheadMessage {...props} />,
213-
tr: (props) => <TrMessage {...props} />,
214-
td: (props) => {
215-
// Conflicts with Td type
216-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
217-
const { width, ...rest } = props;
218-
return <TdMessage {...rest} />;
219-
},
220-
th: (props) => <ThMessage {...props} />
221-
}}
222-
remarkPlugins={[remarkGfm]}
223-
>
224-
{content}
225-
</Markdown>
207+
<>
208+
{beforeMainContent && <>{beforeMainContent}</>}
209+
<Markdown
210+
components={{
211+
p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
212+
code: ({ children, ...props }) => (
213+
<CodeBlockMessage {...props} {...codeBlockProps}>
214+
{children}
215+
</CodeBlockMessage>
216+
),
217+
h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
218+
h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
219+
h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
220+
h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
221+
h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
222+
h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
223+
blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
224+
ul: (props) => <UnorderedListMessage {...props} />,
225+
ol: (props) => <OrderedListMessage {...props} />,
226+
li: (props) => <ListItemMessage {...props} />,
227+
table: (props) => <TableMessage {...props} {...tableProps} />,
228+
tbody: (props) => <TbodyMessage {...props} />,
229+
thead: (props) => <TheadMessage {...props} />,
230+
tr: (props) => <TrMessage {...props} />,
231+
td: (props) => {
232+
// Conflicts with Td type
233+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
234+
const { width, ...rest } = props;
235+
return <TdMessage {...rest} />;
236+
},
237+
th: (props) => <ThMessage {...props} />
238+
}}
239+
remarkPlugins={[remarkGfm]}
240+
>
241+
{content}
242+
</Markdown>
243+
{afterMainContent && <>{afterMainContent}</>}
244+
</>
226245
)}
227246
{!isLoading && sources && <SourcesCard {...sources} />}
228247
{quickStarts && quickStarts.quickStart && (
@@ -264,6 +283,7 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
264283
))}
265284
</div>
266285
)}
286+
{!isLoading && endContent && <>{endContent}</>}
267287
</div>
268288
</div>
269289
</section>

0 commit comments

Comments
 (0)