Skip to content

Commit 5183daa

Browse files
authored
feat(a11y): Add live region to announce contextual bar search results (RocketChat#39847)
1 parent d12b53c commit 5183daa

File tree

22 files changed

+1811
-323
lines changed

22 files changed

+1811
-323
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { VisuallyHidden } from 'react-aria';
2+
import { useTranslation } from 'react-i18next';
3+
4+
const ResultsLiveRegion = ({ shouldAnnounce, itemCount }: { shouldAnnounce: boolean; itemCount: number }) => {
5+
const { t } = useTranslation();
6+
7+
if (itemCount === 0) {
8+
return <VisuallyHidden role='status'>{shouldAnnounce && t('No_results_found')}</VisuallyHidden>;
9+
}
10+
11+
return <VisuallyHidden role='status'>{shouldAnnounce && t('__count__result_found', { count: itemCount })}</VisuallyHidden>;
12+
};
13+
14+
export default ResultsLiveRegion;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { composeStories } from '@storybook/react';
2+
import { render } from '@testing-library/react';
3+
import { axe } from 'jest-axe';
4+
5+
import * as stories from './DiscussionsList.stories';
6+
7+
jest.mock('../../../../lib/rooms/roomCoordinator', () => ({
8+
roomCoordinator: {
9+
getRoomDirectives: jest.fn(() => ({})),
10+
},
11+
}));
12+
13+
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
14+
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
15+
const { baseElement } = render(<Story />);
16+
expect(baseElement).toMatchSnapshot();
17+
});
18+
19+
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
20+
const { container } = render(<Story />);
21+
22+
const results = await axe(container);
23+
expect(results).toHaveNoViolations();
24+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Contextualbar } from '@rocket.chat/ui-client';
2+
import { action } from '@storybook/addon-actions';
3+
import type { Meta, StoryFn } from '@storybook/react';
4+
5+
import DiscussionsList from './DiscussionsList';
6+
7+
export default {
8+
component: DiscussionsList,
9+
parameters: {
10+
layout: 'fullscreen',
11+
actions: { argTypesRegex: '^on.*' },
12+
},
13+
decorators: [(fn) => <Contextualbar height='100vh'>{fn()}</Contextualbar>],
14+
args: {
15+
text: '',
16+
loadMoreItems: action('loadMoreItems'),
17+
},
18+
} satisfies Meta<typeof DiscussionsList>;
19+
20+
const Template: StoryFn<typeof DiscussionsList> = (args) => <DiscussionsList {...args} />;
21+
22+
const fakeDiscussions = Array.from({ length: 10 }, (_, i) => ({
23+
_id: String(i),
24+
msg: `Discussion ${i}`,
25+
ts: new Date('2024-01-01T00:00:00Z'),
26+
username: 'user.name',
27+
dcount: 5,
28+
dlm: new Date('2024-01-01T00:00:00Z'),
29+
drid: `drid-${i}`,
30+
rid: 'roomId',
31+
_updatedAt: new Date('2024-01-01T00:00:00Z'),
32+
u: {
33+
_id: 'user-id',
34+
username: 'user.name',
35+
},
36+
}));
37+
38+
export const Default = Template.bind({});
39+
Default.args = {
40+
isSuccess: true,
41+
discussions: fakeDiscussions,
42+
itemCount: fakeDiscussions.length,
43+
};
44+
45+
export const Loading = Template.bind({});
46+
Loading.args = {
47+
isPending: true,
48+
};
49+
50+
export const Empty = Template.bind({});
51+
Empty.args = {
52+
isSuccess: true,
53+
discussions: [],
54+
itemCount: 0,
55+
};

apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,40 @@ import {
1414
} from '@rocket.chat/ui-client';
1515
import { useSetting } from '@rocket.chat/ui-contexts';
1616
import type { ChangeEvent, MouseEvent, RefObject } from 'react';
17-
import { useCallback } from 'react';
17+
import { useCallback, useId } from 'react';
1818
import { useTranslation } from 'react-i18next';
1919
import { Virtuoso } from 'react-virtuoso';
2020

2121
import DiscussionsListRow from './DiscussionsListRow';
22+
import ResultsLiveRegion from '../../../../components/ResultsLiveRegion';
2223
import { useGoToRoom } from '../../hooks/useGoToRoom';
2324

2425
type DiscussionsListProps = {
25-
total: number;
26+
itemCount: number;
2627
discussions: Array<IDiscussionMessage>;
2728
loadMoreItems: (start: number, end: number) => void;
28-
loading: boolean;
29+
isPending: boolean;
30+
isSuccess: boolean;
2931
onClose: () => void;
3032
error: unknown;
3133
text: string;
3234
onChangeFilter: (e: ChangeEvent<HTMLInputElement>) => void;
3335
};
3436

3537
function DiscussionsList({
36-
total = 10,
38+
itemCount,
3739
discussions = [],
3840
loadMoreItems,
39-
loading,
41+
isPending,
42+
isSuccess,
4043
onClose,
4144
error,
4245
text,
4346
onChangeFilter,
4447
}: DiscussionsListProps) {
4548
const { t } = useTranslation();
49+
const discussionListId = useId();
50+
4651
const showRealNames = useSetting('UI_Use_Real_Name', false);
4752
const inputRef = useAutoFocus(true);
4853

@@ -70,44 +75,46 @@ function DiscussionsList({
7075
<ContextualbarSection>
7176
<TextInput
7277
placeholder={t('Search_Messages')}
78+
aria-label={t('Search_Messages')}
79+
aria-controls={isSuccess ? discussionListId : undefined}
7380
value={text}
7481
onChange={onChangeFilter}
7582
ref={inputRef as RefObject<HTMLInputElement>}
7683
addon={<Icon name='magnifier' size='x20' />}
7784
/>
7885
</ContextualbarSection>
7986
<ContextualbarContent paddingInline={0} ref={ref}>
80-
{loading && (
87+
<ResultsLiveRegion shouldAnnounce={isSuccess} itemCount={itemCount} />
88+
{isPending && (
8189
<Box pi={24} pb={12}>
8290
<Throbber size='x12' />
8391
</Box>
8492
)}
85-
8693
{error instanceof Error && (
8794
<Callout mi={24} type='danger'>
8895
{error.toString()}
8996
</Callout>
9097
)}
91-
92-
{!loading && total === 0 && <ContextualbarEmptyContent title={t('No_Discussions_found')} />}
93-
94-
<Box flexGrow={1} flexShrink={1} overflow='hidden' display='flex'>
95-
{!error && total > 0 && discussions.length > 0 && (
96-
<VirtualizedScrollbars>
97-
<Virtuoso
98-
style={{
99-
height: blockSize,
100-
width: inlineSize,
101-
}}
102-
totalCount={total}
103-
endReached={loading ? () => undefined : (start) => loadMoreItems(start, Math.min(50, total - start))}
104-
overscan={25}
105-
data={discussions}
106-
itemContent={(_, data) => <DiscussionsListRow discussion={data} showRealNames={showRealNames} onClick={onClick} />}
107-
/>
108-
</VirtualizedScrollbars>
109-
)}
110-
</Box>
98+
{isSuccess && (
99+
<Box id={discussionListId} w='full' h='full' overflow='hidden' flexShrink={1}>
100+
{discussions.length === 0 && <ContextualbarEmptyContent title={t('No_Discussions_found')} />}
101+
{discussions.length > 0 && (
102+
<VirtualizedScrollbars>
103+
<Virtuoso
104+
style={{
105+
height: blockSize,
106+
width: inlineSize,
107+
}}
108+
totalCount={itemCount}
109+
endReached={isPending ? () => undefined : (start) => loadMoreItems(start, Math.min(50, itemCount - start))}
110+
overscan={25}
111+
data={discussions}
112+
itemContent={(_, data) => <DiscussionsListRow discussion={data} showRealNames={showRealNames} onClick={onClick} />}
113+
/>
114+
</VirtualizedScrollbars>
115+
)}
116+
</Box>
117+
)}
111118
</ContextualbarContent>
112119
</ContextualbarDialog>
113120
);

apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsListContextBar.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ const DiscussionListContextBar = () => {
2323
[room._id, debouncedText],
2424
);
2525

26-
const { isPending, error, data, fetchNextPage } = useDiscussionsList(options);
26+
const { isPending, isSuccess, error, data, fetchNextPage } = useDiscussionsList(options);
2727

2828
const discussions = data?.items || [];
29-
const totalItemCount = data?.itemCount ?? 0;
29+
const itemCount = data?.itemCount ?? 0;
3030

3131
const handleTextChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
3232
setText(e.currentTarget.value);
@@ -41,8 +41,9 @@ const DiscussionListContextBar = () => {
4141
onClose={closeTab}
4242
error={error}
4343
discussions={discussions}
44-
total={totalItemCount}
45-
loading={isPending}
44+
itemCount={itemCount}
45+
isPending={isPending}
46+
isSuccess={isSuccess}
4647
loadMoreItems={() => fetchNextPage()}
4748
text={text}
4849
onChangeFilter={handleTextChange}

0 commit comments

Comments
 (0)