[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:
22
src/data/services/LmsApiService.js
Normal file
22
src/data/services/LmsApiService.js
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
156
src/studio-header/Header.messages.jsx
Normal file
156
src/studio-header/Header.messages.jsx
Normal 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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user