diff --git a/src/course-tabs/CourseTabsNavigation.jsx b/src/course-tabs/CourseTabsNavigation.tsx similarity index 70% rename from src/course-tabs/CourseTabsNavigation.jsx rename to src/course-tabs/CourseTabsNavigation.tsx index 9c2a12ef8c..8d773e95c8 100644 --- a/src/course-tabs/CourseTabsNavigation.jsx +++ b/src/course-tabs/CourseTabsNavigation.tsx @@ -1,21 +1,31 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { useIntl } from '@edx/frontend-platform/i18n'; import classNames from 'classnames'; - -import messages from './messages'; -import Tabs from '../generic/tabs/Tabs'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { CourseTabLinksSlot } from '../plugin-slots/CourseTabLinksSlot'; import { CoursewareSearch, CoursewareSearchToggle } from '../course-home/courseware-search'; import { useCoursewareSearchState } from '../course-home/courseware-search/hooks'; +import Tabs from '../generic/tabs/Tabs'; +import messages from './messages'; + +export interface CourseTabsNavigationProps { + activeTabSlug?: string; + tabs: Array<{ + title: string; + slug: string; + url: string; + }>; +} + const CourseTabsNavigation = ({ - activeTabSlug, className, tabs, -}) => { + activeTabSlug = undefined, + tabs, +}:CourseTabsNavigationProps) => { const intl = useIntl(); const { show } = useCoursewareSearchState(); return ( -
+
@@ -44,19 +54,4 @@ const CourseTabsNavigation = ({ ); }; -CourseTabsNavigation.propTypes = { - activeTabSlug: PropTypes.string, - className: PropTypes.string, - tabs: PropTypes.arrayOf(PropTypes.shape({ - title: PropTypes.string.isRequired, - slug: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - })).isRequired, -}; - -CourseTabsNavigation.defaultProps = { - activeTabSlug: undefined, - className: null, -}; - export default CourseTabsNavigation; diff --git a/src/course-tabs/index.js b/src/course-tabs/index.ts similarity index 50% rename from src/course-tabs/index.js rename to src/course-tabs/index.ts index e2236ee726..801de27856 100644 --- a/src/course-tabs/index.js +++ b/src/course-tabs/index.ts @@ -1,2 +1,2 @@ -/* eslint-disable import/prefer-default-export */ export { default as CourseTabsNavigation } from './CourseTabsNavigation'; +export type { CourseTabsNavigationProps } from './CourseTabsNavigation'; diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx similarity index 66% rename from src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx rename to src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx index 8eaf5e8a0b..c6b156fe40 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx @@ -1,85 +1,93 @@ -import { useState } from 'react'; -import classNames from 'classnames'; -import { Button, useToggle, IconButton } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button, IconButton, useToggle } from '@openedx/paragon'; +import { LOADING } from '@src/constants'; import { MenuOpen as MenuOpenIcon, ChevronLeft as ChevronLeftIcon, } from '@openedx/paragon/icons'; - -import { LOADING } from '@src/constants'; +import { + useCourseOutlineData, +} from '@src/courseware/course/sidebar/sidebars/course-outline/hooks'; import PageLoading from '@src/generic/PageLoading'; -import SidebarSection from './components/SidebarSection'; +import classNames from 'classnames'; +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; import SidebarSequence from './components/SidebarSequence'; -import { ID } from './constants'; -import { useCourseOutlineSidebar } from './hooks'; +import SidebarSection from './components/SidebarSection'; import messages from './messages'; -const CourseOutlineTray = () => { +interface CourseOutlineProps { + shouldDisplayFullScreen?: boolean; + onToggleCollapse?: () => void; +} + +interface CoursePageParams extends Record { + courseId: string; + unitId: string; +} + +export const CourseOutline = ({ + shouldDisplayFullScreen = false, + onToggleCollapse, +}: CourseOutlineProps) => { const intl = useIntl(); - const [selectedSection, setSelectedSection] = useState(null); + const [selectedSection, setSelectedSection] = useState(null); const [isDisplaySequenceLevel, setDisplaySequenceLevel, setDisplaySectionLevel] = useToggle(true); - + const { unitId, courseId } = useParams(); const { - courseId, - unitId, - currentSidebar, - handleToggleCollapse, - isActiveEntranceExam, - shouldDisplayFullScreen, courseOutlineStatus, activeSequenceId, sections, sequences, - } = useCourseOutlineSidebar(); + isActiveEntranceExam, + } = useCourseOutlineData(); const resolvedSectionId = selectedSection - || Object.keys(sections).find( - (sectionId) => sections[sectionId].sequenceIds.includes(activeSequenceId), - ); + || Object.keys(sections).find( + (sectionId):boolean => sections[sectionId].sequenceIds.includes(activeSequenceId), + )!; const sectionsIds = Object.keys(sections); - const sequenceIds = sections[resolvedSectionId]?.sequenceIds || []; - const backButtonTitle = sections[resolvedSectionId]?.title; + const sequenceIds: string[] = sections[resolvedSectionId]?.sequenceIds || []; + const backButtonTitle: string | undefined = sections[resolvedSectionId]?.title; const handleBackToSectionLevel = () => { setDisplaySectionLevel(); setSelectedSection(null); }; - const handleSelectSection = (id) => { + const handleSelectSection = (id:string) => { setDisplaySequenceLevel(); setSelectedSection(id); }; - const sidebarHeading = ( -
+
{isDisplaySequenceLevel && backButtonTitle ? ( ) : ( - + {intl.formatMessage(messages.courseOutlineTitle)} )} + {onToggleCollapse && ( + )}
); - - if (isActiveEntranceExam || currentSidebar !== ID) { + if (isActiveEntranceExam) { return null; } - if (courseOutlineStatus === LOADING) { return (
{ {sidebarHeading}
    {isDisplaySequenceLevel - ? sequenceIds.map((sequenceId) => ( + ? sequenceIds.map((sequenceId: string) => ( )) : sectionsIds.map((sectionId) => ( @@ -129,7 +136,3 @@ const CourseOutlineTray = () => {
); }; - -CourseOutlineTray.ID = ID; - -export default CourseOutlineTray; diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss index a574f02736..ed06a6424a 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss @@ -13,7 +13,10 @@ } .outline-sidebar-heading-wrapper { - border: 1px solid #d7d3d1; + border-width: var(--learning-sidebar-outline-heading-wrapper-border-width, 1px); + border-style: solid; + border-color: var(--learning-sidebar-outline-heading-wrapper-border-color, var(--pgn-color-light-700)); + background-color: var(--learning-sidebar-outline-heading-wrapper-bg-color, var(--pgn-color-light-200)); &.sticky { position: sticky; @@ -24,12 +27,18 @@ .outline-sidebar-heading { font-weight: var(--pgn-typography-font-weight-bold); + color: var(--learning-sidebar-outline-heading-text-color, var(--pgn-color-dark-500)); } } .course-sidebar-section { - background: var(--pgn-color-white); - border: 1px solid #d7d3d1; + background: var(--learning-sidebar-outline-section-bg-color, var(--pgn-color-white)); + + border-width: var(--learning-sidebar-outline-section-border-width, 1px); + border-style: solid; + border-color: var(--learning-sidebar-outline-section-border-color, var(--pgn-color-light-700)); + + margin-bottom: var(--learning-sidebar-outline-section-margin-bottom, var(--pgn-spacing-spacer-2)); button { line-height: 1.75rem; @@ -38,11 +47,17 @@ &:focus::before { border-radius: 0; } + + padding: var(--learning-sidebar-outline-section-padding-y, var(--pgn-spacing-spacer-3-5)) var(--learning-sidebar-outline-section-padding-x, var(--pgn-spacing-spacer-4)); + } + &.active-section button { + background-color: var(--learning-sidebar-outline-section-active-bg-color, var(--pgn-color-light-100)); } } .outline-sidebar-toggle-btn { font-size: 1.5rem; + background-color: var(--learning-sidebar-outline-toggle-btn-bg-color, var(--pgn-color-light-200)); .collapsed & { transform: scale(-1, 1); @@ -78,7 +93,7 @@ } &:last-child .pgn_collapsible { - margin-bottom: 0px !important; + margin-bottom: 0 !important; } } diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx index aba41c8291..653a9bc8d5 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx @@ -74,7 +74,7 @@ describe('', () => { await expect(screen.queryByText(messages.loading.defaultMessage)).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: section.title })).toBeInTheDocument(); expect(screen.getByRole('button', { name: messages.toggleCourseOutlineTrigger.defaultMessage })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: `${sequence.title} , ${courseOutlineMessages.incompleteAssignment.defaultMessage}` })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: new RegExp(`${sequence.title} , ${courseOutlineMessages.incompleteAssignment.defaultMessage}`) })).toBeInTheDocument(); expect(screen.getByText(unit.title)).toBeInTheDocument(); }); @@ -115,13 +115,13 @@ describe('', () => { const sidebarBackBtn = screen.queryByRole('button', { name: section.title }); expect(sidebarBackBtn).toBeInTheDocument(); - expect(screen.getByRole('button', { name: `${sequence.title} , ${courseOutlineMessages.incompleteAssignment.defaultMessage}` })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: new RegExp(`${sequence.title} , ${courseOutlineMessages.incompleteAssignment.defaultMessage}`) })).toBeInTheDocument(); await user.click(sidebarBackBtn); expect(sidebarBackBtn).not.toBeInTheDocument(); expect(screen.queryByText(messages.courseOutlineTitle.defaultMessage)).toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: `${section.title} , ${courseOutlineMessages.incompleteSection.defaultMessage}` })); + await user.click(screen.getByRole('button', { name: new RegExp(`${section.title} , ${courseOutlineMessages.incompleteSection.defaultMessage}`) })); expect(screen.queryByRole('button', { name: section.title })).toBeInTheDocument(); }); }); diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx new file mode 100644 index 0000000000..88f30fcfbe --- /dev/null +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx @@ -0,0 +1,20 @@ +import { CourseOutline } from './CourseOutline'; +import { ID } from './constants'; +import { useCourseOutlineSidebar } from './hooks'; + +const CourseOutlineTray = () => { + const { + currentSidebar, + shouldDisplayFullScreen, + handleToggleCollapse, + } = useCourseOutlineSidebar(); + + if (currentSidebar !== ID) { + return null; + } + return ; +}; + +CourseOutlineTray.ID = ID; + +export default CourseOutlineTray; diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx index abccd14aed..3d6039598f 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx @@ -4,17 +4,17 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { IconButton } from '@openedx/paragon'; import { MenuOpen as MenuOpenIcon } from '@openedx/paragon/icons'; -import { useCourseOutlineSidebar } from './hooks'; +import { useCourseOutlineData, useCourseOutlineSidebar } from './hooks'; import { ID } from './constants'; import messages from './messages'; const CourseOutlineTrigger = ({ isMobileView }) => { const intl = useIntl(); + const { isActiveEntranceExam } = useCourseOutlineData(); const { currentSidebar, shouldDisplayFullScreen, handleToggleCollapse, - isActiveEntranceExam, } = useCourseOutlineSidebar(); const isDisplayForDesktopView = !isMobileView && !shouldDisplayFullScreen && currentSidebar !== ID; diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.tsx similarity index 97% rename from src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx rename to src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.tsx index cb026fb46b..7664b02357 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; -import CompletionIcon from './CompletionIcon'; +import { CompletionIcon } from './CompletionIcon'; describe('CompletionIcon', () => { it('renders check circle icon when completion is equal to total and completion tracking is enabled', () => { diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.tsx similarity index 70% rename from src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx rename to src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.tsx index 9b3a855db8..a3fa67f5d7 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { CheckCircle as CheckCircleIcon, LmsCompletionSolid as LmsCompletionSolidIcon, @@ -6,7 +5,15 @@ import { import { DashedCircleIcon } from '../icons'; -const CompletionIcon = ({ completionStat: { completed = 0, total = 0 }, enabled }) => { +export interface CompletionIconProps { + completionStat: { + completed: number; + total: number; + }; + enabled: boolean; +} + +export const CompletionIcon = ({ completionStat: { completed = 0, total = 0 }, enabled }: CompletionIconProps) => { const percentage = total !== 0 ? Math.min((completed / total) * 100, 100) : 0; const remainder = 100 - percentage; @@ -20,12 +27,4 @@ const CompletionIcon = ({ completionStat: { completed = 0, total = 0 }, enabled } }; -CompletionIcon.propTypes = { - completionStat: PropTypes.shape({ - completed: PropTypes.number, - total: PropTypes.number, - }).isRequired, - enabled: PropTypes.bool.isRequired, -}; - export default CompletionIcon; diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx index 034575743b..92881ffcc2 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx @@ -1,3 +1,6 @@ +import { + CourseOverviewSectionCompletionIconSlot, +} from '@src/plugin-slots/CourseOutlineSidebarSectionCompletionIconSlot'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -5,8 +8,7 @@ import { Button, Icon } from '@openedx/paragon'; import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons'; import courseOutlineMessages from '@src/course-home/outline-tab/messages'; -import CompletionIcon from './CompletionIcon'; -import { useCourseOutlineSidebar } from '../hooks'; +import { useCourseOutlineData } from '../hooks'; const SidebarSection = ({ section, handleSelectSection }) => { const intl = useIntl(); @@ -18,13 +20,17 @@ const SidebarSection = ({ section, handleSelectSection }) => { completionStat, } = section; - const { activeSequenceId, isEnabledCompletionTracking } = useCourseOutlineSidebar(); + const { activeSequenceId, isEnabledCompletionTracking } = useCourseOutlineData(); const isActiveSection = sequenceIds.includes(activeSequenceId); const sectionTitle = ( <>
- +
{title} @@ -41,13 +47,10 @@ const SidebarSection = ({ section, handleSelectSection }) => { ); return ( -
  • +