Skip to content

Commit 47e07d5

Browse files
refactor after review
1 parent 5199cee commit 47e07d5

File tree

8 files changed

+65
-53
lines changed

8 files changed

+65
-53
lines changed

src/courseware/course/JumpNavMenuItem.test.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const mockData = {
1111
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
1212
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
1313

14-
currentSequence: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
1514
title: 'Demo Menu Item',
1615
courseId: 'course-v1:edX+DemoX+Demo_Course',
1716
currentUnit: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',

src/courseware/course/sidebar/common/Sidebar.test.jsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from 'react';
21
import { Factory } from 'rosie';
2+
33
import {
44
initializeTestStore,
55
render,
@@ -8,9 +8,10 @@ import {
88
waitFor,
99
} from '@src/setupTest';
1010
import userEvent from '@testing-library/user-event';
11+
import { createRef } from 'react';
12+
import messages from '@src/courseware/course/messages';
1113
import SidebarContext from '../SidebarContext';
1214
import SidebarBase from './SidebarBase';
13-
import messages from '../../messages';
1415
import { useSidebarFocusAndKeyboard } from './hooks/useSidebarFocusAndKeyboard';
1516

1617
jest.mock('./hooks/useSidebarFocusAndKeyboard');
@@ -19,7 +20,7 @@ const SIDEBAR_ID = 'test-sidebar';
1920

2021
const mockUseSidebarFocusAndKeyboard = useSidebarFocusAndKeyboard;
2122

22-
describe('SidebarBase (Refactored)', () => {
23+
describe('SidebarBase', () => {
2324
let mockContextValue;
2425
const courseMetadata = Factory.build('courseMetadata');
2526
const user = userEvent.setup();
@@ -61,8 +62,8 @@ describe('SidebarBase (Refactored)', () => {
6162
currentSidebar: null,
6263
};
6364

64-
mockCloseBtnRef = React.createRef();
65-
mockBackBtnRef = React.createRef();
65+
mockCloseBtnRef = createRef();
66+
mockBackBtnRef = createRef();
6667
mockHandleClose = jest.fn();
6768
mockHandleKeyDown = jest.fn();
6869
mockHandleBackBtnKeyDown = jest.fn();

src/courseware/course/sidebar/common/SidebarBase.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from 'react';
99

1010
import { useEventListener } from '@src/generic/hooks';
11-
import messages from '../../messages';
11+
import messages from '@src/courseware/course/messages';
1212
import SidebarContext from '../SidebarContext';
1313
import { useSidebarFocusAndKeyboard } from './hooks/useSidebarFocusAndKeyboard';
1414

@@ -43,7 +43,6 @@ const SidebarBase = ({
4343
if (type === 'learning.events.sidebar.close') {
4444
toggleSidebar(null);
4545
}
46-
// eslint-disable-next-line react-hooks/exhaustive-deps
4746
}, [toggleSidebar]);
4847

4948
useEventListener('message', receiveMessage);

src/courseware/course/sidebar/common/hooks/useSidebarFocusAndKeyboard.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import {
22
useCallback, useContext, useEffect, useRef,
33
} from 'react';
4+
45
import { tryFocusAndPreventDefault } from '../../utils';
56
import SidebarContext from '../../SidebarContext';
67

8+
/**
9+
* Manages accessibility interactions for the SidebarBase component, including:
10+
* 1. Setting initial focus when the sidebar opens.
11+
* 2. Handling sidebar closing and returning focus to the trigger.
12+
* 3. Managing keyboard navigation (Tab/Shift+Tab) for focus trapping/guidance.
13+
*
14+
* @param {string} sidebarId The unique ID of this sidebar.
15+
* @param {string} [triggerButtonSelector] The CSS selector for the trigger button
16+
*/
717
export const useSidebarFocusAndKeyboard = (sidebarId, triggerButtonSelector = '.sidebar-trigger-btn') => {
818
const {
919
toggleSidebar,
@@ -29,12 +39,10 @@ export const useSidebarFocusAndKeyboard = (sidebarId, triggerButtonSelector = '.
2939

3040
const focusSidebarTriggerBtn = useCallback(() => {
3141
requestAnimationFrame(() => {
32-
requestAnimationFrame(() => {
33-
const sidebarTriggerBtn = document.querySelector(triggerButtonSelector);
34-
if (sidebarTriggerBtn) {
35-
sidebarTriggerBtn.focus();
36-
}
37-
});
42+
const sidebarTriggerBtn = document.querySelector(triggerButtonSelector);
43+
if (sidebarTriggerBtn) {
44+
sidebarTriggerBtn.focus();
45+
}
3846
});
3947
}, [triggerButtonSelector]);
4048

src/courseware/course/sidebar/common/hooks/useSidebarFocusAndKeyboard.test.jsx

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import { renderHook, act } from '@testing-library/react';
2+
23
import SidebarContext from '../../SidebarContext';
34
import { useSidebarFocusAndKeyboard } from './useSidebarFocusAndKeyboard';
45

5-
import { tryFocusAndPreventDefault } from '../../utils';
6-
7-
jest.mock('../../utils', () => ({
8-
tryFocusAndPreventDefault: jest.fn(),
9-
}));
10-
116
const SIDEBAR_ID = 'test-sidebar';
127
const TRIGGER_SELECTOR = '.sidebar-trigger-btn';
138

@@ -131,13 +126,20 @@ describe('useSidebarFocusAndKeyboard', () => {
131126

132127
describe('handleKeyDown (Standard Close Button)', () => {
133128
let mockEvent;
129+
let mockOutlineTrigger;
130+
let mockPrevButton;
131+
let mockNextButton;
134132

135133
beforeEach(() => {
136134
mockEvent = {
137135
key: 'Tab',
138136
shiftKey: false,
139137
preventDefault: jest.fn(),
140138
};
139+
140+
mockOutlineTrigger = { focus: jest.fn(), disabled: false };
141+
mockPrevButton = { focus: jest.fn(), disabled: false };
142+
mockNextButton = { focus: jest.fn(), disabled: false };
141143
});
142144

143145
it('should do nothing if key is not Tab', () => {
@@ -151,7 +153,6 @@ describe('useSidebarFocusAndKeyboard', () => {
151153

152154
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
153155
expect(triggerButtonMock.focus).not.toHaveBeenCalled();
154-
expect(tryFocusAndPreventDefault).not.toHaveBeenCalled();
155156
});
156157

157158
it('should call focusSidebarTriggerBtn on Shift+Tab', () => {
@@ -164,46 +165,44 @@ describe('useSidebarFocusAndKeyboard', () => {
164165
});
165166

166167
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
167-
act(() => { jest.runAllTimers(); });
168+
act(() => jest.runAllTimers());
168169
expect(triggerButtonMock.focus).toHaveBeenCalledTimes(1);
169-
expect(tryFocusAndPreventDefault).not.toHaveBeenCalled();
170170
});
171171

172172
it('should attempt to focus elements sequentially on Tab', () => {
173173
mockContextValue = getMockContext(SIDEBAR_ID);
174174
const { result } = renderHookWithContext(mockContextValue);
175-
mockEvent.shiftKey = false;
176175

177-
(tryFocusAndPreventDefault).mockImplementation((event, selector) => {
178-
if (selector === '.previous-button') {
179-
event.preventDefault();
180-
return true;
181-
}
182-
return false;
176+
mockQuerySelector.mockImplementation((selector) => {
177+
if (selector === '#courseOutlineSidebarTrigger') { return mockOutlineTrigger; }
178+
if (selector === '.previous-button') { return mockPrevButton; }
179+
if (selector === '.next-button') { return mockNextButton; }
180+
181+
return null;
183182
});
184183

185184
act(() => {
186185
result.current.handleKeyDown(mockEvent);
187186
});
188187

189-
expect(tryFocusAndPreventDefault).toHaveBeenCalledWith(mockEvent, '#courseOutlineSidebarTrigger');
190-
expect(tryFocusAndPreventDefault).toHaveBeenCalledWith(mockEvent, '.previous-button');
191-
expect(tryFocusAndPreventDefault).not.toHaveBeenCalledWith(mockEvent, '.next-button');
192188
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
189+
expect(mockOutlineTrigger.focus).toHaveBeenCalledTimes(1);
190+
expect(mockPrevButton.focus).not.toHaveBeenCalled();
193191
});
194192

195193
it('should allow default Tab if no elements are focused by tryFocusAndPreventDefault', () => {
196194
mockContextValue = getMockContext(SIDEBAR_ID);
197195
const { result } = renderHookWithContext(mockContextValue);
198-
mockEvent.shiftKey = false;
199196

200-
(tryFocusAndPreventDefault).mockReturnValue(false);
197+
mockQuerySelector.mockImplementation((selector) => {
198+
if (selector === TRIGGER_SELECTOR) { return triggerButtonMock; }
199+
return null;
200+
});
201201

202202
act(() => {
203203
result.current.handleKeyDown(mockEvent);
204204
});
205205

206-
expect(tryFocusAndPreventDefault).toHaveBeenCalledTimes(3);
207206
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
208207
});
209208
});
@@ -237,7 +236,7 @@ describe('useSidebarFocusAndKeyboard', () => {
237236
});
238237

239238
expect(mockToggleSidebar).toHaveBeenCalledWith(null);
240-
act(() => { jest.runAllTimers(); });
239+
act(() => jest.runAllTimers());
241240
expect(triggerButtonMock.focus).toHaveBeenCalledTimes(1);
242241
});
243242

src/courseware/course/sidebar/sidebars/notifications/NotificationTray.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { breakpoints } from '@openedx/paragon';
55

66
import MockAdapter from 'axios-mock-adapter';
77
import { Factory } from 'rosie';
8-
import messages from '../../../messages';
8+
import messages from '@src/courseware/course/messages';
99
import {
1010
fireEvent, initializeMockApp, render, screen, waitFor,
1111
} from '../../../../../setupTest';

src/courseware/course/sidebar/sidebars/notifications/NotificationTrigger.test.jsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import PropTypes from 'prop-types';
22
import { Factory } from 'rosie';
3+
import userEvent from '@testing-library/user-event';
34

4-
import messages from '../../../messages';
5+
import messages from '@src/courseware/course/messages';
56
import {
67
fireEvent, initializeTestStore, render, screen,
78
} from '../../../../../setupTest';
@@ -139,14 +140,14 @@ describe('Notification Trigger', () => {
139140
expect(localStorage.getItem(`notificationStatus.${courseMetadataSecondCourse.id}`)).toBe('"inactive"');
140141
});
141142

142-
it('should call toggleSidebar and onClick prop on click', () => {
143+
it('should call toggleSidebar and onClick prop on click', async () => {
143144
const externalOnClick = jest.fn();
144145
renderWithProvider({}, externalOnClick);
145146

146147
const triggerButton = screen.getByRole('button', {
147148
name: messages.openNotificationTrigger.defaultMessage,
148149
});
149-
fireEvent.click(triggerButton);
150+
await userEvent.click(triggerButton);
150151

151152
expect(mockData.toggleSidebar).toHaveBeenCalledTimes(1);
152153
expect(mockData.toggleSidebar).toHaveBeenCalledWith(ID);
@@ -175,34 +176,29 @@ describe('Notification Trigger', () => {
175176
querySelectorSpy.mockRestore();
176177
});
177178

178-
it('should focus the close button and prevent default behavior if sidebar is open', () => {
179+
it('should focus the close button and prevent default behavior if sidebar is open', async () => {
179180
renderWithProvider({ currentSidebar: ID });
180181
triggerButton = screen.getByRole('button', {
181182
name: messages.openNotificationTrigger.defaultMessage,
182183
});
183184

184-
const defaultPrevented = !fireEvent.keyDown(triggerButton, {
185-
key: 'Tab',
186-
shiftKey: false,
187-
});
185+
triggerButton.focus();
186+
expect(document.activeElement).toBe(triggerButton);
187+
188+
await userEvent.tab();
188189

189-
expect(defaultPrevented).toBe(true);
190190
expect(querySelectorSpy).toHaveBeenCalledWith('.sidebar-close-btn');
191191
expect(mockCloseButtonFocus).toHaveBeenCalledTimes(1);
192192
});
193193

194-
it('should do nothing (allow default Tab behavior) if sidebar is closed', () => {
194+
it('should do nothing (allow default Tab behavior) if sidebar is closed', async () => {
195195
renderWithProvider({ currentSidebar: null });
196196
triggerButton = screen.getByRole('button', {
197197
name: messages.openNotificationTrigger.defaultMessage,
198198
});
199199

200-
const defaultPrevented = !fireEvent.keyDown(triggerButton, {
201-
key: 'Tab',
202-
shiftKey: false,
203-
});
200+
await userEvent.tab();
204201

205-
expect(defaultPrevented).toBe(false);
206202
expect(querySelectorSpy).not.toHaveBeenCalledWith('.sidebar-close-btn');
207203
expect(mockCloseButtonFocus).not.toHaveBeenCalled();
208204
});

src/courseware/course/sidebar/utils.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
/**
2+
* Attempts to find an interactive element by its selector and focus it.
3+
* If the element is found and is not disabled, it prevents the default
4+
* behavior of the event (e.g., standard Tab) and moves focus to the element.
5+
*
6+
* @param {Event} event - The keyboard event object (e.g., from a 'keydown' listener).
7+
* @param {string} selector - The CSS selector for the target element to focus.
8+
* @returns {boolean} - Returns `true` if the element was found, enabled, and focused.
9+
* Returns `false` if the element was not found or was disabled.
10+
*/
111
export const tryFocusAndPreventDefault = (event, selector) => {
212
const element = document.querySelector(selector);
313
if (element && !element.disabled) {

0 commit comments

Comments
 (0)