feat: enable header to accept custom menus
This commit is contained in:
@@ -19,25 +19,31 @@ class DesktopHeader extends React.Component {
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderMainMenu() {
|
||||
const { mainMenu } = this.props;
|
||||
|
||||
renderMenu(menu) {
|
||||
// Nodes are accepted as a prop
|
||||
if (!Array.isArray(mainMenu)) {
|
||||
return mainMenu;
|
||||
if (!Array.isArray(menu)) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
return mainMenu.map((menuItem) => {
|
||||
return menu.map((menuItem) => {
|
||||
const {
|
||||
type,
|
||||
href,
|
||||
content,
|
||||
submenuContent,
|
||||
disabled,
|
||||
isActive,
|
||||
} = menuItem;
|
||||
|
||||
if (type === 'item') {
|
||||
return (
|
||||
<a key={`${type}-${content}`} className="nav-link" href={href}>{content}</a>
|
||||
<a
|
||||
key={`${type}-${content}`}
|
||||
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
|
||||
href={href}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,22 +60,14 @@ class DesktopHeader extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
// Renders an optional App Menu for
|
||||
renderAppMenu() {
|
||||
const { appMenu } = this.props;
|
||||
const { content: appMenuContent, menuItems } = appMenu;
|
||||
return (
|
||||
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
|
||||
<MenuTrigger tag="a" className="nav-link d-inline-flex align-items-center">
|
||||
{appMenuContent} <CaretIcon role="img" aria-hidden focusable="false" />
|
||||
</MenuTrigger>
|
||||
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
|
||||
{menuItems.map(({ type, href, content }) => (
|
||||
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
|
||||
))}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
renderMainMenu() {
|
||||
const { mainMenu } = this.props;
|
||||
return this.renderMenu(mainMenu);
|
||||
}
|
||||
|
||||
renderSecondaryMenu() {
|
||||
const { secondaryMenu } = this.props;
|
||||
return this.renderMenu(secondaryMenu);
|
||||
}
|
||||
|
||||
renderUserMenu() {
|
||||
@@ -91,8 +89,23 @@ class DesktopHeader extends React.Component {
|
||||
{username} <CaretIcon role="img" aria-hidden focusable="false" />
|
||||
</MenuTrigger>
|
||||
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
|
||||
{userMenu.map(({ type, href, content }) => (
|
||||
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
|
||||
{userMenu.map((group, index) => (
|
||||
// eslint-disable-next-line react/jsx-no-comment-textnodes,react/no-array-index-key
|
||||
<React.Fragment key={index}>
|
||||
{group.heading && <div className="dropdown-header" role="heading" aria-level="1">{group.heading}</div>}
|
||||
{group.items.map(({
|
||||
type, content, href, disabled, isActive,
|
||||
}) => (
|
||||
<a
|
||||
className={`dropdown-${type}${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
|
||||
key={`${type}-${content}`}
|
||||
href={href}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
))}
|
||||
{index < userMenu.length - 1 && <div className="dropdown-divider" role="separator" />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
@@ -120,7 +133,6 @@ class DesktopHeader extends React.Component {
|
||||
logoDestination,
|
||||
loggedIn,
|
||||
intl,
|
||||
appMenu,
|
||||
} = this.props;
|
||||
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
|
||||
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
|
||||
@@ -137,19 +149,17 @@ class DesktopHeader extends React.Component {
|
||||
>
|
||||
{this.renderMainMenu()}
|
||||
</nav>
|
||||
{appMenu ? (
|
||||
<nav
|
||||
aria-label={intl.formatMessage(messages['header.label.app.nav'])}
|
||||
className="nav app-nav"
|
||||
>
|
||||
{this.renderAppMenu()}
|
||||
</nav>
|
||||
) : null}
|
||||
<nav
|
||||
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
|
||||
className="nav secondary-menu-container align-items-center ml-auto"
|
||||
>
|
||||
{loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()}
|
||||
{loggedIn
|
||||
? (
|
||||
<>
|
||||
{this.renderSecondaryMenu()}
|
||||
{this.renderUserMenu()}
|
||||
</>
|
||||
) : this.renderLoggedOutItems()}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,10 +173,18 @@ DesktopHeader.propTypes = {
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
secondaryMenu: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
userMenu: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
heading: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
})),
|
||||
})),
|
||||
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
@@ -182,24 +200,11 @@ DesktopHeader.propTypes = {
|
||||
|
||||
// i18n
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
// appMenu
|
||||
appMenu: PropTypes.shape(
|
||||
{
|
||||
content: PropTypes.string,
|
||||
menuItems: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
DesktopHeader.defaultProps = {
|
||||
mainMenu: [],
|
||||
secondaryMenu: [],
|
||||
userMenu: [],
|
||||
loggedOutItems: [],
|
||||
logo: null,
|
||||
@@ -208,7 +213,6 @@ DesktopHeader.defaultProps = {
|
||||
avatar: null,
|
||||
username: null,
|
||||
loggedIn: false,
|
||||
appMenu: null,
|
||||
};
|
||||
|
||||
export default injectIntl(DesktopHeader);
|
||||
|
||||
125
src/Header.jsx
125
src/Header.jsx
@@ -10,6 +10,7 @@ import {
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import DesktopHeader from './DesktopHeader';
|
||||
import MobileHeader from './MobileHeader';
|
||||
|
||||
@@ -30,50 +31,66 @@ subscribe(APP_CONFIG_INITIALIZED, () => {
|
||||
}, 'Header additional config');
|
||||
});
|
||||
|
||||
const Header = ({ intl }) => {
|
||||
/**
|
||||
* Header component for the application.
|
||||
* Displays a header with the provided logo, main menu, secondary menu, and user menu when the user is authenticated.
|
||||
* If any of the props (logo, mainMenuItems, secondaryMenuItems, userMenuItems) are not provided, default
|
||||
* items are displayed.
|
||||
* For more details on how to use this component, please refer to this document:
|
||||
* https://github.com/openedx/frontend-component-header/blob/master/docs/using_custom_header.rst
|
||||
*
|
||||
* @param logo
|
||||
* @param mainMenuItems
|
||||
* @param secondaryMenuItems
|
||||
* @param userMenuItems
|
||||
*/
|
||||
const Header = ({
|
||||
intl, logo, mainMenuItems, secondaryMenuItems, userMenuItems,
|
||||
}) => {
|
||||
const { authenticatedUser, config } = useContext(AppContext);
|
||||
|
||||
const mainMenu = [
|
||||
const defaultMainMenu = [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.LMS_BASE_URL}/dashboard`,
|
||||
content: intl.formatMessage(messages['header.links.courses']),
|
||||
},
|
||||
];
|
||||
const defaultUserMenu = authenticatedUser === null ? [] : [{
|
||||
heading: '',
|
||||
items: [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.LMS_BASE_URL}/dashboard`,
|
||||
content: intl.formatMessage(messages['header.user.menu.dashboard']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`,
|
||||
content: intl.formatMessage(messages['header.user.menu.profile']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: config.ACCOUNT_SETTINGS_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.account.settings']),
|
||||
},
|
||||
// Users should only see Order History if have a ORDER_HISTORY_URL define in the environment.
|
||||
...(config.ORDER_HISTORY_URL ? [{
|
||||
type: 'item',
|
||||
href: config.ORDER_HISTORY_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.order.history']),
|
||||
}] : []),
|
||||
{
|
||||
type: 'item',
|
||||
href: config.LOGOUT_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.logout']),
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
const orderHistoryItem = {
|
||||
type: 'item',
|
||||
href: config.ORDER_HISTORY_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.order.history']),
|
||||
};
|
||||
|
||||
const userMenu = authenticatedUser === null ? [] : [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.LMS_BASE_URL}/dashboard`,
|
||||
content: intl.formatMessage(messages['header.user.menu.dashboard']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${config.ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`,
|
||||
content: intl.formatMessage(messages['header.user.menu.profile']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: config.ACCOUNT_SETTINGS_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.account.settings']),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: config.LOGOUT_URL,
|
||||
content: intl.formatMessage(messages['header.user.menu.logout']),
|
||||
},
|
||||
];
|
||||
|
||||
// Users should only see Order History if have a ORDER_HISTORY_URL define in the environment.
|
||||
if (config.ORDER_HISTORY_URL) {
|
||||
userMenu.splice(-1, 0, orderHistoryItem);
|
||||
}
|
||||
const mainMenu = mainMenuItems || defaultMainMenu;
|
||||
const secondaryMenu = secondaryMenuItems || [];
|
||||
const userMenu = authenticatedUser === null ? [] : userMenuItems || defaultUserMenu;
|
||||
|
||||
const loggedOutItems = [
|
||||
{
|
||||
@@ -89,20 +106,21 @@ const Header = ({ intl }) => {
|
||||
];
|
||||
|
||||
const props = {
|
||||
logo: config.LOGO_URL,
|
||||
logoAltText: config.SITE_NAME,
|
||||
logoDestination: `${config.LMS_BASE_URL}/dashboard`,
|
||||
logo: logo?.src || config.LOGO_URL,
|
||||
logoAltText: logo?.alt || config.SITE_NAME,
|
||||
logoDestination: logo?.href || `${config.LMS_BASE_URL}/dashboard`,
|
||||
loggedIn: authenticatedUser !== null,
|
||||
username: authenticatedUser !== null ? authenticatedUser.username : null,
|
||||
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
|
||||
mainMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : mainMenu,
|
||||
secondaryMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : secondaryMenu,
|
||||
userMenu: getConfig().AUTHN_MINIMAL_HEADER ? [] : userMenu,
|
||||
loggedOutItems: getConfig().AUTHN_MINIMAL_HEADER ? [] : loggedOutItems,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Responsive maxWidth={768}>
|
||||
<Responsive maxWidth={769}>
|
||||
<MobileHeader {...props} />
|
||||
</Responsive>
|
||||
<Responsive minWidth={769}>
|
||||
@@ -112,8 +130,37 @@ const Header = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
logo: null,
|
||||
mainMenuItems: null,
|
||||
secondaryMenuItems: null,
|
||||
userMenuItems: null,
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
logo: PropTypes.shape({
|
||||
src: PropTypes.string,
|
||||
alt: PropTypes.string,
|
||||
href: PropTypes.string,
|
||||
}),
|
||||
mainMenuItems: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
secondaryMenuItems: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
userMenuItems: PropTypes.arrayOf(PropTypes.shape({
|
||||
heading: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
export default injectIntl(Header);
|
||||
|
||||
@@ -19,25 +19,29 @@ class MobileHeader extends React.Component {
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderMainMenu() {
|
||||
const { mainMenu } = this.props;
|
||||
|
||||
renderMenu(menu) {
|
||||
// Nodes are accepted as a prop
|
||||
if (!Array.isArray(mainMenu)) {
|
||||
return mainMenu;
|
||||
if (!Array.isArray(menu)) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
return mainMenu.map((menuItem) => {
|
||||
return menu.map((menuItem) => {
|
||||
const {
|
||||
type,
|
||||
href,
|
||||
content,
|
||||
submenuContent,
|
||||
disabled,
|
||||
isActive,
|
||||
} = menuItem;
|
||||
|
||||
if (type === 'item') {
|
||||
return (
|
||||
<a key={`${type}-${content}`} className="nav-link" href={href}>
|
||||
<a
|
||||
key={`${type}-${content}`}
|
||||
className={`nav-link${disabled ? ' disabled' : ''}${isActive ? ' active' : ''}`}
|
||||
href={href}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
@@ -56,13 +60,32 @@ class MobileHeader extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
renderMainMenu() {
|
||||
const { mainMenu } = this.props;
|
||||
return this.renderMenu(mainMenu);
|
||||
}
|
||||
|
||||
renderSecondaryMenu() {
|
||||
const { secondaryMenu } = this.props;
|
||||
return this.renderMenu(secondaryMenu);
|
||||
}
|
||||
|
||||
renderUserMenuItems() {
|
||||
const { userMenu } = this.props;
|
||||
|
||||
return userMenu.map(({ type, href, content }) => (
|
||||
<li className="nav-item" key={`${type}-${content}`}>
|
||||
<a className="nav-link" href={href}>{content}</a>
|
||||
</li>
|
||||
return userMenu.map((group) => (
|
||||
group.items.map(({
|
||||
type, content, href, disabled, isActive,
|
||||
}) => (
|
||||
<li className="nav-item" key={`${type}-${content}`}>
|
||||
<a
|
||||
className={`nav-link${isActive ? ' active' : ''}${disabled ? ' disabled' : ''}`}
|
||||
href={href}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
@@ -123,6 +146,7 @@ class MobileHeader extends React.Component {
|
||||
className="nav flex-column pin-left pin-right border-top shadow py-2"
|
||||
>
|
||||
{this.renderMainMenu()}
|
||||
{this.renderSecondaryMenu()}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
@@ -157,11 +181,18 @@ MobileHeader.propTypes = {
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
|
||||
secondaryMenu: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.array,
|
||||
]),
|
||||
userMenu: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
heading: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
href: PropTypes.string,
|
||||
content: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
})),
|
||||
})),
|
||||
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.oneOf(['item', 'menu']),
|
||||
@@ -182,6 +213,7 @@ MobileHeader.propTypes = {
|
||||
|
||||
MobileHeader.defaultProps = {
|
||||
mainMenu: [],
|
||||
secondaryMenu: [],
|
||||
userMenu: [],
|
||||
loggedOutItems: [],
|
||||
logo: null,
|
||||
|
||||
Reference in New Issue
Block a user