Skip to content

Commit bfa21a6

Browse files
authored
fix: add arrow key navigation to User Preferences tabs (#1538)
Assisted-by: Claude Opus 4.6 Signed-off-by: Oleksii Orel <oorel@redhat.com>
1 parent b0863cc commit bfa21a6

3 files changed

Lines changed: 186 additions & 4 deletions

File tree

packages/dashboard-frontend/src/pages/UserPreferences/__tests__/__snapshots__/index.spec.tsx.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ exports[`UserPreferences snapshot 1`] = `
4949
data-ouia-safe="true"
5050
id="pf-tab-ContainerRegistries-user-preferences-tabs"
5151
role="tab"
52+
tabindex="0"
5253
type="button"
5354
>
5455
Container Registries
@@ -65,6 +66,7 @@ exports[`UserPreferences snapshot 1`] = `
6566
data-ouia-safe="true"
6667
id="pf-tab-GitServices-user-preferences-tabs"
6768
role="tab"
69+
tabindex="-1"
6870
type="button"
6971
>
7072
Git Services
@@ -81,6 +83,7 @@ exports[`UserPreferences snapshot 1`] = `
8183
data-ouia-safe="true"
8284
id="pf-tab-PersonalAccessTokens-user-preferences-tabs"
8385
role="tab"
86+
tabindex="-1"
8487
type="button"
8588
>
8689
Personal Access Tokens
@@ -97,6 +100,7 @@ exports[`UserPreferences snapshot 1`] = `
97100
data-ouia-safe="true"
98101
id="pf-tab-Gitconfig-user-preferences-tabs"
99102
role="tab"
103+
tabindex="-1"
100104
type="button"
101105
>
102106
Gitconfig
@@ -113,6 +117,7 @@ exports[`UserPreferences snapshot 1`] = `
113117
data-ouia-safe="true"
114118
id="pf-tab-SshKeys-user-preferences-tabs"
115119
role="tab"
120+
tabindex="-1"
116121
type="button"
117122
>
118123
SSH Keys

packages/dashboard-frontend/src/pages/UserPreferences/__tests__/index.spec.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Red Hat, Inc. - initial API and implementation
1111
*/
1212

13+
import { fireEvent } from '@testing-library/react';
1314
import userEvent from '@testing-library/user-event';
1415
import React from 'react';
1516
import { Provider } from 'react-redux';
@@ -141,4 +142,122 @@ describe('UserPreferences', () => {
141142
expect(screen.queryByRole('tabpanel', { name: 'SSH Keys' })).toBeTruthy();
142143
});
143144
});
145+
146+
describe('Keyboard navigation', () => {
147+
it('should move to the next tab on ArrowRight', () => {
148+
const location = buildUserPreferencesLocation(UserPreferencesTab.CONTAINER_REGISTRIES);
149+
renderComponent(location);
150+
151+
const tab = screen.getByRole('tab', { name: 'Container Registries' });
152+
fireEvent.keyDown(tab, { key: 'ArrowRight' });
153+
154+
expect(mockNavigate).toHaveBeenCalledWith(
155+
expect.stringContaining(`tab=${UserPreferencesTab.GIT_SERVICES}`),
156+
);
157+
});
158+
159+
it('should move to the next tab on ArrowDown', () => {
160+
const location = buildUserPreferencesLocation(UserPreferencesTab.CONTAINER_REGISTRIES);
161+
renderComponent(location);
162+
163+
const tab = screen.getByRole('tab', { name: 'Container Registries' });
164+
fireEvent.keyDown(tab, { key: 'ArrowDown' });
165+
166+
expect(mockNavigate).toHaveBeenCalledWith(
167+
expect.stringContaining(`tab=${UserPreferencesTab.GIT_SERVICES}`),
168+
);
169+
});
170+
171+
it('should move to the previous tab on ArrowLeft', () => {
172+
const location = buildUserPreferencesLocation(UserPreferencesTab.GIT_SERVICES);
173+
renderComponent(location);
174+
175+
const tab = screen.getByRole('tab', { name: 'Git Services' });
176+
fireEvent.keyDown(tab, { key: 'ArrowLeft' });
177+
178+
expect(mockNavigate).toHaveBeenCalledWith(
179+
expect.stringContaining(`tab=${UserPreferencesTab.CONTAINER_REGISTRIES}`),
180+
);
181+
});
182+
183+
it('should move to the previous tab on ArrowUp', () => {
184+
const location = buildUserPreferencesLocation(UserPreferencesTab.GIT_SERVICES);
185+
renderComponent(location);
186+
187+
const tab = screen.getByRole('tab', { name: 'Git Services' });
188+
fireEvent.keyDown(tab, { key: 'ArrowUp' });
189+
190+
expect(mockNavigate).toHaveBeenCalledWith(
191+
expect.stringContaining(`tab=${UserPreferencesTab.CONTAINER_REGISTRIES}`),
192+
);
193+
});
194+
195+
it('should wrap around to the first tab on ArrowRight from the last tab', () => {
196+
const location = buildUserPreferencesLocation(UserPreferencesTab.SSH_KEYS);
197+
renderComponent(location);
198+
199+
const tab = screen.getByRole('tab', { name: 'SSH Keys' });
200+
fireEvent.keyDown(tab, { key: 'ArrowRight' });
201+
202+
expect(mockNavigate).toHaveBeenCalledWith(
203+
expect.stringContaining(`tab=${UserPreferencesTab.CONTAINER_REGISTRIES}`),
204+
);
205+
});
206+
207+
it('should wrap around to the last tab on ArrowLeft from the first tab', () => {
208+
const location = buildUserPreferencesLocation(UserPreferencesTab.CONTAINER_REGISTRIES);
209+
renderComponent(location);
210+
211+
const tab = screen.getByRole('tab', { name: 'Container Registries' });
212+
fireEvent.keyDown(tab, { key: 'ArrowLeft' });
213+
214+
expect(mockNavigate).toHaveBeenCalledWith(
215+
expect.stringContaining(`tab=${UserPreferencesTab.SSH_KEYS}`),
216+
);
217+
});
218+
219+
it('should move to the first tab on Home', () => {
220+
const location = buildUserPreferencesLocation(UserPreferencesTab.GITCONFIG);
221+
renderComponent(location);
222+
223+
const tab = screen.getByRole('tab', { name: 'Gitconfig' });
224+
fireEvent.keyDown(tab, { key: 'Home' });
225+
226+
expect(mockNavigate).toHaveBeenCalledWith(
227+
expect.stringContaining(`tab=${UserPreferencesTab.CONTAINER_REGISTRIES}`),
228+
);
229+
});
230+
231+
it('should move to the last tab on End', () => {
232+
const location = buildUserPreferencesLocation(UserPreferencesTab.CONTAINER_REGISTRIES);
233+
renderComponent(location);
234+
235+
const tab = screen.getByRole('tab', { name: 'Container Registries' });
236+
fireEvent.keyDown(tab, { key: 'End' });
237+
238+
expect(mockNavigate).toHaveBeenCalledWith(
239+
expect.stringContaining(`tab=${UserPreferencesTab.SSH_KEYS}`),
240+
);
241+
});
242+
243+
it('should not navigate on unhandled keys', () => {
244+
const location = buildUserPreferencesLocation(UserPreferencesTab.CONTAINER_REGISTRIES);
245+
renderComponent(location);
246+
247+
const tab = screen.getByRole('tab', { name: 'Container Registries' });
248+
fireEvent.keyDown(tab, { key: 'Enter' });
249+
250+
expect(mockNavigate).not.toHaveBeenCalled();
251+
});
252+
253+
it('should not navigate when keydown target is not a tab', () => {
254+
const location = buildUserPreferencesLocation(UserPreferencesTab.CONTAINER_REGISTRIES);
255+
renderComponent(location);
256+
257+
const tabPanel = screen.getByRole('tabpanel', { name: 'Container Registries' });
258+
fireEvent.keyDown(tabPanel, { key: 'ArrowRight' });
259+
260+
expect(mockNavigate).not.toHaveBeenCalled();
261+
});
262+
});
144263
});

packages/dashboard-frontend/src/pages/UserPreferences/index.tsx

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ export type State = {
3737
};
3838

3939
class UserPreferences extends React.PureComponent<Props, State> {
40+
private readonly tabOrder: UserPreferencesTab[] = [
41+
UserPreferencesTab.CONTAINER_REGISTRIES,
42+
UserPreferencesTab.GIT_SERVICES,
43+
UserPreferencesTab.PERSONAL_ACCESS_TOKENS,
44+
UserPreferencesTab.GITCONFIG,
45+
UserPreferencesTab.SSH_KEYS,
46+
];
47+
4048
constructor(props: Props) {
4149
super(props);
4250

@@ -80,6 +88,38 @@ class UserPreferences extends React.PureComponent<Props, State> {
8088
});
8189
}
8290

91+
private handleTabKeyDown(event: React.KeyboardEvent): void {
92+
const target = event.target as HTMLElement;
93+
if (target.getAttribute('role') !== 'tab') {
94+
return;
95+
}
96+
97+
const { activeTabKey } = this.state;
98+
const currentIndex = this.tabOrder.indexOf(activeTabKey);
99+
let nextIndex = -1;
100+
101+
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
102+
nextIndex = (currentIndex + 1) % this.tabOrder.length;
103+
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
104+
nextIndex = (currentIndex - 1 + this.tabOrder.length) % this.tabOrder.length;
105+
} else if (event.key === 'Home') {
106+
nextIndex = 0;
107+
} else if (event.key === 'End') {
108+
nextIndex = this.tabOrder.length - 1;
109+
} else {
110+
return;
111+
}
112+
113+
event.preventDefault();
114+
const nextTabKey = this.tabOrder[nextIndex];
115+
this.props.navigate(`${ROUTE.USER_PREFERENCES}?tab=${nextTabKey}`);
116+
this.setState({ activeTabKey: nextTabKey }, () => {
117+
const tabsContainer = document.getElementById('user-preferences-tabs');
118+
const buttons = tabsContainer?.querySelectorAll<HTMLButtonElement>('[role="tab"]');
119+
buttons?.[nextIndex]?.focus();
120+
});
121+
}
122+
83123
render(): React.ReactNode {
84124
const { activeTabKey } = this.state;
85125

@@ -95,25 +135,43 @@ class UserPreferences extends React.PureComponent<Props, State> {
95135
style={{ backgroundColor: 'var(--pf-global--BackgroundColor--100)' }}
96136
activeKey={activeTabKey}
97137
onSelect={(event, tabKey) => this.handleTabClick(event, tabKey)}
138+
onKeyDown={event => this.handleTabKeyDown(event)}
98139
mountOnEnter={true}
99140
unmountOnExit={true}
100141
>
101-
<Tab eventKey={UserPreferencesTab.CONTAINER_REGISTRIES} title="Container Registries">
142+
<Tab
143+
eventKey={UserPreferencesTab.CONTAINER_REGISTRIES}
144+
title="Container Registries"
145+
tabIndex={activeTabKey === UserPreferencesTab.CONTAINER_REGISTRIES ? 0 : -1}
146+
>
102147
<ContainerRegistries />
103148
</Tab>
104-
<Tab eventKey={UserPreferencesTab.GIT_SERVICES} title="Git Services">
149+
<Tab
150+
eventKey={UserPreferencesTab.GIT_SERVICES}
151+
title="Git Services"
152+
tabIndex={activeTabKey === UserPreferencesTab.GIT_SERVICES ? 0 : -1}
153+
>
105154
<GitServices />
106155
</Tab>
107156
<Tab
108157
eventKey={UserPreferencesTab.PERSONAL_ACCESS_TOKENS}
109158
title="Personal Access Tokens"
159+
tabIndex={activeTabKey === UserPreferencesTab.PERSONAL_ACCESS_TOKENS ? 0 : -1}
110160
>
111161
<PersonalAccessTokens />
112162
</Tab>
113-
<Tab eventKey={UserPreferencesTab.GITCONFIG} title="Gitconfig">
163+
<Tab
164+
eventKey={UserPreferencesTab.GITCONFIG}
165+
title="Gitconfig"
166+
tabIndex={activeTabKey === UserPreferencesTab.GITCONFIG ? 0 : -1}
167+
>
114168
<GitConfig />
115169
</Tab>
116-
<Tab eventKey={UserPreferencesTab.SSH_KEYS} title="SSH Keys">
170+
<Tab
171+
eventKey={UserPreferencesTab.SSH_KEYS}
172+
title="SSH Keys"
173+
tabIndex={activeTabKey === UserPreferencesTab.SSH_KEYS ? 0 : -1}
174+
>
117175
<SshKeys />
118176
</Tab>
119177
</Tabs>

0 commit comments

Comments
 (0)