Skip to content

Commit 06e5fb5

Browse files
authored
feat: Update previous and next unit navigation buttons design (#1617)
* feat: Update previous and next unit navigation buttons design * feat: add unit test * feat: move unit navigation to be inline with unit title
1 parent 2235737 commit 06e5fb5

File tree

11 files changed

+164
-43
lines changed

11 files changed

+164
-43
lines changed

src/courseware/course/sequence/Sequence.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ const Sequence = ({
203203
unitId={unitId}
204204
unitLoadedHandler={handleUnitLoaded}
205205
isOriginalUserStaff={originalUserIsStaff}
206+
isEnabledOutlineSidebar={isEnabledOutlineSidebar}
207+
renderUnitNavigation={renderUnitNavigation}
206208
/>
207209
{unitHasLoaded && renderUnitNavigation(false)}
208210
</div>
@@ -223,7 +225,6 @@ const Sequence = ({
223225
originalUserIsStaff={originalUserIsStaff}
224226
canAccessProctoredExams={canAccessProctoredExams}
225227
>
226-
{isEnabledOutlineSidebar && renderUnitNavigation(true)}
227228
{defaultContent}
228229
</SequenceExamWrapper>
229230
<CourseLicense license={license || undefined} />

src/courseware/course/sequence/SequenceContent.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const SequenceContent = ({
1616
unitId,
1717
unitLoadedHandler,
1818
isOriginalUserStaff,
19+
isEnabledOutlineSidebar,
20+
renderUnitNavigation,
1921
}) => {
2022
const intl = useIntl();
2123
const sequence = useModel('sequences', sequenceId);
@@ -61,6 +63,8 @@ const SequenceContent = ({
6163
id={unitId}
6264
onLoaded={unitLoadedHandler}
6365
isOriginalUserStaff={isOriginalUserStaff}
66+
isEnabledOutlineSidebar={isEnabledOutlineSidebar}
67+
renderUnitNavigation={renderUnitNavigation}
6468
/>
6569
);
6670
};
@@ -72,6 +76,8 @@ SequenceContent.propTypes = {
7276
unitId: PropTypes.string,
7377
unitLoadedHandler: PropTypes.func.isRequired,
7478
isOriginalUserStaff: PropTypes.bool.isRequired,
79+
isEnabledOutlineSidebar: PropTypes.bool.isRequired,
80+
renderUnitNavigation: PropTypes.func.isRequired,
7581
};
7682

7783
SequenceContent.defaultProps = {

src/courseware/course/sequence/Unit/__snapshots__/index.test.jsx.snap

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,22 @@ exports[`Unit component output snapshot: not bookmarked, do not show content 1`]
2121
className="unit"
2222
>
2323
<div
24-
className="mb-0"
24+
className="d-flex justify-content-between align-items-center"
2525
>
26-
<h3
27-
className="h3"
26+
<div
27+
className="mb-0"
2828
>
29-
unit-title
30-
</h3>
31-
<UnitTitleSlot
32-
courseId="test-course-id"
33-
unitId="test-props-id"
34-
unitTitle="unit-title"
35-
/>
29+
<h3
30+
className="h3"
31+
>
32+
unit-title
33+
</h3>
34+
<UnitTitleSlot
35+
courseId="test-course-id"
36+
unitId="test-props-id"
37+
unitTitle="unit-title"
38+
/>
39+
</div>
3640
</div>
3741
<p
3842
className="sr-only"

src/courseware/course/sequence/Unit/index.jsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const Unit = ({
2323
onLoaded,
2424
id,
2525
isOriginalUserStaff,
26+
isEnabledOutlineSidebar,
27+
renderUnitNavigation,
2628
}) => {
2729
const { formatMessage } = useIntl();
2830
const [searchParams] = useSearchParams();
@@ -48,9 +50,12 @@ const Unit = ({
4850

4951
return (
5052
<div className="unit">
51-
<div className="mb-0">
52-
<h3 className="h3">{unit.title}</h3>
53-
<UnitTitleSlot courseId={courseId} unitId={id} unitTitle={unit.title} />
53+
<div className="d-flex justify-content-between align-items-center">
54+
<div className="mb-0">
55+
<h3 className="h3">{unit.title}</h3>
56+
<UnitTitleSlot courseId={courseId} unitId={id} unitTitle={unit.title} />
57+
</div>
58+
{isEnabledOutlineSidebar && renderUnitNavigation(true)}
5459
</div>
5560
<p className="sr-only">{formatMessage(messages.headerPlaceholder)}</p>
5661
<BookmarkButton
@@ -79,6 +84,8 @@ Unit.propTypes = {
7984
id: PropTypes.string.isRequired,
8085
onLoaded: PropTypes.func,
8186
isOriginalUserStaff: PropTypes.bool.isRequired,
87+
isEnabledOutlineSidebar: PropTypes.bool.isRequired,
88+
renderUnitNavigation: PropTypes.func.isRequired,
8289
};
8390

8491
Unit.defaultProps = {

src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ const SequenceNavigation = ({
8888
return navigationDisabledNextSequence || (
8989
<NextUnitTopNavTriggerSlot
9090
{...{
91-
courseId,
9291
disabled,
9392
buttonText,
9493
nextLink,

src/courseware/course/sequence/sequence-navigation/UnitNavigation.jsx

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,36 +23,40 @@ const UnitNavigation = ({
2323
isFirstUnit, isLastUnit, nextLink, previousLink,
2424
} = useSequenceNavigationMetadata(sequenceId, unitId);
2525

26-
const renderPreviousButton = () => (
27-
<PreviousButton
28-
isFirstUnit={isFirstUnit}
29-
variant="outline-secondary"
30-
buttonLabel={intl.formatMessage(messages.previousButton)}
31-
buttonStyle="previous-button justify-content-center"
32-
onClick={onClickPrevious}
33-
previousLink={previousLink}
34-
/>
35-
);
26+
const renderPreviousButton = () => {
27+
const buttonStyle = `previous-button ${isAtTop ? 'text-dark mr-3' : 'justify-content-center'}`;
28+
return (
29+
<PreviousButton
30+
isFirstUnit={isFirstUnit}
31+
variant="outline-secondary"
32+
buttonLabel={intl.formatMessage(messages.previousButton)}
33+
buttonStyle={buttonStyle}
34+
onClick={onClickPrevious}
35+
previousLink={previousLink}
36+
isAtTop={isAtTop}
37+
/>
38+
);
39+
};
3640

3741
const renderNextButton = () => {
3842
const { exitActive, exitText } = GetCourseExitNavigation(courseId, intl);
3943
const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton);
4044
const disabled = isLastUnit && !exitActive;
4145
const variant = 'outline-primary';
42-
const buttonStyle = 'next-button justify-content-center';
46+
const buttonStyle = `next-button ${isAtTop ? 'text-dark' : 'justify-content-center'}`;
4347

4448
if (isAtTop) {
4549
return (
4650
<NextUnitTopNavTriggerSlot
4751
{...{
48-
courseId,
4952
variant,
5053
buttonStyle,
5154
buttonText,
5255
disabled,
5356
sequenceId,
5457
nextLink,
5558
onClickHandler: onClickNext,
59+
isAtTop,
5660
}}
5761
/>
5862
);
@@ -72,7 +76,11 @@ const UnitNavigation = ({
7276
};
7377

7478
return (
75-
<div className={classNames('unit-navigation d-flex', { 'top-unit-navigation mb-3 w-100': isAtTop })}>
79+
<div className={classNames('d-flex', {
80+
'unit-navigation': !isAtTop,
81+
'top-unit-navigation': isAtTop,
82+
})}
83+
>
7684
{renderPreviousButton()}
7785
{renderNextButton()}
7886
</div>

src/courseware/course/sequence/sequence-navigation/UnitNavigation.test.jsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import {
55
} from '../../../../setupTest';
66
import UnitNavigation from './UnitNavigation';
77

8+
const mockNavigate = jest.fn();
9+
10+
jest.mock('react-router-dom', () => ({
11+
...jest.requireActual('react-router-dom'),
12+
useNavigate: () => mockNavigate,
13+
}));
14+
815
describe('Unit Navigation', () => {
916
let mockData;
1017
const courseMetadata = Factory.build('courseMetadata');
@@ -56,6 +63,26 @@ describe('Unit Navigation', () => {
5663
expect(onClickNext).toHaveBeenCalledTimes(1);
5764
});
5865

66+
it('when clicked it calls navigate when is at the top', () => {
67+
const onClickPrevious = jest.fn();
68+
const onClickNext = jest.fn();
69+
70+
render(<UnitNavigation
71+
{...mockData}
72+
onClickPrevious={onClickPrevious}
73+
onClickNext={onClickNext}
74+
isAtTop
75+
/>, { wrapWithRouter: true });
76+
77+
fireEvent.click(screen.getByRole('button', { name: /previous/i }));
78+
expect(onClickPrevious).toHaveBeenCalledTimes(1);
79+
expect(mockNavigate).toHaveBeenCalledTimes(1);
80+
81+
fireEvent.click(screen.getByRole('button', { name: /next/i }));
82+
expect(onClickNext).toHaveBeenCalledTimes(1);
83+
expect(mockNavigate).toHaveBeenCalledTimes(2);
84+
});
85+
5986
it('has the navigation buttons enabled for the non-corner unit in the sequence', () => {
6087
render(<UnitNavigation {...mockData} />, { wrapWithRouter: true });
6188

src/courseware/course/sequence/sequence-navigation/generic/NextButton.jsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import PropTypes from 'prop-types';
2-
import { Link, useLocation } from 'react-router-dom';
3-
import { Button } from '@openedx/paragon';
4-
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
2+
import { Link, useLocation, useNavigate } from 'react-router-dom';
3+
import { Button, IconButton, Icon } from '@openedx/paragon';
4+
import {
5+
ArrowBack,
6+
ArrowForward,
7+
ChevronLeft,
8+
ChevronRight,
9+
} from '@openedx/paragon/icons';
510
import { isRtl, getLocale } from '@edx/frontend-platform/i18n';
611

712
import UnitNavigationEffortEstimate from '../UnitNavigationEffortEstimate';
@@ -14,8 +19,9 @@ const NextButton = ({
1419
buttonStyle,
1520
disabled,
1621
hasEffortEstimate,
22+
isAtTop,
1723
}) => {
18-
const nextArrow = isRtl(getLocale()) ? ChevronLeft : ChevronRight;
24+
const navigate = useNavigate();
1925
const { pathname } = useLocation();
2026
const navLink = pathname.startsWith('/preview') ? `/preview${nextLink}` : nextLink;
2127
const buttonContent = hasEffortEstimate ? (
@@ -24,6 +30,34 @@ const NextButton = ({
2430
</UnitNavigationEffortEstimate>
2531
) : buttonText;
2632

33+
const getNextArrow = () => {
34+
if (isAtTop) {
35+
return isRtl(getLocale()) ? ArrowBack : ArrowForward;
36+
}
37+
return isRtl(getLocale()) ? ChevronLeft : ChevronRight;
38+
};
39+
40+
const nextArrow = getNextArrow();
41+
42+
const onClick = () => {
43+
navigate(navLink);
44+
onClickHandler();
45+
};
46+
47+
if (isAtTop) {
48+
return (
49+
<IconButton
50+
variant="light"
51+
className={buttonStyle}
52+
onClick={onClick}
53+
src={nextArrow}
54+
disabled={disabled}
55+
iconAs={Icon}
56+
alt={buttonText}
57+
/>
58+
);
59+
}
60+
2761
return (
2862
<Button
2963
variant={variant}
@@ -51,6 +85,7 @@ NextButton.propTypes = {
5185
buttonStyle: PropTypes.string.isRequired,
5286
disabled: PropTypes.bool.isRequired,
5387
hasEffortEstimate: PropTypes.bool,
88+
isAtTop: PropTypes.bool.isRequired,
5489
};
5590

5691
export default NextButton;

src/courseware/course/sequence/sequence-navigation/generic/PreviousButton.jsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import PropTypes from 'prop-types';
2-
import { Link, useLocation } from 'react-router-dom';
3-
import { Button } from '@openedx/paragon';
4-
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
2+
import { Link, useLocation, useNavigate } from 'react-router-dom';
3+
import { Button, IconButton, Icon } from '@openedx/paragon';
4+
import {
5+
ArrowBack,
6+
ArrowForward,
7+
ChevronLeft,
8+
ChevronRight,
9+
} from '@openedx/paragon/icons';
510
import { isRtl, getLocale } from '@edx/frontend-platform/i18n';
611

712
const PreviousButton = ({
@@ -11,12 +16,41 @@ const PreviousButton = ({
1116
variant,
1217
buttonStyle,
1318
isFirstUnit,
19+
isAtTop,
1420
}) => {
21+
const navigate = useNavigate();
1522
const disabled = isFirstUnit;
16-
const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft;
1723
const { pathname } = useLocation();
1824
const navLink = pathname.startsWith('/preview') ? `/preview${previousLink}` : previousLink;
1925

26+
const getPrevArrow = () => {
27+
if (isAtTop) {
28+
return isRtl(getLocale()) ? ArrowForward : ArrowBack;
29+
}
30+
return isRtl(getLocale()) ? ChevronRight : ChevronLeft;
31+
};
32+
33+
const prevArrow = getPrevArrow();
34+
35+
const onClickHandler = () => {
36+
navigate(navLink);
37+
onClick();
38+
};
39+
40+
if (isAtTop) {
41+
return (
42+
<IconButton
43+
variant="light"
44+
className={buttonStyle}
45+
onClick={onClickHandler}
46+
src={prevArrow}
47+
disabled={disabled}
48+
iconAs={Icon}
49+
alt={buttonLabel}
50+
/>
51+
);
52+
}
53+
2054
return (
2155
<Button
2256
variant={variant}
@@ -39,6 +73,7 @@ PreviousButton.propTypes = {
3973
variant: PropTypes.string.isRequired,
4074
buttonStyle: PropTypes.string.isRequired,
4175
isFirstUnit: PropTypes.bool.isRequired,
76+
isAtTop: PropTypes.bool.isRequired,
4277
};
4378

4479
export default PreviousButton;

src/index.scss

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -363,15 +363,13 @@
363363
}
364364

365365
.top-unit-navigation {
366+
display: flex;
366367
max-width: 100%;
367368
justify-content: flex-end;
368369

369370
.next-button,
370371
.previous-button {
371-
@media (min-width: map-get($grid-breakpoints, "md")) {
372-
flex-basis: auto;
373-
min-width: 8rem;
374-
}
372+
font-size: 1.5rem;
375373
}
376374
}
377375

0 commit comments

Comments
 (0)