Skip to content

Commit 9da68ca

Browse files
authored
feat: Add show more button to participants list (#60)
Limits participants list to show max 5 people initially, with a 'Show X more participants' button to expand.
1 parent b19b6a9 commit 9da68ca

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

frontend/eslint.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export default tseslint.config([
2525
cssFiles: ['src/styles/index.css', 'src/styles/design-tokens.css'],
2626
},
2727
},
28+
rules: {
29+
// Disable until plugin properly supports Tailwind v4 CSS-based config
30+
'tailwindcss/no-custom-classname': 'off',
31+
},
2832
languageOptions: {
2933
ecmaVersion: 2020,
3034
globals: globals.browser,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {render, screen} from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
4+
import type {IncidentDetail} from '../queries/incidentDetailQueryOptions';
5+
6+
import {ParticipantsList} from './ParticipantsList';
7+
8+
const mockParticipants: IncidentDetail['participants'] = [
9+
{name: 'John Smith', avatar_url: null, role: 'Captain'},
10+
{name: 'Jane Doe', avatar_url: null, role: 'Reporter'},
11+
{name: 'Alice Brown', avatar_url: null, role: 'Participant'},
12+
{name: 'Charlie Davis', avatar_url: null, role: 'Participant'},
13+
{name: 'Eva Foster', avatar_url: null, role: 'Participant'},
14+
{name: 'Frank Garcia', avatar_url: null, role: 'Participant'},
15+
{name: 'Grace Lee', avatar_url: null, role: 'Participant'},
16+
{name: 'Henry Wilson', avatar_url: null, role: 'Participant'},
17+
];
18+
19+
describe('ParticipantsList', () => {
20+
it('returns null when participants array is empty', () => {
21+
const {container} = render(<ParticipantsList participants={[]} />);
22+
expect(container.firstChild).toBeNull();
23+
});
24+
25+
it('renders all participants when 5 or fewer', () => {
26+
const fiveParticipants = mockParticipants.slice(0, 5);
27+
render(<ParticipantsList participants={fiveParticipants} />);
28+
29+
expect(screen.getByText('John Smith')).toBeInTheDocument();
30+
expect(screen.getByText('Eva Foster')).toBeInTheDocument();
31+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
32+
});
33+
34+
it('shows only first 5 participants when more than 5 exist', () => {
35+
render(<ParticipantsList participants={mockParticipants} />);
36+
37+
expect(screen.getByText('John Smith')).toBeInTheDocument();
38+
expect(screen.getByText('Eva Foster')).toBeInTheDocument();
39+
expect(screen.queryByText('Frank Garcia')).not.toBeInTheDocument();
40+
expect(screen.queryByText('Grace Lee')).not.toBeInTheDocument();
41+
expect(screen.queryByText('Henry Wilson')).not.toBeInTheDocument();
42+
});
43+
44+
it('shows "Show X more participants" button when more than 5 participants', () => {
45+
render(<ParticipantsList participants={mockParticipants} />);
46+
47+
expect(
48+
screen.getByRole('button', {name: 'Show 3 more participants'})
49+
).toBeInTheDocument();
50+
});
51+
52+
it('expands to show all participants when button is clicked', async () => {
53+
const user = userEvent.setup();
54+
render(<ParticipantsList participants={mockParticipants} />);
55+
56+
await user.click(screen.getByRole('button', {name: 'Show 3 more participants'}));
57+
58+
expect(screen.getByText('Frank Garcia')).toBeInTheDocument();
59+
expect(screen.getByText('Grace Lee')).toBeInTheDocument();
60+
expect(screen.getByText('Henry Wilson')).toBeInTheDocument();
61+
expect(
62+
screen.getByRole('button', {name: 'Show fewer participants'})
63+
).toBeInTheDocument();
64+
});
65+
66+
it('collapses back to 5 participants when "Show fewer" is clicked', async () => {
67+
const user = userEvent.setup();
68+
render(<ParticipantsList participants={mockParticipants} />);
69+
70+
await user.click(screen.getByRole('button', {name: 'Show 3 more participants'}));
71+
await user.click(screen.getByRole('button', {name: 'Show fewer participants'}));
72+
73+
expect(screen.queryByText('Frank Garcia')).not.toBeInTheDocument();
74+
expect(
75+
screen.getByRole('button', {name: 'Show 3 more participants'})
76+
).toBeInTheDocument();
77+
});
78+
79+
it('displays participant roles for non-Participant roles', () => {
80+
render(<ParticipantsList participants={mockParticipants.slice(0, 3)} />);
81+
82+
expect(screen.getByText('Captain')).toBeInTheDocument();
83+
expect(screen.getByText('Reporter')).toBeInTheDocument();
84+
expect(screen.queryByText('Participant')).not.toBeInTheDocument();
85+
});
86+
});

frontend/src/routes/$incidentId/components/ParticipantsList.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
1+
import {useState} from 'react';
12
import {Avatar} from 'components/Avatar';
23
import {Card} from 'components/Card';
34

45
import type {IncidentDetail} from '../queries/incidentDetailQueryOptions';
56

7+
const MAX_VISIBLE_PARTICIPANTS = 5;
8+
69
interface ParticipantsListProps {
710
participants: IncidentDetail['participants'];
811
}
912

1013
export function ParticipantsList({participants}: ParticipantsListProps) {
14+
const [expanded, setExpanded] = useState(false);
15+
1116
if (participants.length === 0) {
1217
return null;
1318
}
1419

20+
const hasMore = participants.length > MAX_VISIBLE_PARTICIPANTS;
21+
const visibleParticipants = expanded
22+
? participants
23+
: participants.slice(0, MAX_VISIBLE_PARTICIPANTS);
24+
const hiddenCount = participants.length - MAX_VISIBLE_PARTICIPANTS;
25+
1526
return (
1627
<Card>
1728
<Card.Title>Participants</Card.Title>
1829
<div className="gap-space-xl grid grid-cols-1">
19-
{participants.map((participant, index) => (
30+
{visibleParticipants.map((participant, index) => (
2031
<div key={index} className="gap-space-lg flex items-center">
2132
<Avatar name={participant.name} src={participant.avatar_url} />
2233
<div className="text-content-headings flex-1 font-medium">
@@ -29,6 +40,19 @@ export function ParticipantsList({participants}: ParticipantsListProps) {
2940
)}
3041
</div>
3142
))}
43+
{hasMore && (
44+
<div className="text-center">
45+
<button
46+
type="button"
47+
onClick={() => setExpanded(!expanded)}
48+
className="text-content-secondary hover:text-content-accent px-space-md py-space-xs cursor-pointer text-xs"
49+
>
50+
{expanded
51+
? 'Show fewer participants'
52+
: `Show ${hiddenCount} more participants`}
53+
</button>
54+
</div>
55+
)}
3256
</div>
3357
</Card>
3458
);

0 commit comments

Comments
 (0)