Skip to content

Commit 6972d0d

Browse files
feat: enable configurable header (#560)
1 parent 4d37948 commit 6972d0d

File tree

8 files changed

+321
-75
lines changed

8 files changed

+321
-75
lines changed

docs/images/desktop_header.png

192 KB
Loading

docs/images/mobile_main_menu.png

44.3 KB
Loading

docs/images/mobile_user_menu.png

81.1 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: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,31 @@ class DesktopHeader extends React.Component {
2020
super(props);
2121
}
2222

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

31-
return mainMenu.map((menuItem) => {
29+
return menu.map((menuItem) => {
3230
const {
3331
type,
3432
href,
3533
content,
3634
submenuContent,
35+
disabled,
36+
isActive,
3737
} = menuItem;
3838

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

@@ -55,6 +61,16 @@ class DesktopHeader extends React.Component {
5561
});
5662
}
5763

64+
renderMainMenu() {
65+
const { mainMenu } = this.props;
66+
return this.renderMenu(mainMenu);
67+
}
68+
69+
renderSecondaryMenu() {
70+
const { secondaryMenu } = this.props;
71+
return this.renderMenu(secondaryMenu);
72+
}
73+
5874
renderUserMenu() {
5975
const {
6076
userMenu,
@@ -86,10 +102,23 @@ class DesktopHeader extends React.Component {
86102
/>
87103
</Dropdown.Item>
88104
)}
89-
{userMenu.map(({ type, href, content }) => (
90-
<Dropdown.Item key={`${type}-${content}`} href={href}>
91-
{content}
92-
</Dropdown.Item>
105+
{userMenu.map((group, index) => (
106+
// eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key
107+
<React.Fragment key={index}>
108+
{group.heading && <Dropdown.Header>{group.heading}</Dropdown.Header>}
109+
{group.items.map(({
110+
type, content, href, disabled, isActive,
111+
}) => (
112+
<Dropdown.Item
113+
key={`${type}-${content}`}
114+
href={href}
115+
className={`${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
116+
>
117+
{content}
118+
</Dropdown.Item>
119+
))}
120+
{index < userMenu.length - 1 && <Dropdown.Divider />}
121+
</React.Fragment>
93122
))}
94123
</Dropdown.Menu>
95124
</Dropdown>
@@ -137,7 +166,13 @@ class DesktopHeader extends React.Component {
137166
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
138167
className="nav secondary-menu-container align-items-center ml-auto"
139168
>
140-
{loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()}
169+
{loggedIn
170+
? (
171+
<>
172+
{this.renderSecondaryMenu()}
173+
{this.renderUserMenu()}
174+
</>
175+
) : this.renderLoggedOutItems()}
141176
</nav>
142177
</div>
143178
</div>
@@ -151,10 +186,19 @@ DesktopHeader.propTypes = {
151186
PropTypes.node,
152187
PropTypes.array,
153188
]),
189+
secondaryMenu: PropTypes.oneOfType([
190+
PropTypes.node,
191+
PropTypes.array,
192+
]),
154193
userMenu: PropTypes.arrayOf(PropTypes.shape({
155-
type: PropTypes.oneOf(['item', 'menu']),
156-
href: PropTypes.string,
157-
content: PropTypes.string,
194+
heading: PropTypes.string,
195+
items: PropTypes.arrayOf(PropTypes.shape({
196+
type: PropTypes.oneOf(['item', 'menu']),
197+
href: PropTypes.string,
198+
content: PropTypes.string,
199+
disabled: PropTypes.bool,
200+
isActive: PropTypes.bool,
201+
})),
158202
})),
159203
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
160204
type: PropTypes.oneOf(['item', 'menu']),
@@ -175,6 +219,7 @@ DesktopHeader.propTypes = {
175219

176220
DesktopHeader.defaultProps = {
177221
mainMenu: [],
222+
secondaryMenu: [],
178223
userMenu: [],
179224
loggedOutItems: [],
180225
logo: null,

0 commit comments

Comments
 (0)