Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e86335c
Add Sidebar and SidebarNotificationButton components
julianajlk Mar 15, 2021
0853f95
Add tests
julianajlk Mar 16, 2021
ff81ee1
Change resposiveness to apply at tablet md size
julianajlk Mar 18, 2021
b227c94
Remove REV-1512 experiment code
julianajlk Mar 23, 2021
fea9899
After UX asked for changes in responsive version
julianajlk Apr 2, 2021
52a2142
Move notification icon and red dot to separate component
julianajlk Apr 6, 2021
b773e3e
Change conditional outside of Sidebar component and update Sidebar.te…
julianajlk Apr 15, 2021
635f7e0
Update Course/CourseBreadcrumbs screen size conditional
julianajlk Apr 15, 2021
5afefc5
Update styling on notification button and icon
julianajlk Apr 15, 2021
d73f7fc
Update SequenceNav/SequenceNav.test screen size conditional
julianajlk Apr 15, 2021
a238f91
Remove unused props from Sequence
julianajlk Apr 15, 2021
2609953
Add SidebarNotificationButton to Sequence i/o SequenceNavigation comp…
julianajlk Apr 15, 2021
86d79b8
Update inline CSS on sequence div
julianajlk Apr 15, 2021
ade188e
Install js-cookie for handling cookies
julianajlk Apr 15, 2021
a98714a
Add temporary cookie code to hide sidebar and button
julianajlk Apr 12, 2021
3bf88c7
Styling/a11y updates
julianajlk Apr 13, 2021
f6880b4
Add no notifications conditional for the sidebar height
julianajlk Apr 16, 2021
65fb9fa
Styling updates, add NotificationIcons scss file
julianajlk Apr 22, 2021
a44b20c
Add responsiveBreakpoints replacing hard coded pixels
julianajlk Apr 27, 2021
bc2e75f
Add more tests, temporary cookie naming update
julianajlk Apr 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@reduxjs/toolkit": "1.3.6",
"classnames": "2.2.6",
"core-js": "3.6.5",
"js-cookie": "2.2.1",
"lodash.camelcase": "^4.3.0",
"prop-types": "15.7.2",
"react": "16.13.1",
Expand Down
47 changes: 39 additions & 8 deletions src/courseware/course/Course.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useDispatch } from 'react-redux';
import Cookies from 'js-cookie';
import { getConfig } from '@edx/frontend-platform';

import { AlertList } from '../../generic/user-messages';
Expand All @@ -13,8 +14,11 @@ import Sequence from './sequence';
import { CelebrationModal, shouldCelebrateOnSectionLoad } from './celebration';
import ContentTools from './content-tools';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SidebarNotificationButton from './SidebarNotificationButton';

import CourseSock from '../../generic/course-sock';
import { useModel } from '../../generic/model-store';
import useWindowSize, { responsiveBreakpoints } from '../../generic/tabs/useWindowSize';

/** [MM-P2P] Experiment */
import { initCoursewareMMP2P, MMP2PBlockModal } from '../../experiments/mm-p2p';
Expand Down Expand Up @@ -57,6 +61,19 @@ function Course({
courseId, sequenceId, unitId, celebrateFirstSection, dispatch, celebrations,
);

// REV-2130 TODO: temporary cookie code that should be removed.
// In order to see the Value Prop sidebar in prod, a cookie should be set in
// the browser console and refresh: document.cookie = 'value_prop_cookie=true';
const isValuePropCookieSet = Cookies.get('value_prop_cookie') === 'true';

const shouldDisplaySidebarButton = useWindowSize().width >= responsiveBreakpoints.small.minWidth;

const [sidebarVisible, setSidebar] = useState(false);
const isSidebarVisible = () => sidebarVisible && setSidebar;
const toggleSidebar = () => {
if (sidebarVisible) { setSidebar(false); } else { setSidebar(true); }
};

/** [MM-P2P] Experiment */
const MMP2P = initCoursewareMMP2P(courseId, sequenceId, unitId);

Expand All @@ -76,13 +93,23 @@ function Course({
}}
/>
)}
<CourseBreadcrumbs
courseId={courseId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
//* * [MM-P2P] Experiment */
mmp2p={MMP2P}
/>
<div className="position-relative">
<CourseBreadcrumbs
courseId={courseId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
//* * [MM-P2P] Experiment */
mmp2p={MMP2P}
/>

{ isValuePropCookieSet && shouldDisplaySidebarButton ? (
<SidebarNotificationButton
toggleSidebar={toggleSidebar}
isSidebarVisible={isSidebarVisible}
/>
) : null}
</div>

<AlertList topic="sequence" />
<Sequence
unitId={unitId}
Expand All @@ -91,6 +118,10 @@ function Course({
unitNavigationHandler={unitNavigationHandler}
nextSequenceHandler={nextSequenceHandler}
previousSequenceHandler={previousSequenceHandler}
toggleSidebar={toggleSidebar}
isSidebarVisible={isSidebarVisible}
sidebarVisible={sidebarVisible}
isValuePropCookieSet={isValuePropCookieSet}
//* * [MM-P2P] Experiment */
mmp2p={MMP2P}
/>
Expand Down
27 changes: 27 additions & 0 deletions src/courseware/course/Course.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { Factory } from 'rosie';
import Cookies from 'js-cookie';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
loadUnit, render, screen, waitFor, getByRole, initializeTestStore, fireEvent,
Expand All @@ -19,6 +20,7 @@ describe('Course', () => {
nextSequenceHandler: () => {},
previousSequenceHandler: () => {},
unitNavigationHandler: () => {},
toggleSidebar: () => {},
};

beforeAll(async () => {
Expand Down Expand Up @@ -85,6 +87,31 @@ describe('Course', () => {
expect(screen.getByRole('button', { name: 'Learn About Verified Certificates' })).toBeInTheDocument();
});

it('displays sidebar notification button', async () => {
const toggleSidebar = jest.fn();
const isSidebarVisible = jest.fn();

// REV-2130 TODO: remove cookie related code once temporary value prop cookie code is removed.
const cookieName = 'value_prop_cookie';
Cookies.set = jest.fn();
Cookies.get = jest.fn().mockImplementation(() => cookieName);
const getSpy = jest.spyOn(Cookies, 'get').mockReturnValueOnce('true');

const courseMetadata = Factory.build('courseMetadata');
const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false);
const testData = {
...mockData,
toggleSidebar,
isSidebarVisible,
};
render(<Course {...testData} courseId={courseMetadata.id} />, { store: testStore });

const sidebarOpenButton = screen.getByRole('button', { name: /Show sidebar notification/i });

expect(getSpy).toBeCalledWith(cookieName);
expect(sidebarOpenButton).toBeInTheDocument();
});

it('displays offer and expiration alert', async () => {
const courseMetadata = Factory.build('courseMetadata', {
access_expiration: {
Expand Down
2 changes: 1 addition & 1 deletion src/courseware/course/CourseBreadcrumbs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default function CourseBreadcrumbs({
}, [courseStatus, sequenceStatus]);

return (
<nav aria-label="breadcrumb" className="my-4">
<nav aria-label="breadcrumb" className="my-4 d-inline-block col-sm-10">
<ol className="list-unstyled d-flex m-0">
<CourseBreadcrumb
url={`${getConfig().LMS_BASE_URL}/courses/${course.id}/course/`}
Expand Down
28 changes: 28 additions & 0 deletions src/courseware/course/NotificationIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { WatchOutline } from '@edx/paragon/icons';

import './NotificationIcon.scss';
import messages from './messages';

function NotificationIcon({ intl, status, notificationColor }) {
return (
<div className="icon-container">
<Icon src={WatchOutline} className="m-0 m-auto" alt={intl.formatMessage(messages.openSidebarButton)} />
{status === 'active'
? <span className={classNames(notificationColor, 'notification-dot')} data-testid="notification-dot" />
: null}
</div>
);
}

NotificationIcon.propTypes = {
intl: intlShape.isRequired,
status: PropTypes.string.isRequired,
notificationColor: PropTypes.string.isRequired,
};

export default injectIntl(NotificationIcon);
15 changes: 15 additions & 0 deletions src/courseware/course/NotificationIcon.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.icon-container {
position: relative;
display: flex;
align-items: center;
width: 2.4rem;
height: 2rem;
}

.notification-dot {
position: absolute;
top: 0.3rem;
right: 0.55rem;
border-radius: 50% !important;
padding: 0.25rem !important;
}
52 changes: 52 additions & 0 deletions src/courseware/course/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { ArrowBackIos, Close } from '@edx/paragon/icons';
import './Sidebar.scss';
import messages from './messages';
import useWindowSize, { responsiveBreakpoints } from '../../generic/tabs/useWindowSize';

function Sidebar({
intl, toggleSidebar,
}) {
const shouldDisplayFullScreen = useWindowSize().width < responsiveBreakpoints.large.minWidth;

// REV-2130 TODO: temporary variable set to true, should be replaced with
// whether the course can be upgraded (ie. shouldDisplayUpgradeNotification)
const shouldDisplayNoNotification = true;
Comment thread
jhan217 marked this conversation as resolved.

return (
<section className={classNames('sidebar-container ml-0 ml-lg-4', { 'no-notification': shouldDisplayNoNotification && !shouldDisplayFullScreen })} aria-label={intl.formatMessage(messages.sidebarNotification)}>
{shouldDisplayFullScreen ? (
<div className="mobile-close-container" onClick={() => { toggleSidebar(); }} onKeyDown={() => { toggleSidebar(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.responsiveCloseSidebar)}>
<Icon src={ArrowBackIos} />
<span className="mobile-close">{intl.formatMessage(messages.responsiveCloseSidebar)}</span>
</div>
) : null}
<div className="sidebar-header px-3">
<span>{intl.formatMessage(messages.notificationTitle)}</span>
{shouldDisplayFullScreen
? null
: <Icon src={Close} className="close-btn" onClick={() => { toggleSidebar(); }} onKeyDown={() => { toggleSidebar(); }} role="button" tabIndex="0" alt={intl.formatMessage(messages.closeSidebarButton)} />}
</div>
<div className="sidebar-divider" />
<div className="sidebar-content">
{/* REV-2130 TODO: replace logic to display upgrade expiration box if condition is true */}
{shouldDisplayNoNotification ? <p>{intl.formatMessage(messages.noNotificationsMessage)}</p> : null}
</div>
</section>
);
}

Sidebar.propTypes = {
intl: intlShape.isRequired,
toggleSidebar: PropTypes.func,
};

Sidebar.defaultProps = {
toggleSidebar: null,
};

export default injectIntl(Sidebar);
73 changes: 73 additions & 0 deletions src/courseware/course/Sidebar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";

.sidebar-container {
border: 1px solid $light-400;
border-radius: 4px;
width: 20rem;
vertical-align: top;

@media (max-width: -1 + map-get($grid-breakpoints, 'lg')) {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100% !important;
background-color: white;
margin: 0;
border: none;
border-radius: 0;
}
}

.no-notification {
height: 15rem;
}

.sidebar-header {
padding: 0.625rem 0;

span {
display: inline-block;
}
}

.close-btn {
float: right;
}

.sidebar-divider {
width: 100.5%;
height: 0.5rem;
background: $gray-100;
border: 1px solid $light-400;
border-left: 0;
}

.sidebar-content {
padding: 1rem;
font-size: 0.875rem;
}

.mobile-close-container {
padding-top: 0.5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid $light-400;

span {
display: inline-block;
}
svg {
top: 0.4rem;
left: 0.8rem;
}
}

.mobile-close {
font-weight: 500;
margin-left: 1.2rem;
}
58 changes: 58 additions & 0 deletions src/courseware/course/Sidebar.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { Factory } from 'rosie';
import {
render, initializeTestStore, screen, fireEvent, waitFor,
} from '../../setupTest';
import Sidebar from './Sidebar';
import useWindowSize from '../../generic/tabs/useWindowSize';

jest.mock('../../generic/tabs/useWindowSize');

describe('Sidebar', () => {
let mockData;
const courseMetadata = Factory.build('courseMetadata');

beforeEach(async () => {
mockData = {
toggleSidebar: () => {},
};
});

beforeAll(async () => {
await initializeTestStore({ courseMetadata, excludeFetchCourse: true, excludeFetchSequence: true });
});

it('renders sidebar', async () => {
useWindowSize.mockReturnValue({ width: 1200, height: 422 });
const { container } = render(<Sidebar {...mockData} />);

expect(container).toBeInTheDocument();
expect(container).toHaveTextContent('Notifications');
expect(container).not.toHaveTextContent('Back to course');
});

it('renders no notifications message', async () => {
// REV-2130 TODO: add conditional if no expiration box/upgradeable
const testData = { ...mockData };
const { container } = render(<Sidebar {...testData} />);

expect(container).toBeInTheDocument();
expect(container).toHaveTextContent('You have no new notifications at this time.');
});

it('renders sidebar with full screen "Back to course" at response width', async () => {
useWindowSize.mockReturnValue({ width: 991, height: 422 });
const toggleSidebar = jest.fn();
const testData = {
...mockData,
toggleSidebar,
};
render(<Sidebar {...testData} />);

const responsiveCloseButton = screen.getByRole('button', { name: 'Back to course' });
await waitFor(() => expect(responsiveCloseButton).toBeInTheDocument());

fireEvent.click(responsiveCloseButton);
expect(toggleSidebar).toHaveBeenCalledTimes(1);
});
});
Loading