Skip to content

Commit 4205f8e

Browse files
fix(various): IFD accessibility call-outs (#595)
Adjusted default aria-labels on history button, search box label, jump buttons. Jump buttons now also allow more customization via prop passage. The history drawer can also make announcements when content changes (demo included).
1 parent 82a0cc9 commit 4205f8e

File tree

10 files changed

+108
-22
lines changed

10 files changed

+108
-22
lines changed

packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, FunctionComponent } from 'react';
1+
import { useState, useEffect, useRef, FunctionComponent } from 'react';
22
import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
33
import ChatbotConversationHistoryNav, {
44
Conversation
@@ -71,8 +71,28 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => {
7171
const [hasError, setHasError] = useState(false);
7272
const [isEmpty, setIsEmpty] = useState(false);
7373
const [hasNoResults, setHasNoResults] = useState(false);
74+
const [announcement, setAnnouncement] = useState('');
75+
const [debouncedAnnouncement, setDebouncedAnnouncement] = useState('');
76+
const announcementTimeoutRef = useRef<NodeJS.Timeout>();
7477
const displayMode = ChatbotDisplayMode.embedded;
7578

79+
// Debounce announcement updates to prevent screen reader overload
80+
useEffect(() => {
81+
if (announcementTimeoutRef.current) {
82+
clearTimeout(announcementTimeoutRef.current);
83+
}
84+
85+
announcementTimeoutRef.current = setTimeout(() => {
86+
setDebouncedAnnouncement(announcement);
87+
}, 500);
88+
89+
return () => {
90+
if (announcementTimeoutRef.current) {
91+
clearTimeout(announcementTimeoutRef.current);
92+
}
93+
};
94+
}, [announcement]);
95+
7696
const findMatchingItems = (targetValue: string) => {
7797
const filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
7898
const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase()));
@@ -168,12 +188,23 @@ export const ChatbotHeaderTitleDemo: FunctionComponent = () => {
168188
handleTextInputChange={(value: string) => {
169189
if (value === '') {
170190
setConversations(initialConversations);
191+
setAnnouncement('');
192+
setDebouncedAnnouncement('');
193+
setHasNoResults(false);
194+
} else {
195+
// this is where you would perform search on the items in the drawer
196+
// and update the state
197+
const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value);
198+
const totalCount = Object.values(newConversations).flat().length;
199+
const newAnnouncement =
200+
totalCount === 1
201+
? `${totalCount} conversation matches "${value}"`
202+
: `${totalCount} conversations match "${value}"`;
203+
setAnnouncement(newAnnouncement);
204+
setConversations(newConversations);
171205
}
172-
// this is where you would perform search on the items in the drawer
173-
// and update the state
174-
const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value);
175-
setConversations(newConversations);
176206
}}
207+
searchInputScreenReaderText={debouncedAnnouncement}
177208
drawerContent={<div>Drawer content</div>}
178209
isLoading={isLoading}
179210
errorState={hasError ? ERROR : undefined}

packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22
// Chatbot Header - Menu
33
// ============================================================================
44
.pf-chatbot__history {
5+
// hide from view but not assistive technologies
6+
// https://css-tricks.com/inclusively-hidden/
7+
.pf-chatbot__filter-announcement {
8+
clip: rect(0 0 0 0);
9+
clip-path: inset(50%);
10+
height: 1px;
11+
overflow: hidden;
12+
position: absolute;
13+
white-space: nowrap;
14+
width: 1px;
15+
}
16+
517
.pf-chatbot__drawer-backdrop {
618
position: absolute;
719
border-radius: var(--pf-t--global--border--radius--medium);

packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ describe('ChatbotConversationHistoryNav', () => {
348348
).toBeTruthy();
349349
expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy();
350350
expect(screen.getByRole('button', { name: /Loading... Reload/i })).toBeTruthy();
351-
expect(screen.getByRole('textbox', { name: /Filter menu items/i })).toBeTruthy();
351+
expect(screen.getByRole('textbox', { name: /Search previous conversations/i })).toBeTruthy();
352352
expect(screen.getByRole('heading', { name: /Could not load chat history/i })).toBeTruthy();
353353
});
354354

@@ -372,7 +372,7 @@ describe('ChatbotConversationHistoryNav', () => {
372372
).toBeTruthy();
373373
expect(screen.getByRole('button', { name: /Close drawer panel/i })).toBeTruthy();
374374
expect(screen.queryByRole('button', { name: /Loading... Reload/i })).toBeFalsy();
375-
expect(screen.getByRole('textbox', { name: /Filter menu items/i })).toBeTruthy();
375+
expect(screen.getByRole('textbox', { name: /Search previous conversations/i })).toBeTruthy();
376376
expect(screen.getByRole('heading', { name: /Could not load chat history/i })).toBeTruthy();
377377
});
378378

packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
141141
navTitleIcon?: React.ReactNode;
142142
/** Title header level */
143143
navTitleProps?: Partial<TitleProps>;
144+
/** Visually hidden text that gets announced by assistive technologies. Should be used to convey the result count when the search input value changes. */
145+
searchInputScreenReaderText?: string;
144146
}
145147

146148
export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversationHistoryNavProps> = ({
@@ -157,7 +159,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
157159
onNewChat,
158160
newChatButtonProps,
159161
searchInputPlaceholder = 'Search previous conversations...',
160-
searchInputAriaLabel = 'Filter menu items',
162+
searchInputAriaLabel = 'Search previous conversations',
161163
searchInputProps,
162164
handleTextInputChange,
163165
displayMode,
@@ -179,6 +181,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
179181
title = 'Chat history',
180182
navTitleProps,
181183
navTitleIcon = <OutlinedClockIcon />,
184+
searchInputScreenReaderText,
182185
...props
183186
}: ChatbotConversationHistoryNavProps) => {
184187
const drawerRef = useRef<HTMLDivElement>(null);
@@ -309,6 +312,9 @@ export const ChatbotConversationHistoryNav: FunctionComponent<ChatbotConversatio
309312
placeholder={searchInputPlaceholder}
310313
{...searchInputProps}
311314
/>
315+
{searchInputScreenReaderText && (
316+
<div className="pf-chatbot__filter-announcement">{searchInputScreenReaderText}</div>
317+
)}
312318
</div>
313319
)}
314320
</div>

packages/module/src/ChatbotHeader/ChatbotHeaderMenu.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('ChatbotHeaderMenu', () => {
1212
it('should call onMenuToggle when ChatbotHeaderMenu button is clicked', () => {
1313
const onMenuToggle = jest.fn();
1414
render(<ChatbotHeaderMenu className="custom-header-menu" onMenuToggle={onMenuToggle} />);
15-
fireEvent.click(screen.getByRole('button', { name: 'Toggle menu' }));
15+
fireEvent.click(screen.getByRole('button', { name: 'Chat history menu' }));
1616

1717
expect(onMenuToggle).toHaveBeenCalled();
1818
});

packages/module/src/ChatbotHeader/ChatbotHeaderMenu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ const ChatbotHeaderMenuBase: FunctionComponent<ChatbotHeaderMenuProps> = ({
2525
className,
2626
onMenuToggle,
2727
tooltipProps,
28-
menuAriaLabel = 'Toggle menu',
28+
menuAriaLabel = 'Chat history menu',
2929
innerRef,
30-
tooltipContent = 'Menu',
30+
tooltipContent = 'Chat history menu',
3131
isCompact,
3232
...props
3333
}: ChatbotHeaderMenuProps) => (

packages/module/src/MessageBox/JumpButton.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ import userEvent from '@testing-library/user-event';
66
describe('JumpButton', () => {
77
it('should render top button correctly', () => {
88
render(<JumpButton position="top" onClick={jest.fn()} />);
9-
expect(screen.getByRole('button', { name: /Jump top/i })).toBeTruthy();
9+
expect(screen.getByRole('button', { name: /Back to top/i })).toBeTruthy();
1010
});
1111
it('should render bottom button correctly', () => {
1212
render(<JumpButton position="bottom" onClick={jest.fn()} />);
13-
expect(screen.getByRole('button', { name: /Jump bottom/i })).toBeTruthy();
13+
expect(screen.getByRole('button', { name: /Back to bottom/i })).toBeTruthy();
1414
});
1515
it('should call onClick appropriately', async () => {
1616
const spy = jest.fn();
1717
render(<JumpButton position="bottom" onClick={spy} />);
18-
await userEvent.click(screen.getByRole('button', { name: /Jump bottom/i }));
18+
await userEvent.click(screen.getByRole('button', { name: /Back to bottom/i }));
1919
expect(spy).toHaveBeenCalledTimes(1);
2020
});
2121
it('should be hidden if isHidden prop is used', async () => {
2222
render(<JumpButton position="bottom" onClick={jest.fn()} isHidden />);
23-
expect(screen.queryByRole('button', { name: /Jump bottom/i })).toBeFalsy();
23+
expect(screen.queryByRole('button', { name: /Back to bottom/i })).toBeFalsy();
2424
});
2525
});

packages/module/src/MessageBox/JumpButton.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import type { FunctionComponent } from 'react';
55

66
// Import PatternFly components
7-
import { Button, Tooltip, Icon } from '@patternfly/react-core';
7+
import { Button, Tooltip, Icon, TooltipProps, ButtonProps } from '@patternfly/react-core';
88

99
import { ArrowUpIcon } from '@patternfly/react-icons/dist/esm/icons/arrow-up-icon';
1010
import { ArrowDownIcon } from '@patternfly/react-icons/dist/esm/icons/arrow-down-icon';
@@ -16,16 +16,32 @@ export interface JumpButtonProps {
1616
onClick: () => void;
1717
/** Flag to change the visibilty of the button */
1818
isHidden?: boolean;
19+
/** Additional props passed to jump buttons */
20+
jumpButtonProps?: ButtonProps;
21+
/** Additional props passed to tooltip */
22+
jumpButtonTooltipProps?: TooltipProps;
1923
}
2024

21-
const JumpButton: FunctionComponent<JumpButtonProps> = ({ position, isHidden, onClick }: JumpButtonProps) =>
25+
const JumpButton: FunctionComponent<JumpButtonProps> = ({
26+
position,
27+
isHidden,
28+
onClick,
29+
jumpButtonProps,
30+
jumpButtonTooltipProps
31+
}: JumpButtonProps) =>
2232
isHidden ? null : (
23-
<Tooltip id={`pf-chatbot__tooltip--jump-${position}`} content={`Back to ${position}`} position="top">
33+
<Tooltip
34+
id={`pf-chatbot__tooltip--jump-${position}`}
35+
content={`Back to ${position}`}
36+
position="top"
37+
{...jumpButtonTooltipProps}
38+
>
2439
<Button
2540
variant="plain"
2641
className={`pf-chatbot__jump pf-chatbot__jump--${position}`}
27-
aria-label={`Jump ${position}`}
42+
aria-label={`Back to ${position}`}
2843
onClick={onClick}
44+
{...jumpButtonProps}
2945
>
3046
<Icon iconSize="lg" isInline>
3147
{position === 'top' ? <ArrowUpIcon /> : <ArrowDownIcon />}

packages/module/src/MessageBox/MessageBox.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('MessageBox', () => {
6161
});
6262

6363
await waitFor(() => {
64-
userEvent.click(screen.getByRole('button', { name: /Jump bottom/i }));
64+
userEvent.click(screen.getByRole('button', { name: /Back to bottom/i }));
6565
expect(spy).toHaveBeenCalled();
6666
});
6767
});
@@ -85,7 +85,7 @@ describe('MessageBox', () => {
8585
region.dispatchEvent(new Event('scroll'));
8686
});
8787
await waitFor(() => {
88-
userEvent.click(screen.getByRole('button', { name: /Jump top/i }));
88+
userEvent.click(screen.getByRole('button', { name: /Back to top/i }));
8989
expect(spy).toHaveBeenCalled();
9090
});
9191
});

packages/module/src/MessageBox/MessageBox.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
WheelEventHandler
1919
} from 'react';
2020
import JumpButton from './JumpButton';
21+
import { ButtonProps, TooltipProps } from '@patternfly/react-core';
2122

2223
export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
2324
/** Content that can be announced, such as a new message, for screen readers */
@@ -38,6 +39,14 @@ export interface MessageBoxProps extends HTMLProps<HTMLDivElement> {
3839
onScrollToBottomClick?: () => void;
3940
/** Flag to enable automatic scrolling when new messages are added */
4041
enableSmartScroll?: boolean;
42+
/** Props passed to top jump button */
43+
jumpButtonTopProps?: ButtonProps;
44+
/** Props passed to bottom jump button */
45+
jumpButtonBottomProps?: ButtonProps;
46+
/** Props passed to top jump button tooltip */
47+
jumpButtonTopTooltipProps?: TooltipProps;
48+
/** Props passed to top jump button tooltip */
49+
jumpButtonBottomTooltipProps?: TooltipProps;
4150
}
4251

4352
export interface MessageBoxHandle extends HTMLDivElement {
@@ -60,6 +69,10 @@ export const MessageBox = forwardRef(
6069
onScrollToTopClick,
6170
onScrollToBottomClick,
6271
enableSmartScroll = false,
72+
jumpButtonTopProps,
73+
jumpButtonBottomProps,
74+
jumpButtonBottomTooltipProps,
75+
jumpButtonTopTooltipProps,
6376
...props
6477
}: MessageBoxProps,
6578
ref: ForwardedRef<MessageBoxHandle | null>
@@ -305,7 +318,13 @@ export const MessageBox = forwardRef(
305318

306319
return (
307320
<>
308-
<JumpButton position="top" isHidden={isOverflowing && atTop} onClick={scrollToTop} />
321+
<JumpButton
322+
position="top"
323+
isHidden={isOverflowing && atTop}
324+
onClick={scrollToTop}
325+
jumpButtonProps={jumpButtonTopProps}
326+
jumpButtonTooltipProps={jumpButtonTopTooltipProps}
327+
/>
309328
<div
310329
role="region"
311330
tabIndex={0}
@@ -324,6 +343,8 @@ export const MessageBox = forwardRef(
324343
position="bottom"
325344
isHidden={isOverflowing && atBottom}
326345
onClick={() => scrollToBottom({ resumeSmartScroll: true })}
346+
jumpButtonProps={jumpButtonBottomProps}
347+
jumpButtonTooltipProps={jumpButtonBottomTooltipProps}
327348
/>
328349
</>
329350
);

0 commit comments

Comments
 (0)