Skip to content

Commit a54f099

Browse files
author
Brian Smith
committed
refactor: make studio header more flexible
1 parent d529e00 commit a54f099

File tree

3 files changed

+288
-424
lines changed

3 files changed

+288
-424
lines changed

src/StudioHeader.jsx

Lines changed: 129 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import { AppContext } from '@edx/frontend-platform/react';
55
import {
66
APP_CONFIG_INITIALIZED,
77
ensureConfig,
8+
getConfig,
89
mergeConfig,
910
subscribe,
1011
} from '@edx/frontend-platform';
12+
import { ActionRow } from '@edx/paragon';
1113

12-
import DesktopHeader from './DesktopHeader';
14+
import { Menu, MenuTrigger, MenuContent } from './Menu';
15+
import Avatar from './Avatar';
16+
import { LinkedLogo, Logo } from './Logo';
17+
18+
import { CaretIcon } from './Icons';
1319

1420
import messages from './Header.messages';
1521

@@ -28,7 +34,124 @@ subscribe(APP_CONFIG_INITIALIZED, () => {
2834
}, 'StudioHeader additional config');
2935
});
3036

31-
function StudioHeader({ intl, mainMenu, appMenu }) {
37+
class StudioDesktopHeaderBase extends React.Component {
38+
constructor(props) { // eslint-disable-line no-useless-constructor
39+
super(props);
40+
}
41+
42+
renderUserMenu() {
43+
const {
44+
userMenu,
45+
avatar,
46+
username,
47+
intl,
48+
} = this.props;
49+
50+
return (
51+
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
52+
<MenuTrigger
53+
tag="button"
54+
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
55+
className="btn btn-outline-primary d-inline-flex align-items-center pl-2 pr-3"
56+
>
57+
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
58+
{username} <CaretIcon role="img" aria-hidden focusable="false" />
59+
</MenuTrigger>
60+
<MenuContent className="mb-0 dropdown-menu show dropdown-menu-right pin-right shadow py-2">
61+
{userMenu.map(({ type, href, content }) => (
62+
<a className={`dropdown-${type}`} key={`${type}-${content}`} href={href}>{content}</a>
63+
))}
64+
</MenuContent>
65+
</Menu>
66+
);
67+
}
68+
69+
renderLoggedOutItems() {
70+
const { loggedOutItems } = this.props;
71+
72+
return loggedOutItems.map((item, i, arr) => (
73+
<a
74+
key={`${item.type}-${item.content}`}
75+
className={i < arr.length - 1 ? 'btn mr-2 btn-link' : 'btn mr-2 btn-outline-primary'}
76+
href={item.href}
77+
>
78+
{item.content}
79+
</a>
80+
));
81+
}
82+
83+
render() {
84+
const {
85+
logo,
86+
logoAltText,
87+
logoDestination,
88+
loggedIn,
89+
intl,
90+
actionRowContent,
91+
} = this.props;
92+
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
93+
const logoClasses = getConfig().AUTHN_MINIMAL_HEADER ? 'mw-100' : null;
94+
95+
return (
96+
<header className="site-header-desktop">
97+
<a className="nav-skip sr-only sr-only-focusable" href="#main">{intl.formatMessage(messages['header.label.skip.nav'])}</a>
98+
<div className={`container-fluid ${logoClasses}`}>
99+
<div className="nav-container position-relative d-flex align-items-center">
100+
{logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} />}
101+
<ActionRow>
102+
{actionRowContent}
103+
<nav
104+
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
105+
className="nav secondary-menu-container align-items-center ml-auto"
106+
>
107+
{loggedIn ? this.renderUserMenu() : this.renderLoggedOutItems()}
108+
</nav>
109+
</ActionRow>
110+
</div>
111+
</div>
112+
</header>
113+
);
114+
}
115+
}
116+
117+
StudioDesktopHeaderBase.propTypes = {
118+
userMenu: PropTypes.arrayOf(PropTypes.shape({
119+
type: PropTypes.oneOf(['item', 'menu']),
120+
href: PropTypes.string,
121+
content: PropTypes.string,
122+
})),
123+
loggedOutItems: PropTypes.arrayOf(PropTypes.shape({
124+
type: PropTypes.oneOf(['item', 'menu']),
125+
href: PropTypes.string,
126+
content: PropTypes.string,
127+
})),
128+
logo: PropTypes.string,
129+
logoAltText: PropTypes.string,
130+
logoDestination: PropTypes.string,
131+
avatar: PropTypes.string,
132+
username: PropTypes.string,
133+
loggedIn: PropTypes.bool,
134+
actionRowContent: PropTypes.element,
135+
136+
// i18n
137+
intl: intlShape.isRequired,
138+
};
139+
140+
StudioDesktopHeaderBase.defaultProps = {
141+
userMenu: [],
142+
loggedOutItems: [],
143+
logo: null,
144+
logoAltText: null,
145+
logoDestination: null,
146+
avatar: null,
147+
username: null,
148+
loggedIn: false,
149+
actionRowContent: null,
150+
};
151+
152+
const StudioDesktopHeader = injectIntl(StudioDesktopHeaderBase);
153+
154+
function StudioHeader({ intl, actionRowContent }) {
32155
const { authenticatedUser, config } = useContext(AppContext);
33156

34157
const userMenu = authenticatedUser === null ? [] : [
@@ -56,44 +179,21 @@ function StudioHeader({ intl, mainMenu, appMenu }) {
56179
loggedIn: authenticatedUser !== null,
57180
username: authenticatedUser !== null ? authenticatedUser.username : null,
58181
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
59-
mainMenu,
182+
actionRowContent,
60183
userMenu,
61-
appMenu,
62184
loggedOutItems: [],
63185
};
64186

65-
return <DesktopHeader {...props} />;
187+
return <StudioDesktopHeader {...props} />;
66188
}
67189

68190
StudioHeader.propTypes = {
69191
intl: intlShape.isRequired,
70-
appMenu: PropTypes.shape(
71-
{
72-
content: PropTypes.string,
73-
href: PropTypes.string,
74-
menuItems: PropTypes.arrayOf(
75-
PropTypes.shape({
76-
type: PropTypes.string,
77-
href: PropTypes.string,
78-
content: PropTypes.string,
79-
}),
80-
),
81-
},
82-
),
83-
mainMenu: PropTypes.arrayOf(
84-
PropTypes.shape(
85-
{
86-
type: PropTypes.string,
87-
href: PropTypes.string,
88-
content: PropTypes.string,
89-
},
90-
),
91-
),
192+
actionRowContent: PropTypes.element,
92193
};
93194

94195
StudioHeader.defaultProps = {
95-
appMenu: null,
96-
mainMenu: [],
196+
actionRowContent: <></>,
97197
};
98198

99199
export default injectIntl(StudioHeader);

src/StudioHeader.test.jsx

Lines changed: 30 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import React from 'react';
22
import { IntlProvider } from '@edx/frontend-platform/i18n';
33
import TestRenderer from 'react-test-renderer';
4+
import { Link } from 'react-router-dom';
45
import { AppContext } from '@edx/frontend-platform/react';
6+
import {
7+
ActionRow,
8+
Button,
9+
Dropdown,
10+
} from '@edx/paragon';
511

612
import { StudioHeader } from './index';
713

@@ -36,74 +42,31 @@ describe('<StudioHeader />', () => {
3642
expect(wrapper.toJSON()).toMatchSnapshot();
3743
});
3844

39-
it('renders correctly with the optional app menu', () => {
40-
const appMenu = {
41-
content: 'App Menu',
42-
menuItems: [
43-
{
44-
type: 'dropdown',
45-
href: 'https://menu-href-url.org',
46-
content: 'Content 1',
47-
},
48-
{
49-
type: 'dropdown',
50-
href: 'https://menu-href-url.org',
51-
content: 'Content 2',
52-
},
53-
{
54-
type: 'dropdown',
55-
href: 'https://menu-href-url.org',
56-
content: 'Content 3',
57-
},
58-
],
59-
};
60-
const component = (
61-
<IntlProvider locale="en" messages={{}}>
62-
<AppContext.Provider
63-
value={{
64-
authenticatedUser: {
65-
userId: 'abc123',
66-
username: 'edX',
67-
roles: [],
68-
administrator: false,
69-
},
70-
config: {
71-
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
72-
SITE_NAME: process.env.SITE_NAME,
73-
LOGIN_URL: process.env.LOGIN_URL,
74-
LOGOUT_URL: process.env.LOGOUT_URL,
75-
LOGO_URL: process.env.LOGO_URL,
76-
},
77-
}}
78-
>
79-
<StudioHeader appMenu={appMenu} />
80-
</AppContext.Provider>
81-
</IntlProvider>
45+
it('renders correctly with optional action row content', () => {
46+
const actionRowContent = (
47+
<>
48+
<Dropdown>
49+
<Dropdown.Toggle variant="outline-primary" id="library-header-menu-dropdown">
50+
Settings
51+
</Dropdown.Toggle>
52+
<Dropdown.Menu>
53+
<Dropdown.Item as={Link} to="#">Dropdown Item 1</Dropdown.Item>
54+
<Dropdown.Item as={Link} to="#">Dropdown Item 2</Dropdown.Item>
55+
<Dropdown.Item as={Link} to="#">Dropdown Item 3</Dropdown.Item>
56+
</Dropdown.Menu>
57+
</Dropdown>
58+
<ActionRow.Spacer />
59+
<Button
60+
variant="tertiary"
61+
href="#"
62+
rel="noopener noreferrer"
63+
target="_blank"
64+
title="Help Button"
65+
>Help
66+
</Button>
67+
</>
8268
);
8369

84-
const wrapper = TestRenderer.create(component);
85-
86-
expect(wrapper.toJSON()).toMatchSnapshot();
87-
});
88-
89-
it('renders correctly with the optional main menu', () => {
90-
const mainMenu = [
91-
{
92-
type: 'dropdown',
93-
href: 'https://menu-href-url.org',
94-
content: 'Content 1',
95-
},
96-
{
97-
type: 'dropdown',
98-
href: 'https://menu-href-url.org',
99-
content: 'Content 2',
100-
},
101-
{
102-
type: 'dropdown',
103-
href: 'https://menu-href-url.org',
104-
content: 'Content 3',
105-
},
106-
];
10770
const component = (
10871
<IntlProvider locale="en" messages={{}}>
10972
<AppContext.Provider
@@ -123,7 +86,7 @@ describe('<StudioHeader />', () => {
12386
},
12487
}}
12588
>
126-
<StudioHeader mainMenu={mainMenu} />
89+
<StudioHeader actionRowContent={actionRowContent} />
12790
</AppContext.Provider>
12891
</IntlProvider>
12992
);

0 commit comments

Comments
 (0)