Skip to content

Commit db912e6

Browse files
feat: enable header to accept custom menus
1 parent abb08be commit db912e6

File tree

7 files changed

+309
-107
lines changed

7 files changed

+309
-107
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: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 ``logo``, ``mainMenuItems``,
10+
``secondaryMenuItems``, and ``userMenuItems`` props. If props are provided, the component will use them; otherwise,
11+
If any of the props ``(logo, mainMenuItems, secondaryMenuItems, userMenuItems)`` are not provided, default
12+
items are 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+
``logo``
21+
*******
22+
23+
The logo prop is an object containing `src`, `alt`, and `href` properties. If not passed, LOGO_URL from config will be used.
24+
It is displayed on the left of the header in the desktop screen and in the center of the header on the mobile screen.
25+
26+
Example:
27+
::
28+
29+
{
30+
src: 'path/to/logo.png',
31+
alt: 'Logo Alt Text',
32+
href: '/home'
33+
}
34+
35+
``mainMenuItems``
36+
*****************
37+
38+
The main menu items is a list of menu items objects. On desktop screens, these items are displayed on the left, to the right of the logo icon and to the left of the secondary menu.
39+
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.
40+
41+
Example:
42+
::
43+
44+
[
45+
{ type: 'item', href: '/courses', content: 'Courses', isActive: true },
46+
{ type: 'item', href: '/programs', content: 'Programs' },
47+
{ type: 'item', href: '/discover', content: 'Discover New', disabled, true },
48+
{
49+
type: 'submenu',
50+
content: 'Sub Menu Item',
51+
submenuContent: [
52+
'<div className="mb-1"><a rel="noopener" href="#">Submenu item 1</a></div>',
53+
'<div className="mb-1"><a rel="noopener" href="#">Submenu item 2</a></div>'
54+
],
55+
},
56+
]
57+
58+
**Note:**
59+
60+
- The ``type`` should be ``item`` or ``submenu``. If type is ``submenu``, it should contain ``submenuContent`` instead of ``href``.
61+
62+
- If any item is to be disabled, we can pass optional ``disabled: true`` in that item object and
63+
64+
- If any item is to be active, we can pass optional ``isActive: true`` in that item object
65+
66+
secondaryMenuItems
67+
******************
68+
69+
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,
70+
these items are displayed below the mainMenu items in dropdown.
71+
72+
Example:
73+
::
74+
75+
[
76+
{ type: 'item', href: '/help', content: 'Help' },
77+
]
78+
79+
userMenuItems
80+
*************
81+
82+
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.
83+
On mobile screens, the user menu is also displayed as a dropdown menu, appearing under the avatar icon.
84+
85+
User Menu is list of objects. Each object represent a group in user menu. Each object contains the ``heading`` and
86+
list of menu items to be displayed in that group. Heading is optional and will be displayed only if passed. There can
87+
be multiple groups. For a normal user menu, we can pass single group with empty heading.
88+
89+
Example:
90+
::
91+
92+
[
93+
{
94+
heading: '',
95+
items: [
96+
{ type: 'item', href: '/profile', content: 'Profile' },
97+
{ type: 'item', href: '/logout', content: 'Logout' }
98+
]
99+
},
100+
]
101+
102+
Screenshots
103+
***********
104+
105+
Desktop:
106+
107+
.. image:: ./images/desktop_header.png
108+
109+
Mobile:
110+
111+
.. image:: ./images/mobile_main_menu.png
112+
.. image:: ./images/mobile_user_menu.png
113+
114+
Some Important Notes
115+
--------------------
116+
117+
- Intl formatted strings should be passed in content attribute.
118+
- Only menu items in the main menu can be disabled.
119+
- 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)