diff --git a/apps/meteor/client/components/ResultsLiveRegion.tsx b/apps/meteor/client/components/ResultsLiveRegion.tsx new file mode 100644 index 0000000000000..2697575b76494 --- /dev/null +++ b/apps/meteor/client/components/ResultsLiveRegion.tsx @@ -0,0 +1,14 @@ +import { VisuallyHidden } from 'react-aria'; +import { useTranslation } from 'react-i18next'; + +const ResultsLiveRegion = ({ shouldAnnounce, itemCount }: { shouldAnnounce: boolean; itemCount: number }) => { + const { t } = useTranslation(); + + if (itemCount === 0) { + return {shouldAnnounce && t('No_results_found')}; + } + + return {shouldAnnounce && t('__count__result_found', { count: itemCount })}; +}; + +export default ResultsLiveRegion; diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.spec.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.spec.tsx new file mode 100644 index 0000000000000..95f4b9bb6db3d --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.spec.tsx @@ -0,0 +1,24 @@ +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './DiscussionsList.stories'; + +jest.mock('../../../../lib/rooms/roomCoordinator', () => ({ + roomCoordinator: { + getRoomDirectives: jest.fn(() => ({})), + }, +})); + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const { baseElement } = render(); + expect(baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(); + + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.stories.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.stories.tsx new file mode 100644 index 0000000000000..df2d0be0d1e01 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.stories.tsx @@ -0,0 +1,55 @@ +import { Contextualbar } from '@rocket.chat/ui-client'; +import { action } from '@storybook/addon-actions'; +import type { Meta, StoryFn } from '@storybook/react'; + +import DiscussionsList from './DiscussionsList'; + +export default { + component: DiscussionsList, + parameters: { + layout: 'fullscreen', + actions: { argTypesRegex: '^on.*' }, + }, + decorators: [(fn) => {fn()}], + args: { + text: '', + loadMoreItems: action('loadMoreItems'), + }, +} satisfies Meta; + +const Template: StoryFn = (args) => ; + +const fakeDiscussions = Array.from({ length: 10 }, (_, i) => ({ + _id: String(i), + msg: `Discussion ${i}`, + ts: new Date('2024-01-01T00:00:00Z'), + username: 'user.name', + dcount: 5, + dlm: new Date('2024-01-01T00:00:00Z'), + drid: `drid-${i}`, + rid: 'roomId', + _updatedAt: new Date('2024-01-01T00:00:00Z'), + u: { + _id: 'user-id', + username: 'user.name', + }, +})); + +export const Default = Template.bind({}); +Default.args = { + isSuccess: true, + discussions: fakeDiscussions, + itemCount: fakeDiscussions.length, +}; + +export const Loading = Template.bind({}); +Loading.args = { + isPending: true, +}; + +export const Empty = Template.bind({}); +Empty.args = { + isSuccess: true, + discussions: [], + itemCount: 0, +}; diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx index 0626a532d87c1..a34b4d015d3b9 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx @@ -14,18 +14,20 @@ import { } from '@rocket.chat/ui-client'; import { useSetting } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, MouseEvent, RefObject } from 'react'; -import { useCallback } from 'react'; +import { useCallback, useId } from 'react'; import { useTranslation } from 'react-i18next'; import { Virtuoso } from 'react-virtuoso'; import DiscussionsListRow from './DiscussionsListRow'; +import ResultsLiveRegion from '../../../../components/ResultsLiveRegion'; import { useGoToRoom } from '../../hooks/useGoToRoom'; type DiscussionsListProps = { - total: number; + itemCount: number; discussions: Array; loadMoreItems: (start: number, end: number) => void; - loading: boolean; + isPending: boolean; + isSuccess: boolean; onClose: () => void; error: unknown; text: string; @@ -33,16 +35,19 @@ type DiscussionsListProps = { }; function DiscussionsList({ - total = 10, + itemCount, discussions = [], loadMoreItems, - loading, + isPending, + isSuccess, onClose, error, text, onChangeFilter, }: DiscussionsListProps) { const { t } = useTranslation(); + const discussionListId = useId(); + const showRealNames = useSetting('UI_Use_Real_Name', false); const inputRef = useAutoFocus(true); @@ -70,6 +75,8 @@ function DiscussionsList({ } @@ -77,37 +84,37 @@ function DiscussionsList({ /> - {loading && ( + + {isPending && ( )} - {error instanceof Error && ( {error.toString()} )} - - {!loading && total === 0 && } - - - {!error && total > 0 && discussions.length > 0 && ( - - undefined : (start) => loadMoreItems(start, Math.min(50, total - start))} - overscan={25} - data={discussions} - itemContent={(_, data) => } - /> - - )} - + {isSuccess && ( + + {discussions.length === 0 && } + {discussions.length > 0 && ( + + undefined : (start) => loadMoreItems(start, Math.min(50, itemCount - start))} + overscan={25} + data={discussions} + itemContent={(_, data) => } + /> + + )} + + )} ); diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsListContextBar.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsListContextBar.tsx index 76682bbe6b131..23ae4d6081573 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsListContextBar.tsx +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsListContextBar.tsx @@ -23,10 +23,10 @@ const DiscussionListContextBar = () => { [room._id, debouncedText], ); - const { isPending, error, data, fetchNextPage } = useDiscussionsList(options); + const { isPending, isSuccess, error, data, fetchNextPage } = useDiscussionsList(options); const discussions = data?.items || []; - const totalItemCount = data?.itemCount ?? 0; + const itemCount = data?.itemCount ?? 0; const handleTextChange = useCallback((e: ChangeEvent) => { setText(e.currentTarget.value); @@ -41,8 +41,9 @@ const DiscussionListContextBar = () => { onClose={closeTab} error={error} discussions={discussions} - total={totalItemCount} - loading={isPending} + itemCount={itemCount} + isPending={isPending} + isSuccess={isSuccess} loadMoreItems={() => fetchNextPage()} text={text} onChangeFilter={handleTextChange} diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/__snapshots__/DiscussionsList.spec.tsx.snap b/apps/meteor/client/views/room/contextualBar/Discussions/__snapshots__/DiscussionsList.spec.tsx.snap new file mode 100644 index 0000000000000..a8381c4465d03 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Discussions/__snapshots__/DiscussionsList.spec.tsx.snap @@ -0,0 +1,478 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`renders Default without crashing 1`] = ` + +
+
+