diff --git a/src/learning-header/AuthenticatedUserDropdown.jsx b/src/learning-header/AuthenticatedUserDropdown.jsx
index 1fb17d396..f5335abef 100644
--- a/src/learning-header/AuthenticatedUserDropdown.jsx
+++ b/src/learning-header/AuthenticatedUserDropdown.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
@@ -13,6 +13,31 @@ import messages from './messages';
const AuthenticatedUserDropdown = ({ username }) => {
const intl = useIntl();
+
+ const firstMenuItemRef = useRef(null);
+ const lastMenuItemRef = useRef(null);
+
+ const handleKeyDown = (event) => {
+ if (event.key === 'Tab') {
+ event.preventDefault();
+
+ const isShiftTab = event.shiftKey;
+ const currentElement = document.activeElement;
+ const focusElement = isShiftTab
+ ? currentElement.previousElementSibling
+ : currentElement.nextElementSibling;
+
+ // If the element has reached the start or end of the list, loop the focus
+ if (isShiftTab && currentElement === firstMenuItemRef.current) {
+ lastMenuItemRef.current.focus();
+ } else if (!isShiftTab && currentElement === lastMenuItemRef.current) {
+ firstMenuItemRef.current.focus();
+ } else if (focusElement && focusElement.tagName === 'A') {
+ focusElement.focus();
+ }
+ }
+ };
+
const dropdownItems = [
{
message: intl.formatMessage(messages.dashboard),
@@ -41,8 +66,13 @@ const AuthenticatedUserDropdown = ({ username }) => {
-
-
+
+
);
diff --git a/src/learning-header/AuthenticatedUserDropdown.test.jsx b/src/learning-header/AuthenticatedUserDropdown.test.jsx
new file mode 100644
index 000000000..ecd508a42
--- /dev/null
+++ b/src/learning-header/AuthenticatedUserDropdown.test.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
+import messages from './messages';
+import {
+ render, screen, fireEvent, initializeMockApp,
+} from '../setupTest';
+
+describe('AuthenticatedUserDropdown', () => {
+ const username = 'testuser';
+
+ beforeEach(() => {
+ initializeMockApp();
+ });
+
+ const renderComponent = () => {
+ render(
+ ,
+ );
+ };
+
+ it('renders username in toggle button', () => {
+ renderComponent();
+
+ expect(screen.getByText(username)).toBeInTheDocument();
+ });
+
+ it('renders dropdown items after toggle click', async () => {
+ renderComponent();
+
+ const toggleButton = screen.getByRole('button', { name: messages.userOptionsDropdownLabel.defaultMessage });
+ await fireEvent.click(toggleButton);
+
+ expect(await screen.findByText(messages.dashboard.defaultMessage))
+ .toHaveAttribute('href', `${process.env.LMS_BASE_URL}/dashboard`);
+
+ expect(screen.getByText(messages.profile.defaultMessage)).toHaveAttribute('href', `${process.env.ACCOUNT_PROFILE_URL}/u/${username}`);
+ expect(screen.getByText(messages.account.defaultMessage)).toHaveAttribute('href', process.env.ACCOUNT_SETTINGS_URL);
+ expect(screen.getByText(messages.orderHistory.defaultMessage)).toHaveAttribute('href', process.env.ORDER_HISTORY_URL);
+ expect(screen.getByText(messages.signOut.defaultMessage)).toHaveAttribute('href', process.env.LOGOUT_URL);
+ });
+
+ it('loops focus from last to first and vice versa with Tab and Shift+Tab', async () => {
+ renderComponent();
+
+ const toggleButton = screen.getByRole('button', { name: 'User Options' });
+ await fireEvent.click(toggleButton);
+
+ const menuItems = await screen.findAllByRole('menuitem');
+ const firstItem = menuItems[0];
+ const lastItem = menuItems[menuItems.length - 1];
+
+ lastItem.focus();
+ expect(lastItem).toHaveFocus();
+
+ fireEvent.keyDown(lastItem, { key: 'Tab' });
+ expect(firstItem).toHaveFocus();
+
+ firstItem.focus();
+ expect(firstItem).toHaveFocus();
+
+ fireEvent.keyDown(firstItem, { key: 'Tab', shiftKey: true });
+ expect(lastItem).toHaveFocus();
+ });
+
+ it('focuses next link when Tab is pressed on middle item', async () => {
+ renderComponent();
+
+ const toggleButton = screen.getByRole('button', { name: 'User Options' });
+ await fireEvent.click(toggleButton);
+
+ const menuItems = await screen.findAllByRole('menuitem');
+ const secondItem = menuItems[1];
+ const thirdItem = menuItems[2];
+
+ secondItem.focus();
+ expect(secondItem).toHaveFocus();
+
+ Object.defineProperty(secondItem, 'nextElementSibling', {
+ value: thirdItem,
+ configurable: true,
+ });
+ Object.defineProperty(thirdItem, 'tagName', {
+ value: 'A',
+ configurable: true,
+ });
+
+ fireEvent.keyDown(secondItem, { key: 'Tab' });
+
+ expect(thirdItem).toHaveFocus();
+ });
+});
diff --git a/src/learning-header/LearningHeaderUserMenuItems.jsx b/src/learning-header/LearningHeaderUserMenuItems.jsx
index 40c66e510..7ca17a747 100644
--- a/src/learning-header/LearningHeaderUserMenuItems.jsx
+++ b/src/learning-header/LearningHeaderUserMenuItems.jsx
@@ -3,11 +3,30 @@ import PropTypes from 'prop-types';
import { Dropdown } from '@openedx/paragon';
-const LearningHeaderUserMenuItems = ({ items }) => items.map((item) => (
-
- {item.message}
-
-));
+const LearningHeaderUserMenuItems = ({
+ items,
+ handleKeyDown,
+ firstMenuItemRef,
+ lastMenuItemRef,
+}) => {
+ const getRefForIndex = (index, length) => {
+ if (index === 0) { return firstMenuItemRef; }
+ if (index === length - 1) { return lastMenuItemRef; }
+ return null;
+ };
+
+ return items.map((item, index) => (
+
+ {item.message}
+
+ ));
+};
export const learningHeaderUserMenuDataShape = {
items: PropTypes.arrayOf(PropTypes.shape({
diff --git a/src/plugin-slots/LearningUserMenuSlot/index.jsx b/src/plugin-slots/LearningUserMenuSlot/index.jsx
index 126157350..993686916 100644
--- a/src/plugin-slots/LearningUserMenuSlot/index.jsx
+++ b/src/plugin-slots/LearningUserMenuSlot/index.jsx
@@ -4,6 +4,9 @@ import LearningHeaderUserMenuItems, { learningHeaderUserMenuDataShape } from '..
const LearningUserMenuSlot = ({
items,
+ handleKeyDown,
+ firstMenuItemRef,
+ lastMenuItemRef,
}) => (
);
diff --git a/src/setupTest.js b/src/setupTest.js
index 3f99863a9..114bb794c 100644
--- a/src/setupTest.js
+++ b/src/setupTest.js
@@ -64,6 +64,11 @@ export function initializeMockApp() {
CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH || null,
LOGO_URL: process.env.LOGO_URL || null,
SITE_NAME: process.env.SITE_NAME || null,
+ ACCOUNT_PROFILE_URL: process.env.ACCOUNT_PROFILE_URL || null,
+ ACCOUNT_SETTINGS_URL: process.env.ACCOUNT_SETTINGS_URL || null,
+ ORDER_HISTORY_URL: process.env.ORDER_HISTORY_URL || null,
+ ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL || null,
+ CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
authenticatedUser: {
userId: 'abc123',