Skip to content

Commit b156ff4

Browse files
committed
fix(JumpButton): Back to bottom a11y
Users should be able to tab to both buttons. Both should perform well in auto-scroll and normal usage. Assisted-by: Cursor
1 parent dba0d76 commit b156ff4

5 files changed

Lines changed: 259 additions & 62 deletions

File tree

packages/module/src/MessageBox/JumpButton.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
inset-block-end: var(--pf-t--global--spacer--md) !important;
4545
}
4646

47+
4748
// allows for zoom conditions; try zooming to 200% to see
4849
@media screen and (max-height: 518px) {
4950
display: none;

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,17 @@ describe('JumpButton', () => {
1818
await userEvent.click(screen.getByRole('button', { name: /Back to bottom/i }));
1919
expect(spy).toHaveBeenCalledTimes(1);
2020
});
21-
it('should be hidden if isHidden prop is used', async () => {
21+
it('should remain in the DOM but visually hidden when isHidden is used', () => {
2222
render(<JumpButton position="bottom" onClick={jest.fn()} isHidden />);
23-
expect(screen.queryByRole('button', { name: /Back to bottom/i })).toBeFalsy();
23+
const button = screen.getByRole('button', { name: /Back to bottom/i });
24+
expect(button).toHaveClass('pf-chatbot__jump--hidden');
25+
});
26+
27+
it('should become visible when focused while hidden', () => {
28+
render(<JumpButton position="bottom" onClick={jest.fn()} isHidden />);
29+
const button = screen.getByRole('button', { name: /Back to bottom/i });
30+
button.focus();
31+
expect(button).toHaveFocus();
32+
expect(button).toHaveClass('pf-chatbot__jump--hidden');
2433
});
2534
});

packages/module/src/MessageBox/JumpButton.tsx

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,25 @@ const JumpButton: FunctionComponent<JumpButtonProps> = ({
2828
onClick,
2929
jumpButtonProps,
3030
jumpButtonTooltipProps
31-
}: JumpButtonProps) =>
32-
isHidden ? null : (
33-
<Tooltip
34-
id={`pf-chatbot__tooltip--jump-${position}`}
35-
content={`Back to ${position}`}
36-
position="top"
37-
{...jumpButtonTooltipProps}
31+
}: JumpButtonProps) => (
32+
<Tooltip
33+
id={`pf-chatbot__tooltip--jump-${position}`}
34+
content={`Back to ${position}`}
35+
position="top"
36+
{...jumpButtonTooltipProps}
37+
>
38+
<Button
39+
variant="plain"
40+
className={`pf-chatbot__jump pf-chatbot__jump--${position} ${isHidden ? 'pf-chatbot-m-hidden' : ''}`}
41+
aria-label={`Back to ${position}`}
42+
onClick={onClick}
43+
{...jumpButtonProps}
3844
>
39-
<Button
40-
variant="plain"
41-
className={`pf-chatbot__jump pf-chatbot__jump--${position}`}
42-
aria-label={`Back to ${position}`}
43-
onClick={onClick}
44-
{...jumpButtonProps}
45-
>
46-
<Icon iconSize="lg" isInline>
47-
{position === 'top' ? <ArrowUpIcon /> : <ArrowDownIcon />}
48-
</Icon>
49-
</Button>
50-
</Tooltip>
51-
);
45+
<Icon iconSize="lg" isInline>
46+
{position === 'top' ? <ArrowUpIcon /> : <ArrowDownIcon />}
47+
</Icon>
48+
</Button>
49+
</Tooltip>
50+
);
5251

5352
export default JumpButton;

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createRef } from 'react';
22
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
34
import { MessageBox, MessageBoxHandle } from './MessageBox';
45
import userEvent from '@testing-library/user-event';
56

@@ -313,4 +314,94 @@ describe('MessageBox', () => {
313314

314315
expect(ref.current?.isSmartScrollActive()).toBe(true);
315316
});
317+
318+
it('should keep auto-scroll active during programmatic scroll to bottom', async () => {
319+
const ref = createRef<MessageBoxHandle>();
320+
render(
321+
<MessageBox ref={ref} enableSmartScroll>
322+
<div>Test message content</div>
323+
</MessageBox>
324+
);
325+
326+
const element = ref.current!;
327+
Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
328+
Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
329+
Object.defineProperty(element, 'scrollTop', { configurable: true, value: 0, writable: true });
330+
331+
act(() => {
332+
ref.current?.scrollToBottom({ behavior: 'smooth' });
333+
});
334+
335+
expect(ref.current?.isSmartScrollActive()).toBe(true);
336+
337+
act(() => {
338+
element.scrollTop = 500;
339+
element.dispatchEvent(new Event('scroll', { bubbles: true }));
340+
});
341+
342+
expect(ref.current?.isSmartScrollActive()).toBe(true);
343+
});
344+
345+
it('should not scroll to bottom on initial layout when the message box is at the top', () => {
346+
render(
347+
<MessageBox enableSmartScroll>
348+
<div>Tall message content</div>
349+
</MessageBox>
350+
);
351+
352+
const region = screen.getByRole('region');
353+
Object.defineProperty(region, 'scrollHeight', { configurable: true, value: 1000 });
354+
Object.defineProperty(region, 'clientHeight', { configurable: true, value: 300 });
355+
Object.defineProperty(region, 'scrollTop', { configurable: true, value: 0, writable: true });
356+
357+
act(() => {
358+
region.dispatchEvent(new Event('scroll'));
359+
});
360+
361+
expect(region.scrollTop).toBe(0);
362+
});
363+
364+
it('should show the bottom jump button at the top on initial load when overflowing', async () => {
365+
render(
366+
<MessageBox enableSmartScroll>
367+
<div>Tall message content</div>
368+
</MessageBox>
369+
);
370+
371+
const region = screen.getByRole('region');
372+
Object.defineProperty(region, 'scrollHeight', { configurable: true, value: 1000 });
373+
Object.defineProperty(region, 'clientHeight', { configurable: true, value: 300 });
374+
Object.defineProperty(region, 'scrollTop', { configurable: true, value: 0, writable: true });
375+
376+
act(() => {
377+
region.dispatchEvent(new Event('scroll'));
378+
});
379+
380+
await waitFor(() => {
381+
expect(screen.getByRole('button', { name: /Back to top/i })).toHaveClass('pf-chatbot__jump--hidden');
382+
expect(screen.getByRole('button', { name: /Back to bottom/i })).not.toHaveClass('pf-chatbot__jump--hidden');
383+
});
384+
});
385+
386+
it('should hide the bottom jump button while smart scroll is actively following content', async () => {
387+
render(
388+
<MessageBox enableSmartScroll>
389+
<div>Test message content</div>
390+
</MessageBox>
391+
);
392+
393+
const region = screen.getByRole('region');
394+
Object.defineProperty(region, 'scrollHeight', { configurable: true, value: 1000 });
395+
Object.defineProperty(region, 'clientHeight', { configurable: true, value: 300 });
396+
Object.defineProperty(region, 'scrollTop', { configurable: true, value: 640, writable: true });
397+
398+
act(() => {
399+
region.dispatchEvent(new Event('scroll'));
400+
});
401+
402+
await waitFor(() => {
403+
expect(screen.getByRole('button', { name: /Back to top/i })).not.toHaveClass('pf-chatbot__jump--hidden');
404+
expect(screen.getByRole('button', { name: /Back to bottom/i })).toHaveClass('pf-chatbot__jump--hidden');
405+
});
406+
});
316407
});

0 commit comments

Comments
 (0)