| keywords |
|
|---|
The EuiCommentList is a timeline component built on top of EuiTimeline. It allows you to display comments or logged actions that either a user or a system has performed.
:::accessibility Accessibility recommendation
Provide a descriptive aria-label or the ID of an external label to the aria-labelledby prop of the EuiCommentList. A timelineAvatarAriaLabel should be provided for every EuiComment with or without a timelineAvatar as IconType.
:::
Use EuiCommentList to display a list of EuiComments. Pass an array of EuiComment objects and EuiCommentList will generate a comment thread.
import React from 'react';
import {
EuiCommentList,
EuiCommentProps,
EuiButtonIcon,
EuiText,
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
const body = (
<EuiText size="s">
<p>
Far out in the uncharted backwaters of the unfashionable end of the
western spiral arm of the Galaxy lies a small unregarded yellow sun.
</p>
</EuiText>
);
const copyAction = (
<EuiButtonIcon
title="Custom action"
aria-label="Custom action"
color="text"
iconType="copy"
/>
);
const complexEvent = (
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs" wrap>
<EuiFlexItem grow={false}>added tags</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>case</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>phishing</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>security</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
);
const comments: EuiCommentProps[] = [
{
username: 'janed',
timelineAvatarAriaLabel: 'Jane Doe',
event: 'added a comment',
timestamp: 'on Jan 1, 2020',
children: body,
actions: copyAction,
},
{
username: 'juanab',
timelineAvatarAriaLabel: 'Juana Barros',
actions: copyAction,
event: 'pushed incident X0Z235',
timestamp: 'on Jan 3, 2020',
},
{
username: 'pancho1',
timelineAvatarAriaLabel: 'Pancho PΓ©rez',
event: 'edited case',
timestamp: 'on Jan 9, 2020',
eventIcon: 'pencil',
eventIconAriaLabel: 'edit',
},
{
username: 'pedror',
timelineAvatarAriaLabel: 'Pedro Rodriguez',
actions: copyAction,
event: complexEvent,
timestamp: 'on Jan 11, 2020',
eventIcon: 'tag',
eventIconAriaLabel: 'tag',
},
{
username: 'Assistant',
timelineAvatarAriaLabel: 'Assistant',
timestamp: 'on Jan 14, 2020, 1:39:04 PM',
children: <p>An error occurred sending your message.</p>,
actions: copyAction,
eventColor: 'danger',
},
];
export default () => (
<EuiCommentList comments={comments} aria-label="Comment list example" />
);The EuiComment is flexible and adapts the design according to the props passed.
import { CommentListProps, CommentListStyle } from './comment_list_props.tsx';
<CommentListProps
snippet={<EuiCommentList aria-label="Comment example"> <EuiComment eventIcon="pencil" username="janed" event={event} timestamp={timestamp} actions={customActions} > {children} </EuiComment> </EuiCommentList>}
/>
timelineAvatar: Shows an avatar that should indicate who is the author of the comment. To customize, pass astringas anEuiIcon['type']or an EuiAvatar. Use in conjunction withtimelineAvatarAriaLabelto pass an aria label to the avatar. If no avatar is provided, it will default to an avatar with ausericon.eventIcon: Icon that shows before the username. Use in conjunction witheventIconAriaLabelto pass an aria label to the event icon.username: Author of the comment.event: Describes the event that took place.timestamp: Time of occurrence of the event.actions: Custom actions that the user can perform from the comment's header. When having multiple actions, consider grouping them in a nested menu system using a EuiPopover with a EuiContextMenu.children: A user message or any custom component.
The following demo shows how you can combine different props to create different types of comments events like a regular, update, update with a danger background and a custom one.
import React, { useState } from 'react';
import {
EuiTextArea,
EuiCommentList,
EuiComment,
EuiButtonGroup,
EuiButtonIcon,
EuiText,
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiCommentListProps,
EuiSelect,
EuiCode,
EuiCommentEventProps,
} from '@elastic/eui';
const body = (
<EuiText size="s">
<p>
Far out in the uncharted backwaters of the unfashionable end of the
western spiral arm of the Galaxy lies a small unregarded yellow sun.
</p>
</EuiText>
);
const copyAction = (
<EuiButtonIcon
title="Custom action"
aria-label="Custom action"
color="text"
iconType="copy"
/>
);
const eventWithMultipleTags = (
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs" wrap>
<EuiFlexItem grow={false}>added tags</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>case</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>phishing</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>security</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
);
const commentsData: EuiCommentListProps['comments'] = [
{
username: 'janed',
timelineAvatarAriaLabel: 'Jane Doe',
event: 'added a comment',
timestamp: 'on Jan 1, 2020',
children: body,
actions: copyAction,
},
{
username: 'luisg',
timelineAvatarAriaLabel: 'Luis G',
event: eventWithMultipleTags,
timestamp: '22 hours ago',
eventIcon: 'tag',
eventIconAriaLabel: 'tag',
actions: copyAction,
},
{
username: 'pancho1',
timelineAvatarAriaLabel: 'Pancho PΓ©rez',
children: (
<EuiTextArea
fullWidth
placeholder="I'm a textarea in a EuiComment"
value=""
onChange={() => {}}
/>
),
},
];
const toggleButtons = [
{
id: 'regular',
label: 'Regular',
},
{
id: 'update',
label: 'Update',
},
{
id: 'custom',
label: 'Custom',
},
];
export default () => {
const colors: Array<{
value: EuiCommentEventProps['eventColor'];
text: string;
}> = [
{ value: 'subdued', text: 'subdued' },
{ value: 'transparent', text: 'transparent' },
{ value: 'plain', text: 'plain' },
{ value: 'danger', text: 'danger' },
{ value: 'warning', text: 'warning' },
{ value: 'accent', text: 'accent' },
{ value: 'primary', text: 'primary' },
{ value: 'success', text: 'success' },
{ value: undefined, text: 'undefined' },
];
const [toggleIdSelected, setToggleIdSelected] = useState('regular');
const [color, setColor] = useState(colors[0].value);
const [comment, setComment] = useState(commentsData[0]);
const onChangeButtonGroup = (optionId: any) => {
setToggleIdSelected(optionId);
const buttonId = optionId.replace('Button', '');
const selectedCommentIndex = toggleButtons.findIndex(
({ id }) => id === buttonId
);
setComment(commentsData[selectedCommentIndex]);
};
const onChangeSize = (e: React.ChangeEvent<HTMLSelectElement>) => {
const color = e.target.value;
setColor(
color && color !== 'undefined'
? (color as EuiCommentEventProps['eventColor'])
: undefined
);
};
return (
<>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonGroup
legend="Pick an example"
options={toggleButtons}
onChange={onChangeButtonGroup}
idSelected={toggleIdSelected}
type="single"
color="primary"
/>
</EuiFlexItem>
{toggleIdSelected !== 'custom' ? (
<EuiFlexItem grow={false}>
<EuiSelect
prepend="eventColor"
options={colors}
value={color}
onChange={(e) => onChangeSize(e)}
compressed
/>
</EuiFlexItem>
) : undefined}
<EuiFlexItem>
{toggleIdSelected === 'regular' && color === 'subdued' && (
<span>
subdued is the default <EuiCode>eventColor</EuiCode> for regular{' '}
<strong>EuiComment</strong>
</span>
)}
{toggleIdSelected === 'update' && color === 'transparent' && (
<span>
transparent is the default <EuiCode>eventColor</EuiCode> for
update <strong>EuiComment</strong>
</span>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiCommentList>
<EuiComment {...comment} eventColor={color} />
</EuiCommentList>
</>
);
};The timeline icon is a very important part of the comment:
- By default, each EuiComment shows an avatar with the
usericon. AtimelineAvatarAriaLabelshould be provided when using this default option. - You can customize your avatar by passing to the
timelineAvatarany of the icon types that EuiIcon supports. The icon will show inside asubduedavatar. Consider this option when showing a system update. Providing atimelineAvatarAriaLabelis recommended. - You can further customize the timeline icon by passing to the
timelineAvatara EuiAvatar.
import React from 'react';
import {
EuiCommentList,
EuiComment,
EuiCode,
EuiText,
EuiAvatar,
} from '@elastic/eui';
export default () => (
<EuiCommentList aria-label="An example with different timeline icons">
<EuiComment
username="andred"
timelineAvatarAriaLabel="Andre Diaz"
event="is using a default avatar"
>
<EuiText size="s">
<p>
The avatar initials is generated from the <EuiCode>username</EuiCode>{' '}
prop.
</p>
</EuiText>
</EuiComment>
<EuiComment
username="system"
timelineAvatarAriaLabel="System"
timelineAvatar="dot"
event={
<>
The <EuiCode>timelineAvatar</EuiCode> is using a{' '}
<EuiCode>dot</EuiCode> icon.
</>
}
/>
<EuiComment
username="cat"
timelineAvatarAriaLabel="Beautiful cat"
event="is using a custom avatar"
timelineAvatar={
<EuiAvatar name="Cat" imageUrl="https://picsum.photos/id/40/64" />
}
>
<EuiText size="s">
<p>
The <EuiCode>timelineAvatar</EuiCode> is using a custom{' '}
<strong>EuiAvatar</strong>.
</p>
</EuiText>
</EuiComment>
</EuiCommentList>
);There are scenarios where you might want to allow the user to perform actions related to each comment. Some common actions include: editing, deleting, sharing and copying. To add custom actions to a comment, use the actionsprop. These will be placed to the right of the metadata in the comment's header. We recommend using a EuiButtonIcon to trigger an action. When having multiple actions, consider grouping them in a nested menu system using a EuiPopover with a EuiContextMenu.
import React, { useState } from 'react';
import {
EuiCommentList,
EuiComment,
EuiButtonIcon,
EuiText,
EuiPopover,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiLink,
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiTitle,
useGeneratedHtmlId,
} from '@elastic/eui';
const body = (
<EuiText size="s">
<p>
This comment has custom actions available. See the upper right corner.
</p>
</EuiText>
);
export default () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const flyoutTitleId = useGeneratedHtmlId({
prefix: 'flyoutTitleId',
});
const togglePopover = () => {
setIsPopoverOpen(!isPopoverOpen);
};
const closePopover = () => {
setIsPopoverOpen(false);
};
const toggleFlyout = () => {
setIsFlyoutVisible(!isFlyoutVisible);
};
const flyout = isFlyoutVisible && (
<EuiFlyout
ownFocus
onClose={() => setIsFlyoutVisible(false)}
aria-labelledby={flyoutTitleId}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id={flyoutTitleId}>Malware detection alert</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>
<p>
Use a flyout to show more details related to your comment event.
</p>
</EuiText>
</EuiFlyoutBody>
</EuiFlyout>
);
const customActions = (
<EuiPopover
button={
<EuiButtonIcon
aria-label="Actions"
iconType="boxes_vertical"
size="xs"
color="text"
onClick={togglePopover}
/>
}
isOpen={isPopoverOpen}
closePopover={togglePopover}
panelPaddingSize="none"
anchorPosition="leftCenter"
>
<EuiContextMenuPanel
items={[
<EuiContextMenuItem key="A" icon="pencil" onClick={closePopover}>
Edit
</EuiContextMenuItem>,
<EuiContextMenuItem key="B" icon="share" onClick={closePopover}>
Share
</EuiContextMenuItem>,
<EuiContextMenuItem key="C" icon="copy" onClick={closePopover}>
Copy
</EuiContextMenuItem>,
]}
/>
</EuiPopover>
);
const updateActions = [
<EuiButtonIcon
key="copy-alert"
title="Copy alert link"
aria-label="Copy alert link"
iconType="link"
size="xs"
color="text"
/>,
<EuiButtonIcon
key="show-details"
title="Show the alert details in a flyout"
aria-label="Show details"
iconType="external"
size="xs"
color="text"
onClick={toggleFlyout}
/>,
];
return (
<>
<EuiCommentList aria-label="Actions">
<EuiComment
username="janed"
timelineAvatarAriaLabel="Jane Doe"
event="added a comment"
actions={customActions}
timestamp="on Jan 1, 2020"
>
{body}
</EuiComment>
<EuiComment
username="system"
timelineAvatarAriaLabel="System"
timelineAvatar="dot"
event={
<>
pushed a new incident <EuiLink>malware detection</EuiLink>
</>
}
actions={updateActions}
timestamp="on Jan 2, 2020"
eventColor="danger"
/>
</EuiCommentList>
{flyout}
</>
);
};The below example uses a list of EuiComments, a EuiMarkdownEditor, and a EuiMarkdownFormat to create a simple comment system.
- Each comment renders in a EuiComment.
- We use the EuiMarkdownEditor to post the
EuiComment.children. This means the content uses Markdown. - When the new EuiComment is posted, we use the EuiMarkdownFormat to wrap the
EuiComment.childrenand render the Markdown correctly.
When dealing with asynchronous events like posting a message we recommend setting the EuiMarkdownEditor to a readOnly state and the "Add comment" EuiButton to a isLoading state. This will ensure users understand that the content cannot be changed while the comment is being submitted.
import React, { useState, useEffect, useRef } from 'react';
import {
formatDate,
htmlIdGenerator,
EuiCommentList,
EuiComment,
EuiCommentProps,
EuiButtonIcon,
EuiBadge,
EuiMarkdownEditor,
EuiMarkdownFormat,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiToolTip,
EuiAvatar,
} from '@elastic/eui';
const actionButton = (
<EuiButtonIcon
title="Custom action"
aria-label="Custom action"
color="text"
iconType="copy"
/>
);
const complexEvent = (
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs" wrap>
<EuiFlexItem grow={false}>added tags</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>case</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>phishing</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>security</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
);
const UserActionUsername = ({
username,
fullname,
}: {
username: string;
fullname: string;
}) => {
return (
<EuiToolTip position="top" content={<p>{fullname}</p>}>
<strong>{username}</strong>
</EuiToolTip>
);
};
const initialComments: EuiCommentProps[] = [
{
username: <UserActionUsername username="emma" fullname="Emma Watson" />,
timelineAvatar: <EuiAvatar name="emma" />,
event: 'added a comment',
timestamp: 'on 3rd March 2022',
children: (
<EuiMarkdownFormat textSize="s">
Phishing emails have been on the rise since February
</EuiMarkdownFormat>
),
actions: actionButton,
},
{
username: <UserActionUsername username="emma" fullname="Emma Watson" />,
timelineAvatar: <EuiAvatar name="emma" />,
event: complexEvent,
timestamp: 'on 3rd March 2022',
eventIcon: 'tag',
eventIconAriaLabel: 'tag',
},
{
username: 'system',
timelineAvatar: 'dot',
timelineAvatarAriaLabel: 'System',
event: 'pushed a new incident',
timestamp: 'on 4th March 2022',
eventColor: 'danger',
},
{
username: <UserActionUsername username="tiago" fullname="Tiago Pontes" />,
timelineAvatar: <EuiAvatar name="tiago" />,
event: 'added a comment',
timestamp: 'on 4th March 2022',
actions: actionButton,
children: (
<EuiMarkdownFormat textSize="s">
Take a look at this
[Office.exe](http://my-drive.elastic.co/suspicious-file)
</EuiMarkdownFormat>
),
},
{
username: <UserActionUsername username="emma" fullname="Emma Watson" />,
timelineAvatar: <EuiAvatar name="emma" />,
event: (
<>
marked case as <EuiBadge color="warning">In progress</EuiBadge>
</>
),
timestamp: 'on 4th March 2022',
},
];
const replyMsg = `Thanks, Tiago for taking a look. :tada:
I also found something suspicious: [Update.exe](http://my-drive.elastic.co/suspicious-file).
`;
export default () => {
const errorElementId = useRef(htmlIdGenerator()());
const [editorValue, setEditorValue] = useState(replyMsg);
const [comments, setComments] = useState(initialComments);
const [isLoading, setIsLoading] = useState(false);
const [editorError, setEditorError] = useState(true);
useEffect(() => {
if (editorValue === '') {
setEditorError(true);
} else {
setEditorError(false);
}
}, [editorValue, editorError]);
const onAddComment = () => {
setIsLoading(true);
const date = formatDate(Date.now(), 'dobLong');
setTimeout(() => {
setIsLoading(false);
setEditorValue('');
setComments([
...comments,
{
username: (
<UserActionUsername username="emma" fullname="Emma Watson" />
),
timelineAvatar: <EuiAvatar name="emma" />,
event: 'added a comment',
timestamp: `on ${date}`,
actions: actionButton,
children: (
<EuiMarkdownFormat textSize="s">{editorValue}</EuiMarkdownFormat>
),
},
]);
}, 3000);
};
const commentsList = comments.map((comment, index) => {
return (
<EuiComment key={`comment-${index}`} {...comment}>
{comment.children}
</EuiComment>
);
});
return (
<>
<EuiCommentList aria-label="Comment system example">
{commentsList}
<EuiComment
username="juana"
timelineAvatar={<EuiAvatar name="juana" />}
>
<EuiMarkdownEditor
aria-label="Markdown editor"
aria-describedby={errorElementId.current}
placeholder="Add a comment..."
value={editorValue}
onChange={setEditorValue}
readOnly={isLoading}
initialViewMode="editing"
markdownFormatProps={{ textSize: 's' }}
/>
</EuiComment>
</EuiCommentList>
<EuiSpacer />
<EuiFlexGroup justifyContent="flexEnd" responsive={false}>
<EuiFlexItem grow={false}>
<div>
<EuiButton
onClick={onAddComment}
isLoading={isLoading}
isDisabled={editorError}
>
Add comment
</EuiButton>
</div>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};import docgen from '@elastic/eui-docgen/dist/components/comment_list';