Skip to content

Commit b931504

Browse files
authored
feat: Add SequenceBottomNavigationSlot for customizable sequence navigation (#1918)
Introduces a new plugin slot (`SequenceBottomNavigationSlot`) for extending or replacing the default bottom navigation in course sequences. Updates `Sequence` component to integrate with this slot. Includes documentation and examples for plugin usage.
1 parent 6f1dfbb commit b931504

6 files changed

Lines changed: 138 additions & 22 deletions

File tree

src/courseware/course/sequence/Sequence.jsx

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-use-before-define */
2+
import SequenceBottomNavigationSlot from '@src/plugin-slots/SequenceBottomNavigationSlot';
23
import { useEffect, useState } from 'react';
34
import PropTypes from 'prop-types';
45

@@ -91,27 +92,27 @@ const Sequence = ({
9192
};
9293

9394
/* istanbul ignore next */
94-
const nextHandler = () => {
95-
logEvent('edx.ui.lms.sequence.next_selected', 'top');
95+
const nextHandler = (placement) => () => {
96+
logEvent('edx.ui.lms.sequence.next_selected', placement);
9697
handleNext();
9798
};
9899

99100
/* istanbul ignore next */
100-
const previousHandler = () => {
101-
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
101+
const previousHandler = (placement) => () => {
102+
logEvent('edx.ui.lms.sequence.previous_selected', placement);
102103
handlePrevious();
103104
};
104105

105106
/* istanbul ignore next */
106-
const onNavigate = (destinationUnitId) => {
107-
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
107+
const onNavigate = (placement) => (destinationUnitId) => {
108+
logEvent('edx.ui.lms.sequence.tab_selected', placement, destinationUnitId);
108109
handleNavigate(destinationUnitId);
109110
};
110111

111112
const sequenceNavProps = {
112-
nextHandler,
113-
previousHandler,
114-
onNavigate,
113+
nextHandler: nextHandler('top'),
114+
previousHandler: previousHandler('top'),
115+
onNavigate: onNavigate('top'),
115116
};
116117

117118
useSequenceBannerTextAlert(sequenceId);
@@ -166,20 +167,18 @@ const Sequence = ({
166167

167168
const gated = sequence && sequence.gatedContent !== undefined && sequence.gatedContent.gated;
168169

170+
const unitNavigationProps = {
171+
courseId,
172+
sequenceId,
173+
unitId,
174+
onClickPrevious: previousHandler('bottom'),
175+
onClickNext: nextHandler('bottom'),
176+
};
177+
169178
const renderUnitNavigation = (isAtTop) => (
170179
<UnitNavigation
171-
courseId={courseId}
172-
sequenceId={sequenceId}
173-
unitId={unitId}
174180
isAtTop={isAtTop}
175-
onClickPrevious={() => {
176-
logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
177-
handlePrevious();
178-
}}
179-
onClickNext={() => {
180-
logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
181-
handleNext();
182-
}}
181+
{...unitNavigationProps}
183182
/>
184183
);
185184

@@ -224,7 +223,12 @@ const Sequence = ({
224223
isOriginalUserStaff={originalUserIsStaff}
225224
renderUnitNavigation={renderUnitNavigation}
226225
/>
227-
{unitHasLoaded && renderUnitNavigation(false)}
226+
{unitHasLoaded && (
227+
<SequenceBottomNavigationSlot
228+
{...unitNavigationProps}
229+
onNavigate={onNavigate('bottom')}
230+
/>
231+
)}
228232
</div>
229233
</div>
230234
<RightSidebarSlot courseId={courseId} />

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import classNames from 'classnames';
22
import PropTypes from 'prop-types';
33
import { useIntl } from '@edx/frontend-platform/i18n';
44

5+
import { NextUnitTopNavTriggerSlot } from '@src/plugin-slots/NextUnitTopNavTriggerSlot';
56
import { GetCourseExitNavigation } from '../../course-exit';
67

78
import { useSequenceNavigationMetadata } from './hooks';
89
import messages from './messages';
910
import PreviousButton from './generic/PreviousButton';
1011
import NextButton from './generic/NextButton';
11-
import { NextUnitTopNavTriggerSlot } from '../../../../plugin-slots/NextUnitTopNavTriggerSlot';
1212

1313
const UnitNavigation = ({
1414
sequenceId,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Sequence Bottom Navigation Slot
2+
3+
### Slot ID: `org.openedx.frontend.learning.sequence_bottom_navigation.v1`
4+
5+
### Props:
6+
* `courseId` (string) — Current course identifier
7+
* `sequenceId` (string) — Current sequence identifier
8+
* `unitId` (string) — Current unit identifier
9+
* `onClickNext` (function) — Handler for next navigation action
10+
* `onNavigate` (function) — Handler for direct unit navigation
11+
* `onClickPrevious` (function) — Handler for previous navigation action
12+
13+
## Description
14+
15+
This slot is used to replace/modify/hide the sequence navigation component that controls navigation between units within a course sequence at the bottom of the page.
16+
17+
## Example
18+
19+
### Default Navigation
20+
![Default Navigation](./default-navigation.png)
21+
22+
### Replaced with top navigation arrows
23+
![Top navigation at bottom](./arrow-navigation.png)
24+
25+
```jsx
26+
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
27+
28+
const config = {
29+
pluginSlots: {
30+
'org.openedx.frontend.learning.sequence_bottom_navigation.v1': {
31+
keepDefault: false,
32+
plugins: [
33+
{
34+
op: PLUGIN_OPERATIONS.Insert,
35+
widget: {
36+
id: 'custom_sequence_navigation',
37+
type: DIRECT_PLUGIN,
38+
RenderWidget: ({
39+
courseId,
40+
sequenceId,
41+
unitId,
42+
onClickNext,
43+
onNavigate,
44+
onClickPrevious,
45+
}) => (
46+
<div className="d-flex justify-content-center align-items-center">
47+
<UnitNavigation
48+
courseId={courseId}
49+
sequenceId={sequenceId}
50+
unitId={unitId}
51+
isAtTop={true}
52+
onClickPrevious={onClickPrevious}
53+
onClickNext={onClickNext}
54+
/>
55+
</div>
56+
),
57+
},
58+
},
59+
],
60+
},
61+
},
62+
};
63+
64+
export default config;
65+
```
18.3 KB
Loading
21.8 KB
Loading
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { UnitNavigation } from '@src/courseware/course/sequence/sequence-navigation';
2+
import React from 'react';
3+
import { PluginSlot } from '@openedx/frontend-plugin-framework';
4+
5+
export interface SequenceBottomNavigationSlotProps {
6+
courseId: string;
7+
sequenceId: string;
8+
unitId: string;
9+
onClickNext: () => void;
10+
onNavigate: (unitId: string) => void;
11+
onClickPrevious: () => void;
12+
}
13+
14+
const SequenceBottomNavigationSlot = ({
15+
courseId,
16+
sequenceId,
17+
unitId,
18+
onClickNext,
19+
onNavigate,
20+
onClickPrevious,
21+
}: SequenceBottomNavigationSlotProps) => (
22+
<PluginSlot
23+
id="org.openedx.frontend.learning.sequence_bottom_navigation.v1"
24+
slotOptions={{
25+
mergeProps: true,
26+
}}
27+
pluginProps={{
28+
courseId,
29+
sequenceId,
30+
unitId,
31+
onClickNext,
32+
onNavigate,
33+
onClickPrevious,
34+
}}
35+
>
36+
<UnitNavigation
37+
courseId={courseId}
38+
sequenceId={sequenceId}
39+
unitId={unitId}
40+
isAtTop={false}
41+
onClickPrevious={onClickPrevious}
42+
onClickNext={onClickNext}
43+
/>
44+
</PluginSlot>
45+
);
46+
47+
export default SequenceBottomNavigationSlot;

0 commit comments

Comments
 (0)