* feat: Add StudioHeader with optional AppMenu StudioHeader is a graft of Header with an additional optional AppMenu. Some Frontend Apps use Menus in their custom headers to provide more functionality in their apps. By adding this functionality in StudioHeader, it will be easier for frontend apps in Studio to adopt this component without it affecting the main Header component. * test: Add tests for StudioHeader and AppMenu * fix: Remove orderHistory * fix: Remove Responsive components * fix: Redefine User Menu for Studio The userMenu in StudioHeader will be used more for Studio related items such as Studio Home and Studio Maintenance. This requires new messages and reestablishing the url destinations of these menu items. * fix: Remove loggedOutItems * fix: Remove AUTHN_MINIMAL_HEADER items * fix: Remove unnecessary tests Anonymous sessions do not exist in the Studio. And Studio is not Mobile Ready. Tests of these kind are superfluous and have been removed. * feat: Turn mainMenu into an optional prop * test: Add test for optional mainMenu prop * feat: Update snapshots * fix: Remove ResponsiveContext * fix: Remove href and update appMenu prop Dropping the href because having a link that also works as a dropdown can be mildly confusing. Changing menu (type, href, content ) triplet to stick to the pattern; so we removed "menu". Also adding brackets around the triplet. Lastly, updating test and snapshot. Co-authored-by: Carlos Muniz <cmuniz@trcil.org>
215 lines
6.2 KiB
JavaScript
215 lines
6.2 KiB
JavaScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
import { getConfig } from '@edx/frontend-platform';
|
|
|
|
// Local Components
|
|
import { Menu, MenuTrigger, MenuContent } from './Menu';
|
|
import Avatar from './Avatar';
|
|
import { LinkedLogo, Logo } from './Logo';
|
|
|
|
// i18n
|
|
import messages from './Header.messages';
|
|
|
|
// Assets
|
|
import { CaretIcon } from './Icons';
|
|
|
|
class DesktopHeader extends React.Component {
|
|
constructor(props) { // eslint-disable-line no-useless-constructor
|
|
super(props);
|
|
}
|
|
|
|
renderMainMenu() {
|
|
const { mainMenu } = this.props;
|
|
|
|
// Nodes are accepted as a prop
|
|
if (!Array.isArray(mainMenu)) {
|
|
return mainMenu;
|
|
}
|
|
|
|
return mainMenu.map((menuItem) => {
|
|
const {
|
|
type,
|
|
href,
|
|
content,
|
|
submenuContent,
|
|
} = menuItem;
|
|
|
|
if (type === 'item') {
|
|
return (
|
|
<a key={`${type}-${content}`} className="nav-link" href={href}>{content}</a>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Menu key={`${type}-${content}`} tag="div" className="nav-item" respondToPointerEvents>
|
|
<MenuTrigger tag="a" className="nav-link d-inline-flex align-items-center" href={href}>
|
|
{content} <CaretIcon role="img" aria-hidden focusable="false" />
|
|
</MenuTrigger>
|
|
<MenuContent className="pin-left pin-right shadow py-2">
|
|
{submenuContent}
|
|
</MenuContent>
|
|
</Menu>
|
|
);
|
|
});
|
|
}
|
|
|
|
// 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>
|
|
);
|
|
}
|
|
|
|
renderUserMenu() {
|
|
const {
|
|
userMenu,
|
|
avatar,
|
|
username,
|
|
intl,
|
|
} = this.props;
|
|
|
|
return (
|
|
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
|
|
<MenuTrigger
|
|
tag="button"
|
|
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
|
|
className="btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
|
|
>
|
|
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
|
|
{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>
|
|
))}
|
|
</MenuContent>
|
|
</Menu>
|
|
);
|
|
}
|
|
|
|
renderLoggedOutItems() {
|
|
const { loggedOutItems } = this.props;
|
|
|
|
return loggedOutItems.map((item, i, arr) => (
|
|
<a
|
|
key={`${item.type}-${item.content}`}
|
|
className={i < arr.length - 1 ? 'btn mr-2 btn-link' : 'btn mr-2 btn-outline-primary'}
|
|
href={item.href}
|
|
>
|
|
{item.content}
|
|
</a>
|
|
));
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
logo,
|
|
logoAltText,
|
|
logoDestination,
|
|
loggedIn,
|
|
intl,
|
|
appMenu,
|
|
} = this.props;
|
|
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
|
|
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
|
|
|
|
return (
|
|
<header className="site-header-desktop">
|
|
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
|
|
<div className={`container-fluid ${logoClasses}`}>
|
|
<div className="nav-container position-relative d-flex align-items-center">
|
|
{logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} />}
|
|
<nav
|
|
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
|
|
className="nav main-nav"
|
|
>
|
|
{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()}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|
|
}
|
|
|
|
DesktopHeader.propTypes = {
|
|
mainMenu: PropTypes.oneOfType([
|
|
PropTypes.node,
|
|
PropTypes.array,
|
|
]),
|
|
userMenu: PropTypes.arrayOf(PropTypes.shape({
|
|
type: PropTypes.oneOf(['item', 'menu']),
|
|
href: PropTypes.string,
|
|
content: PropTypes.string,
|
|
})),
|
|
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
|
|
type: PropTypes.oneOf(['item', 'menu']),
|
|
href: PropTypes.string,
|
|
content: PropTypes.string,
|
|
})),
|
|
logo: PropTypes.string,
|
|
logoAltText: PropTypes.string,
|
|
logoDestination: PropTypes.string,
|
|
avatar: PropTypes.string,
|
|
username: PropTypes.string,
|
|
loggedIn: PropTypes.bool,
|
|
|
|
// 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: [],
|
|
userMenu: [],
|
|
loggedOutItems: [],
|
|
logo: null,
|
|
logoAltText: null,
|
|
logoDestination: null,
|
|
avatar: null,
|
|
username: null,
|
|
loggedIn: false,
|
|
appMenu: null,
|
|
};
|
|
|
|
export default injectIntl(DesktopHeader);
|