Skip to content

Commit 4cda17e

Browse files
authored
feat: Make sections/subsections/units selectable in course outline [FC-0114] (#2732)
1 parent 969f7a2 commit 4cda17e

File tree

18 files changed

+302
-86
lines changed

18 files changed

+302
-86
lines changed

src/course-outline/CourseOutline.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useState, useEffect, useCallback } from 'react';
22
import { useIntl } from '@edx/frontend-platform/i18n';
3-
import { getConfig } from '@edx/frontend-platform';
43
import {
54
Container,
65
Row,
@@ -69,6 +68,7 @@ import OutlineAddChildButtons from './OutlineAddChildButtons';
6968
import { OutlineSidebarProvider } from './outline-sidebar/OutlineSidebarContext';
7069
import { StatusBar } from './status-bar/StatusBar';
7170
import { LegacyStatusBar } from './status-bar/LegacyStatusBar';
71+
import { isOutlineNewDesignEnabled } from './utils';
7272

7373
const CourseOutline = () => {
7474
const intl = useIntl();
@@ -148,7 +148,7 @@ const CourseOutline = () => {
148148

149149
// Show the new actions bar if it is enabled in the configuration.
150150
// This is a temporary flag until the new design feature is fully implemented.
151-
const showNewActionsBar = getConfig().ENABLE_COURSE_OUTLINE_NEW_DESIGN?.toString().toLowerCase() === 'true';
151+
const showNewActionsBar = isOutlineNewDesignEnabled();
152152
// Use `setToastMessage` to show the toast.
153153
const [toastMessage, setToastMessage] = useState<string | null>(null);
154154

src/course-outline/OutlineAddChildButtons.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import messages from './messages';
99
interface NewChildButtonsProps {
1010
handleNewButtonClick: () => void;
1111
handleUseFromLibraryClick: () => void;
12+
onClickCard?: (e: React.MouseEvent) => void;
1213
childType: ContainerType;
1314
btnVariant?: string;
1415
btnClasses?: string;
@@ -18,6 +19,7 @@ interface NewChildButtonsProps {
1819
const OutlineAddChildButtons = ({
1920
handleNewButtonClick,
2021
handleUseFromLibraryClick,
22+
onClickCard,
2123
childType,
2224
btnVariant = 'outline-primary',
2325
btnClasses = 'mt-4 border-gray-500 rounded-0',
@@ -59,7 +61,7 @@ const OutlineAddChildButtons = ({
5961
}
6062

6163
return (
62-
<Stack direction="horizontal" gap={3}>
64+
<Stack direction="horizontal" gap={3} onClick={onClickCard}>
6365
<Button
6466
className={btnClasses}
6567
variant={btnVariant}

src/course-outline/card-header/CardHeader.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Icon,
1212
IconButton,
1313
IconButtonWithTooltip,
14+
Stack,
1415
useToggle,
1516
} from '@openedx/paragon';
1617
import {
@@ -48,6 +49,7 @@ interface CardHeaderProps {
4849
onClickMoveUp: () => void;
4950
onClickMoveDown: () => void;
5051
onClickCopy?: () => void;
52+
onClickCard?: (e: React.MouseEvent) => void;
5153
titleComponent: ReactNode;
5254
namePrefix: string;
5355
proctoringExamConfigurationLink?: string,
@@ -90,6 +92,7 @@ const CardHeader = ({
9092
onClickMoveUp,
9193
onClickMoveDown,
9294
onClickCopy,
95+
onClickCard,
9396
titleComponent,
9497
namePrefix,
9598
actions,
@@ -154,10 +157,16 @@ const CardHeader = ({
154157

155158
return (
156159
<>
157-
<div
160+
{
161+
/* This is a special case; we can skip accessibility here (tabbing and select with keyboard) since the
162+
`SortableItem` component handles that for the whole `{Container}Card`.
163+
This `onClick` allows the user to select the Card by clicking on white areas of this component. */
164+
}
165+
<div // eslint-disable-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
158166
className="item-card-header"
159167
data-testid={`${namePrefix}-card-header`}
160168
ref={cardHeaderRef}
169+
onClick={onClickCard}
161170
>
162171
{isFormOpen ? (
163172
<Form.Group className="m-0 w-75">
@@ -178,7 +187,7 @@ const CardHeader = ({
178187
/>
179188
</Form.Group>
180189
) : (
181-
<>
190+
<Stack direction="horizontal" gap={2}>
182191
{titleComponent}
183192
<IconButtonWithTooltip
184193
className="item-card-button-icon"
@@ -190,7 +199,7 @@ const CardHeader = ({
190199
// @ts-ignore
191200
disabled={isSaving}
192201
/>
193-
</>
202+
</Stack>
194203
)}
195204
<div className="ml-auto d-flex">
196205
{(isVertical || isSequential) && (

src/course-outline/card-header/TitleButton.tsx

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { useIntl } from '@edx/frontend-platform/i18n';
22
import {
3-
Button,
4-
OverlayTrigger,
5-
Tooltip,
3+
IconButtonWithTooltip,
4+
Stack,
65
} from '@openedx/paragon';
76
import {
87
ArrowDropDown as ArrowDownIcon,
@@ -29,32 +28,23 @@ const TitleButton = ({
2928
const titleTooltipMessage = intl.formatMessage(messages.expandTooltip);
3029

3130
return (
32-
<OverlayTrigger
33-
placement="bottom"
34-
overlay={(
35-
<Tooltip
36-
id={`${title}-${titleTooltipMessage}`}
37-
>
38-
{titleTooltipMessage}
39-
</Tooltip>
40-
)}
41-
>
42-
<Button
43-
iconBefore={isExpanded ? ArrowDownIcon : ArrowRightIcon}
44-
variant="tertiary"
31+
<Stack direction="horizontal">
32+
<IconButtonWithTooltip
33+
src={isExpanded ? ArrowDownIcon : ArrowRightIcon}
4534
data-testid={`${namePrefix}-card-header__expanded-btn`}
35+
alt={title}
36+
tooltipContent={<div>{titleTooltipMessage}</div>}
4637
className="item-card-header__title-btn"
4738
onClick={onTitleClick}
48-
title={title}
49-
>
50-
<div className="mr-2">
51-
{prefixIcon}
52-
</div>
53-
<span className={`${namePrefix}-card-title mb-0 truncate-1-line`}>
54-
{title}
55-
</span>
56-
</Button>
57-
</OverlayTrigger>
39+
size="inline"
40+
/>
41+
<div className="mr-2">
42+
{prefixIcon}
43+
</div>
44+
<span className={`${namePrefix}-card-title mb-0 truncate-1-line`}>
45+
{title}
46+
</span>
47+
</Stack>
5848
);
5949
};
6050

src/course-outline/card-header/TitleLink.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const TitleLink = ({
2626
to={titleLink}
2727
title={title}
2828
>
29-
<span className={`${namePrefix}-card-title mb-0 truncate-1-line text-left`}>
29+
<span className={`${namePrefix}-card-title truncate-1-line mb-0 text-left`}>
3030
{title}
3131
</span>
3232
</Button>

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/outline-sidebar/OutlineSidebar.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { getConfig } from '@edx/frontend-platform';
21
import { breakpoints } from '@openedx/paragon';
32
import { useMediaQuery } from 'react-responsive';
43

54
import { Sidebar } from '@src/generic/sidebar';
65

76
import OutlineHelpSidebar from './OutlineHelpSidebar';
87
import { useOutlineSidebarContext } from './OutlineSidebarContext';
8+
import { isOutlineNewDesignEnabled } from '../utils';
99

1010
const OutlineSideBar = () => {
1111
const isMedium = useMediaQuery({ maxWidth: breakpoints.medium.maxWidth });
12-
const showNewSidebar = getConfig().ENABLE_COURSE_OUTLINE_NEW_DESIGN?.toString().toLowerCase() === 'true';
1312

1413
const {
1514
currentPageKey,
@@ -20,7 +19,7 @@ const OutlineSideBar = () => {
2019
} = useOutlineSidebarContext();
2120

2221
// Returns the previous help sidebar component if the waffle flag is disabled
23-
if (!showNewSidebar) {
22+
if (!isOutlineNewDesignEnabled()) {
2423
// On screens smaller than medium, the help sidebar is shown below the course outline
2524
const colSpan = isMedium ? 'col-12' : 'col-3';
2625
return (

src/course-outline/outline-sidebar/OutlineSidebarContext.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { OutlineInfoSidebar } from './OutlineInfoSidebar';
1515

1616
import messages from './messages';
1717
import { AddSidebar } from './AddSidebar';
18+
import { isOutlineNewDesignEnabled } from '../utils';
1819

1920
export type OutlineSidebarPageKeys = 'help' | 'info' | 'add';
2021
export type OutlineSidebarPages = Record<OutlineSidebarPageKeys, SidebarPage>;
@@ -26,6 +27,8 @@ interface OutlineSidebarContextData {
2627
open: () => void;
2728
toggle: () => void;
2829
sidebarPages: OutlineSidebarPages;
30+
selectedContainerId?: string;
31+
openContainerInfoSidebar: (containerId: string) => void;
2932
}
3033

3134
const OutlineSidebarContext = createContext<OutlineSidebarContextData | undefined>(undefined);
@@ -36,6 +39,14 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
3639
const [currentPageKey, setCurrentPageKeyState] = useState<OutlineSidebarPageKeys>('info');
3740
const [isOpen, open, , toggle] = useToggle(true);
3841

42+
const [selectedContainerId, setSelectedContainerId] = useState<string | undefined>();
43+
44+
const openContainerInfoSidebar = useCallback((containerId: string) => {
45+
if (isOutlineNewDesignEnabled()) {
46+
setSelectedContainerId(containerId);
47+
}
48+
}, [setSelectedContainerId]);
49+
3950
const setCurrentPageKey = useCallback((pageKey: OutlineSidebarPageKeys) => {
4051
setCurrentPageKeyState(pageKey);
4152
open();
@@ -68,6 +79,8 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
6879
isOpen,
6980
open,
7081
toggle,
82+
selectedContainerId,
83+
openContainerInfoSidebar,
7184
}),
7285
[
7386
currentPageKey,
@@ -76,6 +89,8 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
7689
isOpen,
7790
open,
7891
toggle,
92+
selectedContainerId,
93+
openContainerInfoSidebar,
7994
],
8095
);
8196

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { getConfig, setConfig } from '@edx/frontend-platform';
12
import {
23
act, fireEvent, initializeMocks, render, screen, waitFor, within,
34
} from '@src/testUtils';
45
import { XBlock } from '@src/data/types';
56
import SectionCard from './SectionCard';
7+
import { OutlineSidebarProvider } from '../outline-sidebar/OutlineSidebarContext';
68

79
const mockUseAcceptLibraryBlockChanges = jest.fn();
810
const mockUseIgnoreLibraryBlockChanges = jest.fn();
@@ -116,6 +118,7 @@ const renderComponent = (props?: object, entry = '/course/:courseId') => render(
116118
routerProps: {
117119
initialEntries: [entry],
118120
},
121+
extraWrapper: OutlineSidebarProvider,
119122
},
120123
);
121124

@@ -129,6 +132,32 @@ describe('<SectionCard />', () => {
129132

130133
expect(screen.getByTestId('section-card-header')).toBeInTheDocument();
131134
expect(screen.getByTestId('section-card__content')).toBeInTheDocument();
135+
136+
// The card is not selected
137+
const card = screen.getByTestId('section-card');
138+
expect(card).not.toHaveClass('outline-card-selected');
139+
});
140+
141+
it('render SectionCard component in selected state', () => {
142+
setConfig({
143+
...getConfig(),
144+
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
145+
});
146+
const { container } = renderComponent();
147+
148+
expect(screen.getByTestId('section-card-header')).toBeInTheDocument();
149+
150+
// The card is not selected
151+
const card = screen.getByTestId('section-card');
152+
expect(card).not.toHaveClass('outline-card-selected');
153+
154+
// Get the <Row> that contains the card and click it to select the card
155+
const el = container.querySelector('div.row.mx-0') as HTMLInputElement;
156+
expect(el).not.toBeNull();
157+
fireEvent.click(el!);
158+
159+
// The card is selected
160+
expect(card).toHaveClass('outline-card-selected');
132161
});
133162

134163
it('expands/collapses the card when the expand button is clicked', () => {

0 commit comments

Comments
 (0)