From 5c42ceba97c579cc5d1888186c73e4224404d990 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Fri, 11 Jul 2025 11:28:54 -0400 Subject: [PATCH 1/3] fix(ChatbotConversationHistoryNav,JumpButton,ChatbotHeaderMenu): Various a11y fixes Adjusted default text to match visible text and provide more context. Jump buttons now also allow more customization via prop passage. The history drawer can also make announcements when content changes. --- .../examples/UI/ChatbotHeaderDrawer.tsx | 18 ++++++++++--- .../ChatbotConversationHistoryNav.scss | 12 +++++++++ .../ChatbotConversationHistoryNav.test.tsx | 4 +-- .../ChatbotConversationHistoryNav.tsx | 10 +++++++- .../ChatbotHeader/ChatbotHeaderMenu.test.tsx | 2 +- .../src/ChatbotHeader/ChatbotHeaderMenu.tsx | 4 +-- .../module/src/MessageBox/JumpButton.test.tsx | 8 +++--- packages/module/src/MessageBox/JumpButton.tsx | 25 ++++++++++++++++--- .../module/src/MessageBox/MessageBox.test.tsx | 4 +-- packages/module/src/MessageBox/MessageBox.tsx | 23 ++++++++++++++++- 10 files changed, 89 insertions(+), 21 deletions(-) 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..ce63a4e0e 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 @@ -71,6 +71,7 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { const [hasError, setHasError] = useState(false); const [isEmpty, setIsEmpty] = useState(false); const [hasNoResults, setHasNoResults] = useState(false); + const [announcement, setAnnouncement] = useState(''); const displayMode = ChatbotDisplayMode.embedded; const findMatchingItems = (targetValue: string) => { @@ -168,12 +169,21 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => { handleTextInputChange={(value: string) => { if (value === '') { setConversations(initialConversations); + setAnnouncement(''); + } 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); }} + announcement={announcement} 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..f71247210 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..289e328bb 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; + /** Announcement text to be read by screen readers when search results change */ + announcement?: string; } export const ChatbotConversationHistoryNav: FunctionComponent = ({ @@ -157,7 +159,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent, + announcement, ...props }: ChatbotConversationHistoryNavProps) => { const drawerRef = useRef(null); @@ -322,6 +325,11 @@ export const ChatbotConversationHistoryNav: FunctionComponent + {announcement && ( +
+ {announcement} +
+ )} {drawer} ); 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..0b7831615 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,33 @@ 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 : ( - +