@patternfly/chatbot 6.8.0-prerelease.7 → 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.
@@ -12,8 +12,9 @@ import '@testing-library/jest-dom';
12
12
  import { fireEvent, render, screen, waitFor } from '@testing-library/react';
13
13
  import { ChatbotDisplayMode } from '../Chatbot/Chatbot';
14
14
  import ChatbotConversationHistoryNav from './ChatbotConversationHistoryNav';
15
- import { EmptyStateStatus, Spinner } from '@patternfly/react-core';
15
+ import { EmptyStateStatus, Spinner, MenuItem } from '@patternfly/react-core';
16
16
  import { BellIcon, OutlinedCommentsIcon, SearchIcon } from '@patternfly/react-icons';
17
+ import { useState } from 'react';
17
18
  const ERROR = {
18
19
  bodyText: (_jsxs(_Fragment, { children: ["To try again, check your connection and reload this page. If the issue persists,", ' ', _jsx("a", { href: "", children: "contact the support team" }), "."] })),
19
20
  buttonText: 'Reload',
@@ -269,4 +270,327 @@ describe('ChatbotConversationHistoryNav', () => {
269
270
  render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: initialConversations, navTitleIcon: _jsx(BellIcon, { "data-testid": "bell" }) }));
270
271
  expect(screen.getByTestId('bell')).toBeInTheDocument();
271
272
  });
273
+ it('renders static and expandable groups from ConversationGroup[]', () => {
274
+ const groups = [
275
+ {
276
+ id: 'pinned',
277
+ label: 'Pinned chats',
278
+ items: initialConversations
279
+ },
280
+ {
281
+ id: 'chats',
282
+ label: 'Chats',
283
+ expandable: {
284
+ isExpanded: true,
285
+ onToggle: jest.fn()
286
+ },
287
+ items: [{ id: '2', text: 'Chatbot extension' }]
288
+ }
289
+ ];
290
+ render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: groups }));
291
+ expect(screen.getByRole('heading', { name: 'Pinned chats', level: 3 })).toBeInTheDocument();
292
+ expect(screen.queryByRole('button', { name: 'Pinned chats' })).not.toBeInTheDocument();
293
+ expect(screen.getByRole('button', { name: 'Chats' })).toBeInTheDocument();
294
+ });
295
+ it('labels each MenuList with aria-labelledby referencing its visible label', () => {
296
+ const groups = [
297
+ {
298
+ id: 'pinned',
299
+ label: 'Pinned chats',
300
+ items: initialConversations
301
+ },
302
+ {
303
+ id: 'chats',
304
+ label: 'Chats',
305
+ expandable: {
306
+ isExpanded: true,
307
+ onToggle: jest.fn()
308
+ },
309
+ items: [{ id: '2', text: 'Chatbot extension' }]
310
+ }
311
+ ];
312
+ render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: groups }));
313
+ const pinnedHeading = screen.getByRole('heading', { name: 'Pinned chats', level: 3 });
314
+ expect(pinnedHeading).toHaveAttribute('id', 'chatbot-nav-group-pinned-label');
315
+ expect(pinnedHeading.closest('section')).toHaveAttribute('aria-labelledby', 'chatbot-nav-group-pinned-label');
316
+ expect(screen.getByRole('menu', { name: 'Pinned chats' })).toHaveAttribute('aria-labelledby', 'chatbot-nav-group-pinned-label');
317
+ expect(document.querySelector('ul.pf-v6-c-menu__list[aria-labelledby="chatbot-nav-group-pinned-label"]')).toBeTruthy();
318
+ const chatsToggle = screen.getByRole('button', { name: 'Chats' });
319
+ expect(chatsToggle).toHaveAttribute('id', 'chatbot-nav-group-chats-toggle');
320
+ expect(screen.getByRole('menu', { name: 'Chats' })).toHaveAttribute('aria-labelledby', 'chatbot-nav-group-chats-toggle');
321
+ expect(document.querySelector('ul.pf-v6-c-menu__list[aria-labelledby="chatbot-nav-group-chats-toggle"]')).toBeTruthy();
322
+ });
323
+ it('labels grouped object conversations on the menu list element', () => {
324
+ render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: { Today: initialConversations } }));
325
+ expect(document.querySelector('ul.pf-v6-c-menu__list')).toHaveAttribute('aria-labelledby', 'chatbot-nav-group-Today-label');
326
+ });
327
+ it('labels each menu list in a shared static menu with its group title id', () => {
328
+ const groups = [
329
+ {
330
+ id: 'pinned',
331
+ label: 'Pinned chats',
332
+ items: initialConversations
333
+ },
334
+ {
335
+ id: 'recent',
336
+ label: 'Recent chats',
337
+ items: [{ id: '2', text: 'Chatbot extension' }]
338
+ }
339
+ ];
340
+ render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: groups }));
341
+ expect(screen.getByRole('menu', { name: 'Pinned chats' })).toHaveAttribute('aria-labelledby', 'chatbot-nav-group-pinned-label');
342
+ expect(screen.getByRole('menu', { name: 'Recent chats' })).toHaveAttribute('aria-labelledby', 'chatbot-nav-group-recent-label');
343
+ });
344
+ it('does not autofocus the first menu item when an expandable group is expanded', () => __awaiter(void 0, void 0, void 0, function* () {
345
+ const ExpandableGroupDemo = () => {
346
+ const [isExpanded, setIsExpanded] = useState(false);
347
+ return (_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.embedded, setIsDrawerOpen: jest.fn(), conversations: [
348
+ {
349
+ id: 'saved-prompts',
350
+ label: 'Saved prompts',
351
+ expandable: {
352
+ isExpanded,
353
+ onToggle: setIsExpanded
354
+ },
355
+ items: [
356
+ { id: '7', text: 'Summarize this document' },
357
+ { id: '8', text: 'Draft a release announcement' }
358
+ ]
359
+ }
360
+ ] }));
361
+ };
362
+ render(_jsx(ExpandableGroupDemo, {}));
363
+ fireEvent.click(screen.getByRole('button', { name: 'Saved prompts' }));
364
+ yield waitFor(() => {
365
+ expect(screen.getByRole('menuitem', { name: 'Summarize this document' })).toBeInTheDocument();
366
+ });
367
+ expect(screen.getByRole('menuitem', { name: 'Summarize this document' })).not.toHaveFocus();
368
+ }));
369
+ it('does not render a collapsed expandable group menu', () => {
370
+ const groups = [
371
+ {
372
+ id: 'chats',
373
+ label: 'Chats',
374
+ items: [
375
+ { id: '2', text: 'Chat two' },
376
+ { id: '3', text: 'Chat three' }
377
+ ]
378
+ },
379
+ {
380
+ id: 'saved-prompts',
381
+ label: 'Saved prompts',
382
+ expandable: {
383
+ isExpanded: false,
384
+ onToggle: jest.fn()
385
+ },
386
+ items: [
387
+ { id: '7', text: 'Summarize this document' },
388
+ { id: '8', text: 'Draft a release announcement' }
389
+ ]
390
+ }
391
+ ];
392
+ render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.embedded, setIsDrawerOpen: jest.fn(), conversations: groups }));
393
+ // Collapsed expandable groups render their toggle outside the menu and omit the menu
394
+ // entirely until expanded, so their items are not part of arrow-key navigation.
395
+ expect(screen.getAllByRole('menu')).toHaveLength(1);
396
+ expect(screen.getByRole('menu', { name: 'Chats' })).toHaveAttribute('aria-labelledby', 'chatbot-nav-group-chats-label');
397
+ expect(screen.queryByRole('menuitem', { name: 'Summarize this document' })).not.toBeInTheDocument();
398
+ expect(screen.queryByRole('menuitem', { name: 'Draft a release announcement' })).not.toBeInTheDocument();
399
+ const firstItem = screen.getByRole('menuitem', { name: 'Chat two' });
400
+ const lastItem = screen.getByRole('menuitem', { name: 'Chat three' });
401
+ lastItem.focus();
402
+ fireEvent.keyDown(lastItem, { key: 'ArrowDown' });
403
+ expect(firstItem).toHaveFocus();
404
+ });
405
+ it('collapses and expands a group when expandable.onToggle is called', () => __awaiter(void 0, void 0, void 0, function* () {
406
+ const onToggle = jest.fn();
407
+ const groups = [
408
+ {
409
+ id: 'chats',
410
+ label: 'Chats',
411
+ expandable: {
412
+ isExpanded: true,
413
+ onToggle
414
+ },
415
+ items: [{ id: '2', text: 'Chatbot extension' }]
416
+ }
417
+ ];
418
+ const { rerender } = render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: groups }));
419
+ expect(screen.getByRole('menuitem', { name: /Chatbot extension/i })).toBeInTheDocument();
420
+ fireEvent.click(screen.getByRole('button', { name: 'Chats' }));
421
+ expect(onToggle).toHaveBeenCalledWith(false);
422
+ rerender(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: [
423
+ Object.assign(Object.assign({}, groups[0]), { expandable: {
424
+ isExpanded: false,
425
+ onToggle
426
+ } })
427
+ ] }));
428
+ yield waitFor(() => {
429
+ expect(screen.queryByRole('menuitem', { name: /Chatbot extension/i })).not.toBeInTheDocument();
430
+ });
431
+ }));
432
+ it('renders custom menu items and footers supplied in ConversationGroup', () => {
433
+ const groups = [
434
+ {
435
+ id: 'chats',
436
+ label: 'Chats',
437
+ items: [
438
+ initialConversations[0],
439
+ _jsx(MenuItem, { itemId: "show-all", children: "Show all" }, "show-all")
440
+ ],
441
+ footer: _jsx("div", { "data-testid": "group-footer", children: "Footer content" })
442
+ }
443
+ ];
444
+ render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: groups }));
445
+ expect(screen.getByRole('menuitem', { name: 'Show all' })).toBeInTheDocument();
446
+ expect(screen.getByTestId('group-footer')).toBeInTheDocument();
447
+ });
448
+ it('moves focus to the first overflow item when show all is expanded', () => __awaiter(void 0, void 0, void 0, function* () {
449
+ const recentChats = [
450
+ { id: '2', text: 'Chat two' },
451
+ { id: '3', text: 'Chat three' },
452
+ { id: '4', text: 'Chat four' },
453
+ { id: '5', text: 'Chat five' },
454
+ { id: '6', text: 'Chat six' }
455
+ ];
456
+ const VISIBLE_CHAT_COUNT = 3;
457
+ const ShowAllDemo = () => {
458
+ const [isShowingAllChats, setIsShowingAllChats] = useState(false);
459
+ return (_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.embedded, setIsDrawerOpen: jest.fn(), conversations: [
460
+ {
461
+ id: 'chats',
462
+ label: 'Chats',
463
+ items: recentChats,
464
+ showAll: {
465
+ visibleCount: VISIBLE_CHAT_COUNT,
466
+ isExpanded: isShowingAllChats,
467
+ onToggle: setIsShowingAllChats
468
+ }
469
+ }
470
+ ] }));
471
+ };
472
+ render(_jsx(ShowAllDemo, {}));
473
+ fireEvent.click(screen.getByRole('menuitem', { name: /Show all/i }));
474
+ yield waitFor(() => {
475
+ expect(screen.getByRole('menuitem', { name: 'Chat five' })).toHaveFocus();
476
+ });
477
+ }));
478
+ it('keeps focus on the toggle when show all is collapsed', () => __awaiter(void 0, void 0, void 0, function* () {
479
+ const recentChats = [
480
+ { id: '2', text: 'Chat two' },
481
+ { id: '3', text: 'Chat three' },
482
+ { id: '4', text: 'Chat four' },
483
+ { id: '5', text: 'Chat five' }
484
+ ];
485
+ const ShowAllDemo = () => {
486
+ const [isShowingAllChats, setIsShowingAllChats] = useState(true);
487
+ return (_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.embedded, setIsDrawerOpen: jest.fn(), conversations: [
488
+ {
489
+ id: 'chats',
490
+ label: 'Chats',
491
+ items: recentChats,
492
+ showAll: {
493
+ visibleCount: 2,
494
+ isExpanded: isShowingAllChats,
495
+ onToggle: setIsShowingAllChats
496
+ }
497
+ }
498
+ ] }));
499
+ };
500
+ render(_jsx(ShowAllDemo, {}));
501
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Show less' }));
502
+ yield waitFor(() => {
503
+ expect(screen.getByRole('menuitem', { name: /Show all/i })).toHaveFocus();
504
+ });
505
+ }));
506
+ it('renders overflow items above the show all toggle so it stays in a fixed position', () => {
507
+ const recentChats = [
508
+ { id: '2', text: 'Chat two' },
509
+ { id: '3', text: 'Chat three' },
510
+ { id: '4', text: 'Chat four' }
511
+ ];
512
+ render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.embedded, setIsDrawerOpen: jest.fn(), conversations: [
513
+ {
514
+ id: 'chats',
515
+ label: 'Chats',
516
+ items: recentChats,
517
+ showAll: {
518
+ visibleCount: 1,
519
+ isExpanded: true,
520
+ onToggle: jest.fn()
521
+ }
522
+ }
523
+ ] }));
524
+ const overflowItem = screen.getByRole('menuitem', { name: 'Chat four' });
525
+ const toggle = screen.getByRole('menuitem', { name: 'Show less' });
526
+ const isToggleAfterOverflowItem = Boolean(
527
+ // eslint-disable-next-line no-bitwise
528
+ overflowItem.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING);
529
+ expect(isToggleAfterOverflowItem).toBe(true);
530
+ });
531
+ it('allows arrow key navigation to flow through the show all toggle like any other menu item', () => __awaiter(void 0, void 0, void 0, function* () {
532
+ const recentChats = [
533
+ { id: '2', text: 'Chat two' },
534
+ { id: '3', text: 'Chat three' },
535
+ { id: '4', text: 'Chat four' }
536
+ ];
537
+ render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.embedded, setIsDrawerOpen: jest.fn(), conversations: [
538
+ {
539
+ id: 'chats',
540
+ label: 'Chats',
541
+ items: recentChats,
542
+ showAll: {
543
+ visibleCount: 2,
544
+ isExpanded: false,
545
+ onToggle: jest.fn()
546
+ }
547
+ }
548
+ ] }));
549
+ const lastVisibleItem = screen.getByRole('menuitem', { name: 'Chat three' });
550
+ const toggle = screen.getByRole('menuitem', { name: /Show all/i });
551
+ lastVisibleItem.focus();
552
+ fireEvent.keyDown(lastVisibleItem, { key: 'ArrowDown' });
553
+ yield waitFor(() => {
554
+ expect(toggle).toHaveFocus();
555
+ });
556
+ fireEvent.keyDown(toggle, { key: 'ArrowUp' });
557
+ yield waitFor(() => {
558
+ expect(lastVisibleItem).toHaveFocus();
559
+ });
560
+ }));
561
+ it('preserves custom menu item identity when preceding items change', () => {
562
+ const renderShowAll = () => (_jsx(MenuItem, { itemId: "show-all-chats", children: "Show all" }, "show-all-chats"));
563
+ const { rerender } = render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: [
564
+ {
565
+ id: 'chats',
566
+ label: 'Chats',
567
+ items: [initialConversations[0], renderShowAll()]
568
+ }
569
+ ] }));
570
+ const showAllBefore = screen.getByRole('menuitem', { name: 'Show all' });
571
+ rerender(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: [
572
+ {
573
+ id: 'chats',
574
+ label: 'Chats',
575
+ items: [initialConversations[0], initialConversations[1], initialConversations[2], renderShowAll()]
576
+ }
577
+ ] }));
578
+ expect(screen.getByRole('menuitem', { name: 'Show all' })).toBe(showAllBefore);
579
+ });
580
+ it('passes expandableSectionProps from expandable ConversationGroup', () => {
581
+ const groups = [
582
+ {
583
+ id: 'saved',
584
+ label: 'Saved prompts',
585
+ expandable: {
586
+ isExpanded: true,
587
+ onToggle: jest.fn(),
588
+ expandableSectionProps: { className: 'test-expandable-section' }
589
+ },
590
+ items: initialConversations
591
+ }
592
+ ];
593
+ render(_jsx(ChatbotConversationHistoryNav, { onDrawerToggle: onDrawerToggle, isDrawerOpen: true, displayMode: ChatbotDisplayMode.fullscreen, setIsDrawerOpen: jest.fn(), conversations: groups }));
594
+ expect(document.querySelector('.test-expandable-section')).toBeInTheDocument();
595
+ });
272
596
  });
@@ -27,6 +27,8 @@ export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
27
27
  jumpButtonTopTooltipProps?: TooltipProps;
28
28
  /** Props passed to top jump button tooltip */
29
29
  jumpButtonBottomTooltipProps?: TooltipProps;
30
+ /** Whether scrollbar is on the page or message box in embedded mode */
31
+ hasOuterScrollbar?: boolean;
30
32
  }
31
33
  export interface MessageBoxHandle extends HTMLDivElement {
32
34
  /** Scrolls to the top of the message box */
@@ -16,6 +16,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
16
16
  import { useState, useRef, useCallback, useLayoutEffect, forwardRef, useImperativeHandle } from 'react';
17
17
  import JumpButton from './JumpButton';
18
18
  import { getResizeObserver } from '@patternfly/react-core';
19
+ import { css } from '@patternfly/react-styles';
19
20
  const SCROLL_EDGE_DELTA = 100;
20
21
  const getScrollMetrics = (element) => {
21
22
  const { scrollTop, scrollHeight, clientHeight } = element;
@@ -40,7 +41,7 @@ const shouldFollowContentGrowth = (metrics, enableSmartScroll, autoScroll, pause
40
41
  metrics.nearBottom ||
41
42
  (enableSmartScroll && autoScroll && !pauseAutoScroll && metrics.roundedScrollTop > 0);
42
43
  export const MessageBox = forwardRef((_a, ref) => {
43
- var { announcement, ariaLabel = 'Scrollable message log', children, className, position = 'top', onScrollToTopClick, onScrollToBottomClick, enableSmartScroll = false, jumpButtonTopProps, jumpButtonBottomProps, jumpButtonBottomTooltipProps, jumpButtonTopTooltipProps } = _a, props = __rest(_a, ["announcement", "ariaLabel", "children", "className", "position", "onScrollToTopClick", "onScrollToBottomClick", "enableSmartScroll", "jumpButtonTopProps", "jumpButtonBottomProps", "jumpButtonBottomTooltipProps", "jumpButtonTopTooltipProps"]);
44
+ var { announcement, ariaLabel = 'Scrollable message log', children, className, position = 'top', onScrollToTopClick, onScrollToBottomClick, enableSmartScroll = false, jumpButtonTopProps, jumpButtonBottomProps, jumpButtonBottomTooltipProps, jumpButtonTopTooltipProps, hasOuterScrollbar = false } = _a, props = __rest(_a, ["announcement", "ariaLabel", "children", "className", "position", "onScrollToTopClick", "onScrollToBottomClick", "enableSmartScroll", "jumpButtonTopProps", "jumpButtonBottomProps", "jumpButtonBottomTooltipProps", "jumpButtonTopTooltipProps", "hasOuterScrollbar"]);
44
45
  const [atTop, setAtTop] = useState(true);
45
46
  const [atBottom, setAtBottom] = useState(false);
46
47
  const [isOverflowing, setIsOverflowing] = useState(false);
@@ -251,6 +252,6 @@ export const MessageBox = forwardRef((_a, ref) => {
251
252
  onTouchMove,
252
253
  onTouchEnd
253
254
  };
254
- return (_jsxs(_Fragment, { children: [_jsx(JumpButton, { position: "top", isHidden: !hasMeasuredScroll || (isOverflowing && atTop), onClick: scrollToTop, jumpButtonProps: jumpButtonTopProps, jumpButtonTooltipProps: jumpButtonTopTooltipProps }), _jsx(JumpButton, { position: "bottom", isHidden: !hasMeasuredScroll || (isOverflowing && atBottom), onClick: () => scrollToBottom({ resumeSmartScroll: true }), jumpButtonProps: jumpButtonBottomProps, jumpButtonTooltipProps: jumpButtonBottomTooltipProps }), _jsxs("div", Object.assign({ role: "region", tabIndex: 0, "aria-label": ariaLabel, className: `pf-chatbot__messagebox ${position === 'bottom' ? 'pf-chatbot__messagebox--bottom' : ''} ${className !== null && className !== void 0 ? className : ''}`, ref: messageBoxRef }, props, (enableSmartScroll ? Object.assign({}, smartScrollHandlers) : {}), { children: [children, _jsx("div", { className: "pf-chatbot__messagebox-announcement pf-chatbot-m-hidden", "aria-live": "polite", children: announcement })] }))] }));
255
+ return (_jsxs(_Fragment, { children: [_jsx(JumpButton, { position: "top", isHidden: !hasMeasuredScroll || (isOverflowing && atTop), onClick: scrollToTop, jumpButtonProps: jumpButtonTopProps, jumpButtonTooltipProps: jumpButtonTopTooltipProps }), _jsx(JumpButton, { position: "bottom", isHidden: !hasMeasuredScroll || (isOverflowing && atBottom), onClick: () => scrollToBottom({ resumeSmartScroll: true }), jumpButtonProps: jumpButtonBottomProps, jumpButtonTooltipProps: jumpButtonBottomTooltipProps }), _jsxs("div", Object.assign({ role: "region", tabIndex: 0, "aria-label": ariaLabel, className: css('pf-chatbot__messagebox', position === 'bottom' && 'pf-chatbot__messagebox--bottom', hasOuterScrollbar && 'pf-chatbot__messagebox--outer-scroll', className), ref: messageBoxRef }, props, (enableSmartScroll ? Object.assign({}, smartScrollHandlers) : {}), { children: [children, _jsx("div", { className: "pf-chatbot__messagebox-announcement pf-chatbot-m-hidden", "aria-live": "polite", children: announcement })] }))] }));
255
256
  });
256
257
  export default MessageBox;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/chatbot",
3
- "version": "6.8.0-prerelease.7",
3
+ "version": "6.8.0-prerelease.8",
4
4
  "description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -0,0 +1,104 @@
1
+ import { useState, FunctionComponent } from 'react';
2
+ import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
3
+ import ChatbotConversationHistoryNav, {
4
+ Conversation,
5
+ ConversationGroup
6
+ } from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
7
+ import { Checkbox } from '@patternfly/react-core';
8
+
9
+ const pinnedChats: Conversation[] = [
10
+ {
11
+ id: '1',
12
+ text: 'Red Hat products and services'
13
+ }
14
+ ];
15
+
16
+ const recentChats: Conversation[] = [
17
+ {
18
+ id: '2',
19
+ text: 'Enterprise Linux installation and setup'
20
+ },
21
+ {
22
+ id: '3',
23
+ text: 'Troubleshoot system crash'
24
+ },
25
+ {
26
+ id: '4',
27
+ text: 'Ansible security and updates'
28
+ },
29
+ {
30
+ id: '5',
31
+ text: 'Red Hat certification'
32
+ },
33
+ {
34
+ id: '6',
35
+ text: 'Lightspeed user documentation'
36
+ }
37
+ ];
38
+
39
+ const savedPrompts: Conversation[] = [
40
+ {
41
+ id: '7',
42
+ text: 'Summarize this document'
43
+ },
44
+ {
45
+ id: '8',
46
+ text: 'Draft a release announcement'
47
+ }
48
+ ];
49
+
50
+ const VISIBLE_CHAT_COUNT = 3;
51
+
52
+ export const ChatbotHeaderDrawerWithExpandableGroupsDemo: FunctionComponent = () => {
53
+ const [isOpen, setIsOpen] = useState(true);
54
+ const [isSavedPromptsExpanded, setIsSavedPromptsExpanded] = useState(false);
55
+ const [isShowingAllChats, setIsShowingAllChats] = useState(false);
56
+
57
+ const conversations: ConversationGroup[] = [
58
+ {
59
+ id: 'pinned',
60
+ label: 'Pinned chats',
61
+ items: pinnedChats
62
+ },
63
+ {
64
+ id: 'chats',
65
+ label: 'Chats',
66
+ items: recentChats,
67
+ showAll: {
68
+ visibleCount: VISIBLE_CHAT_COUNT,
69
+ isExpanded: isShowingAllChats,
70
+ onToggle: setIsShowingAllChats,
71
+ label: isShowingAllChats ? 'Show less' : 'Show all'
72
+ }
73
+ },
74
+ {
75
+ id: 'saved-prompts',
76
+ label: 'Saved prompts',
77
+ expandable: {
78
+ isExpanded: isSavedPromptsExpanded,
79
+ onToggle: setIsSavedPromptsExpanded
80
+ },
81
+ items: savedPrompts
82
+ }
83
+ ];
84
+
85
+ return (
86
+ <>
87
+ <Checkbox
88
+ label="Display drawer"
89
+ isChecked={isOpen}
90
+ onChange={() => setIsOpen(!isOpen)}
91
+ id="expandable-groups-drawer-visible"
92
+ name="expandable-groups-drawer-visible"
93
+ />
94
+ <ChatbotConversationHistoryNav
95
+ displayMode={ChatbotDisplayMode.embedded}
96
+ onDrawerToggle={() => setIsOpen(!isOpen)}
97
+ isDrawerOpen={isOpen}
98
+ setIsDrawerOpen={setIsOpen}
99
+ conversations={conversations}
100
+ drawerContent={<div>Drawer content</div>}
101
+ />
102
+ </>
103
+ );
104
+ };
@@ -35,7 +35,8 @@ propComponents:
35
35
  'MessageBarWithAttachMenuProps',
36
36
  'SourceDetailsMenuItem',
37
37
  'ChatbotConversationHistoryNav',
38
- 'Conversation'
38
+ 'Conversation',
39
+ 'ConversationGroup'
39
40
  ]
40
41
  sortValue: 2
41
42
  ---
@@ -401,6 +402,16 @@ To help users track important conversations, add a "pin" option to the conversat
401
402
 
402
403
  ```
403
404
 
405
+ ### Grouped and expandable conversation history
406
+
407
+ For grouped chat history, pass a `ConversationGroup[]` to describe each section's label, items, and behavior. Each group's `items` array accepts `Conversation` objects or custom menu content such as a **Show all** action. Use `expandable` on a group to make that section expandable, and `footer` for content rendered after the group's menu list.
408
+
409
+ The object form `{ [groupLabel: string]: Conversation[] }` remains supported as shorthand for simple static groups.
410
+
411
+ ```js file="./ChatbotHeaderDrawerWithExpandableGroups.tsx"
412
+
413
+ ```
414
+
404
415
  ### Renaming conversations in chat history drawer
405
416
 
406
417
  You can allow users to rename a conversation in the chat history drawer by implementing a modal that opens upon clicking a "Rename" (or similar) action. When doing so, you must ensure the following:
@@ -60,12 +60,8 @@
60
60
  --pf-v6-c-menu__item-main--ColumnGap: var(--pf-t--global--spacer--md);
61
61
  }
62
62
 
63
- .pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title {
64
- color: var(--pf-t--global--text--color--subtle);
65
- font-weight: var(--pf-t--global--font--weight--body--bold);
66
- font-size: var(--pf-t--global--icon--size--font--sm);
67
- --pf-v6-c-menu__group-title--PaddingInlineStart: var(--pf-t--global--spacer--sm);
68
- --pf-v6-c-menu__group-title--PaddingInlineEnd: var(--pf-t--global--spacer--sm);
63
+ .pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title,
64
+ .pf-chatbot__menu-item-header--expandable .pf-chatbot__menu-group-toggle {
69
65
  position: -webkit-sticky;
70
66
  position: sticky;
71
67
  top: 0;
@@ -73,6 +69,46 @@
73
69
  z-index: var(--pf-t--global--z-index--md);
74
70
  }
75
71
 
72
+ .pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title,
73
+ .pf-chatbot__menu-item-header--expandable .pf-chatbot__menu-group-toggle > .pf-v6-c-expandable-section__toggle {
74
+ color: var(--pf-t--global--text--color--subtle);
75
+ font-weight: var(--pf-t--global--font--weight--body--bold);
76
+ font-size: var(--pf-t--global--icon--size--font--sm);
77
+ }
78
+
79
+ .pf-chatbot__menu-group-toggle {
80
+ padding-block-start: var(--pf-t--global--spacer--xs);
81
+ padding-block-end: var(--pf-t--global--spacer--xs);
82
+ }
83
+
84
+ .pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title {
85
+ --pf-v6-c-menu__group-title--PaddingInlineStart: var(--pf-t--global--spacer--sm);
86
+ --pf-v6-c-menu__group-title--PaddingInlineEnd: var(--pf-t--global--spacer--sm);
87
+ }
88
+
89
+ .pf-chatbot__menu-item-header--expandable .pf-chatbot__menu-group-toggle {
90
+ width: 100%;
91
+
92
+ .pf-v6-c-expandable-section__toggle {
93
+ margin: 0;
94
+ }
95
+
96
+ .pf-v6-c-button.pf-m-link {
97
+ --pf-v6-c-button--BorderRadius: var(--pf-t--global--border--radius--small);
98
+ --pf-v6-c-button--PaddingInlineStart: var(--pf-t--global--spacer--sm);
99
+ --pf-v6-c-button--PaddingInlineEnd: var(--pf-t--global--spacer--sm);
100
+ color: inherit;
101
+ font-weight: inherit;
102
+ font-size: inherit;
103
+ justify-content: flex-start;
104
+ width: 100%;
105
+ }
106
+
107
+ .pf-v6-c-expandable-section__toggle-icon {
108
+ color: var(--pf-t--global--text--color--subtle);
109
+ }
110
+ }
111
+
76
112
  .pf-chatbot__menu-item {
77
113
  --pf-v6-c-menu__item--PaddingInlineStart: var(--pf-t--global--spacer--sm);
78
114
  --pf-v6-c-menu__item--PaddingInlineEnd: var(--pf-t--global--spacer--sm);
@@ -86,6 +122,16 @@
86
122
  border-radius: var(--pf-t--global--border--radius--small);
87
123
  }
88
124
 
125
+ // "Show all" / "Show less" toggle
126
+ // Rendered as a regular MenuItem positioned after the (conditionally rendered)
127
+ // overflow items, so it participates in the menu's normal roving tabindex and
128
+ // arrow-key navigation, and stays in a fixed position at the bottom of the list.
129
+ .pf-chatbot__menu-show-all-toggle {
130
+ color: var(--pf-t--global--text--color--link--default);
131
+ font-size: var(--pf-t--global--font--size--body--default);
132
+ font-weight: var(--pf-t--global--font--weight--body--default);
133
+ }
134
+
89
135
  li.pf-chatbot__menu-item:hover::after {
90
136
  position: absolute;
91
137
  inset: 0;