Skip to content

Commit 9cfb12d

Browse files
committed
Fix group arrow key navigation
1 parent 989b6fe commit 9cfb12d

3 files changed

Lines changed: 136 additions & 8 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import '@testing-library/jest-dom';
2+
import { fireEvent, render } from '@testing-library/react';
3+
import { Competition } from '@wca/helpers';
4+
import { AnchorLink } from '@/lib/linkRenderer';
5+
import { storybookCompetitionFixture } from '@/storybook/competitionFixtures';
6+
import { CompetitionGroupContainer } from './CompetitionGroup';
7+
8+
const mockSetTitle = jest.fn();
9+
const mockWcif = storybookCompetitionFixture as unknown as Competition;
10+
11+
jest.mock('@/i18n', () => ({
12+
__esModule: true,
13+
default: {
14+
t: (key: string, options?: Record<string, unknown>) =>
15+
key === 'common.activityCodeToName.group' ? `Group ${options?.groupNumber}` : key,
16+
},
17+
}));
18+
19+
jest.mock('@/components', () => ({
20+
ActivityRow: () => <div />,
21+
}));
22+
23+
jest.mock('@/components/AssignmentCodeCell', () => ({
24+
AssignmentCodeCell: ({ count }: { count: number }) => <div>{count}</div>,
25+
}));
26+
27+
jest.mock('@/components/Breadcrumbs/Breadcrumbs', () => ({
28+
Breadcrumbs: () => <div />,
29+
}));
30+
31+
jest.mock('@/components/Container', () => ({
32+
Container: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
33+
}));
34+
35+
jest.mock('@/components/CutoffTimeLimitPanel', () => ({
36+
CutoffTimeLimitPanel: () => <div />,
37+
}));
38+
39+
jest.mock('@/providers/WCIFProvider', () => ({
40+
useWCIF: () => ({
41+
wcif: mockWcif,
42+
setTitle: mockSetTitle,
43+
}),
44+
}));
45+
46+
jest.mock('react-i18next', () => ({
47+
useTranslation: () => ({
48+
t: (key: string) => key,
49+
}),
50+
}));
51+
52+
describe('CompetitionGroupContainer', () => {
53+
beforeEach(() => {
54+
mockSetTitle.mockClear();
55+
});
56+
57+
it('navigates between groups with arrow keys', () => {
58+
const onNavigate = jest.fn();
59+
60+
render(
61+
<CompetitionGroupContainer
62+
competitionId="SeattleSummerOpen2026"
63+
roundId="333-r1"
64+
groupNumber="1"
65+
LinkComponent={AnchorLink}
66+
onNavigate={onNavigate}
67+
/>,
68+
);
69+
70+
fireEvent.keyDown(document, { key: 'ArrowRight' });
71+
72+
expect(onNavigate).toHaveBeenCalledWith('/competitions/SeattleSummerOpen2026/events/333-r1/2');
73+
});
74+
75+
it('does not navigate past the first group with the left arrow key', () => {
76+
const onNavigate = jest.fn();
77+
78+
render(
79+
<CompetitionGroupContainer
80+
competitionId="SeattleSummerOpen2026"
81+
roundId="333-r1"
82+
groupNumber="1"
83+
LinkComponent={AnchorLink}
84+
onNavigate={onNavigate}
85+
/>,
86+
);
87+
88+
fireEvent.keyDown(document, { key: 'ArrowLeft' });
89+
90+
expect(onNavigate).not.toHaveBeenCalled();
91+
});
92+
});

src/containers/CompetitionGroup/CompetitionGroup.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ActivityCode } from '@wca/helpers';
22
import classNames from 'classnames';
3-
import { Fragment, useEffect, useMemo } from 'react';
3+
import { Fragment, useCallback, useEffect } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { ActivityRow } from '@/components';
66
import { AssignmentCodeCell } from '@/components/AssignmentCodeCell';
@@ -26,13 +26,15 @@ export interface CompetitionGroupContainerProps {
2626
roundId: string;
2727
groupNumber: string;
2828
LinkComponent?: LinkRenderer;
29+
onNavigate?: (url: string) => void;
2930
}
3031

3132
export function CompetitionGroupContainer({
3233
competitionId,
3334
roundId,
3435
groupNumber,
3536
LinkComponent = AnchorLink,
37+
onNavigate,
3638
}: CompetitionGroupContainerProps) {
3739
const { t } = useTranslation();
3840
const { wcif, setTitle } = useWCIF();
@@ -95,12 +97,44 @@ export function CompetitionGroupContainer({
9597

9698
const prev = wcif && prevActivityCode(wcif, activityCode);
9799
const next = wcif && nextActivityCode(wcif, activityCode);
98-
const prevUrl = `/competitions/${competitionId}/events/${prev?.split?.('-g')?.[0]}/${
99-
prev?.split?.('-g')?.[1]
100-
}`;
101-
const nextUrl = `/competitions/${competitionId}/events/${next?.split?.('-g')?.[0]}/${
102-
next?.split?.('-g')?.[1]
103-
}`;
100+
const prevUrl = prev
101+
? `/competitions/${competitionId}/events/${prev.split('-g')[0]}/${prev.split('-g')[1]}`
102+
: undefined;
103+
const nextUrl = next
104+
? `/competitions/${competitionId}/events/${next.split('-g')[0]}/${next.split('-g')[1]}`
105+
: undefined;
106+
107+
const goToPrev = useCallback(() => {
108+
if (prevUrl) {
109+
onNavigate?.(prevUrl);
110+
}
111+
}, [onNavigate, prevUrl]);
112+
113+
const goToNext = useCallback(() => {
114+
if (nextUrl) {
115+
onNavigate?.(nextUrl);
116+
}
117+
}, [onNavigate, nextUrl]);
118+
119+
useEffect(() => {
120+
const handleKeydown = (event: KeyboardEvent) => {
121+
if (event.key === 'ArrowLeft') {
122+
event.preventDefault();
123+
goToPrev();
124+
}
125+
126+
if (event.key === 'ArrowRight') {
127+
event.preventDefault();
128+
goToNext();
129+
}
130+
};
131+
132+
document.addEventListener('keydown', handleKeydown);
133+
134+
return () => {
135+
document.removeEventListener('keydown', handleKeydown);
136+
};
137+
}, [goToPrev, goToNext]);
104138

105139
return (
106140
<>

src/pages/Competition/ByGroup/Group.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Link, useParams } from 'react-router-dom';
1+
import { Link, useNavigate, useParams } from 'react-router-dom';
22
import { CompetitionGroupContainer } from '@/containers/CompetitionGroup';
33

44
export default function Group() {
55
const { competitionId, roundId, groupNumber } = useParams();
6+
const navigate = useNavigate();
67

78
if (!competitionId || !roundId || !groupNumber) {
89
return null;
@@ -14,6 +15,7 @@ export default function Group() {
1415
LinkComponent={Link}
1516
roundId={roundId}
1617
groupNumber={groupNumber}
18+
onNavigate={navigate}
1719
/>
1820
);
1921
}

0 commit comments

Comments
 (0)