[WIP] New Header Work (#33)

* update Studio header to include dropdown menus and internationalization

* Added header improvements (#34)

Added header improvements

Squashing commits for header improvements

added header improvements

fixed typo

moved api call to separate file

added course lockup to mobile header, removed snapshot tests

fixed css for mobile header

simplified css styling

updated testing

updated css styling

updated css

simplified course lockup

removed React fragments from lockup

fixed mobile header styling

Co-authored-by: alangsto <46360176+alangsto@users.noreply.github.com>
This commit is contained in:
Michael Roytman
2020-08-13 11:43:01 -04:00
committed by GitHub
parent bb4ab0368f
commit 309aa93607
9 changed files with 472 additions and 339 deletions

View File

@@ -0,0 +1,22 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig, ensureConfig } from '@edx/frontend-platform';
ensureConfig([
'LMS_BASE_URL',
], 'LMS API service');
const lmsBaseUrl = getConfig().LMS_BASE_URL;
class LmsApiService {
static getCourseDetailsUrl(courseID) {
return `${lmsBaseUrl}/api/courses/v1/courses/${courseID}`;
}
static getCourseDetailsData(courseID) {
const apiClient = getAuthenticatedHttpClient();
const url = LmsApiService.getCourseDetailsUrl(courseID);
return apiClient.get(url);
}
}
export default LmsApiService;

View File

@@ -1,8 +1,5 @@
@import '~@edx/paragon/scss/edx/theme.scss';
@import './example/index.scss';
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";
@import "proctored-exam-settings/proctoredExamSettings.scss";

View File

@@ -1,13 +1,16 @@
// This file was copied from edx/frontend-component-header-edx.
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
// 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';
@@ -56,13 +59,14 @@ class DesktopHeader extends React.Component {
userMenu,
avatar,
username,
intl,
} = this.props;
return (
<Menu transitionClassName="menu-dropdown" transitionTimeout={250}>
<MenuTrigger
tag="button"
aria-label={`Account menu for ${username}`}
aria-label={intl.formatMessage(messages['header.label.account.menu.for'], { username })}
className="btn btn-light d-inline-flex align-items-center pl-2 pr-3"
>
<Avatar size="1.5em" src={avatar} alt="" className="mr-2" />
@@ -82,7 +86,7 @@ class DesktopHeader extends React.Component {
logo,
logoAltText,
logoDestination,
courseTitleDestination,
intl,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
@@ -92,24 +96,15 @@ class DesktopHeader extends React.Component {
<div className="nav-container position-relative d-flex align-items-center">
{logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} />}
{/* This lockup HTML was copied from edx/frontend-app-learning/src/course-header/Header.jsx. */}
<a
className="course-title-lockup"
style={{ lineHeight: 1 }}
href={courseTitleDestination}
aria-label="Back to course outline in Studio"
>
{this.props.courseId}
</a>
{ this.props.courseLockUp }
<nav
aria-label="Main"
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
className="nav main-nav"
>
{/* TODO: Create main menu items to populate main navigation. */}
{/* {this.renderMainMenu()} */}
<a style={{ paddingLeft: '1rem' }} href={courseTitleDestination}>Back to Studio Course Outline</a>
{this.renderMainMenu()}
</nav>
<nav
aria-label="Secondary"
aria-label={intl.formatMessage(messages['header.label.secondary.nav'])}
className="nav secondary-menu-container align-items-center ml-auto"
>
{this.renderUserMenu()}
@@ -134,11 +129,14 @@ DesktopHeader.propTypes = {
logo: PropTypes.string,
logoAltText: PropTypes.string,
logoDestination: PropTypes.string,
courseTitleDestination: PropTypes.string,
courseId: PropTypes.string,
avatar: PropTypes.string,
username: PropTypes.string,
loggedIn: PropTypes.bool,
courseLockUp: PropTypes.node.isRequired,
// i18n
intl: intlShape.isRequired,
};
DesktopHeader.defaultProps = {
@@ -147,7 +145,6 @@ DesktopHeader.defaultProps = {
logo: null,
logoAltText: null,
logoDestination: null,
courseTitleDestination: null,
courseId: null,
avatar: null,
username: null,

View File

@@ -1,15 +1,21 @@
// This file was copied from edx/frontend-component-header-edx.
import React, { useContext } from 'react';
/* eslint-disable jsx-a11y/control-has-associated-label */
/* eslint-disable jsx-a11y/anchor-has-content */
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react';
import Responsive from 'react-responsive';
import { injectIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { ensureConfig } from '@edx/frontend-platform';
import {
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import DesktopHeader from './DesktopHeader';
import MobileHeader from './MobileHeader';
import messages from './Header.messages';
import StudioLogoPNG from './assets/studio-logo.png';
import LmsApiService from '../data/services/LmsApiService';
ensureConfig([
'STUDIO_BASE_URL',
@@ -17,28 +23,82 @@ ensureConfig([
'LOGIN_URL',
], 'Header component');
function Header({ courseId }) {
function Header({ courseId, intl }) {
const { authenticatedUser, config } = useContext(AppContext);
const [courseNumber, setCourseNumber] = useState('');
const [courseOrg, setCourseOrg] = useState('');
const [courseTitle, setCourseTitle] = useState('');
const desktopMainMenu = [];
const mobileMainMenu = [
useEffect(
() => {
LmsApiService.getCourseDetailsData(courseId)
.then(
response => {
setCourseNumber(response.data.number);
setCourseOrg(response.data.org);
setCourseTitle(response.data.name);
},
).catch(
() => {
setCourseNumber('');
setCourseOrg('');
setCourseTitle(courseId);
},
);
}, [],
);
const mainMenu = [
{
type: 'item',
href: `${config.STUDIO_BASE_URL}/course/${courseId}`,
content: 'Back to Studio Course Outline',
type: 'submenu',
content: intl.formatMessage(messages['header.links.content']),
submenuContent: (
<>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/course/${courseId}`}>{intl.formatMessage(messages['header.links.outline'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/course_info/${courseId}`}>{intl.formatMessage(messages['header.links.updates'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/tabs/${courseId}`}>{intl.formatMessage(messages['header.links.pages'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/assets/${courseId}`}>{intl.formatMessage(messages['header.links.filesAndUploads'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/textbooks/${courseId}`}>{intl.formatMessage(messages['header.links.textbooks'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/videos/${courseId}`}>{intl.formatMessage(messages['header.links.videoUploads'])}</a></div>
</>
),
},
{
type: 'submenu',
content: intl.formatMessage(messages['header.links.settings']),
submenuContent: (
<>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/settings/details/${courseId}`}>{intl.formatMessage(messages['header.links.scheduleAndDetails'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/settings/grading/${courseId}`}>{intl.formatMessage(messages['header.links.grading'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/course_team/${courseId}`}>{intl.formatMessage(messages['header.links.courseTeam'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/group_configurations/${courseId}`}>{intl.formatMessage(messages['header.links.groupConfigurations'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/settings/advanced/${courseId}`}>{intl.formatMessage(messages['header.links.advancedSettings'])}</a></div>
</>
),
},
{
type: 'submenu',
content: intl.formatMessage(messages['header.links.tools']),
submenuContent: (
<>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/import/${courseId}`}>{intl.formatMessage(messages['header.links.import'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/export/${courseId}`}>{intl.formatMessage(messages['header.links.export'])}</a></div>
<div className="mb-1"><a rel="noopener" href={`${config.STUDIO_BASE_URL}/checklists/${courseId}`}>{intl.formatMessage(messages['header.links.checklists'])}</a></div>
</>
),
},
];
const studioHomeItem = {
type: 'item',
href: config.STUDIO_BASE_URL,
content: 'Studio Home',
content: intl.formatMessage(messages['header.user.menu.studio']),
};
const logoutItem = {
type: 'item',
href: config.LOGOUT_URL,
content: 'Sign Out',
content: intl.formatMessage(messages['header.user.menu.logout']),
};
let userMenu = [];
@@ -50,7 +110,7 @@ function Header({ courseId }) {
{
type: 'item',
href: `${config.STUDIO_BASE_URL}/maintenance`,
content: 'Maintenance',
content: intl.formatMessage(messages['header.user.menu.maintenance']),
},
logoutItem,
];
@@ -62,25 +122,37 @@ function Header({ courseId }) {
}
}
const courseLockUp = (
<a
className="course-title-lockup"
style={{ lineHeight: 1 }}
href={`${config.STUDIO_BASE_URL}/course/${courseId}`}
aria-label={intl.formatMessage(messages['header.label.courseOutline'])}
>
<span className="d-block small m-0" data-test-id="course-org-number">{courseOrg} {courseNumber}</span>
<span className="d-block m-0 font-weight-bold" data-test-id="course-title">{courseTitle}</span>
</a>
);
const props = {
logo: StudioLogoPNG,
logoAltText: 'Studio edX',
siteName: 'edX',
logoDestination: config.STUDIO_BASE_URL,
courseTitleDestination: `${config.STUDIO_BASE_URL}/course/${courseId}`,
courseLockUp,
courseId,
username: authenticatedUser !== null ? authenticatedUser.username : null,
avatar: authenticatedUser !== null ? authenticatedUser.avatar : null,
mainMenu,
userMenu,
};
return (
<>
<Responsive maxWidth={768}>
<MobileHeader {...props} mainMenu={mobileMainMenu} />
<MobileHeader {...props} />
</Responsive>
<Responsive minWidth={769}>
<DesktopHeader {...props} mainMenu={desktopMainMenu} />
<DesktopHeader {...props} />
</Responsive>
</>
);
@@ -88,6 +160,7 @@ function Header({ courseId }) {
Header.propTypes = {
courseId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(Header);

View File

@@ -0,0 +1,156 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'header.links.content': {
id: 'header.links.content',
defaultMessage: 'Content',
description: 'Label for Content menu trigger',
},
'header.links.settings': {
id: 'header.links.settings',
defaultMessage: 'Settings',
description: 'Label for Settings menu trigger',
},
'header.links.tools': {
id: 'header.links.content.tools',
defaultMessage: 'Tools',
description: 'Label for Tools menu trigger',
},
'header.links.outline': {
id: 'header.links.outline',
defaultMessage: 'Outline',
description: 'Link to Studio Outline page',
},
'header.links.updates': {
id: 'header.links.updates',
defaultMessage: 'Updates',
description: 'Link to Studio Updates page',
},
'header.links.pages': {
id: 'header.links.pages',
defaultMessage: 'Pages',
description: 'Link to Studio Pages page',
},
'header.links.filesAndUploads': {
id: 'header.links.filesAndUploads',
defaultMessage: 'Files & Uploads',
description: 'Link to Studio Files & Uploads page',
},
'header.links.textbooks': {
id: 'header.links.textbooks',
defaultMessage: 'Textbooks',
description: 'Link to Studio Textbooks page',
},
'header.links.videoUploads': {
id: 'header.links.videoUploads',
defaultMessage: 'Video Uploads',
description: 'Link to Studio Video Uploads page',
},
'header.links.scheduleAndDetails': {
id: 'header.links.scheduleAndDetails',
defaultMessage: 'Schedule & Details',
description: 'Link to Studio Schedule & Details page',
},
'header.links.grading': {
id: 'header.links.grading',
defaultMessage: 'Grading',
description: 'Link to Studio Grading page',
},
'header.links.courseTeam': {
id: 'header.links.courseTeam',
defaultMessage: 'Course Team',
description: 'Link to Studio Course Team page',
},
'header.links.groupConfigurations': {
id: 'header.links.groupConfigurations',
defaultMessage: 'Group Configurations',
description: 'Link to Studio Group Configurations page',
},
'header.links.proctoredExamSettings': {
id: 'header.links.proctoredExamSettings',
defaultMessage: 'Proctored Exam Settings',
description: 'Link to Studio Proctored Exam Settings page',
},
'header.links.advancedSettings': {
id: 'header.links.advancedSettings',
defaultMessage: 'Advanced Settings',
description: 'Link to Studio Advanced Settings page',
},
'header.links.certificates': {
id: 'header.links.certificates',
defaultMessage: 'Certificates',
description: 'Link to Studio Certificates page',
},
'header.links.publisher': {
id: 'header.links.publisher',
defaultMessage: 'Publisher',
description: 'Link to Publisher',
},
'header.links.import': {
id: 'header.links.import',
defaultMessage: 'Import',
description: 'Link to Studio Import page',
},
'header.links.export': {
id: 'header.links.export',
defaultMessage: 'Export',
description: 'Link to Studio Export page',
},
'header.links.checklists': {
id: 'header.links.checklists',
defaultMessage: 'Checklists',
description: 'Link to Studio Checklists page',
},
'header.user.menu.studio': {
id: 'header.user.menu.studio',
defaultMessage: 'Studio Home',
description: 'Link to Studio Home',
},
'header.user.menu.maintenance': {
id: 'header.user.menu.maintenance',
defaultMessage: 'Maintenance',
description: 'Link to the Studio maintenance page',
},
'header.user.menu.logout': {
id: 'header.user.menu.logout',
defaultMessage: 'Logout',
description: 'Logout link',
},
'header.label.account.menu': {
id: 'header.label.account.menu',
defaultMessage: 'Account Menu',
description: 'The aria label for the account menu trigger',
},
'header.label.account.menu.for': {
id: 'header.label.account.menu.for',
defaultMessage: 'Account menu for {username}',
description: 'The aria label for the account menu trigger when the username is displayed in it',
},
'header.label.main.nav': {
id: 'header.label.main.nav',
defaultMessage: 'Main',
description: 'The aria label for the main menu nav',
},
'header.label.main.menu': {
id: 'header.label.main.menu',
defaultMessage: 'Main Menu',
description: 'The aria label for the main menu trigger',
},
'header.label.main.header': {
id: 'header.label.main.header',
defaultMessage: 'Main',
description: 'The aria label for the main header',
},
'header.label.secondary.nav': {
id: 'header.label.secondary.nav',
defaultMessage: 'Secondary',
description: 'The aria label for the seconary nav',
},
'header.label.courseOutline': {
id: 'header.label.courseOutline',
defaultMessage: 'Back to course outline in Studio',
description: 'The aria label for the link back to the Studio Course Outline',
},
});
export default messages;

View File

@@ -1,21 +1,28 @@
// This file was copied from edx/frontend-component-header-edx.
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import TestRenderer from 'react-test-renderer';
import { AppContext } from '@edx/frontend-platform/react';
import { getConfig } from '@edx/frontend-platform';
import { Context as ResponsiveContext } from 'react-responsive';
import * as auth from '@edx/frontend-platform/auth';
import {
act,
cleanup,
render,
screen,
} from '@testing-library/react';
import Header from './Header';
jest.mock('@edx/frontend-platform');
getConfig.mockReturnValue({});
describe('<Header />', () => {
it('renders correctly for authenticated users on desktop', () => {
const component = (
<ResponsiveContext.Provider value={{ width: 1280 }}>
function mockRequest(getFunction) {
auth.getAuthenticatedHttpClient = jest.fn(() => ({
get: getFunction,
}));
}
function createComponent(screenWidth) {
return (
<ResponsiveContext.Provider value={{ width: screenWidth }}>
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider value={{
authenticatedUser: {
@@ -26,6 +33,7 @@ describe('<Header />', () => {
},
config: {
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
},
@@ -36,38 +44,59 @@ describe('<Header />', () => {
</IntlProvider>
</ResponsiveContext.Provider>
);
}
const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot();
const successfulCall = async () => ({
data: {
number: 'DemoX',
org: 'edX',
name: 'Demonstration Course',
},
});
it('renders correctly for authenticated users on mobile', () => {
const component = (
<ResponsiveContext.Provider value={{ width: 500 }}>
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider value={{
authenticatedUser: {
userId: 'abc123',
username: 'edX',
roles: [],
administrator: false,
},
config: {
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
},
}}
>
<Header courseId="course-v1:edX+DemoX+Demo_Course" />
</AppContext.Provider>
</IntlProvider>
</ResponsiveContext.Provider>
);
const errorObject = {
customAttributes: {
httpErrorStatus: 404,
},
};
const wrapper = TestRenderer.create(component);
const badCall = async () => { throw errorObject; };
expect(wrapper.toJSON()).toMatchSnapshot();
it('renders desktop header correctly with API call', async () => {
mockRequest(successfulCall);
const component = createComponent(1280);
await act(async () => render(component));
expect(screen.getByTestId('course-org-number').textContent).toEqual(expect.stringContaining('edX DemoX'));
expect(screen.getByTestId('course-title').textContent).toEqual(expect.stringContaining('Demonstration Course'));
});
it('renders mobile header correctly with API call', async () => {
mockRequest(successfulCall);
const component = createComponent(500);
await act(async () => render(component));
expect(screen.getAllByTestId('course-org-number')[0].textContent).toEqual(expect.stringContaining('edX DemoX'));
expect(screen.getAllByTestId('course-title')[0].textContent).toEqual(expect.stringContaining('Demonstration Course'));
});
it('renders desktop header correctly with bad API call', async () => {
mockRequest(badCall);
const component = createComponent(1280);
await act(async () => render(component));
expect(screen.getByTestId('course-title').textContent).toEqual(expect.stringContaining('course-v1:edX+DemoX+Demo_Course'));
});
it('renders mobile header correctly with bad API call', async () => {
mockRequest(badCall);
const component = createComponent(500);
await act(async () => render(component));
expect(screen.getAllByTestId('course-title')[0].textContent).toEqual(expect.stringContaining('course-v1:edX+DemoX+Demo_Course'));
});
afterEach(() => {
cleanup();
});
});

View File

@@ -1,12 +1,16 @@
// This file was copied from edx/frontend-component-header-edx.
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
// 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 { MenuIcon } from './Icons';
@@ -71,12 +75,13 @@ class MobileHeader extends React.Component {
username,
stickyOnMobile,
mainMenu,
intl,
} = this.props;
const logoProps = { src: logo, alt: logoAltText, href: logoDestination };
const stickyClassName = stickyOnMobile ? 'sticky-top' : '';
return (
<header
aria-label="Main"
aria-label={intl.formatMessage(messages['header.label.main.header'])}
className={`site-header-mobile d-flex justify-content-between align-items-center shadow ${stickyClassName}`}
>
<div className="w-100 d-flex justify-content-start">
@@ -86,14 +91,14 @@ class MobileHeader extends React.Component {
<MenuTrigger
tag="button"
className="icon-button"
aria-label="Main Menu"
title="Main Menu"
aria-label={intl.formatMessage(messages['header.label.main.menu'])}
title={intl.formatMessage(messages['header.label.main.menu'])}
>
<MenuIcon role="img" aria-hidden focusable="false" style={{ width: '1.5rem', height: '1.5rem' }} />
</MenuTrigger>
<MenuContent
tag="nav"
aria-label="Main"
aria-label={intl.formatMessage(messages['header.label.main.nav'])}
className="nav flex-column pin-left pin-right border-top shadow py-2"
>
{this.renderMainMenu()}
@@ -101,16 +106,17 @@ class MobileHeader extends React.Component {
</Menu>
) : null}
</div>
<div className="w-100 d-flex justify-content-center">
<div className="w-100 d-flex align-items-center mobile-lockup">
{logoDestination === null ? <Logo className="logo" src={logo} alt={logoAltText} /> : <LinkedLogo className="logo" {...logoProps} itemType="http://schema.org/Organization" />}
{ this.props.courseLockUp }
</div>
<div className="w-100 d-flex justify-content-end align-items-center">
<Menu tag="nav" aria-label="Secondary" className="position-static">
<MenuTrigger
tag="button"
className="icon-button"
aria-label="Account Menu"
title="Account Menu"
aria-label={intl.formatMessage(messages['header.label.account.menu'])}
title={intl.formatMessage(messages['header.label.account.menu'])}
>
<Avatar size="1.5rem" src={avatar} alt={username} />
</MenuTrigger>
@@ -141,6 +147,10 @@ MobileHeader.propTypes = {
avatar: PropTypes.string,
username: PropTypes.string,
stickyOnMobile: PropTypes.bool,
courseLockUp: PropTypes.node.isRequired,
// i18n
intl: intlShape.isRequired,
};
MobileHeader.defaultProps = {
@@ -152,7 +162,6 @@ MobileHeader.defaultProps = {
avatar: null,
username: null,
stickyOnMobile: true,
};
export default injectIntl(MobileHeader);

View File

@@ -1,253 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Header /> renders correctly for authenticated users on desktop 1`] = `
<header
className="site-header-desktop"
>
<div
className="container-fluid"
>
<div
className="nav-container position-relative d-flex align-items-center"
>
<a
className="logo"
href="http://localhost:18010"
>
<img
alt="Studio edX"
className="d-block"
src="test-file-stub"
/>
</a>
<a
aria-label="Back to course outline in Studio"
className="course-title-lockup"
href="http://localhost:18010/course/course-v1:edX+DemoX+Demo_Course"
style={
Object {
"lineHeight": 1,
}
}
>
course-v1:edX+DemoX+Demo_Course
</a>
<nav
aria-label="Main"
className="nav main-nav"
>
<a
href="http://localhost:18010/course/course-v1:edX+DemoX+Demo_Course"
style={
Object {
"paddingLeft": "1rem",
}
}
>
Back to Studio Course Outline
</a>
</nav>
<nav
aria-label="Secondary"
className="nav secondary-menu-container align-items-center ml-auto"
>
<div
className="menu null"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account menu for edX"
className="menu-trigger btn btn-light d-inline-flex align-items-center pl-2 pr-3"
onClick={[Function]}
>
<span
className="avatar overflow-hidden d-inline-flex rounded-circle mr-2"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
>
<svg
aria-hidden={true}
className="text-muted"
focusable="false"
height="24px"
role="img"
style={
Object {
"height": "1.5em",
"width": "1.5em",
}
}
version="1.1"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z"
fill="currentColor"
/>
</svg>
</span>
edX
<svg
aria-hidden={true}
focusable="false"
height="16px"
role="img"
version="1.1"
viewBox="0 0 16 16"
width="16px"
>
<path
d="M7,4 L7,8 L11,8 L11,10 L5,10 L5,4 L7,4 Z"
fill="currentColor"
transform="translate(8.000000, 7.000000) rotate(-45.000000) translate(-8.000000, -7.000000) "
/>
</svg>
</button>
</div>
</nav>
</div>
</div>
</header>
`;
exports[`<Header /> renders correctly for authenticated users on mobile 1`] = `
<header
aria-label="Main"
className="site-header-mobile d-flex justify-content-between align-items-center shadow sticky-top"
>
<div
className="w-100 d-flex justify-content-start"
>
<div
className="menu position-static"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Main Menu"
className="menu-trigger icon-button"
onClick={[Function]}
title="Main Menu"
>
<svg
aria-hidden={true}
focusable="false"
height="24px"
role="img"
style={
Object {
"height": "1.5rem",
"width": "1.5rem",
}
}
version="1.1"
viewBox="0 0 24 24"
width="24px"
>
<rect
fill="currentColor"
height="2"
width="20"
x="2"
y="5"
/>
<rect
fill="currentColor"
height="2"
width="20"
x="2"
y="11"
/>
<rect
fill="currentColor"
height="2"
width="20"
x="2"
y="17"
/>
</svg>
</button>
</div>
</div>
<div
className="w-100 d-flex justify-content-center"
>
<a
className="logo"
href="http://localhost:18010"
itemType="http://schema.org/Organization"
>
<img
alt="Studio edX"
className="d-block"
src="test-file-stub"
/>
</a>
</div>
<div
className="w-100 d-flex justify-content-end align-items-center"
>
<nav
aria-label="Secondary"
className="menu position-static"
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-expanded={false}
aria-haspopup="menu"
aria-label="Account Menu"
className="menu-trigger icon-button"
onClick={[Function]}
title="Account Menu"
>
<span
className="avatar overflow-hidden d-inline-flex rounded-circle null"
style={
Object {
"height": "1.5rem",
"width": "1.5rem",
}
}
>
<svg
aria-hidden={true}
className="text-muted"
focusable="false"
height="24px"
role="img"
style={
Object {
"height": "1.5rem",
"width": "1.5rem",
}
}
version="1.1"
viewBox="0 0 24 24"
width="24px"
>
<path
d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z"
fill="currentColor"
/>
</svg>
</span>
</button>
</nav>
</div>
</header>
`;

View File

@@ -1,12 +1,115 @@
// This SCSS was copied from edx/frontend-app-learning/src/index.scss.
// This SCSS was partly copied from edx/frontend-app-support-tools/src/support-header/index.scss.
$spacer: 1rem;
$blue: #007db8;
$white: #fff;
@import './Menu/menu.scss';
.course-title-lockup {
padding: 1rem 0;
@media only screen and (max-width : 768px) {
padding-left: 0.5rem;
max-width: 70%;
}
@media only screen and (min-width : 769px) {
padding: 0.5rem;
padding-right: 1rem;
border-right: 1px solid #e5e5e5;
min-width: 0;
}
overflow: hidden;
span {
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.mobile-lockup {
max-width: 50%;
}
.dropdown-item a {
text-decoration: none;
}
.icon-button {
display: inline-flex;
line-height: 3rem;
background: transparent;
vertical-align: middle;
text-align: center;
border: none;
height: 3rem;
width: 3rem;
padding: .75rem;
justify-content: center;
align-items:center;
&:hover, &:focus {
background: rgba(0,0,0,.1);
}
}
.site-header-mobile,
.site-header-desktop {
position: relative;
z-index: 1000;
}
.site-header-mobile {
.nav-link {
text-decoration: none;
cursor: pointer;
}
img {
height: 1.5rem;
}
}
.site-header-desktop {
height: 3.75rem;
box-shadow: 0 1px 0 0 rgba(0,0,0,.1);
background: $white;
.nav-link {
text-decoration: none;
}
.logo {
display: block;
box-sizing: content-box;
position: relative;
top: -.05em;
height: 1.75rem;
padding: 1rem 0;
margin-right: 1rem;
img {
display: block;
height: 100%;
}
}
.main-nav {
flex-wrap: nowrap;
.nav-link {
padding: 1.125rem 1rem;
text-decoration: none;
font-weight: 500;
letter-spacing: .01em;
}
.nav-link:hover,
.nav-link:focus,
.nav-link.active,
.expanded .nav-link {
background: $component-active-bg;
color: $component-active-color;
}
.menu {
position: relative;
.menu-content {
border-top: solid 2px $component-active-bg;
box-shadow: 0 1px 2px rgba(0,0,0,.25);
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
padding: 1rem;
}
}
}
}