Skip to content

Commit 6cab3f3

Browse files
Merge pull request #466 from openedx/sajjad/VAN-1823-custom-header
feat: enable header to accept custom menus
2 parents e3c8ec0 + e6aa4be commit 6cab3f3

File tree

8 files changed

+303
-104
lines changed

8 files changed

+303
-104
lines changed

docs/images/desktop_header.png

123 KB
Loading

docs/images/mobile_main_menu.png

35.2 KB
Loading

docs/images/mobile_user_menu.png

48.9 KB
Loading

docs/using_custom_header.rst

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
.. title:: Custom Header Component Documentation
2+
3+
Custom Header Component
4+
=======================
5+
6+
Overview
7+
--------
8+
9+
The ``Header`` component is used to display a header with a provided ``mainMenuItems``,
10+
``secondaryMenuItems``, and ``userMenuItems`` props. If props are provided, the component will use them; otherwise,
11+
if any of the props ``(mainMenuItems, secondaryMenuItems, userMenuItems)`` are not provided, default
12+
items will be displayed. This component provides flexibility in customization, making it suitable for a wide
13+
range of applications.
14+
15+
Props Details
16+
-------------
17+
18+
The `Header` component accepts the following **optional** props for customization:
19+
20+
``mainMenuItems``
21+
*****************
22+
23+
The main menu items is a list of menu items objects. On desktop screens, these items are displayed on the left side next to the logo icon.
24+
On mobile screens, the main menu is displayed as a dropdown menu triggered by a hamburger icon. The main menu dropdown appears below the logo when opened.
25+
26+
Example:
27+
::
28+
29+
[
30+
{ type: 'item', href: '/courses', content: 'Courses', isActive: true },
31+
{ type: 'item', href: '/programs', content: 'Programs' },
32+
{ type: 'item', href: '/discover', content: 'Discover New', disabled, true },
33+
{
34+
type: 'submenu',
35+
content: 'Sub Menu Item',
36+
submenuContent: (
37+
<>
38+
<div className="mb-1"><a rel="noopener" href="#">Submenu item 1</a></div>
39+
<div className="mb-1"><a rel="noopener" href="#">Submenu item 2</a></div>
40+
</>
41+
),
42+
},
43+
]
44+
45+
**Submenu Implementation**
46+
47+
To implement a submenu, set the type to ``submenu`` and provide a ``submenuContent`` property.
48+
The submenuContent should be a React component (as shown in above example) that can be rendered.
49+
50+
**Note:**
51+
52+
- The ``type`` should be ``item`` or ``submenu``. If type is ``submenu``, it should contain ``submenuContent`` instead of ``href``.
53+
54+
- If any item is to be disabled, we can pass optional ``disabled: true`` in that item object and
55+
56+
- If any item is to be active, we can pass optional ``isActive: true`` in that item object
57+
58+
secondaryMenuItems
59+
******************
60+
61+
The secondary menu items has same structure as ``mainMenuItems``. On desktop screen, these items are displayed on the right of header just before the userMenu avatar and on mobile screen,
62+
these items are displayed below the mainMenu items in dropdown.
63+
64+
Example:
65+
::
66+
67+
[
68+
{ type: 'item', href: '/help', content: 'Help' },
69+
]
70+
71+
userMenuItems
72+
*************
73+
74+
The user menu items is list of objects. On desktop screens, these items are displayed as a dropdown menu on the most right side of the header. The dropdown is opened by clicking on the avatar icon, which is typically located at the far right of the header.
75+
On mobile screens, the user menu is also displayed as a dropdown menu, appearing under the avatar icon.
76+
77+
Each object represents a group in the user menu. Each object contains the ``heading`` and
78+
list of menu items to be displayed in that group. Heading is optional and will be displayed only if passed. There can
79+
be multiple groups. For a normal user menu, a single group can be passed with empty heading.
80+
81+
Example:
82+
::
83+
84+
[
85+
{
86+
heading: '',
87+
items: [
88+
{ type: 'item', href: '/profile', content: 'Profile' },
89+
{ type: 'item', href: '/logout', content: 'Logout' }
90+
]
91+
},
92+
]
93+
94+
Screenshots
95+
***********
96+
97+
Desktop:
98+
99+
.. image:: ./images/desktop_header.png
100+
101+
Mobile:
102+
103+
.. image:: ./images/mobile_main_menu.png
104+
.. image:: ./images/mobile_user_menu.png
105+
106+
Some Important Notes
107+
--------------------
108+
109+
- Intl formatted strings should be passed in content attribute.
110+
- Only menu items in the main menu can be disabled.
111+
- Menu items in the main menu and user menu can have ``isActive`` prop.

src/DesktopHeader.jsx

Lines changed: 57 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,31 @@ class DesktopHeader extends React.Component {
1919
super(props);
2020
}
2121

22-
renderMainMenu() {
23-
const { mainMenu } = this.props;
24-
22+
renderMenu(menu) {
2523
// Nodes are accepted as a prop
26-
if (!Array.isArray(mainMenu)) {
27-
return mainMenu;
24+
if (!Array.isArray(menu)) {
25+
return menu;
2826
}
2927

30-
return mainMenu.map((menuItem) => {
28+
return menu.map((menuItem) => {
3129
const {
3230
type,
3331
href,
3432
content,
3533
submenuContent,
34+
disabled,
35+
isActive,
3636
} = menuItem;
3737

3838
if (type === 'item') {
3939
return (
40-
<a key={`${type}-${content}`} className="nav-link" href={href}>{content}</a>
40+
<a
41+
key={`${type}-${content}`}
42+
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
43+
href={href}
44+
>
45+
{content}
46+
</a>
4147
);
4248
}
4349

@@ -54,22 +60,14 @@ class DesktopHeader extends React.Component {
5460
});
5561
}
5662

57-
// Renders an optional App Menu for
58-
renderAppMenu() {
59-
const { appMenu } = this.props;
60-
const { content: appMenuContent, menuItems } = appMenu;
61-
return (
62-
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
63-
<MenuTrigger tag="a" className="nav-link d-inline-flex align-items-center">
64-
{appMenuContent} <CaretIcon role="img" aria-hidden focusable="false" />
65-
</MenuTrigger>
66-
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
67-
{menuItems.map(({ type, href, content }) => (
68-
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
69-
))}
70-
</MenuContent>
71-
</Menu>
72-
);
63+
renderMainMenu() {
64+
const { mainMenu } = this.props;
65+
return this.renderMenu(mainMenu);
66+
}
67+
68+
renderSecondaryMenu() {
69+
const { secondaryMenu } = this.props;
70+
return this.renderMenu(secondaryMenu);
7371
}
7472

7573
renderUserMenu() {
@@ -91,8 +89,23 @@ class DesktopHeader extends React.Component {
9189
{username} <CaretIcon role="img" aria-hidden focusable="false" />
9290
</MenuTrigger>
9391
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
94-
{userMenu.map(({ type, href, content }) => (
95-
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
92+
{userMenu.map((group, index) => (
93+
// eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key
94+
<React.Fragment key={index}>
95+
{group.heading && <div className="dropdown-header" role="heading" aria-level="1">{group.heading}</div>}
96+
{group.items.map(({
97+
type, content, href, disabled, isActive,
98+
}) => (
99+
<a
100+
className={`dropdown-${type}${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
101+
key={`${type}-${content}`}
102+
href={href}
103+
>
104+
{content}
105+
</a>
106+
))}
107+
{index < userMenu.length - 1 && <div className="dropdown-divider" role="separator" />}
108+
</React.Fragment>
96109
))}
97110
</MenuContent>
98111
</Menu>
@@ -120,7 +133,6 @@ class DesktopHeader extends React.Component {
120133
logoDestination,
121134
loggedIn,
122135
intl,
123-
appMenu,
124136
} = this.props;
125137
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
126138
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
@@ -137,19 +149,17 @@ class DesktopHeader extends React.Component {
137149
>
138150
{this.renderMainMenu()}
139151
</nav>
140-
{appMenu ? (
141-
<nav
142-
aria-label={intl.formatMessage(messages['header.label.app.nav'])}
143-
className="nav app-nav"
144-
>
145-
{this.renderAppMenu()}
146-
</nav>
147-
) : null}
148152
<nav
149153
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
150154
className="nav secondary-menu-container align-items-center ml-auto"
151155
>
152-
{loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()}
156+
{loggedIn
157+
? (
158+
<>
159+
{this.renderSecondaryMenu()}
160+
{this.renderUserMenu()}
161+
</>
162+
) : this.renderLoggedOutItems()}
153163
</nav>
154164
</div>
155165
</div>
@@ -163,10 +173,18 @@ DesktopHeader.propTypes = {
163173
PropTypes.node,
164174
PropTypes.array,
165175
]),
176+
secondaryMenu: PropTypes.oneOfType([
177+
PropTypes.node,
178+
PropTypes.array,
179+
]),
166180
userMenu: PropTypes.arrayOf(PropTypes.shape({
167-
type: PropTypes.oneOf(['item', 'menu']),
168-
href: PropTypes.string,
169-
content: PropTypes.string,
181+
heading: PropTypes.string,
182+
items: PropTypes.arrayOf(PropTypes.shape({
183+
type: PropTypes.oneOf(['item', 'menu']),
184+
href: PropTypes.string,
185+
content: PropTypes.string,
186+
isActive: PropTypes.bool,
187+
})),
170188
})),
171189
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
172190
type: PropTypes.oneOf(['item', 'menu']),
@@ -182,24 +200,11 @@ DesktopHeader.propTypes = {
182200

183201
// i18n
184202
intl: intlShape.isRequired,
185-
186-
// appMenu
187-
appMenu: PropTypes.shape(
188-
{
189-
content: PropTypes.string,
190-
menuItems: PropTypes.arrayOf(
191-
PropTypes.shape({
192-
type: PropTypes.string,
193-
href: PropTypes.string,
194-
content: PropTypes.string,
195-
}),
196-
),
197-
},
198-
),
199203
};
200204

201205
DesktopHeader.defaultProps = {
202206
mainMenu: [],
207+
secondaryMenu: [],
203208
userMenu: [],
204209
loggedOutItems: [],
205210
logo: null,
@@ -208,7 +213,6 @@ DesktopHeader.defaultProps = {
208213
avatar: null,
209214
username: null,
210215
loggedIn: false,
211-
appMenu: null,
212216
};
213217

214218
export default injectIntl(DesktopHeader);

0 commit comments

Comments
 (0)