diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx index d85293dd5..dfae47e15 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx @@ -1,4 +1,4 @@ -import { useState, FunctionComponent } from 'react'; +import { useState, useEffect, useRef, FunctionComponent } from 'react'; import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; import ChatbotConversationHistoryNav, { Conversation @@ -71,8 +71,28 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { const [hasError, setHasError] = useState(false); const [isEmpty, setIsEmpty] = useState(false); const [hasNoResults, setHasNoResults] = useState(false); + const [announcement, setAnnouncement] = useState(''); + const [debouncedAnnouncement, setDebouncedAnnouncement] = useState(''); + const announcementTimeoutRef = useRef(); const displayMode = ChatbotDisplayMode.embedded; + // Debounce announcement updates to prevent screen reader overload + useEffect(() => { + if (announcementTimeoutRef.current) { + clearTimeout(announcementTimeoutRef.current); + } + + announcementTimeoutRef.current = setTimeout(() => { + setDebouncedAnnouncement(announcement); + }, 500); + + return () => { + if (announcementTimeoutRef.current) { + clearTimeout(announcementTimeoutRef.current); + } + }; + }, [announcement]); + const findMatchingItems = (targetValue: string) => { const filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => { const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase())); @@ -168,12 +188,23 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { handleTextInputChange={(value: string) => { if (value === '') { setConversations(initialConversations); + setAnnouncement(''); + setDebouncedAnnouncement(''); + setHasNoResults(false); + } else { + // this is where you would perform search on the items in the drawer + // and update the state + const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value); + const totalCount = Object.values(newConversations).flat().length; + const newAnnouncement = + totalCount === 1 + ? `${totalCount} conversation matches "${value}"` + : `${totalCount} conversations match "${value}"`; + setAnnouncement(newAnnouncement); + setConversations(newConversations); } - // this is where you would perform search on the items in the drawer - // and update the state - const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value); - setConversations(newConversations); }} + searchInputScreenReaderText={debouncedAnnouncement} drawerContent={
Drawer content
} isLoading={isLoading} errorState={hasError ? ERROR : undefined} diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss index ee336860c..510908960 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss @@ -2,6 +2,18 @@ // Chatbot Header - Menu // ============================================================================ .pf-chatbot__history { + // hide from view but not assistive technologies + // https://css-tricks.com/inclusively-hidden/ + .pf-chatbot__filter-announcement { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; + } + .pf-chatbot__drawer-backdrop { position: absolute; border-radius: var(--pf-t--global--border--radius--medium); diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx index 693f26301..db19f3d49 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx @@ -348,7 +348,7 @@ describe('ChatbotConversationHistoryNav', () => { ).toBeTruthy(); expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy(); expect(screen.getByRole('button', { name: /Loading... Reload/i })).toBeTruthy(); - expect(screen.getByRole('textbox', { name: /Filter menu items/i })).toBeTruthy(); + expect(screen.getByRole('textbox', { name: /Search previous conversations/i })).toBeTruthy(); expect(screen.getByRole('heading', { name: /Could not load chat history/i })).toBeTruthy(); }); @@ -372,7 +372,7 @@ describe('ChatbotConversationHistoryNav', () => { ).toBeTruthy(); expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy(); expect(screen.queryByRole('button', { name: /Loading... Reload/i })).toBeFalsy(); - expect(screen.getByRole('textbox', { name: /Filter menu items/i })).toBeTruthy(); + expect(screen.getByRole('textbox', { name: /Search previous conversations/i })).toBeTruthy(); expect(screen.getByRole('heading', { name: /Could not load chat history/i })).toBeTruthy(); }); diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx index be1d4b41d..cd8dbab41 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx @@ -141,6 +141,8 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps { navTitleIcon?: React.ReactNode; /** Title header level */ navTitleProps?: Partial; + /** Visually hidden text that gets announced by assistive technologies. Should be used to convey the result count when the search input value changes. */ + searchInputScreenReaderText?: string; } export const ChatbotConversationHistoryNav: FunctionComponent = ({ @@ -157,7 +159,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent, + searchInputScreenReaderText, ...props }: ChatbotConversationHistoryNavProps) => { const drawerRef = useRef(null); @@ -309,6 +312,9 @@ export const ChatbotConversationHistoryNav: FunctionComponent + {searchInputScreenReaderText && ( +
{searchInputScreenReaderText}
+ )} )} diff --git a/packages/module/src/ChatbotHeader/ChatbotHeaderMenu.test.tsx b/packages/module/src/ChatbotHeader/ChatbotHeaderMenu.test.tsx index b2650a71f..fbe3e11d9 100644 --- a/packages/module/src/ChatbotHeader/ChatbotHeaderMenu.test.tsx +++ b/packages/module/src/ChatbotHeader/ChatbotHeaderMenu.test.tsx @@ -12,7 +12,7 @@ describe('ChatbotHeaderMenu', () => { it('should call onMenuToggle when ChatbotHeaderMenu button is clicked', () => { const onMenuToggle = jest.fn(); render(); - fireEvent.click(screen.getByRole('button', { name: 'Toggle menu' })); + fireEvent.click(screen.getByRole('button', { name: 'Chat history menu' })); expect(onMenuToggle).toHaveBeenCalled(); }); diff --git a/packages/module/src/ChatbotHeader/ChatbotHeaderMenu.tsx b/packages/module/src/ChatbotHeader/ChatbotHeaderMenu.tsx index 2739407ed..257a1cee4 100644 --- a/packages/module/src/ChatbotHeader/ChatbotHeaderMenu.tsx +++ b/packages/module/src/ChatbotHeader/ChatbotHeaderMenu.tsx @@ -25,9 +25,9 @@ const ChatbotHeaderMenuBase: FunctionComponent = ({ className, onMenuToggle, tooltipProps, - menuAriaLabel = 'Toggle menu', + menuAriaLabel = 'Chat history menu', innerRef, - tooltipContent = 'Menu', + tooltipContent = 'Chat history menu', isCompact, ...props }: ChatbotHeaderMenuProps) => ( diff --git a/packages/module/src/MessageBox/JumpButton.test.tsx b/packages/module/src/MessageBox/JumpButton.test.tsx index 50355cbb6..947ec0fed 100644 --- a/packages/module/src/MessageBox/JumpButton.test.tsx +++ b/packages/module/src/MessageBox/JumpButton.test.tsx @@ -6,20 +6,20 @@ import userEvent from '@testing-library/user-event'; describe('JumpButton', () => { it('should render top button correctly', () => { render(); - expect(screen.getByRole('button', { name: /Jump top/i })).toBeTruthy(); + expect(screen.getByRole('button', { name: /Back to top/i })).toBeTruthy(); }); it('should render bottom button correctly', () => { render(); - expect(screen.getByRole('button', { name: /Jump bottom/i })).toBeTruthy(); + expect(screen.getByRole('button', { name: /Back to bottom/i })).toBeTruthy(); }); it('should call onClick appropriately', async () => { const spy = jest.fn(); render(); - await userEvent.click(screen.getByRole('button', { name: /Jump bottom/i })); + await userEvent.click(screen.getByRole('button', { name: /Back to bottom/i })); expect(spy).toHaveBeenCalledTimes(1); }); it('should be hidden if isHidden prop is used', async () => { render(); - expect(screen.queryByRole('button', { name: /Jump bottom/i })).toBeFalsy(); + expect(screen.queryByRole('button', { name: /Back to bottom/i })).toBeFalsy(); }); }); diff --git a/packages/module/src/MessageBox/JumpButton.tsx b/packages/module/src/MessageBox/JumpButton.tsx index 00e5e2de6..c4f6e4875 100644 --- a/packages/module/src/MessageBox/JumpButton.tsx +++ b/packages/module/src/MessageBox/JumpButton.tsx @@ -4,7 +4,7 @@ import type { FunctionComponent } from 'react'; // Import PatternFly components -import { Button, Tooltip, Icon } from '@patternfly/react-core'; +import { Button, Tooltip, Icon, TooltipProps, ButtonProps } from '@patternfly/react-core'; import { ArrowUpIcon } from '@patternfly/react-icons/dist/esm/icons/arrow-up-icon'; import { ArrowDownIcon } from '@patternfly/react-icons/dist/esm/icons/arrow-down-icon'; @@ -16,16 +16,32 @@ export interface JumpButtonProps { onClick: () => void; /** Flag to change the visibilty of the button */ isHidden?: boolean; + /** Additional props passed to jump buttons */ + jumpButtonProps?: ButtonProps; + /** Additional props passed to tooltip */ + jumpButtonTooltipProps?: TooltipProps; } -const JumpButton: FunctionComponent = ({ position, isHidden, onClick }: JumpButtonProps) => +const JumpButton: FunctionComponent = ({ + position, + isHidden, + onClick, + jumpButtonProps, + jumpButtonTooltipProps +}: JumpButtonProps) => isHidden ? null : ( - +