From 06e5fb5a4450cd2463090f2226a76678909c03f0 Mon Sep 17 00:00:00 2001 From: Rodrigo Martin Date: Tue, 4 Mar 2025 11:05:39 -0300 Subject: [PATCH] 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 --- src/courseware/course/sequence/Sequence.jsx | 3 +- .../course/sequence/SequenceContent.jsx | 6 +++ .../Unit/__snapshots__/index.test.jsx.snap | 24 ++++++----- src/courseware/course/sequence/Unit/index.jsx | 13 ++++-- .../SequenceNavigation.jsx | 1 - .../sequence-navigation/UnitNavigation.jsx | 34 +++++++++------ .../UnitNavigation.test.jsx | 27 ++++++++++++ .../generic/NextButton.jsx | 43 +++++++++++++++++-- .../generic/PreviousButton.jsx | 43 +++++++++++++++++-- src/index.scss | 6 +-- .../NextUnitTopNavTriggerSlot/index.tsx | 7 +-- 11 files changed, 164 insertions(+), 43 deletions(-) diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx index 56828b91..6c56b219 100644 --- a/src/courseware/course/sequence/Sequence.jsx +++ b/src/courseware/course/sequence/Sequence.jsx @@ -203,6 +203,8 @@ const Sequence = ({ unitId={unitId} unitLoadedHandler={handleUnitLoaded} isOriginalUserStaff={originalUserIsStaff} + isEnabledOutlineSidebar={isEnabledOutlineSidebar} + renderUnitNavigation={renderUnitNavigation} /> {unitHasLoaded && renderUnitNavigation(false)} @@ -223,7 +225,6 @@ const Sequence = ({ originalUserIsStaff={originalUserIsStaff} canAccessProctoredExams={canAccessProctoredExams} > - {isEnabledOutlineSidebar && renderUnitNavigation(true)} {defaultContent} diff --git a/src/courseware/course/sequence/SequenceContent.jsx b/src/courseware/course/sequence/SequenceContent.jsx index 567883f5..905ffbf2 100644 --- a/src/courseware/course/sequence/SequenceContent.jsx +++ b/src/courseware/course/sequence/SequenceContent.jsx @@ -16,6 +16,8 @@ const SequenceContent = ({ unitId, unitLoadedHandler, isOriginalUserStaff, + isEnabledOutlineSidebar, + renderUnitNavigation, }) => { const intl = useIntl(); const sequence = useModel('sequences', sequenceId); @@ -61,6 +63,8 @@ const SequenceContent = ({ id={unitId} onLoaded={unitLoadedHandler} isOriginalUserStaff={isOriginalUserStaff} + isEnabledOutlineSidebar={isEnabledOutlineSidebar} + renderUnitNavigation={renderUnitNavigation} /> ); }; @@ -72,6 +76,8 @@ SequenceContent.propTypes = { unitId: PropTypes.string, unitLoadedHandler: PropTypes.func.isRequired, isOriginalUserStaff: PropTypes.bool.isRequired, + isEnabledOutlineSidebar: PropTypes.bool.isRequired, + renderUnitNavigation: PropTypes.func.isRequired, }; SequenceContent.defaultProps = { diff --git a/src/courseware/course/sequence/Unit/__snapshots__/index.test.jsx.snap b/src/courseware/course/sequence/Unit/__snapshots__/index.test.jsx.snap index 9fa03431..f8cc9543 100644 --- a/src/courseware/course/sequence/Unit/__snapshots__/index.test.jsx.snap +++ b/src/courseware/course/sequence/Unit/__snapshots__/index.test.jsx.snap @@ -21,18 +21,22 @@ exports[`Unit component output snapshot: not bookmarked, do not show content 1`] className="unit" >
-

- unit-title -

- +

+ unit-title +

+ +

{ const { formatMessage } = useIntl(); const [searchParams] = useSearchParams(); @@ -48,9 +50,12 @@ const Unit = ({ return (

-
-

{unit.title}

- +
+
+

{unit.title}

+ +
+ {isEnabledOutlineSidebar && renderUnitNavigation(true)}

{formatMessage(messages.headerPlaceholder)}

( - - ); + const renderPreviousButton = () => { + const buttonStyle = `previous-button ${isAtTop ? 'text-dark mr-3' : 'justify-content-center'}`; + return ( + + ); + }; const renderNextButton = () => { const { exitActive, exitText } = GetCourseExitNavigation(courseId, intl); const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton); const disabled = isLastUnit && !exitActive; const variant = 'outline-primary'; - const buttonStyle = 'next-button justify-content-center'; + const buttonStyle = `next-button ${isAtTop ? 'text-dark' : 'justify-content-center'}`; if (isAtTop) { return ( ); @@ -72,7 +76,11 @@ const UnitNavigation = ({ }; return ( -
+
{renderPreviousButton()} {renderNextButton()}
diff --git a/src/courseware/course/sequence/sequence-navigation/UnitNavigation.test.jsx b/src/courseware/course/sequence/sequence-navigation/UnitNavigation.test.jsx index 26e3034f..e812d67d 100644 --- a/src/courseware/course/sequence/sequence-navigation/UnitNavigation.test.jsx +++ b/src/courseware/course/sequence/sequence-navigation/UnitNavigation.test.jsx @@ -5,6 +5,13 @@ import { } from '../../../../setupTest'; import UnitNavigation from './UnitNavigation'; +const mockNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + describe('Unit Navigation', () => { let mockData; const courseMetadata = Factory.build('courseMetadata'); @@ -56,6 +63,26 @@ describe('Unit Navigation', () => { expect(onClickNext).toHaveBeenCalledTimes(1); }); + it('when clicked it calls navigate when is at the top', () => { + const onClickPrevious = jest.fn(); + const onClickNext = jest.fn(); + + render(, { wrapWithRouter: true }); + + fireEvent.click(screen.getByRole('button', { name: /previous/i })); + expect(onClickPrevious).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByRole('button', { name: /next/i })); + expect(onClickNext).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledTimes(2); + }); + it('has the navigation buttons enabled for the non-corner unit in the sequence', () => { render(, { wrapWithRouter: true }); diff --git a/src/courseware/course/sequence/sequence-navigation/generic/NextButton.jsx b/src/courseware/course/sequence/sequence-navigation/generic/NextButton.jsx index 40de2c51..b9f00da3 100644 --- a/src/courseware/course/sequence/sequence-navigation/generic/NextButton.jsx +++ b/src/courseware/course/sequence/sequence-navigation/generic/NextButton.jsx @@ -1,7 +1,12 @@ import PropTypes from 'prop-types'; -import { Link, useLocation } from 'react-router-dom'; -import { Button } from '@openedx/paragon'; -import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { Button, IconButton, Icon } from '@openedx/paragon'; +import { + ArrowBack, + ArrowForward, + ChevronLeft, + ChevronRight, +} from '@openedx/paragon/icons'; import { isRtl, getLocale } from '@edx/frontend-platform/i18n'; import UnitNavigationEffortEstimate from '../UnitNavigationEffortEstimate'; @@ -14,8 +19,9 @@ const NextButton = ({ buttonStyle, disabled, hasEffortEstimate, + isAtTop, }) => { - const nextArrow = isRtl(getLocale()) ? ChevronLeft : ChevronRight; + const navigate = useNavigate(); const { pathname } = useLocation(); const navLink = pathname.startsWith('/preview') ? `/preview${nextLink}` : nextLink; const buttonContent = hasEffortEstimate ? ( @@ -24,6 +30,34 @@ const NextButton = ({ ) : buttonText; + const getNextArrow = () => { + if (isAtTop) { + return isRtl(getLocale()) ? ArrowBack : ArrowForward; + } + return isRtl(getLocale()) ? ChevronLeft : ChevronRight; + }; + + const nextArrow = getNextArrow(); + + const onClick = () => { + navigate(navLink); + onClickHandler(); + }; + + if (isAtTop) { + return ( + + ); + } + return (