@patternfly/chatbot 6.8.0-prerelease.6 → 6.8.0-prerelease.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  // ============================================================================
2
2
  // Chatbot Header - Chatbot Conversation History Nav
3
3
  // ============================================================================
4
- import type { KeyboardEvent, FunctionComponent } from 'react';
5
- import { useRef, Fragment } from 'react';
4
+ import type { KeyboardEvent, FunctionComponent, ReactNode, Ref, RefObject } from 'react';
5
+ import { useEffect, useRef, Fragment, isValidElement } from 'react';
6
6
 
7
7
  // Import PatternFly components
8
8
  import {
@@ -42,7 +42,11 @@ import {
42
42
  MenuContent,
43
43
  MenuItemProps,
44
44
  MenuGroupProps,
45
- MenuContentProps
45
+ MenuContentProps,
46
+ ExpandableSection,
47
+ ExpandableSectionToggle,
48
+ ExpandableSectionProps,
49
+ ExpandableSectionToggleProps
46
50
  } from '@patternfly/react-core';
47
51
 
48
52
  import { RhUiClockIcon, RhUiCommentIcon, RhUiEditFillIcon } from '@patternfly/react-icons';
@@ -51,6 +55,189 @@ import ConversationHistoryDropdown from './ChatbotConversationHistoryDropdown';
51
55
  import LoadingState from './LoadingState';
52
56
  import HistoryEmptyState, { HistoryEmptyStateProps } from './EmptyState';
53
57
 
58
+ const isConversation = (item: unknown): item is Conversation =>
59
+ Boolean(item && typeof item === 'object' && 'id' in item && 'text' in item && !('items' in item));
60
+
61
+ const isConversationGroup = (item: unknown): item is ConversationGroup =>
62
+ Boolean(item && typeof item === 'object' && 'id' in item && 'label' in item && 'items' in item);
63
+
64
+ const isConversationGroupArray = (items: unknown[]): items is ConversationGroup[] =>
65
+ items.length > 0 && isConversationGroup(items[0]);
66
+
67
+ const getExpandableGroupContentId = (groupId: string) => `chatbot-nav-group-${groupId}-content`;
68
+
69
+ const getGroupLabelId = (groupId: string) => `chatbot-nav-group-${groupId}-label`;
70
+
71
+ const getExpandableGroupToggleId = (groupId: string) => `chatbot-nav-group-${groupId}-toggle`;
72
+
73
+ const getShowAllToggleId = (groupId: string) => `chatbot-nav-group-${groupId}-show-all-toggle`;
74
+
75
+ const getStaticMenuLabelId = (group: { id: string; menuGroupProps?: MenuGroupProps }) =>
76
+ group.menuGroupProps?.titleId ?? getGroupLabelId(group.id);
77
+
78
+ const getMenuListLabelledBy = (group: {
79
+ id: string;
80
+ expandable?: { isExpanded: boolean; onToggle: (isExpanded: boolean) => void };
81
+ menuGroupProps?: MenuGroupProps;
82
+ }) => {
83
+ if (group.expandable) {
84
+ return group.menuGroupProps?.['aria-labelledby'] ?? getExpandableGroupToggleId(group.id);
85
+ }
86
+
87
+ return group.menuGroupProps?.['aria-labelledby'] ?? getStaticMenuLabelId(group);
88
+ };
89
+
90
+ const getMenuListProps = (group: {
91
+ id: string;
92
+ label: ReactNode;
93
+ expandable?: { isExpanded: boolean; onToggle: (isExpanded: boolean) => void };
94
+ menuGroupProps?: MenuGroupProps;
95
+ menuListProps?: Omit<MenuListProps, 'children'>;
96
+ }): Omit<MenuListProps, 'children'> => ({
97
+ ...group.menuListProps,
98
+ 'aria-labelledby': group.menuListProps?.['aria-labelledby'] ?? getMenuListLabelledBy(group)
99
+ });
100
+
101
+ const focusElementById = (id: string) => {
102
+ document.getElementById(id)?.focus();
103
+ };
104
+
105
+ // Matches PatternFly's Menu "view more" example: expanding moves focus to the first
106
+ // newly revealed item; collapsing keeps focus on the toggle as it updates to "Show all".
107
+ const useShowAllFocusManagement = (
108
+ isExpanded: boolean,
109
+ toggleId: string,
110
+ firstOverflowItemRef: Ref<HTMLButtonElement | HTMLAnchorElement | null>,
111
+ hasOverflowItems: boolean
112
+ ) => {
113
+ const wasExpandedRef = useRef(isExpanded);
114
+
115
+ useEffect(() => {
116
+ if (hasOverflowItems && isExpanded !== wasExpandedRef.current) {
117
+ if (isExpanded) {
118
+ (firstOverflowItemRef as RefObject<HTMLElement | null>).current?.focus();
119
+ } else {
120
+ focusElementById(toggleId);
121
+ }
122
+ }
123
+
124
+ wasExpandedRef.current = isExpanded;
125
+ }, [isExpanded, toggleId, firstOverflowItemRef, hasOverflowItems]);
126
+ };
127
+
128
+ interface ShowAllGroupBodyProps {
129
+ group: ConversationGroup;
130
+ getNavItem: (
131
+ conversation: Conversation,
132
+ itemOverrides?: Partial<MenuItemProps>,
133
+ itemRef?: Ref<HTMLButtonElement | HTMLAnchorElement>
134
+ ) => ReactNode;
135
+ }
136
+
137
+ // The toggle is rendered as a real MenuItem (rather than a standalone button) so it
138
+ // participates in the Menu's built-in roving tabindex and arrow-key navigation just
139
+ // like any other conversation item, instead of being skipped over or interrupting
140
+ // arrow-key handling. Since it keeps a stable key and DOM position regardless of how
141
+ // many overflow items are shown, it also keeps focus across clicks without being
142
+ // unmounted or recreated.
143
+ const ShowAllGroupBody: FunctionComponent<ShowAllGroupBodyProps> = ({ group, getNavItem }) => {
144
+ const { visibleCount, isExpanded, onToggle, label } = group.showAll!;
145
+ const conversationItems = group.items.filter(isConversation);
146
+ const alwaysVisibleItems = conversationItems.slice(0, visibleCount);
147
+ const overflowItems = conversationItems.slice(visibleCount);
148
+ const hasOverflowItems = overflowItems.length > 0;
149
+ const toggleId = getShowAllToggleId(group.id);
150
+ const firstOverflowItemRef = useRef<HTMLButtonElement | HTMLAnchorElement>(null);
151
+ const toggleLabel = label ?? (isExpanded ? 'Show less' : 'Show all');
152
+
153
+ useShowAllFocusManagement(isExpanded, toggleId, firstOverflowItemRef, hasOverflowItems);
154
+
155
+ return (
156
+ <>
157
+ <MenuList {...getMenuListProps(group)}>
158
+ {alwaysVisibleItems.map((chat) => (
159
+ <Fragment key={chat.id}>{getNavItem(chat)}</Fragment>
160
+ ))}
161
+ {isExpanded &&
162
+ overflowItems.map((chat, index) => (
163
+ <Fragment key={chat.id}>
164
+ {getNavItem(chat, undefined, index === 0 ? firstOverflowItemRef : undefined)}
165
+ </Fragment>
166
+ ))}
167
+ {hasOverflowItems && (
168
+ <MenuItem
169
+ key="show-all-toggle"
170
+ id={toggleId}
171
+ itemId={toggleId}
172
+ className="pf-chatbot__menu-show-all-toggle"
173
+ onClick={() => onToggle(!isExpanded)}
174
+ >
175
+ {toggleLabel}
176
+ </MenuItem>
177
+ )}
178
+ </MenuList>
179
+ {group.footer}
180
+ </>
181
+ );
182
+ };
183
+
184
+ interface ExpandableConversationGroupProps {
185
+ group: ConversationGroup;
186
+ children: ReactNode;
187
+ }
188
+
189
+ const ExpandableConversationGroup: FunctionComponent<ExpandableConversationGroupProps> = ({ group, children }) => {
190
+ const toggleId = getExpandableGroupToggleId(group.id);
191
+ const contentId = getExpandableGroupContentId(group.id);
192
+ const { isExpanded, onToggle, expandableSectionProps, expandableSectionToggleProps } = group.expandable!;
193
+
194
+ return (
195
+ <div
196
+ className={`pf-chatbot__menu-item-header pf-chatbot__menu-item-header--expandable ${group.menuGroupProps?.className ?? ''}`}
197
+ >
198
+ <ExpandableSectionToggle
199
+ toggleId={toggleId}
200
+ contentId={contentId}
201
+ isExpanded={isExpanded}
202
+ onToggle={onToggle}
203
+ toggleWrapper="h3"
204
+ className="pf-chatbot__menu-group-toggle"
205
+ {...expandableSectionToggleProps}
206
+ >
207
+ {group.label}
208
+ </ExpandableSectionToggle>
209
+ <ExpandableSection
210
+ isDetached
211
+ isExpanded={isExpanded}
212
+ toggleId={toggleId}
213
+ contentId={contentId}
214
+ {...expandableSectionProps}
215
+ >
216
+ {isExpanded && children}
217
+ </ExpandableSection>
218
+ </div>
219
+ );
220
+ };
221
+
222
+ type ConversationMenuSegment =
223
+ | { type: 'static'; groups: ConversationGroup[] }
224
+ | { type: 'expandable'; group: ConversationGroup };
225
+
226
+ const buildConversationMenuSegments = (groups: ConversationGroup[]): ConversationMenuSegment[] =>
227
+ groups.reduce<ConversationMenuSegment[]>((segments, group) => {
228
+ if (group.expandable) {
229
+ return [...segments, { type: 'expandable', group }];
230
+ }
231
+
232
+ const last = segments[segments.length - 1];
233
+ if (last?.type === 'static') {
234
+ last.groups.push(group);
235
+ return segments;
236
+ }
237
+
238
+ return [...segments, { type: 'static', groups: [group] }];
239
+ }, []);
240
+
54
241
  export interface Conversation {
55
242
  /** Conversation id */
56
243
  id: string;
@@ -73,6 +260,55 @@ export interface Conversation {
73
260
  /** Custom dropdown ID to ensure uniqueness across demo instances */
74
261
  dropdownId?: string;
75
262
  }
263
+
264
+ export interface ConversationGroupShowAll {
265
+ /** Number of items visible when collapsed */
266
+ visibleCount: number;
267
+ /** Whether all items are shown */
268
+ isExpanded: boolean;
269
+ /** Callback when show all / show less is toggled */
270
+ onToggle: (isExpanded: boolean) => void;
271
+ /** Custom content rendered in the show all / show less toggle. Defaults to "Show all" when collapsed and "Show less" when expanded. */
272
+ label?: ReactNode;
273
+ }
274
+
275
+ export interface ConversationGroupExpandable {
276
+ /** Whether the group content is expanded */
277
+ isExpanded: boolean;
278
+ /** Callback when the group is toggled */
279
+ onToggle: (isExpanded: boolean) => void;
280
+ /** Additional props applied to ExpandableSection */
281
+ expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
282
+ /** Additional props applied to ExpandableSectionToggle */
283
+ expandableSectionToggleProps?: ExpandableSectionToggleProps;
284
+ }
285
+
286
+ export interface ConversationGroup {
287
+ /** Unique group id */
288
+ id: string;
289
+ /** Group label rendered as a MenuGroup heading or expandable toggle label */
290
+ label: ReactNode;
291
+ /** Conversation items or custom menu content such as a "Show all" action */
292
+ items: (Conversation | ReactNode)[];
293
+ /** Content rendered after the group's menu list */
294
+ footer?: ReactNode;
295
+ /** Custom group header that replaces the default label or expandable toggle */
296
+ header?: ReactNode;
297
+ /** When set, the group renders as an expandable section */
298
+ expandable?: ConversationGroupExpandable;
299
+ /** When set, truncates the list with an expandable show all / show less section */
300
+ showAll?: ConversationGroupShowAll;
301
+ /** Additional props applied to the conversation menu group */
302
+ menuGroupProps?: MenuGroupProps;
303
+ /** Additional props applied to the conversation list */
304
+ menuListProps?: Omit<MenuListProps, 'children'>;
305
+ }
306
+
307
+ export type Conversations =
308
+ | (Conversation | ReactNode)[]
309
+ | ConversationGroup[]
310
+ | { [key: string]: (Conversation | ReactNode)[] };
311
+
76
312
  export interface ChatbotConversationHistoryNavProps extends DrawerProps {
77
313
  /** Function called to toggle drawer */
78
314
  onDrawerToggle: (event: React.KeyboardEvent | React.MouseEvent | React.TransitionEvent) => void;
@@ -85,7 +321,7 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
85
321
  /** Callback function for when an item is selected */
86
322
  onSelectActiveItem?: (event?: React.MouseEvent, itemId?: string | number) => void;
87
323
  /** Items shown in chat history */
88
- conversations: Conversation[] | { [key: string]: Conversation[] };
324
+ conversations: Conversations;
89
325
  /** Additional button props for new chat button. */
90
326
  newChatButtonProps?: ButtonProps;
91
327
  /** Additional props applied to conversation menu group. If conversations is an object, you should pass an object of MenuGroupProps for each group. */
@@ -211,13 +447,15 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
211
447
  drawerRef.current && drawerRef.current.focus();
212
448
  };
213
449
 
214
- const isConversation = (item: any): item is Conversation =>
215
- item && typeof item === 'object' && 'id' in item && 'text' in item;
216
-
217
- const getNavItem = (conversation: Conversation) => (
450
+ const getNavItem = (
451
+ conversation: Conversation,
452
+ itemOverrides?: Partial<MenuItemProps>,
453
+ itemRef?: Ref<HTMLButtonElement | HTMLAnchorElement>
454
+ ) => (
218
455
  <MenuItem
219
456
  className={`pf-chatbot__menu-item ${activeItemId && activeItemId === conversation.id ? 'pf-chatbot__menu-item--active' : ''}`}
220
457
  itemId={conversation.id}
458
+ ref={itemRef}
221
459
  {...(conversation.noIcon ? {} : { icon: conversation.icon ?? <RhUiCommentIcon /> })}
222
460
  /* eslint-disable indent */
223
461
  {...(conversation.menuItems
@@ -233,45 +471,118 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
233
471
  }
234
472
  : {})}
235
473
  {...conversation.additionalProps}
474
+ {...itemOverrides}
236
475
  >
237
476
  {conversation.text}
238
477
  </MenuItem>
239
478
  );
240
479
 
241
- const buildConversations = () => {
242
- if (Array.isArray(conversations)) {
480
+ const renderConversationItems = (items: (Conversation | ReactNode)[], keyPrefix = '') =>
481
+ items.map((item, index) => {
482
+ if (isConversation(item)) {
483
+ return <Fragment key={item.id}>{getNavItem(item)}</Fragment>;
484
+ }
485
+
486
+ const key = isValidElement(item) && item.key != null ? String(item.key) : `${keyPrefix}-${index}`;
487
+
488
+ return <Fragment key={key}>{item}</Fragment>;
489
+ });
490
+
491
+ const renderGroupBody = (group: ConversationGroup) => {
492
+ if (group.showAll) {
493
+ return <ShowAllGroupBody group={group} getNavItem={getNavItem} />;
494
+ }
495
+
496
+ return (
497
+ <>
498
+ <MenuList {...getMenuListProps(group)}>{renderConversationItems(group.items, group.id)}</MenuList>
499
+ {group.footer}
500
+ </>
501
+ );
502
+ };
503
+
504
+ const renderConversationGroup = (group: ConversationGroup) => {
505
+ if (group.header) {
243
506
  return (
244
- <MenuList {...menuListProps}>
245
- {conversations.map((conversation) => {
246
- if (isConversation(conversation)) {
247
- return <Fragment key={conversation.id}>{getNavItem(conversation)}</Fragment>;
248
- } else {
249
- return conversation;
250
- }
251
- })}
252
- </MenuList>
507
+ <div className={`pf-chatbot__menu-item-header ${group.menuGroupProps?.className ?? ''}`} key={group.id}>
508
+ {group.header}
509
+ {renderGroupBody(group)}
510
+ </div>
253
511
  );
254
- } else {
255
- return (
256
- <>
257
- {Object.keys(conversations).map((navGroup) => (
258
- <MenuGroup
259
- className="pf-chatbot__menu-item-header"
260
- label={navGroup}
261
- key={navGroup}
262
- labelHeadingLevel="h3"
263
- {...menuGroupProps?.[navGroup]}
264
- >
265
- <MenuList {...menuListProps?.[navGroup]}>
266
- {conversations[navGroup].map((conversation: Conversation) => (
267
- <Fragment key={conversation.id}>{getNavItem(conversation)}</Fragment>
268
- ))}
269
- </MenuList>
270
- </MenuGroup>
271
- ))}
272
- </>
512
+ }
513
+
514
+ const labelId = getStaticMenuLabelId(group);
515
+
516
+ return (
517
+ <MenuGroup
518
+ className="pf-chatbot__menu-item-header"
519
+ label={group.label}
520
+ key={group.id}
521
+ labelHeadingLevel="h3"
522
+ {...group.menuGroupProps}
523
+ titleId={labelId}
524
+ aria-labelledby={group.menuGroupProps?.['aria-labelledby'] ?? labelId}
525
+ >
526
+ {renderGroupBody(group)}
527
+ </MenuGroup>
528
+ );
529
+ };
530
+
531
+ const renderConversationMenu = (content: ReactNode, key?: string) => (
532
+ <Menu
533
+ key={key}
534
+ className="pf-chatbot__history-menu"
535
+ isPlain
536
+ onSelect={onSelectActiveItem}
537
+ activeItemId={activeItemId}
538
+ {...menuProps}
539
+ >
540
+ <MenuContent {...menuContentProps}>{content}</MenuContent>
541
+ </Menu>
542
+ );
543
+
544
+ const renderConversationMenuSegment = (segment: ConversationMenuSegment) => {
545
+ if (segment.type === 'static') {
546
+ return renderConversationMenu(
547
+ <>{segment.groups.map(renderConversationGroup)}</>,
548
+ segment.groups.map((group) => group.id).join('-')
273
549
  );
274
550
  }
551
+
552
+ return (
553
+ <ExpandableConversationGroup group={segment.group} key={segment.group.id}>
554
+ {renderConversationMenu(renderGroupBody(segment.group))}
555
+ </ExpandableConversationGroup>
556
+ );
557
+ };
558
+
559
+ const normalizeObjectGroups = (groupedConversations: { [key: string]: (Conversation | ReactNode)[] }) =>
560
+ Object.keys(groupedConversations).map((groupKey) => ({
561
+ id: groupKey,
562
+ label: groupKey,
563
+ items: groupedConversations[groupKey],
564
+ menuGroupProps:
565
+ menuGroupProps && typeof menuGroupProps === 'object' && groupKey in menuGroupProps
566
+ ? (menuGroupProps as { [key: string]: MenuGroupProps })[groupKey]
567
+ : undefined,
568
+ menuListProps:
569
+ menuListProps && typeof menuListProps === 'object' && groupKey in menuListProps
570
+ ? (menuListProps as { [key: string]: Omit<MenuListProps, 'children'> })[groupKey]
571
+ : undefined
572
+ }));
573
+
574
+ const buildConversations = () => {
575
+ if (Array.isArray(conversations)) {
576
+ if (isConversationGroupArray(conversations)) {
577
+ return <>{buildConversationMenuSegments(conversations).map(renderConversationMenuSegment)}</>;
578
+ }
579
+
580
+ return renderConversationMenu(<MenuList {...menuListProps}>{renderConversationItems(conversations)}</MenuList>);
581
+ }
582
+
583
+ return (
584
+ <>{buildConversationMenuSegments(normalizeObjectGroups(conversations)).map(renderConversationMenuSegment)}</>
585
+ );
275
586
  };
276
587
 
277
588
  // Menu Content
@@ -289,17 +600,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
289
600
  if (noResultsState) {
290
601
  return <HistoryEmptyState {...noResultsState} />;
291
602
  }
292
- return (
293
- <Menu
294
- className="pf-chatbot__history-menu"
295
- isPlain
296
- onSelect={onSelectActiveItem}
297
- activeItemId={activeItemId}
298
- {...menuProps}
299
- >
300
- <MenuContent {...menuContentProps}>{buildConversations()}</MenuContent>
301
- </Menu>
302
- );
603
+ return buildConversations();
303
604
  };
304
605
 
305
606
  const renderDrawerContent = () => (
@@ -21,6 +21,12 @@
21
21
  display: contents;
22
22
  }
23
23
 
24
+ &.pf-chatbot__button--active {
25
+ svg {
26
+ --pf-v6-c-icon__content--svg--VerticalAlign: -1.125em;
27
+ }
28
+ }
29
+
24
30
  // Icon
25
31
  svg {
26
32
  width: var(--pf-t--global--spacer--lg);
@@ -200,6 +200,11 @@
200
200
  &:not(.pf-m-xl, .pf-m-lg, .pf-m-md, .pf-m-sm) {
201
201
  --pf-v6-c-avatar--Width: 1.5rem;
202
202
  --pf-v6-c-avatar--Height: 1.5rem;
203
+
204
+ .pf-v6-svg {
205
+ width: 1rem;
206
+ height: 1rem;
207
+ }
203
208
  }
204
209
  }
205
210
 
@@ -24,7 +24,6 @@
24
24
  }
25
25
 
26
26
  @media screen and (min-width: 64rem) {
27
- .pf-chatbot--embedded,
28
27
  .pf-chatbot--drawer,
29
28
  .pf-chatbot--fullscreen {
30
29
  .pf-chatbot__messagebox {
@@ -32,6 +31,21 @@
32
31
  width: 100%;
33
32
  }
34
33
  }
34
+
35
+ /*
36
+ * Moves the scrollbar; see https://github.com/opendatahub-io/odh-dashboard/pull/8199
37
+ */
38
+ .pf-chatbot--embedded .pf-chatbot__messagebox.pf-chatbot__messagebox--outer-scroll {
39
+ width: 100%;
40
+ max-width: unset;
41
+ align-items: center;
42
+ }
43
+
44
+ .pf-chatbot--embedded .pf-chatbot__messagebox.pf-chatbot__messagebox--outer-scroll > *:not(.pf-chatbot__messagebox-announcement) {
45
+ max-width: 60rem;
46
+ width: 100%;
47
+ }
48
+
35
49
  }
36
50
 
37
51
  .pf-chatbot.pf-m-compact {
@@ -19,6 +19,7 @@ import {
19
19
  } from 'react';
20
20
  import JumpButton from './JumpButton';
21
21
  import { ButtonProps, getResizeObserver, TooltipProps } from '@patternfly/react-core';
22
+ import { css } from '@patternfly/react-styles';
22
23
 
23
24
  export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
24
25
  /** Content that can be announced, such as a new message, for screen readers */
@@ -47,6 +48,8 @@ export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
47
48
  jumpButtonTopTooltipProps?: TooltipProps;
48
49
  /** Props passed to top jump button tooltip */
49
50
  jumpButtonBottomTooltipProps?: TooltipProps;
51
+ /** Whether scrollbar is on the page or message box in embedded mode */
52
+ hasOuterScrollbar?: boolean;
50
53
  }
51
54
 
52
55
  export interface MessageBoxHandle extends HTMLDivElement {
@@ -115,6 +118,7 @@ export const MessageBox = forwardRef(
115
118
  jumpButtonBottomProps,
116
119
  jumpButtonBottomTooltipProps,
117
120
  jumpButtonTopTooltipProps,
121
+ hasOuterScrollbar = false,
118
122
  ...props
119
123
  }: MessageBoxProps,
120
124
  ref: ForwardedRef<MessageBoxHandle | null>
@@ -433,7 +437,12 @@ export const MessageBox = forwardRef(
433
437
  role="region"
434
438
  tabIndex={0}
435
439
  aria-label={ariaLabel}
436
- className={`pf-chatbot__messagebox ${position === 'bottom' ? 'pf-chatbot__messagebox--bottom' : ''} ${className ?? ''}`}
440
+ className={css(
441
+ 'pf-chatbot__messagebox',
442
+ position === 'bottom' && 'pf-chatbot__messagebox--bottom',
443
+ hasOuterScrollbar && 'pf-chatbot__messagebox--outer-scroll',
444
+ className
445
+ )}
437
446
  ref={messageBoxRef}
438
447
  {...props}
439
448
  {...(enableSmartScroll ? { ...smartScrollHandlers } : {})}