@nauedu/frontend-component-header 20.1.0 → 20.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/dist/Header.js +13 -9
  2. package/dist/Header.js.map +1 -1
  3. package/dist/Header.test.jsx +3 -0
  4. package/dist/Icons.js +1 -1
  5. package/dist/LanguageSelector/data/api.js +13 -5
  6. package/dist/LanguageSelector/data/api.js.map +1 -1
  7. package/dist/LanguageSelector/index.js +12 -4
  8. package/dist/LanguageSelector/index.js.map +1 -1
  9. package/dist/Logo.js +10 -23
  10. package/dist/Logo.js.map +1 -1
  11. package/dist/Menu/Menu.js +11 -11
  12. package/dist/Menu/Menu.js.map +1 -1
  13. package/dist/Menu/menu.scss +6 -6
  14. package/dist/__snapshots__/Header.test.jsx.snap +2 -0
  15. package/dist/desktop-header/DesktopHeader.js +172 -0
  16. package/dist/desktop-header/DesktopHeader.js.map +1 -0
  17. package/dist/desktop-header/DesktopHeaderMainOrSecondaryMenu.js +51 -0
  18. package/dist/desktop-header/DesktopHeaderMainOrSecondaryMenu.js.map +1 -0
  19. package/dist/desktop-header/DesktopHeaderUserMenu.js +49 -0
  20. package/dist/desktop-header/DesktopHeaderUserMenu.js.map +1 -0
  21. package/dist/desktop-header/DesktopLoggedOutItems.js +22 -0
  22. package/dist/desktop-header/DesktopLoggedOutItems.js.map +1 -0
  23. package/dist/desktop-header/DesktopUserMenuToggle.js +25 -0
  24. package/dist/desktop-header/DesktopUserMenuToggle.js.map +1 -0
  25. package/dist/frontend-platform.d.ts +41 -0
  26. package/dist/index.scss +27 -15
  27. package/dist/learning-header/AnonymousUserMenu.js +11 -8
  28. package/dist/learning-header/AnonymousUserMenu.js.map +1 -1
  29. package/dist/learning-header/AuthenticatedUserDropdown.js +32 -24
  30. package/dist/learning-header/AuthenticatedUserDropdown.js.map +1 -1
  31. package/dist/learning-header/LearningHeader.js +17 -15
  32. package/dist/learning-header/LearningHeader.js.map +1 -1
  33. package/dist/learning-header/LearningHeaderCourseInfo.js +24 -0
  34. package/dist/learning-header/LearningHeaderCourseInfo.js.map +1 -0
  35. package/dist/learning-header/LearningHeaderHelpLink.js +13 -0
  36. package/dist/learning-header/LearningHeaderHelpLink.js.map +1 -0
  37. package/dist/learning-header/LearningHeaderUserMenuItems.js +20 -0
  38. package/dist/learning-header/LearningHeaderUserMenuItems.js.map +1 -0
  39. package/dist/learning-header/LearningLoggedOutButtons.js +24 -0
  40. package/dist/learning-header/LearningLoggedOutButtons.js.map +1 -0
  41. package/dist/learning-header/LearningUserMenuToggle.js +26 -0
  42. package/dist/learning-header/LearningUserMenuToggle.js.map +1 -0
  43. package/dist/learning-header/data/api.js +12 -4
  44. package/dist/learning-header/data/api.js.map +1 -1
  45. package/dist/learning-header/data/api.test.js +92 -95
  46. package/dist/learning-header/data/api.test.js.map +1 -1
  47. package/dist/learning-header/messages.js +5 -0
  48. package/dist/learning-header/messages.js.map +1 -1
  49. package/dist/mobile-header/MobileHeader.js +197 -0
  50. package/dist/mobile-header/MobileHeader.js.map +1 -0
  51. package/dist/mobile-header/MobileHeaderMainMenu.js +46 -0
  52. package/dist/mobile-header/MobileHeaderMainMenu.js.map +1 -0
  53. package/dist/mobile-header/MobileHeaderUserMenu.js +38 -0
  54. package/dist/mobile-header/MobileHeaderUserMenu.js.map +1 -0
  55. package/dist/mobile-header/MobileLoggedOutItems.js +27 -0
  56. package/dist/mobile-header/MobileLoggedOutItems.js.map +1 -0
  57. package/dist/mobile-header/MobileUserMenuToggle.js +19 -0
  58. package/dist/mobile-header/MobileUserMenuToggle.js.map +1 -0
  59. package/dist/plugin-slots/CourseInfoSlot/README.md +128 -0
  60. package/dist/plugin-slots/CourseInfoSlot/images/add_custom_components_before_and_after_course_info.png +0 -0
  61. package/dist/plugin-slots/CourseInfoSlot/images/replace_course_info_with_custom_component.png +0 -0
  62. package/dist/plugin-slots/CourseInfoSlot/images/replace_course_title.png +0 -0
  63. package/dist/plugin-slots/CourseInfoSlot/index.js +32 -0
  64. package/dist/plugin-slots/CourseInfoSlot/index.js.map +1 -0
  65. package/dist/plugin-slots/DesktopHeaderSlot/README.md +44 -0
  66. package/dist/plugin-slots/DesktopHeaderSlot/images/desktop_header_custom_component.png +0 -0
  67. package/dist/plugin-slots/DesktopHeaderSlot/index.js +16 -0
  68. package/dist/plugin-slots/DesktopHeaderSlot/index.js.map +1 -0
  69. package/dist/plugin-slots/DesktopLoggedOutItemsSlot/README.md +137 -0
  70. package/dist/plugin-slots/DesktopLoggedOutItemsSlot/images/desktop_logged_out_items_custom_component.png +0 -0
  71. package/dist/plugin-slots/DesktopLoggedOutItemsSlot/images/desktop_logged_out_items_custom_components_before_after.png +0 -0
  72. package/dist/plugin-slots/DesktopLoggedOutItemsSlot/images/desktop_logged_out_items_modify_items.png +0 -0
  73. package/dist/plugin-slots/DesktopLoggedOutItemsSlot/index.js +20 -0
  74. package/dist/plugin-slots/DesktopLoggedOutItemsSlot/index.js.map +1 -0
  75. package/dist/plugin-slots/DesktopMainMenuSlot/README.md +137 -0
  76. package/dist/plugin-slots/DesktopMainMenuSlot/images/desktop_main_menu_custom_component.png +0 -0
  77. package/dist/plugin-slots/DesktopMainMenuSlot/images/desktop_main_menu_custom_components_before_after.png +0 -0
  78. package/dist/plugin-slots/DesktopMainMenuSlot/images/desktop_main_menu_modify_items.png +0 -0
  79. package/dist/plugin-slots/DesktopMainMenuSlot/index.js +20 -0
  80. package/dist/plugin-slots/DesktopMainMenuSlot/index.js.map +1 -0
  81. package/dist/plugin-slots/DesktopSecondaryMenuSlot/README.md +132 -0
  82. package/dist/plugin-slots/DesktopSecondaryMenuSlot/images/desktop_secondary_menu_custom_component.png +0 -0
  83. package/dist/plugin-slots/DesktopSecondaryMenuSlot/images/desktop_secondary_menu_custom_components_before_after.png +0 -0
  84. package/dist/plugin-slots/DesktopSecondaryMenuSlot/images/desktop_secondary_menu_modify_items.png +0 -0
  85. package/dist/plugin-slots/DesktopSecondaryMenuSlot/index.js +20 -0
  86. package/dist/plugin-slots/DesktopSecondaryMenuSlot/index.js.map +1 -0
  87. package/dist/plugin-slots/DesktopUserMenuSlot/README.md +144 -0
  88. package/dist/plugin-slots/DesktopUserMenuSlot/images/desktop_user_menu_custom_component.png +0 -0
  89. package/dist/plugin-slots/DesktopUserMenuSlot/images/desktop_user_menu_custom_components_before_after.png +0 -0
  90. package/dist/plugin-slots/DesktopUserMenuSlot/images/desktop_user_menu_modify_items.png +0 -0
  91. package/dist/plugin-slots/DesktopUserMenuSlot/index.js +20 -0
  92. package/dist/plugin-slots/DesktopUserMenuSlot/index.js.map +1 -0
  93. package/dist/plugin-slots/DesktopUserMenuToggleSlot/README.md +74 -0
  94. package/dist/plugin-slots/DesktopUserMenuToggleSlot/images/desktop_user_menu_custom_component.png +0 -0
  95. package/dist/plugin-slots/DesktopUserMenuToggleSlot/images/desktop_user_menu_modified_toggle.png +0 -0
  96. package/dist/plugin-slots/DesktopUserMenuToggleSlot/index.js +19 -0
  97. package/dist/plugin-slots/DesktopUserMenuToggleSlot/index.js.map +1 -0
  98. package/dist/plugin-slots/LearningHelpSlot/README.md +44 -0
  99. package/dist/plugin-slots/LearningHelpSlot/images/learning_help_custom_component.png +0 -0
  100. package/dist/plugin-slots/LearningHelpSlot/index.js +11 -0
  101. package/dist/plugin-slots/LearningHelpSlot/index.js.map +1 -0
  102. package/dist/plugin-slots/LearningLoggedOutItemsSlot/README.md +135 -0
  103. package/dist/plugin-slots/LearningLoggedOutItemsSlot/images/learning_logged_out_items_custom_component.png +0 -0
  104. package/dist/plugin-slots/LearningLoggedOutItemsSlot/images/learning_logged_out_items_custom_components_before_after.png +0 -0
  105. package/dist/plugin-slots/LearningLoggedOutItemsSlot/images/learning_logged_out_items_modified_items.png +0 -0
  106. package/dist/plugin-slots/LearningLoggedOutItemsSlot/index.js +18 -0
  107. package/dist/plugin-slots/LearningLoggedOutItemsSlot/index.js.map +1 -0
  108. package/dist/plugin-slots/LearningUserMenuSlot/README.md +133 -0
  109. package/dist/plugin-slots/LearningUserMenuSlot/images/learning_user_menu_custom_component.png +0 -0
  110. package/dist/plugin-slots/LearningUserMenuSlot/images/learning_user_menu_custom_components_before_after.png +0 -0
  111. package/dist/plugin-slots/LearningUserMenuSlot/images/learning_user_menu_modified_items.png +0 -0
  112. package/dist/plugin-slots/LearningUserMenuSlot/index.js +18 -0
  113. package/dist/plugin-slots/LearningUserMenuSlot/index.js.map +1 -0
  114. package/dist/plugin-slots/LearningUserMenuToggleSlot/README.md +74 -0
  115. package/dist/plugin-slots/LearningUserMenuToggleSlot/images/learning_user_menu_toggle_custom_component.png +0 -0
  116. package/dist/plugin-slots/LearningUserMenuToggleSlot/images/learning_user_menu_toggle_modified_items.png +0 -0
  117. package/dist/plugin-slots/LearningUserMenuToggleSlot/index.js +19 -0
  118. package/dist/plugin-slots/LearningUserMenuToggleSlot/index.js.map +1 -0
  119. package/dist/plugin-slots/LogoSlot/README.md +72 -0
  120. package/dist/plugin-slots/LogoSlot/index.js +27 -0
  121. package/dist/plugin-slots/LogoSlot/index.js.map +1 -0
  122. package/dist/plugin-slots/MobileHeaderSlot/README.md +44 -0
  123. package/dist/plugin-slots/MobileHeaderSlot/images/mobile_header_custom_component.png +0 -0
  124. package/dist/plugin-slots/MobileHeaderSlot/index.js +16 -0
  125. package/dist/plugin-slots/MobileHeaderSlot/index.js.map +1 -0
  126. package/dist/plugin-slots/MobileLoggedOutItemsSlot/README.md +136 -0
  127. package/dist/plugin-slots/MobileLoggedOutItemsSlot/images/mobile_logged_out_items_custom_component.png +0 -0
  128. package/dist/plugin-slots/MobileLoggedOutItemsSlot/images/mobile_logged_out_items_custom_components_before_after.png +0 -0
  129. package/dist/plugin-slots/MobileLoggedOutItemsSlot/images/mobile_logged_out_items_modify_items.png +0 -0
  130. package/dist/plugin-slots/MobileLoggedOutItemsSlot/index.js +20 -0
  131. package/dist/plugin-slots/MobileLoggedOutItemsSlot/index.js.map +1 -0
  132. package/dist/plugin-slots/MobileMainMenuSlot/README.md +136 -0
  133. package/dist/plugin-slots/MobileMainMenuSlot/images/mobile_main_menu_custom_component.png +0 -0
  134. package/dist/plugin-slots/MobileMainMenuSlot/images/mobile_main_menu_custom_components_before_after.png +0 -0
  135. package/dist/plugin-slots/MobileMainMenuSlot/images/mobile_main_menu_modify_items.png +0 -0
  136. package/dist/plugin-slots/MobileMainMenuSlot/index.js +20 -0
  137. package/dist/plugin-slots/MobileMainMenuSlot/index.js.map +1 -0
  138. package/dist/plugin-slots/MobileUserMenuSlot/README.md +144 -0
  139. package/dist/plugin-slots/MobileUserMenuSlot/images/mobile_user_menu_custom_component.png +0 -0
  140. package/dist/plugin-slots/MobileUserMenuSlot/images/mobile_user_menu_custom_components_before_after.png +0 -0
  141. package/dist/plugin-slots/MobileUserMenuSlot/images/mobile_user_menu_modify_items.png +0 -0
  142. package/dist/plugin-slots/MobileUserMenuSlot/index.js +20 -0
  143. package/dist/plugin-slots/MobileUserMenuSlot/index.js.map +1 -0
  144. package/dist/plugin-slots/MobileUserMenuToggleSlot/README.md +74 -0
  145. package/dist/plugin-slots/MobileUserMenuToggleSlot/images/mobile_user_menu_toggle_custom_component.png +0 -0
  146. package/dist/plugin-slots/MobileUserMenuToggleSlot/images/mobile_user_menu_toggle_modified_items.png +0 -0
  147. package/dist/plugin-slots/MobileUserMenuToggleSlot/index.js +19 -0
  148. package/dist/plugin-slots/MobileUserMenuToggleSlot/index.js.map +1 -0
  149. package/dist/plugin-slots/README.md +26 -0
  150. package/dist/setupTest.js +0 -2
  151. package/dist/studio-header/BrandNav.test.tsx +40 -0
  152. package/dist/studio-header/BrandNav.tsx +25 -0
  153. package/dist/studio-header/CourseLockUp.test.tsx +59 -0
  154. package/dist/studio-header/CourseLockUp.tsx +56 -0
  155. package/dist/studio-header/HeaderBody.test.tsx +102 -0
  156. package/dist/studio-header/HeaderBody.tsx +196 -0
  157. package/dist/studio-header/MobileHeader.tsx +74 -0
  158. package/dist/studio-header/MobileMenu.test.tsx +81 -0
  159. package/dist/studio-header/MobileMenu.tsx +50 -0
  160. package/dist/studio-header/NavDropdownMenu.test.tsx +67 -0
  161. package/dist/studio-header/NavDropdownMenu.tsx +42 -0
  162. package/dist/studio-header/StudioHeader.scss +8 -10
  163. package/dist/studio-header/StudioHeader.test.tsx +220 -0
  164. package/dist/studio-header/StudioHeader.tsx +84 -0
  165. package/dist/studio-header/UserMenu.tsx +69 -0
  166. package/dist/studio-header/{index.js → index.ts} +1 -1
  167. package/dist/studio-header/{messages.js → messages.ts} +15 -14
  168. package/dist/studio-header/utils.ts +37 -0
  169. package/dist/test-utils.js +12 -4
  170. package/dist/test-utils.js.map +1 -1
  171. package/package.json +1 -1
  172. package/dist/DesktopHeader.js +0 -242
  173. package/dist/DesktopHeader.js.map +0 -1
  174. package/dist/MobileHeader.js +0 -244
  175. package/dist/MobileHeader.js.map +0 -1
  176. package/dist/studio-header/BrandNav.js +0 -21
  177. package/dist/studio-header/BrandNav.js.map +0 -1
  178. package/dist/studio-header/CourseLockUp.js +0 -45
  179. package/dist/studio-header/CourseLockUp.js.map +0 -1
  180. package/dist/studio-header/HeaderBody.js +0 -131
  181. package/dist/studio-header/HeaderBody.js.map +0 -1
  182. package/dist/studio-header/MobileHeader.js +0 -79
  183. package/dist/studio-header/MobileHeader.js.map +0 -1
  184. package/dist/studio-header/MobileMenu.js +0 -45
  185. package/dist/studio-header/MobileMenu.js.map +0 -1
  186. package/dist/studio-header/NavDropdownMenu.js +0 -30
  187. package/dist/studio-header/NavDropdownMenu.js.map +0 -1
  188. package/dist/studio-header/StudioHeader.js +0 -72
  189. package/dist/studio-header/StudioHeader.js.map +0 -1
  190. package/dist/studio-header/UserMenu.js +0 -55
  191. package/dist/studio-header/UserMenu.js.map +0 -1
  192. package/dist/studio-header/index.js.map +0 -1
  193. package/dist/studio-header/messages.js.map +0 -1
  194. package/dist/studio-header/utils.js +0 -29
  195. package/dist/studio-header/utils.js.map +0 -1
@@ -0,0 +1,102 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import { IntlProvider } from '@edx/frontend-platform/i18n';
5
+ import { MemoryRouter } from 'react-router-dom';
6
+
7
+ import HeaderBody from './HeaderBody';
8
+ import messages from './messages';
9
+
10
+ const mockOnNavigate = jest.fn();
11
+ const mockSearchButtonAction = jest.fn();
12
+ const mockToggleModalPopup = jest.fn();
13
+ const mockSetModalPopupTarget = jest.fn();
14
+
15
+ const defaultProps = {
16
+ studioBaseUrl: 'https://example.com',
17
+ logoutUrl: 'https://example.com/logout',
18
+ onNavigate: mockOnNavigate,
19
+ setModalPopupTarget: mockSetModalPopupTarget,
20
+ toggleModalPopup: mockToggleModalPopup,
21
+ searchButtonAction: mockSearchButtonAction,
22
+ username: 'testuser',
23
+ authenticatedUserAvatar: 'avatar.png',
24
+ isAdmin: true,
25
+ isMobile: false,
26
+ isHiddenMainMenu: false,
27
+ mainMenuDropdowns: [],
28
+ logo: 'logo.png',
29
+ logoAltText: 'Test Logo',
30
+ number: '101',
31
+ org: 'EDX',
32
+ title: 'Test Course',
33
+ outlineLink: '/courses/edx/course-101',
34
+ };
35
+
36
+ const RootWrapper = (props) => (
37
+ <MemoryRouter>
38
+ <IntlProvider locale="en" messages={{}}>
39
+ <HeaderBody {...props} />
40
+ </IntlProvider>
41
+ </MemoryRouter>
42
+ );
43
+
44
+ describe('HeaderBody Component', () => {
45
+ afterEach(() => {
46
+ jest.clearAllMocks();
47
+ });
48
+
49
+ it('renders the logo and brand navigation', () => {
50
+ render(<RootWrapper {...defaultProps} />);
51
+
52
+ const logoImage = screen.getByAltText(defaultProps.logoAltText);
53
+ expect(logoImage).toBeInTheDocument();
54
+ expect(logoImage).toHaveAttribute('src', defaultProps.logo);
55
+ });
56
+
57
+ it('renders course lockup information', () => {
58
+ render(<RootWrapper {...defaultProps} />);
59
+
60
+ const courseTitle = screen.getByText(defaultProps.title);
61
+ const courseOrgNumber = screen.getByText(`${defaultProps.org} ${defaultProps.number}`);
62
+
63
+ expect(courseTitle).toBeInTheDocument();
64
+ expect(courseOrgNumber).toBeInTheDocument();
65
+ });
66
+
67
+ it('renders a course lock-up link with the correct outline URL', () => {
68
+ render(<RootWrapper {...defaultProps} />);
69
+
70
+ const courseLockUpLink = screen.getByTestId('course-lock-up-block');
71
+ expect(courseLockUpLink.getAttribute('href')).toBe(defaultProps.outlineLink);
72
+ });
73
+
74
+ it('displays search button and triggers searchButtonAction on click', () => {
75
+ render(<RootWrapper {...defaultProps} />);
76
+
77
+ const searchButton = screen.getByLabelText(messages['header.label.search.nav'].defaultMessage);
78
+ expect(searchButton).toBeInTheDocument();
79
+
80
+ fireEvent.click(searchButton);
81
+ expect(mockSearchButtonAction).toHaveBeenCalled();
82
+ });
83
+
84
+ it('displays user menu with username and avatar', () => {
85
+ render(<RootWrapper {...defaultProps} />);
86
+
87
+ const userMenu = screen.getByText(defaultProps.username);
88
+ const avatarImage = screen.getByAltText(defaultProps.username);
89
+
90
+ expect(userMenu).toBeInTheDocument();
91
+ expect(avatarImage).toHaveAttribute('src', defaultProps.authenticatedUserAvatar);
92
+ });
93
+
94
+ it('toggles mobile menu popup when button is clicked in mobile view', () => {
95
+ render(<RootWrapper {...defaultProps} isMobile isModalPopupOpen={false} />);
96
+
97
+ const menuButton = screen.getByTestId('mobile-menu-button');
98
+ fireEvent.click(menuButton);
99
+
100
+ expect(mockToggleModalPopup).toHaveBeenCalled();
101
+ });
102
+ });
@@ -0,0 +1,196 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useIntl } from '@edx/frontend-platform/i18n';
4
+ import classNames from 'classnames';
5
+ import {
6
+ ActionRow,
7
+ Button,
8
+ Container,
9
+ Icon,
10
+ IconButton,
11
+ Nav,
12
+ Row,
13
+ } from '@openedx/paragon';
14
+ import { Close, MenuIcon, Search } from '@openedx/paragon/icons';
15
+
16
+ import CourseLockUp from './CourseLockUp';
17
+ import UserMenu from './UserMenu';
18
+ import BrandNav from './BrandNav';
19
+ import NavDropdownMenu from './NavDropdownMenu';
20
+ import messages from './messages';
21
+
22
+ const HeaderBody = ({
23
+ logo,
24
+ logoAltText,
25
+ number,
26
+ org,
27
+ title,
28
+ username,
29
+ isAdmin,
30
+ studioBaseUrl,
31
+ logoutUrl,
32
+ authenticatedUserAvatar,
33
+ isMobile,
34
+ setModalPopupTarget,
35
+ toggleModalPopup,
36
+ isModalPopupOpen,
37
+ isHiddenMainMenu,
38
+ mainMenuDropdowns,
39
+ outlineLink,
40
+ searchButtonAction,
41
+ containerProps,
42
+ }) => {
43
+ const intl = useIntl();
44
+
45
+ const renderBrandNav = (
46
+ <BrandNav
47
+ {...{
48
+ studioBaseUrl,
49
+ logo,
50
+ logoAltText,
51
+ }}
52
+ />
53
+ );
54
+
55
+ const { className: containerClassName, ...restContainerProps } = containerProps || {};
56
+
57
+ return (
58
+ <Container
59
+ size="xl"
60
+ className={classNames('px-2.5', containerClassName)}
61
+ {...restContainerProps}
62
+ >
63
+ <ActionRow as="header">
64
+ {isHiddenMainMenu ? (
65
+ <Row className="flex-nowrap ml-4">
66
+ {renderBrandNav}
67
+ </Row>
68
+ ) : (
69
+ <>
70
+ {isMobile ? (
71
+ <Button
72
+ ref={setModalPopupTarget}
73
+ className="d-inline-flex align-items-center"
74
+ variant="tertiary"
75
+ onClick={toggleModalPopup}
76
+ iconBefore={isModalPopupOpen ? Close : MenuIcon}
77
+ data-testid="mobile-menu-button"
78
+ >
79
+ Menu
80
+ </Button>
81
+ ) : (
82
+ <div className="w-25">
83
+ <Row className="m-0 flex-nowrap align-items-center">
84
+ {renderBrandNav}
85
+ <CourseLockUp
86
+ {...{
87
+ outlineLink,
88
+ number,
89
+ org,
90
+ title,
91
+ }}
92
+ />
93
+ </Row>
94
+ </div>
95
+ )}
96
+ {isMobile ? (
97
+ <>
98
+ <ActionRow.Spacer />
99
+ {renderBrandNav}
100
+ </>
101
+ ) : (
102
+ <Nav data-testid="desktop-menu" className="ml-2">
103
+ {mainMenuDropdowns.map(dropdown => {
104
+ const { id, buttonTitle, items } = dropdown;
105
+ return (
106
+ <NavDropdownMenu
107
+ key={id}
108
+ {...{
109
+ id, buttonTitle, items,
110
+ }}
111
+ />
112
+ );
113
+ })}
114
+ </Nav>
115
+ )}
116
+ </>
117
+ )}
118
+ <ActionRow.Spacer />
119
+ {searchButtonAction && (
120
+ <Nav>
121
+ <IconButton
122
+ src={Search}
123
+ iconAs={Icon}
124
+ onClick={searchButtonAction}
125
+ aria-label={intl.formatMessage(messages['header.label.search.nav'])}
126
+ alt={intl.formatMessage(messages['header.label.search.nav'])}
127
+ />
128
+ </Nav>
129
+ )}
130
+ <Nav>
131
+ <UserMenu
132
+ {...{
133
+ username,
134
+ studioBaseUrl,
135
+ logoutUrl,
136
+ authenticatedUserAvatar,
137
+ isAdmin,
138
+ isMobile,
139
+ }}
140
+ />
141
+ </Nav>
142
+ </ActionRow>
143
+ </Container>
144
+ );
145
+ };
146
+
147
+ HeaderBody.propTypes = {
148
+ studioBaseUrl: PropTypes.string.isRequired,
149
+ logoutUrl: PropTypes.string.isRequired,
150
+ setModalPopupTarget: PropTypes.func,
151
+ toggleModalPopup: PropTypes.func,
152
+ isModalPopupOpen: PropTypes.bool,
153
+ number: PropTypes.string,
154
+ org: PropTypes.string,
155
+ title: PropTypes.string,
156
+ logo: PropTypes.string,
157
+ logoAltText: PropTypes.string,
158
+ authenticatedUserAvatar: PropTypes.string,
159
+ username: PropTypes.string,
160
+ isAdmin: PropTypes.bool,
161
+ isMobile: PropTypes.bool,
162
+ isHiddenMainMenu: PropTypes.bool,
163
+ mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
164
+ id: PropTypes.string,
165
+ buttonTitle: PropTypes.node,
166
+ items: PropTypes.arrayOf(PropTypes.shape({
167
+ href: PropTypes.string,
168
+ title: PropTypes.node,
169
+ })),
170
+ })),
171
+ outlineLink: PropTypes.string,
172
+ searchButtonAction: PropTypes.func,
173
+ containerProps: PropTypes.shape(Container.propTypes),
174
+ };
175
+
176
+ HeaderBody.defaultProps = {
177
+ setModalPopupTarget: null,
178
+ toggleModalPopup: null,
179
+ isModalPopupOpen: false,
180
+ logo: null,
181
+ logoAltText: null,
182
+ number: '',
183
+ org: '',
184
+ title: '',
185
+ authenticatedUserAvatar: null,
186
+ username: null,
187
+ isAdmin: false,
188
+ isMobile: false,
189
+ isHiddenMainMenu: false,
190
+ mainMenuDropdowns: [],
191
+ outlineLink: null,
192
+ searchButtonAction: null,
193
+ containerProps: {},
194
+ };
195
+
196
+ export default HeaderBody;
@@ -0,0 +1,74 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useToggle, ModalPopup } from '@openedx/paragon';
4
+ import HeaderBody from './HeaderBody';
5
+ import MobileMenu from './MobileMenu';
6
+
7
+ const MobileHeader = ({
8
+ mainMenuDropdowns,
9
+ ...props
10
+ }) => {
11
+ const [isOpen, , close, toggle] = useToggle(false);
12
+ const [target, setTarget] = useState(null);
13
+
14
+ return (
15
+ <>
16
+ {/* @ts-expect-error The type of 'props' is any until we convert from propTypes to TypeScript interface/types */}
17
+ <HeaderBody
18
+ {...props}
19
+ isMobile
20
+ setModalPopupTarget={setTarget}
21
+ toggleModalPopup={toggle}
22
+ isModalPopupOpen={isOpen}
23
+ />
24
+ <ModalPopup
25
+ hasArrow
26
+ placement="bottom"
27
+ positionRef={target}
28
+ isOpen={isOpen}
29
+ onClose={close}
30
+ onEscapeKey={close}
31
+ className="mobile-menu-container"
32
+ >
33
+ <MobileMenu {...{ mainMenuDropdowns }} />
34
+ </ModalPopup>
35
+ </>
36
+ );
37
+ };
38
+
39
+ MobileHeader.propTypes = {
40
+ studioBaseUrl: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
41
+ logoutUrl: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
42
+ number: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
43
+ org: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
44
+ title: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
45
+ logo: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
46
+ logoAltText: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
47
+ authenticatedUserAvatar: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
48
+ username: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
49
+ isAdmin: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
50
+ mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
51
+ id: PropTypes.string,
52
+ buttonTitle: PropTypes.node,
53
+ items: PropTypes.arrayOf(PropTypes.shape({
54
+ href: PropTypes.string,
55
+ title: PropTypes.node,
56
+ })),
57
+ })),
58
+ outlineLink: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
59
+ };
60
+
61
+ MobileHeader.defaultProps = {
62
+ logo: null,
63
+ logoAltText: null,
64
+ number: null,
65
+ org: null,
66
+ title: null,
67
+ authenticatedUserAvatar: null,
68
+ username: null,
69
+ isAdmin: false,
70
+ mainMenuDropdowns: [],
71
+ outlineLink: null,
72
+ };
73
+
74
+ export default MobileHeader;
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+
5
+ import '@testing-library/jest-dom/extend-expect';
6
+ import MobileMenu from './MobileMenu';
7
+
8
+ const mockOnNavigate = jest.fn();
9
+
10
+ const defaultProps = {
11
+ mainMenuDropdowns: [
12
+ {
13
+ id: 'menu1',
14
+ buttonTitle: 'Menu 1',
15
+ items: [
16
+ { href: '/menu1/item1', title: 'Item 1' },
17
+ { href: '/menu1/item2', title: 'Item 2' },
18
+ ],
19
+ },
20
+ {
21
+ id: 'menu2',
22
+ buttonTitle: 'Menu 2',
23
+ items: [
24
+ { href: 'https://external-link.com', title: 'External Link' },
25
+ ],
26
+ },
27
+ ],
28
+ onNavigate: mockOnNavigate,
29
+ };
30
+
31
+ const RootWrapper = (props) => (
32
+ <MemoryRouter>
33
+ <MobileMenu {...props} />
34
+ </MemoryRouter>
35
+ );
36
+
37
+ describe('MobileMenu Component', () => {
38
+ afterEach(() => {
39
+ jest.clearAllMocks();
40
+ });
41
+
42
+ test('renders the mobile menu with dropdowns and items', () => {
43
+ render(<RootWrapper {...defaultProps} />);
44
+
45
+ const menu1Title = screen.getByText('Menu 1');
46
+ const menu2Title = screen.getByText('Menu 2');
47
+
48
+ expect(menu1Title).toBeInTheDocument();
49
+ expect(menu2Title).toBeInTheDocument();
50
+ });
51
+
52
+ test('navigates to internal URL when item is clicked', () => {
53
+ render(<RootWrapper {...defaultProps} />);
54
+
55
+ const menu1Title = screen.getByText(defaultProps.mainMenuDropdowns[0].buttonTitle);
56
+ fireEvent.click(menu1Title);
57
+
58
+ const menuItem = screen.getByText(defaultProps.mainMenuDropdowns[0].items[0].title);
59
+ expect(menuItem.getAttribute('href')).toBe(defaultProps.mainMenuDropdowns[0].items[0].href);
60
+ });
61
+
62
+ test('navigates to an external URL when external link is clicked', () => {
63
+ render(<RootWrapper {...defaultProps} />);
64
+
65
+ const menu2Title = screen.getByText(defaultProps.mainMenuDropdowns[1].buttonTitle);
66
+ fireEvent.click(menu2Title);
67
+
68
+ const externalLink = screen.getByText(defaultProps.mainMenuDropdowns[1].items[0].title);
69
+ expect(externalLink.getAttribute('href')).toBe(defaultProps.mainMenuDropdowns[1].items[0].href);
70
+ });
71
+
72
+ test('renders empty state when there are no dropdowns', () => {
73
+ render(<RootWrapper mainMenuDropdowns={[]} onNavigate={mockOnNavigate} />);
74
+
75
+ const mobileMenu = screen.getByTestId('mobile-menu');
76
+ expect(mobileMenu).toBeInTheDocument();
77
+
78
+ const menuItems = screen.queryAllByRole('listitem');
79
+ expect(menuItems.length).toBe(0);
80
+ });
81
+ });
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Collapsible } from '@openedx/paragon';
4
+ import { Link } from 'react-router-dom';
5
+
6
+ const MobileMenu = ({ mainMenuDropdowns }) => (
7
+ <div
8
+ className="ml-4 p-2 bg-light-100 border border-gray-200 small rounded"
9
+ data-testid="mobile-menu"
10
+ >
11
+ <div>
12
+ {mainMenuDropdowns.map(dropdown => {
13
+ const { id, buttonTitle, items } = dropdown;
14
+ return (
15
+ <Collapsible
16
+ className="border-light-100"
17
+ title={buttonTitle}
18
+ key={id}
19
+ >
20
+ <ul className="p-0" style={{ listStyleType: 'none' }}>
21
+ {items.map(item => (
22
+ <li className="mobile-menu-item">
23
+ <Link to={item.href}>
24
+ {item.title}
25
+ </Link>
26
+ </li>
27
+ ))}
28
+ </ul>
29
+ </Collapsible>
30
+ );
31
+ })}
32
+ </div>
33
+ </div>
34
+ );
35
+
36
+ MobileMenu.propTypes = {
37
+ mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({
38
+ id: PropTypes.string,
39
+ buttonTitle: PropTypes.node,
40
+ items: PropTypes.arrayOf(PropTypes.shape({
41
+ href: PropTypes.string,
42
+ title: PropTypes.node,
43
+ })),
44
+ })),
45
+ };
46
+ MobileMenu.defaultProps = {
47
+ mainMenuDropdowns: [],
48
+ };
49
+
50
+ export default MobileMenu;
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import { MemoryRouter } from 'react-router-dom';
5
+
6
+ import NavDropdownMenu from './NavDropdownMenu';
7
+
8
+ const defaultProps = {
9
+ id: 'menu-id',
10
+ buttonTitle: 'Menu',
11
+ items: [
12
+ { href: '/item1', title: 'Item 1' },
13
+ { href: 'https://external.com', title: 'External Link' },
14
+ ],
15
+ };
16
+
17
+ const RootWrapper = (props) => (
18
+ <MemoryRouter>
19
+ <NavDropdownMenu {...props} />
20
+ </MemoryRouter>
21
+ );
22
+
23
+ describe('NavDropdownMenu Component', () => {
24
+ afterEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ test('renders the dropdown button with correct title', () => {
29
+ render(<NavDropdownMenu {...defaultProps} />);
30
+
31
+ const dropdownButton = screen.getByRole('button', { name: defaultProps.buttonTitle });
32
+ expect(dropdownButton).toBeInTheDocument();
33
+ });
34
+
35
+ test('renders all dropdown items', () => {
36
+ render(<RootWrapper {...defaultProps} />);
37
+
38
+ const dropdownButton = screen.getByRole('button', { name: defaultProps.buttonTitle });
39
+ fireEvent.click(dropdownButton);
40
+
41
+ const item1 = screen.getByText(defaultProps.items[0].title);
42
+ const externalLink = screen.getByText(defaultProps.items[1].title);
43
+
44
+ expect(item1).toBeInTheDocument();
45
+ expect(externalLink).toBeInTheDocument();
46
+ });
47
+
48
+ test('calls onNavigate with the correct URL for internal link', () => {
49
+ render(<RootWrapper {...defaultProps} />);
50
+
51
+ const dropdownButton = screen.getByRole('button', { name: defaultProps.buttonTitle });
52
+ fireEvent.click(dropdownButton);
53
+
54
+ const item1 = screen.getByText(defaultProps.items[0].title);
55
+ expect(item1.getAttribute('href')).toBe(defaultProps.items[0].href);
56
+ });
57
+
58
+ test('navigates to external URL when external link is clicked', () => {
59
+ render(<RootWrapper {...defaultProps} />);
60
+
61
+ const dropdownButton = screen.getByRole('button', { name: defaultProps.buttonTitle });
62
+ fireEvent.click(dropdownButton);
63
+
64
+ const externalLink = screen.getByText(defaultProps.items[1].title);
65
+ expect(externalLink.getAttribute('href')).toBe(defaultProps.items[1].href);
66
+ });
67
+ });
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ Dropdown,
5
+ DropdownButton,
6
+ } from '@openedx/paragon';
7
+ import { Link } from 'react-router-dom';
8
+
9
+ const NavDropdownMenu = ({
10
+ id,
11
+ buttonTitle,
12
+ items,
13
+ }) => (
14
+ <DropdownButton
15
+ id={id}
16
+ title={buttonTitle}
17
+ variant="outline-primary"
18
+ className="mr-2"
19
+ >
20
+ {items.map(item => (
21
+ <Dropdown.Item
22
+ as={Link}
23
+ key={`${item.title}-dropdown-item`}
24
+ to={item.href}
25
+ className="small"
26
+ >
27
+ {item.title}
28
+ </Dropdown.Item>
29
+ ))}
30
+ </DropdownButton>
31
+ );
32
+
33
+ NavDropdownMenu.propTypes = {
34
+ id: PropTypes.string.isRequired,
35
+ buttonTitle: PropTypes.node.isRequired,
36
+ items: PropTypes.arrayOf(PropTypes.shape({
37
+ href: PropTypes.string.isRequired,
38
+ title: PropTypes.node.isRequired,
39
+ })).isRequired,
40
+ };
41
+
42
+ export default NavDropdownMenu;
@@ -4,13 +4,11 @@ $white: #FFFFFF;
4
4
  .studio-header {
5
5
  position: relative;
6
6
  z-index: 1000;
7
-
8
- height: 3.75rem;
9
7
  box-shadow: 0 1px 0 0 rgb(0 0 0 / .1);
10
- background: $white;
8
+ background: var(--pgn-color-white, $white);
11
9
 
12
10
  .btn-outline-primary {
13
- border-color: $white;
11
+ border-color: var(--pgn-color-white, $white);
14
12
  }
15
13
 
16
14
  .logo {
@@ -19,8 +17,8 @@ $white: #FFFFFF;
19
17
  position: relative;
20
18
  top: -.05em;
21
19
  height: 1.75rem;
22
- padding: $spacer 0;
23
- margin-right: $spacer;
20
+ padding: var(--pgn-spacing-spacer-base, $spacer) 0;
21
+ margin-right: var(--pgn-spacing-spacer-base, $spacer);
24
22
 
25
23
  img {
26
24
  display: block;
@@ -29,17 +27,17 @@ $white: #FFFFFF;
29
27
  }
30
28
 
31
29
  .course-title-lockup {
30
+ overflow: hidden;
31
+
32
32
  @media only screen and (min-width: 769px) {
33
33
  padding: .5rem;
34
- padding-right: $spacer;
34
+ padding-right: var(--pgn-spacing-spacer-base, $spacer);
35
35
  border-right: 1px solid #E5E5E5;
36
36
  width: 70%;
37
37
  }
38
38
 
39
- overflow: hidden;
40
-
41
39
  span {
42
- color: #333333;
40
+ color: var(--pgn-color-gray-800, #333333);
43
41
  white-space: nowrap;
44
42
  overflow: hidden;
45
43
  text-overflow: ellipsis;