Skip to content

Commit 82a440c

Browse files
committed
feat: Make selectable Section, Subsection, unit cards in Course Outline
1 parent b76b003 commit 82a440c

7 files changed

Lines changed: 122 additions & 5 deletions

File tree

src/course-outline/CourseOutline.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { useCourseOutline } from './hooks';
6262
import messages from './messages';
6363
import { getTagsExportFile } from './data/api';
6464
import OutlineAddChildButtons from './OutlineAddChildButtons';
65+
import { SidebarProvider } from './common/context/SidebarContext';
6566

6667
interface CourseOutlineProps {
6768
courseId: string,
@@ -281,7 +282,7 @@ const CourseOutline = ({ courseId }: CourseOutlineProps) => {
281282
}
282283

283284
return (
284-
<>
285+
<SidebarProvider>
285286
<Helmet>
286287
<title>{getPageHeadTitle(courseName, intl.formatMessage(messages.headingTitle))}</title>
287288
</Helmet>
@@ -579,7 +580,7 @@ const CourseOutline = ({ courseId }: CourseOutlineProps) => {
579580
{toastMessage}
580581
</Toast>
581582
)}
582-
</>
583+
</SidebarProvider>
583584
);
584585
};
585586

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createContext, useCallback, useContext, useMemo, useState } from "react";
2+
3+
export type SidebarContextData = {
4+
selectedContainerId?: string;
5+
openContainerInfoSidebar: (containerId: string) => void;
6+
};
7+
8+
/**
9+
* Course Outline Sidebar Context.
10+
*
11+
* Get this using `useSidebarContext()`
12+
*
13+
*/
14+
const SidebarContext = createContext<SidebarContextData | undefined>(undefined);
15+
16+
type SidebarProviderProps = {
17+
children?: React.ReactNode;
18+
};
19+
20+
export const SidebarProvider = ({ children }: SidebarProviderProps) => {
21+
const [ selectedContainerId, setSelectedContainerId ] = useState<string | undefined>();
22+
23+
const openContainerInfoSidebar = useCallback((containerId: string) => {
24+
setSelectedContainerId(containerId);
25+
}, [setSelectedContainerId]);
26+
27+
const context = useMemo<SidebarContextData>(() => {
28+
const contextValue = {
29+
selectedContainerId,
30+
openContainerInfoSidebar,
31+
};
32+
33+
return contextValue;
34+
}, [
35+
selectedContainerId,
36+
openContainerInfoSidebar,
37+
]);
38+
39+
return (
40+
<SidebarContext.Provider value={context}>
41+
{children}
42+
</SidebarContext.Provider>
43+
);
44+
};
45+
46+
export function useSidebarContext(): SidebarContextData {
47+
const ctx = useContext(SidebarContext);
48+
if (ctx === undefined) {
49+
/* istanbul ignore next */
50+
return {
51+
selectedContainerId: undefined,
52+
openContainerInfoSidebar: () => {},
53+
};
54+
}
55+
return ctx;
56+
}

src/course-outline/drag-helper/SortableItem.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface SortableItemProps {
2121
isDraggable?: boolean;
2222
children: React.ReactNode;
2323
componentStyle?: object;
24+
onClick?: (e: React.MouseEvent) => void;
2425
}
2526

2627
const SortableItem = ({
@@ -30,6 +31,7 @@ const SortableItem = ({
3031
componentStyle,
3132
data,
3233
children,
34+
onClick,
3335
}: SortableItemProps) => {
3436
const intl = useIntl();
3537
const {
@@ -66,8 +68,18 @@ const SortableItem = ({
6668
return (
6769
<Row
6870
ref={setNodeRef}
71+
tabIndex={onClick ? 0 : -1}
6972
style={style}
7073
className="mx-0"
74+
onClick={onClick}
75+
onKeyDown={(e) => {
76+
if (!onClick) return;
77+
78+
if (e.key === "Enter" || e.key === " ") {
79+
e.preventDefault();
80+
onClick(e);
81+
}
82+
}}
7183
>
7284
<Col className="extend-margin px-0">
7385
{children}

src/course-outline/section-card/SectionCard.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
2929
import type { XBlock } from '@src/data/types';
3030
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
3131
import messages from './messages';
32+
import { useSidebarContext } from '../common/context/SidebarContext';
3233

3334
interface SectionCardProps {
3435
section: XBlock,
@@ -77,6 +78,7 @@ const SectionCard = ({
7778
const intl = useIntl();
7879
const dispatch = useDispatch();
7980
const { activeId, overId } = useContext(DragContext);
81+
const { selectedContainerId, openContainerInfoSidebar } = useSidebarContext();
8082
const [searchParams] = useSearchParams();
8183
const locatorId = searchParams.get('show');
8284
const isScrolledToElement = locatorId === section.id;
@@ -263,6 +265,12 @@ const SectionCard = ({
263265

264266
const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown);
265267

268+
const onClickCard = useCallback((e: React.MouseEvent) => {
269+
if (e.target === e.currentTarget) {
270+
openContainerInfoSidebar(section.id);
271+
}
272+
}, [openContainerInfoSidebar]);
273+
266274
return (
267275
<>
268276
<SortableItem
@@ -278,9 +286,15 @@ const SectionCard = ({
278286
padding: '1.75rem',
279287
...borderStyle,
280288
}}
289+
onClick={onClickCard}
281290
>
282291
<div
283-
className={`section-card ${isScrolledToElement ? 'highlight' : ''}`}
292+
className={classNames('section-card',
293+
{
294+
'highlight': isScrolledToElement,
295+
'outline-card-selected': section.id === selectedContainerId,
296+
}
297+
)}
284298
data-testid="section-card"
285299
ref={currentRef}
286300
>

src/course-outline/subsection-card/SubsectionCard.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
3030
import type { XBlock } from '@src/data/types';
3131
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
3232
import messages from './messages';
33+
import { useSidebarContext } from '../common/context/SidebarContext';
3334

3435
interface SubsectionCardProps {
3536
section: XBlock,
@@ -88,6 +89,7 @@ const SubsectionCard = ({
8889
const intl = useIntl();
8990
const dispatch = useDispatch();
9091
const { activeId, overId } = useContext(DragContext);
92+
const { selectedContainerId, openContainerInfoSidebar } = useSidebarContext();
9193
const [searchParams] = useSearchParams();
9294
const locatorId = searchParams.get('show');
9395
const isScrolledToElement = locatorId === subsection.id;
@@ -263,6 +265,12 @@ const SubsectionCard = ({
263265
closeAddLibraryUnitModal();
264266
}, [id, onAddUnitFromLibrary, closeAddLibraryUnitModal]);
265267

268+
const onClickCard = useCallback((e: React.MouseEvent) => {
269+
if (e.target === e.currentTarget) {
270+
openContainerInfoSidebar(subsection.id);
271+
}
272+
}, [openContainerInfoSidebar]);
273+
266274
return (
267275
<>
268276
<SortableItem
@@ -280,9 +288,15 @@ const SubsectionCard = ({
280288
background: '#f8f7f6',
281289
...borderStyle,
282290
}}
291+
onClick={onClickCard}
283292
>
284293
<div
285-
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`}
294+
className={classNames('subsection-card',
295+
{
296+
'highlight': isScrolledToElement,
297+
'outline-card-selected': subsection.id === selectedContainerId,
298+
}
299+
)}
286300
data-testid="subsection-card"
287301
ref={currentRef}
288302
>

src/course-outline/unit-card/UnitCard.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
useMemo,
55
useRef,
66
} from 'react';
7+
import classNames from 'classnames';
78
import { useDispatch } from 'react-redux';
89
import { useToggle } from '@openedx/paragon';
910
import { isEmpty } from 'lodash';
@@ -24,6 +25,7 @@ import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
2425
import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
2526
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
2627
import type { XBlock } from '@src/data/types';
28+
import { useSidebarContext } from '../common/context/SidebarContext';
2729

2830
interface UnitCardProps {
2931
unit: XBlock;
@@ -70,6 +72,7 @@ const UnitCard = ({
7072
const currentRef = useRef(null);
7173
const dispatch = useDispatch();
7274
const [searchParams] = useSearchParams();
75+
const { selectedContainerId, openContainerInfoSidebar } = useSidebarContext();
7376
const locatorId = searchParams.get('show');
7477
const isScrolledToElement = locatorId === unit.id;
7578
const [isFormOpen, openForm, closeForm] = useToggle(false);
@@ -207,6 +210,12 @@ const UnitCard = ({
207210
&& !subsection.upstreamInfo?.upstreamRef
208211
);
209212

213+
const onClickCard = useCallback((e: React.MouseEvent) => {
214+
if (e.target === e.currentTarget) {
215+
openContainerInfoSidebar(unit.id);
216+
}
217+
}, [openContainerInfoSidebar]);
218+
210219
return (
211220
<>
212221
<SortableItem
@@ -223,9 +232,15 @@ const UnitCard = ({
223232
background: '#fdfdfd',
224233
...borderStyle,
225234
}}
235+
onClick={onClickCard}
226236
>
227237
<div
228-
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`}
238+
className={classNames('unit-card',
239+
{
240+
'highlight': isScrolledToElement,
241+
'outline-card-selected': unit.id === selectedContainerId,
242+
}
243+
)}
229244
data-testid="unit-card"
230245
ref={currentRef}
231246
>

src/index.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ div.row:has(> div > div.highlight) {
3939
animation-timing-function: cubic-bezier(1, 0, .72, .04);
4040
}
4141

42+
// To apply selection style to selected Section/Subsecion/Units, in the Course Outline
43+
div.row:has(> div > div.outline-card-selected) {
44+
box-shadow: 0 0 3px 3px var(--pgn-color-primary-500) !important;
45+
}
46+
4247
// To apply the glow effect to the selected xblock, in the Unit Outline
4348
div.xblock-highlight {
4449
animation: 5s glow;

0 commit comments

Comments
 (0)