Skip to content

Commit 2c5fd54

Browse files
fix(JumpButton): Back to bottom a11y (#865)
Users should be able to tab to both buttons. Both should perform well in auto-scroll and normal usage. Assisted-by: Cursor
1 parent 9b2c004 commit 2c5fd54

4 files changed

Lines changed: 230 additions & 42 deletions

File tree

.github/workflows/build-lint-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,6 @@ jobs:
156156
- name: Build docs
157157
run: npm run build:docs
158158
- name: Install Chrome for Puppeteer
159-
run: npx puppeteer browsers install chrome
159+
run: npm exec -w packages/module -- puppeteer browsers install chrome
160160
- name: A11y tests
161161
run: npm run serve:docs & npm run test:a11y

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ 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 be hidden if isHidden prop is used', () => {
2222
render(<JumpButton position="bottom" onClick={jest.fn()} isHidden />);
2323
expect(screen.queryByRole('button', { name: /Back to bottom/i })).toBeFalsy();
2424
});

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.queryByRole('button', { name: /Back to top/i })).toBeFalsy();
382+
expect(screen.getByRole('button', { name: /Back to bottom/i })).toBeTruthy();
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 })).toBeTruthy();
404+
expect(screen.queryByRole('button', { name: /Back to bottom/i })).toBeFalsy();
405+
});
406+
});
316407
});

0 commit comments

Comments
 (0)